diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..458293c --- /dev/null +++ b/.clang-format @@ -0,0 +1,56 @@ +--- +Language: Cpp +BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignOperands: false +AlignTrailingComments: true +AllowShortBlocksOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: false +AlwaysBreakTemplateDeclarations: Yes +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: false + BeforeCatch: true + BeforeElse: false + BeforeLambdaBody: true + BeforeWhile: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBraces: Custom +ColumnLimit: 120 +IncludeCategories: + - Regex: '^<.*' + Priority: 1 + - Regex: '^".*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseBlocks: true +IndentGotoLabels: false +IndentPPDirectives: BeforeHash +IndentWidth: 4 +InsertNewlineAtEOF: true +MacroBlockBegin: '' +MacroBlockEnd: '' +PointerAlignment: Left +SpaceInEmptyParentheses: false +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +TabWidth: 4 +... diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..7e854f1 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,145 @@ +# Generated from CLion Inspection settings +--- +Checks: '-*, +bugprone-argument-comment, +bugprone-assert-side-effect, +bugprone-bad-signal-to-kill-thread, +bugprone-branch-clone, +bugprone-copy-constructor-init, +bugprone-dangling-handle, +bugprone-dynamic-static-initializers, +bugprone-fold-init-type, +bugprone-forward-declaration-namespace, +bugprone-forwarding-reference-overload, +bugprone-inaccurate-erase, +bugprone-incorrect-roundings, +bugprone-integer-division, +bugprone-lambda-function-name, +bugprone-macro-parentheses, +bugprone-macro-repeated-side-effects, +bugprone-misplaced-operator-in-strlen-in-alloc, +bugprone-misplaced-pointer-arithmetic-in-alloc, +bugprone-misplaced-widening-cast, +bugprone-move-forwarding-reference, +bugprone-multiple-statement-macro, +bugprone-no-escape, +bugprone-parent-virtual-call, +bugprone-posix-return, +bugprone-reserved-identifier, +bugprone-sizeof-container, +bugprone-sizeof-expression, +bugprone-spuriously-wake-up-functions, +bugprone-string-constructor, +bugprone-string-integer-assignment, +bugprone-string-literal-with-embedded-nul, +bugprone-suspicious-enum-usage, +bugprone-suspicious-include, +bugprone-suspicious-memset-usage, +bugprone-suspicious-missing-comma, +bugprone-suspicious-semicolon, +bugprone-suspicious-string-compare, +bugprone-suspicious-memory-comparison, +bugprone-suspicious-realloc-usage, +bugprone-swapped-arguments, +bugprone-terminating-continue, +bugprone-throw-keyword-missing, +bugprone-too-small-loop-variable, +bugprone-undefined-memory-manipulation, +bugprone-undelegated-constructor, +bugprone-unhandled-self-assignment, +bugprone-unused-raii, +bugprone-unused-return-value, +bugprone-use-after-move, +bugprone-virtual-near-miss, +cert-dcl21-cpp, +cert-dcl58-cpp, +cert-err34-c, +cert-err52-cpp, +cert-err60-cpp, +cert-flp30-c, +cert-msc50-cpp, +cert-msc51-cpp, +cert-str34-c, +cppcoreguidelines-interfaces-global-init, +cppcoreguidelines-narrowing-conversions, +cppcoreguidelines-pro-type-member-init, +cppcoreguidelines-pro-type-static-cast-downcast, +cppcoreguidelines-slicing, +google-default-arguments, +google-runtime-operator, +hicpp-exception-baseclass, +hicpp-multiway-paths-covered, +misc-misplaced-const, +misc-new-delete-overloads, +misc-non-copyable-objects, +misc-throw-by-value-catch-by-reference, +misc-unconventional-assign-operator, +misc-uniqueptr-reset-release, +modernize-avoid-bind, +modernize-concat-nested-namespaces, +modernize-deprecated-headers, +modernize-deprecated-ios-base-aliases, +modernize-loop-convert, +modernize-make-shared, +modernize-make-unique, +modernize-pass-by-value, +modernize-raw-string-literal, +modernize-redundant-void-arg, +modernize-replace-auto-ptr, +modernize-replace-disallow-copy-and-assign-macro, +modernize-replace-random-shuffle, +modernize-return-braced-init-list, +modernize-shrink-to-fit, +modernize-unary-static-assert, +modernize-use-auto, +modernize-use-bool-literals, +modernize-use-emplace, +modernize-use-equals-default, +modernize-use-equals-delete, +modernize-use-nodiscard, +modernize-use-noexcept, +modernize-use-nullptr, +modernize-use-override, +modernize-use-transparent-functors, +modernize-use-uncaught-exceptions, +mpi-buffer-deref, +mpi-type-mismatch, +openmp-use-default-none, +performance-faster-string-find, +performance-for-range-copy, +performance-implicit-conversion-in-loop, +performance-inefficient-algorithm, +performance-inefficient-string-concatenation, +performance-inefficient-vector-operation, +performance-move-const-arg, +performance-move-constructor-init, +performance-no-automatic-move, +performance-noexcept-move-constructor, +performance-trivially-destructible, +performance-type-promotion-in-math-fn, +performance-unnecessary-copy-initialization, +performance-unnecessary-value-param, +portability-simd-intrinsics, +readability-avoid-const-params-in-decls, +readability-const-return-type, +readability-container-size-empty, +readability-convert-member-functions-to-static, +readability-delete-null-pointer, +readability-deleted-default, +readability-inconsistent-declaration-parameter-name, +readability-make-member-function-const, +readability-misleading-indentation, +readability-misplaced-array-index, +readability-non-const-parameter, +readability-redundant-control-flow, +readability-redundant-declaration, +readability-redundant-function-ptr-dereference, +readability-redundant-smartptr-get, +readability-redundant-string-cstr, +readability-redundant-string-init, +readability-simplify-subscript-expr, +readability-static-accessed-through-instance, +readability-static-definition-in-anonymous-namespace, +readability-string-compare, +readability-uniqueptr-delete-release, +readability-use-anyofallof' \ No newline at end of file diff --git a/.gitignore b/.gitignore index dbb1bac..e8bcaa8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ cmake-build-release /result /.gdb_history /valgrind.log +.idea diff --git a/CMakeLists.txt b/CMakeLists.txt index 01658b1..b50b26d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,10 +19,10 @@ endif() if(NOT DEFINED DISABLE_TRACY) include(FetchContent) FetchContent_Declare(tracy - GIT_REPOSITORY https://github.com/wolfpld/tracy.git - GIT_TAG v0.11.1 - GIT_SHALLOW TRUE - GIT_PROGRESS TRUE + GIT_REPOSITORY https://github.com/wolfpld/tracy.git + GIT_TAG v0.11.1 + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE ) FetchContent_MakeAvailable(tracy) option(TRACY_ENABLE "" ON) @@ -32,7 +32,7 @@ if(NOT DEFINED DISABLE_TRACY) list(APPEND FLAGS TRACY) endif() -if (WIN32) +if(WIN32) list(APPEND LIBS opengl32 gdi32 winmm) endif() @@ -57,12 +57,11 @@ set(SOURCES src/octree.cpp src/physics.cpp src/puzzle.cpp - src/state.cpp - src/input.cpp - src/tracy.cpp - src/backward.cpp src/distance.cpp - src/gui.cpp + src/state_manager.cpp + src/input.cpp + src/user_interface.cpp + src/backward.cpp ) # Main target @@ -72,7 +71,7 @@ target_link_libraries(masssprings PRIVATE ${LIBS}) target_compile_definitions(masssprings PRIVATE ${FLAGS}) # LTO -if (NOT WIN32) +if(NOT WIN32) include(CheckIPOSupported) check_ipo_supported(RESULT supported OUTPUT error) if(supported) @@ -84,4 +83,4 @@ if (NOT WIN32) else() message(STATUS "IPO / LTO not supported: <${error}>") endif() -endif() +endif() \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3abc96e --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# MassSprings - Puzzle Board State Space Explorer + +All combinations of pieces reachable from an initial puzzle are explored, the resulting puzzle state-space is visualized as a force-directed graph. +The graph layout is calculated iteratively using a mass-spring-system with additional pairwise repulsive forces simulated using Barnes-Hut. + +![](screenshot.png) + +Build and run on NixOS: `nix run git+https://gitea.local.chriphost.de/christoph/cpp-masssprings`. \ No newline at end of file diff --git a/flake.nix b/flake.nix index 2c0698c..6d349ed 100644 --- a/flake.nix +++ b/flake.nix @@ -108,6 +108,7 @@ rec { valgrind # gnumake cmake + ninja # pkg-config # clang-tools # compdb @@ -253,7 +254,7 @@ rec { cd cmake-build-${typeLower} echo "Running cmake" - cmake -G "Unix Makefiles" \ + cmake -G "Ninja" \ -DCMAKE_BUILD_TYPE="${type}" \ .. @@ -297,6 +298,8 @@ rec { abbr -a rungdb "${buildDebug} && gdb --tui ./cmake-build-debug/masssprings" abbr -a runtracy "tracy -a 127.0.0.1 &; ${buildRelease} && sudo -E ./cmake-build-release/masssprings" abbr -a runvalgrind "${buildDebug} && valgrind --leak-check=full --show-reachable=no --show-leak-kinds=definite,indirect,possible --track-origins=no --suppressions=valgrind.supp --log-file=valgrind.log ./cmake-build-debug/masssprings && cat valgrind.log" + + abbr -a runclion "clion ./CMakeLists.txt 2>/dev/null 1>&2 & disown;" ''; in builtins.concatStringsSep "\n" [ diff --git a/include/camera.hpp b/include/camera.hpp index 8ce15b4..6f59f1c 100644 --- a/include/camera.hpp +++ b/include/camera.hpp @@ -1,31 +1,30 @@ -#ifndef __CAMERA_HPP_ -#define __CAMERA_HPP_ +#ifndef CAMERA_HPP_ +#define CAMERA_HPP_ #include "config.hpp" #include #include -class OrbitCamera3D { +class orbit_camera +{ public: - Vector3 position = Vector3Zero(); - Vector3 target = Vector3Zero(); - float distance = CAMERA_DISTANCE; - float fov = CAMERA_FOV; - CameraProjection projection = CAMERA_PERSPECTIVE; - float angle_x = 0.0; - float angle_y = 0.0; + Vector3 position = Vector3Zero(); + Vector3 target = Vector3Zero(); + float distance = CAMERA_DISTANCE; + float fov = CAMERA_FOV; + CameraProjection projection = CAMERA_PERSPECTIVE; + float angle_x = 0.0; + float angle_y = 0.0; - Camera camera = Camera{Vector3(0, 0, -distance), target, Vector3(0, 1.0, 0), - fov, projection}; + Camera camera = Camera{Vector3(0, 0, -distance), target, Vector3(0, 1.0, 0), fov, projection}; public: - auto Rotate(Vector2 last_mouse, Vector2 mouse) -> void; + auto rotate(Vector2 last_mouse, Vector2 mouse) -> void; - auto Pan(Vector2 last_mouse, Vector2 mouse) -> void; + auto pan(Vector2 last_mouse, Vector2 mouse) -> void; - auto Update(const Vector3 ¤t_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 diff --git a/include/config.hpp b/include/config.hpp index d5614b0..3df0cbb 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -1,5 +1,5 @@ -#ifndef __CONFIG_HPP_ -#define __CONFIG_HPP_ +#ifndef CONFIG_HPP_ +#define CONFIG_HPP_ #include @@ -12,14 +12,16 @@ // Window constexpr int INITIAL_WIDTH = 600; constexpr int INITIAL_HEIGHT = 600; -constexpr int MENU_HEIGHT = 350; +constexpr int MENU_HEIGHT = 300; +constexpr int POPUP_WIDTH = 450; +constexpr int POPUP_HEIGHT = 150; // Menu constexpr int MENU_PAD = 5; constexpr int BUTTON_PAD = 12; constexpr int MENU_ROWS = 7; constexpr int MENU_COLS = 3; -constexpr const char *FONT = "fonts/SpaceMono.ttf"; +constexpr const char* FONT = "fonts/SpaceMono.ttf"; constexpr int FONT_SIZE = 26; // Camera Controls diff --git a/include/distance.hpp b/include/distance.hpp index 38237eb..5040903 100644 --- a/include/distance.hpp +++ b/include/distance.hpp @@ -1,32 +1,24 @@ -#ifndef __DISTANCE_HPP_ -#define __DISTANCE_HPP_ - -#include "config.hpp" +#ifndef DISTANCE_HPP_ +#define DISTANCE_HPP_ #include #include -struct DistanceResult { - // distances[n] = distance from n to target - std::vector distances; +class graph_distances +{ +public: + std::vector distances; // distances[n] = distance from node n to target + std::vector parents; // parents[n] = next node on the path from node n to target + std::vector nearest_targets; // nearest_target[n] = closest target node to node n - // parents[n] = next node on the path from n to target - std::vector parents; +public: + auto clear() -> void; + [[nodiscard]] auto empty() const -> bool; - // nearest_target[n] = closest target node to n - std::vector nearest_targets; + auto calculate_distances(size_t node_count, const std::vector>& edges, + const std::vector& targets) -> void; - auto Clear() -> void; - - auto Empty() -> bool; + [[nodiscard]] auto get_shortest_path(size_t source) const -> std::vector; }; -auto CalculateDistances( - std::size_t node_count, - const std::vector> &edges, - const std::vector &targets) -> DistanceResult; - -auto GetPath(const DistanceResult &result, std::size_t source) - -> std::vector; - #endif diff --git a/include/gui.hpp b/include/gui.hpp deleted file mode 100644 index cad67cd..0000000 --- a/include/gui.hpp +++ /dev/null @@ -1,184 +0,0 @@ -#ifndef __GUI_HPP_ -#define __GUI_HPP_ - -#include "camera.hpp" -#include "config.hpp" -#include "input.hpp" -#include "state.hpp" - -#include -#include - -class Grid { -public: - int x; - int y; - int width; - int height; - int columns; - int rows; - const int padding; - -public: - Grid(int _x, int _y, int _width, int _height, int _columns, int _rows, - int _padding) - : x(_x), y(_y), width(_width), height(_height), columns(_columns), - rows(_rows), padding(_padding) {} - -public: - auto UpdateBounds(int _x, int _y, int _width, int _height, int _columns, - int _rows) -> void; - - auto UpdateBounds(int _x, int _y, int _width, int _height) -> void; - - auto UpdateBounds(int _x, int _y) -> void; - - auto Bounds() const -> Rectangle; - - auto Bounds(int _x, int _y, int _width, int _height) const -> Rectangle; - - auto SquareBounds() const -> Rectangle; - - auto SquareBounds(int _x, int _y, int _width, int _height) const -> Rectangle; -}; - -class Gui { - struct Style { - int border_color_normal; - int base_color_normal; - int text_color_normal; - - int border_color_focused; - int base_color_focused; - int text_color_focused; - - int border_color_pressed; - int base_color_pressed; - int text_color_pressed; - - int border_color_disabled; - int base_color_disabled; - int text_color_disabled; - }; - - struct DefaultStyle : Style { - int background_color; - int line_color; - - int text_size; - int text_spacing; - int text_line_spacing; - int text_alignment_vertical; - int text_wrap_mode; - }; - - struct ComponentStyle : Style { - int border_width; - int text_padding; - int text_alignment; - }; - -private: - InputHandler &input; - StateManager &state; - const OrbitCamera3D &camera; - - 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.current_state.width, state.current_state.height, BOARD_PADDING); - - Grid graph_overlay_grid = - Grid(GetScreenWidth() / 2, MENU_HEIGHT, 200, 100, 1, 4, MENU_PAD); - - bool save_window = false; - std::array preset_name = {0}; - bool help_window = false; - -public: - Gui(InputHandler &_input, StateManager &_state, const OrbitCamera3D &_camera) - : input(_input), state(_state), camera(_camera) { - Init(); - } - - Gui(const Gui ©) = delete; - Gui &operator=(const Gui ©) = delete; - Gui(Gui &&move) = delete; - Gui &operator=(Gui &&move) = delete; - -private: - auto Init() const -> void; - - auto ApplyColor(Style &style, Color color) const -> void; - - auto ApplyBlockColor(Style &style, Color color) const -> void; - - auto ApplyTextColor(Style &style, Color color) const -> void; - - auto GetDefaultStyle() const -> DefaultStyle; - - auto SetDefaultStyle(const DefaultStyle &style) const -> void; - - auto GetComponentStyle(int component) const -> ComponentStyle; - - auto SetComponentStyle(int component, const ComponentStyle &style) const - -> void; - - auto DrawButton(Rectangle bounds, const std::string &label, Color color, - bool enabled = true, int font_size = FONT_SIZE) const -> int; - - auto DrawMenuButton(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 DrawToggleSlider(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 DrawMenuToggleSlider(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; - - auto DrawSpinner(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 DrawMenuSpinner(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 DrawLabel(Rectangle bounds, const std::string &text, Color color, - bool enabled = true, int font_size = FONT_SIZE) const -> int; - - auto DrawBoardBlock(int x, int y, int width, int height, Color color, - bool enabled = true) const -> bool; - - auto WindowOpen() const -> bool; - - // Different menu sections - auto DrawMenuHeader(Color color) const -> void; - auto DrawGraphInfo(Color color) const -> void; - auto DrawGraphControls(Color color) const -> void; - auto DrawCameraControls(Color color) const -> void; - auto DrawPuzzleControls(Color color) const -> void; - auto DrawEditControls(Color color) const -> void; - auto DrawMenuFooter(Color color) -> void; - -public: - auto GetBackgroundColor() const -> Color; - auto HelpPopup() -> void; - auto DrawSavePresetPopup() -> void; - auto DrawMainMenu() -> void; - auto DrawPuzzleBoard() -> void; - auto DrawGraphOverlay(int fps, int ups) -> void; - auto Update() -> void; -}; - -#endif diff --git a/include/input.hpp b/include/input.hpp index 9774b86..f7a0317 100644 --- a/include/input.hpp +++ b/include/input.hpp @@ -1,158 +1,151 @@ -#ifndef __INPUT_HPP_ -#define __INPUT_HPP_ +#ifndef INPUT_HPP_ +#define INPUT_HPP_ #include "camera.hpp" -#include "config.hpp" -#include "state.hpp" +#include "state_manager.hpp" #include #include #include -class InputHandler { - struct GenericHandler { - std::function handler; - }; +class input_handler +{ + struct generic_handler + { + std::function handler; + }; - struct MouseHandler : GenericHandler { - MouseButton button; - }; + struct mouse_handler : generic_handler + { + MouseButton button; + }; - struct KeyboardHandler : GenericHandler { - KeyboardKey key; - }; + struct keyboard_handler : generic_handler + { + KeyboardKey key; + }; private: - std::vector generic_handlers; - std::vector mouse_pressed_handlers; - std::vector mouse_released_handlers; - std::vector key_pressed_handlers; - std::vector key_released_handlers; + std::vector generic_handlers; + std::vector mouse_pressed_handlers; + std::vector mouse_released_handlers; + std::vector key_pressed_handlers; + std::vector key_released_handlers; public: - StateManager &state; - OrbitCamera3D &camera; + state_manager& state; + orbit_camera& camera; - bool disable = false; + bool disable = false; - // Block selection - int hov_x = -1; - int hov_y = -1; - int sel_x = 0; - int sel_y = 0; + // Block selection + int hov_x = -1; + int hov_y = -1; + int sel_x = 0; + int sel_y = 0; - // Editing - bool editing = false; - bool has_block_add_xy = false; - int block_add_x = -1; - int block_add_y = -1; + // Editing + bool editing = false; + bool has_block_add_xy = false; + int block_add_x = -1; + int block_add_y = -1; - // Graph display - bool mark_path = false; - bool mark_solutions = false; - bool connect_solutions = false; + // Graph display + bool mark_path = false; + bool mark_solutions = false; + bool connect_solutions = false; - // Camera - bool camera_lock = true; - bool camera_mass_center_lock = false; - bool camera_panning = false; - bool camera_rotating = false; + // Camera + bool camera_lock = true; + bool camera_mass_center_lock = false; + bool camera_panning = false; + bool camera_rotating = false; - // Mouse dragging - Vector2 mouse = Vector2Zero(); - Vector2 last_mouse = Vector2Zero(); + // Mouse dragging + Vector2 mouse = Vector2Zero(); + Vector2 last_mouse = Vector2Zero(); public: - InputHandler(StateManager &_state, OrbitCamera3D &_camera) - : state(_state), camera(_camera) { - InitHandlers(); - } + input_handler(state_manager& _state, orbit_camera& _camera) : state(_state), camera(_camera) + { + init_handlers(); + } - InputHandler(const InputHandler ©) = delete; - InputHandler &operator=(const InputHandler ©) = delete; - InputHandler(InputHandler &&move) = delete; - InputHandler &operator=(InputHandler &&move) = delete; - - ~InputHandler() {} + input_handler(const input_handler& copy) = delete; + auto operator=(const input_handler& copy) -> input_handler& = delete; + input_handler(input_handler&& move) = delete; + auto operator=(input_handler&& move) -> input_handler& = delete; private: - auto InitHandlers() -> void; + auto init_handlers() -> void; public: - // Helpers - auto MouseInMenuPane() -> bool; - auto MouseInBoardPane() -> bool; - auto MouseInGraphPane() -> bool; + // Helpers + [[nodiscard]] auto mouse_in_menu_pane() const -> bool; + [[nodiscard]] auto mouse_in_board_pane() const -> bool; + [[nodiscard]] auto mouse_in_graph_pane() const -> bool; - // Mouse actions - auto MouseHover() -> void; - auto CameraStartPan() -> void; - auto CameraPan() -> void; - auto CameraStopPan() -> void; - auto CameraStartRotate() -> void; - auto CameraRotate() -> void; - auto CameraStopRotate() -> void; - auto CameraZoom() -> void; - auto CameraFov() -> void; - auto SelectBlock() -> void; - auto StartAddBlock() -> void; - auto ClearAddBlock() -> void; - auto AddBlock() -> void; - auto RemoveBlock() -> void; - auto PlaceGoal() -> void; + // Mouse actions + auto mouse_hover() -> void; + auto camera_start_pan() -> void; + auto camera_pan() const -> void; + auto camera_stop_pan() -> void; + auto camera_start_rotate() -> void; + auto camera_rotate() const -> void; + auto camera_stop_rotate() -> void; + auto camera_zoom() const -> void; + auto camera_fov() const -> void; + auto select_block() -> void; + auto start_add_block() -> void; + auto clear_add_block() -> void; + auto add_block() -> void; + auto remove_block() -> void; + auto place_goal() const -> void; - // Key actions - auto ToggleCameraLock() -> void; - auto ToggleCameraMassCenterLock() -> void; - auto ToggleCameraProjection() -> void; - auto MoveBlockNor() -> void; - auto MoveBlockWes() -> void; - auto MoveBlockSou() -> void; - auto MoveBlockEas() -> void; - auto PrintState() const -> void; - auto PreviousPreset() -> void; - auto NextPreset() -> void; - auto ResetState() -> void; - auto FillGraph() -> void; - auto ClearGraph() -> void; - auto ToggleMarkSolutions() -> void; - auto ToggleConnectSolutions() -> void; - auto ToggleMarkPath() -> void; - auto MakeOptimalMove() -> void; - auto GoToWorstState() -> void; - auto GoToNearestTarget() -> void; - auto UndoLastMove() -> void; - auto ToggleRestrictedMovement() -> void; - auto ToggleTargetBlock() -> void; - auto ToggleWallBlock() -> void; - auto RemoveBoardColumn() -> void; - auto AddBoardColumn() -> void; - auto RemoveBoardRow() -> void; - auto AddBoardRow() -> void; - auto ToggleEditing() -> void; - auto ClearGoal() -> void; + // Key actions + auto toggle_camera_lock() -> void; + auto toggle_camera_mass_center_lock() -> void; + auto toggle_camera_projection() const -> void; + auto move_block_nor() -> void; + auto move_block_wes() -> void; + auto move_block_sou() -> void; + auto move_block_eas() -> void; + auto print_state() const -> void; + auto load_previous_preset() -> void; + auto load_next_preset() -> void; + auto goto_starting_state() -> void; + auto populate_graph() const -> void; + auto clear_graph() const -> void; + auto toggle_mark_solutions() -> void; + auto toggle_connect_solutions() -> void; + auto toggle_mark_path() -> void; + auto goto_optimal_next_state() const -> void; + auto goto_most_distant_state() const -> void; + auto goto_closest_target_state() const -> void; + auto goto_previous_state() const -> void; + auto toggle_restricted_movement() const -> void; + auto toggle_target_block() const -> void; + auto toggle_wall_block() const -> void; + auto remove_board_column() const -> void; + auto add_board_column() const -> void; + auto remove_board_row() const -> void; + auto add_board_row() const -> void; + auto toggle_editing() -> void; + auto clear_goal() const -> void; - // General - auto RegisterGenericHandler(std::function handler) - -> void; + // General + auto register_generic_handler(const std::function& handler) -> void; - auto RegisterMousePressedHandler(MouseButton button, - std::function handler) - -> void; + auto register_mouse_pressed_handler(MouseButton button, const std::function& handler) -> void; - auto RegisterMouseReleasedHandler(MouseButton button, - std::function handler) - -> void; + auto register_mouse_released_handler(MouseButton button, const std::function& handler) + -> void; - auto RegisterKeyPressedHandler(KeyboardKey key, - std::function handler) - -> void; + auto register_key_pressed_handler(KeyboardKey key, const std::function& handler) -> void; - auto RegisterKeyReleasedHandler(KeyboardKey key, - std::function handler) - -> void; + auto register_key_released_handler(KeyboardKey key, const std::function& handler) -> void; - auto HandleInput() -> void; + auto handle_input() -> void; }; #endif diff --git a/include/octree.hpp b/include/octree.hpp index 4cec38b..1ffce4d 100644 --- a/include/octree.hpp +++ b/include/octree.hpp @@ -1,54 +1,51 @@ -#ifndef __OCTREE_HPP_ -#define __OCTREE_HPP_ - -#include "config.hpp" +#ifndef OCTREE_HPP_ +#define OCTREE_HPP_ +#include #include #include #include -class OctreeNode { -public: - Vector3 mass_center; - float mass_total; - Vector3 box_min; // area start - Vector3 box_max; // area end - int children[8]; - int mass_id; - bool leaf; +class octree +{ + class node + { + public: + Vector3 mass_center = Vector3Zero(); + float mass_total = 0.0; + Vector3 box_min = Vector3Zero(); // area start + Vector3 box_max = Vector3Zero(); // area end + std::array children = {-1, -1, -1, -1, -1, -1, -1, -1}; + int mass_id = -1; + bool leaf = true; + + public: + [[nodiscard]] auto child_count() const -> int; + }; public: - OctreeNode() - : mass_center(Vector3Zero()), mass_total(0.0), - children(-1, -1, -1, -1, -1, -1, -1, -1), mass_id(-1), leaf(true) {} + static constexpr int MAX_DEPTH = 20; + + std::vector nodes; public: - auto ChildCount() const -> int; -}; + octree() = default; -class Octree { -public: - std::vector nodes; + octree(const octree& copy) = delete; + auto operator=(const octree& copy) -> octree& = delete; + octree(octree&& move) = delete; + auto operator=(octree&& move) -> octree& = delete; public: - Octree() {} + auto create_empty_leaf(const Vector3& box_min, const Vector3& box_max) -> int; - Octree(const Octree ©) = delete; - Octree &operator=(const Octree ©) = delete; - Octree(Octree &&move) = delete; - Octree &operator=(Octree &&move) = delete; + [[nodiscard]] auto get_octant(int node_idx, const Vector3& pos) const -> int; -public: - auto CreateNode(const Vector3 &box_min, const Vector3 &box_max) -> int; + [[nodiscard]] auto get_child_bounds(int node_idx, int octant) const -> std::pair; - auto GetOctant(int node_idx, const Vector3 &pos) -> int; + auto insert(int node_idx, int mass_id, const Vector3& pos, float mass, int depth) -> void; - auto GetChildBounds(int node_idx, int octant) -> std::pair; - - auto Insert(int node_idx, int mass_id, const Vector3 &pos, float mass) - -> void; - - auto CalculateForce(int node_idx, const Vector3 &pos) const -> Vector3; + [[nodiscard]] auto calculate_force(int node_idx, const Vector3& pos) const -> Vector3; }; #endif diff --git a/include/physics.hpp b/include/physics.hpp index 45466be..9faa1be 100644 --- a/include/physics.hpp +++ b/include/physics.hpp @@ -1,12 +1,11 @@ -#ifndef __PHYSICS_HPP_ -#define __PHYSICS_HPP_ +#ifndef PHYSICS_HPP_ +#define PHYSICS_HPP_ #include "config.hpp" #include "octree.hpp" #include #include -#include #include #include #include @@ -15,190 +14,198 @@ #include #include +#include "util.hpp" + #ifdef THREADPOOL -#if defined(_WIN32) -#define NOGDI // All GDI defines and routines -#define NOUSER // All USER defines and routines -#endif -#define BS_THREAD_POOL_NATIVE_EXTENSIONS -#include -#if defined(_WIN32) // raylib uses these names as function parameters -#undef near -#undef far -#endif + #if defined(_WIN32) + #define NOGDI // All GDI defines and routines + #define NOUSER // All USER defines and routines + #endif + #define BS_THREAD_POOL_NATIVE_EXTENSIONS + #include + #if defined(_WIN32) // raylib uses these names as function parameters + #undef near + #undef far + #endif #endif #ifdef TRACY -#include + #include #endif -class Mass { +class mass +{ public: - Vector3 position; - Vector3 previous_position; // for verlet integration - Vector3 velocity; - Vector3 force; + Vector3 position = Vector3Zero(); + Vector3 previous_position = Vector3Zero(); // for verlet integration + Vector3 velocity = Vector3Zero(); + Vector3 force = Vector3Zero(); public: - Mass(Vector3 _position) - : position(_position), previous_position(_position), - velocity(Vector3Zero()), force(Vector3Zero()) {} + mass() = delete; + + explicit mass(const Vector3 _position) : position(_position), previous_position(_position) + {} public: - auto ClearForce() -> void; - - auto CalculateVelocity(const float delta_time) -> void; - - auto CalculatePosition(const float delta_time) -> void; - - auto VerletUpdate(const float delta_time) -> void; + auto clear_force() -> void; + auto calculate_velocity(float delta_time) -> void; + auto calculate_position(float delta_time) -> void; + auto verlet_update(float delta_time) -> void; }; -class Spring { +class spring +{ public: - std::size_t a; - std::size_t b; + size_t a; + size_t b; public: - Spring(std::size_t _a, std::size_t _b) : a(_a), b(_b) {} + spring(const size_t _a, const size_t _b) : a(_a), b(_b) + {} public: - auto CalculateSpringForce(Mass &_a, Mass &_b) const -> void; + static auto calculate_spring_force(mass& _a, mass& _b) -> void; }; -class MassSpringSystem { +class mass_spring_system +{ private: #ifdef THREADPOOL - BS::thread_pool threads; + BS::thread_pool<> threads; #endif public: - Octree octree; + octree tree; - // This is the main ownership of all the states/masses/springs. - std::vector masses; - std::vector springs; + // This is the main ownership of all the states/masses/springs. + std::vector masses; + std::vector springs; public: - MassSpringSystem() + mass_spring_system() #ifdef THREADPOOL - : threads(std::thread::hardware_concurrency() - 1, SetThreadName) + : threads(std::thread::hardware_concurrency() - 1, set_thread_name) #endif - { - std::cout << std::format( - "Using Barnes-Hut + Octree repulsion force calculation.") - << std::endl; + { + infoln("Using Barnes-Hut + Octree repulsion force calculation."); #ifdef THREADPOOL - std::cout << std::format("Thread-pool: {} threads.", - threads.get_thread_count()) - << std::endl; + infoln("Thread-pool: {} threads.", threads.get_thread_count()); #else - std::cout << std::format("Thread-pool: Disabled.") << std::endl; + infoln("Thread-pool: Disabled."); #endif - }; + } - MassSpringSystem(const MassSpringSystem ©) = delete; - MassSpringSystem &operator=(const MassSpringSystem ©) = delete; - MassSpringSystem(MassSpringSystem &move) = delete; - MassSpringSystem &operator=(MassSpringSystem &&move) = delete; + mass_spring_system(const mass_spring_system& copy) = delete; + auto operator=(const mass_spring_system& copy) -> mass_spring_system& = delete; + mass_spring_system(mass_spring_system& move) = delete; + auto operator=(mass_spring_system&& move) -> mass_spring_system& = delete; private: #ifdef THREADPOOL - static auto SetThreadName(std::size_t idx) -> void; + static auto set_thread_name(size_t idx) -> void; #endif - auto BuildOctree() -> void; + auto build_octree() -> void; public: - auto AddMass() -> void; + auto clear() -> void; + auto add_mass() -> void; + auto add_spring(size_t a, size_t b) -> void; - auto AddSpring(int a, int b) -> void; + auto clear_forces() -> void; + auto calculate_spring_forces() -> void; + auto calculate_repulsion_forces() -> void; + auto verlet_update(float delta_time) -> void; - auto Clear() -> void; - - auto ClearForces() -> void; - - auto CalculateSpringForces() -> void; - - auto CalculateRepulsionForces() -> void; - - auto VerletUpdate(float delta_time) -> void; + auto center_masses() -> void; }; -class ThreadedPhysics { - struct AddMass {}; - struct AddSpring { - std::size_t a; - std::size_t b; - }; - struct ClearGraph {}; +class threaded_physics +{ + struct add_mass + {}; - using Command = std::variant; + struct add_spring + { + size_t a; + size_t b; + }; - struct PhysicsState { + struct clear_graph + {}; + + using command = std::variant; + + struct physics_state + { #ifdef TRACY - TracyLockable(std::mutex, command_mtx); + TracyLockable(std::mutex, command_mtx); #else - std::mutex command_mtx; + std::mutex command_mtx; #endif - std::queue pending_commands; + std::queue pending_commands; #ifdef TRACY - TracyLockable(std::mutex, data_mtx); + TracyLockable(std::mutex, data_mtx); #else - std::mutex data_mtx; + std::mutex data_mtx; #endif - std::condition_variable_any data_ready_cnd; - std::condition_variable_any data_consumed_cnd; - Vector3 mass_center = Vector3Zero(); - unsigned int ups = 0; - std::vector masses; // Read by renderer - bool data_ready = false; - bool data_consumed = true; + std::condition_variable_any data_ready_cnd; + std::condition_variable_any data_consumed_cnd; + Vector3 mass_center = Vector3Zero(); + int ups = 0; + size_t mass_count = 0; // For debug + size_t spring_count = 0; // For debug + std::vector masses; // Read by renderer + bool data_ready = false; + bool data_consumed = true; - std::atomic running{true}; - }; + std::atomic running{true}; + }; private: - std::thread physics; + std::thread physics; public: - PhysicsState state; + physics_state state; public: - ThreadedPhysics() : physics(PhysicsThread, std::ref(state)) {} + threaded_physics() : physics(physics_thread, std::ref(state)) + {} - ThreadedPhysics(const ThreadedPhysics ©) = delete; - ThreadedPhysics &operator=(const ThreadedPhysics ©) = delete; - ThreadedPhysics(ThreadedPhysics &&move) = delete; - ThreadedPhysics &operator=(ThreadedPhysics &&move) = delete; + threaded_physics(const threaded_physics& copy) = delete; + auto operator=(const threaded_physics& copy) -> threaded_physics& = delete; + threaded_physics(threaded_physics&& move) = delete; + auto operator=(threaded_physics&& move) -> threaded_physics& = delete; - ~ThreadedPhysics() { - state.running = false; - state.data_ready_cnd.notify_all(); - state.data_consumed_cnd.notify_all(); - physics.join(); - } + ~threaded_physics() + { + state.running = false; + state.data_ready_cnd.notify_all(); + state.data_consumed_cnd.notify_all(); + physics.join(); + } private: - static auto PhysicsThread(PhysicsState &state) -> void; + static auto physics_thread(physics_state& state) -> void; public: - auto AddMassCmd() -> void; + auto add_mass_cmd() -> void; - auto AddSpringCmd(std::size_t a, std::size_t b) -> void; + auto add_spring_cmd(size_t a, size_t b) -> void; - auto ClearCmd() -> void; + auto clear_cmd() -> void; - auto AddMassSpringsCmd( - std::size_t num_masses, - const std::vector> &springs) -> void; + auto add_mass_springs_cmd(size_t num_masses, const std::vector>& springs) -> void; }; // https://en.cppreference.com/w/cpp/utility/variant/visit -template struct overloads : Ts... { - using Ts::operator()...; +template +struct overloads : Ts... +{ + using Ts::operator()...; }; #endif diff --git a/include/puzzle.hpp b/include/puzzle.hpp index 4f2f2bd..8fb9e29 100644 --- a/include/puzzle.hpp +++ b/include/puzzle.hpp @@ -1,128 +1,21 @@ -#ifndef __PUZZLE_HPP_ -#define __PUZZLE_HPP_ +#ifndef PUZZLE_HPP_ +#define PUZZLE_HPP_ -#include "config.hpp" +#include "util.hpp" #include #include #include #include -#include #include #include -enum Direction { - NOR = 1 << 0, - EAS = 1 << 1, - SOU = 1 << 2, - WES = 1 << 3, -}; - -// A block is represented as a 2-digit string "wh", where w is the block width -// and h the block height. -// The target block (to remove from the board) is represented as a 2-letter -// lower-case string "xy", where x is the block width and y the block height and -// width/height are represented by [abcdefghi] (~= [123456789]). -// Immovable blocks are represented as a 2-letter upper-case string "XY", where -// X is the block width and Y the block height and width/height are represented -// by [ABCDEFGHI] (~= [123456789]). -class Block { -public: - int x; - int y; - int width; - int height; - bool target; - bool immovable; - -public: - Block(int _x, int _y, int _width, int _height, bool _target, bool _immovable) - : x(_x), y(_y), width(_width), height(_height), target(_target), - immovable(_immovable) { - if (_x < 0 || _x + _width >= 10 || _y < 0 || _y + _height >= 10) { - std::cout << std::format("Block must fit in a 9x9 board!") << std::endl; - exit(1); - } - } - - Block(int _x, int _y, int _width, int _height, bool _target) - : Block(_x, _y, _width, _height, _target, false) {} - - Block(int _x, int _y, std::string block) : x(_x), y(_y) { - if (block == "..") { - this->x = 0; - this->y = 0; - width = 0; - height = 0; - target = false; - return; - } - - const std::array target_chars{'a', 'b', 'c', 'd', 'e', - 'f', 'g', 'h', 'i'}; - - target = false; - for (const char c : target_chars) { - if (block.contains(c)) { - target = true; - break; - } - } - - const std::array immovable_chars{'A', 'B', 'C', 'D', 'E', - 'F', 'G', 'H', 'I'}; - - immovable = false; - for (const char c : immovable_chars) { - if (block.contains(c)) { - immovable = true; - break; - } - } - - if (target) { - width = static_cast(block.at(0)) - static_cast('a') + 1; - height = static_cast(block.at(1)) - static_cast('a') + 1; - } else if (immovable) { - width = static_cast(block.at(0)) - static_cast('A') + 1; - height = static_cast(block.at(1)) - static_cast('A') + 1; - } else { - width = std::stoi(block.substr(0, 1)); - height = std::stoi(block.substr(1, 1)); - } - - if (_x < 0 || _x + width >= 10 || _y < 0 || _y + height >= 10) { - std::cout << std::format("Block must fit in a 9x9 board!") << std::endl; - exit(1); - } - if (block.length() != 2) { - std::cout << std::format("Block representation must have length 2!") - << std::endl; - exit(1); - } - } - - bool operator==(const Block &other) { - return x == other.x && y == other.y && width && other.width && - target == other.target && immovable == other.immovable; - } - - bool operator!=(const Block &other) { return !(*this == other); } - -public: - auto Hash() const -> std::size_t; - - static auto Invalid() -> Block; - - auto IsValid() const -> bool; - - auto ToString() const -> std::string; - - auto GetPrincipalDirs() const -> int; - - auto Covers(int xx, int yy) const -> bool; - - auto Collides(const Block &other) const -> bool; +enum direction +{ + nor = 1 << 0, + eas = 1 << 1, + sou = 1 << 2, + wes = 1 << 3, }; // A state is represented by a string "MWHXYblocks", where M is "R" @@ -133,189 +26,319 @@ public: // representation with length 5 + 3*3 * 2). The board's cells are enumerated // from top-left to bottom-right with each block's pivot being its top-left // corner. -class State { +class puzzle +{ public: - static constexpr int prefix = 5; + // A block is represented as a 2-digit string "wh", where w is the block width + // and h the block height. + // The target block (to remove from the board) is represented as a 2-letter + // lower-case string "xy", where x is the block width and y the block height and + // width/height are represented by [abcdefghi] (~= [123456789]). + // Immovable blocks are represented as a 2-letter upper-case string "XY", where + // X is the block width and Y the block height and width/height are represented + // by [ABCDEFGHI] (~= [123456789]). + class block + { + public: + int x = 0; + int y = 0; + int width = 0; + int height = 0; + bool target = false; + bool immovable = false; - int width; - int height; - int target_x; - int target_y; - bool restricted; // Only allow blocks to move in their principal direction - std::string state; + public: + 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) { + errln("Block must fit in a 9x9 board!"); + exit(1); + } + } - // https://en.cppreference.com/w/cpp/iterator/input_iterator.html - class BlockIterator { - public: - using difference_type = std::ptrdiff_t; - using value_type = Block; + block(const int _x, const int _y, const std::string& b) : x(_x), y(_y) + { + if (b.length() != 2) { + errln("Block representation must have length 2!"); + exit(1); + } - private: - const State &state; - int current_pos; + if (b == "..") { + this->x = 0; + this->y = 0; + width = 0; + height = 0; + target = false; + return; + } - public: - BlockIterator(const State &_state) : state(_state), current_pos(0) {} + target = false; + constexpr std::array target_chars{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'}; + for (const char c : target_chars) { + if (b.contains(c)) { + target = true; + break; + } + } - BlockIterator(const State &_state, int _current_pos) - : state(_state), current_pos(_current_pos) {} + immovable = false; + constexpr std::array immovable_chars{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'}; + for (const char c : immovable_chars) { + if (b.contains(c)) { + immovable = true; + break; + } + } - Block operator*() const { - return Block(current_pos % state.width, current_pos / state.width, - state.state.substr(current_pos * 2 + prefix, 2)); - } + if (target) { + width = static_cast(b.at(0)) - static_cast('a') + 1; + height = static_cast(b.at(1)) - static_cast('a') + 1; + } else if (immovable) { + width = static_cast(b.at(0)) - static_cast('A') + 1; + height = static_cast(b.at(1)) - static_cast('A') + 1; + } else { + // println("Parsing block string '{}' at ({}, {})", b, _x, _y); + width = std::stoi(b.substr(0, 1)); + height = std::stoi(b.substr(1, 1)); + } - BlockIterator &operator++() { - do { - current_pos++; - } while (state.state.substr(current_pos * 2 + prefix, 2) == ".."); - return *this; - } + if (_x < 0 || _x + width > 9 || _y < 0 || _y + height > 9) { + errln("Block must fit in a 9x9 board!"); + exit(1); + } + } - bool operator==(const BlockIterator &other) { - return state == other.state && current_pos == other.current_pos; - } + block() = delete; - bool operator!=(const BlockIterator &other) { return !(*this == other); } - }; + auto operator==(const block& other) const -> bool + { + return x == other.x && y == other.y && width && other.width && target == other.target && + immovable == other.immovable; + } + + auto operator!=(const block& other) const -> bool + { + return !(*this == other); + } + + public: + [[nodiscard]] auto hash() const -> size_t; + [[nodiscard]] auto valid() const -> bool; + [[nodiscard]] auto string() const -> std::string; + + // Movement + + [[nodiscard]] auto principal_dirs() const -> int; + [[nodiscard]] auto covers(int _x, int _y) const -> bool; + [[nodiscard]] auto collides(const block& b) const -> bool; + }; + +private: + // https://en.cppreference.com/w/cpp/iterator/input_iterator.html + class block_iterator + { + public: + using difference_type = std::ptrdiff_t; + using value_type = block; + + private: + const puzzle& state; + int current_pos; + + public: + 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) + {} + + auto operator*() const -> block + { + return {current_pos % state.width, current_pos / state.width, + state.state.substr(current_pos * 2 + PREFIX, 2)}; + } + + auto operator++() -> block_iterator& + { + do { + current_pos++; + } while (current_pos < state.width * state.height && + state.state.substr(current_pos * 2 + PREFIX, 2) == ".."); + return *this; + } + + auto operator==(const block_iterator& other) const -> bool + { + return state == other.state && current_pos == other.current_pos; + } + + auto operator!=(const block_iterator& other) const -> bool + { + return !(*this == other); + } + }; public: - State(int _width, int _height, int _target_x, int _target_y, bool _restricted) - : width(_width), height(_height), target_x(_target_x), - target_y(_target_y), restricted(_restricted), - state(std::format("{}{}{}{}{}{}", _restricted ? "R" : "F", _width, - _height, _target_x, _target_y, - std::string(_width * _height * 2, '.'))) { - if (_width < 1 || _width > 9 || _height < 1 || _height > 9) { - std::cout << std::format("State width/height must be in [1, 9]!") - << std::endl; - exit(1); - } - if (_target_x < 0 || _target_x >= 9 || _target_y < 0 || _target_y >= 9) { - if (_target_x != 9 && _target_y != 9) { - std::cout << std::format( - "State target must be within the board bounds!") - << std::endl; - exit(1); - } - } - } + static constexpr int PREFIX = 5; + static constexpr int MIN_WIDTH = 3; + static constexpr int MIN_HEIGHT = 3; + static constexpr int MAX_WIDTH = 9; + static constexpr int MAX_HEIGHT = 9; - State(int _width, int _height, bool _restricted) - : State(_width, _height, 9, 9, _restricted) {} - - State() : State(4, 5, 9, 9, false) {} - - explicit State(std::string _state) - : width(std::stoi(_state.substr(1, 1))), - height(std::stoi(_state.substr(2, 1))), - target_x(std::stoi(_state.substr(3, 1))), - target_y(std::stoi(_state.substr(4, 1))), - restricted(_state.substr(0, 1) == "R"), state(_state) { - if (width < 1 || width > 9 || height < 1 || height > 9) { - std::cout << std::format("State width/height must be in [1, 9]!") - << std::endl; - exit(1); - } - if (target_x < 0 || target_x >= 9 || target_y < 0 || target_y >= 9) { - if (target_x != 9 && target_y != 9) { - std::cout << std::format( - "State target must be within the board bounds!") - << std::endl; - exit(1); - } - } - if (static_cast(_state.length()) != width * height * 2 + prefix) { - std::cout << std::format("State representation must have length width * " - "height * 2 + {}!", - prefix) - << std::endl; - exit(1); - } - } - - bool operator==(const State &other) const { return state == other.state; } - - bool operator!=(const State &other) const { return !(*this == other); } - - BlockIterator begin() const { - BlockIterator it = BlockIterator(*this); - if (!(*it).IsValid()) { - ++it; - } - return it; - } - - BlockIterator end() const { return BlockIterator(*this, width * height); } + int width = 0; + int height = 0; + int target_x = 9; + int target_y = 9; + bool restricted = false; // Only allow blocks to move in their principal direction + std::string state; public: - auto Hash() const -> std::size_t; + puzzle() = delete; - auto HasWinCondition() const -> bool; + 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, '.'))) + { + if (w < 1 || w > 9 || h < 1 || h > 9) { + errln("State width/height must be in [1, 9]!"); + exit(1); + } + if (tx < 0 || tx >= 9 || ty < 0 || ty >= 9) { + if (tx != 9 && ty != 9) { + errln("State target must be within the board bounds!"); + exit(1); + } + } + } - auto IsWon() const -> bool; + puzzle(const int w, const int h, const bool r) : puzzle(w, h, 9, 9, r) + {} - auto SetGoal(int x, int y) -> bool; + 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) + { + if (width < 1 || width > 9 || height < 1 || height > 9) { + errln("State width/height must be in [1, 9]!"); + exit(1); + } + if (target_x < 0 || target_x >= 9 || target_y < 0 || target_y >= 9) { + if (target_x != 9 && target_y != 9) { + errln("State target must be within the board bounds!"); + exit(1); + } + } + if (static_cast(s.length()) != width * height * 2 + PREFIX) { + errln("State representation must have length width * " + "height * 2 + {}!", + PREFIX); + exit(1); + } + } - auto ClearGoal() -> void; +public: + auto operator==(const puzzle& other) const -> bool + { + return state == other.state; + } - auto AddColumn() const -> State; + auto operator!=(const puzzle& other) const -> bool + { + return !(*this == other); + } - auto RemoveColumn() const -> State; + [[nodiscard]] auto begin() const -> block_iterator + { + block_iterator it{*this}; + if (!(*it).valid()) { + ++it; + } + return it; + } - auto AddRow() const -> State; + [[nodiscard]] auto end() const -> block_iterator + { + return {*this, width * height}; + } - auto RemoveRow() const -> State; +private: + [[nodiscard]] auto get_index(int x, int y) const -> int; - auto AddBlock(const Block &block) -> bool; +public: + [[nodiscard]] auto hash() const -> size_t; + [[nodiscard]] auto has_win_condition() const -> bool; + [[nodiscard]] auto won() const -> bool; + [[nodiscard]] auto valid() const -> bool; + [[nodiscard]] auto valid_thorough() const -> bool; - auto GetBlock(int x, int y) const -> Block; + // Repr helpers - auto GetBlockAt(int x, int y) const -> std::string; + [[nodiscard]] auto try_get_block(int x, int y) const -> std::optional; + [[nodiscard]] auto try_get_target_block() const -> std::optional; + [[nodiscard]] auto covers(int x, int y, int w, int h) const -> bool; + [[nodiscard]] auto covers(int x, int y) const -> bool; + [[nodiscard]] auto covers(const block& b) const -> bool; - auto GetTargetBlock() const -> Block; + // Editing - auto GetIndex(int x, int y) const -> int; + [[nodiscard]] auto try_toggle_restricted() const -> std::optional; + [[nodiscard]] auto try_set_goal(int x, int y) const -> std::optional; + [[nodiscard]] auto try_clear_goal() const -> std::optional; + [[nodiscard]] auto try_add_column() const -> std::optional; + [[nodiscard]] auto try_remove_column() const -> std::optional; + [[nodiscard]] auto try_add_row() const -> std::optional; + [[nodiscard]] auto try_remove_row() const -> std::optional; + [[nodiscard]] auto try_add_block(const block& b) const -> std::optional; + [[nodiscard]] auto try_remove_block(int x, int y) const -> std::optional; + [[nodiscard]] auto try_toggle_target(int x, int y) const -> std::optional; + [[nodiscard]] auto try_toggle_wall(int x, int y) const -> std::optional; - auto RemoveBlock(int x, int y) -> bool; + // Playing - auto ToggleTarget(int x, int y) -> bool; + [[nodiscard]] auto try_move_block_at(int x, int y, direction dir) const -> std::optional; - auto ToggleWall(int x, int y) -> bool; + // Statespace - auto ToggleRestricted() -> void; - - auto MoveBlockAt(int x, int y, Direction dir) -> bool; - - auto GetNextStates() const -> std::vector; - - auto Closure() const - -> std::pair, - std::vector>>; + [[nodiscard]] auto find_adjacent_puzzles() const -> std::vector; + [[nodiscard]] auto explore_state_space() const + -> std::pair, std::vector>>; }; // Provide hash functions so we can use State and as hash-set // keys for masses and springs. -template <> struct std::hash { - std::size_t operator()(const State &s) const noexcept { return s.Hash(); } +template <> +struct std::hash +{ + auto operator()(const puzzle& s) const noexcept -> size_t + { + return s.hash(); + } }; -template <> struct std::hash> { - std::size_t operator()(const std::pair &s) const noexcept { - auto h1 = std::hash{}(s.first); - auto h2 = std::hash{}(s.second); - return h1 + h2 + (h1 * h2); - } +template <> +struct std::hash> +{ + auto operator()(const std::pair& s) const noexcept -> size_t + { + const size_t h1 = std::hash{}(s.first); + const size_t h2 = std::hash{}(s.second); + + return h1 + h2 + h1 * h2; + // return (h1 ^ h2) + 0x9e3779b9 + (std::min(h1, h2) << 6) + (std::max(h1, h2) >> 2); + } }; -template <> struct std::equal_to> { - bool operator()(const std::pair &a, - const std::pair &b) const noexcept { - return (a.first == b.first && a.second == b.second) || - (a.first == b.second && a.second == b.first); - } +template <> +struct std::equal_to> +{ + auto operator()(const std::pair& a, const std::pair& b) const noexcept -> bool + { + return (a.first == b.first && a.second == b.second) || (a.first == b.second && a.second == b.first); + } }; -using WinCondition = std::function; +using win_condition = std::function; #endif diff --git a/include/renderer.hpp b/include/renderer.hpp index 8d48139..08c056f 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -1,80 +1,100 @@ -#ifndef __RENDERER_HPP_ -#define __RENDERER_HPP_ +#ifndef RENDERER_HPP_ +#define RENDERER_HPP_ #include "camera.hpp" #include "config.hpp" -#include "gui.hpp" #include "input.hpp" -#include "state.hpp" +#include "state_manager.hpp" +#include "user_interface.hpp" #include -#include +#include -class Renderer { +class renderer +{ private: - const StateManager &state; - const InputHandler &input; - Gui &gui; + const state_manager& state; + const input_handler& input; + user_interface& gui; - const OrbitCamera3D &camera; - RenderTexture render_target; - RenderTexture klotski_target; - RenderTexture menu_target; + const orbit_camera& camera; + RenderTexture render_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); + RenderTexture klotski_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); + RenderTexture menu_target = LoadRenderTexture(GetScreenWidth(), MENU_HEIGHT); - // Instancing - Material vertex_mat; - std::size_t transforms_size = 0; - Matrix *transforms = nullptr; - Mesh cube_instance; - Shader instancing_shader; - -public: - Renderer(const OrbitCamera3D &_camera, const StateManager &_state, - const InputHandler &_input, Gui &_gui) - : state(_state), input(_input), gui(_gui), camera(_camera) { - render_target = LoadRenderTexture(GetScreenWidth() / 2.0, - GetScreenHeight() - MENU_HEIGHT); - klotski_target = LoadRenderTexture(GetScreenWidth() / 2.0, - GetScreenHeight() - MENU_HEIGHT); - menu_target = LoadRenderTexture(GetScreenWidth(), MENU_HEIGHT); - } - - Renderer(const Renderer ©) = delete; - Renderer &operator=(const Renderer ©) = delete; - Renderer(Renderer &&move) = delete; - Renderer &operator=(Renderer &&move) = delete; - - ~Renderer() { - UnloadRenderTexture(render_target); - UnloadRenderTexture(klotski_target); - UnloadRenderTexture(menu_target); + // Batching + std::vector> connections; // Instancing - if (transforms != nullptr) { - UnloadMaterial(vertex_mat); - MemFree(transforms); - UnloadMesh(cube_instance); + static constexpr int INSTANCE_COLOR_ATTR = 5; + std::vector transforms; + std::vector 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"); - // I think the shader already gets unloaded with the material? - // UnloadShader(instancing_shader); - } - } - -private: - auto AllocateGraphInstancing(std::size_t size) -> void; - - auto ReallocateGraphInstancingIfNecessary(std::size_t size) -> void; + unsigned int color_vbo_id = 0; public: - auto UpdateTextureSizes() -> void; + renderer(const orbit_camera& _camera, const state_manager& _state, const input_handler& _input, + user_interface& _gui) + : state(_state), input(_input), gui(_gui), camera(_camera) + { + 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"); - auto DrawMassSprings(const std::vector &masses) -> void; + infoln("LOC vertexPosition: {}", rlGetLocationAttrib(instancing_shader.id, "vertexPosition")); + infoln("LOC instanceTransform: {}", rlGetLocationAttrib(instancing_shader.id, "instanceTransform")); + infoln("LOC instanceColor: {}", rlGetLocationAttrib(instancing_shader.id, "instanceColor")); - auto DrawKlotski() -> void; + // vertex_mat.maps[MATERIAL_MAP_DIFFUSE].color = VERTEX_COLOR; + vertex_mat.shader = instancing_shader; - auto DrawMenu(const std::vector &masses) -> void; + transforms.reserve(DRAW_VERTICES_LIMIT); + colors.reserve(DRAW_VERTICES_LIMIT); + color_vbo_id = rlLoadVertexBuffer(colors.data(), DRAW_VERTICES_LIMIT * sizeof(Color), true); - auto DrawTextures(int fps, int ups) -> void; + rlEnableVertexArray(cube_instance.vaoId); + rlEnableVertexBuffer(color_vbo_id); + rlSetVertexAttribute(INSTANCE_COLOR_ATTR, 4, RL_UNSIGNED_BYTE, true, 0, 0); + rlEnableVertexAttribute(INSTANCE_COLOR_ATTR); + rlSetVertexAttributeDivisor(INSTANCE_COLOR_ATTR, 1); + + rlDisableVertexBuffer(); + rlDisableVertexArray(); + } + + renderer(const renderer& copy) = delete; + auto operator=(const renderer& copy) -> renderer& = delete; + renderer(renderer&& move) = delete; + auto operator=(renderer&& move) -> renderer& = delete; + + ~renderer() + { + UnloadRenderTexture(render_target); + UnloadRenderTexture(klotski_target); + UnloadRenderTexture(menu_target); + + // Instancing + UnloadMaterial(vertex_mat); + UnloadMesh(cube_instance); + + // I think the shader already gets unloaded with the material? + // UnloadShader(instancing_shader); + } + +private: + auto update_texture_sizes() -> void; + + auto draw_mass_springs(const std::vector& masses) -> void; + auto draw_klotski() const -> void; + auto draw_menu() const -> void; + auto draw_textures(int fps, int ups, size_t mass_count, size_t spring_count) const -> void; + +public: + auto render(const std::vector& masses, int fps, int ups, size_t mass_count, size_t spring_count) -> void; }; #endif diff --git a/include/state.hpp b/include/state.hpp deleted file mode 100644 index 14f6533..0000000 --- a/include/state.hpp +++ /dev/null @@ -1,103 +0,0 @@ -#ifndef __STATE_HPP_ -#define __STATE_HPP_ - -#include "config.hpp" -#include "distance.hpp" -#include "physics.hpp" -#include "puzzle.hpp" - -#include -#include -#include -#include - -class StateManager { -public: - ThreadedPhysics &physics; - - std::vector presets = {State()}; - std::vector comments = {"Empty"}; - - // Some stuff is faster to map from state to mass (e.g. in the renderer) - std::unordered_map states; // State to mass id - std::unordered_set winning_states; - std::unordered_map visited_states; // How often we've been here - std::stack history; - - // Other stuff maps from mass to state :/ - std::unordered_map masses; // Mass id to state - std::vector winning_path; - - // Fuck it, duplicate the springs too, we don't even need to copy them from - // the physics thread then... - std::vector> springs; - - // Distance calculation result can be buffered and reused to calculate a new - // path on the same graph - DistanceResult target_distances; - - std::string preset_file; - - int total_moves = 0; - int current_preset = 0; - State starting_state; - State current_state; - State previous_state; - - bool edited = false; - -public: - StateManager(ThreadedPhysics &_physics, const std::string &_preset_file) - : physics(_physics) { - ParsePresetFile(_preset_file); - current_state = presets.at(current_preset); - ClearGraph(); - } - - StateManager(const StateManager ©) = delete; - StateManager &operator=(const StateManager ©) = delete; - StateManager(StateManager &&move) = delete; - StateManager &operator=(StateManager &&move) = delete; - - ~StateManager() {} - -private: - auto ParsePresetFile(const std::string &_preset_file) -> bool; - -public: - auto AppendPresetFile(const std::string preset_name) -> void; - - auto LoadPreset(int preset) -> void; - - auto ResetState() -> void; - - auto PreviousPreset() -> void; - - auto NextPreset() -> void; - - auto NextPath() -> void; - - auto FillGraph() -> void; - - auto UpdateGraph() -> void; - - auto ClearGraph() -> void; - - auto FindWinningStates() -> void; - - auto FindTargetDistances() -> void; - - auto FindTargetPath() -> void; - - auto FindWorstState() -> State; - - auto GoToWorst() -> void; - - auto GoToNearestTarget() -> void; - - auto PopHistory() -> void; - - auto CurrentMassIndex() const -> std::size_t; -}; - -#endif diff --git a/include/state_manager.hpp b/include/state_manager.hpp new file mode 100644 index 0000000..6f4db1c --- /dev/null +++ b/include/state_manager.hpp @@ -0,0 +1,151 @@ +#ifndef STATE_MANAGER_HPP_ +#define STATE_MANAGER_HPP_ + +#include "distance.hpp" +#include "physics.hpp" +#include "puzzle.hpp" + +#include +#include +#include + +class state_manager +{ +private: + threaded_physics& physics; + + std::string preset_file; + size_t current_preset = 0; + std::vector preset_states = {puzzle(4, 5, 9, 9, false)}; + std::vector preset_comments = {"Empty"}; + + // State storage (store states twice for bidirectional lookup). + // Everything else should only store indices to state_pool. + + std::vector state_pool; // Indices are equal to mass_springs mass indices + std::unordered_map state_indices; // Maps states to indices + std::vector> 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 winning_indices; // Indices of all states where the board is solved + std::vector winning_path; // Ordered list of node indices leading to the nearest solved state + std::unordered_set path_indices; // For faster lookup if a vertex is part of the path in renderer + + std::stack move_history; // Moves between the starting state and the current state + std::unordered_map visit_counts; // How often each state was visited + + size_t starting_state_index = 0; + size_t current_state_index = 0; + size_t previous_state_index = 0; + + int total_moves = 0; + +public: + state_manager(threaded_physics& _physics, const std::string& _preset_file) : physics(_physics) + { + parse_preset_file(_preset_file); + load_preset(0); + } + + state_manager(const state_manager& copy) = delete; + auto operator=(const state_manager& copy) -> state_manager& = delete; + state_manager(state_manager&& move) = delete; + auto operator=(state_manager&& move) -> state_manager& = delete; + +private: + /** + * Inserts a board state into the state_manager and the physics system. + * States should only be inserted using this function to keep both systems in sync. + * The function checks for duplicates before insertion. + * + * @param state State to insert + * @return Index of insertion (or existing index if duplicate) + */ + auto synced_try_insert_state(const puzzle& state) -> size_t; + + /** + * Inserts a state link into the state_manager and the physics system. + * Links should only be inserted using this function to keep both systems in sync. + * The function does not check for duplicates before insertion. + * + * @param first_index Index of the first linked state + * @param second_index Index of the second linked state + */ + auto synced_insert_link(size_t first_index, size_t second_index) -> void; + + /** + * Inserts an entire statespace into the state_manager and the physics system. + * If inserting many states and links in bulk, this function should always be used + * to not stress the physics command mutex. + * The function does not check for duplicates before insertion. + * + * @param states List of states to insert + * @param _links List of links to insert + */ + auto synced_insert_statespace(const std::vector& states, + const std::vector>& _links) -> void; + + /** + * Clears all states and links (and related) from the state_manager and the physics system. + * Note that this leaves any dangling indices (e.g., current_state_index) in an invalid state. + */ + auto synced_clear_statespace() -> void; + +public: + // Presets + + auto parse_preset_file(const std::string& _preset_file) -> bool; + auto append_preset_file(const std::string& preset_name) -> bool; + auto load_preset(size_t preset) -> void; + auto load_previous_preset() -> void; + auto load_next_preset() -> void; + + // Update current_state + + auto update_current_state(const puzzle& p) -> void; + auto edit_starting_state(const puzzle& p) -> void; + auto goto_starting_state() -> void; + auto goto_optimal_next_state() -> void; + auto goto_previous_state() -> void; + auto goto_most_distant_state() -> void; + auto goto_closest_target_state() -> void; + + // Update graph + + auto populate_graph() -> void; + auto clear_graph_and_add_current(const puzzle& p) -> void; + auto clear_graph_and_add_current() -> void; + auto populate_winning_indices() -> void; + auto populate_node_target_distances() -> void; + auto populate_winning_path() -> void; + + // Index mapping + + [[nodiscard]] auto get_index(const puzzle& state) const -> size_t; + [[nodiscard]] auto get_current_index() const -> size_t; + [[nodiscard]] auto get_starting_index() const -> size_t; + [[nodiscard]] auto get_state(size_t index) const -> const puzzle&; + [[nodiscard]] auto get_current_state() const -> const puzzle&; + [[nodiscard]] auto get_starting_state() const -> const puzzle&; + + // Access + [[nodiscard]] auto get_state_count() const -> size_t; + [[nodiscard]] auto get_target_count() const -> size_t; + [[nodiscard]] auto get_link_count() const -> size_t; + [[nodiscard]] auto get_path_length() const -> size_t; + [[nodiscard]] auto get_links() const -> const std::vector>&; + [[nodiscard]] auto get_winning_indices() const -> const std::unordered_set&; + [[nodiscard]] auto get_visit_counts() const -> const std::unordered_map&; + [[nodiscard]] auto get_winning_path() const -> const std::vector&; + [[nodiscard]] auto get_path_indices() const -> const std::unordered_set&; + [[nodiscard]] auto get_current_visits() const -> int; + [[nodiscard]] auto get_current_preset() const -> size_t; + [[nodiscard]] auto get_preset_count() const -> size_t; + [[nodiscard]] auto get_current_preset_comment() const -> const std::string&; + [[nodiscard]] auto has_history() const -> bool; + [[nodiscard]] auto has_distances() const -> bool; + [[nodiscard]] auto get_total_moves() const -> size_t; + [[nodiscard]] auto was_edited() const -> bool; +}; + +#endif diff --git a/include/tracy.hpp b/include/tracy.hpp deleted file mode 100644 index 5aa0e82..0000000 --- a/include/tracy.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef __TRACY_HPP_ -#define __TRACY_HPP_ - -#include "config.hpp" - -#ifdef TRACY - -#include - -void *operator new(std::size_t count); -void operator delete(void *ptr) noexcept; -void operator delete(void *ptr, std::size_t count) noexcept; - -#endif - -#endif diff --git a/include/user_interface.hpp b/include/user_interface.hpp new file mode 100644 index 0000000..07a77ed --- /dev/null +++ b/include/user_interface.hpp @@ -0,0 +1,167 @@ +#ifndef GUI_HPP_ +#define GUI_HPP_ + +#include "camera.hpp" +#include "config.hpp" +#include "input.hpp" +#include "state_manager.hpp" + +#include + +class user_interface +{ + class grid + { + public: + int x; + int y; + int width; + int height; + int columns; + int rows; + 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) + {} + + 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) -> void; + auto update_bounds(int _x, int _y) -> void; + + [[nodiscard]] auto bounds() const -> Rectangle; + [[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; + }; + + struct style + { + int border_color_normal; + int base_color_normal; + int text_color_normal; + + int border_color_focused; + int base_color_focused; + int text_color_focused; + + int border_color_pressed; + int base_color_pressed; + int text_color_pressed; + + int border_color_disabled; + int base_color_disabled; + int text_color_disabled; + }; + + struct default_style : style + { + int background_color; + int line_color; + + int text_size; + int text_spacing; + int text_line_spacing; + int text_alignment_vertical; + int text_wrap_mode; + }; + + struct component_style : style + { + int border_width; + int text_padding; + int text_alignment; + }; + +private: + input_handler& input; + state_manager& state; + const orbit_camera& camera; + + 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 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); + + bool save_window = false; + std::array preset_name = {}; + bool help_window = false; + +public: + user_interface(input_handler& _input, state_manager& _state, const orbit_camera& _camera) + : input(_input), state(_state), camera(_camera) + { + init(); + } + + user_interface(const user_interface& copy) = delete; + auto operator=(const user_interface& copy) -> user_interface& = delete; + user_interface(user_interface&& move) = delete; + auto operator=(user_interface&& move) -> user_interface& = delete; + +private: + static auto init() -> void; + + static auto apply_color(style& style, Color color) -> void; + static auto apply_block_color(style& style, Color color) -> void; + static auto apply_text_color(style& style, Color color) -> void; + + static auto get_default_style() -> default_style; + static auto set_default_style(const default_style& style) -> void; + static auto get_component_style(int component) -> component_style; + static auto set_component_style(int component, const component_style& style) -> void; + + 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_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; + + 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_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; + + [[nodiscard]] auto window_open() const -> bool; + + // Different menu sections + auto draw_menu_header(Color color) const -> void; + auto draw_graph_info(Color color) const -> void; + auto draw_graph_controls(Color color) const -> void; + auto draw_camera_controls(Color color) const -> void; + auto draw_puzzle_controls(Color color) const -> void; + auto draw_edit_controls(Color color) const -> void; + auto draw_menu_footer(Color color) -> void; + +public: + static auto get_background_color() -> Color; + auto help_popup() -> void; + auto draw_save_preset_popup() -> 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; +}; + +#endif diff --git a/include/util.hpp b/include/util.hpp index dcb0f3f..27e997a 100644 --- a/include/util.hpp +++ b/include/util.hpp @@ -1,20 +1,87 @@ -#ifndef __UTIL_HPP_ -#define __UTIL_HPP_ - -#include "config.hpp" +#ifndef UTIL_HPP_ +#define UTIL_HPP_ #include #include #include -inline std::ostream &operator<<(std::ostream &os, const Vector2 &v) { - os << "(" << v.x << ", " << v.y << ")"; - return os; +inline auto operator<<(std::ostream& os, const Vector2& v) -> std::ostream& +{ + os << "(" << v.x << ", " << v.y << ")"; + return os; } -inline std::ostream &operator<<(std::ostream &os, const Vector3 &v) { - os << "(" << v.x << ", " << v.y << ", " << v.z << ")"; - return os; +inline auto operator<<(std::ostream& os, const Vector3& v) -> std::ostream& +{ + os << "(" << v.x << ", " << v.y << ", " << v.z << ")"; + return os; +} + +enum ctrl +{ + reset = 0, + bold_bright = 1, + underline = 4, + inverse = 7, + bold_bright_off = 21, + underline_off = 24, + inverse_off = 27 +}; + +enum fg +{ + fg_black = 30, + fg_red = 31, + fg_green = 32, + fg_yellow = 33, + fg_blue = 34, + fg_magenta = 35, + fg_cyan = 36, + fg_white = 37 +}; + +enum bg +{ + bg_black = 40, + bg_red = 41, + bg_green = 42, + bg_yellow = 43, + bg_blue = 44, + bg_magenta = 45, + bg_cyan = 46, + bg_white = 47 +}; + +inline auto ansi_bold_fg(const fg color) -> std::string +{ + return std::format("\033[1;{}m", static_cast(color)); +} + +inline auto ansi_reset() -> std::string +{ + return "\033[0m"; +} + +// std::println doesn't work with mingw +template +auto infoln(std::format_string fmt, Args&&... args) -> void +{ + std::cout << std::format("[{}INFO{}]: ", ansi_bold_fg(fg_blue), ansi_reset()) + << std::format(fmt, std::forward(args)...) << std::endl; +} + +template +auto warnln(std::format_string fmt, Args&&... args) -> void +{ + std::cout << std::format("[{}WARNING{}]: ", ansi_bold_fg(fg_yellow), ansi_reset()) + << std::format(fmt, std::forward(args)...) << std::endl; +} + +template +auto errln(std::format_string fmt, Args&&... args) -> void +{ + std::cout << std::format("[{}ERROR{}]: ", ansi_bold_fg(fg_red), ansi_reset()) + << std::format(fmt, std::forward(args)...) << std::endl; } #endif diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..cff4e29 Binary files /dev/null and b/screenshot.png differ diff --git a/shader/instancing_fragment.glsl b/shader/instancing_fragment.glsl index 493b423..ce97a8c 100644 --- a/shader/instancing_fragment.glsl +++ b/shader/instancing_fragment.glsl @@ -1,8 +1,9 @@ #version 330 -uniform vec4 colDiffuse; +in vec4 fragColor; out vec4 finalColor; void main() { - finalColor = colDiffuse; -} + // Advanced coloring. CG lecture really paying off now + finalColor = fragColor; +} \ No newline at end of file diff --git a/shader/instancing_vertex.glsl b/shader/instancing_vertex.glsl index 3af83a5..b9573a4 100644 --- a/shader/instancing_vertex.glsl +++ b/shader/instancing_vertex.glsl @@ -1,10 +1,14 @@ #version 330 -in vec3 vertexPosition; -in mat4 instanceTransform; +layout(location=0) in vec3 vertexPosition; +layout(location=1) in mat4 instanceTransform; +layout(location=5) in vec4 instanceColor; uniform mat4 mvp; +out vec4 fragColor; + void main() { + fragColor = instanceColor; gl_Position = mvp * instanceTransform * vec4(vertexPosition, 1.0); -} +} \ No newline at end of file diff --git a/src/backward.cpp b/src/backward.cpp index 7eb1740..6526cc2 100644 --- a/src/backward.cpp +++ b/src/backward.cpp @@ -2,14 +2,14 @@ // // On GNU/Linux, you have few choices to get the most out of your stack trace. // -// By default you get: -// - object filename -// - function name +// By default, you get: +// - object filename +// - function name // // In order to add: -// - source filename -// - line and column numbers -// - source code snippet (assuming the file is accessible) +// - source filename +// - line and column numbers +// - source code snippet (assuming the file is accessible) // Install one of the following libraries then uncomment one of the macro (or // better, add the detection of the lib and the macro definition in your build @@ -33,16 +33,13 @@ // - g++/clang++ -lunwind // #define BACKWARD_HAS_LIBUNWIND 1 -#include "config.hpp" - #ifdef BACKWARD -#include "backward.hpp" - -namespace backward { - -backward::SignalHandling sh; + #include "backward.hpp" +namespace backward +{ +SignalHandling sh; } // namespace backward #endif diff --git a/src/camera.cpp b/src/camera.cpp index 632ea4f..bcda4c7 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -5,74 +5,71 @@ #include #ifdef TRACY -#include "tracy.hpp" -#include + #include #endif -auto OrbitCamera3D::Rotate(Vector2 last_mouse, Vector2 mouse) -> void { - Vector2 dx = Vector2Subtract(mouse, last_mouse); +auto orbit_camera::rotate(const Vector2 last_mouse, const Vector2 mouse) -> void +{ + const auto [dx, dy] = Vector2Subtract(mouse, last_mouse); - angle_x -= dx.x * ROT_SPEED / 200.0; - angle_y += dx.y * ROT_SPEED / 200.0; + angle_x -= dx * ROT_SPEED / 200.0f; + angle_y += dy * ROT_SPEED / 200.0f; - angle_y = Clamp(angle_y, -1.5, 1.5); // Prevent flipping + angle_y = Clamp(angle_y, -1.5, 1.5); // Prevent flipping } -auto OrbitCamera3D::Pan(Vector2 last_mouse, Vector2 mouse) -> void { - Vector2 dx = Vector2Subtract(mouse, last_mouse); +auto orbit_camera::pan(const Vector2 last_mouse, const Vector2 mouse) -> void +{ + const auto [dx, dy] = Vector2Subtract(mouse, last_mouse); - float speed; - if (IsKeyDown(KEY_LEFT_SHIFT)) { - speed = distance * PAN_SPEED / 1000.0 * PAN_MULTIPLIER; - } else { - speed = distance * PAN_SPEED / 1000.0; - } - - // The panning needs to happen in camera coordinates, otherwise rotating the - // camera breaks it - Vector3 forward = - Vector3Normalize(Vector3Subtract(camera.target, camera.position)); - Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up)); - Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward)); - - Vector3 offset = Vector3Add(Vector3Scale(right, -dx.x * speed), - Vector3Scale(up, dx.y * speed)); - - target = Vector3Add(target, offset); -} - -auto OrbitCamera3D::Update(const Vector3 ¤t_target, - const Vector3 &mass_center, bool lock, - bool mass_center_lock) -> void { - if (lock) { - if (mass_center_lock) { - target = Vector3MoveTowards( - target, mass_center, - CAMERA_SMOOTH_SPEED * GetFrameTime() * - Vector3Length(Vector3Subtract(target, mass_center))); + float speed; + if (IsKeyDown(KEY_LEFT_SHIFT)) { + speed = distance * PAN_SPEED / 1000.0f * PAN_MULTIPLIER; } else { - target = Vector3MoveTowards( - target, current_target, - CAMERA_SMOOTH_SPEED * GetFrameTime() * - Vector3Length(Vector3Subtract(target, current_target))); + speed = distance * PAN_SPEED / 1000.0f; } - } - distance = Clamp(distance, MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE); - int actual_distance = distance; - if (projection == CAMERA_ORTHOGRAPHIC) { - actual_distance = MAX_CAMERA_DISTANCE; - } + // The panning needs to happen in camera coordinates, otherwise rotating the + // camera breaks it + const Vector3 forward = Vector3Normalize(Vector3Subtract(camera.target, camera.position)); + const Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up)); + const Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward)); - // Spherical coordinates - float x = cos(angle_y) * sin(angle_x) * actual_distance; - float y = sin(angle_y) * actual_distance; - float z = cos(angle_y) * cos(angle_x) * actual_distance; + const Vector3 offset = Vector3Add(Vector3Scale(right, -dx * speed), Vector3Scale(up, dy * speed)); - fov = Clamp(fov, MIN_FOV, MAX_FOV); - - camera.position = Vector3Add(target, Vector3(x, y, z)); - camera.target = target; - camera.fovy = fov; - camera.projection = projection; + 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 +{ + if (lock) { + if (mass_center_lock) { + target = Vector3MoveTowards(target, mass_center, + CAMERA_SMOOTH_SPEED * GetFrameTime() * + Vector3Length(Vector3Subtract(target, mass_center))); + } else { + target = Vector3MoveTowards(target, current_target, + CAMERA_SMOOTH_SPEED * GetFrameTime() * + Vector3Length(Vector3Subtract(target, current_target))); + } + } + + distance = Clamp(distance, MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE); + float actual_distance = distance; + if (projection == CAMERA_ORTHOGRAPHIC) { + actual_distance = MAX_CAMERA_DISTANCE; + } + + // Spherical coordinates + const float x = cos(angle_y) * sin(angle_x) * actual_distance; + const float y = sin(angle_y) * actual_distance; + const float z = cos(angle_y) * cos(angle_x) * actual_distance; + + fov = Clamp(fov, MIN_FOV, MAX_FOV); + + camera.position = Vector3Add(target, Vector3(x, y, z)); + camera.target = target; + camera.fovy = fov; + camera.projection = projection; } diff --git a/src/distance.cpp b/src/distance.cpp index 82bd7fe..bbdf78a 100644 --- a/src/distance.cpp +++ b/src/distance.cpp @@ -1,78 +1,72 @@ #include "distance.hpp" -#include "config.hpp" -#include #include #ifdef TRACY -#include "tracy.hpp" -#include + #include #endif -auto DistanceResult::Clear() -> void { - distances.clear(); - parents.clear(); - nearest_targets.clear(); +auto graph_distances::clear() -> void +{ + distances.clear(); + parents.clear(); + nearest_targets.clear(); } -auto DistanceResult::Empty() -> bool { - return distances.empty() || parents.empty() || nearest_targets.empty(); +auto graph_distances::empty() const -> bool +{ + return distances.empty() || parents.empty() || nearest_targets.empty(); } -auto CalculateDistances( - std::size_t node_count, - const std::vector> &edges, - const std::vector &targets) -> DistanceResult { - - // Build a list of adjacent nodes to speed up BFS - std::vector> adjacency(node_count); - for (const auto &[from, to] : edges) { - adjacency[from].push_back(to); - adjacency[to].push_back(from); - } - - std::vector distances(node_count, -1); - std::vector parents(node_count, -1); - std::vector nearest_targets(node_count, -1); - - std::queue queue; - for (std::size_t target : targets) { - distances[target] = 0; - nearest_targets[target] = target; - queue.push(target); - } - - while (!queue.empty()) { - std::size_t current = queue.front(); - queue.pop(); - - for (std::size_t neighbor : adjacency[current]) { - if (distances[neighbor] == -1) { - // If distance is -1 we haven't visited the node yet - distances[neighbor] = distances[current] + 1; - parents[neighbor] = current; - nearest_targets[neighbor] = nearest_targets[current]; - - queue.push(neighbor); - } +auto graph_distances::calculate_distances(const size_t node_count, const std::vector>& edges, + const std::vector& targets) -> void +{ + // Build a list of adjacent nodes to speed up BFS + std::vector> adjacency(node_count); + for (const auto& [from, to] : edges) { + adjacency[from].push_back(to); + adjacency[to].push_back(from); } - } - return {distances, parents, nearest_targets}; + distances = std::vector(node_count, -1); + parents = std::vector(node_count, -1); + nearest_targets = std::vector(node_count, -1); + + std::queue queue; + for (size_t target : targets) { + distances[target] = 0; + nearest_targets[target] = target; + queue.push(target); + } + + while (!queue.empty()) { + const size_t current = queue.front(); + queue.pop(); + + for (size_t neighbor : adjacency[current]) { + if (distances[neighbor] == -1) { + // If distance is -1 we haven't visited the node yet + distances[neighbor] = distances[current] + 1; + parents[neighbor] = current; + nearest_targets[neighbor] = nearest_targets[current]; + + queue.push(neighbor); + } + } + } } -auto GetPath(const DistanceResult &result, std::size_t source) - -> std::vector { - if (result.distances[source] == -1) { - // Unreachable - return {}; - } +auto graph_distances::get_shortest_path(const size_t source) const -> std::vector +{ + if (empty() || distances[source] == -1) { + // Unreachable + return {}; + } - std::vector path; - for (std::size_t n = source; n != static_cast(-1); - n = result.parents[n]) { - path.push_back(n); - } + std::vector path; + for (size_t n = source; n != static_cast(-1); n = parents[n]) { + path.push_back(n); + } - return path; + return path; } diff --git a/src/gui.cpp b/src/gui.cpp deleted file mode 100644 index b07fbed..0000000 --- a/src/gui.cpp +++ /dev/null @@ -1,804 +0,0 @@ -#include "gui.hpp" -#include "config.hpp" - -#include - -#define RAYGUI_IMPLEMENTATION -#include - -#ifdef TRACY -#include "tracy.hpp" -#include -#endif - -auto Grid::UpdateBounds(int _x, int _y, int _width, int _height, int _columns, - int _rows) -> void { - x = _x; - y = _y; - width = _width; - height = _height; - columns = _columns; - rows = _rows; -} - -auto Grid::UpdateBounds(int _x, int _y, int _width, int _height) -> void { - x = _x; - y = _y; - width = _width; - height = _height; -} - -auto Grid::UpdateBounds(int _x, int _y) -> void { - x = _x; - y = _y; -} - -auto Grid::Bounds() const -> Rectangle { - Rectangle bounds = Bounds(0, 0, columns, rows); - bounds.x -= padding; - bounds.y -= padding; - bounds.width += 2 * padding; - bounds.height += 2 * padding; - return bounds; -} - -auto Grid::Bounds(int _x, int _y, int _width, int _height) const -> Rectangle { - if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) { - std::cout << std::format("Grid bounds are outside range.") << std::endl; - exit(1); - } - - int cell_width = (width - padding) / columns; - 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); -} - -auto Grid::SquareBounds() const -> Rectangle { - Rectangle bounds = SquareBounds(0, 0, columns, rows); - bounds.x -= padding; - bounds.y -= padding; - bounds.width += 2 * padding; - bounds.height += 2 * padding; - return bounds; -} - -auto Grid::SquareBounds(int _x, int _y, int _width, int _height) const - -> Rectangle { - // Assumes each cell is square, so either width or height are not completely - // filled - - if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) { - std::cout << std::format("Grid bounds are outside range.") << std::endl; - exit(1); - } - - int available_width = width - padding * (columns + 1); - int available_height = height - padding * (rows + 1); - int cell_size = std::min(available_width / columns, available_height / rows); - - int grid_width = cell_size * columns + padding * (columns + 1); - int grid_height = cell_size * rows + padding * (rows + 1); - int x_offset = (width - grid_width) / 2; - 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)); -} - -auto Gui::Init() const -> void { - Font font = LoadFontEx(FONT, FONT_SIZE, 0, 0); - SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR); - GuiSetFont(font); - - DefaultStyle style = GetDefaultStyle(); - style.text_size = FONT_SIZE; - ApplyColor(style, GRAY); - - SetDefaultStyle(style); -} - -auto Gui::ApplyColor(Style &style, Color color) const -> void { - style.base_color_normal = ColorToInt(Fade(color, 0.8)); - style.base_color_focused = ColorToInt(Fade(color, 0.3)); - style.base_color_pressed = ColorToInt(Fade(color, 0.8)); - style.base_color_disabled = ColorToInt(Fade(color, 0.5)); - - style.border_color_normal = ColorToInt(Fade(color, 1.0)); - style.border_color_focused = ColorToInt(Fade(color, 0.7)); - style.border_color_pressed = ColorToInt(Fade(color, 1.0)); - style.border_color_disabled = ColorToInt(Fade(GRAY, 0.5)); - - style.text_color_normal = ColorToInt(Fade(BLACK, 1.0)); - style.text_color_focused = ColorToInt(Fade(BLACK, 1.0)); - style.text_color_pressed = ColorToInt(Fade(BLACK, 1.0)); - style.text_color_disabled = ColorToInt(Fade(BLACK, 0.5)); -} - -auto Gui::ApplyBlockColor(Style &style, Color color) const -> void { - style.base_color_normal = ColorToInt(Fade(color, 0.5)); - style.base_color_focused = ColorToInt(Fade(color, 0.3)); - style.base_color_pressed = ColorToInt(Fade(color, 0.8)); - style.base_color_disabled = ColorToInt(Fade(color, 0.5)); - - style.border_color_normal = ColorToInt(Fade(color, 1.0)); - style.border_color_focused = ColorToInt(Fade(color, 0.7)); - style.border_color_pressed = ColorToInt(Fade(color, 1.0)); - style.border_color_disabled = ColorToInt(Fade(GRAY, 0.5)); -} - -auto Gui::ApplyTextColor(Style &style, Color color) const -> void { - style.text_color_normal = ColorToInt(Fade(color, 1.0)); - style.text_color_focused = ColorToInt(Fade(color, 1.0)); - style.text_color_pressed = ColorToInt(Fade(color, 1.0)); - style.text_color_disabled = ColorToInt(Fade(BLACK, 0.5)); -} - -auto Gui::GetDefaultStyle() const -> DefaultStyle { - // Could've iterated over the values but then it wouldn't be as nice to - // access... - return {{GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL), - GuiGetStyle(DEFAULT, BASE_COLOR_NORMAL), - GuiGetStyle(DEFAULT, TEXT_COLOR_NORMAL), - - GuiGetStyle(DEFAULT, BORDER_COLOR_FOCUSED), - GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED), - GuiGetStyle(DEFAULT, TEXT_COLOR_FOCUSED), - - GuiGetStyle(DEFAULT, BORDER_COLOR_PRESSED), - GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED), - GuiGetStyle(DEFAULT, TEXT_COLOR_PRESSED), - - GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED), - GuiGetStyle(DEFAULT, BASE_COLOR_DISABLED), - GuiGetStyle(DEFAULT, TEXT_COLOR_DISABLED)}, - - GuiGetStyle(DEFAULT, BACKGROUND_COLOR), - GuiGetStyle(DEFAULT, LINE_COLOR), - - GuiGetStyle(DEFAULT, TEXT_SIZE), - GuiGetStyle(DEFAULT, TEXT_SPACING), - GuiGetStyle(DEFAULT, TEXT_LINE_SPACING), - GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL), - GuiGetStyle(DEFAULT, TEXT_WRAP_MODE)}; -} - -auto Gui::SetDefaultStyle(const DefaultStyle &style) const -> void { - GuiSetStyle(DEFAULT, BORDER_COLOR_NORMAL, style.border_color_normal); - GuiSetStyle(DEFAULT, BASE_COLOR_NORMAL, style.base_color_normal); - GuiSetStyle(DEFAULT, TEXT_COLOR_NORMAL, style.text_color_normal); - - GuiSetStyle(DEFAULT, BORDER_COLOR_FOCUSED, style.border_color_focused); - GuiSetStyle(DEFAULT, BASE_COLOR_FOCUSED, style.base_color_focused); - GuiSetStyle(DEFAULT, TEXT_COLOR_FOCUSED, style.text_color_focused); - - GuiSetStyle(DEFAULT, BORDER_COLOR_PRESSED, style.border_color_pressed); - GuiSetStyle(DEFAULT, BASE_COLOR_PRESSED, style.base_color_pressed); - GuiSetStyle(DEFAULT, TEXT_COLOR_PRESSED, style.text_color_pressed); - - GuiSetStyle(DEFAULT, BORDER_COLOR_DISABLED, style.border_color_disabled); - GuiSetStyle(DEFAULT, BASE_COLOR_DISABLED, style.base_color_disabled); - GuiSetStyle(DEFAULT, TEXT_COLOR_DISABLED, style.text_color_disabled); - - GuiSetStyle(DEFAULT, BACKGROUND_COLOR, style.background_color); - GuiSetStyle(DEFAULT, LINE_COLOR, style.line_color); - - GuiSetStyle(DEFAULT, TEXT_SIZE, style.text_size); - GuiSetStyle(DEFAULT, TEXT_SPACING, style.text_spacing); - GuiSetStyle(DEFAULT, TEXT_LINE_SPACING, style.text_line_spacing); - GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, style.text_alignment_vertical); - GuiSetStyle(DEFAULT, TEXT_WRAP_MODE, style.text_wrap_mode); -} - -auto Gui::GetComponentStyle(int component) const -> ComponentStyle { - 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 Gui::SetComponentStyle(int component, const ComponentStyle &style) const - -> void { - GuiSetStyle(component, BORDER_COLOR_NORMAL, style.border_color_normal); - GuiSetStyle(component, BASE_COLOR_NORMAL, style.base_color_normal); - GuiSetStyle(component, TEXT_COLOR_NORMAL, style.text_color_normal); - - GuiSetStyle(component, BORDER_COLOR_FOCUSED, style.border_color_focused); - GuiSetStyle(component, BASE_COLOR_FOCUSED, style.base_color_focused); - GuiSetStyle(component, TEXT_COLOR_FOCUSED, style.text_color_focused); - - GuiSetStyle(component, BORDER_COLOR_PRESSED, style.border_color_pressed); - GuiSetStyle(component, BASE_COLOR_PRESSED, style.base_color_pressed); - GuiSetStyle(component, TEXT_COLOR_PRESSED, style.text_color_pressed); - - GuiSetStyle(component, BORDER_COLOR_DISABLED, style.border_color_disabled); - GuiSetStyle(component, BASE_COLOR_DISABLED, style.base_color_disabled); - GuiSetStyle(component, TEXT_COLOR_DISABLED, style.text_color_disabled); - - GuiSetStyle(component, BORDER_WIDTH, style.border_width); - GuiSetStyle(component, TEXT_PADDING, style.text_padding); - GuiSetStyle(component, TEXT_ALIGNMENT, style.text_alignment); -} - -auto Gui::DrawButton(Rectangle bounds, const std::string &label, Color color, - bool enabled, int font_size) const -> int { - // Save original styling - const DefaultStyle original_default = GetDefaultStyle(); - const ComponentStyle original_button = GetComponentStyle(BUTTON); - - // Change styling - DefaultStyle style_default = original_default; - ComponentStyle style_button = original_button; - style_default.text_size = font_size; - ApplyColor(style_button, color); - SetDefaultStyle(style_default); - SetComponentStyle(BUTTON, style_button); - - const int _state = GuiGetState(); - if (!enabled || WindowOpen()) { - GuiSetState(STATE_DISABLED); - } - int pressed = GuiButton(bounds, label.data()); - if (!enabled || WindowOpen()) { - GuiSetState(_state); - } - - // Restore original styling - SetDefaultStyle(original_default); - SetComponentStyle(BUTTON, original_button); - - return pressed; -} - -auto Gui::DrawMenuButton(int x, int y, int width, int height, - const std::string &label, Color color, bool enabled, - int font_size) const -> int { - Rectangle bounds = menu_grid.Bounds(x, y, width, height); - return DrawButton(bounds, label, color, enabled, font_size); -} - -auto Gui::DrawToggleSlider(Rectangle bounds, const std::string &off_label, - const std::string &on_label, int *active, - Color color, bool enabled, int font_size) const - -> int { - // Save original styling - const DefaultStyle original_default = GetDefaultStyle(); - const ComponentStyle original_slider = GetComponentStyle(SLIDER); - const ComponentStyle original_toggle = GetComponentStyle(TOGGLE); - - // Change styling - DefaultStyle style_default = original_default; - ComponentStyle style_slider = original_slider; - ComponentStyle style_toggle = original_toggle; - style_default.text_size = font_size; - ApplyColor(style_slider, color); - ApplyColor(style_toggle, color); - SetDefaultStyle(style_default); - SetComponentStyle(SLIDER, style_slider); - SetComponentStyle(TOGGLE, style_toggle); - - const int _state = GuiGetState(); - if (!enabled || WindowOpen()) { - GuiSetState(STATE_DISABLED); - } - int pressed = GuiToggleSlider( - bounds, std::format("{};{}", off_label, on_label).data(), active); - if (!enabled || WindowOpen()) { - GuiSetState(_state); - } - - // Restore original styling - SetDefaultStyle(original_default); - SetComponentStyle(SLIDER, original_slider); - SetComponentStyle(TOGGLE, original_toggle); - - return pressed; -} - -auto Gui::DrawMenuToggleSlider(int x, int y, int width, int height, - const std::string &off_label, - const std::string &on_label, int *active, - Color color, bool enabled, int font_size) const - -> int { - Rectangle bounds = menu_grid.Bounds(x, y, width, height); - return DrawToggleSlider(bounds, off_label, on_label, active, color, enabled, - font_size); -}; - -auto Gui::DrawSpinner(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 DefaultStyle original_default = GetDefaultStyle(); - const ComponentStyle original_valuebox = GetComponentStyle(VALUEBOX); - const ComponentStyle original_button = GetComponentStyle(BUTTON); - - // Change styling - DefaultStyle style_default = original_default; - ComponentStyle style_valuebox = original_valuebox; - ComponentStyle style_button = original_button; - style_default.text_size = font_size; - ApplyColor(style_valuebox, color); - ApplyColor(style_button, color); - SetDefaultStyle(style_default); - SetComponentStyle(VALUEBOX, style_valuebox); - SetComponentStyle(BUTTON, style_button); - - const int _state = GuiGetState(); - if (!enabled || WindowOpen()) { - GuiSetState(STATE_DISABLED); - } - int pressed = GuiSpinner(bounds, "", label.data(), value, min, max, false); - if (!enabled || WindowOpen()) { - GuiSetState(_state); - } - - // Restore original styling - SetDefaultStyle(original_default); - SetComponentStyle(VALUEBOX, original_valuebox); - SetComponentStyle(BUTTON, style_button); - - return pressed; -} - -auto Gui::DrawMenuSpinner(int x, int y, int width, int height, - const std::string &label, int *value, int min, - int max, Color color, bool enabled, - int font_size) const -> int { - Rectangle bounds = menu_grid.Bounds(x, y, width, height); - return DrawSpinner(bounds, label, value, min, max, color, enabled, font_size); -} - -auto Gui::DrawLabel(Rectangle bounds, const std::string &text, Color color, - bool enabled, int font_size) const -> int { - // Save original styling - const DefaultStyle original_default = GetDefaultStyle(); - const ComponentStyle original_label = GetComponentStyle(LABEL); - - // Change styling - DefaultStyle style_default = original_default; - ComponentStyle style_label = original_label; - style_default.text_size = font_size; - ApplyTextColor(style_label, color); - SetDefaultStyle(style_default); - SetComponentStyle(LABEL, style_label); - - const int _state = GuiGetState(); - if (!enabled || WindowOpen()) { - GuiSetState(STATE_DISABLED); - } - int pressed = GuiLabel(bounds, text.data()); - if (!enabled || WindowOpen()) { - GuiSetState(_state); - } - - // Restore original styling - SetDefaultStyle(original_default); - SetComponentStyle(LABEL, original_label); - - return pressed; -} - -auto Gui::DrawBoardBlock(int x, int y, int width, int height, Color color, - bool enabled) const -> bool { - ComponentStyle style = GetComponentStyle(BUTTON); - ApplyBlockColor(style, color); - - Rectangle bounds = board_grid.SquareBounds(x, y, width, height); - - bool focused = - CheckCollisionPointRec(input.mouse - Vector2(0, MENU_HEIGHT), bounds); - bool pressed = - Block(x, y, width, height, false).Covers(input.sel_x, input.sel_y); - - // Background to make faded colors work - DrawRectangleRec(bounds, RAYWHITE); - - Color base = GetColor(style.base_color_normal); - Color border = GetColor(style.base_color_normal); - if (pressed) { - base = GetColor(style.base_color_pressed); - border = GetColor(style.base_color_pressed); - } - if (focused) { - base = GetColor(style.base_color_focused); - border = GetColor(style.base_color_focused); - } - if (focused && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { - base = GetColor(style.base_color_pressed); - border = GetColor(style.base_color_pressed); - } - if (!enabled) { - base = BOARD_COLOR_RESTRICTED; - } - DrawRectangleRec(bounds, base); - if (enabled) { - DrawRectangleLinesEx(bounds, 2.0, border); - } - - return focused && enabled; -} - -auto Gui::WindowOpen() const -> bool { return save_window || help_window; } - -auto Gui::DrawMenuHeader(Color color) const -> void { - int preset = state.current_preset; - DrawMenuSpinner(0, 0, 1, 1, "Preset: ", &preset, -1, state.presets.size(), - color, !input.editing); - if (preset > state.current_preset) { - input.NextPreset(); - } else if (preset < state.current_preset) { - input.PreviousPreset(); - } - - DrawMenuButton(1, 0, 1, 1, - std::format("Puzzle: \"{}\"", - state.comments.at(state.current_preset).substr(2)), - color); - - int editing = input.editing; - DrawMenuToggleSlider(2, 0, 1, 1, "Puzzle Mode (Tab)", "Edit Mode (Tab)", - &editing, color); - if (editing != input.editing) { - input.ToggleEditing(); - } -} - -auto Gui::DrawGraphInfo(Color color) const -> void { - DrawMenuButton(0, 1, 1, 1, - std::format("Found {} States ({} Winning)", - state.states.size(), state.winning_states.size()), - color); - - DrawMenuButton(1, 1, 1, 1, - std::format("Found {} Transitions", state.springs.size()), - color); - - DrawMenuButton(2, 1, 1, 1, - std::format("{} Moves to Nearest Solution", - state.winning_path.size() > 0 - ? state.winning_path.size() - 1 - : 0), - color); -} - -auto Gui::DrawGraphControls(Color color) const -> void { - if (DrawMenuButton(0, 2, 1, 1, "Populate Graph (G)", color)) { - input.FillGraph(); - } - - // int mark_path = input.mark_path; - // DrawMenuToggleSlider(2, 2, 1, 1, "Path Hidden (U)", "Path Shown (U)", - // &mark_path, color); - // if (mark_path != input.mark_path) { - // input.ToggleMarkPath(); - // } - - if (DrawMenuButton(1, 2, 1, 1, "Clear Graph (C)", color)) { - input.ClearGraph(); - } - - int mark_solutions = input.mark_solutions; - DrawMenuToggleSlider(2, 2, 1, 1, "Solution Hidden (I)", "Solution Shown (I)", - &mark_solutions, color); - if (mark_solutions != input.mark_solutions) { - input.ToggleMarkSolutions(); - } - input.mark_path = input.mark_solutions; -} - -auto Gui::DrawCameraControls(Color color) const -> void { - int lock_camera = input.camera_lock; - DrawMenuToggleSlider(0, 3, 1, 1, "Free Camera (L)", "Locked Camera (L)", - &lock_camera, color); - if (lock_camera != input.camera_lock) { - input.ToggleCameraLock(); - } - - int lock_camera_mass_center = input.camera_mass_center_lock; - DrawMenuToggleSlider(1, 3, 1, 1, "Current Block (Y)", "Graph Center (Y)", - &lock_camera_mass_center, color, input.camera_lock); - if (lock_camera_mass_center != input.camera_mass_center_lock) { - input.ToggleCameraMassCenterLock(); - } - - int projection = camera.projection == CAMERA_ORTHOGRAPHIC; - DrawMenuToggleSlider(2, 3, 1, 1, "Perspective (Alt)", "Orthographic (Alt)", - &projection, color); - if (projection != (camera.projection == CAMERA_ORTHOGRAPHIC)) { - input.ToggleCameraProjection(); - } -} - -auto Gui::DrawPuzzleControls(Color color) const -> void { - auto nth = [&](int n) { - if (n == 11 || n == 12 || n == 13) - return "th"; - if (n % 10 == 1) - return "st"; - if (n % 10 == 2) - return "nd"; - if (n % 10 == 3) - return "rd"; - return "th"; - }; - int visits = state.visited_states.at(state.current_state); - DrawMenuButton(0, 4, 1, 1, - std::format("{} Moves ({}{} Time at this State)", - state.total_moves, visits, nth(visits)), - color); - - if (DrawMenuButton(1, 4, 1, 1, "Make Optimal Move (Space)", color, - !state.target_distances.Empty())) { - input.MakeOptimalMove(); - } - - if (DrawMenuButton(2, 4, 1, 1, "Undo Last Move (Backspace)", color, - state.history.size() > 0)) { - input.UndoLastMove(); - } - - if (DrawMenuButton(0, 5, 1, 1, "Go to Nearest Solution (B)", color, - !state.target_distances.Empty())) { - input.GoToNearestTarget(); - } - - if (DrawMenuButton(1, 5, 1, 1, "Go to Worst State (V)", color, - !state.target_distances.Empty())) { - input.GoToWorstState(); - } - - if (DrawMenuButton(2, 5, 1, 1, "Go to Starting State (R)", color)) { - input.ResetState(); - } -} - -auto Gui::DrawEditControls(Color color) const -> void { - // Toggle Target Block - if (DrawMenuButton(0, 4, 1, 1, "Toggle Target Block (T)", color)) { - input.ToggleTargetBlock(); - } - - // Toggle Wall Block - if (DrawMenuButton(0, 5, 1, 1, "Toggle Wall Block (Y)", color)) { - input.ToggleWallBlock(); - } - - // Toggle Restricted/Free Block Movement - int free = !state.current_state.restricted; - DrawMenuToggleSlider(1, 4, 1, 1, "Restricted (F)", "Free (F)", &free, color); - if (free != !state.current_state.restricted) { - input.ToggleRestrictedMovement(); - } - - // Clear Goal - if (DrawMenuButton(1, 5, 1, 1, "Clear Goal (X)", color)) { - } - - // Column Count Spinner - int columns = state.current_state.width; - DrawMenuSpinner(2, 4, 1, 1, "Cols: ", &columns, 1, 9, color); - if (columns > state.current_state.width) { - input.AddBoardColumn(); - } else if (columns < state.current_state.width) { - input.RemoveBoardColumn(); - } - - // Row Count Spinner - int rows = state.current_state.height; - DrawMenuSpinner(2, 5, 1, 1, "Rows: ", &rows, 1, 9, color); - if (rows > state.current_state.height) { - input.AddBoardRow(); - } else if (rows < state.current_state.height) { - input.RemoveBoardRow(); - } -} - -auto Gui::DrawMenuFooter(Color color) -> void { - DrawMenuButton(0, 6, 2, 1, - std::format("State: \"{}\"", state.current_state.state), - color); - - if (DrawMenuButton(2, 6, 1, 1, "Save as Preset", color)) { - save_window = true; - } -} - -auto Gui::GetBackgroundColor() const -> Color { - return GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)); -} - -auto Gui::HelpPopup() -> void {} - -auto Gui::DrawSavePresetPopup() -> void { - if (!save_window) { - return; - } - - int width = 450; - int height = 150; - // Returns the pressed button index - int button = GuiTextInputBox(Rectangle((GetScreenWidth() - width) / 2.0, - (GetScreenHeight() - height) / 2.0, - width, height), - "Save as Preset", "Enter Preset Name", - "Ok;Cancel", preset_name.data(), 255, NULL); - if (button == 1) { - state.AppendPresetFile(preset_name.data()); - } - if ((button == 0) || (button == 1) || (button == 2)) { - save_window = false; - TextCopy(preset_name.data(), "\0"); - } -} - -auto Gui::DrawMainMenu() -> void { - menu_grid.UpdateBounds(0, 0, GetScreenWidth(), MENU_HEIGHT); - - DrawMenuHeader(GRAY); - DrawGraphInfo(ORANGE); - DrawGraphControls(RED); - DrawCameraControls(DARKGREEN); - - if (input.editing) { - DrawEditControls(PURPLE); - } else { - DrawPuzzleControls(BLUE); - } - - DrawMenuFooter(GRAY); -} - -auto Gui::DrawPuzzleBoard() -> void { - board_grid.UpdateBounds( - 0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, - state.current_state.width, state.current_state.height); - - // Draw outer border - Rectangle bounds = board_grid.SquareBounds(); - DrawRectangleRec(bounds, state.current_state.IsWon() - ? 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, - state.current_state.restricted ? BOARD_COLOR_RESTRICTED - : BOARD_COLOR_FREE); - - // Draw target opening - if (state.current_state.HasWinCondition()) { - int target_x = state.current_state.target_x; - int target_y = state.current_state.target_y; - Block target_block = state.current_state.GetTargetBlock(); - Rectangle target_bounds = board_grid.SquareBounds( - target_x, target_y, target_block.width, target_block.height); - - Color opening_color = Fade( - state.current_state.IsWon() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, - 0.3); - - if (target_x == 0) { - - // Left opening - DrawRectangle(target_bounds.x - BOARD_PADDING, target_bounds.y, - BOARD_PADDING, target_bounds.height, RAYWHITE); - DrawRectangle(target_bounds.x - BOARD_PADDING, target_bounds.y, - BOARD_PADDING, target_bounds.height, opening_color); - } - if (target_x + target_block.width == state.current_state.width) { - - // Right opening - DrawRectangle(target_bounds.x + target_bounds.width, target_bounds.y, - BOARD_PADDING, target_bounds.height, RAYWHITE); - DrawRectangle(target_bounds.x + target_bounds.width, target_bounds.y, - BOARD_PADDING, target_bounds.height, opening_color); - } - if (target_y == 0) { - - // Top opening - DrawRectangle(target_bounds.x, target_bounds.y - BOARD_PADDING, - target_bounds.width, BOARD_PADDING, RAYWHITE); - DrawRectangle(target_bounds.x, target_bounds.y - BOARD_PADDING, - target_bounds.width, BOARD_PADDING, opening_color); - } - if (target_y + target_block.height == state.current_state.height) { - - // Bottom opening - DrawRectangle(target_bounds.x, target_bounds.y + target_bounds.height, - target_bounds.width, BOARD_PADDING, RAYWHITE); - DrawRectangle(target_bounds.x, target_bounds.y + target_bounds.height, - target_bounds.width, BOARD_PADDING, opening_color); - } - } - - // Draw empty cells. Also set hovered blocks - input.hov_x = -1; - input.hov_y = -1; - for (int x = 0; x < board_grid.columns; ++x) { - for (int y = 0; y < board_grid.rows; ++y) { - DrawRectangleRec(board_grid.SquareBounds(x, y, 1, 1), RAYWHITE); - - Rectangle hov_bounds = board_grid.SquareBounds(x, y, 1, 1); - hov_bounds.x -= BOARD_PADDING; - hov_bounds.y -= BOARD_PADDING; - hov_bounds.width += BOARD_PADDING; - hov_bounds.height += BOARD_PADDING; - if (CheckCollisionPointRec(GetMousePosition() - Vector2(0, MENU_HEIGHT), - hov_bounds)) { - input.hov_x = x; - input.hov_y = y; - } - } - } - - // Draw blocks - for (const Block &block : state.current_state) { - Color c = BLOCK_COLOR; - if (block.target) { - c = TARGET_BLOCK_COLOR; - } else if (block.immovable) { - c = WALL_COLOR; - } - - DrawBoardBlock(block.x, block.y, block.width, block.height, c, - !block.immovable); - } - - // Draw block placing - if (input.editing && input.has_block_add_xy) { - if (input.hov_x >= 0 && input.hov_x < state.current_state.width && - input.hov_y >= 0 && input.hov_y < state.current_state.height && - input.hov_x >= input.block_add_x && input.hov_y >= input.block_add_y) { - bool collides = false; - for (const Block &block : state.current_state) { - if (block.Collides(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) { - DrawBoardBlock(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, false); - } - } - } -} - -auto Gui::DrawGraphOverlay(int fps, int ups) -> void { - graph_overlay_grid.UpdateBounds(GetScreenWidth() / 2, MENU_HEIGHT); - - DrawLabel(graph_overlay_grid.Bounds(0, 0, 1, 1), - std::format("Dist: {:0>7.2f}", camera.distance), BLACK); - DrawLabel(graph_overlay_grid.Bounds(0, 1, 1, 1), - std::format("FoV: {:0>6.2f}", camera.fov), BLACK); - DrawLabel(graph_overlay_grid.Bounds(0, 2, 1, 1), - std::format("FPS: {:0>3}", fps), LIME); - DrawLabel(graph_overlay_grid.Bounds(0, 3, 1, 1), - std::format("UPS: {:0>3}", ups), ORANGE); -} - -auto Gui::Update() -> void { input.disable = WindowOpen(); } diff --git a/src/input.cpp b/src/input.cpp index da5abe1..dacf119 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -3,494 +3,568 @@ #include -#ifdef TRACY -#include "tracy.hpp" -#include -#endif +auto input_handler::init_handlers() -> void +{ + // The order matters if multiple handlers are registered to the same key -auto InputHandler::InitHandlers() -> void { - // The order matters if multiple handlers are registered to the same key + register_generic_handler(&input_handler::camera_pan); + register_generic_handler(&input_handler::camera_rotate); + register_generic_handler(&input_handler::camera_zoom); + register_generic_handler(&input_handler::camera_fov); + register_generic_handler(&input_handler::mouse_hover); - RegisterGenericHandler(&InputHandler::CameraPan); - RegisterGenericHandler(&InputHandler::CameraRotate); - RegisterGenericHandler(&InputHandler::CameraZoom); - RegisterGenericHandler(&InputHandler::CameraFov); - RegisterGenericHandler(&InputHandler::MouseHover); + register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::camera_start_pan); + register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::select_block); + register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::add_block); + register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::start_add_block); + register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::camera_start_rotate); + register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::remove_block); + register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::clear_add_block); + register_mouse_pressed_handler(MOUSE_BUTTON_MIDDLE, &input_handler::place_goal); - RegisterMousePressedHandler(MOUSE_BUTTON_LEFT, &InputHandler::CameraStartPan); - RegisterMousePressedHandler(MOUSE_BUTTON_LEFT, &InputHandler::SelectBlock); - RegisterMousePressedHandler(MOUSE_BUTTON_LEFT, &InputHandler::AddBlock); - RegisterMousePressedHandler(MOUSE_BUTTON_LEFT, &InputHandler::StartAddBlock); - RegisterMousePressedHandler(MOUSE_BUTTON_RIGHT, - &InputHandler::CameraStartRotate); - RegisterMousePressedHandler(MOUSE_BUTTON_RIGHT, &InputHandler::RemoveBlock); - RegisterMousePressedHandler(MOUSE_BUTTON_RIGHT, &InputHandler::ClearAddBlock); - RegisterMousePressedHandler(MOUSE_BUTTON_MIDDLE, &InputHandler::PlaceGoal); + register_mouse_released_handler(MOUSE_BUTTON_LEFT, &input_handler::camera_stop_pan); + register_mouse_released_handler(MOUSE_BUTTON_RIGHT, &input_handler::camera_stop_rotate); - RegisterMouseReleasedHandler(MOUSE_BUTTON_LEFT, &InputHandler::CameraStopPan); - RegisterMouseReleasedHandler(MOUSE_BUTTON_RIGHT, - &InputHandler::CameraStopRotate); - - RegisterKeyPressedHandler(KEY_W, &InputHandler::MoveBlockNor); - RegisterKeyPressedHandler(KEY_D, &InputHandler::MoveBlockEas); - RegisterKeyPressedHandler(KEY_S, &InputHandler::MoveBlockSou); - RegisterKeyPressedHandler(KEY_A, &InputHandler::MoveBlockWes); - RegisterKeyPressedHandler(KEY_P, &InputHandler::PrintState); - RegisterKeyPressedHandler(KEY_N, &InputHandler::PreviousPreset); - RegisterKeyPressedHandler(KEY_M, &InputHandler::NextPreset); - RegisterKeyPressedHandler(KEY_R, &InputHandler::ResetState); - RegisterKeyPressedHandler(KEY_G, &InputHandler::FillGraph); - RegisterKeyPressedHandler(KEY_C, &InputHandler::ClearGraph); - RegisterKeyPressedHandler(KEY_I, &InputHandler::ToggleMarkSolutions); - RegisterKeyPressedHandler(KEY_O, &InputHandler::ToggleConnectSolutions); - // RegisterKeyPressedHandler(KEY_U, &InputHandler::ToggleMarkPath); - RegisterKeyPressedHandler(KEY_SPACE, &InputHandler::MakeOptimalMove); - RegisterKeyPressedHandler(KEY_V, &InputHandler::GoToWorstState); - RegisterKeyPressedHandler(KEY_B, &InputHandler::GoToNearestTarget); - RegisterKeyPressedHandler(KEY_BACKSPACE, &InputHandler::UndoLastMove); - RegisterKeyPressedHandler(KEY_F, &InputHandler::ToggleRestrictedMovement); - RegisterKeyPressedHandler(KEY_T, &InputHandler::ToggleTargetBlock); - RegisterKeyPressedHandler(KEY_Y, &InputHandler::ToggleWallBlock); - RegisterKeyPressedHandler(KEY_UP, &InputHandler::AddBoardRow); - RegisterKeyPressedHandler(KEY_RIGHT, &InputHandler::AddBoardColumn); - RegisterKeyPressedHandler(KEY_DOWN, &InputHandler::RemoveBoardRow); - RegisterKeyPressedHandler(KEY_LEFT, &InputHandler::RemoveBoardColumn); - RegisterKeyPressedHandler(KEY_TAB, &InputHandler::ToggleEditing); - RegisterKeyPressedHandler(KEY_L, &InputHandler::ToggleCameraLock); - RegisterKeyPressedHandler(KEY_LEFT_ALT, - &InputHandler::ToggleCameraProjection); - RegisterKeyPressedHandler(KEY_X, &InputHandler::ClearGoal); - RegisterKeyPressedHandler(KEY_Y, &InputHandler::ToggleCameraMassCenterLock); + register_key_pressed_handler(KEY_W, &input_handler::move_block_nor); + register_key_pressed_handler(KEY_D, &input_handler::move_block_eas); + register_key_pressed_handler(KEY_S, &input_handler::move_block_sou); + register_key_pressed_handler(KEY_A, &input_handler::move_block_wes); + register_key_pressed_handler(KEY_P, &input_handler::print_state); + register_key_pressed_handler(KEY_N, &input_handler::load_previous_preset); + register_key_pressed_handler(KEY_M, &input_handler::load_next_preset); + register_key_pressed_handler(KEY_R, &input_handler::goto_starting_state); + register_key_pressed_handler(KEY_G, &input_handler::populate_graph); + register_key_pressed_handler(KEY_C, &input_handler::clear_graph); + register_key_pressed_handler(KEY_I, &input_handler::toggle_mark_solutions); + register_key_pressed_handler(KEY_O, &input_handler::toggle_connect_solutions); + // RegisterKeyPressedHandler(KEY_U, &InputHandler::ToggleMarkPath); + register_key_pressed_handler(KEY_SPACE, &input_handler::goto_optimal_next_state); + register_key_pressed_handler(KEY_V, &input_handler::goto_most_distant_state); + register_key_pressed_handler(KEY_B, &input_handler::goto_closest_target_state); + register_key_pressed_handler(KEY_BACKSPACE, &input_handler::goto_previous_state); + register_key_pressed_handler(KEY_F, &input_handler::toggle_restricted_movement); + register_key_pressed_handler(KEY_T, &input_handler::toggle_target_block); + register_key_pressed_handler(KEY_Y, &input_handler::toggle_wall_block); + register_key_pressed_handler(KEY_UP, &input_handler::add_board_row); + register_key_pressed_handler(KEY_RIGHT, &input_handler::add_board_column); + register_key_pressed_handler(KEY_DOWN, &input_handler::remove_board_row); + register_key_pressed_handler(KEY_LEFT, &input_handler::remove_board_column); + register_key_pressed_handler(KEY_TAB, &input_handler::toggle_editing); + register_key_pressed_handler(KEY_L, &input_handler::toggle_camera_lock); + register_key_pressed_handler(KEY_LEFT_ALT, &input_handler::toggle_camera_projection); + register_key_pressed_handler(KEY_X, &input_handler::clear_goal); + register_key_pressed_handler(KEY_U, &input_handler::toggle_camera_mass_center_lock); } -auto InputHandler::MouseInMenuPane() -> bool { return mouse.y < MENU_HEIGHT; } - -auto InputHandler::MouseInBoardPane() -> bool { - return mouse.x < GetScreenWidth() / 2.0 && mouse.y >= MENU_HEIGHT; +auto input_handler::mouse_in_menu_pane() const -> bool +{ + return mouse.y < MENU_HEIGHT; } -auto InputHandler::MouseInGraphPane() -> bool { - return mouse.x >= GetScreenWidth() / 2.0 && mouse.y >= MENU_HEIGHT; +auto input_handler::mouse_in_board_pane() const -> bool +{ + return mouse.x < GetScreenWidth() / 2.0 && mouse.y >= MENU_HEIGHT; } -auto InputHandler::MouseHover() -> void { - last_mouse = mouse; - mouse = GetMousePosition(); +auto input_handler::mouse_in_graph_pane() const -> bool +{ + return mouse.x >= GetScreenWidth() / 2.0 && mouse.y >= MENU_HEIGHT; } -auto InputHandler::CameraStartPan() -> void { - if (!MouseInGraphPane()) { - return; - } - - camera_panning = true; - // Enable this if the camera should be pannable even when locked (releasing - // the lock in the process): - // camera_lock = false; +auto input_handler::mouse_hover() -> void +{ + last_mouse = mouse; + mouse = GetMousePosition(); } -auto InputHandler::CameraPan() -> void { - if (camera_panning) { - camera.Pan(last_mouse, mouse); - } +auto input_handler::camera_start_pan() -> void +{ + if (!mouse_in_graph_pane()) { + return; + } + + camera_panning = true; + // Enable this if the camera should be pannable even when locked (releasing the lock in the process): + // camera_lock = false; } -auto InputHandler::CameraStopPan() -> void { camera_panning = false; } - -auto InputHandler::CameraStartRotate() -> void { - if (!MouseInGraphPane()) { - return; - } - - camera_rotating = true; +auto input_handler::camera_pan() const -> void +{ + if (camera_panning) { + camera.pan(last_mouse, mouse); + } } -auto InputHandler::CameraRotate() -> void { - if (camera_rotating) { - camera.Rotate(last_mouse, mouse); - } -} - -auto InputHandler::CameraStopRotate() -> void { camera_rotating = false; } - -auto InputHandler::CameraZoom() -> void { - if (!MouseInGraphPane() || IsKeyDown(KEY_LEFT_CONTROL) || - camera.projection == CAMERA_ORTHOGRAPHIC) { - return; - } - - float wheel = GetMouseWheelMove(); - - if (IsKeyDown(KEY_LEFT_SHIFT)) { - camera.distance -= wheel * ZOOM_SPEED * ZOOM_MULTIPLIER; - } else { - camera.distance -= wheel * ZOOM_SPEED; - } -} - -auto InputHandler::CameraFov() -> void { - if (!MouseInGraphPane() || !IsKeyDown(KEY_LEFT_CONTROL) || - IsKeyDown(KEY_LEFT_SHIFT)) { - return; - } - - float wheel = GetMouseWheelMove(); - camera.fov -= wheel * FOV_SPEED; -} - -auto InputHandler::ToggleCameraLock() -> void { - if (!camera_lock) { +auto input_handler::camera_stop_pan() -> void +{ camera_panning = false; - } - - camera_lock = !camera_lock; } -auto InputHandler::ToggleCameraMassCenterLock() -> void { - if (!camera_mass_center_lock) { - camera_lock = true; - camera_panning = false; - } +auto input_handler::camera_start_rotate() -> void +{ + if (!mouse_in_graph_pane()) { + return; + } - camera_mass_center_lock = !camera_mass_center_lock; + camera_rotating = true; } -auto InputHandler::ToggleCameraProjection() -> void { - camera.projection = camera.projection == CAMERA_PERSPECTIVE - ? CAMERA_ORTHOGRAPHIC - : CAMERA_PERSPECTIVE; +auto input_handler::camera_rotate() const -> void +{ + if (camera_rotating) { + camera.rotate(last_mouse, mouse); + } } -auto InputHandler::SelectBlock() -> void { - if (state.current_state.GetBlock(hov_x, hov_y).IsValid()) { - sel_x = hov_x; - sel_y = hov_y; - } +auto input_handler::camera_stop_rotate() -> void +{ + camera_rotating = false; } -auto InputHandler::StartAddBlock() -> void { - if (!editing || state.current_state.GetBlock(hov_x, hov_y).IsValid() || - has_block_add_xy) { - return; - } +auto input_handler::camera_zoom() const -> void +{ + if (!mouse_in_graph_pane() || IsKeyDown(KEY_LEFT_CONTROL) || camera.projection == CAMERA_ORTHOGRAPHIC) { + return; + } - if (hov_x >= 0 && hov_x < state.current_state.width && hov_y >= 0 && - hov_y < state.current_state.height) { - block_add_x = hov_x; - block_add_y = hov_y; - has_block_add_xy = true; - } + const float wheel = GetMouseWheelMove(); + + if (IsKeyDown(KEY_LEFT_SHIFT)) { + camera.distance -= wheel * ZOOM_SPEED * ZOOM_MULTIPLIER; + } else { + camera.distance -= wheel * ZOOM_SPEED; + } } -auto InputHandler::ClearAddBlock() -> void { - if (!editing || !has_block_add_xy) { - return; - } +auto input_handler::camera_fov() const -> void +{ + if (!mouse_in_graph_pane() || !IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_LEFT_SHIFT)) { + return; + } - block_add_x = -1; - block_add_y = -1; - has_block_add_xy = false; + const float wheel = GetMouseWheelMove(); + camera.fov -= wheel * FOV_SPEED; } -auto InputHandler::AddBlock() -> void { - if (!editing || state.current_state.GetBlock(hov_x, hov_y).IsValid() || - !has_block_add_xy) { - return; - } +auto input_handler::select_block() -> void +{ + const puzzle& current = state.get_current_state(); + if (current.try_get_block(hov_x, hov_y)) { + sel_x = hov_x; + sel_y = hov_y; + } +} + +auto input_handler::start_add_block() -> void +{ + const puzzle& current = state.get_current_state(); + if (!editing || current.try_get_block(hov_x, hov_y) || has_block_add_xy) { + return; + } + + if (hov_x >= 0 && hov_x < current.width && hov_y >= 0 && hov_y < current.height) { + block_add_x = hov_x; + block_add_y = hov_y; + has_block_add_xy = true; + } +} + +auto input_handler::clear_add_block() -> void +{ + if (!editing || !has_block_add_xy) { + return; + } - int block_add_width = hov_x - block_add_x + 1; - int block_add_height = hov_y - block_add_y + 1; - if (block_add_width <= 0 || block_add_height <= 0) { block_add_x = -1; block_add_y = -1; has_block_add_xy = false; - } else if (block_add_x >= 0 && - block_add_x + block_add_width <= state.current_state.width && - block_add_y >= 0 && - block_add_y + block_add_height <= state.current_state.height) { - bool success = state.current_state.AddBlock(Block( - block_add_x, block_add_y, block_add_width, block_add_height, false)); - - if (success) { - sel_x = block_add_x; - sel_y = block_add_y; - block_add_x = -1; - block_add_y = -1; - has_block_add_xy = false; - state.ClearGraph(); - state.edited = true; - } - } } -auto InputHandler::RemoveBlock() -> void { - Block block = state.current_state.GetBlock(hov_x, hov_y); - if (!editing || has_block_add_xy || !block.IsValid() || - !state.current_state.RemoveBlock(hov_x, hov_y)) { - return; - } +auto input_handler::add_block() -> void +{ + const puzzle& current = state.get_current_state(); + if (!editing || current.try_get_block(hov_x, hov_y) || !has_block_add_xy) { + return; + } - if (block.Covers(sel_x, sel_y)) { + const int block_add_width = hov_x - block_add_x + 1; + const int block_add_height = hov_y - block_add_y + 1; + if (block_add_width <= 0 || block_add_height <= 0) { + block_add_x = -1; + 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& 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; + sel_y = block_add_y; + block_add_x = -1; + block_add_y = -1; + has_block_add_xy = false; + state.edit_starting_state(*next); + } + } +} + +auto input_handler::remove_block() -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& b = current.try_get_block(hov_x, hov_y); + if (!editing || has_block_add_xy || !b) { + return; + } + + const std::optional& next = current.try_remove_block(hov_x, hov_y); + if (!next) { + return; + } + + // Reset selection if we removed the selected block + if (b->covers(sel_x, sel_y)) { + sel_x = 0; + sel_y = 0; + } + + state.edit_starting_state(*next); +} + +auto input_handler::place_goal() const -> void +{ + const puzzle& current = state.get_current_state(); + if (!editing || !current.covers(hov_x, hov_y)) { + return; + } + + const std::optional& next = current.try_set_goal(hov_x, hov_y); + if (!next) { + return; + } + + state.edit_starting_state(*next); +} + +auto input_handler::toggle_camera_lock() -> void +{ + if (!camera_lock) { + camera_panning = false; + } + + camera_lock = !camera_lock; +} + +auto input_handler::toggle_camera_mass_center_lock() -> void +{ + if (!camera_mass_center_lock) { + camera_lock = true; + camera_panning = false; + } + + camera_mass_center_lock = !camera_mass_center_lock; +} + +auto input_handler::toggle_camera_projection() const -> void +{ + camera.projection = camera.projection == CAMERA_PERSPECTIVE ? CAMERA_ORTHOGRAPHIC : CAMERA_PERSPECTIVE; +} + +auto input_handler::move_block_nor() -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_move_block_at(sel_x, sel_y, nor); + if (!next) { + return; + } + + sel_y--; + state.update_current_state(*next); +} + +auto input_handler::move_block_wes() -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_move_block_at(sel_x, sel_y, wes); + if (!next) { + return; + } + + sel_x--; + state.update_current_state(*next); +} + +auto input_handler::move_block_sou() -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_move_block_at(sel_x, sel_y, sou); + if (!next) { + return; + } + + sel_y++; + state.update_current_state(*next); +} + +auto input_handler::move_block_eas() -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_move_block_at(sel_x, sel_y, eas); + if (!next) { + return; + } + + sel_x++; + state.update_current_state(*next); +} + +auto input_handler::print_state() const -> void +{ + infoln("State: \"{}\"", state.get_current_state().state); +} + +auto input_handler::load_previous_preset() -> void +{ + if (editing) { + return; + } + + block_add_x = -1; + block_add_y = -1; + has_block_add_xy = false; + state.load_previous_preset(); +} + +auto input_handler::load_next_preset() -> void +{ + if (editing) { + return; + } + + block_add_x = -1; + block_add_y = -1; + has_block_add_xy = false; + state.load_next_preset(); +} + +auto input_handler::goto_starting_state() -> void +{ + state.goto_starting_state(); sel_x = 0; sel_y = 0; - } - - state.ClearGraph(); - state.edited = true; } -auto InputHandler::PlaceGoal() -> void { - if (!editing || hov_x < 0 || hov_x >= state.current_state.width || - hov_y < 0 || hov_y >= state.current_state.height) { - return; - } - - if (state.current_state.SetGoal(hov_x, hov_y)) { - // We can't just call state.FindWinningStates() because the - // state is entirely different if it has a different win condition. - state.ClearGraph(); - } +auto input_handler::populate_graph() const -> void +{ + state.populate_graph(); } -auto InputHandler::MoveBlockNor() -> void { - if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::NOR)) { - sel_y--; - } +auto input_handler::clear_graph() const -> void +{ + state.clear_graph_and_add_current(); } -auto InputHandler::MoveBlockEas() -> void { - if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::EAS)) { - sel_x++; - } +auto input_handler::toggle_mark_solutions() -> void +{ + mark_solutions = !mark_solutions; } -auto InputHandler::MoveBlockSou() -> void { - if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::SOU)) { - sel_y++; - } +auto input_handler::toggle_connect_solutions() -> void +{ + connect_solutions = !connect_solutions; } -auto InputHandler::MoveBlockWes() -> void { - if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::WES)) { - sel_x--; - } +auto input_handler::toggle_mark_path() -> void +{ + mark_path = !mark_path; } -auto InputHandler::PrintState() const -> void { - std::cout << std::format("State: \"{}\"", state.current_state.state) - << std::endl; - Block sel = state.current_state.GetBlock(sel_x, sel_y); - int idx = state.current_state.GetIndex(sel.x, sel.y) - State::prefix; - if (sel.IsValid()) { - std::cout << std::format("Sel: \"{}{}{}{}\"", - state.current_state.state.substr(0, State::prefix), - std::string(idx, '.'), sel.ToString(), - std::string(state.current_state.state.length() - - idx - State::prefix - 2, - '.')) - << std::endl; - } +auto input_handler::goto_optimal_next_state() const -> void +{ + state.goto_optimal_next_state(); } -auto InputHandler::PreviousPreset() -> void { - if (editing) { - return; - } - - block_add_x = -1; - block_add_y = -1; - has_block_add_xy = false; - state.PreviousPreset(); +auto input_handler::goto_most_distant_state() const -> void +{ + state.goto_most_distant_state(); } -auto InputHandler::NextPreset() -> void { - if (editing) { - return; - } - - block_add_x = -1; - block_add_y = -1; - has_block_add_xy = false; - state.NextPreset(); +auto input_handler::goto_closest_target_state() const -> void +{ + state.goto_closest_target_state(); } -auto InputHandler::ResetState() -> void { - if (editing) { - return; - } - - state.ResetState(); - sel_x = 0; - sel_y = 0; +auto input_handler::goto_previous_state() const -> void +{ + state.goto_previous_state(); } -auto InputHandler::FillGraph() -> void { state.FillGraph(); } +auto input_handler::toggle_restricted_movement() const -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_toggle_restricted(); -auto InputHandler::ClearGraph() -> void { state.ClearGraph(); } - -auto InputHandler::ToggleMarkSolutions() -> void { - mark_solutions = !mark_solutions; -} - -auto InputHandler::ToggleConnectSolutions() -> void { - connect_solutions = !connect_solutions; -} - -auto InputHandler::ToggleMarkPath() -> void { mark_path = !mark_path; } - -auto InputHandler::MakeOptimalMove() -> void { state.NextPath(); } - -auto InputHandler::GoToWorstState() -> void { state.GoToWorst(); } - -auto InputHandler::GoToNearestTarget() -> void { state.GoToNearestTarget(); } - -auto InputHandler::UndoLastMove() -> void { state.PopHistory(); } - -auto InputHandler::ToggleRestrictedMovement() -> void { - if (!editing) { - return; - } - - state.current_state.ToggleRestricted(); - state.ClearGraph(); - state.edited = true; -} - -auto InputHandler::ToggleTargetBlock() -> void { - if (!editing) { - return; - } - - state.current_state.ToggleTarget(sel_x, sel_y); - state.ClearGraph(); - state.edited = true; -} - -auto InputHandler::ToggleWallBlock() -> void { - if (!editing) { - return; - } - - state.current_state.ToggleWall(sel_x, sel_y); - state.ClearGraph(); - state.edited = true; -} - -auto InputHandler::RemoveBoardColumn() -> void { - if (!editing || state.current_state.width <= 1) { - return; - } - - state.current_state = state.current_state.RemoveColumn(); - state.ClearGraph(); - state.edited = true; -} - -auto InputHandler::AddBoardColumn() -> void { - if (!editing || state.current_state.width >= 9) { - return; - } - - state.current_state = state.current_state.AddColumn(); - state.ClearGraph(); - state.edited = true; -} - -auto InputHandler::RemoveBoardRow() -> void { - if (!editing || state.current_state.height <= 1) { - return; - } - - state.current_state = state.current_state.RemoveRow(); - state.ClearGraph(); - state.edited = true; -} - -auto InputHandler::AddBoardRow() -> void { - if (!editing || state.current_state.height >= 9) { - return; - } - - state.current_state = state.current_state.AddRow(); - state.ClearGraph(); - state.edited = true; -} - -auto InputHandler::ToggleEditing() -> void { - if (editing) { - has_block_add_xy = false; - block_add_x = -1; - block_add_y = -1; - } - - editing = !editing; -} - -auto InputHandler::ClearGoal() -> void { - if (!editing) { - return; - } - - state.current_state.ClearGoal(); - state.ClearGraph(); - state.edited = true; -} - -auto InputHandler::RegisterGenericHandler( - std::function handler) -> void { - generic_handlers.push_back({handler}); -} - -auto InputHandler::RegisterMousePressedHandler( - MouseButton button, std::function handler) -> void { - mouse_pressed_handlers.push_back({handler, button}); -} - -auto InputHandler::RegisterMouseReleasedHandler( - MouseButton button, std::function handler) -> void { - mouse_released_handlers.push_back({handler, button}); -} - -auto InputHandler::RegisterKeyPressedHandler( - KeyboardKey key, std::function handler) -> void { - key_pressed_handlers.push_back({handler, key}); -} - -auto InputHandler::RegisterKeyReleasedHandler( - KeyboardKey key, std::function handler) -> void { - key_released_handlers.push_back({handler, key}); -} - -auto InputHandler::HandleInput() -> void { - if (disable) { - return; - } - - for (const GenericHandler &handler : generic_handlers) { - handler.handler(*this); - } - - for (const MouseHandler &handler : mouse_pressed_handlers) { - if (IsMouseButtonPressed(handler.button)) { - handler.handler(*this); + if (!editing || !next) { + return; } - } - for (const MouseHandler &handler : mouse_released_handlers) { - if (IsMouseButtonReleased(handler.button)) { - handler.handler(*this); - } - } - - for (const KeyboardHandler &handler : key_pressed_handlers) { - if (IsKeyPressed(handler.key)) { - handler.handler(*this); - } - } - - for (const KeyboardHandler &handler : key_released_handlers) { - if (IsKeyReleased(handler.key)) { - handler.handler(*this); - } - } + state.edit_starting_state(*next); +} + +auto input_handler::toggle_target_block() const -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_toggle_target(sel_x, sel_y); + + if (!editing || !next) { + return; + } + + state.edit_starting_state(*next); +} + +auto input_handler::toggle_wall_block() const -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_toggle_wall(sel_x, sel_y); + + if (!editing || !next) { + return; + } + + state.edit_starting_state(*next); +} + +auto input_handler::remove_board_column() const -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_remove_column(); + + if (!editing || current.width <= puzzle::MIN_WIDTH || !next) { + return; + } + + state.edit_starting_state(*next); +} + +auto input_handler::add_board_column() const -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_add_column(); + + if (!editing || current.width >= puzzle::MAX_WIDTH || !next) { + return; + } + + state.edit_starting_state(*next); +} + +auto input_handler::remove_board_row() const -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_remove_row(); + + if (!editing || current.height <= puzzle::MIN_HEIGHT || !next) { + return; + } + + state.edit_starting_state(*next); +} + +auto input_handler::add_board_row() const -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_add_row(); + + if (!editing || current.height >= puzzle::MAX_HEIGHT || !next) { + return; + } + + state.edit_starting_state(*next); +} + +auto input_handler::toggle_editing() -> void +{ + if (editing) { + has_block_add_xy = false; + block_add_x = -1; + block_add_y = -1; + } + + editing = !editing; +} + +auto input_handler::clear_goal() const -> void +{ + const puzzle& current = state.get_current_state(); + const std::optional& next = current.try_clear_goal(); + + if (!editing || !next) { + return; + } + + state.edit_starting_state(*next); +} + +auto input_handler::register_generic_handler(const std::function& handler) -> void +{ + generic_handlers.push_back({handler}); +} + +auto input_handler::register_mouse_pressed_handler(const MouseButton button, + const std::function& handler) -> void +{ + mouse_pressed_handlers.push_back({{handler}, button}); +} + +auto input_handler::register_mouse_released_handler(const MouseButton button, + const std::function& handler) -> void +{ + mouse_released_handlers.push_back({{handler}, button}); +} + +auto input_handler::register_key_pressed_handler(const KeyboardKey key, + const std::function& handler) -> void +{ + key_pressed_handlers.push_back({{handler}, key}); +} + +auto input_handler::register_key_released_handler(const KeyboardKey key, + const std::function& handler) -> void +{ + key_released_handlers.push_back({{handler}, key}); +} + +auto input_handler::handle_input() -> void +{ + if (disable) { + return; + } + + for (const auto& [handler] : generic_handlers) { + handler(*this); + } + + for (const mouse_handler& handler : mouse_pressed_handlers) { + if (IsMouseButtonPressed(handler.button)) { + handler.handler(*this); + } + } + + for (const mouse_handler& handler : mouse_released_handlers) { + if (IsMouseButtonReleased(handler.button)) { + handler.handler(*this); + } + } + + for (const keyboard_handler& handler : key_pressed_handlers) { + if (IsKeyPressed(handler.key)) { + handler.handler(*this); + } + } + + for (const keyboard_handler& handler : key_released_handlers) { + if (IsKeyReleased(handler.key)) { + handler.handler(*this); + } + } } diff --git a/src/main.cpp b/src/main.cpp index 794a9e5..160a3e3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,18 +1,16 @@ #include #include #include -#include #include "config.hpp" -#include "gui.hpp" #include "input.hpp" #include "physics.hpp" #include "renderer.hpp" -#include "state.hpp" +#include "state_manager.hpp" +#include "user_interface.hpp" #ifdef TRACY -#include "tracy.hpp" -#include + #include #endif // TODO: Click states in the graph to display them in the board @@ -25,11 +23,9 @@ // - Clear graph: Notify that this will clear the visited states and move // history // - Reset state: Notify that this will reset the move count -// Remove the keybindings, as it's simpler to show the popups from the -// button? +// - Next/Previous preset: Notify that this will clear all edits // TODO: Reduce memory usage -// - State.cpp stores a lot of duplicates, do I need all of them? // - The memory model of the puzzle board is terrible (bitboards?) // TODO: Improve solver @@ -44,127 +40,126 @@ // NOTE: Tracy uses a huge amount of memory. For longer testing disable Tracy. -auto main(int argc, char *argv[]) -> int { - std::string preset_file; - if (argc != 2) { - preset_file = "default.puzzle"; - } else { - preset_file = argv[1]; - } +auto main(int argc, char* argv[]) -> int +{ + std::string preset_file; + if (argc != 2) { + preset_file = "default.puzzle"; + } else { + preset_file = argv[1]; + } #ifdef BACKWARD - std::cout << std::format("Backward stack-traces enabled.") << std::endl; + infoln("Backward stack-traces enabled."); #else - std::cout << std::format("Backward stack-traces disabled.") << std::endl; + infoln("Backward stack-traces disabled."); #endif #ifdef TRACY - std::cout << std::format("Tracy adapter enabled.") << std::endl; + infoln("Tracy adapter enabled."); #else - std::cout << std::format("Tracy adapter disabled.") << std::endl; + infoln("Tracy adapter disabled."); #endif - // RayLib window setup - SetTraceLogLevel(LOG_ERROR); - SetConfigFlags(FLAG_VSYNC_HINT); - SetConfigFlags(FLAG_MSAA_4X_HINT); - SetConfigFlags(FLAG_WINDOW_RESIZABLE); - SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN); - InitWindow(INITIAL_WIDTH * 2, INITIAL_HEIGHT + MENU_HEIGHT, "MassSprings"); + // RayLib window setup + SetTraceLogLevel(LOG_ERROR); + SetConfigFlags(FLAG_VSYNC_HINT); + SetConfigFlags(FLAG_MSAA_4X_HINT); + SetConfigFlags(FLAG_WINDOW_RESIZABLE); + SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN); + InitWindow(INITIAL_WIDTH * 2, INITIAL_HEIGHT + MENU_HEIGHT, "MassSprings"); - // Game setup - ThreadedPhysics physics; - StateManager state(physics, preset_file); - OrbitCamera3D camera; - InputHandler input(state, camera); - Gui gui(input, state, camera); - Renderer renderer(camera, state, input, gui); + // Game setup + threaded_physics physics; + state_manager state(physics, preset_file); + orbit_camera camera; + input_handler input(state, camera); + user_interface gui(input, state, camera); + renderer renderer(camera, state, input, gui); - std::chrono::time_point last = std::chrono::high_resolution_clock::now(); - std::chrono::duration fps_accumulator(0); - unsigned int loop_iterations = 0; + std::chrono::time_point last = std::chrono::high_resolution_clock::now(); + std::chrono::duration fps_accumulator(0); + int loop_iterations = 0; - unsigned int fps = 0; - unsigned int ups = 0; // Read from physics - Vector3 mass_center = Vector3Zero(); // Read from physics - std::vector masses; // Read from physics + int fps = 0; + int ups = 0; // Read from physics + Vector3 mass_center; // Read from physics + std::vector masses; // Read from physics + size_t mass_count = 0; + size_t spring_count = 0; - // Game loop - while (!WindowShouldClose()) { + // Game loop + while (!WindowShouldClose()) { #ifdef TRACY - FrameMarkStart("MainThread"); + FrameMarkStart("MainThread"); #endif - // Time tracking - std::chrono::time_point now = std::chrono::high_resolution_clock::now(); - std::chrono::duration deltatime = now - last; - fps_accumulator += deltatime; - last = now; + // Time tracking + std::chrono::time_point now = std::chrono::high_resolution_clock::now(); + std::chrono::duration delta_time = now - last; + fps_accumulator += delta_time; + last = now; - // Input update - input.HandleInput(); - state.UpdateGraph(); // Add state added after user input + // Input update + input.handle_input(); - // Read positions from physics thread +// Read positions from physics thread #ifdef TRACY - FrameMarkStart("MainThreadConsumeLock"); + FrameMarkStart("MainThreadConsumeLock"); #endif - { + { #ifdef TRACY - std::unique_lock lock(physics.state.data_mtx); + std::unique_lock lock(physics.state.data_mtx); #else - std::unique_lock lock(physics.state.data_mtx); + std::unique_lock lock(physics.state.data_mtx); #endif - ups = physics.state.ups; - mass_center = physics.state.mass_center; + ups = physics.state.ups; + mass_center = physics.state.mass_center; + mass_count = physics.state.mass_count; + spring_count = physics.state.spring_count; - // Only copy data if any has been produced - if (physics.state.data_ready) { - masses = physics.state.masses; + // Only copy data if any has been produced + if (physics.state.data_ready) { + masses = physics.state.masses; - physics.state.data_ready = false; - physics.state.data_consumed = true; + physics.state.data_ready = false; + physics.state.data_consumed = true; - lock.unlock(); - // Notify the physics thread that data has been consumed - physics.state.data_consumed_cnd.notify_all(); - } - } + lock.unlock(); + // Notify the physics thread that data has been consumed + physics.state.data_consumed_cnd.notify_all(); + } + } #ifdef TRACY - FrameMarkEnd("MainThreadConsumeLock"); + FrameMarkEnd("MainThreadConsumeLock"); #endif - // Update the camera after the physics, so target lock is smooth - std::size_t current_index = state.CurrentMassIndex(); - if (masses.size() > current_index) { - const Mass ¤t_mass = masses.at(current_index); - camera.Update(current_mass.position, mass_center, input.camera_lock, - input.camera_mass_center_lock); + // Update the camera after the physics, so target lock is smooth + 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); + } + + // Rendering + renderer.render(masses, fps, ups, mass_count, spring_count); + + if (fps_accumulator.count() > 1.0) { + // Update each second + fps = loop_iterations; + loop_iterations = 0; + fps_accumulator = std::chrono::duration(0); + } + ++loop_iterations; + +#ifdef TRACY + FrameMark; + FrameMarkEnd("MainThread"); +#endif } - // Rendering - renderer.UpdateTextureSizes(); - renderer.DrawMassSprings(masses); - renderer.DrawKlotski(); - renderer.DrawMenu(masses); - renderer.DrawTextures(fps, ups); + CloseWindow(); - if (fps_accumulator.count() > 1.0) { - // Update each second - fps = loop_iterations; - loop_iterations = 0; - fps_accumulator = std::chrono::duration(0); - } - ++loop_iterations; - -#ifdef TRACY - FrameMark; - FrameMarkEnd("MainThread"); -#endif - } - - CloseWindow(); - - return 0; + return 0; } diff --git a/src/octree.cpp b/src/octree.cpp index 6ac4580..3374696 100644 --- a/src/octree.cpp +++ b/src/octree.cpp @@ -1,168 +1,194 @@ #include "octree.hpp" #include "config.hpp" +#include "util.hpp" #include #ifdef TRACY -#include "tracy.hpp" -#include + #include #endif -auto OctreeNode::ChildCount() const -> int { - int child_count = 0; - for (int child : children) { - if (child != -1) { - ++child_count; +auto octree::node::child_count() const -> int +{ + int child_count = 0; + for (const int child : children) { + if (child != -1) { + ++child_count; + } } - } - return child_count; + return child_count; } -auto Octree::CreateNode(const Vector3 &box_min, const Vector3 &box_max) -> int { - OctreeNode node; - node.box_min = box_min; - node.box_max = box_max; - nodes.push_back(node); +auto octree::create_empty_leaf(const Vector3& box_min, const Vector3& box_max) -> int +{ + node n; + n.box_min = box_min; + n.box_max = box_max; + nodes.emplace_back(n); - return nodes.size() - 1; + return static_cast(nodes.size() - 1); } -auto Octree::GetOctant(int node_idx, const Vector3 &pos) -> int { - OctreeNode &node = nodes[node_idx]; - Vector3 center = Vector3((node.box_min.x + node.box_max.x) / 2.0, - (node.box_min.y + node.box_max.y) / 2.0, - (node.box_min.z + node.box_max.z) / 2.0); +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); - // 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. - // If a position is right of the x-axis and y-axis and left of the z-axis, the - // encoded octant is "011". - int octant = 0; - if (pos.x >= center.x) { - octant |= 1; - } - if (pos.y >= center.y) { - octant |= 2; - } - if (pos.z >= center.z) { - octant |= 4; - } + // 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. + // If a position is right of the x-axis and y-axis and left of the z-axis, the + // encoded octant is "011". + int octant = 0; + if (pos.x >= cx) { + octant |= 1; + } + if (pos.y >= cy) { + octant |= 2; + } + if (pos.z >= cz) { + octant |= 4; + } - return octant; + return octant; } -auto Octree::GetChildBounds(int node_idx, int octant) - -> std::pair { - OctreeNode &node = nodes[node_idx]; - Vector3 center = Vector3((node.box_min.x + node.box_max.x) / 2.0, - (node.box_min.y + node.box_max.y) / 2.0, - (node.box_min.z + node.box_max.z) / 2.0); +auto octree::get_child_bounds(const int node_idx, const int octant) const -> std::pair +{ + 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); - Vector3 min = Vector3Zero(); - Vector3 max = Vector3Zero(); + Vector3 min = Vector3Zero(); + Vector3 max = Vector3Zero(); - // If (octant & 1), the octant is to the right of the node region's x-axis - // (see GetOctant). This means the left bound is the x-axis and the right - // bound the node's region max. - min.x = (octant & 1) ? center.x : node.box_min.x; - max.x = (octant & 1) ? node.box_max.x : center.x; - min.y = (octant & 2) ? center.y : node.box_min.y; - max.y = (octant & 2) ? node.box_max.y : center.y; - min.z = (octant & 4) ? center.z : node.box_min.z; - max.z = (octant & 4) ? node.box_max.z : center.z; + // If (octant & 1), the octant is to the right of the node region's x-axis + // (see GetOctant). This means the left bound is the x-axis and the right + // bound the node's region max. + min.x = octant & 1 ? cx : n.box_min.x; + max.x = octant & 1 ? n.box_max.x : cx; + min.y = octant & 2 ? cy : n.box_min.y; + max.y = octant & 2 ? n.box_max.y : cy; + min.z = octant & 4 ? cz : n.box_min.z; + max.z = octant & 4 ? n.box_max.z : cz; - return std::make_pair(min, max); + return std::make_pair(min, max); } -auto Octree::Insert(int node_idx, int mass_id, const Vector3 &pos, float mass) - -> void { - // NOTE: Do not store a nodes[node_idx] reference beforehand as the nodes - // vector might reallocate during this function +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); + 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); + + // 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 - if (nodes[node_idx].leaf && nodes[node_idx].mass_id == -1) { // We can place the particle in the empty leaf - nodes[node_idx].mass_id = mass_id; - nodes[node_idx].mass_center = pos; - nodes[node_idx].mass_total = mass; - return; - } + if (nodes[node_idx].leaf && nodes[node_idx].mass_id == -1) { + nodes[node_idx].mass_id = mass_id; + nodes[node_idx].mass_center = pos; + nodes[node_idx].mass_total = mass; + return; + } - if (nodes[node_idx].leaf) { // The leaf is occupied, we need to subdivide - int existing_id = nodes[node_idx].mass_id; - Vector3 existing_pos = nodes[node_idx].mass_center; - float existing_mass = nodes[node_idx].mass_total; - nodes[node_idx].mass_id = -1; - nodes[node_idx].leaf = false; - nodes[node_idx].mass_total = 0.0; + if (nodes[node_idx].leaf) { + const int existing_id = nodes[node_idx].mass_id; + const Vector3 existing_pos = nodes[node_idx].mass_center; + const float existing_mass = nodes[node_idx].mass_total; - // Re-insert the existing mass into a new empty leaf (see above) - int oct = GetOctant(node_idx, existing_pos); + // If positions are identical we jitter the particles + const Vector3 diff = Vector3Subtract(pos, existing_pos); + if (diff == Vector3Zero()) { + // warnln("Trying to insert an identical partical into octree (jittering position)"); + + Vector3 jittered = pos; + jittered.x += 0.001; + jittered.y += 0.001; + 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; + } + + // Convert the leaf to an internal node + nodes[node_idx].mass_id = -1; + nodes[node_idx].leaf = false; + nodes[node_idx].mass_total = 0.0; + nodes[node_idx].mass_center = Vector3Zero(); + + // Re-insert the existing mass into a new empty leaf (see above) + const int oct = get_octant(node_idx, existing_pos); + if (nodes[node_idx].children[oct] == -1) { + const auto& [min, max] = get_child_bounds(node_idx, oct); + const int child_idx = create_empty_leaf(min, max); + nodes[node_idx].children[oct] = child_idx; + } + insert(nodes[node_idx].children[oct], existing_id, existing_pos, existing_mass, depth + 1); + } + + // Insert the new mass + const int oct = get_octant(node_idx, pos); if (nodes[node_idx].children[oct] == -1) { - auto [min, max] = GetChildBounds(node_idx, oct); - nodes[node_idx].children[oct] = CreateNode(min, max); + const auto& [min, max] = get_child_bounds(node_idx, oct); + const int child_idx = create_empty_leaf(min, max); + nodes[node_idx].children[oct] = child_idx; } - Insert(nodes[node_idx].children[oct], existing_id, existing_pos, - existing_mass); - } + insert(nodes[node_idx].children[oct], mass_id, pos, mass, depth + 1); - // Insert the new mass - int oct = GetOctant(node_idx, pos); - if (nodes[node_idx].children[oct] == -1) { - auto [min, max] = GetChildBounds(node_idx, oct); - nodes[node_idx].children[oct] = CreateNode(min, max); - } - Insert(nodes[node_idx].children[oct], mass_id, pos, mass); - - // Update the center of mass - 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_total = new_mass; + // 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_total = new_mass; } -auto Octree::CalculateForce(int node_idx, const Vector3 &pos) const -> Vector3 { - if (node_idx < 0) { - return Vector3Zero(); - } - - const OctreeNode &node = nodes[node_idx]; - if (std::abs(node.mass_total) <= 0.001f) { - return Vector3Zero(); - } - - Vector3 diff = Vector3Subtract(pos, node.mass_center); - float dist_sq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z; - - // Softening - dist_sq += SOFTENING; - - float size = node.box_max.x - node.box_min.x; - - // Barnes-Hut - if (node.leaf || (size * size / dist_sq) < (THETA * THETA)) { - float dist = std::sqrt(dist_sq); - float force_mag = BH_FORCE * node.mass_total / dist_sq; - - return Vector3Scale(diff, force_mag / dist); - } - - // Collect child forces - Vector3 force = Vector3Zero(); - for (int i = 0; i < 8; ++i) { - if (node.children[i] >= 0) { - Vector3 child_force = CalculateForce(node.children[i], pos); - - force = Vector3Add(force, child_force); +auto octree::calculate_force(const int node_idx, const Vector3& pos) const -> Vector3 +{ + if (node_idx < 0) { + return Vector3Zero(); } - } - return force; + const node& n = nodes[node_idx]; + if (std::abs(n.mass_total) <= 0.001f) { + return Vector3Zero(); + } + + const Vector3 diff = Vector3Subtract(pos, n.mass_center); + float dist_sq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z; + + // Softening + dist_sq += SOFTENING; + + // Barnes-Hut + const float size = n.box_max.x - n.box_min.x; + if (n.leaf || size * size / dist_sq < THETA * THETA) { + const float dist = std::sqrt(dist_sq); + const float force_mag = BH_FORCE * n.mass_total / dist_sq; + + return Vector3Scale(diff, force_mag / dist); + } + + // Collect child forces + Vector3 force = Vector3Zero(); + for (const int child : n.children) { + if (child >= 0) { + const Vector3 child_force = calculate_force(child, pos); + + force = Vector3Add(force, child_force); + } + } + + return force; } diff --git a/src/physics.cpp b/src/physics.cpp index 7cdc12d..cb72e53 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -4,350 +4,401 @@ #include #include #include -#include #include #include #include #include #ifdef TRACY -#include "tracy.hpp" -#include + #include #endif -auto Mass::ClearForce() -> void { force = Vector3Zero(); } - -auto Mass::CalculateVelocity(const float delta_time) -> void { - Vector3 acceleration; - Vector3 temp; - - acceleration = Vector3Scale(force, 1.0 / MASS); - temp = Vector3Scale(acceleration, delta_time); - velocity = Vector3Add(velocity, temp); +auto mass::clear_force() -> void +{ + force = Vector3Zero(); } -auto Mass::CalculatePosition(const float delta_time) -> void { - previous_position = position; - - Vector3 temp; - - temp = Vector3Scale(velocity, delta_time); - position = Vector3Add(position, temp); +auto mass::calculate_velocity(const float delta_time) -> void +{ + const Vector3 acceleration = Vector3Scale(force, 1.0 / MASS); + const Vector3 temp = Vector3Scale(acceleration, delta_time); + velocity = Vector3Add(velocity, temp); } -auto Mass::VerletUpdate(const float delta_time) -> void { - Vector3 acceleration = Vector3Scale(force, 1.0 / MASS); - Vector3 temp_position = position; +auto mass::calculate_position(const float delta_time) -> void +{ + previous_position = position; - Vector3 displacement = Vector3Subtract(position, previous_position); - Vector3 accel_term = Vector3Scale(acceleration, delta_time * delta_time); - - // Minimal dampening - displacement = Vector3Scale(displacement, 1.0 - VERLET_DAMPENING); - - position = Vector3Add(Vector3Add(position, displacement), accel_term); - previous_position = temp_position; + const Vector3 temp = Vector3Scale(velocity, delta_time); + position = Vector3Add(position, temp); } -auto Spring::CalculateSpringForce(Mass &_mass_a, Mass &_mass_b) const -> void { - Vector3 delta_position = Vector3Subtract(_mass_a.position, _mass_b.position); - float current_length = Vector3Length(delta_position); - float inv_current_length = 1.0 / current_length; - Vector3 delta_velocity = Vector3Subtract(_mass_a.velocity, _mass_b.velocity); +auto mass::verlet_update(const float delta_time) -> void +{ + const Vector3 acceleration = Vector3Scale(force, 1.0 / MASS); + const Vector3 temp_position = position; - float hooke = SPRING_CONSTANT * (current_length - REST_LENGTH); - float dampening = DAMPENING_CONSTANT * - Vector3DotProduct(delta_velocity, delta_position) * - inv_current_length; + Vector3 displacement = Vector3Subtract(position, previous_position); + const Vector3 accel_term = Vector3Scale(acceleration, delta_time * delta_time); - Vector3 force_a = - Vector3Scale(delta_position, -(hooke + dampening) * inv_current_length); - Vector3 force_b = Vector3Scale(force_a, -1.0); + // Minimal dampening + displacement = Vector3Scale(displacement, 1.0 - VERLET_DAMPENING); - _mass_a.force = Vector3Add(_mass_a.force, force_a); - _mass_b.force = Vector3Add(_mass_b.force, force_b); + position = Vector3Add(Vector3Add(position, displacement), accel_term); + previous_position = temp_position; } -auto MassSpringSystem::AddMass() -> void { masses.emplace_back(Vector3Zero()); } +auto spring::calculate_spring_force(mass& _a, mass& _b) -> void +{ + // TODO: Use a bungee force here instead of springs, since we already have global repulsion? + const Vector3 delta_position = Vector3Subtract(_a.position, _b.position); + const float current_length = Vector3Length(delta_position); + const float inv_current_length = 1.0f / current_length; + const Vector3 delta_velocity = Vector3Subtract(_a.velocity, _b.velocity); -auto MassSpringSystem::AddSpring(int a, int b) -> void { - Mass &mass_a = masses.at(a); - Mass &mass_b = masses.at(b); + const float hooke = SPRING_CONSTANT * (current_length - REST_LENGTH); + const float dampening = DAMPENING_CONSTANT * Vector3DotProduct(delta_velocity, delta_position) * inv_current_length; - Vector3 position = mass_a.position; - Vector3 offset = Vector3(static_cast(GetRandomValue(-100, 100)), - static_cast(GetRandomValue(-100, 100)), - static_cast(GetRandomValue(-100, 100))); - offset = Vector3Scale(Vector3Normalize(offset), REST_LENGTH); + const Vector3 force_a = Vector3Scale(delta_position, -(hooke + dampening) * inv_current_length); + const Vector3 force_b = Vector3Scale(force_a, -1.0); - if (mass_b.position == Vector3Zero()) { - mass_b.position = Vector3Add(position, offset); - } - - springs.emplace_back(a, b); + _a.force = Vector3Add(_a.force, force_a); + _b.force = Vector3Add(_b.force, force_b); } -auto MassSpringSystem::Clear() -> void { - masses.clear(); - springs.clear(); - octree.nodes.clear(); +auto mass_spring_system::add_mass() -> void +{ + // Adding all positions to (0, 0, 0) breaks the octree + + // Done when adding springs + // Vector3 position{ + // static_cast(GetRandomValue(-100, 100)), static_cast(GetRandomValue(-100, 100)), + // static_cast(GetRandomValue(-100, 100)) + // }; + // position = Vector3Scale(Vector3Normalize(position), REST_LENGTH * 2.0); + + masses.emplace_back(Vector3Zero()); } -auto MassSpringSystem::ClearForces() -> void { +auto mass_spring_system::add_spring(size_t a, size_t b) -> void +{ + // Update masses to be located along a random walk when adding the springs + const mass& mass_a = masses.at(a); + mass& mass_b = masses.at(b); + + Vector3 offset{static_cast(GetRandomValue(-100, 100)), static_cast(GetRandomValue(-100, 100)), + static_cast(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 float mass_center_distance = Vector3Length(mass_center_direction); + + if (mass_center_distance > 0 && Vector3DotProduct(offset, mass_center_direction) < 0.0f) { + offset = Vector3Negate(offset); + } + } + + 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, + // mass_b.position.x, mass_b.position.y, mass_b.position.z); + + springs.emplace_back(a, b); +} + +auto mass_spring_system::clear() -> void +{ + masses.clear(); + springs.clear(); + tree.nodes.clear(); +} + +auto mass_spring_system::clear_forces() -> void +{ #ifdef TRACY - ZoneScoped; + ZoneScoped; #endif - for (auto &mass : masses) { - mass.ClearForce(); - } + for (auto& mass : masses) { + mass.clear_force(); + } } -auto MassSpringSystem::CalculateSpringForces() -> void { +auto mass_spring_system::calculate_spring_forces() -> void +{ #ifdef TRACY - ZoneScoped; + ZoneScoped; #endif - for (const auto spring : springs) { - Mass &a = masses.at(spring.a); - Mass &b = masses.at(spring.b); - spring.CalculateSpringForce(a, b); - } + for (const auto s : springs) { + mass& a = masses.at(s.a); + mass& b = masses.at(s.b); + spring::calculate_spring_force(a, b); + } } #ifdef THREADPOOL -auto MassSpringSystem::SetThreadName(std::size_t idx) -> void { - BS::this_thread::set_os_thread_name(std::format("bh-worker-{}", idx)); +auto mass_spring_system::set_thread_name(size_t idx) -> void +{ + BS::this_thread::set_os_thread_name(std::format("bh-worker-{}", idx)); } #endif -auto MassSpringSystem::BuildOctree() -> void { +auto mass_spring_system::build_octree() -> void +{ #ifdef TRACY - ZoneScoped; + ZoneScoped; #endif - octree.nodes.clear(); - octree.nodes.reserve(masses.size() * 2); + tree.nodes.clear(); + tree.nodes.reserve(masses.size() * 2); - // Compute bounding box around all masses - Vector3 min = Vector3(FLT_MAX, FLT_MAX, FLT_MAX); - Vector3 max = Vector3(-FLT_MAX, -FLT_MAX, -FLT_MAX); - for (const auto &mass : masses) { - min.x = std::min(min.x, mass.position.x); - max.x = std::max(max.x, mass.position.x); - min.y = std::min(min.y, mass.position.y); - max.y = std::max(max.y, mass.position.y); - min.z = std::min(min.z, mass.position.z); - max.z = std::max(max.z, mass.position.z); - } + // Compute bounding box around all masses + Vector3 min{FLT_MAX, FLT_MAX, FLT_MAX}; + Vector3 max{-FLT_MAX, -FLT_MAX, -FLT_MAX}; + for (const auto& mass : masses) { + min.x = std::min(min.x, mass.position.x); + max.x = std::max(max.x, mass.position.x); + min.y = std::min(min.y, mass.position.y); + max.y = std::max(max.y, mass.position.y); + min.z = std::min(min.z, mass.position.z); + max.z = std::max(max.z, mass.position.z); + } - // Pad the bounding box - float pad = 1.0; - min = Vector3Subtract(min, Vector3Scale(Vector3One(), pad)); - max = Vector3Add(max, Vector3Scale(Vector3One(), pad)); + // Pad the bounding box + constexpr float pad = 1.0; + min = Vector3Subtract(min, Vector3Scale(Vector3One(), pad)); + max = Vector3Add(max, Vector3Scale(Vector3One(), pad)); - // Make it cubic (so subdivisions are balanced) - float max_extent = std::max({max.x - min.x, max.y - min.y, max.z - min.z}); - max = Vector3Add(min, Vector3Scale(Vector3One(), max_extent)); + // Make it cubic (so subdivisions are balanced) + const float max_extent = std::max({max.x - min.x, max.y - min.y, max.z - min.z}); + max = Vector3Add(min, Vector3Scale(Vector3One(), max_extent)); - // Root node spans the entire area - int root = octree.CreateNode(min, max); + // Root node spans the entire area + const int root = tree.create_empty_leaf(min, max); - for (std::size_t i = 0; i < masses.size(); ++i) { - octree.Insert(root, i, masses[i].position, MASS); - } + for (size_t i = 0; i < masses.size(); ++i) { + tree.insert(root, i, masses[i].position, MASS, 0); + } } -auto MassSpringSystem::CalculateRepulsionForces() -> void { +auto mass_spring_system::calculate_repulsion_forces() -> void +{ #ifdef TRACY - ZoneScoped; + ZoneScoped; #endif - BuildOctree(); + build_octree(); - auto solve_octree = [&](int i) { - Vector3 force = octree.CalculateForce(0, masses[i].position); - masses[i].force = Vector3Add(masses[i].force, force); - }; + auto solve_octree = [&](const int i) + { + const Vector3 force = tree.calculate_force(0, masses[i].position); + masses[i].force = Vector3Add(masses[i].force, force); + }; // Calculate forces using Barnes-Hut #ifdef THREADPOOL - BS::multi_future loop_future = - threads.submit_loop(0, masses.size(), solve_octree, 256); - loop_future.wait(); + const BS::multi_future loop_future = threads.submit_loop(0, masses.size(), solve_octree, 256); + loop_future.wait(); #else - for (std::size_t i = 0; i < masses.size(); ++i) { - solve_octree(i); - } + for (size_t i = 0; i < masses.size(); ++i) { + solve_octree(i); + } #endif } -auto MassSpringSystem::VerletUpdate(float delta_time) -> void { +auto mass_spring_system::verlet_update(const float delta_time) -> void +{ #ifdef TRACY - ZoneScoped; + ZoneScoped; #endif - for (auto &mass : masses) { - mass.VerletUpdate(delta_time); - } + for (auto& mass : masses) { + mass.verlet_update(delta_time); + } } -auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state) - -> void { +auto mass_spring_system::center_masses() -> void +{ + Vector3 mean = Vector3Zero(); + for (const auto& mass : masses) { + mean += mass.position; + } + mean /= masses.size(); + + for (auto& mass : masses) { + mass.position -= mean; + } +} + +auto threaded_physics::physics_thread(physics_state& state) -> void +{ #ifdef THREADPOOL - BS::this_thread::set_os_thread_name("physics"); + BS::this_thread::set_os_thread_name("physics"); #endif - MassSpringSystem mass_springs; + mass_spring_system mass_springs; - const auto visitor = overloads{ - [&](const struct AddMass &am) { mass_springs.AddMass(); }, - [&](const struct AddSpring &as) { mass_springs.AddSpring(as.a, as.b); }, - [&](const struct ClearGraph &cg) { mass_springs.Clear(); }, - }; + const auto visitor = overloads{ + [&](const struct add_mass& am) { mass_springs.add_mass(); }, + [&](const struct add_spring& as) { mass_springs.add_spring(as.a, as.b); }, + [&](const struct clear_graph& cg) { mass_springs.clear(); }, + }; - std::chrono::time_point last = std::chrono::high_resolution_clock::now(); - std::chrono::duration physics_accumulator(0); - std::chrono::duration ups_accumulator(0); - unsigned int loop_iterations = 0; + std::chrono::time_point last = std::chrono::high_resolution_clock::now(); + std::chrono::duration physics_accumulator(0); + std::chrono::duration ups_accumulator(0); + int loop_iterations = 0; - while (state.running.load()) { + while (state.running.load()) { #ifdef TRACY - FrameMarkStart("PhysicsThread"); + FrameMarkStart("PhysicsThread"); #endif - // Time tracking - std::chrono::time_point now = std::chrono::high_resolution_clock::now(); - std::chrono::duration deltatime = now - last; - physics_accumulator += deltatime; - ups_accumulator += deltatime; - last = now; + // Time tracking + std::chrono::time_point now = std::chrono::high_resolution_clock::now(); + const std::chrono::duration deltatime = now - last; + physics_accumulator += deltatime; + ups_accumulator += deltatime; + last = now; - // Handle queued commands + // Handle queued commands + { +#ifdef TRACY + std::lock_guard lock(state.command_mtx); +#else + std::lock_guard lock(state.command_mtx); +#endif + while (!state.pending_commands.empty()) { + command& cmd = state.pending_commands.front(); + cmd.visit(visitor); + state.pending_commands.pop(); + } + } + + if (mass_springs.masses.empty()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + + // Physics update + if (physics_accumulator.count() > TIMESTEP) { + mass_springs.clear_forces(); + mass_springs.calculate_spring_forces(); + 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(); + + ++loop_iterations; + physics_accumulator -= std::chrono::duration(TIMESTEP); + } + +// Publish the positions for the renderer (copy) +#ifdef TRACY + FrameMarkStart("PhysicsThreadProduceLock"); +#endif + { +#ifdef TRACY + std::unique_lock lock(state.data_mtx); +#else + std::unique_lock lock(state.data_mtx); +#endif + 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; + } + + if (ups_accumulator.count() > 1.0) { + // Update each second + state.ups = loop_iterations; + loop_iterations = 0; + ups_accumulator = std::chrono::duration(0); + } + if (mass_springs.tree.nodes.empty()) { + state.mass_center = Vector3Zero(); + } else { + state.mass_center = mass_springs.tree.nodes.at(0).mass_center; + } + + state.masses.clear(); + state.masses.reserve(mass_springs.masses.size()); + for (const auto& mass : mass_springs.masses) { + state.masses.emplace_back(mass.position); + } + + state.mass_count = mass_springs.masses.size(); + state.spring_count = mass_springs.springs.size(); + + state.data_ready = true; + state.data_consumed = false; + } + // Notify the rendering thread that new data is available + state.data_ready_cnd.notify_all(); +#ifdef TRACY + FrameMarkEnd("PhysicsThreadProduceLock"); + + FrameMarkEnd("PhysicsThread"); +#endif + } +} + +auto threaded_physics::add_mass_cmd() -> void +{ { #ifdef TRACY - std::lock_guard lock(state.command_mtx); + std::lock_guard lock(state.command_mtx); #else - std::lock_guard lock(state.command_mtx); + std::lock_guard lock(state.command_mtx); #endif - while (!state.pending_commands.empty()) { - Command &cmd = state.pending_commands.front(); - cmd.visit(visitor); - state.pending_commands.pop(); - } + state.pending_commands.emplace(add_mass{}); } +} - if (mass_springs.masses.empty()) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - continue; - } - - // Physics update - if (physics_accumulator.count() > TIMESTEP) { - mass_springs.ClearForces(); - mass_springs.CalculateSpringForces(); - mass_springs.CalculateRepulsionForces(); - mass_springs.VerletUpdate(TIMESTEP * SIM_SPEED); - - ++loop_iterations; - physics_accumulator -= std::chrono::duration(TIMESTEP); - } - - // Publish the positions for the renderer (copy) -#ifdef TRACY - FrameMarkStart("PhysicsThreadProduceLock"); -#endif +auto threaded_physics::add_spring_cmd(const size_t a, const size_t b) -> void +{ { #ifdef TRACY - std::unique_lock lock(state.data_mtx); + std::lock_guard lock(state.command_mtx); #else - std::unique_lock lock(state.data_mtx); + std::lock_guard lock(state.command_mtx); #endif - 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; - } - - if (ups_accumulator.count() > 1.0) { - // Update each second - state.ups = loop_iterations; - loop_iterations = 0; - ups_accumulator = std::chrono::duration(0); - } - if (mass_springs.octree.nodes.size() > 0) { - state.mass_center = mass_springs.octree.nodes.at(0).mass_center; - } else { - state.mass_center = Vector3Zero(); - } - - state.masses.clear(); - state.masses.reserve(mass_springs.masses.size()); - for (const auto &mass : mass_springs.masses) { - state.masses.emplace_back(mass.position); - } - - state.data_ready = true; - state.data_consumed = false; + state.pending_commands.emplace(add_spring{a, b}); } - // Notify the rendering thread that new data is available - state.data_ready_cnd.notify_all(); -#ifdef TRACY - FrameMarkEnd("PhysicsThreadProduceLock"); - - FrameMarkEnd("PhysicsThread"); -#endif - } } -auto ThreadedPhysics::AddMassCmd() -> void { - { +auto threaded_physics::clear_cmd() -> void +{ + { #ifdef TRACY - std::lock_guard lock(state.command_mtx); + std::lock_guard lock(state.command_mtx); #else - std::lock_guard lock(state.command_mtx); + std::lock_guard lock(state.command_mtx); #endif - state.pending_commands.push(AddMass{}); - } -} - -auto ThreadedPhysics::AddSpringCmd(std::size_t a, std::size_t b) -> void { - { -#ifdef TRACY - std::lock_guard lock(state.command_mtx); -#else - std::lock_guard lock(state.command_mtx); -#endif - state.pending_commands.push(AddSpring{a, b}); - } -} - -auto ThreadedPhysics::ClearCmd() -> void { - { -#ifdef TRACY - std::lock_guard lock(state.command_mtx); -#else - std::lock_guard lock(state.command_mtx); -#endif - state.pending_commands.push(ClearGraph{}); - } -} - -auto ThreadedPhysics::AddMassSpringsCmd( - std::size_t num_masses, - const std::vector> &springs) -> void { - { -#ifdef TRACY - std::lock_guard lock(state.command_mtx); -#else - std::lock_guard lock(state.command_mtx); -#endif - for (std::size_t i = 0; i < num_masses; ++i) { - state.pending_commands.push(AddMass{}); + state.pending_commands.emplace(clear_graph{}); + } +} + +auto threaded_physics::add_mass_springs_cmd(const size_t num_masses, + const std::vector>& springs) -> void +{ + { +#ifdef TRACY + std::lock_guard lock(state.command_mtx); +#else + std::lock_guard lock(state.command_mtx); +#endif + for (size_t i = 0; i < num_masses; ++i) { + state.pending_commands.emplace(add_mass{}); + } + for (const auto& [from, to] : springs) { + state.pending_commands.emplace(add_spring{from, to}); + } } - for (const auto &[from, to] : springs) { - state.pending_commands.push(AddSpring{from, to}); - } - } } diff --git a/src/puzzle.cpp b/src/puzzle.cpp index 415ea45..6a8a034 100644 --- a/src/puzzle.cpp +++ b/src/puzzle.cpp @@ -1,396 +1,509 @@ #include "puzzle.hpp" -#include "config.hpp" #include #ifdef TRACY -#include "tracy.hpp" -#include + #include #endif -auto Block::Hash() const -> std::size_t { - std::string s = std::format("{},{},{},{}", x, y, width, height); - return std::hash{}(s); +auto puzzle::block::hash() const -> size_t +{ + const std::string s = std::format("{},{},{},{}", x, y, width, height); + return std::hash{}(s); } -auto Block::Invalid() -> Block { - Block block = Block(0, 0, 1, 1, false, false); - block.width = 0; - block.height = 0; - return block; +auto puzzle::block::valid() const -> bool +{ + return width > 0 && height > 0 && x >= 0 && x + width <= 9 && y >= 0 && y + height <= 9; } -auto Block::IsValid() const -> bool { return width != 0 && height != 0; } - -auto Block::ToString() const -> std::string { - if (target) { - return std::format("{}{}", - static_cast(width + static_cast('a') - 1), - static_cast(height + static_cast('a') - 1)); - } else if (immovable) { - return std::format("{}{}", - static_cast(width + static_cast('A') - 1), - static_cast(height + static_cast('A') - 1)); - } else { +auto puzzle::block::string() const -> std::string +{ + if (target) { + return std::format("{}{}", static_cast(width + static_cast('a') - 1), + static_cast(height + static_cast('a') - 1)); + } + if (immovable) { + return std::format("{}{}", static_cast(width + static_cast('A') - 1), + static_cast(height + static_cast('A') - 1)); + } return std::format("{}{}", width, height); - } } -auto Block::GetPrincipalDirs() const -> int { - if (immovable) { - return 0; - } - - if (width > height) { - return Direction::EAS | Direction::WES; - } else if (height > width) { - return Direction::NOR | Direction::SOU; - } else { - return Direction::NOR | Direction::EAS | Direction::SOU | Direction::WES; - } -} - -auto Block::Covers(int xx, int yy) const -> bool { - return xx >= x && xx < x + width && yy >= y && yy < y + height; -} - -auto Block::Collides(const Block &other) const -> bool { - return x < other.x + other.width && x + width > other.x && - y < other.y + other.height && y + height > other.y; -} - -auto State::Hash() const -> std::size_t { - return std::hash{}(state); -} - -auto State::HasWinCondition() const -> bool { - return target_x != 9 && target_y != 9; -} - -auto State::IsWon() const -> bool { - if (!HasWinCondition()) { - return false; - } - - for (const auto &block : *this) { - if (block.target) { - return block.x == target_x && block.y == target_y; - } - } - - return false; -} - -auto State::SetGoal(int x, int y) -> bool { - Block target_block = GetTargetBlock(); - if (!target_block.IsValid() || x < 0 || x + target_block.width > width || - y < 0 || y + target_block.height > height) { - return false; - } - - if (target_x == x && target_y == y) { - target_x = 9; - target_y = 9; - } else { - target_x = x; - target_y = y; - } - - state.replace(3, 1, std::format("{}", target_x)); - state.replace(4, 1, std::format("{}", target_y)); - - return true; -} - -auto State::ClearGoal() -> void { - target_x = 9; - target_y = 9; - state.replace(3, 2, "99"); -} - -auto State::AddColumn() const -> State { - State newstate = State(width + 1, height, restricted); - - for (const auto &block : *this) { - newstate.AddBlock(block); - } - - return newstate; -} - -auto State::RemoveColumn() const -> State { - State newstate = State(width - 1, height, restricted); - - for (const auto &block : *this) { - newstate.AddBlock(block); - } - - return newstate; -} - -auto State::AddRow() const -> State { - State newstate = State(width, height + 1, restricted); - - for (const auto &block : *this) { - newstate.AddBlock(block); - } - - return newstate; -} - -auto State::RemoveRow() const -> State { - State newstate = State(width, height - 1, restricted); - - for (const auto &block : *this) { - newstate.AddBlock(block); - } - - return newstate; -} - -auto State::AddBlock(const Block &block) -> bool { - if (block.x + block.width > width || block.y + block.height > height) { - return false; - } - - for (Block b : *this) { - if (b.Collides(block)) { - return false; - } - } - - int index = GetIndex(block.x, block.y); - state.replace(index, 2, block.ToString()); - - return true; -} - -auto State::GetBlock(int x, int y) const -> Block { - if (x >= width || y >= height) { - return Block::Invalid(); - } - - for (Block b : *this) { - if (b.Covers(x, y)) { - return b; - } - } - - return Block::Invalid(); -} - -auto State::GetBlockAt(int x, int y) const -> std::string { - return state.substr(GetIndex(x, y), 2); -} - -auto State::GetTargetBlock() const -> Block { - for (Block b : *this) { - if (b.target) { - return b; - } - } - - return Block::Invalid(); -} - -auto State::GetIndex(int x, int y) const -> int { - return prefix + (y * width + x) * 2; -} - -auto State::RemoveBlock(int x, int y) -> bool { - Block block = GetBlock(x, y); - if (!block.IsValid()) { - return false; - } - - int index = GetIndex(block.x, block.y); - state.replace(index, 2, ".."); - - return true; -} - -auto State::ToggleTarget(int x, int y) -> bool { - Block block = GetBlock(x, y); - if (!block.IsValid() || block.immovable) { - return false; - } - - // Remove the current target - int index; - for (const auto &b : *this) { - if (b.target) { - index = GetIndex(b.x, b.y); - state.replace(index, 2, - Block(b.x, b.y, b.width, b.height, false).ToString()); - break; - } - } - - // Add the new target - block.target = !block.target; - index = GetIndex(block.x, block.y); - state.replace(index, 2, block.ToString()); - - return true; -} - -auto State::ToggleWall(int x, int y) -> bool { - Block block = GetBlock(x, y); - if (!block.IsValid() || block.target) { - return false; - } - - // Add the new target - block.immovable = !block.immovable; - int index = GetIndex(block.x, block.y); - state.replace(index, 2, block.ToString()); - - return true; -} - -auto State::ToggleRestricted() -> void { - restricted = !restricted; - state.replace(0, 1, restricted ? "R" : "F"); -} - -auto State::MoveBlockAt(int x, int y, Direction dir) -> bool { - Block block = GetBlock(x, y); - if (!block.IsValid() || block.immovable) { - return false; - } - - int dirs = restricted ? block.GetPrincipalDirs() - : Direction::NOR | Direction::EAS | Direction::SOU | - Direction::WES; - - // Get target block - int _target_x = block.x; - int _target_y = block.y; - switch (dir) { - case Direction::NOR: - if (!(dirs & Direction::NOR) || _target_y < 1) { - return false; - } - _target_y--; - break; - case Direction::EAS: - if (!(dirs & Direction::EAS) || _target_x + block.width >= width) { - return false; - } - _target_x++; - break; - case Direction::SOU: - if (!(dirs & Direction::SOU) || _target_y + block.height >= height) { - return false; - } - _target_y++; - break; - case Direction::WES: - if (!(dirs & Direction::WES) || _target_x < 1) { - return false; - } - _target_x--; - break; - } - Block target = - Block(_target_x, _target_y, block.width, block.height, block.target); - - // Check collisions - for (Block b : *this) { - if (b != block && b.Collides(target)) { - return false; - } - } - - RemoveBlock(x, y); - AddBlock(target); - - return true; -} - -auto State::GetNextStates() const -> std::vector { - std::vector new_states; - - for (const Block &b : *this) { - int dirs = restricted ? b.GetPrincipalDirs() - : Direction::NOR | Direction::EAS | Direction::SOU | - Direction::WES; - - if (b.immovable) { - continue; +auto puzzle::block::principal_dirs() const -> int +{ + if (immovable) { + return 0; } - if (dirs & Direction::NOR) { - State north = *this; - if (north.MoveBlockAt(b.x, b.y, Direction::NOR)) { - new_states.push_back(north); - } + if (width > height) { + return eas | wes; } - - if (dirs & Direction::EAS) { - State east = *this; - if (east.MoveBlockAt(b.x, b.y, Direction::EAS)) { - new_states.push_back(east); - } + if (height > width) { + return nor | sou; } - - if (dirs & Direction::SOU) { - State south = *this; - if (south.MoveBlockAt(b.x, b.y, Direction::SOU)) { - new_states.push_back(south); - } - } - - if (dirs & Direction::WES) { - State west = *this; - if (west.MoveBlockAt(b.x, b.y, Direction::WES)) { - new_states.push_back(west); - } - } - } - - return new_states; + return nor | eas | sou | wes; } -auto State::Closure() const - -> std::pair, - std::vector>> { +auto puzzle::block::covers(const int _x, const int _y) const -> bool +{ + return _x >= x && _x < x + width && _y >= y && _y < y + height; +} + +auto puzzle::block::collides(const block& b) const -> bool +{ + return x < b.x + b.width && x + width > b.x && y < b.y + b.height && y + height > b.y; +} + +auto puzzle::get_index(const int x, const int y) const -> int +{ + if (x < 0 || x >= width || y < 0 || y >= height) { + errln("Trying to calculating index of invalid board coordinates ({}, {})", x, y); + exit(1); + } + return PREFIX + (y * width + x) * 2; +} + +auto puzzle::hash() const -> size_t +{ + return std::hash{}(state); +} + +auto puzzle::has_win_condition() const -> bool +{ + return target_x != MAX_WIDTH && target_y != MAX_HEIGHT; +} + +auto puzzle::won() const -> bool +{ + const std::optional& b = try_get_target_block(); + return has_win_condition() && b && b->x == target_x && b->y == target_y; +} + +auto puzzle::valid() const -> bool +{ + return width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT; +} + +auto puzzle::valid_thorough() const -> bool +{ + if (has_win_condition() && !try_get_target_block()) { + return false; + } + + infoln("Validating puzzle {}", state); + + if (static_cast(state.length()) != width * height * 2 + PREFIX) { + infoln("Puzzle invalid: Representation length {} doesn't match {}x{} board", state.length(), width, height); + return false; + } + + // Check prefix + if (!std::string("FR").contains(state[0])) { + infoln("Puzzle invalid: Representation[0] {} doesn't match [FR]", state[0]); + return false; + } + if (restricted && state[0] != 'R') { + infoln("Puzzle invalid: Representation[0] {} doesn't match restricted={}", state[0], restricted); + return false; + } + if (!std::string("3456789").contains(state[1]) || !std::string("3456789").contains(state[2])) { + infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match [3-9]", state[1], state[2]); + return false; + } + 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; + } + if (!std::string("012345678").contains(state[3]) || !std::string("012345678").contains(state[4])) { + infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match [1-9]", state[1], state[2]); + return false; + } + 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[1], state[2], target_x, + target_y); + return false; + } + + // Check blocks + const std::string allowed_chars = ".123456789abcdefghiABCDEFGHI"; + 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; + } + } + + 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 success; +} + +auto puzzle::try_get_block(const int x, const int y) const -> std::optional +{ + if (!covers(x, y)) { + return std::nullopt; + } + + for (const block& b : *this) { + if (b.covers(x, y)) { + return b; + } + } + + return std::nullopt; +} + +auto puzzle::try_get_target_block() const -> std::optional +{ + for (const block b : *this) { + if (b.target) { + return b; + } + } + + return std::nullopt; +} + +auto puzzle::covers(const int x, const int y, const int w, const int h) const -> bool +{ + return x >= 0 && x + w <= width && y >= 0 && y + h <= height; +} + +auto puzzle::covers(const int x, const int y) const -> bool +{ + return covers(x, y, 1, 1); +} + +auto puzzle::covers(const block& b) const -> bool +{ + return covers(b.x, b.y, b.width, b.height); +} + +auto puzzle::try_toggle_restricted() const -> std::optional +{ + puzzle p = *this; + p.restricted = !restricted; + p.state.replace(0, 1, p.restricted ? "R" : "F"); + return p; +} + +auto puzzle::try_set_goal(const int x, const int y) const -> std::optional +{ + const std::optional& b = try_get_target_block(); + if (!b || !covers(x, y, b->width, b->height)) { + return std::nullopt; + } + + puzzle p = *this; + if (target_x == x && target_y == y) { + p.target_x = MAX_WIDTH; + p.target_y = MAX_HEIGHT; + } else { + p.target_x = x; + p.target_y = y; + } + + p.state.replace(3, 1, std::format("{}", target_x)); + p.state.replace(4, 1, std::format("{}", target_y)); + + return p; +} + +auto puzzle::try_clear_goal() const -> std::optional +{ + puzzle p = *this; + p.target_x = MAX_WIDTH; + p.target_y = MAX_HEIGHT; + p.state.replace(3, 2, "99"); + return p; +} + +auto puzzle::try_add_column() const -> std::optional +{ + if (width >= MAX_WIDTH) { + return std::nullopt; + } + + puzzle p = {width + 1, height, restricted}; + + // Non-fitting blocks won't be added + for (const block& b : *this) { + if (const std::optional& _p = p.try_add_block(b)) { + p = *_p; + } + } + + return p; +} + +auto puzzle::try_remove_column() const -> std::optional +{ + if (width <= MIN_WIDTH) { + return std::nullopt; + } + + puzzle p = {width - 1, height, restricted}; + + // Non-fitting blocks won't be added + for (const block& b : *this) { + if (const std::optional& _p = p.try_add_block(b)) { + p = *_p; + } + } + + return p; +} + +auto puzzle::try_add_row() const -> std::optional +{ + if (height >= 9) { + return std::nullopt; + } + + puzzle p = puzzle(width, height + 1, restricted); + + for (const block& b : *this) { + if (const std::optional& _p = p.try_add_block(b)) { + p = *_p; + } + } + + return p; +} + +auto puzzle::try_remove_row() const -> std::optional +{ + if (height == 0) { + return std::nullopt; + } + + puzzle p = puzzle(width, height - 1, restricted); + + for (const block& b : *this) { + if (const std::optional& _p = p.try_add_block(b)) { + p = *_p; + } + } + + return p; +} + +auto puzzle::try_add_block(const block& b) const -> std::optional +{ + if (!covers(b)) { + return std::nullopt; + } + + for (block _b : *this) { + if (_b.collides(b)) { + return std::nullopt; + } + } + + puzzle p = *this; + const int index = get_index(b.x, b.y); + p.state.replace(index, 2, b.string()); + + return p; +} + +auto puzzle::try_remove_block(const int x, const int y) const -> std::optional +{ + const std::optional& b = try_get_block(x, y); + if (!b) { + return std::nullopt; + } + + puzzle p = *this; + const int index = get_index(b->x, b->y); + p.state.replace(index, 2, ".."); + + return p; +} + +auto puzzle::try_toggle_target(const int x, const int y) const -> std::optional +{ + std::optional b = try_get_block(x, y); + if (!b || b->immovable) { + return std::nullopt; + } + + // Remove the current target if it exists + puzzle p = *this; + if (const std::optional& _b = try_get_target_block()) { + const int index = get_index(_b->x, _b->y); + p.state.replace(index, 2, block(_b->x, _b->y, _b->width, _b->height, false).string()); + } + + // Add the new target + b->target = !b->target; + const int index = get_index(b->x, b->y); + p.state.replace(index, 2, b->string()); + + return p; +} + +auto puzzle::try_toggle_wall(const int x, const int y) const -> std::optional +{ + std::optional b = try_get_block(x, y); + if (!b || b->target) { + return std::nullopt; + } + + // Add the new target + puzzle p = *this; + b->immovable = !b->immovable; + const int index = get_index(b->x, b->y); + p.state.replace(index, 2, b->string()); + + return p; +} + +auto puzzle::try_move_block_at(const int x, const int y, const direction dir) const -> std::optional +{ + const std::optional& b = try_get_block(x, y); + if (!b || b->immovable) { + return std::nullopt; + } + + const int dirs = restricted ? b->principal_dirs() : nor | eas | sou | wes; + + // Get target block + int _target_x = b->x; + int _target_y = b->y; + switch (dir) { + case nor: + if (!(dirs & nor) || _target_y < 1) { + return std::nullopt; + } + _target_y--; + break; + case eas: + if (!(dirs & eas) || _target_x + b->width >= width) { + return std::nullopt; + } + _target_x++; + break; + case sou: + if (!(dirs & sou) || _target_y + b->height >= height) { + return std::nullopt; + } + _target_y++; + break; + case wes: + if (!(dirs & wes) || _target_x < 1) { + return std::nullopt; + } + _target_x--; + break; + } + const block moved_b = block(_target_x, _target_y, b->width, b->height, b->target); + + // Check collisions + for (const block& _b : *this) { + if (_b != b && _b.collides(moved_b)) { + return std::nullopt; + } + } + + std::optional p = try_remove_block(x, y); + if (!p) { + return std::nullopt; + } + + p = p->try_add_block(moved_b); + if (!p) { + return std::nullopt; + } + + return p; +} + +auto puzzle::find_adjacent_puzzles() const -> std::vector +{ + std::vector puzzles; + + for (const block& b : *this) { + if (b.immovable) { + continue; + } + + const int dirs = restricted ? b.principal_dirs() : nor | eas | sou | wes; + + if (dirs & nor) { + if (const std::optional& north = try_move_block_at(b.x, b.y, nor)) { + puzzles.push_back(*north); + } + } + + if (dirs & eas) { + if (const std::optional& east = try_move_block_at(b.x, b.y, eas)) { + puzzles.push_back(*east); + } + } + + if (dirs & sou) { + if (const std::optional& south = try_move_block_at(b.x, b.y, sou)) { + puzzles.push_back(*south); + } + } + + if (dirs & wes) { + if (const std::optional& west = try_move_block_at(b.x, b.y, wes)) { + puzzles.push_back(*west); + } + } + } + + // for (const puzzle& p : puzzles) { + // println("Adjacent puzzle: {}", p.state); + // } + + return puzzles; +} + +auto puzzle::explore_state_space() const -> std::pair, std::vector>> +{ #ifdef TRACY - ZoneScoped; + ZoneScoped; #endif - std::vector states; - std::vector> links; + infoln("Exploring state space, this might take a while..."); - // Helper to construct the links vector - std::unordered_map state_indices; + std::vector state_pool; + std::unordered_map state_indices; // Helper to construct the links vector + std::vector> links; - // Buffer for all states we want to call GetNextStates() on - std::unordered_set remaining_states; - remaining_states.insert(*this); + // Buffer for all states we want to call GetNextStates() on + std::unordered_set remaining_states; + remaining_states.insert(*this); - do { - const State current = *remaining_states.begin(); - remaining_states.erase(current); + do { + const puzzle current = *remaining_states.begin(); + remaining_states.erase(current); - if (!state_indices.contains(current)) { - state_indices.emplace(current, states.size()); - states.push_back(current); - } + if (!state_indices.contains(current)) { + state_indices.emplace(current, state_pool.size()); + state_pool.push_back(current); + } - for (const State &s : current.GetNextStates()) { - if (!state_indices.contains(s)) { - remaining_states.insert(s); - state_indices.emplace(s, states.size()); - states.push_back(s); - } - links.emplace_back(state_indices.at(current), state_indices.at(s)); - } - } while (remaining_states.size() > 0); + for (const puzzle& s : current.find_adjacent_puzzles()) { + if (!state_indices.contains(s)) { + remaining_states.insert(s); + state_indices.emplace(s, state_pool.size()); + state_pool.push_back(s); + } + links.emplace_back(state_indices.at(current), state_indices.at(s)); + } + } while (!remaining_states.empty()); - std::cout << std::format("State space has size {} with {} transitions.", - states.size(), links.size()) - << std::endl; + infoln("State space has size {} with {} transitions.", state_pool.size(), links.size()); - return std::make_pair(states, links); + return std::make_pair(state_pool, links); } diff --git a/src/renderer.cpp b/src/renderer.cpp index 1a9f33a..1b0f71a 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -1,244 +1,226 @@ #include "renderer.hpp" #include "config.hpp" -#include "puzzle.hpp" -#include #include #include #include #ifdef TRACY -#include "tracy.hpp" -#include + #include #endif -#ifdef BATCHING -#include -#endif - -auto Renderer::UpdateTextureSizes() -> void { - if (!IsWindowResized()) { - return; - } - - UnloadRenderTexture(render_target); - UnloadRenderTexture(klotski_target); - UnloadRenderTexture(menu_target); - - int width = GetScreenWidth() / 2.0; - int height = GetScreenHeight() - MENU_HEIGHT; - - render_target = LoadRenderTexture(width, height); - klotski_target = LoadRenderTexture(width, height); - menu_target = LoadRenderTexture(width * 2, MENU_HEIGHT); -} - -auto Renderer::AllocateGraphInstancing(std::size_t size) -> void { - cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE); - - instancing_shader = LoadShader("shader/instancing_vertex.glsl", - "shader/instancing_fragment.glsl"); - instancing_shader.locs[SHADER_LOC_MATRIX_MVP] = - GetShaderLocation(instancing_shader, "mvp"); - instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] = - GetShaderLocation(instancing_shader, "viewPos"); - - vertex_mat = LoadMaterialDefault(); - vertex_mat.maps[MATERIAL_MAP_DIFFUSE].color = VERTEX_COLOR; - vertex_mat.shader = instancing_shader; - - transforms = (Matrix *)MemAlloc(size * sizeof(Matrix)); - transforms_size = size; -} - -auto Renderer::ReallocateGraphInstancingIfNecessary(std::size_t size) -> void { - if (transforms_size != size) { - transforms = (Matrix *)MemRealloc(transforms, size * sizeof(Matrix)); - transforms_size = size; - } -} - -auto Renderer::DrawMassSprings(const std::vector &masses) -> void { -#ifdef TRACY - ZoneScoped; -#endif - - // Prepare cube instancing - { -#ifdef TRACY - ZoneNamedN(prepare_masses, "PrepareMasses", true); -#endif - if (masses.size() < DRAW_VERTICES_LIMIT) { - if (transforms == nullptr) { - AllocateGraphInstancing(masses.size()); - } - ReallocateGraphInstancingIfNecessary(masses.size()); - - int i = 0; - for (const Vector3 &mass : masses) { - transforms[i] = MatrixTranslate(mass.x, mass.y, mass.z); - ++i; - } +auto renderer::update_texture_sizes() -> void +{ + if (!IsWindowResized()) { + return; } - } - BeginTextureMode(render_target); - ClearBackground(RAYWHITE); + UnloadRenderTexture(render_target); + UnloadRenderTexture(klotski_target); + UnloadRenderTexture(menu_target); - BeginMode3D(camera.camera); + const int width = GetScreenWidth() / 2; + const int height = GetScreenHeight() - MENU_HEIGHT; - // Draw springs (batched) - { + render_target = LoadRenderTexture(width, height); + klotski_target = LoadRenderTexture(width, height); + menu_target = LoadRenderTexture(width * 2, MENU_HEIGHT); +} + +auto renderer::draw_mass_springs(const std::vector& masses) -> void +{ #ifdef TRACY - ZoneNamedN(draw_springs, "DrawSprings", true); + ZoneScoped; #endif + + if (masses.size() != state.get_state_count()) { + // Because the physics run in a different thread, it might need time to catch up + return; + } + + // Prepare connection batching + { +#ifdef TRACY + ZoneNamedN(prepare_masses, "PrepareConnectionsBatching", true); +#endif + + connections.clear(); + connections.reserve(state.get_target_count()); + if (input.connect_solutions) { + for (const size_t& _state : state.get_winning_indices()) { + const Vector3& current_mass = masses.at(state.get_current_index()); + const Vector3& winning_mass = masses.at(_state); + connections.emplace_back(current_mass, winning_mass); + DrawLine3D(current_mass, winning_mass, Fade(TARGET_BLOCK_COLOR, 0.5)); + } + } + } + + // Prepare cube instancing + { +#ifdef TRACY + ZoneNamedN(prepare_masses, "PrepareMassInstancing", true); +#endif + + if (masses.size() < DRAW_VERTICES_LIMIT) { + // Don't have to reserve, capacity is already set to DRAW_VERTICES_LIMIT in constructor + transforms.clear(); + colors.clear(); + + size_t mass = 0; + for (const auto& [x, y, z] : masses) { + transforms.emplace_back(MatrixTranslate(x, y, z)); + + // Normal vertex + Color c = VERTEX_COLOR; + 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)) { + // Path vertex + c = VERTEX_PATH_COLOR; + } else if (mass == state.get_starting_index()) { + // Starting vertex + c = VERTEX_START_COLOR; + } else if (state.get_visit_counts().at(mass) > 0) { + // Visited vertex + c = VERTEX_VISITED_COLOR; + } + // Current vertex is drawn as individual cube to increase its size + + colors.emplace_back(c); + ++mass; + } + } + + rlUpdateVertexBuffer(color_vbo_id, colors.data(), colors.size() * sizeof(Color), 0); + } + + BeginTextureMode(render_target); + ClearBackground(RAYWHITE); + BeginMode3D(camera.camera); + + // Draw springs (batched) + { +#ifdef TRACY + ZoneNamedN(draw_springs, "DrawSprings", true); +#endif + + rlBegin(RL_LINES); + for (const auto& [from, to] : state.get_links()) { + if (masses.size() > from && masses.size() > to) { + const auto& [ax, ay, az] = masses.at(from); + const auto& [bx, by, bz] = masses.at(to); + rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a); + rlVertex3f(ax, ay, az); + rlVertex3f(bx, by, bz); + } + } + rlEnd(); + } + + // Draw masses (instanced) + { +#ifdef TRACY + ZoneNamedN(draw_masses, "DrawMasses", true); +#endif + + if (masses.size() < DRAW_VERTICES_LIMIT) { + // NOTE: I don't know if drawing all this inside a shader would make it + // much faster... The amount of data sent to the GPU would be + // reduced (just positions instead of matrices), but is this + // noticable for < 100000 cubes? + DrawMeshInstanced(cube_instance, vertex_mat, transforms.data(), + masses.size()); // NOLINT(*-narrowing-conversions) + } + } + + // Connect current to winning states (batched) + const auto [r, g, b, a] = Fade(VERTEX_CURRENT_COLOR, 0.3); rlBegin(RL_LINES); - for (const auto &[from, to] : state.springs) { - if (masses.size() > from && masses.size() > to) { - const Vector3 &a = masses.at(from); - const Vector3 &b = masses.at(to); - rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a); - rlVertex3f(a.x, a.y, a.z); - rlVertex3f(b.x, b.y, b.z); - } + for (const auto& [from, to] : connections) { + const auto& [ax, ay, az] = from; + const auto& [bx, by, bz] = to; + rlColor4ub(r, g, b, a); + rlVertex3f(ax, ay, az); + rlVertex3f(bx, by, bz); } rlEnd(); - } - // Draw masses (instanced) - { -#ifdef TRACY - ZoneNamedN(draw_masses, "DrawMasses", true); -#endif - if (masses.size() < DRAW_VERTICES_LIMIT) { - // NOTE: I don't know if drawing all this inside a shader would make it - // much faster... The amount of data sent to the GPU would be - // reduced (just positions instead of matrices), but is this - // noticable for < 100000 cubes? - DrawMeshInstanced(cube_instance, vertex_mat, transforms, masses.size()); + // Mark current state + 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); } - } - // Mark winning states - if (input.mark_solutions || input.connect_solutions) { - for (const State &_state : state.winning_states) { - - std::size_t winning_index = state.states.at(_state); - if (masses.size() > winning_index) { - - const Vector3 &winning_mass = masses.at(winning_index); - if (input.mark_solutions) { - DrawCube(winning_mass, 2 * VERTEX_SIZE, 2 * VERTEX_SIZE, - 2 * VERTEX_SIZE, VERTEX_TARGET_COLOR); - } - - std::size_t current_index = state.CurrentMassIndex(); - if (input.connect_solutions && masses.size() > current_index) { - const Vector3 ¤t_mass = masses.at(current_index); - DrawLine3D(winning_mass, current_mass, Fade(TARGET_BLOCK_COLOR, 0.5)); - } - } - } - } - - // Mark visited states - for (const auto &[_state, visits] : state.visited_states) { - std::size_t visited_index = state.states.at(_state); - - if (masses.size() > visited_index) { - const Vector3 &visited_mass = masses.at(visited_index); - DrawCube(visited_mass, VERTEX_SIZE * 1.5, VERTEX_SIZE * 1.5, - VERTEX_SIZE * 1.5, VERTEX_VISITED_COLOR); - } - } - - // Mark winning path - if (input.mark_path) { - for (const std::size_t &_state : state.winning_path) { - if (masses.size() > _state) { - const Vector3 &path_mass = masses.at(_state); - DrawCube(path_mass, VERTEX_SIZE * 1.75, VERTEX_SIZE * 1.75, - VERTEX_SIZE * 1.75, VERTEX_PATH_COLOR); - } - } - } - - // Mark starting state - std::size_t starting_index = state.states.at(state.starting_state); - if (masses.size() > starting_index) { - const Vector3 &starting_mass = masses.at(starting_index); - DrawCube(starting_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2, - VERTEX_START_COLOR); - } - - // Mark current state - std::size_t current_index = state.states.at(state.current_state); - if (masses.size() > current_index) { - const Vector3 ¤t_mass = masses.at(current_index); - DrawCube(current_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2, - VERTEX_CURRENT_COLOR); - } - - EndMode3D(); - EndTextureMode(); + EndMode3D(); + EndTextureMode(); } -auto Renderer::DrawKlotski() -> void { +auto renderer::draw_klotski() const -> void +{ #ifdef TRACY - ZoneScoped; + ZoneScoped; #endif - BeginTextureMode(klotski_target); - ClearBackground(RAYWHITE); + BeginTextureMode(klotski_target); + ClearBackground(RAYWHITE); - gui.DrawPuzzleBoard(); + gui.draw_puzzle_board(); - EndTextureMode(); + EndTextureMode(); } -auto Renderer::DrawMenu(const std::vector &masses) -> void { +auto renderer::draw_menu() const -> void +{ #ifdef TRACY - ZoneScoped; + ZoneScoped; #endif - BeginTextureMode(menu_target); - ClearBackground(RAYWHITE); + BeginTextureMode(menu_target); + ClearBackground(RAYWHITE); - gui.DrawMainMenu(); + gui.draw_main_menu(); - EndTextureMode(); + EndTextureMode(); } -auto Renderer::DrawTextures(int fps, int ups) -> void { - BeginDrawing(); +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, - -1 * menu_target.texture.height), - Vector2(0, 0), WHITE); - DrawTextureRec(klotski_target.texture, - Rectangle(0, 0, klotski_target.texture.width, - -1 * klotski_target.texture.height), - Vector2(0, MENU_HEIGHT), WHITE); - DrawTextureRec(render_target.texture, - Rectangle(0, 0, render_target.texture.width, - -1 * render_target.texture.height), - Vector2(GetScreenWidth() / 2.0, MENU_HEIGHT), WHITE); + 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), + Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT), WHITE); - // Draw borders - DrawRectangleLinesEx(Rectangle(0, 0, GetScreenWidth(), MENU_HEIGHT), 1.0, - BLACK); - DrawRectangleLinesEx(Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0, - GetScreenHeight() - MENU_HEIGHT), - 1.0, BLACK); - DrawRectangleLinesEx(Rectangle(GetScreenWidth() / 2.0, MENU_HEIGHT, - GetScreenWidth() / 2.0, - GetScreenHeight() - MENU_HEIGHT), - 1.0, BLACK); + // 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, + BLACK); - gui.DrawGraphOverlay(fps, ups); - gui.DrawSavePresetPopup(); - gui.Update(); + gui.draw_graph_overlay(fps, ups, mass_count, spring_count); + gui.draw_save_preset_popup(); + gui.update(); - EndDrawing(); + EndDrawing(); +} + +auto renderer::render(const std::vector& masses, const int fps, const int ups, const size_t mass_count, + const size_t spring_count) -> void +{ + update_texture_sizes(); + + draw_mass_springs(masses); + draw_klotski(); + draw_menu(); + draw_textures(fps, ups, mass_count, spring_count); } diff --git a/src/state.cpp b/src/state.cpp deleted file mode 100644 index aa4e320..0000000 --- a/src/state.cpp +++ /dev/null @@ -1,292 +0,0 @@ -#include "state.hpp" -#include "config.hpp" -#include "distance.hpp" - -#include -#include -#include - -#ifdef TRACY -#include "tracy.hpp" -#include -#endif - -auto StateManager::ParsePresetFile(const std::string &_preset_file) -> bool { - preset_file = _preset_file; - - std::ifstream file(preset_file); - if (!file) { - std::cout << std::format("Preset file \"{}\" couldn't be loaded.", - preset_file) - << std::endl; - return false; - } - - std::string line; - std::vector comment_lines; - std::vector preset_lines; - while (std::getline(file, line)) { - if (line.starts_with("F") || line.starts_with("R")) { - preset_lines.push_back(line); - } else if (line.starts_with("#")) { - comment_lines.push_back(line); - } - } - - if (preset_lines.size() == 0 || comment_lines.size() != preset_lines.size()) { - std::cout << std::format("Preset file \"{}\" couldn't be loaded.", - preset_file) - << std::endl; - return false; - } - - presets.clear(); - for (const auto &preset : preset_lines) { - presets.emplace_back(preset); - } - comments = comment_lines; - - std::cout << std::format("Loaded {} presets from \"{}\".", - preset_lines.size(), preset_file) - << std::endl; - - return true; -} - -auto StateManager::AppendPresetFile(const std::string preset_name) -> void { - std::cout << std::format("Saving preset \"{}\" to \"{}\"", preset_name, - preset_file) - << std::endl; - - std::ofstream file(preset_file, std::ios_base::app | std::ios_base::out); - if (!file) { - std::cout << std::format("Preset file \"{}\" couldn't be loaded.", - preset_file) - << std::endl; - return; - } - - file << "\n# " << preset_name << "\n" << current_state.state << std::flush; - - std::cout << std::format("Refreshing presets...") << std::endl; - if (ParsePresetFile(preset_file)) { - LoadPreset(presets.size() - 1); - } -} - -auto StateManager::LoadPreset(int preset) -> void { - current_preset = preset; - current_state = presets.at(current_preset); - ClearGraph(); - edited = false; -} - -auto StateManager::ResetState() -> void { - current_state = presets.at(current_preset); - previous_state = current_state; - for (auto &[state, visits] : visited_states) { - visits = 0; - } - visited_states[current_state]++; - history = std::stack(); - total_moves = 0; - if (edited || !states.contains(current_state)) { - // We also need to clear the graph in case the state has been edited - // because the graph could contain states that are impossible to reach - // now. - ClearGraph(); - } - edited = false; -} - -auto StateManager::PreviousPreset() -> void { - LoadPreset((presets.size() + current_preset - 1) % presets.size()); -} - -auto StateManager::NextPreset() -> void { - LoadPreset((current_preset + 1) % presets.size()); -} - -auto StateManager::NextPath() -> void { - if (target_distances.Empty()) { - return; - } - - // Already there - if (target_distances.distances[CurrentMassIndex()] == 0) { - return; - } - - std::size_t parent = target_distances.parents[CurrentMassIndex()]; - current_state = masses.at(parent); - FindTargetPath(); -} - -auto StateManager::FillGraph() -> void { -#ifdef TRACY - ZoneScoped; -#endif - - ClearGraph(); - - std::pair, - std::vector>> - closure = current_state.Closure(); - - physics.ClearCmd(); - physics.AddMassSpringsCmd(closure.first.size(), closure.second); - for (const State &state : closure.first) { - states.insert(std::make_pair(state, states.size())); - masses.insert(std::make_pair(states.size() - 1, state)); - } - for (const auto &[from, to] : closure.second) { - springs.emplace_back(from, to); - } - FindWinningStates(); - FindTargetDistances(); - FindTargetPath(); - - // Sanity check. Both values need to be equal - // for (const auto &[mass, state] : masses) { - // std::cout << std::format("Masses: {}, States: {}", mass, - // states.at(state)) << std::endl; - // } -} - -auto StateManager::UpdateGraph() -> void { - if (previous_state == current_state) { - return; - } - - if (!states.contains(current_state)) { - states.insert(std::make_pair(current_state, states.size())); - masses.insert(std::make_pair(states.size() - 1, current_state)); - springs.emplace_back(states.at(current_state), states.at(previous_state)); - physics.AddMassCmd(); - physics.AddSpringCmd(states.at(current_state), states.at(previous_state)); - - if (current_state.IsWon()) { - winning_states.insert(current_state); - } - FindTargetDistances(); - } - - // Adds the element with 0 if it doesn't exist - visited_states[current_state]++; - total_moves++; - - if (history.size() > 0 && history.top() == current_state) { - // We don't pop the stack when moving backwards to indicate if we need to - // push or pop here - history.pop(); - } else { - history.push(previous_state); - } - - FindTargetPath(); - previous_state = current_state; -} - -auto StateManager::ClearGraph() -> void { - states.clear(); - winning_states.clear(); - visited_states.clear(); - masses.clear(); - winning_path.clear(); - springs.clear(); - // history = std::stack(); - target_distances.Clear(); - physics.ClearCmd(); - - // Re-add the default stuff to the graph - states.insert(std::make_pair(current_state, states.size())); - masses.insert(std::make_pair(states.size() - 1, current_state)); - visited_states.insert(std::make_pair(current_state, 1)); - physics.AddMassCmd(); - - // These states are no longer in the graph - previous_state = current_state; - starting_state = current_state; -} - -auto StateManager::FindWinningStates() -> void { - winning_states.clear(); - for (const auto &[state, mass] : states) { - if (state.IsWon()) { - winning_states.insert(state); - } - } -} - -auto StateManager::FindTargetDistances() -> void { -#ifdef TRACY - ZoneScoped; -#endif - - if (springs.size() == 0 || winning_states.size() == 0) { - return; - } - - // Find target indices - std::vector targets; - targets.reserve(winning_states.size()); - for (const auto &_state : winning_states) { - targets.push_back(states.at(_state)); - } - - target_distances = CalculateDistances(states.size(), springs, targets); - - // std::cout << std::format("Calculated {} distances to {} targets.", - // target_distances.distances.size(), targets.size()) << std::endl; -} - -auto StateManager::FindTargetPath() -> void { - if (target_distances.Empty()) { - return; - } - - winning_path = GetPath(target_distances, CurrentMassIndex()); - // std::cout << std::format("Nearest target is {} moves away.", - // winning_path.size()) << std::endl; -} - -auto StateManager::FindWorstState() -> State { - if (target_distances.Empty()) { - return current_state; - } - - int max = 0; - int index = 0; - for (std::size_t i = 0; i < target_distances.distances.size(); ++i) { - if (target_distances.distances.at(i) > max) { - max = target_distances.distances.at(i); - index = i; - } - } - - return masses.at(index); -} - -auto StateManager::GoToWorst() -> void { current_state = FindWorstState(); } - -auto StateManager::GoToNearestTarget() -> void { - if (target_distances.Empty()) { - return; - } - - current_state = - masses.at(target_distances.nearest_targets.at(CurrentMassIndex())); -} - -auto StateManager::PopHistory() -> void { - if (history.size() == 0) { - return; - } - - current_state = history.top(); - // history.pop(); // Done in UpdateGraph(); -} - -auto StateManager::CurrentMassIndex() const -> std::size_t { - return states.at(current_state); -} diff --git a/src/state_manager.cpp b/src/state_manager.cpp new file mode 100644 index 0000000..be38115 --- /dev/null +++ b/src/state_manager.cpp @@ -0,0 +1,485 @@ +#include "state_manager.hpp" +#include "distance.hpp" +#include "util.hpp" + +#include +#include + +#ifdef TRACY + #include +#endif + +auto state_manager::synced_try_insert_state(const puzzle& state) -> size_t +{ + if (state_indices.contains(state)) { + return state_indices.at(state); + } + + const size_t index = state_pool.size(); + state_pool.emplace_back(state); + state_indices.emplace(state, index); + visit_counts[index] = 0; + + // Queue an update to the physics engine state to keep in sync + physics.add_mass_cmd(); + + return index; +} + +auto state_manager::synced_insert_link(size_t first_index, size_t second_index) -> void +{ + links.emplace_back(first_index, second_index); + + // Queue an update to the physics engine state to keep in sync + physics.add_spring_cmd(first_index, second_index); +} + +auto state_manager::synced_insert_statespace(const std::vector& states, + const std::vector>& _links) -> void +{ + if (!state_pool.empty() || !state_indices.empty() || !links.empty()) { + warnln("Inserting statespace but collections haven't been cleared"); + } + + for (const puzzle& state : states) { + const size_t index = state_pool.size(); + state_pool.emplace_back(state); + state_indices.emplace(state, index); + visit_counts[index] = 0; + } + for (const auto& [from, to] : _links) { + links.emplace_back(from, to); + } + + // Queue an update to the physics engine state to keep in sync + physics.add_mass_springs_cmd(state_pool.size(), links); +} + +auto state_manager::synced_clear_statespace() -> void +{ + // Those are invalid without any states + current_state_index = -1; + previous_state_index = -1; + starting_state_index = -1; + + state_pool.clear(); + state_indices.clear(); + links.clear(); + node_target_distances.clear(); + winning_indices.clear(); + winning_path.clear(); + path_indices.clear(); + + // move_history does not get cleared here, but when resetting the board + visit_counts.clear(); + + // Queue an update to the physics engine state to keep in sync + physics.clear_cmd(); +} + +auto state_manager::parse_preset_file(const std::string& _preset_file) -> bool +{ + preset_file = _preset_file; + + std::ifstream file(preset_file); + if (!file) { + infoln("Preset file \"{}\" couldn't be loaded.", preset_file); + return false; + } + + std::string line; + std::vector comment_lines; + std::vector preset_lines; + while (std::getline(file, line)) { + if (line.starts_with("F") || line.starts_with("R")) { + preset_lines.push_back(line); + } else if (line.starts_with("#")) { + comment_lines.push_back(line); + } + } + + if (preset_lines.empty() || comment_lines.size() != preset_lines.size()) { + infoln("Preset file \"{}\" couldn't be loaded.", preset_file); + return false; + } + + preset_states.clear(); + for (const auto& preset : preset_lines) { + const puzzle& p = puzzle(preset); + + if (!p.valid_thorough()) { + preset_states = {puzzle(4, 5, 9, 9, false)}; + infoln("Preset file \"{}\" contained invalid presets.", preset_file); + return false; + } + preset_states.emplace_back(p); + } + preset_comments = comment_lines; + + infoln("Loaded {} presets from \"{}\".", preset_lines.size(), preset_file); + + return true; +} + +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()) { + return false; + } + + std::ofstream file(preset_file, std::ios_base::app | std::ios_base::out); + if (!file) { + infoln("Preset file \"{}\" couldn't be loaded.", preset_file); + return false; + } + + file << "\n# " << preset_name << "\n" << get_current_state().state << std::flush; + + infoln("Refreshing presets..."); + if (parse_preset_file(preset_file)) { + load_preset(preset_states.size() - 1); + } + + return true; +} + +auto state_manager::load_preset(const size_t preset) -> void +{ + clear_graph_and_add_current(preset_states.at(preset)); + current_preset = preset; +} + +auto state_manager::load_previous_preset() -> void +{ + load_preset((preset_states.size() + current_preset - 1) % preset_states.size()); +} + +auto state_manager::load_next_preset() -> void +{ + load_preset((current_preset + 1) % preset_states.size()); +} + +auto state_manager::update_current_state(const puzzle& p) -> void +{ + if (!p.valid()) { + return; + } + + const size_t size_before = state_pool.size(); + + // If state is a duplicate, index will be the existing index, + // if state is new, index will be state_pool.size() - 1 + const size_t index = synced_try_insert_state(p); + + // 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 + synced_insert_link(current_state_index, index); + } + + previous_state_index = current_state_index; + current_state_index = index; + + if (current_state_index != previous_state_index) { + move_history.push(previous_state_index); + } + + if (p.won()) { + winning_indices.insert(current_state_index); + } + + // Adds the element with 0 if it doesn't exist + visit_counts[current_state_index]++; + total_moves++; + + // Recalculate distances only if the graph changed + if (state_pool.size() > size_before) { + populate_node_target_distances(); + } + populate_winning_path(); +} + +auto state_manager::edit_starting_state(const puzzle& p) -> void +{ + clear_graph_and_add_current(p); + + move_history = std::stack(); + total_moves = 0; + for (int& visits : visit_counts | std::views::values) { + visits = 0; + } + visit_counts[current_state_index]++; +} + +auto state_manager::goto_starting_state() -> void +{ + update_current_state(get_state(starting_state_index)); + + // Reset previous movement data since we're starting over (because we're fucking stupid) + previous_state_index = current_state_index; + for (int& visits : visit_counts | std::views::values) { + visits = 0; + } + visit_counts[current_state_index]++; + move_history = std::stack(); + total_moves = 0; +} + +auto state_manager::goto_optimal_next_state() -> void +{ + if (node_target_distances.empty()) { + return; + } + + // Already there + if (node_target_distances.distances[current_state_index] == 0) { + return; + } + + const size_t parent_index = node_target_distances.parents[current_state_index]; + update_current_state(get_state(parent_index)); +} + +auto state_manager::goto_previous_state() -> void +{ + if (move_history.empty()) { + return; + } + + update_current_state(get_state(move_history.top())); + + // Pop twice because update_current_state adds the state again... + move_history.pop(); + move_history.pop(); +} + +auto state_manager::goto_most_distant_state() -> void +{ + if (node_target_distances.empty()) { + return; + } + + int max_distance = 0; + size_t max_distance_index = 0; + for (size_t i = 0; i < node_target_distances.distances.size(); ++i) { + if (node_target_distances.distances.at(i) > max_distance) { + max_distance = node_target_distances.distances.at(i); + max_distance_index = i; + } + } + + update_current_state(get_state(max_distance_index)); +} + +auto state_manager::goto_closest_target_state() -> void +{ + if (node_target_distances.empty()) { + return; + } + + update_current_state(get_state(node_target_distances.nearest_targets.at(current_state_index))); +} + +auto state_manager::populate_graph() -> void +{ +#ifdef TRACY + ZoneScoped; +#endif + + // Need to make a copy before clearing the state_pool + const puzzle p = get_current_state(); + + // Clear the graph first so we don't add duplicates somehow + synced_clear_statespace(); + + // Explore the entire statespace starting from the current state + const auto& [states, _links] = p.explore_state_space(); + synced_insert_statespace(states, _links); + + current_state_index = state_indices.at(p); + previous_state_index = current_state_index; + starting_state_index = current_state_index; + + // Search for cool stuff + populate_winning_indices(); + populate_node_target_distances(); + populate_winning_path(); +} + +auto state_manager::clear_graph_and_add_current(const puzzle& p) -> void +{ + // Need to make a copy before clearing the state_pool + const puzzle _p = p; + + synced_clear_statespace(); + + // Re-add the current state + current_state_index = synced_try_insert_state(_p); + + // These states are no longer in the graph + previous_state_index = current_state_index; + starting_state_index = current_state_index; + + visit_counts[current_state_index]++; +} + +auto state_manager::clear_graph_and_add_current() -> void +{ + clear_graph_and_add_current(get_current_state()); +} + +auto state_manager::populate_winning_indices() -> void +{ + winning_indices.clear(); + for (const auto& [state, index] : state_indices) { + if (state.won()) { + winning_indices.insert(index); + } + } +} + +auto state_manager::populate_node_target_distances() -> void +{ +#ifdef TRACY + ZoneScoped; +#endif + + if (links.empty() || winning_indices.empty()) { + return; + } + + const std::vector targets(winning_indices.begin(), winning_indices.end()); + node_target_distances.calculate_distances(state_pool.size(), links, targets); +} + +auto state_manager::populate_winning_path() -> void +{ + if (node_target_distances.empty()) { + return; + } + + winning_path = node_target_distances.get_shortest_path(current_state_index); + + path_indices.clear(); + for (const size_t index : winning_path) { + path_indices.insert(index); + } +} + +auto state_manager::get_index(const puzzle& state) const -> size_t +{ + return state_indices.at(state); +} + +auto state_manager::get_current_index() const -> size_t +{ + return current_state_index; +} + +auto state_manager::get_starting_index() const -> size_t +{ + return starting_state_index; +} + +auto state_manager::get_state(const size_t index) const -> const puzzle& +{ + return state_pool.at(index); +} + +auto state_manager::get_current_state() const -> const puzzle& +{ + return get_state(current_state_index); +} + +auto state_manager::get_starting_state() const -> const puzzle& +{ + return get_state(starting_state_index); +} + +auto state_manager::get_state_count() const -> size_t +{ + return state_pool.size(); +} + +auto state_manager::get_target_count() const -> size_t +{ + return winning_indices.size(); +} + +auto state_manager::get_link_count() const -> size_t +{ + return links.size(); +} + +auto state_manager::get_path_length() const -> size_t +{ + return winning_path.size(); +} + +auto state_manager::get_links() const -> const std::vector>& +{ + return links; +} + +auto state_manager::get_winning_indices() const -> const std::unordered_set& +{ + return winning_indices; +} + +auto state_manager::get_visit_counts() const -> const std::unordered_map& +{ + return visit_counts; +} + +auto state_manager::get_winning_path() const -> const std::vector& +{ + return winning_path; +} + +auto state_manager::get_path_indices() const -> const std::unordered_set& +{ + return path_indices; +} + +auto state_manager::get_current_visits() const -> int +{ + return visit_counts.at(current_state_index); +} + +auto state_manager::get_current_preset() const -> size_t +{ + return current_preset; +} + +auto state_manager::get_preset_count() const -> size_t +{ + return preset_states.size(); +} + +auto state_manager::get_current_preset_comment() const -> const std::string& +{ + return preset_comments.at(current_preset); +} + +auto state_manager::has_history() const -> bool +{ + return !move_history.empty(); +} + +auto state_manager::has_distances() const -> bool +{ + return !node_target_distances.empty(); +} + +auto state_manager::get_total_moves() const -> size_t +{ + return total_moves; +} + +auto state_manager::was_edited() const -> bool +{ + return preset_states.at(current_preset) != get_state(starting_state_index); +} diff --git a/src/tracy.cpp b/src/tracy.cpp deleted file mode 100644 index 37a0669..0000000 --- a/src/tracy.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "config.hpp" - -#ifdef TRACY - -#include "tracy.hpp" -#include - -void *operator new(std::size_t count) { - auto ptr = malloc(count); - TracyAllocS(ptr, count, 20); - return ptr; -} -void operator delete(void *ptr) noexcept { - TracyFreeS(ptr, 20); - free(ptr); -} -void operator delete(void *ptr, std::size_t count) noexcept { - TracyFreeS(ptr, 20); - free(ptr); -} - -#endif diff --git a/src/user_interface.cpp b/src/user_interface.cpp new file mode 100644 index 0000000..3191d16 --- /dev/null +++ b/src/user_interface.cpp @@ -0,0 +1,785 @@ +#include "user_interface.hpp" +#include "config.hpp" + +#include + +#define RAYGUI_IMPLEMENTATION +#include + +#ifdef TRACY + #include +#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 +{ + x = _x; + y = _y; + width = _width; + height = _height; + columns = _columns; + rows = _rows; +} + +auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height) -> void +{ + x = _x; + y = _y; + width = _width; + height = _height; +} + +auto user_interface::grid::update_bounds(const int _x, const int _y) -> void +{ + x = _x; + y = _y; +} + +auto user_interface::grid::bounds() const -> Rectangle +{ + Rectangle bounds{0, 0, static_cast(columns), static_cast(rows)}; + bounds.x -= padding; + bounds.y -= padding; + bounds.width += 2 * padding; + bounds.height += 2 * padding; + return bounds; +} + +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."); + exit(1); + } + + 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); +} + +auto user_interface::grid::square_bounds() const -> Rectangle +{ + Rectangle bounds = square_bounds(0, 0, columns, rows); + bounds.x -= padding; + bounds.y -= padding; + bounds.width += 2 * padding; + bounds.height += 2 * padding; + return bounds; +} + +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 + + if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) { + errln("Grid bounds are outside range."); + exit(1); + } + + const int available_width = width - padding * (columns + 1); + const int available_height = height - padding * (rows + 1); + const int cell_size = std::min(available_width / columns, available_height / rows); + + const int grid_width = cell_size * columns + padding * (columns + 1); + const int grid_height = cell_size * rows + padding * (rows + 1); + 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)); +} + +auto user_interface::init() -> void +{ + const Font font = LoadFontEx(FONT, FONT_SIZE, nullptr, 0); + SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR); + GuiSetFont(font); + + default_style style = get_default_style(); + style.text_size = FONT_SIZE; + apply_color(style, GRAY); + + set_default_style(style); +} + +auto user_interface::apply_color(style& style, const Color color) -> void +{ + style.base_color_normal = ColorToInt(Fade(color, 0.8)); + style.base_color_focused = ColorToInt(Fade(color, 0.3)); + style.base_color_pressed = ColorToInt(Fade(color, 0.8)); + style.base_color_disabled = ColorToInt(Fade(color, 0.5)); + + style.border_color_normal = ColorToInt(Fade(color, 1.0)); + style.border_color_focused = ColorToInt(Fade(color, 0.7)); + style.border_color_pressed = ColorToInt(Fade(color, 1.0)); + style.border_color_disabled = ColorToInt(Fade(GRAY, 0.5)); + + style.text_color_normal = ColorToInt(Fade(BLACK, 1.0)); + style.text_color_focused = ColorToInt(Fade(BLACK, 1.0)); + style.text_color_pressed = ColorToInt(Fade(BLACK, 1.0)); + style.text_color_disabled = ColorToInt(Fade(BLACK, 0.5)); +} + +auto user_interface::apply_block_color(style& style, const Color color) -> void +{ + style.base_color_normal = ColorToInt(Fade(color, 0.5)); + style.base_color_focused = ColorToInt(Fade(color, 0.3)); + style.base_color_pressed = ColorToInt(Fade(color, 0.8)); + style.base_color_disabled = ColorToInt(Fade(color, 0.5)); + + style.border_color_normal = ColorToInt(Fade(color, 1.0)); + style.border_color_focused = ColorToInt(Fade(color, 0.7)); + style.border_color_pressed = ColorToInt(Fade(color, 1.0)); + style.border_color_disabled = ColorToInt(Fade(GRAY, 0.5)); +} + +auto user_interface::apply_text_color(style& style, const Color color) -> void +{ + style.text_color_normal = ColorToInt(Fade(color, 1.0)); + style.text_color_focused = ColorToInt(Fade(color, 1.0)); + style.text_color_pressed = ColorToInt(Fade(color, 1.0)); + style.text_color_disabled = ColorToInt(Fade(BLACK, 0.5)); +} + +auto user_interface::get_default_style() -> default_style +{ + // Could've iterated over the values, but then it wouldn't be as nice to + // access... + return {{GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL), GuiGetStyle(DEFAULT, BASE_COLOR_NORMAL), + GuiGetStyle(DEFAULT, TEXT_COLOR_NORMAL), GuiGetStyle(DEFAULT, BORDER_COLOR_FOCUSED), + GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED), GuiGetStyle(DEFAULT, TEXT_COLOR_FOCUSED), + GuiGetStyle(DEFAULT, BORDER_COLOR_PRESSED), GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED), + GuiGetStyle(DEFAULT, TEXT_COLOR_PRESSED), GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED), + GuiGetStyle(DEFAULT, BASE_COLOR_DISABLED), GuiGetStyle(DEFAULT, TEXT_COLOR_DISABLED)}, + GuiGetStyle(DEFAULT, BACKGROUND_COLOR), + GuiGetStyle(DEFAULT, LINE_COLOR), + GuiGetStyle(DEFAULT, TEXT_SIZE), + GuiGetStyle(DEFAULT, TEXT_SPACING), + GuiGetStyle(DEFAULT, TEXT_LINE_SPACING), + GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL), + GuiGetStyle(DEFAULT, TEXT_WRAP_MODE)}; +} + +auto user_interface::set_default_style(const default_style& style) -> void +{ + GuiSetStyle(DEFAULT, BORDER_COLOR_NORMAL, style.border_color_normal); + GuiSetStyle(DEFAULT, BASE_COLOR_NORMAL, style.base_color_normal); + GuiSetStyle(DEFAULT, TEXT_COLOR_NORMAL, style.text_color_normal); + + GuiSetStyle(DEFAULT, BORDER_COLOR_FOCUSED, style.border_color_focused); + GuiSetStyle(DEFAULT, BASE_COLOR_FOCUSED, style.base_color_focused); + GuiSetStyle(DEFAULT, TEXT_COLOR_FOCUSED, style.text_color_focused); + + GuiSetStyle(DEFAULT, BORDER_COLOR_PRESSED, style.border_color_pressed); + GuiSetStyle(DEFAULT, BASE_COLOR_PRESSED, style.base_color_pressed); + GuiSetStyle(DEFAULT, TEXT_COLOR_PRESSED, style.text_color_pressed); + + GuiSetStyle(DEFAULT, BORDER_COLOR_DISABLED, style.border_color_disabled); + GuiSetStyle(DEFAULT, BASE_COLOR_DISABLED, style.base_color_disabled); + GuiSetStyle(DEFAULT, TEXT_COLOR_DISABLED, style.text_color_disabled); + + GuiSetStyle(DEFAULT, BACKGROUND_COLOR, style.background_color); + GuiSetStyle(DEFAULT, LINE_COLOR, style.line_color); + + GuiSetStyle(DEFAULT, TEXT_SIZE, style.text_size); + GuiSetStyle(DEFAULT, TEXT_SPACING, style.text_spacing); + GuiSetStyle(DEFAULT, TEXT_LINE_SPACING, style.text_line_spacing); + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, style.text_alignment_vertical); + GuiSetStyle(DEFAULT, TEXT_WRAP_MODE, style.text_wrap_mode); +} + +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)}; +} + +auto user_interface::set_component_style(const int component, const component_style& style) -> void +{ + GuiSetStyle(component, BORDER_COLOR_NORMAL, style.border_color_normal); + GuiSetStyle(component, BASE_COLOR_NORMAL, style.base_color_normal); + GuiSetStyle(component, TEXT_COLOR_NORMAL, style.text_color_normal); + + GuiSetStyle(component, BORDER_COLOR_FOCUSED, style.border_color_focused); + GuiSetStyle(component, BASE_COLOR_FOCUSED, style.base_color_focused); + GuiSetStyle(component, TEXT_COLOR_FOCUSED, style.text_color_focused); + + GuiSetStyle(component, BORDER_COLOR_PRESSED, style.border_color_pressed); + GuiSetStyle(component, BASE_COLOR_PRESSED, style.base_color_pressed); + GuiSetStyle(component, TEXT_COLOR_PRESSED, style.text_color_pressed); + + GuiSetStyle(component, BORDER_COLOR_DISABLED, style.border_color_disabled); + GuiSetStyle(component, BASE_COLOR_DISABLED, style.base_color_disabled); + GuiSetStyle(component, TEXT_COLOR_DISABLED, style.text_color_disabled); + + GuiSetStyle(component, BORDER_WIDTH, style.border_width); + GuiSetStyle(component, TEXT_PADDING, style.text_padding); + 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 +{ + // Save original styling + const default_style original_default = get_default_style(); + const component_style original_button = get_component_style(BUTTON); + + // Change styling + default_style style_default = original_default; + component_style style_button = original_button; + style_default.text_size = font_size; + apply_color(style_button, color); + set_default_style(style_default); + set_component_style(BUTTON, style_button); + + const int _state = GuiGetState(); + if (!enabled || window_open()) { + GuiSetState(STATE_DISABLED); + } + const int pressed = GuiButton(bounds, label.data()); + if (!enabled || window_open()) { + GuiSetState(_state); + } + + // Restore original styling + set_default_style(original_default); + set_component_style(BUTTON, original_button); + + return pressed; +} + +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 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 +{ + // Save original styling + const default_style original_default = get_default_style(); + const component_style original_slider = get_component_style(SLIDER); + const component_style original_toggle = get_component_style(TOGGLE); + + // Change styling + default_style style_default = original_default; + component_style style_slider = original_slider; + component_style style_toggle = original_toggle; + style_default.text_size = font_size; + apply_color(style_slider, color); + apply_color(style_toggle, color); + set_default_style(style_default); + set_component_style(SLIDER, style_slider); + set_component_style(TOGGLE, style_toggle); + + const int _state = GuiGetState(); + if (!enabled || window_open()) { + GuiSetState(STATE_DISABLED); + } + int pressed = GuiToggleSlider(bounds, std::format("{};{}", off_label, on_label).data(), active); + if (!enabled || window_open()) { + GuiSetState(_state); + } + + // Restore original styling + set_default_style(original_default); + set_component_style(SLIDER, original_slider); + set_component_style(TOGGLE, original_toggle); + + 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 +{ + 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 +{ + // Save original styling + const default_style original_default = get_default_style(); + const component_style original_valuebox = get_component_style(VALUEBOX); + const component_style original_button = get_component_style(BUTTON); + + // Change styling + default_style style_default = original_default; + component_style style_valuebox = original_valuebox; + component_style style_button = original_button; + style_default.text_size = font_size; + apply_color(style_valuebox, color); + apply_color(style_button, color); + set_default_style(style_default); + set_component_style(VALUEBOX, style_valuebox); + set_component_style(BUTTON, style_button); + + const int _state = GuiGetState(); + if (!enabled || window_open()) { + GuiSetState(STATE_DISABLED); + } + int pressed = GuiSpinner(bounds, "", label.data(), value, min, max, false); + if (!enabled || window_open()) { + GuiSetState(_state); + } + + // Restore original styling + set_default_style(original_default); + set_component_style(VALUEBOX, original_valuebox); + set_component_style(BUTTON, style_button); + + return pressed; +} + +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 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 +{ + // Save original styling + const default_style original_default = get_default_style(); + const component_style original_label = get_component_style(LABEL); + + // Change styling + default_style style_default = original_default; + component_style style_label = original_label; + style_default.text_size = font_size; + apply_text_color(style_label, color); + set_default_style(style_default); + set_component_style(LABEL, style_label); + + const int _state = GuiGetState(); + if (!enabled || window_open()) { + GuiSetState(STATE_DISABLED); + } + const int pressed = GuiLabel(bounds, text.data()); + if (!enabled || window_open()) { + GuiSetState(_state); + } + + // Restore original styling + set_default_style(original_default); + set_component_style(LABEL, original_label); + + 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 +{ + component_style s = get_component_style(BUTTON); + apply_block_color(s, color); + + const Rectangle bounds = board_grid.square_bounds(x, y, width, height); + + const bool focused = CheckCollisionPointRec(input.mouse - Vector2(0, MENU_HEIGHT), bounds); + const bool pressed = puzzle::block(x, y, width, height, false).covers(input.sel_x, input.sel_y); + + // Background to make faded colors work + DrawRectangleRec(bounds, RAYWHITE); + + Color base = GetColor(s.base_color_normal); + Color border = GetColor(s.base_color_normal); + if (pressed) { + base = GetColor(s.base_color_pressed); + border = GetColor(s.base_color_pressed); + } + if (focused) { + base = GetColor(s.base_color_focused); + border = GetColor(s.base_color_focused); + } + if (focused && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { + base = GetColor(s.base_color_pressed); + border = GetColor(s.base_color_pressed); + } + if (!enabled) { + base = BOARD_COLOR_RESTRICTED; + } + DrawRectangleRec(bounds, base); + if (enabled) { + DrawRectangleLinesEx(bounds, 2.0, border); + } + + return focused && enabled; +} + +auto user_interface::window_open() const -> bool +{ + return save_window || help_window; +} + +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); + if (preset > static_cast(state.get_current_preset())) { + input.load_next_preset(); + } else if (preset < static_cast(state.get_current_preset())) { + input.load_previous_preset(); + } + + draw_menu_button(1, 0, 1, 1, + std::format("Puzzle: \"{}\"{}", state.get_current_preset_comment().substr(2), + state.was_edited() ? " (Modified)" : ""), + color); + + int editing = input.editing; + draw_menu_toggle_slider(2, 0, 1, 1, "Puzzle Mode (Tab)", "Edit Mode (Tab)", &editing, color); + if (editing != input.editing) { + input.toggle_editing(); + } +} + +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()), + 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); +} + +auto user_interface::draw_graph_controls(const Color color) const -> void +{ + if (draw_menu_button(0, 2, 1, 1, "Populate Graph (G)", color)) { + input.populate_graph(); + } + + // int mark_path = input.mark_path; + // DrawMenuToggleSlider(2, 2, 1, 1, "Path Hidden (U)", "Path Shown (U)", + // &mark_path, color); + // if (mark_path != input.mark_path) { + // input.ToggleMarkPath(); + // } + + if (draw_menu_button(1, 2, 1, 1, "Clear Graph (C)", color)) { + input.clear_graph(); + } + + int mark_solutions = input.mark_solutions; + 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(); + } + input.mark_path = input.mark_solutions; +} + +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); + 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); + 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); + if (projection != (camera.projection == CAMERA_ORTHOGRAPHIC)) { + input.toggle_camera_projection(); + } +} + +auto user_interface::draw_puzzle_controls(const Color color) const -> void +{ + auto nth = [&](const int n) + { + if (n == 11 || n == 12 || n == 13) { + return "th"; + } + if (n % 10 == 1) { + return "st"; + } + if (n % 10 == 2) { + return "nd"; + } + if (n % 10 == 3) { + return "rd"; + } + return "th"; + }; + + 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)), + color); + + if (draw_menu_button(1, 4, 1, 1, "Make Optimal Move (Space)", color, state.has_distances())) { + input.goto_optimal_next_state(); + } + + if (draw_menu_button(2, 4, 1, 1, "Undo Last Move (Backspace)", color, state.has_history())) { + input.goto_previous_state(); + } + + if (draw_menu_button(0, 5, 1, 1, "Go to Nearest Solution (B)", color, state.has_distances())) { + input.goto_closest_target_state(); + } + + if (draw_menu_button(1, 5, 1, 1, "Go to Worst State (V)", color, state.has_distances())) { + input.goto_most_distant_state(); + } + + if (draw_menu_button(2, 5, 1, 1, "Go to Starting State (R)", color)) { + input.goto_starting_state(); + } +} + +auto user_interface::draw_edit_controls(const Color color) const -> void +{ + const puzzle& current = state.get_current_state(); + + // Toggle Target Block + if (draw_menu_button(0, 4, 1, 1, "Toggle Target Block (T)", color)) { + input.toggle_target_block(); + } + + // Toggle Wall Block + if (draw_menu_button(0, 5, 1, 1, "Toggle Wall Block (Y)", color)) { + input.toggle_wall_block(); + } + + // Toggle Restricted/Free Block Movement + int free = !current.restricted; + draw_menu_toggle_slider(1, 4, 1, 1, "Restricted (F)", "Free (F)", &free, color); + if (free != !current.restricted) { + input.toggle_restricted_movement(); + } + + // Clear Goal + if (draw_menu_button(1, 5, 1, 1, "Clear Goal (X)", color)) { + } + + // Column Count Spinner + int columns = current.width; + draw_menu_spinner(2, 4, 1, 1, "Cols: ", &columns, puzzle::MIN_WIDTH, puzzle::MAX_WIDTH, color); + if (columns > current.width) { + input.add_board_column(); + } else if (columns < current.width) { + input.remove_board_column(); + } + + // Row Count Spinner + int rows = current.height; + draw_menu_spinner(2, 5, 1, 1, "Rows: ", &rows, puzzle::MIN_WIDTH, puzzle::MAX_WIDTH, color); + if (rows > current.height) { + input.add_board_row(); + } else if (rows < current.height) { + input.remove_board_row(); + } +} + +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); + + if (draw_menu_button(2, 6, 1, 1, "Save as Preset", color)) { + save_window = true; + } +} + +auto user_interface::get_background_color() -> Color +{ + return GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)); +} + +auto user_interface::help_popup() -> void +{} + +auto user_interface::draw_save_preset_popup() -> void +{ + if (!save_window) { + return; + } + + // 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); + if (button == 1) { + state.append_preset_file(preset_name.data()); + } + if (button == 0 || button == 1 || button == 2) { + save_window = false; + TextCopy(preset_name.data(), "\0"); + } +} + +auto user_interface::draw_main_menu() -> void +{ + menu_grid.update_bounds(0, 0, GetScreenWidth(), MENU_HEIGHT); + + draw_menu_header(GRAY); + draw_graph_info(ORANGE); + draw_graph_controls(RED); + draw_camera_controls(DARKGREEN); + + if (input.editing) { + draw_edit_controls(PURPLE); + } else { + draw_puzzle_controls(BLUE); + } + + draw_menu_footer(GRAY); +} + +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); + + // 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); + + // Draw target opening + // TODO: Only draw single direction (in corner) if restricted (use target block principal direction) + const std::optional 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 Color opening_color = Fade(current.won() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3); + + if (target_x == 0) { + // Left opening + DrawRectangle(x - BOARD_PADDING, y, BOARD_PADDING, height, RAYWHITE); + DrawRectangle(x - BOARD_PADDING, y, BOARD_PADDING, height, opening_color); + } + if (target_x + target_block.value().width == current.width) { + // Right opening + DrawRectangle(x + width, y, BOARD_PADDING, height, RAYWHITE); + DrawRectangle(x + width, y, BOARD_PADDING, height, opening_color); + } + if (target_y == 0) { + // Top opening + DrawRectangle(x, y - BOARD_PADDING, width, BOARD_PADDING, RAYWHITE); + DrawRectangle(x, y - BOARD_PADDING, width, BOARD_PADDING, opening_color); + } + if (target_y + target_block.value().height == current.height) { + // Bottom opening + DrawRectangle(x, y + height, width, BOARD_PADDING, RAYWHITE); + DrawRectangle(x, y + height, width, BOARD_PADDING, opening_color); + } + } + + // Draw empty cells. Also set hovered blocks + input.hov_x = -1; + input.hov_y = -1; + for (int x = 0; x < board_grid.columns; ++x) { + for (int y = 0; y < board_grid.rows; ++y) { + DrawRectangleRec(board_grid.square_bounds(x, y, 1, 1), RAYWHITE); + + Rectangle hov_bounds = board_grid.square_bounds(x, y, 1, 1); + hov_bounds.x -= BOARD_PADDING; + hov_bounds.y -= BOARD_PADDING; + hov_bounds.width += BOARD_PADDING; + hov_bounds.height += BOARD_PADDING; + if (CheckCollisionPointRec(GetMousePosition() - Vector2(0, MENU_HEIGHT), hov_bounds)) { + input.hov_x = x; + input.hov_y = y; + } + } + } + + // Draw blocks + for (const puzzle::block& b : current) { + Color c = BLOCK_COLOR; + if (b.target) { + c = TARGET_BLOCK_COLOR; + } else if (b.immovable) { + c = WALL_COLOR; + } + + if (!b.valid() || b.x < 0 || b.y < 0 || b.width <= 0 || b.height <= 0) { + warnln("Iterator returned invalid block for state \"{}\".", current.state); + continue; + } + + // ReSharper disable once CppExpressionWithoutSideEffects + draw_board_block(b.x, b.y, b.width, b.height, c, !b.immovable); + } + + // 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) { + 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, + 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, + input.hov_y - input.block_add_y + 1, PURPLE); + } + } + } +} + +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, 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); +} + +auto user_interface::update() const -> void +{ + input.disable = window_open(); +}