small refactor
This commit is contained in:
56
.clang-format
Normal file
56
.clang-format
Normal file
@ -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
|
||||
...
|
||||
145
.clang-tidy
Normal file
145
.clang-tidy
Normal file
@ -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'
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@ cmake-build-release
|
||||
/result
|
||||
/.gdb_history
|
||||
/valgrind.log
|
||||
.idea
|
||||
|
||||
@ -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()
|
||||
8
README.md
Normal file
8
README.md
Normal file
@ -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.
|
||||
|
||||

|
||||
|
||||
Build and run on NixOS: `nix run git+https://gitea.local.chriphost.de/christoph/cpp-masssprings`.
|
||||
@ -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" [
|
||||
@ -307,7 +310,7 @@ rec {
|
||||
];
|
||||
};
|
||||
|
||||
# TODO: Doesn't work
|
||||
# TODO: Can't get renderdoc in FHS to work
|
||||
|
||||
# FHS environment for renderdoc. Access with "nix develop .#renderdoc".
|
||||
# https://ryantm.github.io/nixpkgs/builders/special/fhs-environments
|
||||
|
||||
@ -1,31 +1,30 @@
|
||||
#ifndef __CAMERA_HPP_
|
||||
#define __CAMERA_HPP_
|
||||
#ifndef CAMERA_HPP_
|
||||
#define CAMERA_HPP_
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include <raylib.h>
|
||||
#include <raymath.h>
|
||||
|
||||
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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#ifndef __CONFIG_HPP_
|
||||
#define __CONFIG_HPP_
|
||||
#ifndef CONFIG_HPP_
|
||||
#define CONFIG_HPP_
|
||||
|
||||
#include <raylib.h>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,32 +1,24 @@
|
||||
#ifndef __DISTANCE_HPP_
|
||||
#define __DISTANCE_HPP_
|
||||
|
||||
#include "config.hpp"
|
||||
#ifndef DISTANCE_HPP_
|
||||
#define DISTANCE_HPP_
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
struct DistanceResult {
|
||||
// distances[n] = distance from n to target
|
||||
std::vector<int> distances;
|
||||
class graph_distances
|
||||
{
|
||||
public:
|
||||
std::vector<int> distances; // distances[n] = distance from node n to target
|
||||
std::vector<size_t> parents; // parents[n] = next node on the path from node n to target
|
||||
std::vector<size_t> nearest_targets; // nearest_target[n] = closest target node to node n
|
||||
|
||||
// parents[n] = next node on the path from n to target
|
||||
std::vector<std::size_t> parents;
|
||||
public:
|
||||
auto clear() -> void;
|
||||
[[nodiscard]] auto empty() const -> bool;
|
||||
|
||||
// nearest_target[n] = closest target node to n
|
||||
std::vector<std::size_t> nearest_targets;
|
||||
auto calculate_distances(size_t node_count, const std::vector<std::pair<size_t, size_t>>& edges,
|
||||
const std::vector<size_t>& targets) -> void;
|
||||
|
||||
auto Clear() -> void;
|
||||
|
||||
auto Empty() -> bool;
|
||||
[[nodiscard]] auto get_shortest_path(size_t source) const -> std::vector<size_t>;
|
||||
};
|
||||
|
||||
auto CalculateDistances(
|
||||
std::size_t node_count,
|
||||
const std::vector<std::pair<std::size_t, std::size_t>> &edges,
|
||||
const std::vector<std::size_t> &targets) -> DistanceResult;
|
||||
|
||||
auto GetPath(const DistanceResult &result, std::size_t source)
|
||||
-> std::vector<std::size_t>;
|
||||
|
||||
#endif
|
||||
|
||||
184
include/gui.hpp
184
include/gui.hpp
@ -1,184 +0,0 @@
|
||||
#ifndef __GUI_HPP_
|
||||
#define __GUI_HPP_
|
||||
|
||||
#include "camera.hpp"
|
||||
#include "config.hpp"
|
||||
#include "input.hpp"
|
||||
#include "state.hpp"
|
||||
|
||||
#include <raygui.h>
|
||||
#include <raylib.h>
|
||||
|
||||
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<char, 256> 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
|
||||
@ -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 <functional>
|
||||
#include <raylib.h>
|
||||
#include <raymath.h>
|
||||
|
||||
class InputHandler {
|
||||
struct GenericHandler {
|
||||
std::function<void(InputHandler &)> handler;
|
||||
};
|
||||
class input_handler
|
||||
{
|
||||
struct generic_handler
|
||||
{
|
||||
std::function<void(input_handler&)> 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<GenericHandler> generic_handlers;
|
||||
std::vector<MouseHandler> mouse_pressed_handlers;
|
||||
std::vector<MouseHandler> mouse_released_handlers;
|
||||
std::vector<KeyboardHandler> key_pressed_handlers;
|
||||
std::vector<KeyboardHandler> key_released_handlers;
|
||||
std::vector<generic_handler> generic_handlers;
|
||||
std::vector<mouse_handler> mouse_pressed_handlers;
|
||||
std::vector<mouse_handler> mouse_released_handlers;
|
||||
std::vector<keyboard_handler> key_pressed_handlers;
|
||||
std::vector<keyboard_handler> 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<void(InputHandler &)> handler)
|
||||
-> void;
|
||||
// General
|
||||
auto register_generic_handler(const std::function<void(input_handler&)>& handler) -> void;
|
||||
|
||||
auto RegisterMousePressedHandler(MouseButton button,
|
||||
std::function<void(InputHandler &)> handler)
|
||||
-> void;
|
||||
auto register_mouse_pressed_handler(MouseButton button, const std::function<void(input_handler&)>& handler) -> void;
|
||||
|
||||
auto RegisterMouseReleasedHandler(MouseButton button,
|
||||
std::function<void(InputHandler &)> handler)
|
||||
-> void;
|
||||
auto register_mouse_released_handler(MouseButton button, const std::function<void(input_handler&)>& handler)
|
||||
-> void;
|
||||
|
||||
auto RegisterKeyPressedHandler(KeyboardKey key,
|
||||
std::function<void(InputHandler &)> handler)
|
||||
-> void;
|
||||
auto register_key_pressed_handler(KeyboardKey key, const std::function<void(input_handler&)>& handler) -> void;
|
||||
|
||||
auto RegisterKeyReleasedHandler(KeyboardKey key,
|
||||
std::function<void(InputHandler &)> handler)
|
||||
-> void;
|
||||
auto register_key_released_handler(KeyboardKey key, const std::function<void(input_handler&)>& handler) -> void;
|
||||
|
||||
auto HandleInput() -> void;
|
||||
auto handle_input() -> void;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@ -1,54 +1,51 @@
|
||||
#ifndef __OCTREE_HPP_
|
||||
#define __OCTREE_HPP_
|
||||
|
||||
#include "config.hpp"
|
||||
#ifndef OCTREE_HPP_
|
||||
#define OCTREE_HPP_
|
||||
|
||||
#include <array>
|
||||
#include <raylib.h>
|
||||
#include <raymath.h>
|
||||
#include <vector>
|
||||
|
||||
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<int, 8> 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<node> nodes;
|
||||
|
||||
public:
|
||||
auto ChildCount() const -> int;
|
||||
};
|
||||
octree() = default;
|
||||
|
||||
class Octree {
|
||||
public:
|
||||
std::vector<OctreeNode> 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<Vector3, Vector3>;
|
||||
|
||||
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<Vector3, Vector3>;
|
||||
|
||||
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
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
#ifndef __PHYSICS_HPP_
|
||||
#define __PHYSICS_HPP_
|
||||
#ifndef PHYSICS_HPP_
|
||||
#define PHYSICS_HPP_
|
||||
|
||||
#include "config.hpp"
|
||||
#include "octree.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <raylib.h>
|
||||
@ -15,190 +14,198 @@
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#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 <BS_thread_pool.hpp>
|
||||
#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 <BS_thread_pool.hpp>
|
||||
#if defined(_WIN32) // raylib uses these names as function parameters
|
||||
#undef near
|
||||
#undef far
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef TRACY
|
||||
#include <tracy/Tracy.hpp>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#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<BS::tp::none> threads;
|
||||
BS::thread_pool<> threads;
|
||||
#endif
|
||||
|
||||
public:
|
||||
Octree octree;
|
||||
octree tree;
|
||||
|
||||
// This is the main ownership of all the states/masses/springs.
|
||||
std::vector<Mass> masses;
|
||||
std::vector<Spring> springs;
|
||||
// This is the main ownership of all the states/masses/springs.
|
||||
std::vector<mass> masses;
|
||||
std::vector<spring> 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<AddMass, AddSpring, ClearGraph>;
|
||||
struct add_spring
|
||||
{
|
||||
size_t a;
|
||||
size_t b;
|
||||
};
|
||||
|
||||
struct PhysicsState {
|
||||
struct clear_graph
|
||||
{};
|
||||
|
||||
using command = std::variant<add_mass, add_spring, clear_graph>;
|
||||
|
||||
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<Command> pending_commands;
|
||||
std::queue<command> 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<Vector3> 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<Vector3> masses; // Read by renderer
|
||||
bool data_ready = false;
|
||||
bool data_consumed = true;
|
||||
|
||||
std::atomic<bool> running{true};
|
||||
};
|
||||
std::atomic<bool> 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<std::pair<std::size_t, std::size_t>> &springs) -> void;
|
||||
auto add_mass_springs_cmd(size_t num_masses, const std::vector<std::pair<size_t, size_t>>& springs) -> void;
|
||||
};
|
||||
|
||||
// https://en.cppreference.com/w/cpp/utility/variant/visit
|
||||
template <class... Ts> struct overloads : Ts... {
|
||||
using Ts::operator()...;
|
||||
template <class... Ts>
|
||||
struct overloads : Ts...
|
||||
{
|
||||
using Ts::operator()...;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@ -1,128 +1,21 @@
|
||||
#ifndef __PUZZLE_HPP_
|
||||
#define __PUZZLE_HPP_
|
||||
#ifndef PUZZLE_HPP_
|
||||
#define PUZZLE_HPP_
|
||||
|
||||
#include "config.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<char, 9> 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<char, 9> 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<int>(block.at(0)) - static_cast<int>('a') + 1;
|
||||
height = static_cast<int>(block.at(1)) - static_cast<int>('a') + 1;
|
||||
} else if (immovable) {
|
||||
width = static_cast<int>(block.at(0)) - static_cast<int>('A') + 1;
|
||||
height = static_cast<int>(block.at(1)) - static_cast<int>('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<char, 9> 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<char, 9> immovable_chars{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'};
|
||||
for (const char c : immovable_chars) {
|
||||
if (b.contains(c)) {
|
||||
immovable = true;
|
||||
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<int>(b.at(0)) - static_cast<int>('a') + 1;
|
||||
height = static_cast<int>(b.at(1)) - static_cast<int>('a') + 1;
|
||||
} else if (immovable) {
|
||||
width = static_cast<int>(b.at(0)) - static_cast<int>('A') + 1;
|
||||
height = static_cast<int>(b.at(1)) - static_cast<int>('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<int>(_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<int>(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<block>;
|
||||
[[nodiscard]] auto try_get_target_block() const -> std::optional<block>;
|
||||
[[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<puzzle>;
|
||||
[[nodiscard]] auto try_set_goal(int x, int y) const -> std::optional<puzzle>;
|
||||
[[nodiscard]] auto try_clear_goal() const -> std::optional<puzzle>;
|
||||
[[nodiscard]] auto try_add_column() const -> std::optional<puzzle>;
|
||||
[[nodiscard]] auto try_remove_column() const -> std::optional<puzzle>;
|
||||
[[nodiscard]] auto try_add_row() const -> std::optional<puzzle>;
|
||||
[[nodiscard]] auto try_remove_row() const -> std::optional<puzzle>;
|
||||
[[nodiscard]] auto try_add_block(const block& b) const -> std::optional<puzzle>;
|
||||
[[nodiscard]] auto try_remove_block(int x, int y) const -> std::optional<puzzle>;
|
||||
[[nodiscard]] auto try_toggle_target(int x, int y) const -> std::optional<puzzle>;
|
||||
[[nodiscard]] auto try_toggle_wall(int x, int y) const -> std::optional<puzzle>;
|
||||
|
||||
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<puzzle>;
|
||||
|
||||
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<State>;
|
||||
|
||||
auto Closure() const
|
||||
-> std::pair<std::vector<State>,
|
||||
std::vector<std::pair<std::size_t, std::size_t>>>;
|
||||
[[nodiscard]] auto find_adjacent_puzzles() const -> std::vector<puzzle>;
|
||||
[[nodiscard]] auto explore_state_space() const
|
||||
-> std::pair<std::vector<puzzle>, std::vector<std::pair<size_t, size_t>>>;
|
||||
};
|
||||
|
||||
// Provide hash functions so we can use State and <State, State> as hash-set
|
||||
// keys for masses and springs.
|
||||
template <> struct std::hash<State> {
|
||||
std::size_t operator()(const State &s) const noexcept { return s.Hash(); }
|
||||
template <>
|
||||
struct std::hash<puzzle>
|
||||
{
|
||||
auto operator()(const puzzle& s) const noexcept -> size_t
|
||||
{
|
||||
return s.hash();
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct std::hash<std::pair<State, State>> {
|
||||
std::size_t operator()(const std::pair<State, State> &s) const noexcept {
|
||||
auto h1 = std::hash<State>{}(s.first);
|
||||
auto h2 = std::hash<State>{}(s.second);
|
||||
return h1 + h2 + (h1 * h2);
|
||||
}
|
||||
template <>
|
||||
struct std::hash<std::pair<puzzle, puzzle>>
|
||||
{
|
||||
auto operator()(const std::pair<puzzle, puzzle>& s) const noexcept -> size_t
|
||||
{
|
||||
const size_t h1 = std::hash<puzzle>{}(s.first);
|
||||
const size_t h2 = std::hash<puzzle>{}(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<std::pair<State, State>> {
|
||||
bool operator()(const std::pair<State, State> &a,
|
||||
const std::pair<State, State> &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<std::pair<puzzle, puzzle>>
|
||||
{
|
||||
auto operator()(const std::pair<puzzle, puzzle>& a, const std::pair<puzzle, puzzle>& b) const noexcept -> bool
|
||||
{
|
||||
return (a.first == b.first && a.second == b.second) || (a.first == b.second && a.second == b.first);
|
||||
}
|
||||
};
|
||||
|
||||
using WinCondition = std::function<bool(const State &)>;
|
||||
using win_condition = std::function<bool(const puzzle&)>;
|
||||
|
||||
#endif
|
||||
|
||||
@ -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 <raylib.h>
|
||||
#include <raymath.h>
|
||||
#include <rlgl.h>
|
||||
|
||||
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<std::pair<Vector3, Vector3>> connections;
|
||||
|
||||
// Instancing
|
||||
if (transforms != nullptr) {
|
||||
UnloadMaterial(vertex_mat);
|
||||
MemFree(transforms);
|
||||
UnloadMesh(cube_instance);
|
||||
static constexpr int INSTANCE_COLOR_ATTR = 5;
|
||||
std::vector<Matrix> transforms;
|
||||
std::vector<Color> colors;
|
||||
Material vertex_mat = LoadMaterialDefault();
|
||||
Mesh cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE);
|
||||
Shader instancing_shader = LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl");
|
||||
|
||||
// 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<Vector3> &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<Vector3> &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<Vector3>& 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<Vector3>& masses, int fps, int ups, size_t mass_count, size_t spring_count) -> void;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
#ifndef __STATE_HPP_
|
||||
#define __STATE_HPP_
|
||||
|
||||
#include "config.hpp"
|
||||
#include "distance.hpp"
|
||||
#include "physics.hpp"
|
||||
#include "puzzle.hpp"
|
||||
|
||||
#include <raymath.h>
|
||||
#include <stack>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
class StateManager {
|
||||
public:
|
||||
ThreadedPhysics &physics;
|
||||
|
||||
std::vector<State> presets = {State()};
|
||||
std::vector<std::string> comments = {"Empty"};
|
||||
|
||||
// Some stuff is faster to map from state to mass (e.g. in the renderer)
|
||||
std::unordered_map<State, std::size_t> states; // State to mass id
|
||||
std::unordered_set<State> winning_states;
|
||||
std::unordered_map<State, int> visited_states; // How often we've been here
|
||||
std::stack<State> history;
|
||||
|
||||
// Other stuff maps from mass to state :/
|
||||
std::unordered_map<std::size_t, State> masses; // Mass id to state
|
||||
std::vector<std::size_t> winning_path;
|
||||
|
||||
// Fuck it, duplicate the springs too, we don't even need to copy them from
|
||||
// the physics thread then...
|
||||
std::vector<std::pair<std::size_t, std::size_t>> 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
|
||||
151
include/state_manager.hpp
Normal file
151
include/state_manager.hpp
Normal file
@ -0,0 +1,151 @@
|
||||
#ifndef STATE_MANAGER_HPP_
|
||||
#define STATE_MANAGER_HPP_
|
||||
|
||||
#include "distance.hpp"
|
||||
#include "physics.hpp"
|
||||
#include "puzzle.hpp"
|
||||
|
||||
#include <stack>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
class state_manager
|
||||
{
|
||||
private:
|
||||
threaded_physics& physics;
|
||||
|
||||
std::string preset_file;
|
||||
size_t current_preset = 0;
|
||||
std::vector<puzzle> preset_states = {puzzle(4, 5, 9, 9, false)};
|
||||
std::vector<std::string> preset_comments = {"Empty"};
|
||||
|
||||
// State storage (store states twice for bidirectional lookup).
|
||||
// Everything else should only store indices to state_pool.
|
||||
|
||||
std::vector<puzzle> state_pool; // Indices are equal to mass_springs mass indices
|
||||
std::unordered_map<puzzle, size_t> state_indices; // Maps states to indices
|
||||
std::vector<std::pair<size_t, size_t>> links; // Indices are equal to mass_springs springs indices
|
||||
|
||||
graph_distances node_target_distances; // Buffered and reused if the graph doesn't change
|
||||
std::unordered_set<size_t> winning_indices; // Indices of all states where the board is solved
|
||||
std::vector<size_t> winning_path; // Ordered list of node indices leading to the nearest solved state
|
||||
std::unordered_set<size_t> path_indices; // For faster lookup if a vertex is part of the path in renderer
|
||||
|
||||
std::stack<size_t> move_history; // Moves between the starting state and the current state
|
||||
std::unordered_map<size_t, int> visit_counts; // How often each state was visited
|
||||
|
||||
size_t starting_state_index = 0;
|
||||
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<puzzle>& states,
|
||||
const std::vector<std::pair<size_t, size_t>>& _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<std::pair<size_t, size_t>>&;
|
||||
[[nodiscard]] auto get_winning_indices() const -> const std::unordered_set<size_t>&;
|
||||
[[nodiscard]] auto get_visit_counts() const -> const std::unordered_map<size_t, int>&;
|
||||
[[nodiscard]] auto get_winning_path() const -> const std::vector<size_t>&;
|
||||
[[nodiscard]] auto get_path_indices() const -> const std::unordered_set<size_t>&;
|
||||
[[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
|
||||
@ -1,16 +0,0 @@
|
||||
#ifndef __TRACY_HPP_
|
||||
#define __TRACY_HPP_
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#ifdef TRACY
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
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
|
||||
167
include/user_interface.hpp
Normal file
167
include/user_interface.hpp
Normal file
@ -0,0 +1,167 @@
|
||||
#ifndef GUI_HPP_
|
||||
#define GUI_HPP_
|
||||
|
||||
#include "camera.hpp"
|
||||
#include "config.hpp"
|
||||
#include "input.hpp"
|
||||
#include "state_manager.hpp"
|
||||
|
||||
#include <raylib.h>
|
||||
|
||||
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<char, 256> 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
|
||||
@ -1,20 +1,87 @@
|
||||
#ifndef __UTIL_HPP_
|
||||
#define __UTIL_HPP_
|
||||
|
||||
#include "config.hpp"
|
||||
#ifndef UTIL_HPP_
|
||||
#define UTIL_HPP_
|
||||
|
||||
#include <iostream>
|
||||
#include <raylib.h>
|
||||
#include <raymath.h>
|
||||
|
||||
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<int>(color));
|
||||
}
|
||||
|
||||
inline auto ansi_reset() -> std::string
|
||||
{
|
||||
return "\033[0m";
|
||||
}
|
||||
|
||||
// std::println doesn't work with mingw
|
||||
template <typename... Args>
|
||||
auto infoln(std::format_string<Args...> fmt, Args&&... args) -> void
|
||||
{
|
||||
std::cout << std::format("[{}INFO{}]: ", ansi_bold_fg(fg_blue), ansi_reset())
|
||||
<< std::format(fmt, std::forward<Args>(args)...) << std::endl;
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
auto warnln(std::format_string<Args...> fmt, Args&&... args) -> void
|
||||
{
|
||||
std::cout << std::format("[{}WARNING{}]: ", ansi_bold_fg(fg_yellow), ansi_reset())
|
||||
<< std::format(fmt, std::forward<Args>(args)...) << std::endl;
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
auto errln(std::format_string<Args...> fmt, Args&&... args) -> void
|
||||
{
|
||||
std::cout << std::format("[{}ERROR{}]: ", ansi_bold_fg(fg_red), ansi_reset())
|
||||
<< std::format(fmt, std::forward<Args>(args)...) << std::endl;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 326 KiB |
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
113
src/camera.cpp
113
src/camera.cpp
@ -5,74 +5,71 @@
|
||||
#include <raymath.h>
|
||||
|
||||
#ifdef TRACY
|
||||
#include "tracy.hpp"
|
||||
#include <tracy/Tracy.hpp>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#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;
|
||||
}
|
||||
|
||||
114
src/distance.cpp
114
src/distance.cpp
@ -1,78 +1,72 @@
|
||||
#include "distance.hpp"
|
||||
#include "config.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <queue>
|
||||
|
||||
#ifdef TRACY
|
||||
#include "tracy.hpp"
|
||||
#include <tracy/Tracy.hpp>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#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<std::pair<std::size_t, std::size_t>> &edges,
|
||||
const std::vector<std::size_t> &targets) -> DistanceResult {
|
||||
|
||||
// Build a list of adjacent nodes to speed up BFS
|
||||
std::vector<std::vector<std::size_t>> adjacency(node_count);
|
||||
for (const auto &[from, to] : edges) {
|
||||
adjacency[from].push_back(to);
|
||||
adjacency[to].push_back(from);
|
||||
}
|
||||
|
||||
std::vector<int> distances(node_count, -1);
|
||||
std::vector<std::size_t> parents(node_count, -1);
|
||||
std::vector<std::size_t> nearest_targets(node_count, -1);
|
||||
|
||||
std::queue<std::size_t> 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<std::pair<size_t, size_t>>& edges,
|
||||
const std::vector<size_t>& targets) -> void
|
||||
{
|
||||
// Build a list of adjacent nodes to speed up BFS
|
||||
std::vector<std::vector<size_t>> 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<int>(node_count, -1);
|
||||
parents = std::vector<size_t>(node_count, -1);
|
||||
nearest_targets = std::vector<size_t>(node_count, -1);
|
||||
|
||||
std::queue<size_t> 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<std::size_t> {
|
||||
if (result.distances[source] == -1) {
|
||||
// Unreachable
|
||||
return {};
|
||||
}
|
||||
auto graph_distances::get_shortest_path(const size_t source) const -> std::vector<size_t>
|
||||
{
|
||||
if (empty() || distances[source] == -1) {
|
||||
// Unreachable
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::size_t> path;
|
||||
for (std::size_t n = source; n != static_cast<std::size_t>(-1);
|
||||
n = result.parents[n]) {
|
||||
path.push_back(n);
|
||||
}
|
||||
std::vector<size_t> path;
|
||||
for (size_t n = source; n != static_cast<size_t>(-1); n = parents[n]) {
|
||||
path.push_back(n);
|
||||
}
|
||||
|
||||
return path;
|
||||
return path;
|
||||
}
|
||||
|
||||
804
src/gui.cpp
804
src/gui.cpp
@ -1,804 +0,0 @@
|
||||
#include "gui.hpp"
|
||||
#include "config.hpp"
|
||||
|
||||
#include <raylib.h>
|
||||
|
||||
#define RAYGUI_IMPLEMENTATION
|
||||
#include <raygui.h>
|
||||
|
||||
#ifdef TRACY
|
||||
#include "tracy.hpp"
|
||||
#include <tracy/Tracy.hpp>
|
||||
#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(); }
|
||||
922
src/input.cpp
922
src/input.cpp
@ -3,494 +3,568 @@
|
||||
|
||||
#include <raylib.h>
|
||||
|
||||
#ifdef TRACY
|
||||
#include "tracy.hpp"
|
||||
#include <tracy/Tracy.hpp>
|
||||
#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<puzzle>& next =
|
||||
current.try_add_block(puzzle::block(block_add_x, block_add_y, block_add_width, block_add_height, false));
|
||||
|
||||
if (next) {
|
||||
sel_x = block_add_x;
|
||||
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<puzzle::block>& b = current.try_get_block(hov_x, hov_y);
|
||||
if (!editing || has_block_add_xy || !b) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::optional<puzzle>& 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<puzzle>& 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<puzzle>& 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<puzzle>& 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<puzzle>& 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<puzzle>& 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<puzzle>& 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<void(InputHandler &)> handler) -> void {
|
||||
generic_handlers.push_back({handler});
|
||||
}
|
||||
|
||||
auto InputHandler::RegisterMousePressedHandler(
|
||||
MouseButton button, std::function<void(InputHandler &)> handler) -> void {
|
||||
mouse_pressed_handlers.push_back({handler, button});
|
||||
}
|
||||
|
||||
auto InputHandler::RegisterMouseReleasedHandler(
|
||||
MouseButton button, std::function<void(InputHandler &)> handler) -> void {
|
||||
mouse_released_handlers.push_back({handler, button});
|
||||
}
|
||||
|
||||
auto InputHandler::RegisterKeyPressedHandler(
|
||||
KeyboardKey key, std::function<void(InputHandler &)> handler) -> void {
|
||||
key_pressed_handlers.push_back({handler, key});
|
||||
}
|
||||
|
||||
auto InputHandler::RegisterKeyReleasedHandler(
|
||||
KeyboardKey key, std::function<void(InputHandler &)> 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<puzzle>& 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<puzzle>& 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<puzzle>& 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<puzzle>& 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<puzzle>& 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<puzzle>& 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<puzzle>& next = current.try_clear_goal();
|
||||
|
||||
if (!editing || !next) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.edit_starting_state(*next);
|
||||
}
|
||||
|
||||
auto input_handler::register_generic_handler(const std::function<void(input_handler&)>& handler) -> void
|
||||
{
|
||||
generic_handlers.push_back({handler});
|
||||
}
|
||||
|
||||
auto input_handler::register_mouse_pressed_handler(const MouseButton button,
|
||||
const std::function<void(input_handler&)>& handler) -> void
|
||||
{
|
||||
mouse_pressed_handlers.push_back({{handler}, button});
|
||||
}
|
||||
|
||||
auto input_handler::register_mouse_released_handler(const MouseButton button,
|
||||
const std::function<void(input_handler&)>& handler) -> void
|
||||
{
|
||||
mouse_released_handlers.push_back({{handler}, button});
|
||||
}
|
||||
|
||||
auto input_handler::register_key_pressed_handler(const KeyboardKey key,
|
||||
const std::function<void(input_handler&)>& handler) -> void
|
||||
{
|
||||
key_pressed_handlers.push_back({{handler}, key});
|
||||
}
|
||||
|
||||
auto input_handler::register_key_released_handler(const KeyboardKey key,
|
||||
const std::function<void(input_handler&)>& 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
191
src/main.cpp
191
src/main.cpp
@ -1,18 +1,16 @@
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <raylib.h>
|
||||
#include <raymath.h>
|
||||
|
||||
#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 <tracy/Tracy.hpp>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#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<double> fps_accumulator(0);
|
||||
unsigned int loop_iterations = 0;
|
||||
std::chrono::time_point last = std::chrono::high_resolution_clock::now();
|
||||
std::chrono::duration<double> 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<Vector3> masses; // Read from physics
|
||||
int fps = 0;
|
||||
int ups = 0; // Read from physics
|
||||
Vector3 mass_center; // Read from physics
|
||||
std::vector<Vector3> 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<double> deltatime = now - last;
|
||||
fps_accumulator += deltatime;
|
||||
last = now;
|
||||
// Time tracking
|
||||
std::chrono::time_point now = std::chrono::high_resolution_clock::now();
|
||||
std::chrono::duration<double> 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<LockableBase(std::mutex)> lock(physics.state.data_mtx);
|
||||
std::unique_lock<LockableBase(std::mutex)> lock(physics.state.data_mtx);
|
||||
#else
|
||||
std::unique_lock<std::mutex> lock(physics.state.data_mtx);
|
||||
std::unique_lock<std::mutex> 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<double>(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<double>(0);
|
||||
}
|
||||
++loop_iterations;
|
||||
|
||||
#ifdef TRACY
|
||||
FrameMark;
|
||||
FrameMarkEnd("MainThread");
|
||||
#endif
|
||||
}
|
||||
|
||||
CloseWindow();
|
||||
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
290
src/octree.cpp
290
src/octree.cpp
@ -1,168 +1,194 @@
|
||||
#include "octree.hpp"
|
||||
#include "config.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
#include <raymath.h>
|
||||
|
||||
#ifdef TRACY
|
||||
#include "tracy.hpp"
|
||||
#include <tracy/Tracy.hpp>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#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<int>(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<Vector3, Vector3> {
|
||||
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<Vector3, Vector3>
|
||||
{
|
||||
const node& n = nodes[node_idx];
|
||||
auto [cx, cy, cz] = Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f,
|
||||
(n.box_min.z + n.box_max.z) / 2.0f);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
555
src/physics.cpp
555
src/physics.cpp
@ -4,350 +4,401 @@
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <raylib.h>
|
||||
#include <raymath.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#ifdef TRACY
|
||||
#include "tracy.hpp"
|
||||
#include <tracy/Tracy.hpp>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#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<float>(GetRandomValue(-100, 100)),
|
||||
static_cast<float>(GetRandomValue(-100, 100)),
|
||||
static_cast<float>(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<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100, 100)),
|
||||
// static_cast<float>(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<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100, 100)),
|
||||
static_cast<float>(GetRandomValue(-100, 100))};
|
||||
offset = Vector3Normalize(offset) * REST_LENGTH;
|
||||
|
||||
// If the offset moves the mass closer to the current center of mass, flip it
|
||||
if (!tree.nodes.empty()) {
|
||||
const Vector3 mass_center_direction = Vector3Subtract(mass_a.position, tree.nodes.at(0).mass_center);
|
||||
const 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, static_cast<int>(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<void> loop_future =
|
||||
threads.submit_loop(0, masses.size(), solve_octree, 256);
|
||||
loop_future.wait();
|
||||
const BS::multi_future<void> 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 /= static_cast<float>(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<double> physics_accumulator(0);
|
||||
std::chrono::duration<double> ups_accumulator(0);
|
||||
unsigned int loop_iterations = 0;
|
||||
std::chrono::time_point last = std::chrono::high_resolution_clock::now();
|
||||
std::chrono::duration<double> physics_accumulator(0);
|
||||
std::chrono::duration<double> 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<double> 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<double> deltatime = now - last;
|
||||
physics_accumulator += deltatime;
|
||||
ups_accumulator += deltatime;
|
||||
last = now;
|
||||
|
||||
// Handle queued commands
|
||||
// Handle queued commands
|
||||
{
|
||||
#ifdef TRACY
|
||||
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
|
||||
#else
|
||||
std::lock_guard<std::mutex> 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<double>(TIMESTEP);
|
||||
}
|
||||
|
||||
// Publish the positions for the renderer (copy)
|
||||
#ifdef TRACY
|
||||
FrameMarkStart("PhysicsThreadProduceLock");
|
||||
#endif
|
||||
{
|
||||
#ifdef TRACY
|
||||
std::unique_lock<LockableBase(std::mutex)> lock(state.data_mtx);
|
||||
#else
|
||||
std::unique_lock<std::mutex> 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<double>(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<LockableBase(std::mutex)> lock(state.command_mtx);
|
||||
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
|
||||
#else
|
||||
std::lock_guard<std::mutex> lock(state.command_mtx);
|
||||
std::lock_guard<std::mutex> 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<double>(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<LockableBase(std::mutex)> lock(state.data_mtx);
|
||||
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
|
||||
#else
|
||||
std::unique_lock<std::mutex> lock(state.data_mtx);
|
||||
std::lock_guard<std::mutex> 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<double>(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<LockableBase(std::mutex)> lock(state.command_mtx);
|
||||
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
|
||||
#else
|
||||
std::lock_guard<std::mutex> lock(state.command_mtx);
|
||||
std::lock_guard<std::mutex> 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<LockableBase(std::mutex)> lock(state.command_mtx);
|
||||
#else
|
||||
std::lock_guard<std::mutex> lock(state.command_mtx);
|
||||
#endif
|
||||
state.pending_commands.push(AddSpring{a, b});
|
||||
}
|
||||
}
|
||||
|
||||
auto ThreadedPhysics::ClearCmd() -> void {
|
||||
{
|
||||
#ifdef TRACY
|
||||
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
|
||||
#else
|
||||
std::lock_guard<std::mutex> lock(state.command_mtx);
|
||||
#endif
|
||||
state.pending_commands.push(ClearGraph{});
|
||||
}
|
||||
}
|
||||
|
||||
auto ThreadedPhysics::AddMassSpringsCmd(
|
||||
std::size_t num_masses,
|
||||
const std::vector<std::pair<std::size_t, std::size_t>> &springs) -> void {
|
||||
{
|
||||
#ifdef TRACY
|
||||
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
|
||||
#else
|
||||
std::lock_guard<std::mutex> 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<std::pair<size_t, size_t>>& springs) -> void
|
||||
{
|
||||
{
|
||||
#ifdef TRACY
|
||||
std::lock_guard<LockableBase(std::mutex)> lock(state.command_mtx);
|
||||
#else
|
||||
std::lock_guard<std::mutex> 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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
843
src/puzzle.cpp
843
src/puzzle.cpp
@ -1,396 +1,509 @@
|
||||
#include "puzzle.hpp"
|
||||
#include "config.hpp"
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
#ifdef TRACY
|
||||
#include "tracy.hpp"
|
||||
#include <tracy/Tracy.hpp>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#endif
|
||||
|
||||
auto Block::Hash() const -> std::size_t {
|
||||
std::string s = std::format("{},{},{},{}", x, y, width, height);
|
||||
return std::hash<std::string>{}(s);
|
||||
auto puzzle::block::hash() const -> size_t
|
||||
{
|
||||
const std::string s = std::format("{},{},{},{}", x, y, width, height);
|
||||
return std::hash<std::string>{}(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<char>(width + static_cast<int>('a') - 1),
|
||||
static_cast<char>(height + static_cast<int>('a') - 1));
|
||||
} else if (immovable) {
|
||||
return std::format("{}{}",
|
||||
static_cast<char>(width + static_cast<int>('A') - 1),
|
||||
static_cast<char>(height + static_cast<int>('A') - 1));
|
||||
} else {
|
||||
auto puzzle::block::string() const -> std::string
|
||||
{
|
||||
if (target) {
|
||||
return std::format("{}{}", static_cast<char>(width + static_cast<int>('a') - 1),
|
||||
static_cast<char>(height + static_cast<int>('a') - 1));
|
||||
}
|
||||
if (immovable) {
|
||||
return std::format("{}{}", static_cast<char>(width + static_cast<int>('A') - 1),
|
||||
static_cast<char>(height + static_cast<int>('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<std::string>{}(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<State> {
|
||||
std::vector<State> 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<State>,
|
||||
std::vector<std::pair<std::size_t, std::size_t>>> {
|
||||
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<std::string>{}(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<block>& 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<int>(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<block>
|
||||
{
|
||||
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<block>
|
||||
{
|
||||
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>
|
||||
{
|
||||
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<puzzle>
|
||||
{
|
||||
const std::optional<block>& 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>
|
||||
{
|
||||
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<puzzle>
|
||||
{
|
||||
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<puzzle>& _p = p.try_add_block(b)) {
|
||||
p = *_p;
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
auto puzzle::try_remove_column() const -> std::optional<puzzle>
|
||||
{
|
||||
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<puzzle>& _p = p.try_add_block(b)) {
|
||||
p = *_p;
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
auto puzzle::try_add_row() const -> std::optional<puzzle>
|
||||
{
|
||||
if (height >= 9) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
puzzle p = puzzle(width, height + 1, restricted);
|
||||
|
||||
for (const block& b : *this) {
|
||||
if (const std::optional<puzzle>& _p = p.try_add_block(b)) {
|
||||
p = *_p;
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
auto puzzle::try_remove_row() const -> std::optional<puzzle>
|
||||
{
|
||||
if (height == 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
puzzle p = puzzle(width, height - 1, restricted);
|
||||
|
||||
for (const block& b : *this) {
|
||||
if (const std::optional<puzzle>& _p = p.try_add_block(b)) {
|
||||
p = *_p;
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
auto puzzle::try_add_block(const block& b) const -> std::optional<puzzle>
|
||||
{
|
||||
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<puzzle>
|
||||
{
|
||||
const std::optional<block>& 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<puzzle>
|
||||
{
|
||||
std::optional<block> 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<block>& _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<puzzle>
|
||||
{
|
||||
std::optional<block> 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<puzzle>
|
||||
{
|
||||
const std::optional<block>& 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<puzzle> 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<puzzle>
|
||||
{
|
||||
std::vector<puzzle> 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<puzzle>& north = try_move_block_at(b.x, b.y, nor)) {
|
||||
puzzles.push_back(*north);
|
||||
}
|
||||
}
|
||||
|
||||
if (dirs & eas) {
|
||||
if (const std::optional<puzzle>& east = try_move_block_at(b.x, b.y, eas)) {
|
||||
puzzles.push_back(*east);
|
||||
}
|
||||
}
|
||||
|
||||
if (dirs & sou) {
|
||||
if (const std::optional<puzzle>& south = try_move_block_at(b.x, b.y, sou)) {
|
||||
puzzles.push_back(*south);
|
||||
}
|
||||
}
|
||||
|
||||
if (dirs & wes) {
|
||||
if (const std::optional<puzzle>& 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<puzzle>, std::vector<std::pair<size_t, size_t>>>
|
||||
{
|
||||
#ifdef TRACY
|
||||
ZoneScoped;
|
||||
ZoneScoped;
|
||||
#endif
|
||||
|
||||
std::vector<State> states;
|
||||
std::vector<std::pair<std::size_t, std::size_t>> links;
|
||||
infoln("Exploring state space, this might take a while...");
|
||||
|
||||
// Helper to construct the links vector
|
||||
std::unordered_map<State, std::size_t> state_indices;
|
||||
std::vector<puzzle> state_pool;
|
||||
std::unordered_map<puzzle, std::size_t> state_indices; // Helper to construct the links vector
|
||||
std::vector<std::pair<size_t, std::size_t>> links;
|
||||
|
||||
// Buffer for all states we want to call GetNextStates() on
|
||||
std::unordered_set<State> remaining_states;
|
||||
remaining_states.insert(*this);
|
||||
// Buffer for all states we want to call GetNextStates() on
|
||||
std::unordered_set<puzzle> 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);
|
||||
}
|
||||
|
||||
384
src/renderer.cpp
384
src/renderer.cpp
@ -1,244 +1,226 @@
|
||||
#include "renderer.hpp"
|
||||
#include "config.hpp"
|
||||
#include "puzzle.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <raylib.h>
|
||||
#include <raymath.h>
|
||||
#include <rlgl.h>
|
||||
|
||||
#ifdef TRACY
|
||||
#include "tracy.hpp"
|
||||
#include <tracy/Tracy.hpp>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#endif
|
||||
|
||||
#ifdef BATCHING
|
||||
#include <cstring>
|
||||
#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<Vector3> &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<Vector3>& 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<Vector3> &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<Vector3>& 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);
|
||||
}
|
||||
|
||||
292
src/state.cpp
292
src/state.cpp
@ -1,292 +0,0 @@
|
||||
#include "state.hpp"
|
||||
#include "config.hpp"
|
||||
#include "distance.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <raymath.h>
|
||||
|
||||
#ifdef TRACY
|
||||
#include "tracy.hpp"
|
||||
#include <tracy/Tracy.hpp>
|
||||
#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<std::string> comment_lines;
|
||||
std::vector<std::string> 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<State>();
|
||||
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<State>,
|
||||
std::vector<std::pair<std::size_t, std::size_t>>>
|
||||
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<State>();
|
||||
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<std::size_t> 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);
|
||||
}
|
||||
485
src/state_manager.cpp
Normal file
485
src/state_manager.cpp
Normal file
@ -0,0 +1,485 @@
|
||||
#include "state_manager.hpp"
|
||||
#include "distance.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
|
||||
#ifdef TRACY
|
||||
#include <tracy/Tracy.hpp>
|
||||
#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<puzzle>& states,
|
||||
const std::vector<std::pair<size_t, size_t>>& _links) -> void
|
||||
{
|
||||
if (!state_pool.empty() || !state_indices.empty() || !links.empty()) {
|
||||
warnln("Inserting statespace but collections haven't been cleared");
|
||||
}
|
||||
|
||||
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<std::string> comment_lines;
|
||||
std::vector<std::string> 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<size_t>();
|
||||
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<size_t>();
|
||||
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
|
||||
{
|
||||
// Do we 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<size_t> 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<std::pair<size_t, size_t>>&
|
||||
{
|
||||
return links;
|
||||
}
|
||||
|
||||
auto state_manager::get_winning_indices() const -> const std::unordered_set<size_t>&
|
||||
{
|
||||
return winning_indices;
|
||||
}
|
||||
|
||||
auto state_manager::get_visit_counts() const -> const std::unordered_map<size_t, int>&
|
||||
{
|
||||
return visit_counts;
|
||||
}
|
||||
|
||||
auto state_manager::get_winning_path() const -> const std::vector<size_t>&
|
||||
{
|
||||
return winning_path;
|
||||
}
|
||||
|
||||
auto state_manager::get_path_indices() const -> const std::unordered_set<size_t>&
|
||||
{
|
||||
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);
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
#include "config.hpp"
|
||||
|
||||
#ifdef TRACY
|
||||
|
||||
#include "tracy.hpp"
|
||||
#include <tracy/Tracy.hpp>
|
||||
|
||||
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
|
||||
784
src/user_interface.cpp
Normal file
784
src/user_interface.cpp
Normal file
@ -0,0 +1,784 @@
|
||||
#include "user_interface.hpp"
|
||||
#include "config.hpp"
|
||||
|
||||
#include <raylib.h>
|
||||
|
||||
#define RAYGUI_IMPLEMENTATION
|
||||
#include <raygui.h>
|
||||
|
||||
#ifdef TRACY
|
||||
#include <tracy/Tracy.hpp>
|
||||
#endif
|
||||
|
||||
auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height,
|
||||
const int _columns, const int _rows) -> void
|
||||
{
|
||||
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<float>(columns), static_cast<float>(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<int>(state.get_current_preset())) {
|
||||
input.load_next_preset();
|
||||
} else if (preset < static_cast<int>(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<puzzle::block> target_block = current.try_get_target_block();
|
||||
if (current.has_win_condition() && target_block.has_value()) {
|
||||
const int target_x = current.target_x;
|
||||
const int target_y = current.target_y;
|
||||
auto [x, y, width, height] =
|
||||
board_grid.square_bounds(target_x, target_y, target_block.value().width, target_block.value().height);
|
||||
|
||||
const 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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user