implement option to lock camera to graph's center of mass

This commit is contained in:
2026-02-27 14:06:30 +01:00
parent e8bd90911d
commit b01cfdecfe
10 changed files with 85 additions and 38 deletions

View File

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

View File

@ -23,12 +23,14 @@ constexpr const char *FONT = "fonts/SpaceMono.ttf";
constexpr int FONT_SIZE = 26;
// Camera Controls
constexpr float CAMERA_FOV = 120.0;
constexpr float CAMERA_FOV = 90.0;
constexpr float FOV_SPEED = 1.0;
constexpr float MIN_FOV = 10.0;
constexpr float MAX_FOV = 180.0;
constexpr float CAMERA_DISTANCE = 20.0;
constexpr float ZOOM_SPEED = 2.5;
constexpr float MIN_CAMERA_DISTANCE = 2.0;
constexpr float MAX_CAMERA_DISTANCE = 2000.0;
constexpr float ZOOM_SPEED = 2.5;
constexpr float FOV_SPEED = 1.0;
constexpr float ZOOM_MULTIPLIER = 4.0;
constexpr float PAN_SPEED = 2.0;
constexpr float PAN_MULTIPLIER = 10.0;
@ -42,16 +44,21 @@ constexpr float SIM_SPEED = 4.0; // How large each update should be
constexpr float MASS = 1.0; // Mass spring system
constexpr float SPRING_CONSTANT = 5.0; // Mass spring system
constexpr float DAMPENING_CONSTANT = 1.0; // Mass spring system
constexpr float REST_LENGTH = 2.0; // Mass spring system
constexpr float REST_LENGTH = 3.0; // Mass spring system
constexpr float VERLET_DAMPENING = 0.05; // [0, 1]
constexpr float BH_FORCE = 2.0; // Barnes-Hut [1.0, 3.0]
constexpr float THETA = 0.9; // Barnes-Hut [0.5, 1.0]
constexpr float SOFTENING = 0.01; // Barnes-Hut [0.01, 1.0]
// Graph Drawing
constexpr Color EDGE_COLOR = DARKBLUE;
constexpr float VERTEX_SIZE = 0.5;
constexpr Color VERTEX_COLOR = GREEN;
constexpr Color EDGE_COLOR = DARKGREEN;
static const Color VERTEX_COLOR = Fade(BLUE, 0.5);
constexpr Color VERTEX_VISITED_COLOR = DARKBLUE;
constexpr Color VERTEX_PATH_COLOR = GREEN;
constexpr Color VERTEX_TARGET_COLOR = RED;
constexpr Color VERTEX_START_COLOR = ORANGE;
constexpr Color VERTEX_CURRENT_COLOR = PURPLE;
constexpr int DRAW_VERTICES_LIMIT = 1000000;
// Klotski Drawing
@ -62,6 +69,5 @@ constexpr Color BOARD_COLOR_FREE = RAYWHITE;
constexpr Color BLOCK_COLOR = DARKBLUE;
constexpr Color TARGET_BLOCK_COLOR = RED;
constexpr Color WALL_COLOR = BLACK;
constexpr Color GOAL_COLOR = ORANGE;
#endif

View File

@ -54,6 +54,7 @@ public:
// Camera
bool camera_lock = true;
bool camera_mass_center_lock = false;
bool camera_panning = false;
bool camera_rotating = false;
@ -102,6 +103,7 @@ public:
// Key actions
auto ToggleCameraLock() -> void;
auto ToggleCameraMassCenterLock() -> void;
auto ToggleCameraProjection() -> void;
auto MoveBlockNor() -> void;
auto MoveBlockWes() -> void;

View File

