From d885a8989c62d9236c4874dbb399eb72cfb7f429 Mon Sep 17 00:00:00 2001 From: Sagi Amangeldi Date: Sat, 24 Oct 2026 21:55:52 +0900 Subject: [PATCH 1/5] Remove stale core_hd_mapping from clang-format scripts --- scripts/README.md | 1 - scripts/run_check_clang_format.py | 1 - scripts/run_clang_format.py | 1 - 3 files changed, 3 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index 7aef4409..a86cfae0 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -3,7 +3,6 @@ * `run_clang_format.py` — runs `clang-format` on all C/C++ source and header files located in: * `core` - * `core_hd_mapping` * `apps` * `pybind` * `shared` diff --git a/scripts/run_check_clang_format.py b/scripts/run_check_clang_format.py index 0165ce81..29906e9c 100644 --- a/scripts/run_check_clang_format.py +++ b/scripts/run_check_clang_format.py @@ -9,7 +9,6 @@ FILE_LOCATIONS = [ os.path.join(PROJECT_DIRECTORY, "core"), - os.path.join(PROJECT_DIRECTORY, "core_hd_mapping"), os.path.join(PROJECT_DIRECTORY, "apps"), os.path.join(PROJECT_DIRECTORY, "pybind"), os.path.join(PROJECT_DIRECTORY, "shared"), diff --git a/scripts/run_clang_format.py b/scripts/run_clang_format.py index 3f78d1a5..521edb1c 100644 --- a/scripts/run_clang_format.py +++ b/scripts/run_clang_format.py @@ -7,7 +7,6 @@ PROJECT_DIRECTORY = os.path.abspath(os.path.join(SCRIPT_DIR, "..")) FILE_LOCATIONS = [os.path.join(PROJECT_DIRECTORY, 'core'), - os.path.join(PROJECT_DIRECTORY, 'core_hd_mapping'), os.path.join(PROJECT_DIRECTORY, 'apps'), os.path.join(PROJECT_DIRECTORY, 'pybind'), os.path.join(PROJECT_DIRECTORY, 'shared')] From 33ba8e13c13c539117f4a02ec567da2ae76ae49f Mon Sep 17 00:00:00 2001 From: Sagi Amangeldi Date: Sat, 24 Oct 2026 21:58:07 +0900 Subject: [PATCH 2/5] Keep raymath.h after raylib.h in CompassRuler.cpp --- raylib_widgets/src/CompassRuler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/raylib_widgets/src/CompassRuler.cpp b/raylib_widgets/src/CompassRuler.cpp index 8431168e..2132da88 100644 --- a/raylib_widgets/src/CompassRuler.cpp +++ b/raylib_widgets/src/CompassRuler.cpp @@ -1,4 +1,5 @@ #include + #include "raymath.h" #include From 03d4c1d35d06d5ea44c831b4efe47998d65ee521 Mon Sep 17 00:00:00 2001 From: Sagi Amangeldi Date: Sat, 24 Oct 2026 22:01:21 +0900 Subject: [PATCH 3/5] Format raylib_widgets with clang-format --- .../include/RaylibWidgets/AppShell.h | 42 +- .../RaylibWidgets/CenterOfRotationWindow.h | 40 +- .../include/RaylibWidgets/CompassRuler.h | 35 +- .../include/RaylibWidgets/OrbitCamera.h | 477 ++++----- .../include/RaylibWidgets/RayPlaneD.h | 33 +- .../include/RaylibWidgets/Shaders.h | 25 +- .../include/RaylibWidgets/ShortcutsTable.h | 21 +- .../include/RaylibWidgets/WindowFit.h | 13 +- raylib_widgets/src/AppShell.cpp | 197 ++-- raylib_widgets/src/CenterOfRotationWindow.cpp | 146 +-- raylib_widgets/src/CompassRuler.cpp | 98 +- raylib_widgets/src/OrbitCamera.cpp | 958 ++++++++++-------- raylib_widgets/src/PointPicking.cpp | 7 +- raylib_widgets/src/RayPlaneD.cpp | 35 +- raylib_widgets/src/ShortcutsTable.cpp | 65 +- raylib_widgets/src/WindowFit.cpp | 73 +- 16 files changed, 1195 insertions(+), 1070 deletions(-) diff --git a/raylib_widgets/include/RaylibWidgets/AppShell.h b/raylib_widgets/include/RaylibWidgets/AppShell.h index 766c122a..873c5c01 100644 --- a/raylib_widgets/include/RaylibWidgets/AppShell.h +++ b/raylib_widgets/include/RaylibWidgets/AppShell.h @@ -12,27 +12,31 @@ // ShowMainDockSpace/ImGuiHyperlink/info_window) and any other raylib_widgets // consumer that wants the same scaffolding. Depends on raylib_widgets' // existing imgui_raylib link only -- no Eigen/core coupling. -namespace raylib_widgets { +namespace raylib_widgets +{ -// Full-window transparent host for ImGui docking. Call once per frame, -// before building any dockable panel windows. On its first call ever, splits -// off a left column and a bottom "Console" dock -- later calls just -// re-assert the dockspace. -void ShowMainDockSpace(); + // Full-window transparent host for ImGui docking. Call once per frame, + // before building any dockable panel windows. On its first call ever, splits + // off a left column and a bottom "Console" dock -- later calls just + // re-assert the dockspace. + void ShowMainDockSpace(); -// Clickable-looking text that opens `url` in the system browser when -// clicked (Windows: ShellExecuteA; macOS/Linux: `open`/`xdg-open`). -void ImGuiHyperlink(const char* url, ImVec4 color = ImVec4(0.2f, 0.4f, 0.8f, 1.0f)); + // Clickable-looking text that opens `url` in the system browser when + // clicked (Windows: ShellExecuteA; macOS/Linux: `open`/`xdg-open`). + void ImGuiHyperlink(const char* url, ImVec4 color = ImVec4(0.2f, 0.4f, 0.8f, 1.0f)); -// "About/shortcuts" window. `open` is both the visibility toggle (the call -// is a no-op while false) and gets cleared by the window's own close -// button/X. infoLines is rendered as plain text, except a blank line (blank -// line in the window) or one starting with "https://" (rendered as a -// clickable ImGuiHyperlink()); appShortcuts is rendered via -// ShowShortcutsTable(). versionString/buildDate go into a "Version: %s (%s)" -// line (pass e.g. HDMAPPING_CONFIGURE_PROJECT_VERSION and __DATE__). -void ShowInfoWindow( - bool& open, const std::vector& infoLines, const std::vector& appShortcuts, - const char* versionString, const char* buildDate); + // "About/shortcuts" window. `open` is both the visibility toggle (the call + // is a no-op while false) and gets cleared by the window's own close + // button/X. infoLines is rendered as plain text, except a blank line (blank + // line in the window) or one starting with "https://" (rendered as a + // clickable ImGuiHyperlink()); appShortcuts is rendered via + // ShowShortcutsTable(). versionString/buildDate go into a "Version: %s (%s)" + // line (pass e.g. HDMAPPING_CONFIGURE_PROJECT_VERSION and __DATE__). + void ShowInfoWindow( + bool& open, + const std::vector& infoLines, + const std::vector& appShortcuts, + const char* versionString, + const char* buildDate); } // namespace raylib_widgets diff --git a/raylib_widgets/include/RaylibWidgets/CenterOfRotationWindow.h b/raylib_widgets/include/RaylibWidgets/CenterOfRotationWindow.h index 4389ad60..f03e6873 100644 --- a/raylib_widgets/include/RaylibWidgets/CenterOfRotationWindow.h +++ b/raylib_widgets/include/RaylibWidgets/CenterOfRotationWindow.h @@ -6,27 +6,27 @@ // Eigen rotation_center to OrbitCamera's Vector3 target). Depends on raylib // + ImGui + OrbitCamera.h only -- no Eigen, no core -- same dependency story // as OrbitCamera.h itself. -namespace raylib_widgets { +namespace raylib_widgets +{ -// Call once per frame. `open` is an edge-triggered request: the caller sets -// it true (e.g. from a keyboard shortcut or menu item) to pop the dialog; -// this function opens the ImGui popup and immediately clears `open` back to -// false (the popup then manages its own visibility until Set/Cancel). On -// "Set", starts an eased transition of camera.target to the typed X/Y/Z via -// camera.moveTargetTo(). -void showCenterOfRotationWindow(bool& open, OrbitCamera& camera); + // Call once per frame. `open` is an edge-triggered request: the caller sets + // it true (e.g. from a keyboard shortcut or menu item) to pop the dialog; + // this function opens the ImGui popup and immediately clears `open` back to + // false (the popup then manages its own visibility until Set/Cancel). On + // "Set", starts an eased transition of camera.target to the typed X/Y/Z via + // camera.moveTargetTo(). + void showCenterOfRotationWindow(bool& open, OrbitCamera& camera); -// Same dialog, for OrbitCamera's Euler mode instead of its azimuth/ -// elevation/target mode above -- was multi_view_tls_registration's local -// cor_window(). Binds to camera.eulerGoal.rotationCenter (edited in place, -// matching cor_window()'s original behavior of editing its own persistent -// new_rotation_center field rather than a freshly-seeded copy) and calls -// camera.moveEulerRotationCenterTo() on Set. xTooltip/yTooltip/zTooltip are -// optional per-field hover tooltips (multi_view_tls_registration's original -// had these -- e.g. "Longitudinal (forward/backward)"); pass nullptr (the -// default) to skip. -void showEulerCenterOfRotationWindow( - bool& open, OrbitCamera& camera, const char* xTooltip = nullptr, const char* yTooltip = nullptr, - const char* zTooltip = nullptr); + // Same dialog, for OrbitCamera's Euler mode instead of its azimuth/ + // elevation/target mode above -- was multi_view_tls_registration's local + // cor_window(). Binds to camera.eulerGoal.rotationCenter (edited in place, + // matching cor_window()'s original behavior of editing its own persistent + // new_rotation_center field rather than a freshly-seeded copy) and calls + // camera.moveEulerRotationCenterTo() on Set. xTooltip/yTooltip/zTooltip are + // optional per-field hover tooltips (multi_view_tls_registration's original + // had these -- e.g. "Longitudinal (forward/backward)"); pass nullptr (the + // default) to skip. + void showEulerCenterOfRotationWindow( + bool& open, OrbitCamera& camera, const char* xTooltip = nullptr, const char* yTooltip = nullptr, const char* zTooltip = nullptr); } // namespace raylib_widgets diff --git a/raylib_widgets/include/RaylibWidgets/CompassRuler.h b/raylib_widgets/include/RaylibWidgets/CompassRuler.h index dcc4739f..f77fa448 100644 --- a/raylib_widgets/include/RaylibWidgets/CompassRuler.h +++ b/raylib_widgets/include/RaylibWidgets/CompassRuler.h @@ -7,26 +7,23 @@ // core -- so it stays linkable from both the core_raylib side (which already // pulls in core/core_math) and the calib_core side (which deliberately // doesn't) without adding coupling either way. -namespace raylib_widgets { +namespace raylib_widgets +{ -struct CompassAxisLabels { - const char* x = "X"; - const char* y = "Y"; - const char* z = "Z"; -}; + struct CompassAxisLabels + { + const char* x = "X"; + const char* y = "Y"; + const char* z = "Z"; + }; -// right/up: the current camera's screen-right/up directions in world space -// (e.g. cross(forward, camUp) / cross(right, forward) for a raylib Camera3D, -// or the first two rows of a world-to-eye rotation matrix). -// zoomDistance: current camera distance/zoom, used to size the ruler. -// rulerColor: contrast color for the ruler ticks/label (axis lines are -// always drawn in RGB = X/Y/Z). Call after 3D drawing ends (2D screen-space -// overlay). -void drawCompassRuler( - Vector3 right, - Vector3 up, - float zoomDistance, - Color rulerColor, - CompassAxisLabels labels = {}); + // right/up: the current camera's screen-right/up directions in world space + // (e.g. cross(forward, camUp) / cross(right, forward) for a raylib Camera3D, + // or the first two rows of a world-to-eye rotation matrix). + // zoomDistance: current camera distance/zoom, used to size the ruler. + // rulerColor: contrast color for the ruler ticks/label (axis lines are + // always drawn in RGB = X/Y/Z). Call after 3D drawing ends (2D screen-space + // overlay). + void drawCompassRuler(Vector3 right, Vector3 up, float zoomDistance, Color rulerColor, CompassAxisLabels labels = {}); } // namespace raylib_widgets diff --git a/raylib_widgets/include/RaylibWidgets/OrbitCamera.h b/raylib_widgets/include/RaylibWidgets/OrbitCamera.h index 9b85f44b..26a728b5 100644 --- a/raylib_widgets/include/RaylibWidgets/OrbitCamera.h +++ b/raylib_widgets/include/RaylibWidgets/OrbitCamera.h @@ -1,6 +1,6 @@ #pragma once -#include #include "raylib.h" +#include #include @@ -11,256 +11,279 @@ // Eigen (setViewPose) -- no core -- so it stays linkable from both the core_raylib side // (which already pulls in core/core_math) and the calib_core side (which // deliberately doesn't) without adding coupling either way. -namespace raylib_widgets { +namespace raylib_widgets +{ -struct OrbitCamera { - float azimuth = 30.f; // degrees - float elevation = 25.f; // degrees - float distance = 30.f; - Vector3 target = {0.f, 0.f, 0.f}; + struct OrbitCamera + { + float azimuth = 30.f; // degrees + float elevation = 25.f; // degrees + float distance = 30.f; + Vector3 target = { 0.f, 0.f, 0.f }; - // Center-of-rotation ("target") smooth-transition state, mirroring - // multi_view_tls_registration's rotation_center/new_rotation_center/ - // camera_transition_active animation. moveTargetTo() (the only way - // pickGroundPlaneTarget()/CenterOfRotationWindow trigger a transition) - // only ever moves `target` -- it freezes transitionAzimuth/Elevation/ - // Distance/SphericalOrthoHeight to their current values, so those ease - // "toward themselves" (a no-op) whenever only the target changes, - // exactly like before this struct grew orientation/zoom transitions. - // setPreset() below is what actually drives those too, as one combined - // transition. - Vector3 transitionTarget = target; - float transitionAzimuth = azimuth; - float transitionElevation = elevation; - float transitionDistance = distance; - float transitionSphericalOrthoHeight = 20.f; // matches sphericalOrthoHeight's default below - bool transitionActive = false; - float transitionSpeed = 1.f; + // Center-of-rotation ("target") smooth-transition state, mirroring + // multi_view_tls_registration's rotation_center/new_rotation_center/ + // camera_transition_active animation. moveTargetTo() (the only way + // pickGroundPlaneTarget()/CenterOfRotationWindow trigger a transition) + // only ever moves `target` -- it freezes transitionAzimuth/Elevation/ + // Distance/SphericalOrthoHeight to their current values, so those ease + // "toward themselves" (a no-op) whenever only the target changes, + // exactly like before this struct grew orientation/zoom transitions. + // setPreset() below is what actually drives those too, as one combined + // transition. + Vector3 transitionTarget = target; + float transitionAzimuth = azimuth; + float transitionElevation = elevation; + float transitionDistance = distance; + float transitionSphericalOrthoHeight = 20.f; // matches sphericalOrthoHeight's default below + bool transitionActive = false; + float transitionSpeed = 1.f; - Camera3D toRaylib() const; - // Processes mouse input when active (mouse not over ImGui): left-drag - // orbits, right-drag pans, wheel zooms. A manual drag cancels any - // in-flight transition. - void update(bool active); + Camera3D toRaylib() const; + // Processes mouse input when active (mouse not over ImGui): left-drag + // orbits, right-drag pans, wheel zooms. A manual drag cancels any + // in-flight transition. + void update(bool active); - // Starts (or retargets) an eased transition of `target` to newTarget. - void moveTargetTo(Vector3 newTarget); - // Eases `target` toward `transitionTarget`; call once per frame with the - // frame's delta time. No-op when no transition is active. - void updateTransition(float dt); - // Immediately snaps `target` to `transitionTarget` and ends the transition. - void cancelTransition(); + // Starts (or retargets) an eased transition of `target` to newTarget. + void moveTargetTo(Vector3 newTarget); + // Eases `target` toward `transitionTarget`; call once per frame with the + // frame's delta time. No-op when no transition is active. + void updateTransition(float dt); + // Immediately snaps `target` to `transitionTarget` and ends the transition. + void cancelTransition(); - // Casts a ray from `mouse` through `cam` and intersects the y = groundY - // plane, starting a transition of `target` to the hit point on success. - // Returns false (no-op) when the ray is ~parallel to the plane. - bool pickGroundPlaneTarget(Vector2 mouse, Camera3D cam, float groundY = 0.f); + // Casts a ray from `mouse` through `cam` and intersects the y = groundY + // plane, starting a transition of `target` to the hit point on success. + // Returns false (no-op) when the ray is ~parallel to the plane. + bool pickGroundPlaneTarget(Vector2 mouse, Camera3D cam, float groundY = 0.f); - // Orthographic toggle for this (azimuth/elevation/distance) camera -- - // raylib's own Camera3D/BeginMode3D already supports - // CAMERA_ORTHOGRAPHIC natively, so unlike the Euler mode below this - // doesn't need any manual rlgl matrix-stack work: toRaylib() just sets - // cam.projection/cam.fovy accordingly. sphericalOrthoHeight is the - // view's vertical extent in world units (raylib's ortho fovy - // convention) -- update()'s wheel-zoom branch adjusts this instead of - // `distance` while sphericalOrtho is set, mirroring - // multi_view_tls_registration's separate ortho zoom. Named distinctly - // from the Euler mode's own isOrtho/orthoProjection/etc. below -- - // that's a separate camera model bolted onto this same struct, not - // shared state. - bool sphericalOrtho = false; - float sphericalOrthoHeight = 20.f; + // Orthographic toggle for this (azimuth/elevation/distance) camera -- + // raylib's own Camera3D/BeginMode3D already supports + // CAMERA_ORTHOGRAPHIC natively, so unlike the Euler mode below this + // doesn't need any manual rlgl matrix-stack work: toRaylib() just sets + // cam.projection/cam.fovy accordingly. sphericalOrthoHeight is the + // view's vertical extent in world units (raylib's ortho fovy + // convention) -- update()'s wheel-zoom branch adjusts this instead of + // `distance` while sphericalOrtho is set, mirroring + // multi_view_tls_registration's separate ortho zoom. Named distinctly + // from the Euler mode's own isOrtho/orthoProjection/etc. below -- + // that's a separate camera model bolted onto this same struct, not + // shared state. + bool sphericalOrtho = false; + float sphericalOrthoHeight = 20.f; - // Fixed preset views, ported from multi_view_tls_registration's - // CameraPreset/setCameraPreset() (see EulerPreset/setEulerPreset() - // below) but expressed in this camera's own azimuth/elevation terms -- - // the two apps use different world "up" conventions (this one is - // raylib's standard Y-up; multi_view_tls_registration's Euler mode is - // Z-up), so the *numbers* differ, but each preset names the same - // logical view. Front/Back/Left/Right/Top/Bottom/Iso only change - // azimuth/elevation -- target/distance/sphericalOrthoHeight (pan/zoom) - // carry over unchanged, matching the original's behavior of preserving - // pan and zoom across a preset switch. Reset also restores target/ - // distance/sphericalOrthoHeight to this struct's own defaults. - enum class ViewPreset { Front, Back, Left, Right, Top, Bottom, Iso, Reset }; - void setPreset(ViewPreset preset); + // Fixed preset views, ported from multi_view_tls_registration's + // CameraPreset/setCameraPreset() (see EulerPreset/setEulerPreset() + // below) but expressed in this camera's own azimuth/elevation terms -- + // the two apps use different world "up" conventions (this one is + // raylib's standard Y-up; multi_view_tls_registration's Euler mode is + // Z-up), so the *numbers* differ, but each preset names the same + // logical view. Front/Back/Left/Right/Top/Bottom/Iso only change + // azimuth/elevation -- target/distance/sphericalOrthoHeight (pan/zoom) + // carry over unchanged, matching the original's behavior of preserving + // pan and zoom across a preset switch. Reset also restores target/ + // distance/sphericalOrthoHeight to this struct's own defaults. + enum class ViewPreset + { + Front, + Back, + Left, + Right, + Top, + Bottom, + Iso, + Reset + }; + void setPreset(ViewPreset preset); - // ------------------------------------------------------------------ - // Euler-angle + orthographic mode, ported from - // multi_view_tls_registration's original rotate_x/rotate_y/ - // translate_x/y/z/rotation_center/is_ortho camera (driven through - // rlgl's manual matrix stack -- rlMatrixMode/rlFrustum/rlOrtho/ - // rlMultMatrixf -- rather than raylib's Camera3D/BeginMode3D). Kept - // entirely separate from the azimuth/elevation/distance/target fields - // and methods above, which remain the camera_lidar_* apps' own model - // and are untouched by any of this. - // ------------------------------------------------------------------ + // ------------------------------------------------------------------ + // Euler-angle + orthographic mode, ported from + // multi_view_tls_registration's original rotate_x/rotate_y/ + // translate_x/y/z/rotation_center/is_ortho camera (driven through + // rlgl's manual matrix stack -- rlMatrixMode/rlFrustum/rlOrtho/ + // rlMultMatrixf -- rather than raylib's Camera3D/BeginMode3D). Kept + // entirely separate from the azimuth/elevation/distance/target fields + // and methods above, which remain the camera_lidar_* apps' own model + // and are untouched by any of this. + // ------------------------------------------------------------------ - struct EulerState { - float rotateX = -35.264f; - float rotateY = 135.0f; - Vector3 translate = { 0.f, 0.f, -50.f }; - Vector3 rotationCenter = { 0.f, 0.f, 0.f }; - }; + struct EulerState + { + float rotateX = -35.264f; + float rotateY = 135.0f; + Vector3 translate = { 0.f, 0.f, -50.f }; + Vector3 rotationCenter = { 0.f, 0.f, 0.f }; + }; - EulerState euler; + EulerState euler; - // Eased-transition goal state for the Euler mode (separate from - // transitionTarget/transitionActive above, which only ever ease - // `target`). - EulerState eulerGoal = euler; - bool eulerTransitionActive = false; - float eulerTransitionSpeed = 1.f; + // Eased-transition goal state for the Euler mode (separate from + // transitionTarget/transitionActive above, which only ever ease + // `target`). + EulerState eulerGoal = euler; + bool eulerTransitionActive = false; + float eulerTransitionSpeed = 1.f; - bool lockZ = false; - bool isOrtho = false; - float orthoZoom = 10.f; - float orthoShiftX = 0.f, orthoShiftY = 0.f; - float orthoZCenterH = 0.f; - // Mirrors the original's app_state.mouse_sensitivity: fabs(translate.z)/100, - // recomputed by zoom()/setEulerPreset(Reset) -- scales keyboard-shortcut - // and perspective-pan step sizes so they still feel proportional at any - // zoom level. - float eulerMouseSensitivity = 1.f; + bool lockZ = false; + bool isOrtho = false; + float orthoZoom = 10.f; + float orthoShiftX = 0.f, orthoShiftY = 0.f; + float orthoZCenterH = 0.f; + // Mirrors the original's app_state.mouse_sensitivity: fabs(translate.z)/100, + // recomputed by zoom()/setEulerPreset(Reset) -- scales keyboard-shortcut + // and perspective-pan step sizes so they still feel proportional at any + // zoom level. + float eulerMouseSensitivity = 1.f; - // Ortho projection + ortho "gizmo view" matrices, rebuilt each frame by - // updateOrtho() -- flat column-major float[16] (index = col*4+row), the - // layout ImGuizmo::Manipulate expects. Distinct from the real rlgl - // projection updateOrtho() also sets up (right-handed, [-1,1] depth, - // via rlOrtho): these mirror the original's GLM-computed - // left-handed/[0,1]-depth pair, used only for ImGuizmo's own matrices. - float orthoProjection[16] = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; - float orthoGizmoView[16] = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; + // Ortho projection + ortho "gizmo view" matrices, rebuilt each frame by + // updateOrtho() -- flat column-major float[16] (index = col*4+row), the + // layout ImGuizmo::Manipulate expects. Distinct from the real rlgl + // projection updateOrtho() also sets up (right-handed, [-1,1] depth, + // via rlOrtho): these mirror the original's GLM-computed + // left-handed/[0,1]-depth pair, used only for ImGuizmo's own matrices. + float orthoProjection[16] = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; + float orthoGizmoView[16] = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; - // This frame's captured 3D view/projection -- set by captureFrameMatrices() - // right after applyEuler.../updateOrtho() build them, and read back by - // picking code (e.g. multi_view_tls_registration's GetLaserBeam) to build - // an unproject ray. Defaults to identity so an out-of-sequence call - // before the first captureFrameMatrices() degrades gracefully instead of - // unprojecting through a zero matrix. - Matrix frameView3D = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; - Matrix frameProj3D = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; + // This frame's captured 3D view/projection -- set by captureFrameMatrices() + // right after applyEuler.../updateOrtho() build them, and read back by + // picking code (e.g. multi_view_tls_registration's GetLaserBeam) to build + // an unproject ray. Defaults to identity so an out-of-sequence call + // before the first captureFrameMatrices() degrades gracefully instead of + // unprojecting through a zero matrix. + Matrix frameView3D = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; + Matrix frameProj3D = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; - enum class EulerPreset { Front, Back, Left, Right, Top, Bottom, Iso, Reset }; + enum class EulerPreset + { + Front, + Back, + Left, + Right, + Top, + Bottom, + Iso, + Reset + }; - // Sets up rlgl's projection matrix stack for a perspective frame -- was - // multi_view_tls_registration's reshape()'s !is_ortho branch. w/h should - // be logical window size (the GL viewport itself is still sized from the - // actual framebuffer, matching the original). - void applyPerspectiveProjection(int w, int h) const; - // Was reshape()'s is_ortho branch. `aspect` is the caller's display - // width/height (e.g. ImGui's io.DisplaySize.x/y) -- kept as a plain - // parameter rather than reading ImGui here, since OrbitCamera otherwise - // depends on nothing but raylib. - void applyOrthoProjection(float aspect) const; + // Sets up rlgl's projection matrix stack for a perspective frame -- was + // multi_view_tls_registration's reshape()'s !is_ortho branch. w/h should + // be logical window size (the GL viewport itself is still sized from the + // actual framebuffer, matching the original). + void applyPerspectiveProjection(int w, int h) const; + // Was reshape()'s is_ortho branch. `aspect` is the caller's display + // width/height (e.g. ImGui's io.DisplaySize.x/y) -- kept as a plain + // parameter rather than reading ImGui here, since OrbitCamera otherwise + // depends on nothing but raylib. + void applyOrthoProjection(float aspect) const; - //! Camera-to-world pose in OpenGL axes (x right, y up, looking down -z), set by - //! setViewPose(). While set, the caller builds the view from it instead of from - //! `euler`, so mouse orbit/pan/zoom have no effect; reset() it to return to euler. - std::optional viewPose; + //! Camera-to-world pose in OpenGL axes (x right, y up, looking down -z), set by + //! setViewPose(). While set, the caller builds the view from it instead of from + //! `euler`, so mouse orbit/pan/zoom have no effect; reset() it to return to euler. + std::optional viewPose; - //! Places the camera at a pose, without animation. Used for flyovers. - //! @param view pose on the trajectory in LiDAR axes (x forward, y left, z up); - //! stored in `viewPose` swapped to OpenGL axes - //! @note Only the perspective view uses it; ortho mode ignores it. - void setViewPose(Eigen::Affine3f view); + //! Places the camera at a pose, without animation. Used for flyovers. + //! @param view pose on the trajectory in LiDAR axes (x forward, y left, z up); + //! stored in `viewPose` swapped to OpenGL axes + //! @note Only the perspective view uses it; ortho mode ignores it. + void setViewPose(Eigen::Affine3f view); - // Builds this frame's ortho projection + folds an eye/center/up lookAt - // (derived from euler.rotateX/rotateY and the ortho pan/height state) - // into rlgl's current (projection) matrix -- was updateOrthoView(), - // minus the caller's own app-specific compass-rotation bookkeeping - // (that line stays with the caller, which already owns an Eigen - // dependency this widget deliberately doesn't have). Must be called - // with RL_PROJECTION active and freshly loaded identity (display()'s - // job, same as today), and leaves RL_MODELVIEW active + identity after, - // like the original. - void updateOrtho(float aspect); + // Builds this frame's ortho projection + folds an eye/center/up lookAt + // (derived from euler.rotateX/rotateY and the ortho pan/height state) + // into rlgl's current (projection) matrix -- was updateOrthoView(), + // minus the caller's own app-specific compass-rotation bookkeeping + // (that line stays with the caller, which already owns an Eigen + // dependency this widget deliberately doesn't have). Must be called + // with RL_PROJECTION active and freshly loaded identity (display()'s + // job, same as today), and leaves RL_MODELVIEW active + identity after, + // like the original. + void updateOrtho(float aspect); - // Stores rlGetMatrixModelview()/rlGetMatrixProjection() into - // frameView3D/frameProj3D. Call once per frame right after - // applyPerspectiveProjection()+the view multiply, or updateOrtho() -- - // before any 2D/ImGui drawing resets the matrix stack. - void captureFrameMatrices(); + // Stores rlGetMatrixModelview()/rlGetMatrixProjection() into + // frameView3D/frameProj3D. Call once per frame right after + // applyPerspectiveProjection()+the view multiply, or updateOrtho() -- + // before any 2D/ImGui drawing resets the matrix stack. + void captureFrameMatrices(); - // Unprojects screen point (x,y) through frameView3D/frameProj3D into a - // world-space ray -- was multi_view_tls_registration's GetLaserBeam(). - // Pass the actual framebuffer width/height (e.g. GetScreenWidth()/ - // GetScreenHeight()), not a cached value, so a mid-frame resize can't - // desync it from the viewport applyPerspectiveProjection()/ - // applyOrthoProjection() actually set up. Like GetLaserBeam(), this - // reads whatever frameView3D/frameProj3D last captured -- typically the - // *previous* frame's matrices when called from input handling that runs - // before this frame's captureFrameMatrices(), which is deliberate (see - // frameView3D's declaration comment). - Ray eulerScreenRay(int x, int y, int screenW, int screenH) const; + // Unprojects screen point (x,y) through frameView3D/frameProj3D into a + // world-space ray -- was multi_view_tls_registration's GetLaserBeam(). + // Pass the actual framebuffer width/height (e.g. GetScreenWidth()/ + // GetScreenHeight()), not a cached value, so a mid-frame resize can't + // desync it from the viewport applyPerspectiveProjection()/ + // applyOrthoProjection() actually set up. Like GetLaserBeam(), this + // reads whatever frameView3D/frameProj3D last captured -- typically the + // *previous* frame's matrices when called from input handling that runs + // before this frame's captureFrameMatrices(), which is deliberate (see + // frameView3D's declaration comment). + Ray eulerScreenRay(int x, int y, int screenW, int screenH) const; - // Left-drag: orbit. Was motion()'s `mouse_buttons & 1` branch. - void dragOrbit(float dx, float dy); - // Right-drag in perspective mode: pan. Was motion()'s `mouse_buttons & 4` - // branch, !is_ortho case. - void dragPanPerspective(float dx, float dy); - // Right-drag in ortho mode: pan. Was motion()'s `mouse_buttons & 4` - // branch, is_ortho case. displayW/displayH: caller's display size (e.g. - // ImGui's io.DisplaySize), needed for the same screen-space-to-world - // scaling the original used. - void dragPanOrtho(float dx, float dy, float displayW, float displayH); - // Mouse-wheel zoom/dolly -- was wheel()'s camera-mutating branch, now - // scaled by the actual per-frame scroll magnitude (raylib's - // GetMouseWheelMove()) instead of only its sign. A physical mouse wheel - // notch reports ~1.0, so passing that through reproduces the original's - // fixed per-notch step exactly; a trackpad's continuous smooth-scroll - // deltas (fractions of 1.0, many per frame during a gesture) now move - // the camera proportionally instead of snapping a full step on every - // one of those frames -- which is what made a trackpad pinch/scroll - // feel like it "teleported" before this took magnitude into account. - // Deliberately does NOT call breakEulerTransition() (which also snaps - // rotationCenter) -- matches the original, which just cleared - // camera_transition_active here, leaving rotationCenter wherever it had - // eased to. - void zoom(float wheelDelta, bool shiftHeld); + // Left-drag: orbit. Was motion()'s `mouse_buttons & 1` branch. + void dragOrbit(float dx, float dy); + // Right-drag in perspective mode: pan. Was motion()'s `mouse_buttons & 4` + // branch, !is_ortho case. + void dragPanPerspective(float dx, float dy); + // Right-drag in ortho mode: pan. Was motion()'s `mouse_buttons & 4` + // branch, is_ortho case. displayW/displayH: caller's display size (e.g. + // ImGui's io.DisplaySize), needed for the same screen-space-to-world + // scaling the original used. + void dragPanOrtho(float dx, float dy, float displayW, float displayH); + // Mouse-wheel zoom/dolly -- was wheel()'s camera-mutating branch, now + // scaled by the actual per-frame scroll magnitude (raylib's + // GetMouseWheelMove()) instead of only its sign. A physical mouse wheel + // notch reports ~1.0, so passing that through reproduces the original's + // fixed per-notch step exactly; a trackpad's continuous smooth-scroll + // deltas (fractions of 1.0, many per frame during a gesture) now move + // the camera proportionally instead of snapping a full step on every + // one of those frames -- which is what made a trackpad pinch/scroll + // feel like it "teleported" before this took magnitude into account. + // Deliberately does NOT call breakEulerTransition() (which also snaps + // rotationCenter) -- matches the original, which just cleared + // camera_transition_active here, leaving rotationCenter wherever it had + // eased to. + void zoom(float wheelDelta, bool shiftHeld); - // Was setCameraPreset(). Note: the original's CAMERA_RESET also reset - // an app-specific viewer_decimate_point_cloud setting outside the - // camera's own state -- callers still need to do that themselves. - void setEulerPreset(EulerPreset preset); - // General eased-transition start -- was the repeated - // `new_rotation_center = ...; new_rotate_x = rotate_x; ...; - // camera_transition_active = true;` pattern at each call site. - void startEulerTransition(float rotateX, float rotateY, Vector3 translate, Vector3 rotationCenter); - // Convenience for the common "recenter on a picked point" pattern (was - // repeated in getClosestTrajectoryPoint()/setNewRotationCenter()/ - // cor_window()'s Set button): keeps the current rotate angles and Z - // distance, moves rotationCenter to `center`, and sets translate.xy to - // -center.xy so the new center recentres under the (unchanged) view. - void moveEulerRotationCenterTo(Vector3 center); - // Eases euler toward eulerGoal; call once per frame with the frame's - // delta time. Was updateCameraTransition(). - void updateEulerTransition(float dt); - // Was breakCameraTransition(): snaps euler.rotationCenter to - // eulerGoal.rotationCenter and ends the transition. Note this leaves - // rotateX/rotateY/translate wherever they had already eased to -- matches - // the original exactly (asymmetric with updateEulerTransition's - // full-snap-on-completion). - void breakEulerTransition(); -}; + // Was setCameraPreset(). Note: the original's CAMERA_RESET also reset + // an app-specific viewer_decimate_point_cloud setting outside the + // camera's own state -- callers still need to do that themselves. + void setEulerPreset(EulerPreset preset); + // General eased-transition start -- was the repeated + // `new_rotation_center = ...; new_rotate_x = rotate_x; ...; + // camera_transition_active = true;` pattern at each call site. + void startEulerTransition(float rotateX, float rotateY, Vector3 translate, Vector3 rotationCenter); + // Convenience for the common "recenter on a picked point" pattern (was + // repeated in getClosestTrajectoryPoint()/setNewRotationCenter()/ + // cor_window()'s Set button): keeps the current rotate angles and Z + // distance, moves rotationCenter to `center`, and sets translate.xy to + // -center.xy so the new center recentres under the (unchanged) view. + void moveEulerRotationCenterTo(Vector3 center); + // Eases euler toward eulerGoal; call once per frame with the frame's + // delta time. Was updateCameraTransition(). + void updateEulerTransition(float dt); + // Was breakCameraTransition(): snaps euler.rotationCenter to + // eulerGoal.rotationCenter and ends the transition. Note this leaves + // rotateX/rotateY/translate wherever they had already eased to -- matches + // the original exactly (asymmetric with updateEulerTransition's + // full-snap-on-completion). + void breakEulerTransition(); + }; -// Small 3-axis crosshair marking the current center of rotation. Call inside -// BeginMode3D/EndMode3D, every frame -- drawing at camera.target (which -// updateTransition() eases each frame) makes the cross visibly slide to a -// newly-picked/typed center along with the camera. -void drawRotationCenterCross(Vector3 center, float size, Color color); + // Small 3-axis crosshair marking the current center of rotation. Call inside + // BeginMode3D/EndMode3D, every frame -- drawing at camera.target (which + // updateTransition() eases each frame) makes the cross visibly slide to a + // newly-picked/typed center along with the camera. + void drawRotationCenterCross(Vector3 center, float size, Color color); -// Restores rlgl's default 2D screen-space projection (matches what raylib's -// own EndMode3D() does) -- for apps that drive the rlgl matrix stack -// manually each frame (rlMatrixMode/rlFrustum/rlOrtho/rlMultMatrixf, e.g. -// via OrbitCamera's Euler-mode applyPerspectiveProjection()/updateOrtho()) -// instead of using raylib's BeginMode3D/EndMode3D wrapper, which can't be -// mixed with that -- was multi_view_tls_registration's end3DMatrixStack(). -// Must be called after all 3D drawing and before any 2D drawing (a mini- -// compass, ImGui) each frame. displayW/displayH should be the caller's -// logical display size (e.g. ImGui's io.DisplaySize.x/y), not -// GetScreenWidth()/Height() -- those can differ under DPI scaling, and this -// has to match whatever the viewport was actually set from. -void end3DMatrixStack(float displayW, float displayH); + // Restores rlgl's default 2D screen-space projection (matches what raylib's + // own EndMode3D() does) -- for apps that drive the rlgl matrix stack + // manually each frame (rlMatrixMode/rlFrustum/rlOrtho/rlMultMatrixf, e.g. + // via OrbitCamera's Euler-mode applyPerspectiveProjection()/updateOrtho()) + // instead of using raylib's BeginMode3D/EndMode3D wrapper, which can't be + // mixed with that -- was multi_view_tls_registration's end3DMatrixStack(). + // Must be called after all 3D drawing and before any 2D drawing (a mini- + // compass, ImGui) each frame. displayW/displayH should be the caller's + // logical display size (e.g. ImGui's io.DisplaySize.x/y), not + // GetScreenWidth()/Height() -- those can differ under DPI scaling, and this + // has to match whatever the viewport was actually set from. + void end3DMatrixStack(float displayW, float displayH); } // namespace raylib_widgets diff --git a/raylib_widgets/include/RaylibWidgets/RayPlaneD.h b/raylib_widgets/include/RaylibWidgets/RayPlaneD.h index 25622353..3bb6c783 100644 --- a/raylib_widgets/include/RaylibWidgets/RayPlaneD.h +++ b/raylib_widgets/include/RaylibWidgets/RayPlaneD.h @@ -9,23 +9,24 @@ // PointPicking.h, which stays Eigen-free for camera_lidar_calibration/ // camera_lidar_intrinsics_calib -- only link/include this header if you // actually need double precision. -namespace raylib_widgets { +namespace raylib_widgets +{ -// Intersects the ray (rayPos + t*rayDir, t >= 0 not enforced -- callers -// that only want points ahead of the ray should check the sign of the -// result relative to rayPos themselves) with the plane -// a*x + b*y + c*z + d = 0. Returns false (outPoint untouched) if the ray is -// ~parallel to the plane (rayDir's component along the plane normal is -// within 1e-4 of zero). -bool intersectPlane( - const Eigen::Vector3d& rayPos, const Eigen::Vector3d& rayDir, double a, double b, double c, double d, Eigen::Vector3d& outPoint); + // Intersects the ray (rayPos + t*rayDir, t >= 0 not enforced -- callers + // that only want points ahead of the ray should check the sign of the + // result relative to rayPos themselves) with the plane + // a*x + b*y + c*z + d = 0. Returns false (outPoint untouched) if the ray is + // ~parallel to the plane (rayDir's component along the plane normal is + // within 1e-4 of zero). + bool intersectPlane( + const Eigen::Vector3d& rayPos, const Eigen::Vector3d& rayDir, double a, double b, double c, double d, Eigen::Vector3d& outPoint); -// Perpendicular distance from `point` to the infinite line through rayPos -// with direction rayDir. Does NOT normalize by |rayDir| -- the result scales -// with rayDir's own magnitude, matching the original call sites' only use -// of this (finding the nearest of several candidate points against one -// fixed ray, where a constant scale factor across all candidates doesn't -// change which one wins). -double distancePointToLine(const Eigen::Vector3d& point, const Eigen::Vector3d& rayPos, const Eigen::Vector3d& rayDir); + // Perpendicular distance from `point` to the infinite line through rayPos + // with direction rayDir. Does NOT normalize by |rayDir| -- the result scales + // with rayDir's own magnitude, matching the original call sites' only use + // of this (finding the nearest of several candidate points against one + // fixed ray, where a constant scale factor across all candidates doesn't + // change which one wins). + double distancePointToLine(const Eigen::Vector3d& point, const Eigen::Vector3d& rayPos, const Eigen::Vector3d& rayDir); } // namespace raylib_widgets diff --git a/raylib_widgets/include/RaylibWidgets/Shaders.h b/raylib_widgets/include/RaylibWidgets/Shaders.h index 2246cf42..6708774b 100644 --- a/raylib_widgets/include/RaylibWidgets/Shaders.h +++ b/raylib_widgets/include/RaylibWidgets/Shaders.h @@ -6,19 +6,20 @@ // core_raylib gets via a plain target_include_directories (not a full // raylib_widgets link, to avoid pulling imgui_raylib into core_raylib as a // new transitive dependency for what's just a compile-time string constant). -namespace raylib_widgets { +namespace raylib_widgets +{ -// Standard "jet" colormap (blue -> cyan -> yellow -> red), t in [0,1]. Was -// byte-for-byte duplicated in core/src/raylib_render.cpp's ScanRenderer, -// apps/camera_lidar_calibration/Renderer.cpp's two point shaders, and -// apps/camera_lidar_trajectory_viewer/TrajectoryViewer.cpp's point shader. -// Splice into a fragment shader's source via string concatenation, e.g.: -// static const std::string kMyFS = std::string(R"(#version 330 -// ...uniforms/varyings... -// )") + raylib_widgets::kJetColormapGLSL + R"( -// void main() { ... } -// )"; -inline constexpr const char* kJetColormapGLSL = R"( + // Standard "jet" colormap (blue -> cyan -> yellow -> red), t in [0,1]. Was + // byte-for-byte duplicated in core/src/raylib_render.cpp's ScanRenderer, + // apps/camera_lidar_calibration/Renderer.cpp's two point shaders, and + // apps/camera_lidar_trajectory_viewer/TrajectoryViewer.cpp's point shader. + // Splice into a fragment shader's source via string concatenation, e.g.: + // static const std::string kMyFS = std::string(R"(#version 330 + // ...uniforms/varyings... + // )") + raylib_widgets::kJetColormapGLSL + R"( + // void main() { ... } + // )"; + inline constexpr const char* kJetColormapGLSL = R"( vec3 jet(float t) { t = clamp(t, 0.0, 1.0); return clamp(vec3(1.5 - abs(4.0*t - 3.0), diff --git a/raylib_widgets/include/RaylibWidgets/ShortcutsTable.h b/raylib_widgets/include/RaylibWidgets/ShortcutsTable.h index f3b8063e..552a55e8 100644 --- a/raylib_widgets/include/RaylibWidgets/ShortcutsTable.h +++ b/raylib_widgets/include/RaylibWidgets/ShortcutsTable.h @@ -6,16 +6,19 @@ // shared between apps/multi_view_tls_registration (rl_utils.cpp's // ShowShortcutsTable/ShortcutEntry) and the camera_lidar_* apps. Depends on // raylib_widgets' existing imgui_raylib link, no Eigen/core coupling. -namespace raylib_widgets { +namespace raylib_widgets +{ -struct ShortcutEntry { - std::string type; // group header (e.g. "Normal keys"), shown once when it changes from the previous entry ("" = no header) - std::string shortcut; // key combo label, e.g. "Ctrl+O" - std::string description; // shown next to shortcut; entries with an empty description are skipped (but still contribute a header if `type` is set) -}; + struct ShortcutEntry + { + std::string type; // group header (e.g. "Normal keys"), shown once when it changes from the previous entry ("" = no header) + std::string shortcut; // key combo label, e.g. "Ctrl+O" + std::string description; // shown next to shortcut; entries with an empty description are skipped (but still contribute a header if + // `type` is set) + }; -// Renders `entries` as a bordered, scrollable two-column table (Shortcut | -// Description) inside the current ImGui window. -void ShowShortcutsTable(const std::vector& entries); + // Renders `entries` as a bordered, scrollable two-column table (Shortcut | + // Description) inside the current ImGui window. + void ShowShortcutsTable(const std::vector& entries); } // namespace raylib_widgets diff --git a/raylib_widgets/include/RaylibWidgets/WindowFit.h b/raylib_widgets/include/RaylibWidgets/WindowFit.h index cd1545bf..f4bb9c1d 100644 --- a/raylib_widgets/include/RaylibWidgets/WindowFit.h +++ b/raylib_widgets/include/RaylibWidgets/WindowFit.h @@ -6,12 +6,13 @@ // apps/camera_lidar_intrinsics_calib; multi_view_tls_registration_step_2 had // its own inline version without the DPI-scale correction (fine on a 1x // display, but centers/sizes wrong on a 2x Retina one). -namespace raylib_widgets { +namespace raylib_widgets +{ -// marginW/marginH: side/top+bottom breathing room (OS menu bar, title bar, -// dock) subtracted from the monitor's usable area. -// centerVertically: false positions the window near the top (Y=30, the -// calib apps' behavior); true centers it vertically (step2's behavior). -void fitWindowToScreen(int marginW = 40, int marginH = 100, bool centerVertically = false); + // marginW/marginH: side/top+bottom breathing room (OS menu bar, title bar, + // dock) subtracted from the monitor's usable area. + // centerVertically: false positions the window near the top (Y=30, the + // calib apps' behavior); true centers it vertically (step2's behavior). + void fitWindowToScreen(int marginW = 40, int marginH = 100, bool centerVertically = false); } // namespace raylib_widgets diff --git a/raylib_widgets/src/AppShell.cpp b/raylib_widgets/src/AppShell.cpp index 0e29a850..cb30749e 100644 --- a/raylib_widgets/src/AppShell.cpp +++ b/raylib_widgets/src/AppShell.cpp @@ -19,133 +19,138 @@ // clang-format on #endif -namespace raylib_widgets { - -void ShowMainDockSpace() +namespace raylib_widgets { - static bool first_time = true; - ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | - ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs; + void ShowMainDockSpace() + { + static bool first_time = true; - ImGuiViewport* viewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(viewport->WorkPos); - ImGui::SetNextWindowSize(viewport->WorkSize); - ImGui::SetNextWindowViewport(viewport->ID); + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | + ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs; - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->WorkPos); + ImGui::SetNextWindowSize(viewport->WorkSize); + ImGui::SetNextWindowViewport(viewport->ID); - ImGui::Begin("MainDockSpace", nullptr, window_flags); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - ImGui::PopStyleVar(2); + ImGui::Begin("MainDockSpace", nullptr, window_flags); - ImGuiID dockspace_id = ImGui::GetID("MyDockSpace"); - ImGui::DockSpace(dockspace_id, ImVec2(0, 0), ImGuiDockNodeFlags_PassthruCentralNode | ImGuiDockNodeFlags_NoDockingInCentralNode); + ImGui::PopStyleVar(2); - if (first_time) - { - first_time = false; + ImGuiID dockspace_id = ImGui::GetID("MyDockSpace"); + ImGui::DockSpace(dockspace_id, ImVec2(0, 0), ImGuiDockNodeFlags_PassthruCentralNode | ImGuiDockNodeFlags_NoDockingInCentralNode); - auto dock_id_left = ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.2f, nullptr, &dockspace_id); - auto dock_id_bottom = ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Down, 0.2f, nullptr, &dockspace_id); - (void)dock_id_left; + if (first_time) + { + first_time = false; - ImGui::DockBuilderDockWindow("Console", dock_id_bottom); - ImGui::DockBuilderFinish(dockspace_id); - } + auto dock_id_left = ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.2f, nullptr, &dockspace_id); + auto dock_id_bottom = ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Down, 0.2f, nullptr, &dockspace_id); + (void)dock_id_left; - ImGui::End(); -} + ImGui::DockBuilderDockWindow("Console", dock_id_bottom); + ImGui::DockBuilderFinish(dockspace_id); + } -void ImGuiHyperlink(const char* url, ImVec4 color) -{ - ImGui::PushStyleColor(ImGuiCol_Text, color); - ImGui::TextUnformatted(url); - ImGui::PopStyleColor(); + ImGui::End(); + } - ImVec2 pos = ImGui::GetItemRectMin(); - ImVec2 size = ImGui::GetItemRectSize(); + void ImGuiHyperlink(const char* url, ImVec4 color) + { + ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::TextUnformatted(url); + ImGui::PopStyleColor(); - if (ImGui::IsItemHovered()) - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImVec2 pos = ImGui::GetItemRectMin(); + ImVec2 size = ImGui::GetItemRectSize(); - if (ImGui::IsItemHovered()) - { - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - draw_list->AddLine(ImVec2(pos.x, pos.y + size.y), ImVec2(pos.x + size.x, pos.y + size.y), ImColor(color)); - } + if (ImGui::IsItemHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - if (ImGui::IsItemClicked()) - { + if (ImGui::IsItemHovered()) + { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + draw_list->AddLine(ImVec2(pos.x, pos.y + size.y), ImVec2(pos.x + size.x, pos.y + size.y), ImColor(color)); + } + + if (ImGui::IsItemClicked()) + { #ifdef _WIN32 - ShellExecuteA(0, "open", url, 0, 0, 1 /* SW_SHOWNORMAL, unavailable under NOUSER -- see this file's top comment */); + ShellExecuteA(0, "open", url, 0, 0, 1 /* SW_SHOWNORMAL, unavailable under NOUSER -- see this file's top comment */); #elif __APPLE__ - std::string cmd = std::string("open ") + url; - system(cmd.c_str()); + std::string cmd = std::string("open ") + url; + system(cmd.c_str()); #else - std::string cmd = std::string("xdg-open ") + url; - system(cmd.c_str()); + std::string cmd = std::string("xdg-open ") + url; + system(cmd.c_str()); #endif + } } -} -void ShowInfoWindow( - bool& open, const std::vector& infoLines, const std::vector& appShortcuts, const char* versionString, - const char* buildDate) -{ - if (!open) - return; + void ShowInfoWindow( + bool& open, + const std::vector& infoLines, + const std::vector& appShortcuts, + const char* versionString, + const char* buildDate) + { + if (!open) + return; - static bool show_about = false; + static bool show_about = false; - if (ImGui::Begin( - "Info", &open, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking | - ImGuiWindowFlags_NoCollapse)) - { - bool firstLine = true; - for (const auto& line : infoLines) + if (ImGui::Begin( + "Info", + &open, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoCollapse)) { - if (line.empty()) - ImGui::NewLine(); - else if (line.rfind("https://", 0) == 0) - ImGuiHyperlink(line.c_str()); - else - ImGui::TextUnformatted(line.c_str()); - - if (firstLine) + bool firstLine = true; + for (const auto& line : infoLines) { - ImGui::SameLine( - ImGui::GetWindowWidth() - ImGui::CalcTextSize("ImGui").x - ImGui::GetStyle().ItemSpacing.x * 2 - - ImGui::GetStyle().FramePadding.x * 2); - if (ImGui::Button("ImGui")) - show_about = true; - - firstLine = false; + if (line.empty()) + ImGui::NewLine(); + else if (line.rfind("https://", 0) == 0) + ImGuiHyperlink(line.c_str()); + else + ImGui::TextUnformatted(line.c_str()); + + if (firstLine) + { + ImGui::SameLine( + ImGui::GetWindowWidth() - ImGui::CalcTextSize("ImGui").x - ImGui::GetStyle().ItemSpacing.x * 2 - + ImGui::GetStyle().FramePadding.x * 2); + if (ImGui::Button("ImGui")) + show_about = true; + + firstLine = false; + } } - } - ImGui::NewLine(); - ImGui::Text("Author: Janusz Bedkowski & contributors"); - ImGui::NewLine(); - ImGui::Text("Part of HDMapping software suite"); - ImGui::Text("Version: %s (%s)", versionString, buildDate); - ImGui::Text("Project page: "); - ImGui::SameLine(); - ImGuiHyperlink("https://github.com/MapsHD/HDMapping"); + ImGui::NewLine(); + ImGui::Text("Author: Janusz Bedkowski & contributors"); + ImGui::NewLine(); + ImGui::Text("Part of HDMapping software suite"); + ImGui::Text("Version: %s (%s)", versionString, buildDate); + ImGui::Text("Project page: "); + ImGui::SameLine(); + ImGuiHyperlink("https://github.com/MapsHD/HDMapping"); - ImGui::NewLine(); - ImGui::Separator(); - ImGui::NewLine(); + ImGui::NewLine(); + ImGui::Separator(); + ImGui::NewLine(); - ShowShortcutsTable(appShortcuts); + ShowShortcutsTable(appShortcuts); - if (show_about) - ImGui::ShowAboutWindow(&show_about); - } + if (show_about) + ImGui::ShowAboutWindow(&show_about); + } - ImGui::End(); -} + ImGui::End(); + } } // namespace raylib_widgets diff --git a/raylib_widgets/src/CenterOfRotationWindow.cpp b/raylib_widgets/src/CenterOfRotationWindow.cpp index 6a86beb9..c6725dba 100644 --- a/raylib_widgets/src/CenterOfRotationWindow.cpp +++ b/raylib_widgets/src/CenterOfRotationWindow.cpp @@ -2,95 +2,95 @@ #include -namespace raylib_widgets { - -void showCenterOfRotationWindow(bool& open, OrbitCamera& camera) +namespace raylib_widgets { - static Vector3 pending = { 0.f, 0.f, 0.f }; - - if (open) - { - pending = camera.target; - ImGui::OpenPopup("Center of rotation"); - open = false; - } - if (ImGui::BeginPopupModal("Center of rotation", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + void showCenterOfRotationWindow(bool& open, OrbitCamera& camera) { - ImGui::Text("Select new center of rotation [m]:"); - ImGui::PushItemWidth(120.f); - ImGui::InputFloat("X", &pending.x, 0.0f, 0.0f, "%.3f"); - ImGui::SameLine(); - ImGui::InputFloat("Y", &pending.y, 0.0f, 0.0f, "%.3f"); - ImGui::SameLine(); - ImGui::InputFloat("Z", &pending.z, 0.0f, 0.0f, "%.3f"); - ImGui::PopItemWidth(); - - ImGui::Separator(); + static Vector3 pending = { 0.f, 0.f, 0.f }; - if (ImGui::Button("Set")) + if (open) { - camera.moveTargetTo(pending); - ImGui::CloseCurrentPopup(); + pending = camera.target; + ImGui::OpenPopup("Center of rotation"); + open = false; } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) + + if (ImGui::BeginPopupModal("Center of rotation", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::CloseCurrentPopup(); - } + ImGui::Text("Select new center of rotation [m]:"); + ImGui::PushItemWidth(120.f); + ImGui::InputFloat("X", &pending.x, 0.0f, 0.0f, "%.3f"); + ImGui::SameLine(); + ImGui::InputFloat("Y", &pending.y, 0.0f, 0.0f, "%.3f"); + ImGui::SameLine(); + ImGui::InputFloat("Z", &pending.z, 0.0f, 0.0f, "%.3f"); + ImGui::PopItemWidth(); - ImGui::EndPopup(); - } -} + ImGui::Separator(); -void showEulerCenterOfRotationWindow( - bool& open, OrbitCamera& camera, const char* xTooltip, const char* yTooltip, const char* zTooltip) -{ - if (open) - { - ImGui::OpenPopup("Center of rotation"); - open = false; + if (ImGui::Button("Set")) + { + camera.moveTargetTo(pending); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } } - if (ImGui::BeginPopupModal("Center of rotation", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + void showEulerCenterOfRotationWindow(bool& open, OrbitCamera& camera, const char* xTooltip, const char* yTooltip, const char* zTooltip) { - // Bound directly to camera.eulerGoal.rotationCenter (not a - // separately-initialized local) -- matches the original - // cor_window(), which edited its own persistent new_rotation_center - // field in place rather than seeding a fresh copy each time the - // popup opens. - Vector3& pending = camera.eulerGoal.rotationCenter; - - ImGui::Text("Select new center of rotation [m]:"); - ImGui::PushItemWidth(120.f); - ImGui::InputFloat("X", &pending.x, 0.0f, 0.0f, "%.3f"); - if (xTooltip && ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", xTooltip); - ImGui::SameLine(); - ImGui::InputFloat("Y", &pending.y, 0.0f, 0.0f, "%.3f"); - if (yTooltip && ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", yTooltip); - ImGui::SameLine(); - ImGui::InputFloat("Z", &pending.z, 0.0f, 0.0f, "%.3f"); - if (zTooltip && ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", zTooltip); - ImGui::PopItemWidth(); - - ImGui::Separator(); - - if (ImGui::Button("Set")) + if (open) { - camera.moveEulerRotationCenterTo(pending); - ImGui::CloseCurrentPopup(); + ImGui::OpenPopup("Center of rotation"); + open = false; } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) + + if (ImGui::BeginPopupModal("Center of rotation", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::CloseCurrentPopup(); - } + // Bound directly to camera.eulerGoal.rotationCenter (not a + // separately-initialized local) -- matches the original + // cor_window(), which edited its own persistent new_rotation_center + // field in place rather than seeding a fresh copy each time the + // popup opens. + Vector3& pending = camera.eulerGoal.rotationCenter; + + ImGui::Text("Select new center of rotation [m]:"); + ImGui::PushItemWidth(120.f); + ImGui::InputFloat("X", &pending.x, 0.0f, 0.0f, "%.3f"); + if (xTooltip && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", xTooltip); + ImGui::SameLine(); + ImGui::InputFloat("Y", &pending.y, 0.0f, 0.0f, "%.3f"); + if (yTooltip && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", yTooltip); + ImGui::SameLine(); + ImGui::InputFloat("Z", &pending.z, 0.0f, 0.0f, "%.3f"); + if (zTooltip && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", zTooltip); + ImGui::PopItemWidth(); + + ImGui::Separator(); - ImGui::EndPopup(); + if (ImGui::Button("Set")) + { + camera.moveEulerRotationCenterTo(pending); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } } -} } // namespace raylib_widgets diff --git a/raylib_widgets/src/CompassRuler.cpp b/raylib_widgets/src/CompassRuler.cpp index 2132da88..ade1a0b2 100644 --- a/raylib_widgets/src/CompassRuler.cpp +++ b/raylib_widgets/src/CompassRuler.cpp @@ -6,56 +6,58 @@ #include #include -namespace raylib_widgets { - -void drawCompassRuler(Vector3 right, Vector3 up, float zoomDistance, Color rulerColor, CompassAxisLabels labels) +namespace raylib_widgets { - const float compassSize = 200.0f; - const float originX = compassSize * 0.5f; - const float originY = static_cast(GetScreenHeight()) - compassSize * 0.5f; - const float axisPixelLength = compassSize * 0.35f; - - struct Axis { - Vector3 dir; - const char* label; - Color color; - }; - const Axis axes[3] = { - { Vector3{ 1.f, 0.f, 0.f }, labels.x, RED }, - { Vector3{ 0.f, 1.f, 0.f }, labels.y, GREEN }, - { Vector3{ 0.f, 0.f, 1.f }, labels.z, BLUE }, - }; - for (const auto& axis : axes) + + void drawCompassRuler(Vector3 right, Vector3 up, float zoomDistance, Color rulerColor, CompassAxisLabels labels) { - float ex = Vector3DotProduct(axis.dir, right); - float ey = Vector3DotProduct(axis.dir, up); - Vector2 tip = { originX + ex * axisPixelLength, originY - ey * axisPixelLength }; - DrawLineEx(Vector2{ originX, originY }, tip, 2.f, axis.color); - DrawText(axis.label, (int)tip.x + 4, (int)tip.y - 6, 12, axis.color); + const float compassSize = 200.0f; + const float originX = compassSize * 0.5f; + const float originY = static_cast(GetScreenHeight()) - compassSize * 0.5f; + const float axisPixelLength = compassSize * 0.35f; + + struct Axis + { + Vector3 dir; + const char* label; + Color color; + }; + const Axis axes[3] = { + { Vector3{ 1.f, 0.f, 0.f }, labels.x, RED }, + { Vector3{ 0.f, 1.f, 0.f }, labels.y, GREEN }, + { Vector3{ 0.f, 0.f, 1.f }, labels.z, BLUE }, + }; + for (const auto& axis : axes) + { + float ex = Vector3DotProduct(axis.dir, right); + float ey = Vector3DotProduct(axis.dir, up); + Vector2 tip = { originX + ex * axisPixelLength, originY - ey * axisPixelLength }; + DrawLineEx(Vector2{ originX, originY }, tip, 2.f, axis.color); + DrawText(axis.label, (int)tip.x + 4, (int)tip.y - 6, 12, axis.color); + } + + // "Nice" (1/2/5 x 10^n) ruler length tied to the current camera zoom. + float rawUnit = std::max(0.001f, 0.1f * std::fabs(zoomDistance)); + float base = std::pow(10.0f, std::floor(std::log10(rawUnit))); + float normalized = rawUnit / base; + float niceUnit = normalized < 2.0f ? 1.0f : (normalized < 5.0f ? 2.0f : 5.0f); + float worldLength = niceUnit * base; + + char label[32]; + if (worldLength >= 1000.0f) + snprintf(label, sizeof(label), "%.0f [km]", worldLength / 1000.0f); + else if (worldLength >= 1.0f) + snprintf(label, sizeof(label), "%.0f [m]", worldLength); + else if (worldLength >= 0.01f) + snprintf(label, sizeof(label), "%.0f [cm]", worldLength * 100.0f); + else + snprintf(label, sizeof(label), "<1 [cm]"); + + float rulerY = originY + compassSize * 0.45f; + DrawLineEx(Vector2{ originX - 40.f, rulerY }, Vector2{ originX + 40.f, rulerY }, 2.f, rulerColor); + DrawLineEx(Vector2{ originX - 40.f, rulerY - 5.f }, Vector2{ originX - 40.f, rulerY + 5.f }, 2.f, rulerColor); + DrawLineEx(Vector2{ originX + 40.f, rulerY - 5.f }, Vector2{ originX + 40.f, rulerY + 5.f }, 2.f, rulerColor); + DrawText(label, (int)originX - 20, (int)rulerY + 6, 14, rulerColor); } - // "Nice" (1/2/5 x 10^n) ruler length tied to the current camera zoom. - float rawUnit = std::max(0.001f, 0.1f * std::fabs(zoomDistance)); - float base = std::pow(10.0f, std::floor(std::log10(rawUnit))); - float normalized = rawUnit / base; - float niceUnit = normalized < 2.0f ? 1.0f : (normalized < 5.0f ? 2.0f : 5.0f); - float worldLength = niceUnit * base; - - char label[32]; - if (worldLength >= 1000.0f) - snprintf(label, sizeof(label), "%.0f [km]", worldLength / 1000.0f); - else if (worldLength >= 1.0f) - snprintf(label, sizeof(label), "%.0f [m]", worldLength); - else if (worldLength >= 0.01f) - snprintf(label, sizeof(label), "%.0f [cm]", worldLength * 100.0f); - else - snprintf(label, sizeof(label), "<1 [cm]"); - - float rulerY = originY + compassSize * 0.45f; - DrawLineEx(Vector2{ originX - 40.f, rulerY }, Vector2{ originX + 40.f, rulerY }, 2.f, rulerColor); - DrawLineEx(Vector2{ originX - 40.f, rulerY - 5.f }, Vector2{ originX - 40.f, rulerY + 5.f }, 2.f, rulerColor); - DrawLineEx(Vector2{ originX + 40.f, rulerY - 5.f }, Vector2{ originX + 40.f, rulerY + 5.f }, 2.f, rulerColor); - DrawText(label, (int)originX - 20, (int)rulerY + 6, 14, rulerColor); -} - } // namespace raylib_widgets diff --git a/raylib_widgets/src/OrbitCamera.cpp b/raylib_widgets/src/OrbitCamera.cpp index 98876790..51dda4ea 100644 --- a/raylib_widgets/src/OrbitCamera.cpp +++ b/raylib_widgets/src/OrbitCamera.cpp @@ -6,466 +6,546 @@ #include #include -namespace raylib_widgets { - -Camera3D OrbitCamera::toRaylib() const { - float az = azimuth * (float)DEG2RAD; - float el = elevation * (float)DEG2RAD; - Vector3 pos = { - target.x + distance * std::cos(el) * std::sin(az), - target.y + distance * std::sin(el), - target.z + distance * std::cos(el) * std::cos(az) - }; - Camera3D cam; - cam.position = pos; - cam.target = target; - cam.up = {0.f, 1.f, 0.f}; - if (sphericalOrtho) { - cam.fovy = sphericalOrthoHeight; - cam.projection = CAMERA_ORTHOGRAPHIC; - } else { - cam.fovy = 45.f; - cam.projection = CAMERA_PERSPECTIVE; +namespace raylib_widgets +{ + + Camera3D OrbitCamera::toRaylib() const + { + float az = azimuth * (float)DEG2RAD; + float el = elevation * (float)DEG2RAD; + Vector3 pos = { target.x + distance * std::cos(el) * std::sin(az), + target.y + distance * std::sin(el), + target.z + distance * std::cos(el) * std::cos(az) }; + Camera3D cam; + cam.position = pos; + cam.target = target; + cam.up = { 0.f, 1.f, 0.f }; + if (sphericalOrtho) + { + cam.fovy = sphericalOrthoHeight; + cam.projection = CAMERA_ORTHOGRAPHIC; + } + else + { + cam.fovy = 45.f; + cam.projection = CAMERA_PERSPECTIVE; + } + return cam; } - return cam; -} - -void OrbitCamera::update(bool active) { - if (!active) return; - - // Left-drag → orbit - if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { - Vector2 d = GetMouseDelta(); - azimuth -= d.x * 0.4f; - elevation += d.y * 0.4f; - elevation = std::max(-89.f, std::min(89.f, elevation)); - cancelTransition(); + + void OrbitCamera::update(bool active) + { + if (!active) + return; + + // Left-drag → orbit + if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) + { + Vector2 d = GetMouseDelta(); + azimuth -= d.x * 0.4f; + elevation += d.y * 0.4f; + elevation = std::max(-89.f, std::min(89.f, elevation)); + cancelTransition(); + } + // Right-drag → pan + if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) + { + Camera3D cam = toRaylib(); + Vector3 fwd = Vector3Normalize(Vector3Subtract(cam.target, cam.position)); + Vector3 right = Vector3Normalize(Vector3CrossProduct(fwd, cam.up)); + Vector3 up = Vector3CrossProduct(right, fwd); + Vector2 d = GetMouseDelta(); + float speed = distance * 0.002f; + target = Vector3Add(target, Vector3Scale(right, -d.x * speed)); + target = Vector3Add(target, Vector3Scale(up, d.y * speed)); + cancelTransition(); + } + // Scroll → zoom (distance in perspective, sphericalOrthoHeight in ortho + // -- mirrors multi_view_tls_registration's own perspective-dolly vs. + // ortho-zoom split in OrbitCamera::zoom()). + float wheel = GetMouseWheelMove(); + if (wheel != 0.f) + { + if (sphericalOrtho) + { + sphericalOrthoHeight -= wheel * sphericalOrthoHeight * 0.1f; + sphericalOrthoHeight = std::max(0.5f, sphericalOrthoHeight); + } + else + { + distance -= wheel * distance * 0.1f; + distance = std::max(0.5f, distance); + } + } } - // Right-drag → pan - if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) { - Camera3D cam = toRaylib(); - Vector3 fwd = Vector3Normalize(Vector3Subtract(cam.target, cam.position)); - Vector3 right = Vector3Normalize(Vector3CrossProduct(fwd, cam.up)); - Vector3 up = Vector3CrossProduct(right, fwd); - Vector2 d = GetMouseDelta(); - float speed = distance * 0.002f; - target = Vector3Add(target, Vector3Scale(right, -d.x * speed)); - target = Vector3Add(target, Vector3Scale(up, d.y * speed)); - cancelTransition(); + + void OrbitCamera::moveTargetTo(Vector3 newTarget) + { + transitionTarget = newTarget; + // Freeze the others to their current value so they ease "toward + // themselves" (a no-op) -- see this field's declaration comment. + transitionAzimuth = azimuth; + transitionElevation = elevation; + transitionDistance = distance; + transitionSphericalOrthoHeight = sphericalOrthoHeight; + transitionActive = true; } - // Scroll → zoom (distance in perspective, sphericalOrthoHeight in ortho - // -- mirrors multi_view_tls_registration's own perspective-dolly vs. - // ortho-zoom split in OrbitCamera::zoom()). - float wheel = GetMouseWheelMove(); - if (wheel != 0.f) { - if (sphericalOrtho) { - sphericalOrthoHeight -= wheel * sphericalOrthoHeight * 0.1f; - sphericalOrthoHeight = std::max(0.5f, sphericalOrthoHeight); - } else { - distance -= wheel * distance * 0.1f; - distance = std::max(0.5f, distance); + + void OrbitCamera::updateTransition(float dt) + { + if (!transitionActive) + return; + + float t = 1.f - std::pow(1.f - std::min(dt * transitionSpeed, 1.f), 3.f); + + bool doneX = std::fabs(transitionTarget.x - target.x) < 0.01f; + bool doneY = std::fabs(transitionTarget.y - target.y) < 0.01f; + bool doneZ = std::fabs(transitionTarget.z - target.z) < 0.01f; + + if (!doneX) + target.x += (transitionTarget.x - target.x) * t; + if (!doneY) + target.y += (transitionTarget.y - target.y) * t; + if (!doneZ) + target.z += (transitionTarget.z - target.z) * t; + + // Azimuth is an unbounded, unwrapped angle (unlike elevation, which + // update() clamps to +-89) -- after enough manual dragging it can sit + // far outside [0,360), so the raw goal-minus-current delta can be a + // near-full-circle the "long way around". Wrapping it into (-180,180] + // first makes the eased rotation always take the shorter path. + float deltaAz = transitionAzimuth - azimuth; + while (deltaAz > 180.f) + deltaAz -= 360.f; + while (deltaAz < -180.f) + deltaAz += 360.f; + bool doneAz = std::fabs(deltaAz) < 0.01f; + if (!doneAz) + azimuth += deltaAz * t; + + bool doneEl = std::fabs(transitionElevation - elevation) < 0.01f; + if (!doneEl) + elevation += (transitionElevation - elevation) * t; + + bool doneDist = std::fabs(transitionDistance - distance) < 0.01f; + if (!doneDist) + distance += (transitionDistance - distance) * t; + + bool doneOrtho = std::fabs(transitionSphericalOrthoHeight - sphericalOrthoHeight) < 0.01f; + if (!doneOrtho) + sphericalOrthoHeight += (transitionSphericalOrthoHeight - sphericalOrthoHeight) * t; + + transitionActive = !(doneX && doneY && doneZ && doneAz && doneEl && doneDist && doneOrtho); + if (!transitionActive) + { + target = transitionTarget; + azimuth = transitionAzimuth; + elevation = transitionElevation; + distance = transitionDistance; + sphericalOrthoHeight = transitionSphericalOrthoHeight; } } -} - -void OrbitCamera::moveTargetTo(Vector3 newTarget) { - transitionTarget = newTarget; - // Freeze the others to their current value so they ease "toward - // themselves" (a no-op) -- see this field's declaration comment. - transitionAzimuth = azimuth; - transitionElevation = elevation; - transitionDistance = distance; - transitionSphericalOrthoHeight = sphericalOrthoHeight; - transitionActive = true; -} - -void OrbitCamera::updateTransition(float dt) { - if (!transitionActive) return; - - float t = 1.f - std::pow(1.f - std::min(dt * transitionSpeed, 1.f), 3.f); - - bool doneX = std::fabs(transitionTarget.x - target.x) < 0.01f; - bool doneY = std::fabs(transitionTarget.y - target.y) < 0.01f; - bool doneZ = std::fabs(transitionTarget.z - target.z) < 0.01f; - - if (!doneX) target.x += (transitionTarget.x - target.x) * t; - if (!doneY) target.y += (transitionTarget.y - target.y) * t; - if (!doneZ) target.z += (transitionTarget.z - target.z) * t; - - // Azimuth is an unbounded, unwrapped angle (unlike elevation, which - // update() clamps to +-89) -- after enough manual dragging it can sit - // far outside [0,360), so the raw goal-minus-current delta can be a - // near-full-circle the "long way around". Wrapping it into (-180,180] - // first makes the eased rotation always take the shorter path. - float deltaAz = transitionAzimuth - azimuth; - while (deltaAz > 180.f) deltaAz -= 360.f; - while (deltaAz < -180.f) deltaAz += 360.f; - bool doneAz = std::fabs(deltaAz) < 0.01f; - if (!doneAz) azimuth += deltaAz * t; - - bool doneEl = std::fabs(transitionElevation - elevation) < 0.01f; - if (!doneEl) elevation += (transitionElevation - elevation) * t; - - bool doneDist = std::fabs(transitionDistance - distance) < 0.01f; - if (!doneDist) distance += (transitionDistance - distance) * t; - - bool doneOrtho = std::fabs(transitionSphericalOrthoHeight - sphericalOrthoHeight) < 0.01f; - if (!doneOrtho) sphericalOrthoHeight += (transitionSphericalOrthoHeight - sphericalOrthoHeight) * t; - - transitionActive = !(doneX && doneY && doneZ && doneAz && doneEl && doneDist && doneOrtho); - if (!transitionActive) { + + void OrbitCamera::cancelTransition() + { + if (!transitionActive) + return; target = transitionTarget; azimuth = transitionAzimuth; elevation = transitionElevation; distance = transitionDistance; sphericalOrthoHeight = transitionSphericalOrthoHeight; + transitionActive = false; + } + + void OrbitCamera::setPreset(ViewPreset preset) + { + // Only Front/Back/Left/Right/Top/Bottom/Iso change azimuth/elevation; + // Reset changes target/distance/sphericalOrthoHeight too -- see this + // method's declaration comment. + transitionTarget = target; + transitionDistance = distance; + transitionSphericalOrthoHeight = sphericalOrthoHeight; + + switch (preset) + { + case ViewPreset::Front: + transitionAzimuth = 0.f; + transitionElevation = 0.f; + break; + case ViewPreset::Back: + transitionAzimuth = 180.f; + transitionElevation = 0.f; + break; + case ViewPreset::Left: + transitionAzimuth = 90.f; + transitionElevation = 0.f; + break; + case ViewPreset::Right: + transitionAzimuth = -90.f; + transitionElevation = 0.f; + break; + // +-89, not +-90: exactly 90 sits right on update()'s own elevation + // clamp boundary -- landing a hair inside it means a manual drag right + // after the transition completes doesn't immediately re-clamp/jump. + case ViewPreset::Top: + transitionAzimuth = azimuth; + transitionElevation = 89.f; + break; + case ViewPreset::Bottom: + transitionAzimuth = azimuth; + transitionElevation = -89.f; + break; + // Matches multi_view_tls_registration's CAMERA_ISO angle (35.264 deg + // -- true isometric) for consistency between the two apps. + case ViewPreset::Iso: + transitionAzimuth = 45.f; + transitionElevation = 35.264f; + break; + case ViewPreset::Reset: + transitionAzimuth = 30.f; + transitionElevation = 25.f; + transitionDistance = 30.f; + transitionTarget = { 0.f, 0.f, 0.f }; + transitionSphericalOrthoHeight = 20.f; + break; + } + + transitionActive = true; + } + + bool OrbitCamera::pickGroundPlaneTarget(Vector2 mouse, Camera3D cam, float groundY) + { + Ray ray = GetScreenToWorldRay(mouse, cam); + + const float kTolerance = 0.0001f; + if (ray.direction.y > -kTolerance && ray.direction.y < kTolerance) + return false; // ray ~parallel to the ground plane + + float t = (groundY - ray.position.y) / ray.direction.y; + Vector3 hit = Vector3Add(ray.position, Vector3Scale(ray.direction, t)); + moveTargetTo(hit); + return true; + } + + // --------------------------------------------------------------------- + // Euler-angle + orthographic mode -- see OrbitCamera.h's comment on the + // `euler`/`isOrtho`/... fields for what this ports from. + // --------------------------------------------------------------------- + + void OrbitCamera::applyPerspectiveProjection(int w, int h) const + { + // GetRenderWidth/Height(), not w/h: the GL viewport must be sized in + // actual framebuffer pixels, which can differ from logical w/h under + // DPI scaling -- matches the original reshape(). + rlViewport(0, 0, GetRenderWidth(), GetRenderHeight()); + rlMatrixMode(RL_PROJECTION); + rlLoadIdentity(); + + const double fovy = 60.0; + const double aspect = (double)w / (double)h; + const double nearP = 0.01, farP = 10000.0; + const double top = nearP * tan(fovy * 0.5 * (double)DEG2RAD); + const double bottom = -top; + const double right = top * aspect; + const double left = -right; + rlFrustum(left, right, bottom, top, nearP, farP); + + rlMatrixMode(RL_MODELVIEW); + rlLoadIdentity(); + } + + void OrbitCamera::applyOrthoProjection(float aspect) const + { + rlViewport(0, 0, GetRenderWidth(), GetRenderHeight()); + rlMatrixMode(RL_PROJECTION); + rlLoadIdentity(); + rlOrtho(-orthoZoom, orthoZoom, -orthoZoom / aspect, orthoZoom / aspect, -100000, 100000); + rlMatrixMode(RL_MODELVIEW); + rlLoadIdentity(); + } + + void OrbitCamera::updateOrtho(float aspect) + { + // Real rlgl projection (right-handed, [-1,1] depth) -- what the scene + // actually renders through. + rlOrtho(-orthoZoom, orthoZoom, -orthoZoom / aspect, orthoZoom / aspect, -100000, 100000); + + // Second, independent projection matrix for ImGuizmo only -- ported + // from the original's glm::orthoLH_ZO(-zoom, zoom, -zoom/aspect, + // zoom/aspect, -100, 100) (left-handed, [0,1] depth -- distinct from + // rlOrtho's own convention above). Written directly as flat + // column-major floats (index = col*4+row) rather than through raylib's + // Matrix type: Matrix's m0..m15 field *names* follow that same + // indexing, but its C++ declaration order groups them by row, so a raw + // `&matrix.m0` reinterpret-as-float* would silently scramble this. + for (float& f : orthoProjection) + f = 0.f; + orthoProjection[0] = 1.f / orthoZoom; + orthoProjection[5] = aspect / orthoZoom; + orthoProjection[10] = 1.f / 200.f; // 1/(zFar - zNear), zNear=-100, zFar=100 + orthoProjection[14] = 0.5f; // -zNear/(zFar - zNear) + orthoProjection[15] = 1.f; + + // Ortho "camera" for the gizmo view: looks straight down at + // (orthoShiftX, orthoShiftY, orthoZCenterH) from 10 units above, with + // "up" rotated to match the current in-plane rotation + // (rotateX + rotateY, same yaw the perspective mode's rlMultMatrixf + // view applies) -- ported from the original's TaitBryan-yaw-only + // rotation of (0,1,0), which reduces to this plain 2D rotation. + Vector3 eye = { -orthoShiftX, orthoShiftY, orthoZCenterH + 10.f }; + Vector3 center = { -orthoShiftX, orthoShiftY, orthoZCenterH }; + float ka = -(euler.rotateX + euler.rotateY) * DEG2RAD; + Vector3 up = { -std::sin(ka), std::cos(ka), 0.f }; + + Matrix lookat = MatrixLookAt(eye, center, up); + float16 flat = MatrixToFloatV(lookat); + for (int i = 0; i < 16; ++i) + orthoGizmoView[i] = flat.v[i]; + + // Folds the lookAt into rlgl's *projection* stack (not modelview) -- + // matches the original's "was glOrtho + gluLookAt folded into + // GL_PROJECTION" comment: this app drives its ortho camera entirely + // through the projection matrix, leaving modelview identity (see the + // rlMatrixMode(RL_MODELVIEW) below). MatrixToFloat(), not `&lookat.m0` + // -- see this function's comment above on why the latter is unsafe. + rlMultMatrixf(MatrixToFloat(lookat)); + + rlMatrixMode(RL_MODELVIEW); + rlLoadIdentity(); + } + + void OrbitCamera::captureFrameMatrices() + { + frameView3D = rlGetMatrixModelview(); + frameProj3D = rlGetMatrixProjection(); + } + + Ray OrbitCamera::eulerScreenRay(int x, int y, int screenW, int screenH) const + { + float ndcX = (2.0f * (float)x) / (float)screenW - 1.0f; + float ndcY = 1.0f - (2.0f * (float)y) / (float)screenH; + + // Far point uses NDC z=1 (the actual far plane) rather than an + // out-of-range hack -- only direction, not magnitude, matters to + // callers, and this matches raylib's own GetScreenToWorldRayEx. + Vector3 nearPoint = Vector3Unproject(Vector3{ ndcX, ndcY, 0.0f }, frameProj3D, frameView3D); + Vector3 farPoint = Vector3Unproject(Vector3{ ndcX, ndcY, 1.0f }, frameProj3D, frameView3D); + + Ray ray; + ray.position = nearPoint; + ray.direction = Vector3Subtract(farPoint, nearPoint); + return ray; } -} - -void OrbitCamera::cancelTransition() { - if (!transitionActive) return; - target = transitionTarget; - azimuth = transitionAzimuth; - elevation = transitionElevation; - distance = transitionDistance; - sphericalOrthoHeight = transitionSphericalOrthoHeight; - transitionActive = false; -} - -void OrbitCamera::setPreset(ViewPreset preset) { - // Only Front/Back/Left/Right/Top/Bottom/Iso change azimuth/elevation; - // Reset changes target/distance/sphericalOrthoHeight too -- see this - // method's declaration comment. - transitionTarget = target; - transitionDistance = distance; - transitionSphericalOrthoHeight = sphericalOrthoHeight; - - switch (preset) { - case ViewPreset::Front: transitionAzimuth = 0.f; transitionElevation = 0.f; break; - case ViewPreset::Back: transitionAzimuth = 180.f; transitionElevation = 0.f; break; - case ViewPreset::Left: transitionAzimuth = 90.f; transitionElevation = 0.f; break; - case ViewPreset::Right: transitionAzimuth = -90.f; transitionElevation = 0.f; break; - // +-89, not +-90: exactly 90 sits right on update()'s own elevation - // clamp boundary -- landing a hair inside it means a manual drag right - // after the transition completes doesn't immediately re-clamp/jump. - case ViewPreset::Top: transitionAzimuth = azimuth; transitionElevation = 89.f; break; - case ViewPreset::Bottom: transitionAzimuth = azimuth; transitionElevation = -89.f; break; - // Matches multi_view_tls_registration's CAMERA_ISO angle (35.264 deg - // -- true isometric) for consistency between the two apps. - case ViewPreset::Iso: transitionAzimuth = 45.f; transitionElevation = 35.264f; break; - case ViewPreset::Reset: - transitionAzimuth = 30.f; - transitionElevation = 25.f; - transitionDistance = 30.f; - transitionTarget = { 0.f, 0.f, 0.f }; - transitionSphericalOrthoHeight = 20.f; - break; + + void OrbitCamera::dragOrbit(float dx, float dy) + { + euler.rotateX += dy * 0.2f; + euler.rotateY += dx * 0.2f; + breakEulerTransition(); + } + + void OrbitCamera::dragPanPerspective(float dx, float dy) + { + euler.translate.x += dx * 0.1f * eulerMouseSensitivity; + euler.translate.y -= dy * 0.1f * eulerMouseSensitivity; + breakEulerTransition(); } - transitionActive = true; -} - -bool OrbitCamera::pickGroundPlaneTarget(Vector2 mouse, Camera3D cam, float groundY) { - Ray ray = GetScreenToWorldRay(mouse, cam); - - const float kTolerance = 0.0001f; - if (ray.direction.y > -kTolerance && ray.direction.y < kTolerance) - return false; // ray ~parallel to the ground plane - - float t = (groundY - ray.position.y) / ray.direction.y; - Vector3 hit = Vector3Add(ray.position, Vector3Scale(ray.direction, t)); - moveTargetTo(hit); - return true; -} - -// --------------------------------------------------------------------- -// Euler-angle + orthographic mode -- see OrbitCamera.h's comment on the -// `euler`/`isOrtho`/... fields for what this ports from. -// --------------------------------------------------------------------- - -void OrbitCamera::applyPerspectiveProjection(int w, int h) const { - // GetRenderWidth/Height(), not w/h: the GL viewport must be sized in - // actual framebuffer pixels, which can differ from logical w/h under - // DPI scaling -- matches the original reshape(). - rlViewport(0, 0, GetRenderWidth(), GetRenderHeight()); - rlMatrixMode(RL_PROJECTION); - rlLoadIdentity(); - - const double fovy = 60.0; - const double aspect = (double)w / (double)h; - const double nearP = 0.01, farP = 10000.0; - const double top = nearP * tan(fovy * 0.5 * (double)DEG2RAD); - const double bottom = -top; - const double right = top * aspect; - const double left = -right; - rlFrustum(left, right, bottom, top, nearP, farP); - - rlMatrixMode(RL_MODELVIEW); - rlLoadIdentity(); -} - -void OrbitCamera::applyOrthoProjection(float aspect) const { - rlViewport(0, 0, GetRenderWidth(), GetRenderHeight()); - rlMatrixMode(RL_PROJECTION); - rlLoadIdentity(); - rlOrtho(-orthoZoom, orthoZoom, -orthoZoom / aspect, orthoZoom / aspect, -100000, 100000); - rlMatrixMode(RL_MODELVIEW); - rlLoadIdentity(); -} - -void OrbitCamera::updateOrtho(float aspect) { - // Real rlgl projection (right-handed, [-1,1] depth) -- what the scene - // actually renders through. - rlOrtho(-orthoZoom, orthoZoom, -orthoZoom / aspect, orthoZoom / aspect, -100000, 100000); - - // Second, independent projection matrix for ImGuizmo only -- ported - // from the original's glm::orthoLH_ZO(-zoom, zoom, -zoom/aspect, - // zoom/aspect, -100, 100) (left-handed, [0,1] depth -- distinct from - // rlOrtho's own convention above). Written directly as flat - // column-major floats (index = col*4+row) rather than through raylib's - // Matrix type: Matrix's m0..m15 field *names* follow that same - // indexing, but its C++ declaration order groups them by row, so a raw - // `&matrix.m0` reinterpret-as-float* would silently scramble this. - for (float& f : orthoProjection) f = 0.f; - orthoProjection[0] = 1.f / orthoZoom; - orthoProjection[5] = aspect / orthoZoom; - orthoProjection[10] = 1.f / 200.f; // 1/(zFar - zNear), zNear=-100, zFar=100 - orthoProjection[14] = 0.5f; // -zNear/(zFar - zNear) - orthoProjection[15] = 1.f; - - // Ortho "camera" for the gizmo view: looks straight down at - // (orthoShiftX, orthoShiftY, orthoZCenterH) from 10 units above, with - // "up" rotated to match the current in-plane rotation - // (rotateX + rotateY, same yaw the perspective mode's rlMultMatrixf - // view applies) -- ported from the original's TaitBryan-yaw-only - // rotation of (0,1,0), which reduces to this plain 2D rotation. - Vector3 eye = { -orthoShiftX, orthoShiftY, orthoZCenterH + 10.f }; - Vector3 center = { -orthoShiftX, orthoShiftY, orthoZCenterH }; - float ka = -(euler.rotateX + euler.rotateY) * DEG2RAD; - Vector3 up = { -std::sin(ka), std::cos(ka), 0.f }; - - Matrix lookat = MatrixLookAt(eye, center, up); - float16 flat = MatrixToFloatV(lookat); - for (int i = 0; i < 16; ++i) orthoGizmoView[i] = flat.v[i]; - - // Folds the lookAt into rlgl's *projection* stack (not modelview) -- - // matches the original's "was glOrtho + gluLookAt folded into - // GL_PROJECTION" comment: this app drives its ortho camera entirely - // through the projection matrix, leaving modelview identity (see the - // rlMatrixMode(RL_MODELVIEW) below). MatrixToFloat(), not `&lookat.m0` - // -- see this function's comment above on why the latter is unsafe. - rlMultMatrixf(MatrixToFloat(lookat)); - - rlMatrixMode(RL_MODELVIEW); - rlLoadIdentity(); -} - -void OrbitCamera::captureFrameMatrices() { - frameView3D = rlGetMatrixModelview(); - frameProj3D = rlGetMatrixProjection(); -} - -Ray OrbitCamera::eulerScreenRay(int x, int y, int screenW, int screenH) const { - float ndcX = (2.0f * (float)x) / (float)screenW - 1.0f; - float ndcY = 1.0f - (2.0f * (float)y) / (float)screenH; - - // Far point uses NDC z=1 (the actual far plane) rather than an - // out-of-range hack -- only direction, not magnitude, matters to - // callers, and this matches raylib's own GetScreenToWorldRayEx. - Vector3 nearPoint = Vector3Unproject(Vector3{ ndcX, ndcY, 0.0f }, frameProj3D, frameView3D); - Vector3 farPoint = Vector3Unproject(Vector3{ ndcX, ndcY, 1.0f }, frameProj3D, frameView3D); - - Ray ray; - ray.position = nearPoint; - ray.direction = Vector3Subtract(farPoint, nearPoint); - return ray; -} - -void OrbitCamera::dragOrbit(float dx, float dy) { - euler.rotateX += dy * 0.2f; - euler.rotateY += dx * 0.2f; - breakEulerTransition(); -} - -void OrbitCamera::dragPanPerspective(float dx, float dy) { - euler.translate.x += dx * 0.1f * eulerMouseSensitivity; - euler.translate.y -= dy * 0.1f * eulerMouseSensitivity; - breakEulerTransition(); -} - -void OrbitCamera::dragPanOrtho(float dx, float dy, float displayW, float displayH) { - if (displayW <= 0.f || displayH <= 0.f) return; - - float ratio = displayW / displayH; - float vx = dx * (orthoZoom / displayW * 2.f); - float vy = dy * (orthoZoom / displayH * 2.f / ratio); - - // Rotate the pan vector by the current in-plane rotation (rotateX + - // rotateY), matching the original's TaitBryan-yaw-only rotation - // (opposite sign convention from updateOrtho()'s gizmo-up rotation -- - // this is a different vector, ported independently from the same - // original source). - float ka = (euler.rotateX + euler.rotateY) * DEG2RAD; - float ck = std::cos(ka), sk = std::sin(ka); - - orthoShiftX += ck * vx - sk * vy; - orthoShiftY += sk * vx + ck * vy; - // Deliberately no breakEulerTransition() call here -- matches the - // original, which didn't break the transition on ortho pan either. -} - -void OrbitCamera::zoom(float wheelDelta, bool shiftHeld) { - if (wheelDelta == 0.f) return; - - // wheelDelta=+-1 (a real mouse wheel's per-notch magnitude) reproduces - // the original's fixed per-notch step exactly; any other magnitude - // (trackpad smooth-scroll) scales it proportionally -- see this - // function's declaration comment. - if (isOrtho) { - orthoZoom -= 0.1f * orthoZoom * wheelDelta; - if (orthoZoom < 0.1f) orthoZoom = 0.1f; - } else { - float step = shiftHeld ? 5.f : 1.f; - euler.translate.z += step * wheelDelta; + void OrbitCamera::dragPanOrtho(float dx, float dy, float displayW, float displayH) + { + if (displayW <= 0.f || displayH <= 0.f) + return; + + float ratio = displayW / displayH; + float vx = dx * (orthoZoom / displayW * 2.f); + float vy = dy * (orthoZoom / displayH * 2.f / ratio); + + // Rotate the pan vector by the current in-plane rotation (rotateX + + // rotateY), matching the original's TaitBryan-yaw-only rotation + // (opposite sign convention from updateOrtho()'s gizmo-up rotation -- + // this is a different vector, ported independently from the same + // original source). + float ka = (euler.rotateX + euler.rotateY) * DEG2RAD; + float ck = std::cos(ka), sk = std::sin(ka); + + orthoShiftX += ck * vx - sk * vy; + orthoShiftY += sk * vx + ck * vy; + // Deliberately no breakEulerTransition() call here -- matches the + // original, which didn't break the transition on ortho pan either. } - eulerMouseSensitivity = std::fabs(euler.translate.z) / 100.f; - // Not breakEulerTransition(): matches the original, which just cleared - // camera_transition_active here without snapping rotation_center. - eulerTransitionActive = false; -} - -void OrbitCamera::setEulerPreset(EulerPreset preset) { - bool triggered = false; - - switch (preset) { - case EulerPreset::Front: - eulerGoal.rotateX = -90.0f; - eulerGoal.rotateY = +90.0f; - triggered = true; - break; - case EulerPreset::Back: - eulerGoal.rotateX = -90.0f; - eulerGoal.rotateY = -90.0f; - triggered = true; - break; - case EulerPreset::Left: - eulerGoal.rotateX = -90.0f; - eulerGoal.rotateY = 180.0f; - triggered = true; - break; - case EulerPreset::Right: - eulerGoal.rotateX = -90.0f; - eulerGoal.rotateY = 0.0f; - triggered = true; - break; - case EulerPreset::Top: - eulerGoal.rotateX = 0.0f; - eulerGoal.rotateY = 90.0f; - triggered = true; - break; - case EulerPreset::Bottom: - eulerGoal.rotateX = 180.0f; - eulerGoal.rotateY = -90.0f; - triggered = true; - break; - case EulerPreset::Iso: - eulerGoal.rotateX = -35.264f; - eulerGoal.rotateY = 135.0f; - triggered = true; - break; - case EulerPreset::Reset: - eulerGoal.rotationCenter = { 0.f, 0.f, 0.f }; - eulerGoal.rotateX = 0.f; - eulerGoal.rotateY = 0.f; - eulerGoal.translate = { 0.f, 0.f, -50.0f }; + void OrbitCamera::zoom(float wheelDelta, bool shiftHeld) + { + if (wheelDelta == 0.f) + return; + + // wheelDelta=+-1 (a real mouse wheel's per-notch magnitude) reproduces + // the original's fixed per-notch step exactly; any other magnitude + // (trackpad smooth-scroll) scales it proportionally -- see this + // function's declaration comment. + if (isOrtho) + { + orthoZoom -= 0.1f * orthoZoom * wheelDelta; + if (orthoZoom < 0.1f) + orthoZoom = 0.1f; + } + else + { + float step = shiftHeld ? 5.f : 1.f; + euler.translate.z += step * wheelDelta; + } + eulerMouseSensitivity = std::fabs(euler.translate.z) / 100.f; + // Not breakEulerTransition(): matches the original, which just cleared + // camera_transition_active here without snapping rotation_center. + eulerTransitionActive = false; + } + + void OrbitCamera::setEulerPreset(EulerPreset preset) + { + bool triggered = false; + + switch (preset) + { + case EulerPreset::Front: + eulerGoal.rotateX = -90.0f; + eulerGoal.rotateY = +90.0f; + triggered = true; + break; + case EulerPreset::Back: + eulerGoal.rotateX = -90.0f; + eulerGoal.rotateY = -90.0f; + triggered = true; + break; + case EulerPreset::Left: + eulerGoal.rotateX = -90.0f; + eulerGoal.rotateY = 180.0f; + triggered = true; + break; + case EulerPreset::Right: + eulerGoal.rotateX = -90.0f; + eulerGoal.rotateY = 0.0f; + triggered = true; + break; + case EulerPreset::Top: + eulerGoal.rotateX = 0.0f; + eulerGoal.rotateY = 90.0f; + triggered = true; + break; + case EulerPreset::Bottom: + eulerGoal.rotateX = 180.0f; + eulerGoal.rotateY = -90.0f; + triggered = true; + break; + case EulerPreset::Iso: + eulerGoal.rotateX = -35.264f; + eulerGoal.rotateY = 135.0f; + triggered = true; + break; + case EulerPreset::Reset: + eulerGoal.rotationCenter = { 0.f, 0.f, 0.f }; + eulerGoal.rotateX = 0.f; + eulerGoal.rotateY = 0.f; + eulerGoal.translate = { 0.f, 0.f, -50.0f }; + eulerMouseSensitivity = std::fabs(euler.translate.z) / 100.f; + + orthoZoom = 10.f; + orthoShiftX = 0.f; + orthoShiftY = 0.f; + orthoZCenterH = 0.f; + + triggered = false; + break; + } - orthoZoom = 10.f; - orthoShiftX = 0.f; - orthoShiftY = 0.f; - orthoZCenterH = 0.f; + if (triggered) + { + eulerGoal.rotationCenter = euler.rotationCenter; + eulerGoal.translate = euler.translate; + } + + eulerTransitionActive = true; + } + + void OrbitCamera::startEulerTransition(float rotateX, float rotateY, Vector3 translate, Vector3 rotationCenter) + { + eulerGoal.rotateX = rotateX; + eulerGoal.rotateY = rotateY; + eulerGoal.translate = translate; + eulerGoal.rotationCenter = rotationCenter; + eulerTransitionActive = true; + } + + void OrbitCamera::moveEulerRotationCenterTo(Vector3 center) + { + startEulerTransition(euler.rotateX, euler.rotateY, Vector3{ -center.x, -center.y, euler.translate.z }, center); + } + + void OrbitCamera::setViewPose(Eigen::Affine3f view) + { + // Columns are the OpenGL camera axes in LiDAR coordinates: right = -y, + // up = +z, and +z (backward) = -x, so the camera looks along LiDAR +x. + Eigen::Matrix3f lidarFromGl; + lidarFromGl << 0.f, 0.f, -1.f, -1.f, 0.f, 0.f, 0.f, 1.f, 0.f; + view.linear() = view.linear() * lidarFromGl; + viewPose = view; + } - triggered = false; - break; + void OrbitCamera::updateEulerTransition(float dt) + { + if (!eulerTransitionActive) + return; + + float t = 1.0f - std::pow(1.0f - std::min(dt * eulerTransitionSpeed, 1.0f), 3.0f); + + auto ease = [t](float& cur, float goal) -> bool + { + if (std::fabs(goal - cur) < 0.01f) + return true; + cur += (goal - cur) * t; + return false; + }; + + bool doneRcX = ease(euler.rotationCenter.x, eulerGoal.rotationCenter.x); + bool doneRcY = ease(euler.rotationCenter.y, eulerGoal.rotationCenter.y); + bool doneRcZ = ease(euler.rotationCenter.z, eulerGoal.rotationCenter.z); + bool doneRx = ease(euler.rotateX, eulerGoal.rotateX); + bool doneRy = ease(euler.rotateY, eulerGoal.rotateY); + bool doneTx = ease(euler.translate.x, eulerGoal.translate.x); + bool doneTy = ease(euler.translate.y, eulerGoal.translate.y); + bool doneTz = ease(euler.translate.z, eulerGoal.translate.z); + + eulerTransitionActive = !(doneRcX && doneRcY && doneRcZ && doneRx && doneRy && doneTx && doneTy && doneTz); + + if (!eulerTransitionActive) + euler = eulerGoal; } - if (triggered) { - eulerGoal.rotationCenter = euler.rotationCenter; - eulerGoal.translate = euler.translate; + void OrbitCamera::breakEulerTransition() + { + if (!eulerTransitionActive) + return; + euler.rotationCenter = eulerGoal.rotationCenter; + eulerTransitionActive = false; } - eulerTransitionActive = true; -} - -void OrbitCamera::startEulerTransition(float rotateX, float rotateY, Vector3 translate, Vector3 rotationCenter) { - eulerGoal.rotateX = rotateX; - eulerGoal.rotateY = rotateY; - eulerGoal.translate = translate; - eulerGoal.rotationCenter = rotationCenter; - eulerTransitionActive = true; -} - -void OrbitCamera::moveEulerRotationCenterTo(Vector3 center) { - startEulerTransition(euler.rotateX, euler.rotateY, Vector3{ -center.x, -center.y, euler.translate.z }, center); -} - -void OrbitCamera::setViewPose(Eigen::Affine3f view) { - // Columns are the OpenGL camera axes in LiDAR coordinates: right = -y, - // up = +z, and +z (backward) = -x, so the camera looks along LiDAR +x. - Eigen::Matrix3f lidarFromGl; - lidarFromGl << 0.f, 0.f, -1.f, - -1.f, 0.f, 0.f, - 0.f, 1.f, 0.f; - view.linear() = view.linear() * lidarFromGl; - viewPose = view; -} - -void OrbitCamera::updateEulerTransition(float dt) { - if (!eulerTransitionActive) return; - - float t = 1.0f - std::pow(1.0f - std::min(dt * eulerTransitionSpeed, 1.0f), 3.0f); - - auto ease = [t](float& cur, float goal) -> bool { - if (std::fabs(goal - cur) < 0.01f) return true; - cur += (goal - cur) * t; - return false; - }; - - bool doneRcX = ease(euler.rotationCenter.x, eulerGoal.rotationCenter.x); - bool doneRcY = ease(euler.rotationCenter.y, eulerGoal.rotationCenter.y); - bool doneRcZ = ease(euler.rotationCenter.z, eulerGoal.rotationCenter.z); - bool doneRx = ease(euler.rotateX, eulerGoal.rotateX); - bool doneRy = ease(euler.rotateY, eulerGoal.rotateY); - bool doneTx = ease(euler.translate.x, eulerGoal.translate.x); - bool doneTy = ease(euler.translate.y, eulerGoal.translate.y); - bool doneTz = ease(euler.translate.z, eulerGoal.translate.z); - - eulerTransitionActive = !(doneRcX && doneRcY && doneRcZ && doneRx && doneRy && doneTx && doneTy && doneTz); - - if (!eulerTransitionActive) euler = eulerGoal; -} - -void OrbitCamera::breakEulerTransition() { - if (!eulerTransitionActive) return; - euler.rotationCenter = eulerGoal.rotationCenter; - eulerTransitionActive = false; -} - -void drawRotationCenterCross(Vector3 center, float size, Color color) { - DrawLine3D(Vector3{ center.x - size, center.y, center.z }, Vector3{ center.x + size, center.y, center.z }, color); - DrawLine3D(Vector3{ center.x, center.y - size, center.z }, Vector3{ center.x, center.y + size, center.z }, color); - DrawLine3D(Vector3{ center.x, center.y, center.z - size }, Vector3{ center.x, center.y, center.z + size }, color); -} - -void end3DMatrixStack(float displayW, float displayH) { - rlDrawRenderBatchActive(); - rlMatrixMode(RL_PROJECTION); - rlLoadIdentity(); - rlOrtho(0, displayW, displayH, 0, 0.0f, 1.0f); - rlMatrixMode(RL_MODELVIEW); - rlLoadIdentity(); - rlDisableDepthTest(); -} + void drawRotationCenterCross(Vector3 center, float size, Color color) + { + DrawLine3D(Vector3{ center.x - size, center.y, center.z }, Vector3{ center.x + size, center.y, center.z }, color); + DrawLine3D(Vector3{ center.x, center.y - size, center.z }, Vector3{ center.x, center.y + size, center.z }, color); + DrawLine3D(Vector3{ center.x, center.y, center.z - size }, Vector3{ center.x, center.y, center.z + size }, color); + } + + void end3DMatrixStack(float displayW, float displayH) + { + rlDrawRenderBatchActive(); + rlMatrixMode(RL_PROJECTION); + rlLoadIdentity(); + rlOrtho(0, displayW, displayH, 0, 0.0f, 1.0f); + rlMatrixMode(RL_MODELVIEW); + rlLoadIdentity(); + rlDisableDepthTest(); + } } // namespace raylib_widgets diff --git a/raylib_widgets/src/PointPicking.cpp b/raylib_widgets/src/PointPicking.cpp index 30327a5a..89934ae0 100644 --- a/raylib_widgets/src/PointPicking.cpp +++ b/raylib_widgets/src/PointPicking.cpp @@ -6,7 +6,12 @@ namespace raylib_widgets { bool pickNearestPoint( - const Vector3* points, size_t count, const Ray& ray, float cameraFovYDeg, float viewportHeightPx, float pixelThreshold, + const Vector3* points, + size_t count, + const Ray& ray, + float cameraFovYDeg, + float viewportHeightPx, + float pixelThreshold, size_t& outIndex) { if (points == nullptr || count == 0 || viewportHeightPx <= 0.f) diff --git a/raylib_widgets/src/RayPlaneD.cpp b/raylib_widgets/src/RayPlaneD.cpp index 1e783862..9075f1d5 100644 --- a/raylib_widgets/src/RayPlaneD.cpp +++ b/raylib_widgets/src/RayPlaneD.cpp @@ -1,25 +1,26 @@ #include "RaylibWidgets/RayPlaneD.h" -namespace raylib_widgets { - -bool intersectPlane( - const Eigen::Vector3d& rayPos, const Eigen::Vector3d& rayDir, double a, double b, double c, double d, Eigen::Vector3d& outPoint) +namespace raylib_widgets { - const double kTolerance = 0.0001; - double denom = a * rayDir.x() + b * rayDir.y() + c * rayDir.z(); - if (denom > -kTolerance && denom < kTolerance) - return false; + bool intersectPlane( + const Eigen::Vector3d& rayPos, const Eigen::Vector3d& rayDir, double a, double b, double c, double d, Eigen::Vector3d& outPoint) + { + const double kTolerance = 0.0001; - double distFromPlane = a * rayPos.x() + b * rayPos.y() + c * rayPos.z() + d; - outPoint = rayPos - rayDir * (distFromPlane / denom); - return true; -} + double denom = a * rayDir.x() + b * rayDir.y() + c * rayDir.z(); + if (denom > -kTolerance && denom < kTolerance) + return false; -double distancePointToLine(const Eigen::Vector3d& point, const Eigen::Vector3d& rayPos, const Eigen::Vector3d& rayDir) -{ - Eigen::Vector3d AP = point - rayPos; - return (AP.cross(rayDir)).norm(); -} + double distFromPlane = a * rayPos.x() + b * rayPos.y() + c * rayPos.z() + d; + outPoint = rayPos - rayDir * (distFromPlane / denom); + return true; + } + + double distancePointToLine(const Eigen::Vector3d& point, const Eigen::Vector3d& rayPos, const Eigen::Vector3d& rayDir) + { + Eigen::Vector3d AP = point - rayPos; + return (AP.cross(rayDir)).norm(); + } } // namespace raylib_widgets diff --git a/raylib_widgets/src/ShortcutsTable.cpp b/raylib_widgets/src/ShortcutsTable.cpp index 55717905..85cc5b8c 100644 --- a/raylib_widgets/src/ShortcutsTable.cpp +++ b/raylib_widgets/src/ShortcutsTable.cpp @@ -3,44 +3,45 @@ #include -namespace raylib_widgets { - -void ShowShortcutsTable(const std::vector& entries) +namespace raylib_widgets { - if (!ImGui::BeginTable( - "ShortcutsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, ImVec2(-FLT_MIN, 200))) - return; - - ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("Shortcut", ImGuiTableColumnFlags_WidthFixed, 120); - ImGui::TableSetupColumn("Description"); - ImGui::TableHeadersRow(); - std::string lastType; - for (const auto& e : entries) + void ShowShortcutsTable(const std::vector& entries) { - if (!e.type.empty() && e.type != lastType) - { - lastType = e.type; - ImGui::TableNextRow(); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, IM_COL32(70, 70, 140, 255)); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "%s", lastType.c_str()); - ImGui::TableSetColumnIndex(1); - ImGui::TextUnformatted(""); - } + if (!ImGui::BeginTable( + "ShortcutsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, ImVec2(-FLT_MIN, 200))) + return; - if (!e.description.empty()) + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Shortcut", ImGuiTableColumnFlags_WidthFixed, 120); + ImGui::TableSetupColumn("Description"); + ImGui::TableHeadersRow(); + + std::string lastType; + for (const auto& e : entries) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(e.shortcut.c_str()); - ImGui::TableSetColumnIndex(1); - ImGui::TextUnformatted(e.description.c_str()); + if (!e.type.empty() && e.type != lastType) + { + lastType = e.type; + ImGui::TableNextRow(); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, IM_COL32(70, 70, 140, 255)); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "%s", lastType.c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(""); + } + + if (!e.description.empty()) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(e.shortcut.c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(e.description.c_str()); + } } - } - ImGui::EndTable(); -} + ImGui::EndTable(); + } } // namespace raylib_widgets diff --git a/raylib_widgets/src/WindowFit.cpp b/raylib_widgets/src/WindowFit.cpp index fcd353f5..1a798324 100644 --- a/raylib_widgets/src/WindowFit.cpp +++ b/raylib_widgets/src/WindowFit.cpp @@ -1,46 +1,47 @@ -#include #include "raylib.h" +#include #include -namespace raylib_widgets { - -void fitWindowToScreen(int marginW, int marginH, bool centerVertically) +namespace raylib_widgets { - int monitor = GetCurrentMonitor(); - // GetMonitorWidth/Height return the monitor's native PIXEL resolution - // (GLFW's glfwGetVideoMode), while GetScreenWidth/Height, SetWindowSize - // and SetWindowPosition all operate in logical points -- on a 2x Retina - // display that's a 2x unit mismatch. Divide by the DPI scale to bring - // the monitor size into the same points space everything else uses; - // without this, SetWindowPosition computes an X centered on a monitor - // twice too wide, pushing most of the window off the right edge of the - // actual (points-sized) screen. - Vector2 dpi = GetWindowScaleDPI(); - if (dpi.x <= 0.f) - dpi.x = 1.f; - if (dpi.y <= 0.f) - dpi.y = 1.f; - int monW = (int)(GetMonitorWidth(monitor) / dpi.x); - int monH = (int)(GetMonitorHeight(monitor) / dpi.y); - if (monW <= 0 || monH <= 0) - return; // monitor info unavailable, leave as-is - int w = std::min(GetScreenWidth(), monW - marginW); - int h = std::min(GetScreenHeight(), monH - marginH); - if (w != GetScreenWidth() || h != GetScreenHeight()) - SetWindowSize(w, h); + void fitWindowToScreen(int marginW, int marginH, bool centerVertically) + { + int monitor = GetCurrentMonitor(); + // GetMonitorWidth/Height return the monitor's native PIXEL resolution + // (GLFW's glfwGetVideoMode), while GetScreenWidth/Height, SetWindowSize + // and SetWindowPosition all operate in logical points -- on a 2x Retina + // display that's a 2x unit mismatch. Divide by the DPI scale to bring + // the monitor size into the same points space everything else uses; + // without this, SetWindowPosition computes an X centered on a monitor + // twice too wide, pushing most of the window off the right edge of the + // actual (points-sized) screen. + Vector2 dpi = GetWindowScaleDPI(); + if (dpi.x <= 0.f) + dpi.x = 1.f; + if (dpi.y <= 0.f) + dpi.y = 1.f; + int monW = (int)(GetMonitorWidth(monitor) / dpi.x); + int monH = (int)(GetMonitorHeight(monitor) / dpi.y); + if (monW <= 0 || monH <= 0) + return; // monitor info unavailable, leave as-is + + int w = std::min(GetScreenWidth(), monW - marginW); + int h = std::min(GetScreenHeight(), monH - marginH); + if (w != GetScreenWidth() || h != GetScreenHeight()) + SetWindowSize(w, h); - int posY = centerVertically ? std::max(0, (monH - h) / 2) : 30; - SetWindowPosition(std::max(0, (monW - w) / 2), posY); + int posY = centerVertically ? std::max(0, (monH - h) / 2) : 30; + SetWindowPosition(std::max(0, (monW - w) / 2), posY); - // SetWindowSize/SetWindowPosition only update GLFW's window state; raylib's - // cached mouse/window geometry (what rlImGui reads into io.MousePos every - // frame) isn't refreshed until the next PollInputEvents(), which otherwise - // wouldn't happen until the first EndDrawing() -- after rlImGuiSetup() has - // already run. Without this, every click lands offset from the cursor by - // however far this function just moved/resized the window. - PollInputEvents(); -} + // SetWindowSize/SetWindowPosition only update GLFW's window state; raylib's + // cached mouse/window geometry (what rlImGui reads into io.MousePos every + // frame) isn't refreshed until the next PollInputEvents(), which otherwise + // wouldn't happen until the first EndDrawing() -- after rlImGuiSetup() has + // already run. Without this, every click lands offset from the cursor by + // however far this function just moved/resized the window. + PollInputEvents(); + } } // namespace raylib_widgets From 65a30a01347388a581fc51c1f0cf2279627762ec Mon Sep 17 00:00:00 2001 From: Sagi Amangeldi Date: Sat, 24 Oct 2026 22:02:03 +0900 Subject: [PATCH 4/5] Add raylib_widgets to clang-format scripts --- scripts/README.md | 1 + scripts/run_check_clang_format.py | 1 + scripts/run_clang_format.py | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/README.md b/scripts/README.md index a86cfae0..b407667d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -6,6 +6,7 @@ * `apps` * `pybind` * `shared` + * `raylib_widgets` * `run_check_clang_format.py` — checks whether running `clang-format` would modify any tracked project files. The script runs `run_clang_format.py` and then checks the Git working tree for changes. If formatting would produce changes, the check fails and lists the affected files. diff --git a/scripts/run_check_clang_format.py b/scripts/run_check_clang_format.py index 29906e9c..61170116 100644 --- a/scripts/run_check_clang_format.py +++ b/scripts/run_check_clang_format.py @@ -12,6 +12,7 @@ os.path.join(PROJECT_DIRECTORY, "apps"), os.path.join(PROJECT_DIRECTORY, "pybind"), os.path.join(PROJECT_DIRECTORY, "shared"), + os.path.join(PROJECT_DIRECTORY, "raylib_widgets"), ] FILE_EXTENSIONS = [ diff --git a/scripts/run_clang_format.py b/scripts/run_clang_format.py index 521edb1c..c7293cf8 100644 --- a/scripts/run_clang_format.py +++ b/scripts/run_clang_format.py @@ -9,7 +9,8 @@ FILE_LOCATIONS = [os.path.join(PROJECT_DIRECTORY, 'core'), os.path.join(PROJECT_DIRECTORY, 'apps'), os.path.join(PROJECT_DIRECTORY, 'pybind'), - os.path.join(PROJECT_DIRECTORY, 'shared')] + os.path.join(PROJECT_DIRECTORY, 'shared'), + os.path.join(PROJECT_DIRECTORY, 'raylib_widgets')] FILE_EXTENSIONS = ['.cpp', '.hpp', From c984a6ea35399c371be503db069642e7f84c619b Mon Sep 17 00:00:00 2001 From: Sagi Amangeldi Date: Sat, 24 Oct 2026 22:02:38 +0900 Subject: [PATCH 5/5] Ignore raylib_widgets reformat in git blame --- .git-blame-ignore-revs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 26424cf6..0292508a 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -6,4 +6,7 @@ # GitHub's blame view applies this file automatically. # clang-format mass reformat -7c667fce790efe905c8f825246c8e3ad417d9890 \ No newline at end of file +7c667fce790efe905c8f825246c8e3ad417d9890 + +# clang-format raylib_widgets +03d4c1d35d06d5ea44c831b4efe47998d65ee521