@ -68,13 +68,13 @@ public:
class MassSpringSystem {
private:
Octree octree;
#ifdef THREADPOOL
BS::thread_pool<BS::tp::none> threads;
#endif
public:
Octree octree;
// This is the main ownership of all the states/masses/springs.
std::vector<Mass> masses;
std::vector<Spring> springs;
@ -151,6 +151,7 @@ class ThreadedPhysics {
#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;

View File

@ -41,13 +41,22 @@ auto OrbitCamera3D::Pan(Vector2 last_mouse, Vector2 mouse) -> void {
target = Vector3Add(target, offset);
}
auto OrbitCamera3D::Update(const Vector3 &current_target, bool lock) -> void {
auto OrbitCamera3D::Update(const Vector3 &current_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)));
} else {
target = Vector3MoveTowards(
target, current_target,
CAMERA_SMOOTH_SPEED * GetFrameTime() *
Vector3Length(Vector3Subtract(target, current_target)));
}
}
distance = Clamp(distance, MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE);
int actual_distance = distance;
@ -60,7 +69,7 @@ auto OrbitCamera3D::Update(const Vector3 &current_target, bool lock) -> void {
float y = sin(angle_y) * actual_distance;
float z = cos(angle_y) * cos(angle_x) * actual_distance;
fov = Clamp(fov, 25.0, 155.0);
fov = Clamp(fov, MIN_FOV, MAX_FOV);
camera.position = Vector3Add(target, Vector3(x, y, z));
camera.target = target;

View File

@ -482,39 +482,47 @@ auto Gui::DrawGraphInfo(Color color) const -> void {
}
auto Gui::DrawGraphControls(Color color) const -> void {
if (DrawMenuButton(1, 2, 1, 1, "Populate Graph (G)", color)) {
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();
}
// 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, 3, 1, 1, "Clear Graph (C)", color)) {
if (DrawMenuButton(1, 2, 1, 1, "Clear Graph (C)", color)) {
input.ClearGraph();
}
int mark_solutions = input.mark_solutions;
DrawMenuToggleSlider(2, 3, 1, 1, "Solutions Hidden (I)",
"Solutions Shown (I)", &mark_solutions, color);
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, 2, 1, 1, "Free Camera (L)", "Locked Camera (L)",
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(0, 3, 1, 1, "Perspective (Alt)", "Orthographic (Alt)",
DrawMenuToggleSlider(2, 3, 1, 1, "Perspective (Alt)", "Orthographic (Alt)",
&projection, color);
if (projection != (camera.projection == CAMERA_ORTHOGRAPHIC)) {
input.ToggleCameraProjection();

View File

@ -43,7 +43,7 @@ auto InputHandler::InitHandlers() -> void {
RegisterKeyPressedHandler(KEY_C, &InputHandler::ClearGraph);
RegisterKeyPressedHandler(KEY_I, &InputHandler::ToggleMarkSolutions);
RegisterKeyPressedHandler(KEY_O, &InputHandler::ToggleConnectSolutions);
RegisterKeyPressedHandler(KEY_U, &InputHandler::ToggleMarkPath);
// RegisterKeyPressedHandler(KEY_U, &InputHandler::ToggleMarkPath);
RegisterKeyPressedHandler(KEY_SPACE, &InputHandler::MakeOptimalMove);
RegisterKeyPressedHandler(KEY_V, &InputHandler::GoToWorstState);
RegisterKeyPressedHandler(KEY_B, &InputHandler::GoToNearestTarget);
@ -60,6 +60,7 @@ auto InputHandler::InitHandlers() -> void {
RegisterKeyPressedHandler(KEY_LEFT_ALT,
&InputHandler::ToggleCameraProjection);
RegisterKeyPressedHandler(KEY_X, &InputHandler::ClearGoal);
RegisterKeyPressedHandler(KEY_Y, &InputHandler::ToggleCameraMassCenterLock);
}
auto InputHandler::MouseInMenuPane() -> bool { return mouse.y < MENU_HEIGHT; }
@ -145,6 +146,15 @@ auto InputHandler::ToggleCameraLock() -> void {
camera_lock = !camera_lock;
}
auto InputHandler::ToggleCameraMassCenterLock() -> void {
if (!camera_mass_center_lock) {
camera_lock = true;
camera_panning = false;
}
camera_mass_center_lock = !camera_mass_center_lock;
}
auto InputHandler::ToggleCameraProjection() -> void {
camera.projection = camera.projection == CAMERA_PERSPECTIVE
? CAMERA_ORTHOGRAPHIC

View File

@ -16,6 +16,8 @@
#endif
// TODO: Click states in the graph to display them in the board
// TODO: Move selection accordingly when undoing moves (need to diff two states
// and get the moved blocks)
// TODO: Add some popups (my split between input.cpp/gui.cpp makes this ugly)
// - Next move, goto target, goto worst: Notify that the graph needs to be
@ -84,6 +86,7 @@ auto main(int argc, char *argv[]) -> int {
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
// Game loop
@ -114,6 +117,7 @@ auto main(int argc, char *argv[]) -> int {
#endif
ups = physics.state.ups;
mass_center = physics.state.mass_center;
// Only copy data if any has been produced
if (physics.state.data_ready) {
@ -135,7 +139,8 @@ auto main(int argc, char *argv[]) -> int {
std::size_t current_index = state.CurrentMassIndex();
if (masses.size() > current_index) {
const Mass &current_mass = masses.at(current_index);
camera.Update(current_mass.position, input.camera_lock);
camera.Update(current_mass.position, mass_center, input.camera_lock,
input.camera_mass_center_lock);
}
// Rendering

View File

@ -276,6 +276,11 @@ auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state)
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());

View File

@ -129,13 +129,13 @@ auto Renderer::DrawMassSprings(const std::vector<Vector3> &masses) -> void {
const Vector3 &winning_mass = masses.at(winning_index);
if (input.mark_solutions) {
DrawCube(winning_mass, 2 * VERTEX_SIZE, 2 * VERTEX_SIZE,
2 * VERTEX_SIZE, TARGET_BLOCK_COLOR);
2 * VERTEX_SIZE, VERTEX_TARGET_COLOR);
}
std::size_t current_index = state.CurrentMassIndex();
if (input.connect_solutions && masses.size() > current_index) {
const Vector3 &current_mass = masses.at(current_index);
DrawLine3D(winning_mass, current_mass, ORANGE);
DrawLine3D(winning_mass, current_mass, Fade(TARGET_BLOCK_COLOR, 0.5));
}
}
}
@ -148,7 +148,7 @@ auto Renderer::DrawMassSprings(const std::vector<Vector3> &masses) -> void {
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, PURPLE);
VERTEX_SIZE * 1.5, VERTEX_VISITED_COLOR);
}
}
@ -158,7 +158,7 @@ auto Renderer::DrawMassSprings(const std::vector<Vector3> &masses) -> void {
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, YELLOW);
VERTEX_SIZE * 1.75, VERTEX_PATH_COLOR);
}
}
}
@ -168,7 +168,7 @@ auto Renderer::DrawMassSprings(const std::vector<Vector3> &masses) -> void {
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,
ORANGE);
VERTEX_START_COLOR);
}
// Mark current state
@ -176,7 +176,7 @@ auto Renderer::DrawMassSprings(const std::vector<Vector3> &masses) -> void {
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,
BLUE);
VERTEX_CURRENT_COLOR);
}
EndMode3D();