From 98bc4c29c921ca300f91200fab61129e3d400e5e Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 2 Jun 2026 15:37:50 +0100 Subject: [PATCH 001/110] feat: Added Qt6 find_package to GUI cmake * This will be hardcoded UNTIL I have it working as expected, then I will convert it to a cmake preset file! --- DSFE_App/DSFE_GUI/CMakeLists.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 72f4af9a..b2e6300e 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -4,10 +4,21 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) cmake_minimum_required(VERSION 3.10) project(DSFE_GUI LANGUAGES C CXX) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_PREFIX_PATH "C:/Qt/6.11.1/msvc2022_64") + add_library(DSFE_GUI SHARED) find_package(Threads REQUIRED) find_package(OpenGL REQUIRED) +find_package(Qt6 REQUIRED COMPONENTS + Core + Gui + Widgets + OpenGLWidgets +) target_compile_definitions(DSFE_GUI PRIVATE DSFE_GUI_EXPORTS @@ -133,4 +144,8 @@ target_link_libraries(DSFE_GUI glm::glm imgui OpenGL::GL + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::OpenGLWidgets ) \ No newline at end of file From 45120c39249f67098e56f989f4bce81ee95b4ae7 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 2 Jun 2026 15:38:09 +0100 Subject: [PATCH 002/110] feat: Added basic debug test initialisation of qt6 in `Application` --- DSFE_App/DSFE_GUI/src/Application.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/DSFE_App/DSFE_GUI/src/Application.cpp b/DSFE_App/DSFE_GUI/src/Application.cpp index 4e56e905..81fee1ca 100644 --- a/DSFE_App/DSFE_GUI/src/Application.cpp +++ b/DSFE_App/DSFE_GUI/src/Application.cpp @@ -13,10 +13,17 @@ #include "Platform/Paths.h" #include "EngineLib/LogMacros.h" +#include +#include +#include + namespace fs = std::filesystem; // Initialise the static instance pointer to nullptr Application* Application::sInstance = nullptr; +static QApplication* gQtApp = nullptr; +static QMainWindow* gQtWindow = nullptr; + // Constructor: Initialises paths, sets up data manager, and creates the main application window Application::Application(const std::string& appName) { paths::init(); @@ -28,6 +35,7 @@ Application::Application(const std::string& appName) { LOG_INFO("Runs path: %s", paths::runs().string().c_str()); int winW = 1920, winH = 1080; + if (glfwInit()) { const GLFWvidmode* mode = glfwGetVideoMode(glfwGetPrimaryMonitor()); if (mode) { @@ -43,6 +51,18 @@ Application::Application(const std::string& appName) { glfwTerminate(); // OpenGLContext::init will call glfwInit again } + if (!gQtApp) { + QCoreApplication::addLibraryPath("C:/Qt/6.11.1/msvc2022_64/plugins"); + qputenv("QT_DEBUG_PLUGINS", "1"); + int argc = 0; + gQtApp = new QApplication(argc, nullptr); + gQtWindow = new QMainWindow(); + gQtWindow->setWindowTitle(QString::fromStdString(appName)); + gQtWindow->resize(800, 600); + gQtWindow->show(); + LOG_INFO("Qt application and main window created with title '%s'", appName.c_str()); + } + _window = std::make_unique(); _window->init(winW, winH, appName); } @@ -52,6 +72,8 @@ Application::~Application() = default; // Main application loop: Continues running until the window signals to close, updating and rendering each frame void Application::run() { while (_window->isRunning() && !_window->shouldClose()) { + QCoreApplication::processEvents(); + _window->update(); _window->render(); } From 22e3a9728daefe63647112184b9f8ec43350eab4 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 2 Jun 2026 15:55:35 +0100 Subject: [PATCH 003/110] feat: Added `DSFE_MainWindow` header and compiler files, with defined constructor. --- DSFE_App/DSFE_GUI/CMakeLists.txt | 5 +++++ .../DSFE_GUI/include/MainWindow/DSFE_MainWindow.h | 11 +++++++++++ DSFE_App/DSFE_GUI/src/Application.cpp | 1 - DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp | 9 +++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h create mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index b2e6300e..321e911d 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -37,6 +37,10 @@ target_include_directories(stb INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include ) +set(MAIN_WINDOW_SRC + src/MainWindow/DSFE_MainWindow.cpp +) + set(RENDER_SRC src/Rendering/CubeVertices.cpp src/Rendering/GUIContext.cpp @@ -82,6 +86,7 @@ set(ASSETS_SRC ) target_sources(DSFE_GUI PRIVATE + ${MAIN_WINDOW_SRC} ${RENDER_SRC} ${SCENE_SRC} ${UI_SRC} diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h new file mode 100644 index 00000000..5ed46a4c --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h @@ -0,0 +1,11 @@ +// DSFE_GUI DSFE_MainWindow.h +#pragma once + +#include + +namespace window { + class DSFE_MainWindow : public QMainWindow { + public: + explicit DSFE_MainWindow(QWidget* parent = nullptr); + }; +} diff --git a/DSFE_App/DSFE_GUI/src/Application.cpp b/DSFE_App/DSFE_GUI/src/Application.cpp index 81fee1ca..d7ce485f 100644 --- a/DSFE_App/DSFE_GUI/src/Application.cpp +++ b/DSFE_App/DSFE_GUI/src/Application.cpp @@ -53,7 +53,6 @@ Application::Application(const std::string& appName) { if (!gQtApp) { QCoreApplication::addLibraryPath("C:/Qt/6.11.1/msvc2022_64/plugins"); - qputenv("QT_DEBUG_PLUGINS", "1"); int argc = 0; gQtApp = new QApplication(argc, nullptr); gQtWindow = new QMainWindow(); diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp new file mode 100644 index 00000000..b6eb8a5c --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -0,0 +1,9 @@ +//DSFE_GUI DSFE_MainWindow.cpp +#include "MainWindow/DSFE_MainWindow.h" + +namespace window { + DSFE_MainWindow::DSFE_MainWindow(QWidget* parent) : QMainWindow(parent) { + setWindowTitle("DSFE Simulator"); + resize(800, 600); + } +} \ No newline at end of file From 62a964e4150fd71fc7b2d98cb91df3c50ec2d1ec Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 2 Jun 2026 15:59:45 +0100 Subject: [PATCH 004/110] feat: Added basic call from `Application` --- DSFE_App/DSFE_GUI/src/Application.cpp | 10 ++++------ DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/DSFE_App/DSFE_GUI/src/Application.cpp b/DSFE_App/DSFE_GUI/src/Application.cpp index d7ce485f..5e8313da 100644 --- a/DSFE_App/DSFE_GUI/src/Application.cpp +++ b/DSFE_App/DSFE_GUI/src/Application.cpp @@ -2,6 +2,7 @@ #include "Application.h" #include "Platform/WindowManager.h" +#include "MainWindow/DSFE_MainWindow.h" #include "Scene/Camera.h" #ifdef __gl_h_ @@ -22,7 +23,7 @@ namespace fs = std::filesystem; Application* Application::sInstance = nullptr; static QApplication* gQtApp = nullptr; -static QMainWindow* gQtWindow = nullptr; +static window::DSFE_MainWindow* gQtWindow = nullptr; // Constructor: Initialises paths, sets up data manager, and creates the main application window Application::Application(const std::string& appName) { @@ -55,11 +56,8 @@ Application::Application(const std::string& appName) { QCoreApplication::addLibraryPath("C:/Qt/6.11.1/msvc2022_64/plugins"); int argc = 0; gQtApp = new QApplication(argc, nullptr); - gQtWindow = new QMainWindow(); - gQtWindow->setWindowTitle(QString::fromStdString(appName)); - gQtWindow->resize(800, 600); - gQtWindow->show(); - LOG_INFO("Qt application and main window created with title '%s'", appName.c_str()); + gQtWindow = new window::DSFE_MainWindow(); + gQtWindow->show();` } _window = std::make_unique(); diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index b6eb8a5c..e260eb21 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -3,7 +3,7 @@ namespace window { DSFE_MainWindow::DSFE_MainWindow(QWidget* parent) : QMainWindow(parent) { - setWindowTitle("DSFE Simulator"); + setWindowTitle("DSFE"); resize(800, 600); } } \ No newline at end of file From c75ae3a77eb00ce2506bb921e45a796735a75da1 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 2 Jun 2026 16:17:02 +0100 Subject: [PATCH 005/110] feat: Added shell of `ProjectPage` class * This eventually will be the workspaces internal default for a "new" project, however for now its going to be used to ensure the project can effectively replicate the imgui version --- DSFE_App/DSFE_GUI/CMakeLists.txt | 2 ++ .../include/MainWindow/Workspace/ProjectPage.h | 11 +++++++++++ DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp | 5 ++++- .../DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp | 8 ++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h create mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 321e911d..484cf0cc 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -39,6 +39,7 @@ target_include_directories(stb INTERFACE set(MAIN_WINDOW_SRC src/MainWindow/DSFE_MainWindow.cpp + src/MainWindow/Workspace/ProjectPage.cpp ) set(RENDER_SRC @@ -128,6 +129,7 @@ target_include_directories(DSFE_GUI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/include/MainWindow PRIVATE ${imfilebrowser_ROOT} diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h b/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h new file mode 100644 index 00000000..bed7285a --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h @@ -0,0 +1,11 @@ +// DSFE_GUI ProjectPage.h +#pragma once + +#include + +namespace Workspace { + class ProjectPage : public QWidget { + public: + explicit ProjectPage(QWidget* parent = nullptr); + }; +} // namespace Workspace \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index e260eb21..c801b805 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -1,9 +1,12 @@ //DSFE_GUI DSFE_MainWindow.cpp #include "MainWindow/DSFE_MainWindow.h" +#include "Workspace/ProjectPage.h" namespace window { DSFE_MainWindow::DSFE_MainWindow(QWidget* parent) : QMainWindow(parent) { setWindowTitle("DSFE"); resize(800, 600); + + setCentralWidget(new Workspace::ProjectPage(this)); } -} \ No newline at end of file +} // namespace window \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp new file mode 100644 index 00000000..db2eac64 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp @@ -0,0 +1,8 @@ +// DSFE_GUI ProjectPage.cpp +#include "Workspace/ProjectPage.h" + +namespace Workspace { + ProjectPage::ProjectPage(QWidget* parent) : QWidget(parent) { + // Set up the UI for the project page here (e.g., using QVBoxLayout, adding widgets, etc.) + } +} // namespace Workspace \ No newline at end of file From 7953ebeb489bcb84d4edf2241ee76c7b252b94e6 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 2 Jun 2026 17:34:49 +0100 Subject: [PATCH 006/110] feat: Addded viewport widget logic and docking variant logic (though unused due to differng logic I am still exploring) --- DSFE_App/DSFE_GUI/CMakeLists.txt | 2 ++ .../include/MainWindow/DockWidgets/ViewportDock.h | 13 +++++++++++++ .../include/MainWindow/Widgets/ViewportWidget.h | 11 +++++++++++ DSFE_App/DSFE_GUI/src/Application.cpp | 2 +- .../DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp | 4 +++- .../src/MainWindow/DockWidgets/ViewportDock.cpp | 11 +++++++++++ .../src/MainWindow/Widgets/ViewportWidget.cpp | 10 ++++++++++ .../src/MainWindow/Workspace/ProjectPage.cpp | 13 ++++++++++++- 8 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/DockWidgets/ViewportDock.h create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h create mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/DockWidgets/ViewportDock.cpp create mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 484cf0cc..8e04f4ce 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -40,6 +40,8 @@ target_include_directories(stb INTERFACE set(MAIN_WINDOW_SRC src/MainWindow/DSFE_MainWindow.cpp src/MainWindow/Workspace/ProjectPage.cpp + src/MainWindow/DockWidgets/ViewportDock.cpp + src/MainWindow/Widgets/ViewportWidget.cpp ) set(RENDER_SRC diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/DockWidgets/ViewportDock.h b/DSFE_App/DSFE_GUI/include/MainWindow/DockWidgets/ViewportDock.h new file mode 100644 index 00000000..7ea44d8a --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/DockWidgets/ViewportDock.h @@ -0,0 +1,13 @@ +// DSFE_GUI ViewportDock.h +#pragma once + +#include + +class ViewportWidget; + +namespace dockwidgets { + class ViewportDock : public QDockWidget { + public: + explicit ViewportDock(QWidget* parent = nullptr); + }; +} // namespace dockwidgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h new file mode 100644 index 00000000..51868f9e --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h @@ -0,0 +1,11 @@ +// DSFE_GUI ViewportWidget.h +#pragma once + +#include + +namespace widgets { + class ViewportWidget : public QOpenGLWidget { + public: + explicit ViewportWidget(QWidget* parent = nullptr); + }; +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Application.cpp b/DSFE_App/DSFE_GUI/src/Application.cpp index 5e8313da..5e95f7e1 100644 --- a/DSFE_App/DSFE_GUI/src/Application.cpp +++ b/DSFE_App/DSFE_GUI/src/Application.cpp @@ -57,7 +57,7 @@ Application::Application(const std::string& appName) { int argc = 0; gQtApp = new QApplication(argc, nullptr); gQtWindow = new window::DSFE_MainWindow(); - gQtWindow->show();` + gQtWindow->show(); } _window = std::make_unique(); diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index c801b805..8c2ea55a 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -7,6 +7,8 @@ namespace window { setWindowTitle("DSFE"); resize(800, 600); - setCentralWidget(new Workspace::ProjectPage(this)); + auto* page = new Workspace::ProjectPage(this); + setCentralWidget(page); + page->show(); } } // namespace window \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DockWidgets/ViewportDock.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DockWidgets/ViewportDock.cpp new file mode 100644 index 00000000..732f92c1 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DockWidgets/ViewportDock.cpp @@ -0,0 +1,11 @@ +// DSFE_GUI ViewportDock.cpp +#include "DockWidgets/ViewportDock.h" +#include "Widgets/ViewportWidget.h" + +namespace dockwidgets { + ViewportDock::ViewportDock(QWidget* parent) : QDockWidget("Viewport", parent) { + setAllowedAreas(Qt::AllDockWidgetAreas); + setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); + setWidget(new widgets::ViewportWidget(this)); + } +} // namespace dockwidgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp new file mode 100644 index 00000000..5cae3be8 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp @@ -0,0 +1,10 @@ +// DSFE_GUI ViewportWidget.cpp +#include "Widgets/ViewportWidget.h" + +#include +#include + +namespace widgets { + ViewportWidget::ViewportWidget(QWidget* parent) : QOpenGLWidget(parent) { + } +} // namespace widgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp index db2eac64..dc4feb7e 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp @@ -1,8 +1,19 @@ // DSFE_GUI ProjectPage.cpp #include "Workspace/ProjectPage.h" +#include "Widgets/ViewportWidget.h" + +#include +#include +#include namespace Workspace { ProjectPage::ProjectPage(QWidget* parent) : QWidget(parent) { - // Set up the UI for the project page here (e.g., using QVBoxLayout, adding widgets, etc.) + auto* layout = new QVBoxLayout(this); + auto* splitter = new QSplitter(Qt::Horizontal, this); + splitter->addWidget(new widgets::ViewportWidget(splitter)); + splitter->addWidget(new QLabel("Properties(PlaceHolder)", splitter)); + splitter->setStretchFactor(0, 4); // Viewport takes 4/5 of space + splitter->setStretchFactor(1, 1); // Properties takes 1/5 of space + layout->addWidget(splitter); } } // namespace Workspace \ No newline at end of file From b3ca889dfb558c49431fa46e72d167dac746f45c Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Thu, 4 Jun 2026 09:03:12 +0100 Subject: [PATCH 007/110] feat: Separated `SimManager` compiler file into domain-specific subfiles to reduce file size and code stink --- .../include/Scene/Manager/SimImplementation.h | 611 ++++++ .../include/Scene/SimulationManager.h | 1 + .../DSFE_GUI/src/Scene/Manager/SimBackend.cpp | 112 ++ .../DSFE_GUI/src/Scene/Manager/SimCamera.cpp | 143 ++ .../DSFE_GUI/src/Scene/Manager/SimObjects.cpp | 110 ++ .../DSFE_GUI/src/Scene/Manager/SimPostFX.cpp | 10 + .../src/Scene/Manager/SimRendering.cpp | 433 +++++ .../DSFE_GUI/src/Scene/Manager/SimRobots.cpp | 77 + DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp | 180 ++ .../src/Scene/Manager/SimViewport.cpp | 10 + .../DSFE_GUI/src/Scene/SimulationManager.cpp | 1634 +---------------- 11 files changed, 1691 insertions(+), 1630 deletions(-) create mode 100644 DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h create mode 100644 DSFE_App/DSFE_GUI/src/Scene/Manager/SimBackend.cpp create mode 100644 DSFE_App/DSFE_GUI/src/Scene/Manager/SimCamera.cpp create mode 100644 DSFE_App/DSFE_GUI/src/Scene/Manager/SimObjects.cpp create mode 100644 DSFE_App/DSFE_GUI/src/Scene/Manager/SimPostFX.cpp create mode 100644 DSFE_App/DSFE_GUI/src/Scene/Manager/SimRendering.cpp create mode 100644 DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp create mode 100644 DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp create mode 100644 DSFE_App/DSFE_GUI/src/Scene/Manager/SimViewport.cpp diff --git a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h new file mode 100644 index 00000000..6b4303d3 --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h @@ -0,0 +1,611 @@ +// DSFE_GUI SimImplementation.h +#include "Scene/SimulationManager.h" +#include "Scene/Object.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "Scene/Input.h" +#include "Scene/Camera.h" +#include "Scene/Mesh.h" +#include "Assets/MeshLoader.h" +#include "Scene/Light.h" +#include "Scene/AxisOrientator.h" + +#include "Robots/RobotSystem.h" +#include "Robots/RobotModel.h" +#include "Robots/TrajectoryManager.h" + +#include "Rendering/SkyboxRenderer.h" +#include "Rendering/ShaderUtil.h" +#include "Rendering/OpenGLBufferManager.h" +#include "Rendering/IBL.h" +#include "Rendering/Texture.h" + +#include "Robots/RobotPresentationBuilder.h" +#include "Robots/RobotRenderer.h" + +#include +#include "Platform/Paths.h" +#include "EngineLib/LogMacros.h" +#include "Platform/DataManager.h" + +namespace gui { + // --- PIMPL Implementation --- + struct SimManager::Impl { + // Completed Simulation Runs (thread-safe) + std::mutex _completedRunsMutex; + std::vector _completedRuns; + + // View ID Alias + using VID = gui::ViewID; + + // View Modes + enum class ViewMode { Single, Quad }; + + // Current View Mode + ViewMode viewMode = ViewMode::Single; + + // Viewport Structure + struct Viewport { + std::unique_ptr cam; + std::unique_ptr fb; + std::unique_ptr post; + + // Cache size + int w = 1, h = 1; + int displayW = 1, displayH = 1; + + // Follow Target + scene::Object* followTarget = nullptr; + glm::vec3 followOffset = glm::vec3(0.0f, 0.25f, 1.0f); // tweak + bool followEnabled = false; + }; + + // Viewports + std::array _views; + VID activeView = VID::Manual; + + // Skybox & IBL + std::unique_ptr _ibl; + std::unique_ptr _skybox; + + // Post-Processing Shader + std::unique_ptr _postShader; + std::shared_ptr _shaderBasic; + std::shared_ptr _shaderLit; + std::shared_ptr _shaderPBR; + + // World Grid & Shadow Shaders + std::unique_ptr _worldGridShader; + std::unique_ptr _shadowShader; + shaders::Shader* currentShader; + + // Fullscreen Quad VAO + GLuint _fullscreenVAO = 0; + GLuint _worldGridVAO = 0; + + // Shadow Mapping (Cascaded) + GLuint _cascadeFBO[SimManager::NUM_CASCADES]{}; + GLuint _cascadeDepth[SimManager::NUM_CASCADES]{}; + glm::mat4 _lightSpaceMatrixCascade[SimManager::NUM_CASCADES] = {}; + + // Scene Objects + std::unique_ptr _light; + std::unique_ptr _axisOrientator; + + scene::Object* _selectedObject = nullptr; + scene::Object* _cameraFollowTarget = nullptr; + + std::shared_ptr _mesh; + std::vector> _objects; + std::unordered_map> _linkToObjects; + std::unordered_map _primaryLinkObject; + + // Robot System + std::unique_ptr _robotSystem; // simulation + + // Robot Rendering + std::unique_ptr _robotRenderer; // rendering + RobotRenderBinding _currentBinding; // current render binding + + // Robot Follow Target + bool eeFollowBound = false; + scene::Object* eeObject = nullptr; + + // Trajectory Manager + control::TrajectoryManager _traj; + + // SSAO Resources + std::unique_ptr _ssaoShader; + std::unique_ptr _ssaoBlurShader; + GLuint _ssaoFBO = 0, _ssaoTex = 0; + GLuint _ssaoBlurFBO = 0, _ssaoBlurTex = 0; + GLuint _ssaoNoiseTex = 0; + int _ssaoW = 0, _ssaoH = 0; + std::vector _ssaoKernel; + + Impl(SimManager& owner) { + _postShader = std::make_unique(); + _postShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "post.frag.glsl").string()); + + glGenVertexArrays(1, &_fullscreenVAO); + + // Lambda to create views + auto makeView = [&](ViewID id, glm::vec3 pos, float fovDeg, glm::vec3 target, glm::vec3 /*upHint*/) { + auto& v = _views[(size_t)id]; + + // INTERNAL render target size (scene render) + const int rtW = std::max(1, (int)owner._internalSize.x); + const int rtH = std::max(1, (int)owner._internalSize.y); + + // DISPLAY size (final post-process target) + const int dispW = std::max(1, (int)owner._displaySize.x); + const int dispH = std::max(1, (int)owner._displaySize.y); + const int postW = (dispW > 1 && dispH > 1) ? dispW : rtW; + const int postH = (dispW > 1 && dispH > 1) ? dispH : rtH; + + // Cache sizes + v.w = rtW; + v.h = rtH; + v.displayW = postW; + v.displayH = postH; + + // Framebuffers + v.fb = std::make_unique(); + v.fb->createBuffers(rtW, rtH, std::max(1, owner._settingsCurrent.msaaSamples)); + + v.post = std::make_unique(); + v.post->createBuffers(postW, postH, 1); + + // Camera aspect should match DISPLAY (what you're presenting in ImGui) + v.cam = std::make_unique(pos, fovDeg, (float)postW / (float)postH, 0.1f, 5000.0f); + v.cam->setFocus(target); + v.cam->updateViewMatrix(); + }; + + glm::vec3 target(0.0f); + + // Perspective + makeView(ViewID::Manual, { 0.0f, 0.5f, 1.0f }, 60.0f, target, { 0.0f, 1.0f, 0.0f }); // Default + makeView(ViewID::Follow, { 3.0f, 0.25f, 0.0f }, 20.0f, target, { 0.0f, 1.0f, 0.0f }); // Follow + // Ortho-ish + makeView(ViewID::Top, { 0.0f, 2.0f, 0.0f }, 20.0f, target, { 0.0f, 0.0f, -1.0f }); // Top + makeView(ViewID::Right, { 3.0f, 0.25f, 0.0f }, 20.0f, target, { 0.0f, 1.0f, 0.0f }); // Right + makeView(ViewID::Front, { 0.0f, 0.25f, 3.0f }, 20.0f, target, { 0.0f, -1.0f, 0.0f }); // Front + + { + // Top + auto* camTop = _views[(size_t)ViewID::Top].cam.get(); + camTop->setFocus(target); + camTop->setYaw(-glm::half_pi()); + camTop->setPitch(-glm::half_pi() + 0.001f); + camTop->setOrbitDistance(2.0f); + camTop->setMinDistance(0.5f); + camTop->updateViewMatrix(); + + // Right + auto* camRight = _views[(size_t)ViewID::Right].cam.get(); + camRight->setFocus(target); + camRight->setYaw(glm::pi()); + camRight->setPitch(0.0f); + camRight->setOrbitDistance(3.0f); + camRight->setMinDistance(0.5f); + camRight->updateViewMatrix(); + + // Front + auto* camFront = _views[(size_t)ViewID::Front].cam.get(); + camFront->setFocus(target); + camFront->setYaw(-glm::half_pi()); + camFront->setPitch(0.0f); + camFront->setOrbitDistance(3.0f); + camFront->setMinDistance(0.5f); + camFront->updateViewMatrix(); + + // Follow + auto* camFollow = _views[(size_t)ViewID::Follow].cam.get(); + camFollow->setFocus(target); + camFollow->setYaw(glm::pi()); + camFollow->setPitch(0.0f); + camFollow->setOrbitDistance(3.0f); + camFollow->setMinDistance(0.5f); + camFollow->updateViewMatrix(); + } + + // Shader Types A + // Basic shader with no lighting + _shaderBasic = std::make_shared(); + _shaderBasic->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_basic.frag.glsl").string()); + // Lit shader with simple Blinn-Phong lighting + _shaderLit = std::make_shared(); + _shaderLit->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_lit.frag.glsl").string()); + // PBR shader with full Physically Based Rendering (for release visuals) + _shaderPBR = std::make_shared(); + _shaderPBR->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_pbr.frag.glsl").string()); + + currentShader = _shaderPBR.get(); + _skybox = std::make_unique(); + + // Shader Types B + _worldGridShader = std::make_unique(); + _worldGridShader->load((paths::assets() / "shaders" / "world_grid.vert.glsl").string(), (paths::assets() / "shaders" / "world_grid.frag.glsl").string()); + + _shadowShader = std::make_unique(); + _shadowShader->load((paths::assets() / "shaders" / "shadow_depth.vert.glsl").string(), (paths::assets() / "shaders" / "shadow_depth.frag.glsl").string()); + + // Light + _light = std::make_unique(); + _light->_isDirectional = true; + + // Axis Orientator + _axisOrientator = std::make_unique(); + + // World Grid VAO + glGenVertexArrays(1, &_worldGridVAO); + + // Test Mesh + _mesh = std::make_shared(); + _mesh->init(); + + // Robot system with mesh loading (for normal simulation) + _robotSystem = std::make_unique(); + + _robotRenderer = std::make_unique(); + + // SSAO shaders + _ssaoShader = std::make_unique(); + _ssaoShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "ssao.frag.glsl").string()); + + _ssaoBlurShader = std::make_unique(); + _ssaoBlurShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "ssao_blur.frag.glsl").string()); + + // Generate hemisphere kernel + initSSAOKernel(64); + + // Generate noise texture (4x4 random rotation vectors) + initSSAONoise(); + } + + void clearRobotPresentation() { + _primaryLinkObject.clear(); + _linkToObjects.clear(); + _objects.clear(); + } + + scene::Object* primaryObjectForLink(const std::string& linkName) { + auto it = _primaryLinkObject.find(linkName); + if (it == _primaryLinkObject.end()) { return nullptr; } + return it->second; + } + + void buildRobotPresentationFromModel(const robots::RobotModel& model, SimManager& owner) { + clearRobotPresentation(); + + RobotPresentationBuilder builder; + RobotRenderBinding binding = builder.build(model); + + for (auto& owned : binding.ownedObjects) { + _objects.push_back(std::move(owned)); + } + + _robotRenderer->bind(binding); + + for (const auto& [linkName, visuals] : binding.linkVisuals) { + auto& target = _linkToObjects[linkName]; + + for (auto* obj : visuals) { + if (!obj) { continue; } + + target.push_back(obj); + + if (!_primaryLinkObject.contains(linkName)) { + _primaryLinkObject[linkName] = obj; + } + } + } + } + + // Random number generation for SSAO kernel and noise + std::mt19937 _rng{ std::random_device{}() }; + std::uniform_real_distribution _uni{ -1.0f, 1.0f }; + + // Helper to get random float in [a,b] + inline float rngFloat(float a, float b) { + std::uniform_real_distribution d(a, b); + return d(_rng); + } + + // Initialises the SSAO kernel with random samples in a hemisphere oriented along the positive Z axis, scaled to favor samples closer to the origin + void initSSAOKernel(int size) { + _ssaoKernel.clear(); + _ssaoKernel.reserve(size); + for (int i = 0; i < size; ++i) { + glm::vec3 sample( + rngFloat(-1.0f, 1.0f), + rngFloat(-1.0f, 1.0f), + rngFloat(0.0f, 1.0f) // hemisphere: z in [0,1] + ); + sample = glm::normalize(sample); + sample *= rngFloat(0.0f, 1.0f); + float scale = (float)i / (float)size; + scale = 0.1f + scale * scale * 0.9f; + sample *= scale; + _ssaoKernel.push_back(sample); + } + } + + // Initializes the SSAO noise texture with random rotation vectors in the XY plane + void initSSAONoise() { + std::vector noise(16); + // Generate 16 random rotation vectors in the XY plane (Z=0) + for (int i = 0; i < 16; ++i) { + noise[i] = glm::vec3( + rngFloat(-1.0f, 1.0f), + rngFloat(-1.0f, 1.0f), + 0.0f + ); + } + // Create OpenGL texture + glGenTextures(1, &_ssaoNoiseTex); + glBindTexture(GL_TEXTURE_2D, _ssaoNoiseTex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 4, 4, 0, GL_RGB, GL_FLOAT, noise.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + } + + // Ensures SSAO FBOs and textures are created and match the given size, recreating them if necessary + void ensureSSAOBuffers(int w, int h) { + if (_ssaoW == w && _ssaoH == h) return; + _ssaoW = w; _ssaoH = h; + // Delete old buffers if they exist + if (_ssaoFBO) { glDeleteFramebuffers(1, &_ssaoFBO); glDeleteTextures(1, &_ssaoTex); } + if (_ssaoBlurFBO) { glDeleteFramebuffers(1, &_ssaoBlurFBO); glDeleteTextures(1, &_ssaoBlurTex); } + // Lambda to create a single-channel floating point FBO and texture + auto makeSingleChannelFBO = [](GLuint& fbo, GLuint& tex, int w, int h) { + glGenFramebuffers(1, &fbo); + glGenTextures(1, &tex); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glBindTexture(GL_TEXTURE_2D, tex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, w, h, 0, GL_RED, GL_FLOAT, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + }; + // Create SSAO and blur FBOs/textures + makeSingleChannelFBO(_ssaoFBO, _ssaoTex, w, h); + makeSingleChannelFBO(_ssaoBlurFBO, _ssaoBlurTex, w, h); + } + + // Renders the SSAO pass and subsequent blur pass, writing results to SSAO FBOs. Should be called after rendering the scene to the view's framebuffer (to provide depth texture input). + void renderSSAO(SimManager& owner, Viewport& v) { + if (!owner._settingsCurrent.ssao) return; + // Determine SSAO buffer size based on division factor + int ssaoDiv = std::max(1, owner._settingsCurrent.ssaoResDiv); + int ssaoW = std::max(1, v.w / ssaoDiv); + int ssaoH = std::max(1, v.h / ssaoDiv); + ensureSSAOBuffers(ssaoW, ssaoH); + // Clamp kernel size to available samples + int kernelSize = std::min((int)_ssaoKernel.size(), owner._settingsCurrent.ssaoSamples); + // SSAO Pass + glBindFramebuffer(GL_FRAMEBUFFER, _ssaoFBO); + glViewport(0, 0, ssaoW, ssaoH); + glClear(GL_COLOR_BUFFER_BIT); + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + + _ssaoShader->use(); + + // Depth texture from the resolved scene FBO + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, v.fb->getDepthTexture()); + _ssaoShader->setInt1(0, "gDepth"); + // Noise texture + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, _ssaoNoiseTex); + _ssaoShader->setInt1(1, "gNoise"); + + // Camera matrices and parameters + _ssaoShader->setMat4(v.cam->getProjection(), "projection"); + _ssaoShader->setMat4(glm::inverse(v.cam->getProjection()), "invProjection"); + _ssaoShader->setVec2(glm::vec2((float)ssaoW / 4.0f, (float)ssaoH / 4.0f), "noiseScale"); + _ssaoShader->setInt1(kernelSize, "kernelSize"); + _ssaoShader->setFlt1(0.25f, "radius"); + _ssaoShader->setFlt1(0.035f, "bias"); + _ssaoShader->setFlt1(owner._settingsCurrent.ssaoStrength, "strength"); + + // SSAO kernel samples + for (int i = 0; i < kernelSize; ++i) { _ssaoShader->setVec3(_ssaoKernel[i], "samples[" + std::to_string(i) + "]"); } + + glBindVertexArray(_fullscreenVAO); + glDrawArrays(GL_TRIANGLES, 0, 3); + + // Blur pass + glBindFramebuffer(GL_FRAMEBUFFER, _ssaoBlurFBO); + glViewport(0, 0, ssaoW, ssaoH); + glClear(GL_COLOR_BUFFER_BIT); + + // No need for depth test or blending for a simple fullscreen blur + _ssaoBlurShader->use(); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, _ssaoTex); + _ssaoBlurShader->setInt1(0, "ssaoInput"); + glDrawArrays(GL_TRIANGLES, 0, 3); + glBindVertexArray(0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + // Renders the given viewport to its framebuffer, handling dynamic resizing of the framebuffer and camera aspect ratio based on the provided display size + void renderView(SimManager& owner, Viewport& v, int displayW, int displayH) { + displayW = std::max(1, displayW); + displayH = std::max(1, displayH); + + // Internal render target size + glm::ivec2 rt(std::max(1, (int)owner._internalSize.x), std::max(1, (int)owner._internalSize.y)); + + // In quad mode, render at the cell's display resolution to avoid scaling artifacts + if (owner._impl->viewMode == Impl::ViewMode::Quad) { + rt.x = std::max(1, displayW); + rt.y = std::max(1, displayH); + } + const int rtW = std::max(1, (int)rt.x); + const int rtH = std::max(1, (int)rt.y); + + LOG_INFO_ONCE("Rendering Viewport: RT Size = %dx%d, Display Size = %dx%d", rtW, rtH, displayW, displayH); + + // Resize only when internal RT changes OR display changes (post buffer) + const bool rtChanged = (v.w != rtW) || (v.h != rtH); + const bool displayChanged = (v.displayW != displayW) || (v.displayH != displayH); + const bool msaaChanged = false; + + if (rtChanged || displayChanged || msaaChanged) { + // Store internal RT size + v.w = rtW; + v.h = rtH; + + // Store display size + v.displayW = displayW; + v.displayH = displayH; + + // Adjust MSAA based on internal RT size + int msaa = std::max(1, owner._settingsCurrent.msaaSamples); + + // Scene framebuffer: INTERNAL resolution (rtW x rtH) + v.fb->deleteBuffers(); + v.fb->createBuffers(rtW, rtH, msaa); + + // Post-processing framebuffer: DISPLAY resolution (displayW x displayH) + v.post->deleteBuffers(); + v.post->createBuffers(displayW, displayH, 1); + + // Camera aspect must match DISPLAY aspect + v.cam->setAspect((float)displayW / (float)displayH); + } + + v.fb->bind(); + glViewport(0, 0, v.w, v.h); + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + glDepthFunc(GL_LESS); + + glClearColor(owner._backgroundColour.r, owner._backgroundColour.g, owner._backgroundColour.b, owner._backgroundAlpha); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + if (owner._settingsCurrent.msaaSamples > 1) { + glEnable(GL_MULTISAMPLE); + } + else { + glDisable(GL_MULTISAMPLE); + } + + // Update Follow Target: use the explicitly bound target (e.g. end-effector from loadRobot), + // only fall back to _selectedObject if no explicit target was set via setViewFollowTarget + if (&v == &_views[(size_t)gui::ViewID::Follow]) { + scene::Object* target = v.followEnabled ? v.followTarget : _selectedObject; + + if (target && target->getMesh()) { + glm::mat4 M = target->transform.toMatrix() * target->getMesh()->localTransform; + glm::vec3 worldPos = glm::vec3(M[3]); + v.cam->setFollowTarget(worldPos, target->transform.rotQ); + } + } + + // Skybox (renders before all opaque geometry, doesn't write depth) + if (owner.skyboxEnabled) { + glDepthMask(GL_FALSE); + glDepthFunc(GL_LEQUAL); + owner.SkyboxRender(v.cam.get()); + glDepthMask(GL_TRUE); + glDepthFunc(GL_LESS); + } + + // Meshes & World Grid + GLint sampleBuffers = 0, samples = 0; + glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers); + glGetIntegerv(GL_SAMPLES, &samples); + LOG_INFO_ONCE("FB MSAA state: GL_SAMPLE_BUFFERS=%d GL_SAMPLES=%d", sampleBuffers, samples); + + owner.MeshRender(v.cam.get()); + if (owner._settingsCurrent.grid) { owner.WorldGridRender(v.cam.get(), v.w); } + + v.fb->unbind(); + + // SSAO pass (reads resolved depth, writes to _ssaoBlurTex) + renderSSAO(owner, v); + + // Only valid if you allocated mip levels for _texID (via glTexStorage2D) + glBindTexture(GL_TEXTURE_2D, v.fb->getTexture()); + glGenerateMipmap(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, 0); + + // Post-Processing + v.post->bind(); + glDisable(GL_MULTISAMPLE); + glViewport(0, 0, v.displayW, v.displayH); + + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + glClear(GL_COLOR_BUFFER_BIT); + + _postShader->use(); + _postShader->setInt1(0, "hdrScene"); + _postShader->setInt1(1, "ssaoTex"); + _postShader->setBool(owner._settingsCurrent.ssao, "ssaoEnabled"); + _postShader->setFlt1(owner._settingsCurrent.exposure, "exposure"); + _postShader->setFlt1(owner._settingsCurrent.whitePoint, "whitePoint"); + _postShader->setVec2(glm::vec2(v.w, v.h), "uRes"); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, v.fb->getTexture()); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, owner._settingsCurrent.ssao ? _ssaoBlurTex : 0); + + glBindVertexArray(_fullscreenVAO); + glDrawArrays(GL_TRIANGLES, 0, 3); + if (owner._settingsCurrent.axisOrientator) { _axisOrientator->render(v.cam->getViewMatrix()); } + glBindVertexArray(0); + + v.post->unbind(); + } + + static bool icontains(const std::string& s, const char* sub) { + if (sub == nullptr || *sub == '\0') { return false; } + + // Case-insensitive search using std::search with a custom comparator + auto it = std::search( + s.begin(), s.end(), + sub, sub + std::strlen(sub), + [](char a, char b) { + return std::tolower((unsigned char)a) == std::tolower((unsigned char)b); + } + ); + return it != s.end(); + } + + scene::Object* findEndEffectorFromRange(size_t startIdx) { + for (size_t i = startIdx; i < _objects.size(); ++i) { + scene::Object* o = _objects[i].get(); + if (!o) continue; + + const std::string& n = o->name; + if (icontains(n, "end") || icontains(n, "eff") || icontains(n, "ee") || icontains(n, "tool") || icontains(n, "tcp") || icontains(n, "gripper")) { + return o; + } + } + + // Fallback: last object added (usually the last link) + if (_objects.size() > startIdx) return _objects.back().get(); + return nullptr; + } + }; +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h index 923b47db..b0dda2f2 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h +++ b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h @@ -161,6 +161,7 @@ namespace gui { // Rendering Entry Points void render(); + void tick(double dt); void resize(int32_t width, int32_t height); void syncRobotToScene(); diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimBackend.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimBackend.cpp new file mode 100644 index 00000000..c746a575 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimBackend.cpp @@ -0,0 +1,112 @@ +// DSFE_GUI SimBackend.cpp +#include "Scene/SimulationManager.h" +#include "Scene/SimulationCore.h" +#ifdef __gl_h_ +#undef __gl_h_ +#endif +#include "Manager/SimImplementation.h" + +#include "Interpreter/IStoredProgram.h" +#include "Interpreter/StoredProgram.h" +#include "Interpreter/Parser.h" + +namespace gui { + // Start the simulation + void SimManager::startSimulation() { + if (!hasRobot()) { + LOG_WARN("Cannot start simulation: no robot loaded"); + return; + } + if (hasRobot() && !_bodyLoaded) { + loadRobot(_impl->_robotSystem->robotName()); + return; + } + _core->startSimulation(); + } + + // Stop the simulation + void SimManager::stopSimulation() { _core->stopSimulation(); } + + // Check if the simulation is currently running + const bool SimManager::isSimRunning() const { return _core->isSimRunning(); } + + // Setter for current simulation time (in seconds) + void SimManager::setSimTime(double time) { _core->setSimTime(time); } + const double SimManager::simTime() const { return _core->simTime(); } + + // Setter and gettter for fixed timstep (in seconds) + void SimManager::setFixedDt(double dt) { _core->setFixedDt(dt); } + const double SimManager::fixedDt() const { return _core->fixedDt(); } + + // Setter and getter for telemetry frequency (in Hz) + void SimManager::setTelemetryHz(double hz) { _core->setTelemetryHz(hz); } + const double SimManager::telemetryHz() const { return _core->telemetryHz(); } + + // Set whether a script is currently running (used to disable UI elements, etc.) + void SimManager::setScriptRunning(bool running) { _core->setScriptRunning(running); } + const bool SimManager::isScriptRunning() const { return _core->isScriptRunning(); } + + // Setters and getters for last script text + void SimManager::setLastScriptText(const std::string& text) { _core->setLastScriptText(text); } + const std::string& SimManager::lastScriptText() const { return _core->lastScriptText(); } + + // Accessors for the Simulation Core's telemetry data + diagnostics::TelemetryRecorder& SimManager::telemetry() { return _core->telemetry(); } + const diagnostics::TelemetryRecorder& SimManager::telemetry() const { return _core->telemetry(); } + + // Accesors for the active program (if any) + void SimManager::setActiveProgram(interpreter::IStoredProgram* program) { _core->setActiveProgram(program); } + interpreter::IStoredProgram* SimManager::activeProgram() { return _core->activeProgram(); } + const interpreter::IStoredProgram* SimManager::activeProgram() const { return _core->activeProgram(); } + + // Access the simulation core interface (non-const and const versions) + core::ISimulationCore* SimManager::simCoreInterface() { return _core.get(); } + const core::ISimulationCore* SimManager::simCoreInterface() const { return _core.get(); } + + // Access the concrete simulation core (non-const and const versions) + core::SimulationCore* SimManager::simCore() { return _core.get(); } + const core::SimulationCore* SimManager::simCore() const { return _core.get(); } + + // Set the integrator method for the current simulation run + void SimManager::setIntegrationMethod(integration::eIntegrationMethod method) { + _core->setIntegrationMethod(method); + } + const integration::eIntegrationMethod SimManager::integrationMethod() const { + return _core->integrationMethod(); + } + void SimManager::setADIntegrationMethod(integration::eAutoDiffIntegrationMethod method) { + _core->setADIntegrationMethod(method); + } + const integration::eAutoDiffIntegrationMethod SimManager::autoDiffIntegrationMethod() const { + return _core->autoDiffIntegrationMethod(); + } + + // This seems to be the better solution? + static std::string replaceIntegratorInScript(const std::string& script, const std::string& methodName) { + std::regex re(R"((?i)set\s*\(\s*integrator\s*,\s*([a-z0-9_]+)\s*\))"); // case-insensitive regex to match my DSL command -> set(integrator, method) + std::string replacement = "set(integrator, " + methodName + ")"; + return std::regex_replace(script, re, replacement); + } + + // Run a script to completion synchronously with a specific integrator + bool SimManager::runScriptToCompletion(const std::string& scriptText, integration::eIntegrationMethod method) { + if (!hasRobot()) { return false; } + + // Map method enum to string name + static const char* names[] = { "euler", "midpoint", "heun", "ralston", "rk4", "rk45", "implicit_euler", "implicit_midpoint", "glrk2", "glrk3" }; + const std::string methodName = names[static_cast(method)]; + + // Replace the integrator method in the script text + std::string modifiedScript = replaceIntegratorInScript(scriptText, methodName); + + // Create program and parser (bound to headless core) + auto program = std::make_unique(_core.get()); + //if (scene::Object* o = getObject()) program->setDefaultObject(o); + auto parser = std::make_unique(program.get()); + + // Parse the modified script and start the program + parser->parse(modifiedScript); + program->start(); + return _core->runScriptToCompletion(program.get(), method); // this will block until the script finishes + } +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimCamera.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimCamera.cpp new file mode 100644 index 00000000..c61cd927 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimCamera.cpp @@ -0,0 +1,143 @@ +// DSFE_GUI SimCamera.cpp +#include "Scene/SimulationManager.h" +#ifdef __gl_h_ +#undef __gl_h_ +#endif +#include "Manager/SimImplementation.h" + +namespace gui { + // Get the active view camera + scene::Camera* SimManager::getCamera() { return _impl->_views[static_cast(_impl->activeView)].cam.get(); } + // Reset the active view camera to default position + void SimManager::resetView() { + auto& v = _impl->_views[static_cast(_impl->activeView)]; + + glm::vec3 pos = { 0.0f, 0.25f, 1.0f }; + float fov = 60.0f; + + switch (_impl->activeView) { + case gui::ViewID::Top: pos = { 0, 5, 0 }; fov = 20.0f; break; + case gui::ViewID::Right: pos = { 5, 0, 0 }; fov = 20.0f; break; + case gui::ViewID::Front: pos = { 0, 0, 5 }; fov = 20.0f; break; + case gui::ViewID::Follow: fov = 20.0f; break; + case gui::ViewID::Manual: fov = 20.0f; break; + default: break; + } + + float aspect = (float)std::max(1, v.w) / (float)std::max(1, v.h); + v.cam = std::make_unique(pos, fov, aspect, 0.1f, 5000.0f); + + v.cam->setFocus(glm::vec3(0.0f)); + + switch (_impl->activeView) { + case gui::ViewID::Top: + v.cam->setYaw(-glm::half_pi()); + v.cam->setPitch(-glm::half_pi() + 0.001f); + break; + case gui::ViewID::Right: + v.cam->setYaw(glm::pi()); + v.cam->setPitch(0.0f); + break; + case gui::ViewID::Front: + v.cam->setYaw(-glm::half_pi()); + v.cam->setPitch(0.0f); + break; + default: + break; + } + + v.cam->updateViewMatrix(); + } + + // Attach camera to an object and start following it. The camera will maintain a fixed offset from the object's position and orientation. + void SimManager::attachCameraToObject(scene::Object* obj) { + if (!obj) return; + scene::Camera* cam = _impl->_views[static_cast(_impl->activeView)].cam.get(); + + _impl->_cameraFollowTarget = obj; + + glm::vec3 pos = obj->transform.position; + glm::quat rot = obj->transform.rotQ; + + cam->startFollow(pos, rot, glm::vec3(0, 2, 5)); + } + + // Detach camera from any object and stop following + void SimManager::detachCameraFromObject() { + scene::Camera* cam = _impl->_views[static_cast(_impl->activeView)].cam.get(); + _impl->_cameraFollowTarget = nullptr; + cam->clearFollow(); + } + + // Set the follow target for a specific view + void SimManager::setViewFollowTarget(ViewID view, scene::Object* obj, const glm::vec3& offset) { + if (view < ViewID::Manual || view >= ViewID::COUNT) { return; } + auto& v = _impl->_views[static_cast(view)]; + v.followTarget = obj; + v.followOffset = offset; + v.followEnabled = (obj != nullptr); + + if (v.followEnabled && v.cam && obj) { + v.cam->startFollow(obj->transform.position, obj->transform.rotQ, offset); + } + } + + // Clear the follow target for a specific view + void SimManager::clearViewFollowTarget(ViewID view) { + if (view < ViewID::Manual || view >= ViewID::COUNT) { return; } + auto& v = _impl->_views[static_cast(view)]; + v.followTarget = nullptr; + v.followEnabled = false; + if (v.cam) { v.cam->clearFollow(); } + } + + // Convenience for Follow view: set the follow target to the object attached to a robot joint (e.g. end-effector) + bool SimManager::setViewFollowRobotJoint(ViewID view, const std::string& jointName, const glm::vec3& offset) { + if (!hasRobot()) { + LOG_WARN("setViewFollowRobotJoint: no robot loaded"); + return false; + } + + robots::RobotSystem* rs = robotSystem(); + if (!rs) return false; + + auto& joints = rs->joints(); + auto& links = rs->links(); + + // 1) Find joint by name + const robots::RobotJoint* jPtr = nullptr; + for (auto& j : joints) { + if (j.name == jointName) { jPtr = &j; break; } + } + if (!jPtr) { + LOG_WARN("setViewFollowRobotJoint: joint not found: %s", jointName.c_str()); + return false; + } + + // 2) Find child link -> attached object + scene::Object* targetObj = nullptr; + if (auto it = _impl->_primaryLinkObject.find(jPtr->child); + it != _impl->_primaryLinkObject.end()) { + targetObj = it->second; + } + + if (!targetObj) { + LOG_WARN("setViewFollowRobotJoint: no attached object for joint=%s child=%s", + jointName.c_str(), jPtr->child.c_str()); + return false; + } + + // 3) Bind the view follow target + setViewFollowTarget(view, targetObj, offset); + + /*LOG_INFO("Follow view=%d bound to joint='%s' -> child='%s' -> obj='%s'", + (int)view, jointName.c_str(), jPtr->child.c_str(), targetObj->name.c_str());*/ + + return true; + } + + // Convenience for Follow view + bool SimManager::followRobotJoint(const std::string& jointName, const glm::vec3& offset) { + return setViewFollowRobotJoint(gui::ViewID::Follow, jointName, offset); + } +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimObjects.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimObjects.cpp new file mode 100644 index 00000000..6335f9c2 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimObjects.cpp @@ -0,0 +1,110 @@ +// DSFE_GUI SimObjects.cpp +#include "Scene/SimulationManager.h" +#ifdef __gl_h_ +#undef __gl_h_ +#endif +#include "Manager/SimImplementation.h" + +namespace gui { + // Helper to get the next ObjectID + static inline scene::ObjectID next(scene::ObjectID id) { + return static_cast(static_cast(id) + 1); + } + + // Load a mesh from file and create one Object per submesh. The last loaded mesh becomes the active selection. + void SimManager::loadMesh(const std::string& filepath) { + assets::MeshLoader loader; + auto meshes = loader.load(filepath); + + if (meshes.empty()) { + LOG_WARN("No meshes imported from %s", filepath.c_str()); + D_WARN("No meshes imported from %s", filepath.c_str()); + return; + } + + // For now: spawn one Object per submesh + for (auto& m : meshes) { + auto obj = std::make_unique(m); + obj->id = next(_nextObjectID); + obj->source.filename = filepath; + obj->name = m->getName().empty() ? "Object_" + std::to_string(scene::toUInt32(obj->id)) : m->getName(); + + // initialise physics state + obj->state.q = Quat(1.0, 0.0, 0.0, 0.0); + obj->state.angularVelocity = Vec3::Zero(); + obj->state.linearVelocity = Vec3::Zero(); + obj->state.mass = 1.0; + obj->state.damping = 0.0; + obj->state.inertia = Mat3::Identity(); + obj->state.forces = Vec3::Zero(); + obj->state.torques = Vec3::Zero(); + + _impl->_selectedObject = obj.get(); + _impl->_objects.push_back(std::move(obj)); + } + + LOG_INFO("Loaded %zu submeshes from %s", meshes.size(), filepath.c_str()); + D_INFO("Loaded %zu submeshes from %s", meshes.size(), filepath.c_str()); + } + + // Returns the loaded objects so they can be used as targets for robot joints in the same frame (e.g. end-effector) + std::vector SimManager::loadMeshReturn(const std::string& filepath) { + assets::MeshLoader loader; + auto meshes = loader.load(filepath); + std::vector result; + for (auto& m : meshes) { + auto obj = std::make_unique(m); + auto raw = obj.get(); + raw->internal = true; + _impl->_objects.push_back(std::move(obj)); + result.push_back(raw); + } + return result; + } + + // Setter and Getter for the active mesh + void SimManager::setMesh(std::shared_ptr mesh) { _impl->_mesh = mesh; } + std::shared_ptr SimManager::getMesh() { return _impl->_mesh; } + + // Set the currently selected object (can be nullptr to deselect) + void SimManager::setSelectedObject(scene::Object* obj) { _impl->_selectedObject = obj; } + // Add a new object to the scene and select it + void SimManager::addObject(std::unique_ptr obj) { _impl->_objects.push_back(std::move(obj)); } // Cache the unique_ptr + + // Remove an object by index + void SimManager::deleteObject(int index) { + if (index < 0 || index >= _impl->_objects.size()) { return; } + if (_impl->_selectedObject == _impl->_objects[index].get()) { _impl->_selectedObject = nullptr; } + _impl->_objects.erase(_impl->_objects.begin() + index); + } + + // Remove an object by pointer + void SimManager::removeObject(scene::Object* obj) { + if (!obj) return; + + auto it = std::remove_if( + _impl->_objects.begin(), + _impl->_objects.end(), + [obj](const std::unique_ptr& o) { + return o.get() == obj; + } + ); + + _impl->_objects.erase(it, _impl->_objects.end()); + } + + // Access the objects as raw pointers for use in the rest of the codebase, while maintaining ownership in SimManager + std::vector>& SimManager::getObjects() { return _impl->_objects; } + // Get the currently selected object (can be nullptr) + scene::Object* SimManager::getObject() { return _impl->_selectedObject; } + + // Helper to find an object by its ID (returns nullptr if not found) + scene::Object* SimManager::getObjectByID(scene::ObjectID id) { + for (auto& obj : _impl->_objects) { + if (obj && obj->id == id) { + return obj.get(); + } + } + return nullptr; + } +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimPostFX.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimPostFX.cpp new file mode 100644 index 00000000..94109812 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimPostFX.cpp @@ -0,0 +1,10 @@ +// DSFE_GUI SimPostFX.cpp +#include "Scene/SimulationManager.h" +#ifdef __gl_h_ +#undef __gl_h_ +#endif +#include "Manager/SimImplementation.h" + +namespace gui { + +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRendering.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRendering.cpp new file mode 100644 index 00000000..2e3dda89 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRendering.cpp @@ -0,0 +1,433 @@ +// DSFE_GUI SimRendering.cpp +#include "Scene/Object.h" +#include "Scene/SimulationManager.h" +#ifdef __gl_h_ +#undef __gl_h_ +#endif +#include "Manager/SimImplementation.h" + +namespace gui { + scene::Light* SimManager::getLight() { return _impl->_light.get(); } + + void SimManager::loadNewHDR(const std::string& path) { + D_INFO("Loading new HDR: %s", path.c_str()); + + // Make sure IBL system exists + if (!_impl->_ibl) { + LOG_ERROR("Cannot load HDR because IBL system is not initialised."); + D_FAIL("Cannot load HDR because IBL system is not initialised."); + return; + } + + _impl->_ibl->init(path); // rebuild envCubemap, irradiance, prefilter, brdfLUT + D_SUCCESS("IBL rebuilt successfully."); + + // Update skybox + _impl->_skybox->setEnvironmentTexture(_impl->_ibl->getEnvCubemap()); + + _activeHDRPath = path; + + D_SUCCESS("Loaded HDR successfully."); + } + + void SimManager::loadNewHDR_UI(const std::string& path) { + loadNewHDR(path); + _hdrUserOverride = true; + } + + void SimManager::loadNewHDR_Preset(const std::string& path) { + loadNewHDR(path); + _hdrUserOverride = false; + } + + // Initialize shadow map resources for cascaded shadow mapping + void SimManager::InitShadowResource(int baseRes) { + if (_shadowsInit) { + glDeleteFramebuffers(SimManager::NUM_CASCADES, _impl->_cascadeFBO); + glDeleteTextures(SimManager::NUM_CASCADES, _impl->_cascadeDepth); + } + + glGenFramebuffers(SimManager::NUM_CASCADES, _impl->_cascadeFBO); + glGenTextures(SimManager::NUM_CASCADES, _impl->_cascadeDepth); + + for (int i = 0; i < SimManager::NUM_CASCADES; i++) { + const int res = (i == 0) ? baseRes : (baseRes / 2); // 8192, 4096, 2048, 1024, 512, 256, 128 + + glBindTexture(GL_TEXTURE_2D, _impl->_cascadeDepth[i]); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, res, res, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + const float border[] = { 1.0f, 1.0f, 1.0f, 1.0f }; + glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); + + glBindFramebuffer(GL_FRAMEBUFFER, _impl->_cascadeFBO[i]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, _impl->_cascadeDepth[i], 0); + + glDrawBuffer(GL_NONE); + glReadBuffer(GL_NONE); + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + _shadowsInit = true; + } + + // Initialise the IBL system with a default HDR environment map + void SimManager::InitIBL() { + _impl->_ibl = std::make_unique(); + _impl->_ibl->init((paths::assets() / "hdr" / "default_white.hdr").string()); + } + + // Render the world grid overlay in the viewport + void SimManager::WorldGridRender(scene::Camera* cam, int rtW) { + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); + glDepthMask(GL_FALSE); + + const int msaa = std::max(1, _settingsCurrent.msaaSamples); + + if (msaa > 1) { + glDisable(GL_BLEND); + glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE); + glEnable(GL_MULTISAMPLE); + + glEnable(GL_POLYGON_OFFSET_FILL); + glPolygonOffset(-0.2f, -0.2f); + } + else { + glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE); + glDisable(GL_MULTISAMPLE); + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + } + + // Scale grid line thickness so smaller RTs (quad mode) match single view appearance + const float fullW = std::max(1.0f, _internalSize.x); + const float internalScale = (float)std::max(1, rtW) / fullW; + + _impl->_worldGridShader->use(); + _impl->_worldGridShader->setMat4(cam->getViewProjection(), "gVP"); + _impl->_worldGridShader->setMat4(cam->getViewMatrix(), "gView"); + _impl->_worldGridShader->setVec3(cam->getPosition(), "gCameraWorldPos"); + _impl->_worldGridShader->setFlt1(_settingsCurrent.renderScale, "gRenderScale"); + _impl->_worldGridShader->setFlt1(internalScale, "gInternalScale"); + _impl->_worldGridShader->setFlt1(2500.0f, "gGridSize"); + + glBindVertexArray(_impl->_worldGridVAO); + glDrawArrays(GL_TRIANGLES, 0, 6); + glBindVertexArray(0); + + glDisable(GL_POLYGON_OFFSET_FILL); + glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE); + + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); + glDepthFunc(GL_LESS); + } + + // Render the meshes in the scene using the currently selected shader mode, setting appropriate uniforms for each mode + void SimManager::MeshRender(scene::Camera* cam) { + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); + + shaders::Shader* shader = nullptr; + + switch (currentShaderMode) { + case ShaderMode::Basic: + shader = _impl->_shaderBasic.get(); + break; + case ShaderMode::Lit: + shader = _impl->_shaderLit.get(); + break; + case ShaderMode::PBR: + shader = _impl->_shaderPBR.get(); + break; + } + + if (!shader) { + LOG_ERROR("Shader is NULL after switch!"); + return; + } + + shader->use(); + shader->setBool(false, "isFloor"); + + // Only PBR know about cascades & those uniforms + if (currentShaderMode == ShaderMode::PBR) { + for (int i = 0; i < NUM_CASCADES; i++) { + glActiveTexture(GL_TEXTURE5 + i); + glBindTexture(GL_TEXTURE_2D, _impl->_cascadeDepth[i]); + shader->setInt1(5 + i, "cascadeShadowMap[" + std::to_string(i) + "]"); + shader->setMat4(_impl->_lightSpaceMatrixCascade[i], "lightSpaceMatrix[" + std::to_string(i) + "]"); + } + + // Convert fractional splits (0..1 of far) into world-space distances + const float nearPlane = cam->getNear(); + const float farPlane = cam->getFar(); + + const float splitFrac0 = _cascadeSplits[0]; + const float splitFrac1 = _cascadeSplits[1]; + + const float splitDist0 = nearPlane + splitFrac0 * farPlane; + const float splitDist1 = nearPlane + splitFrac1 * farPlane; + + shader->setFlt2(splitDist0, splitDist1, "cascadeSplits"); + } + + // Camera / SunLight / light common to all mesh shaders + cam->update(shader); + _impl->_light->update(shader); + + // Main mesh rendering loop + for (auto& obj : _impl->_objects) { + if (!obj || !obj->getMesh()) continue; + + if (_impl->_cameraFollowTarget == obj.get()) { cam->setFollowTarget(obj->transform.position, obj->transform.rotQ); } + + glm::mat4 model = obj->transform.toMatrix() * obj->getMesh()->localTransform; + shader->setMat4(model, "model"); + + // Per-mode material uniforms + switch (currentShaderMode) { + case ShaderMode::Basic: + // (IMPORTANT) mesh_basic.frag needs: uniform vec3 color; + shader->setVec3(obj->material.albedo, "albedo"); + break; + + case ShaderMode::Lit: + // (IMPORTANT) mesh_lit.frag needs: albedo, lightPosition, lightColour, lightIntensity, camPos + shader->setVec3(obj->material.albedo, "albedo"); + shader->setVec3(_impl->_light->getPosition(), "lightPosition"); + shader->setFlt1(_impl->_light->getIntensity(), "lightIntensity"); + shader->setVec3(_impl->_light->getColour(), "lightColour"); + shader->setVec3(cam->getPosition(), "camPos"); + break; + + case ShaderMode::PBR: + // Per-mesh PBR material properties + shader->setVec3(obj->material.albedo, "albedo"); + shader->setFlt1(obj->material.metallic, "metallic"); + shader->setFlt1(obj->material.roughness, "roughness"); + shader->setFlt1(1.0f, "ao"); + shader->setFlt1(_settingsCurrent.ambientStrength, "ambientStrength"); + + shader->setVec3(glm::normalize(_impl->_light->getDirection()), "lightDirection"); + shader->setFlt1(_impl->_light->getIntensity(), "lightIntensity"); + shader->setVec3(_impl->_light->getColour(), "lightColour"); + shader->setVec3(cam->getPosition(), "camPos"); + + shader->setInt1(0, "irradianceMap"); + shader->setInt1(1, "prefilterMap"); + shader->setInt1(2, "brdfLUT"); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_CUBE_MAP, _impl->_ibl->getIrradianceMap()); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_CUBE_MAP, _impl->_ibl->getPrefilterMap()); + + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, _impl->_ibl->getBRDFLUT()); + + break; + } + + _impl->currentShader = shader; // for external access + obj->getMesh()->render(); + } + } + + // Render the shadow maps for each cascade by rendering the scene from the light's perspective into the depth textures + void SimManager::ShadowPass(scene::Camera* cam) { + float nearPlane = cam->getNear(); + float farPlane = cam->getFar(); + + float cascadeNear[NUM_CASCADES]{}; + float cascadeFar[NUM_CASCADES]{}; + + cascadeNear[0] = nearPlane; + cascadeFar[0] = nearPlane + _cascadeSplits[0] * (farPlane); + + cascadeNear[1] = cascadeFar[0]; + cascadeFar[1] = nearPlane + _cascadeSplits[1] * (farPlane); + + glEnable(GL_POLYGON_OFFSET_FILL); + glPolygonOffset(2.0f, 4.0f); + + for (int i = 0; i < NUM_CASCADES; i++) { + _impl->_lightSpaceMatrixCascade[i] = LightSpaceMatrix(cam, cascadeNear[i], cascadeFar[i]); + + // set viewport to shadow map size + int baseRes = _settingsCurrent.shadowMapRes; + int res = (i == 0) ? baseRes : (baseRes / 2); // 4096, 2048, 1024, 512, 256, 128 + glViewport(0, 0, res, res); + + // render to cascade FBO + glBindFramebuffer(GL_FRAMEBUFFER, _impl->_cascadeFBO[i]); + glClear(GL_DEPTH_BUFFER_BIT); + + // render scene from light's point of view + _impl->_shadowShader->use(); + _impl->_shadowShader->setMat4(_impl->_lightSpaceMatrixCascade[i], "lightSpaceMatrix"); + + // main mesh + for (auto& obj : _impl->_objects) { + if (!obj || !obj->getMesh()) continue; + + glm::mat4 model = obj->transform.toMatrix() * obj->getMesh()->localTransform; + + _impl->_shadowShader->setMat4(model, "model"); + obj->getMesh()->render(); + } + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, (int)_internalSize.x, (int)_internalSize.y); + + glDisable(GL_POLYGON_OFFSET_FILL); + } + + // Compute the light-space matrix for a given camera frustum slice (cascade) + glm::mat4 SimManager::LightSpaceMatrix(scene::Camera* cam, float nearPlane, float farPlane) { + std::array corners = cam->getFrustumCornersWorldSpace(nearPlane, farPlane); + + glm::vec3 lightDir = glm::normalize(_impl->_light->getDirection()); + + // Fake camera position far along direction + glm::vec3 lightPos = -lightDir * 50.0f; + + glm::mat4 lightView = glm::lookAt( + lightPos, + glm::vec3(0.0f), + glm::vec3(0, 1, 0) + ); + + float minX = FLT_MAX, maxX = -FLT_MAX; + float minY = FLT_MAX, maxY = -FLT_MAX; + float minZ = FLT_MAX, maxZ = -FLT_MAX; + + for (auto& corner : corners) { + glm::vec4 trf = lightView * glm::vec4(corner); + minX = std::min(minX, trf.x); + maxX = std::max(maxX, trf.x); + minY = std::min(minY, trf.y); + maxY = std::max(maxY, trf.y); + minZ = std::min(minZ, trf.z); + maxZ = std::max(maxZ, trf.z); + } + + // Compute cascade center in light space + glm::vec3 center = { + 0.5f * (minX + maxX), + 0.5f * (minY + maxY), + 0.5f * (minZ + maxZ) + }; + + // Cascade radius (half-size of the bounding sphere) + float radius = glm::length(glm::vec3(maxX - minX, maxY - minY, 0.0f)) * 0.5f; + + int shadowMapResolution = _settingsCurrent.shadowMapRes; + + // The size of one texel in world-space + float worldUnitsPerTexel = (radius * 2.0f) / shadowMapResolution; + + // Snap X and Y (Z never snapped) + center.x = std::floor(center.x / worldUnitsPerTexel) * worldUnitsPerTexel; + center.y = std::floor(center.y / worldUnitsPerTexel) * worldUnitsPerTexel; + + // Recompute min/max using snapped centre + minX = center.x - radius; + maxX = center.x + radius; + minY = center.y - radius; + maxY = center.y + radius; + + glm::mat4 lightProj = glm::ortho(minX, maxX, minY, maxY, minZ - 20.0f, maxZ + 20.0f); + + return lightProj * lightView; + } + + // Render the skybox using the IBL environment cubemap + void SimManager::SkyboxRender(scene::Camera* cam) { + glm::mat4 view = cam->getViewMatrix(); + glm::mat4 projection = cam->getProjection(); + + _impl->_skybox->setEnvironmentTexture(_impl->_ibl->getEnvCubemap()); + _impl->_skybox->render(projection, view); + } + + // Load a new HDR environment map for IBL + std::string SimManager::getDefaultHDR() const { return (paths::assets() / "hdr" / "default_white.hdr").string(); } + + // Load a new HDR environment map for IBL + const shaders::Shader* SimManager::getCurrentShader() const { return _impl->currentShader; } + void SimManager::applyRenderSettings(const render::RenderSettings& s, render::ResolutionPreset r) { applyRenderProfile(s, r); } + + void SimManager::applyRenderProfile(const render::RenderSettings& s, render::ResolutionPreset r) { + const bool first = !_settingsValid; + + const bool shadowResChanged = first || (s.shadowMapRes != _settingsCurrent.shadowMapRes); + const bool msaaChanged = first || (s.msaaSamples != _settingsCurrent.msaaSamples); + const bool renderScaleChanged = first || (s.renderScale != _settingsCurrent.renderScale); + const bool presetChanged = first || (r != _resCurrent); + + if (shadowResChanged) { + InitShadowResource(s.shadowMapRes); + } + + _settingsCurrent = s; + _resCurrent = r; + + if (msaaChanged || renderScaleChanged || presetChanged) { + for (auto& v : _impl->_views) { v.w = v.h = 0; v.displayW = v.displayH = 0; } + } + + glm::vec2 px = getPresetResolutionPx(); + _internalSize = px; + for (auto& v : _impl->_views) { v.w = v.h = 0; } // internal invalidation + + //LOG_INFO("Render settings applied: resPreset=%d shadowRes=%d msaa=%d renderScale=%.2f", (int)r, _settingsCurrent.shadowMapRes, _settingsCurrent.msaaSamples, _settingsCurrent.renderScale); + D_RUNTIME("Render settings applied: resPreset=%d shadowRes=%d msaa=%d renderScale=%.2f", (int)r, _settingsCurrent.shadowMapRes, _settingsCurrent.msaaSamples, _settingsCurrent.renderScale); + + _settingsValid = true; + } + + // Get the pixel dimensions for the current resolution preset + glm::vec2 SimManager::getPresetResolutionPx() const { + switch (_resCurrent) { + case render::ResolutionPreset::R_720p: return { 1280, 720 }; + case render::ResolutionPreset::R_1080p: return { 1920, 1080 }; + case render::ResolutionPreset::R_1440p: return { 2560, 1440 }; + case render::ResolutionPreset::R_4K: return { 3840, 2160 }; + default: return { 1920, 1080 }; + } + } + + glm::vec2 SimManager::getInternalResolutionSizePx() const { + return getPresetResolutionPx(); // no scale + } + + void SimManager::resetHDRToPreset() { + _hdrUserOverride = false; + const std::string hdr = getDefaultHDR(); + if (hdr != _activeHDRPath) { loadNewHDR(hdr); } + } + + void SimManager::reloadAllShaders() { + _impl->_shaderBasic->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_basic.frag.glsl").string()); + _impl->_shaderLit->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_lit.frag.glsl").string()); + _impl->_shaderPBR->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_pbr.frag.glsl").string()); + _impl->_ssaoShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "ssao.frag.glsl").string()); + _impl->_ssaoBlurShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "ssao_blur.frag.glsl").string()); + + //LOG_INFO("All shaders reloaded from disk."); + D_INFO_ONCE("All shaders reloaded from disk."); + } + + void SimManager::setLightColour(const glm::vec3& colour) { _impl->_light->_colour = colour; } +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp new file mode 100644 index 00000000..ff40df7d --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp @@ -0,0 +1,77 @@ +// DSFE_GUI SimRobots.cpp +#include "Scene/SimulationManager.h" +#include "Scene/SimulationCore.h" +#ifdef __gl_h_ +#undef __gl_h_ +#endif +#include "Manager/SimImplementation.h" + +namespace gui { + // Load a robot by name from the robot system + void SimManager::loadRobot(const std::string& name) { + // Clear any existing robot first + if (hasRobot()) { clearRobot(); } + _bodyLoaded = true; + + setSelectedObject(nullptr); + detachCameraFromObject(); + clearViewFollowTarget(gui::ViewID::Follow); + _impl->eeFollowBound = false; + _impl->eeObject = nullptr; + + if (!_impl->_robotSystem) { return; } + _core->loadRobotInternal(name); + + size_t startIdx = _impl->_objects.size(); + + _impl->buildRobotPresentationFromModel(_impl->_robotSystem->model(), *this); + + _impl->_robotRenderer->applyTransforms( + _impl->_robotSystem->model(), + _impl->_robotSystem->worldTransforms() + ); + + if (auto* simInteg = _impl->_robotSystem->getIntegrator()) { + simInteg->resetAdaptiveState(); + } + + // Attempt to find an end-effector candidate among the newly added objects and bind the Follow view to it + scene::Object* ee = _impl->findEndEffectorFromRange(startIdx); + if (ee) { + _impl->eeObject = ee; + setViewFollowTarget(gui::ViewID::Follow, ee, glm::vec3(0.0f, 0.2f, 0.6f)); + _impl->eeFollowBound = true; + LOG_INFO("Follow view bound to end-effector candidate: %s", ee->name.c_str()); + } + } + void SimManager::setRobotLinkRotation(const std::string& linkName, double angle) { + if (_impl->_robotSystem) { _impl->_robotSystem->setRobotLinkRotation(linkName, angle); } + } + void SimManager::setRobotRootPose(const Vec3& pos, Quat& rot) { + if (_impl->_robotSystem) { _impl->_robotSystem->setRobotRootPose(pos, rot); } + } + void SimManager::setRobotRootHome(const Vec3& pos, Quat& rot) { + if (_impl->_robotSystem) { _impl->_robotSystem->setRobotRootHome(pos, rot); } + } + void SimManager::resetRobot() { + if (_impl->_robotSystem) { _impl->_robotSystem->resetRobot(); } + } + void SimManager::clearRobot() { + setSelectedObject(nullptr); // deselect any selected object + _impl->clearRobotPresentation(); + _bodyLoaded = false; + + clearViewFollowTarget(gui::ViewID::Follow); + _impl->eeFollowBound = false; + _impl->eeObject = nullptr; + } + const bool SimManager::hasRobot() const { return _impl->_robotSystem && _impl->_robotSystem->hasRobot(); } + + // Access the robot system (non-const and const versions) + robots::RobotSystem* SimManager::robotSystem() { return _core->robotSystem(); } + const robots::RobotSystem* SimManager::robotSystem() const { return _core->robotSystem(); } + + // Access the trajectory manager (non-const and const versions) + control::TrajectoryManager* SimManager::traj() { return _core->trajectoryManager(); } + const control::TrajectoryManager* SimManager::traj() const { return _core->trajectoryManager(); } +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp new file mode 100644 index 00000000..addd2a6b --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp @@ -0,0 +1,180 @@ +// DSFE_GUI SimUI.cpp +#include "Scene/SimulationManager.h" +#ifdef __gl_h_ +#undef __gl_h_ +#endif +#include "Manager/SimImplementation.h" + +#include + +namespace gui { + // Main Dockspace with Menu Bar + void SimManager::drawMainDockspace() { + ImGuiWindowFlags flags = + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoNavFocus; + + const ImGuiViewport* vp = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(vp->Pos); + ImGui::SetNextWindowSize(vp->Size); + ImGui::SetNextWindowViewport(vp->ID); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + + // Must be a window so DockSpace has somewhere to live + ImGui::Begin("##MainDockspace", nullptr, flags); + + ImGui::PopStyleVar(2); + + beginSimManager("##MainDockspaceChild"); + + ImGuiID dock_id = ImGui::GetID("MainDockspaceID"); + ImGui::DockSpace(dock_id, ImVec2(0, 0), ImGuiDockNodeFlags_PassthruCentralNode); + + endSimManager(); + + ImGui::End(); + } + + // Viewport Window + void SimManager::drawViewportWindow() { + ImGui::Begin("Viewport", nullptr, + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse); + + beginSimManager("##ViewportBody"); + + // --- Tabs: Single / Quad --- + if (ImGui::BeginTabBar("ViewportTabs", ImGuiTabBarFlags_None)) { + const bool singleSelected = ImGui::BeginTabItem("Single"); + if (singleSelected) { + // Restore to Manual view when switching back from Quad + if (_impl->viewMode == Impl::ViewMode::Quad) { + _impl->activeView = gui::ViewID::Manual; + } + _impl->viewMode = Impl::ViewMode::Single; + ImGui::EndTabItem(); + } + + const bool quadSelected = ImGui::BeginTabItem("Quad"); + if (quadSelected) { + _impl->viewMode = Impl::ViewMode::Quad; + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + // Everything below tabs is render output + _isHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); + + ImVec2 panel = ImGui::GetContentRegionAvail(); + + // Pixel size (framebuffer coords) + int vpW = (int)(panel.x); + int vpH = (int)(panel.y); + vpW = std::max(1, vpW); + vpH = std::max(1, vpH); + + // Update DISPLAY size only + if ((int)_displaySize.x != vpW || (int)_displaySize.y != vpH) { + _displaySize = { (float)vpW, (float)vpH }; + + // invalidate only display cached sizes so post buffers resize + for (auto& v : _impl->_views) { + v.displayW = 0; + v.displayH = 0; + } + //LOG_INFO("Viewport display size updated to %dx%d", vpW, vpH); + } + + // --- Render + Present --- + if (_impl->viewMode == Impl::ViewMode::Quad) { + int halfW = std::max(1, vpW / 2); + int halfH = std::max(1, vpH / 2); + + _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Top], halfW, halfH); + _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Front], halfW, halfH); + _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Right], halfW, halfH); + _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Follow], halfW, halfH); + + // Stable 2x2 layout + ImVec2 avail = ImGui::GetContentRegionAvail(); + ImVec2 cell = ImVec2(avail.x * 0.5f, avail.y * 0.5f); + + // Helper to draw each cell with the same pattern + auto drawCell = [&](const char* childId, gui::ViewID id, bool sameLine) { + if (sameLine) ImGui::SameLine(); + ImGui::BeginChild(childId, cell, false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + + auto& v = _impl->_views[(size_t)id]; + ImVec2 inner = ImGui::GetContentRegionAvail(); + + ImGui::Image((ImTextureID)(intptr_t)v.post->getTexture(), inner, ImVec2(0, 1), ImVec2(1, 0)); + + // Set active view when clicking inside the quad cell + if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + _impl->activeView = id; + } + + // Handle scroll zoom on hovered quad cell + if (ImGui::IsWindowHovered()) { + float scrollY = ImGui::GetIO().MouseWheel; + if (scrollY != 0.0f) { + v.cam->onMouseWheel((double)scrollY); + } + } + + // Visual indication: draw a border around the active cell + if (_impl->activeView == id) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 p0 = ImGui::GetWindowPos(); + ImVec2 p1 = ImVec2(p0.x + ImGui::GetWindowSize().x, p0.y + ImGui::GetWindowSize().y); + dl->AddRect(p0, p1, IM_COL32(255, 200, 0, 180), 4.0f, 0, 2.0f); + } + + ImGui::EndChild(); + }; + + drawCell("##Top", gui::ViewID::Top, false); + drawCell("##Front", gui::ViewID::Front, true); + drawCell("##Right", gui::ViewID::Right, false); + drawCell("##Follow", gui::ViewID::Follow, true); + } + else { + auto& v = _impl->_views[static_cast(_impl->activeView)]; + _impl->renderView(*this, v, vpW, vpH); + + ImGui::Image((ImTextureID)(intptr_t)v.post->getTexture(), panel, ImVec2(0, 1), ImVec2(1, 0)); + } + + endSimManager(); + + ImGui::End(); + } + + // Begin Control Panel Helper + void SimManager::beginSimManager(const char* id) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 5.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 8.0f)); + + ImGui::BeginChild(id, ImVec2(0, 0), true, + ImGuiChildFlags_AlwaysUseWindowPadding | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse); + } + + // End Control Panel Helper + void SimManager::endSimManager() { + ImGui::EndChild(); + ImGui::PopStyleVar(4); + } +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimViewport.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimViewport.cpp new file mode 100644 index 00000000..71c13e70 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimViewport.cpp @@ -0,0 +1,10 @@ +// DSFE_GUI SimViewport.cpp +#include "Scene/SimulationManager.h" +#ifdef __gl_h_ +#undef __gl_h_ +#endif +#include "Manager/SimImplementation.h" + +namespace gui { + +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp index b696bf40..6d0279c7 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp @@ -14,40 +14,8 @@ extern "C" void DestroySimulationCore(core::ISimulationCore*); #endif #include #include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "Scene/Input.h" -#include "Scene/Camera.h" -#include "Scene/Mesh.h" -#include "Assets/MeshLoader.h" -#include "Scene/Light.h" -#include "Scene/AxisOrientator.h" - -#include "Robots/RobotSystem.h" -#include "Robots/RobotModel.h" -#include "Robots/TrajectoryManager.h" - -#include "Interpreter/IStoredProgram.h" -#include "Interpreter/StoredProgram.h" -#include "Interpreter/Parser.h" - -#include "Rendering/SkyboxRenderer.h" -#include "Rendering/ShaderUtil.h" -#include "Rendering/OpenGLBufferManager.h" -#include "Rendering/IBL.h" -#include "Rendering/Texture.h" - -#include "Robots/RobotPresentationBuilder.h" -#include "Robots/RobotRenderer.h" +#include "Manager/SimImplementation.h" #include @@ -96,579 +64,6 @@ namespace gui { return g; // (4x4) } - // --- PIMPL Implementation --- - struct SimManager::Impl { - // Completed Simulation Runs (thread-safe) - std::mutex _completedRunsMutex; - std::vector _completedRuns; - - // View ID Alias - using VID = gui::ViewID; - - // View Modes - enum class ViewMode { Single, Quad }; - - // Current View Mode - ViewMode viewMode = ViewMode::Single; - - // Viewport Structure - struct Viewport { - std::unique_ptr cam; - std::unique_ptr fb; - std::unique_ptr post; - - // Cache size - int w = 1, h = 1; - int displayW = 1, displayH = 1; - - // Follow Target - scene::Object* followTarget = nullptr; - glm::vec3 followOffset = glm::vec3(0.0f, 0.25f, 1.0f); // tweak - bool followEnabled = false; - }; - - // Viewports - std::array _views; - VID activeView = VID::Manual; - - // Skybox & IBL - std::unique_ptr _ibl; - std::unique_ptr _skybox; - - // Post-Processing Shader - std::unique_ptr _postShader; - std::shared_ptr _shaderBasic; - std::shared_ptr _shaderLit; - std::shared_ptr _shaderPBR; - - // World Grid & Shadow Shaders - std::unique_ptr _worldGridShader; - std::unique_ptr _shadowShader; - shaders::Shader* currentShader; - - // Fullscreen Quad VAO - GLuint _fullscreenVAO = 0; - GLuint _worldGridVAO = 0; - - // Shadow Mapping (Cascaded) - GLuint _cascadeFBO[SimManager::NUM_CASCADES]{}; - GLuint _cascadeDepth[SimManager::NUM_CASCADES]{}; - glm::mat4 _lightSpaceMatrixCascade[SimManager::NUM_CASCADES] = {}; - - // Scene Objects - std::unique_ptr _light; - std::unique_ptr _axisOrientator; - - scene::Object* _selectedObject = nullptr; - scene::Object* _cameraFollowTarget = nullptr; - - std::shared_ptr _mesh; - std::vector> _objects; - std::unordered_map> _linkToObjects; - std::unordered_map _primaryLinkObject; - - // Robot System - std::unique_ptr _robotSystem; // simulation - - // Robot Rendering - std::unique_ptr _robotRenderer; // rendering - RobotRenderBinding _currentBinding; // current render binding - - // Robot Follow Target - bool eeFollowBound = false; - scene::Object* eeObject = nullptr; - - // Trajectory Manager - control::TrajectoryManager _traj; - - // SSAO Resources - std::unique_ptr _ssaoShader; - std::unique_ptr _ssaoBlurShader; - GLuint _ssaoFBO = 0, _ssaoTex = 0; - GLuint _ssaoBlurFBO = 0, _ssaoBlurTex = 0; - GLuint _ssaoNoiseTex = 0; - int _ssaoW = 0, _ssaoH = 0; - std::vector _ssaoKernel; - - Impl(SimManager& owner) { - _postShader = std::make_unique(); - _postShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "post.frag.glsl").string()); - - glGenVertexArrays(1, &_fullscreenVAO); - - // Lambda to create views - auto makeView = [&](ViewID id, glm::vec3 pos, float fovDeg, glm::vec3 target, glm::vec3 /*upHint*/) { - auto& v = _views[(size_t)id]; - - // INTERNAL render target size (scene render) - const int rtW = std::max(1, (int)owner._internalSize.x); - const int rtH = std::max(1, (int)owner._internalSize.y); - - // DISPLAY size (final post-process target) - const int dispW = std::max(1, (int)owner._displaySize.x); - const int dispH = std::max(1, (int)owner._displaySize.y); - const int postW = (dispW > 1 && dispH > 1) ? dispW : rtW; - const int postH = (dispW > 1 && dispH > 1) ? dispH : rtH; - - // Cache sizes - v.w = rtW; - v.h = rtH; - v.displayW = postW; - v.displayH = postH; - - // Framebuffers - v.fb = std::make_unique(); - v.fb->createBuffers(rtW, rtH, std::max(1, owner._settingsCurrent.msaaSamples)); - - v.post = std::make_unique(); - v.post->createBuffers(postW, postH, 1); - - // Camera aspect should match DISPLAY (what you're presenting in ImGui) - v.cam = std::make_unique(pos, fovDeg, (float)postW / (float)postH, 0.1f, 5000.0f); - v.cam->setFocus(target); - v.cam->updateViewMatrix(); - }; - - glm::vec3 target(0.0f); - - // Perspective - makeView(ViewID::Manual, { 0.0f, 0.5f, 1.0f }, 60.0f, target, { 0.0f, 1.0f, 0.0f }); // Default - makeView(ViewID::Follow, { 3.0f, 0.25f, 0.0f }, 20.0f, target, { 0.0f, 1.0f, 0.0f }); // Follow - // Ortho-ish - makeView(ViewID::Top, { 0.0f, 2.0f, 0.0f }, 20.0f, target, { 0.0f, 0.0f, -1.0f }); // Top - makeView(ViewID::Right, { 3.0f, 0.25f, 0.0f }, 20.0f, target, { 0.0f, 1.0f, 0.0f }); // Right - makeView(ViewID::Front, { 0.0f, 0.25f, 3.0f }, 20.0f, target, { 0.0f, -1.0f, 0.0f }); // Front - - { - // Top - auto* camTop = _views[(size_t)ViewID::Top].cam.get(); - camTop->setFocus(target); - camTop->setYaw(-glm::half_pi()); - camTop->setPitch(-glm::half_pi() + 0.001f); - camTop->setOrbitDistance(2.0f); - camTop->setMinDistance(0.5f); - camTop->updateViewMatrix(); - - // Right - auto* camRight = _views[(size_t)ViewID::Right].cam.get(); - camRight->setFocus(target); - camRight->setYaw(glm::pi()); - camRight->setPitch(0.0f); - camRight->setOrbitDistance(3.0f); - camRight->setMinDistance(0.5f); - camRight->updateViewMatrix(); - - // Front - auto* camFront = _views[(size_t)ViewID::Front].cam.get(); - camFront->setFocus(target); - camFront->setYaw(-glm::half_pi()); - camFront->setPitch(0.0f); - camFront->setOrbitDistance(3.0f); - camFront->setMinDistance(0.5f); - camFront->updateViewMatrix(); - - // Follow - auto* camFollow = _views[(size_t)ViewID::Follow].cam.get(); - camFollow->setFocus(target); - camFollow->setYaw(glm::pi()); - camFollow->setPitch(0.0f); - camFollow->setOrbitDistance(3.0f); - camFollow->setMinDistance(0.5f); - camFollow->updateViewMatrix(); - } - - // Shader Types A - // Basic shader with no lighting - _shaderBasic = std::make_shared(); - _shaderBasic->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_basic.frag.glsl").string()); - // Lit shader with simple Blinn-Phong lighting - _shaderLit = std::make_shared(); - _shaderLit->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_lit.frag.glsl").string()); - // PBR shader with full Physically Based Rendering (for release visuals) - _shaderPBR = std::make_shared(); - _shaderPBR->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_pbr.frag.glsl").string()); - - currentShader = _shaderPBR.get(); - _skybox = std::make_unique(); - - // Shader Types B - _worldGridShader = std::make_unique(); - _worldGridShader->load((paths::assets() / "shaders" / "world_grid.vert.glsl").string(), (paths::assets() / "shaders" / "world_grid.frag.glsl").string()); - - _shadowShader = std::make_unique(); - _shadowShader->load((paths::assets() / "shaders" / "shadow_depth.vert.glsl").string(), (paths::assets() / "shaders" / "shadow_depth.frag.glsl").string()); - - // Light - _light = std::make_unique(); - _light->_isDirectional = true; - - // Axis Orientator - _axisOrientator = std::make_unique(); - - // World Grid VAO - glGenVertexArrays(1, &_worldGridVAO); - - // Test Mesh - _mesh = std::make_shared(); - _mesh->init(); - - // Robot system with mesh loading (for normal simulation) - _robotSystem = std::make_unique(); - - _robotRenderer = std::make_unique(); - - // SSAO shaders - _ssaoShader = std::make_unique(); - _ssaoShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "ssao.frag.glsl").string()); - - _ssaoBlurShader = std::make_unique(); - _ssaoBlurShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "ssao_blur.frag.glsl").string()); - - // Generate hemisphere kernel - initSSAOKernel(64); - - // Generate noise texture (4x4 random rotation vectors) - initSSAONoise(); - } - - void clearRobotPresentation() { - _primaryLinkObject.clear(); - _linkToObjects.clear(); - _objects.clear(); - } - - scene::Object* primaryObjectForLink(const std::string& linkName) { - auto it = _primaryLinkObject.find(linkName); - if (it == _primaryLinkObject.end()) { return nullptr; } - return it->second; - } - - void buildRobotPresentationFromModel(const robots::RobotModel& model, SimManager& owner) { - clearRobotPresentation(); - - RobotPresentationBuilder builder; - RobotRenderBinding binding = builder.build(model); - - for (auto& owned : binding.ownedObjects) { - _objects.push_back(std::move(owned)); - } - - _robotRenderer->bind(binding); - - for (const auto& [linkName, visuals] : binding.linkVisuals) { - auto& target = _linkToObjects[linkName]; - - for (auto* obj : visuals) { - if (!obj) { continue; } - - target.push_back(obj); - - if (!_primaryLinkObject.contains(linkName)) { - _primaryLinkObject[linkName] = obj; - } - } - } - } - - // Random number generation for SSAO kernel and noise - std::mt19937 _rng{ std::random_device{}() }; - std::uniform_real_distribution _uni{ -1.0f, 1.0f }; - - // Helper to get random float in [a,b] - inline float rngFloat(float a, float b) { - std::uniform_real_distribution d(a, b); - return d(_rng); - } - - // Initialises the SSAO kernel with random samples in a hemisphere oriented along the positive Z axis, scaled to favor samples closer to the origin - void initSSAOKernel(int size) { - _ssaoKernel.clear(); - _ssaoKernel.reserve(size); - for (int i = 0; i < size; ++i) { - glm::vec3 sample( - rngFloat(-1.0f, 1.0f), - rngFloat(-1.0f, 1.0f), - rngFloat(0.0f, 1.0f) // hemisphere: z in [0,1] - ); - sample = glm::normalize(sample); - sample *= rngFloat(0.0f, 1.0f); - float scale = (float)i / (float)size; - scale = 0.1f + scale * scale * 0.9f; - sample *= scale; - _ssaoKernel.push_back(sample); - } - } - - // Initializes the SSAO noise texture with random rotation vectors in the XY plane - void initSSAONoise() { - std::vector noise(16); - // Generate 16 random rotation vectors in the XY plane (Z=0) - for (int i = 0; i < 16; ++i) { - noise[i] = glm::vec3( - rngFloat(-1.0f, 1.0f), - rngFloat(-1.0f, 1.0f), - 0.0f - ); - } - // Create OpenGL texture - glGenTextures(1, &_ssaoNoiseTex); - glBindTexture(GL_TEXTURE_2D, _ssaoNoiseTex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 4, 4, 0, GL_RGB, GL_FLOAT, noise.data()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - } - - // Ensures SSAO FBOs and textures are created and match the given size, recreating them if necessary - void ensureSSAOBuffers(int w, int h) { - if (_ssaoW == w && _ssaoH == h) return; - _ssaoW = w; _ssaoH = h; - // Delete old buffers if they exist - if (_ssaoFBO) { glDeleteFramebuffers(1, &_ssaoFBO); glDeleteTextures(1, &_ssaoTex); } - if (_ssaoBlurFBO) { glDeleteFramebuffers(1, &_ssaoBlurFBO); glDeleteTextures(1, &_ssaoBlurTex); } - // Lambda to create a single-channel floating point FBO and texture - auto makeSingleChannelFBO = [](GLuint& fbo, GLuint& tex, int w, int h) { - glGenFramebuffers(1, &fbo); - glGenTextures(1, &tex); - glBindFramebuffer(GL_FRAMEBUFFER, fbo); - glBindTexture(GL_TEXTURE_2D, tex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, w, h, 0, GL_RED, GL_FLOAT, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - }; - // Create SSAO and blur FBOs/textures - makeSingleChannelFBO(_ssaoFBO, _ssaoTex, w, h); - makeSingleChannelFBO(_ssaoBlurFBO, _ssaoBlurTex, w, h); - } - - // Renders the SSAO pass and subsequent blur pass, writing results to SSAO FBOs. Should be called after rendering the scene to the view's framebuffer (to provide depth texture input). - void renderSSAO(SimManager& owner, Viewport& v) { - if (!owner._settingsCurrent.ssao) return; - // Determine SSAO buffer size based on division factor - int ssaoDiv = std::max(1, owner._settingsCurrent.ssaoResDiv); - int ssaoW = std::max(1, v.w / ssaoDiv); - int ssaoH = std::max(1, v.h / ssaoDiv); - ensureSSAOBuffers(ssaoW, ssaoH); - // Clamp kernel size to available samples - int kernelSize = std::min((int)_ssaoKernel.size(), owner._settingsCurrent.ssaoSamples); - // SSAO Pass - glBindFramebuffer(GL_FRAMEBUFFER, _ssaoFBO); - glViewport(0, 0, ssaoW, ssaoH); - glClear(GL_COLOR_BUFFER_BIT); - glDisable(GL_DEPTH_TEST); - glDisable(GL_BLEND); - - _ssaoShader->use(); - - // Depth texture from the resolved scene FBO - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, v.fb->getDepthTexture()); - _ssaoShader->setInt1(0, "gDepth"); - // Noise texture - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, _ssaoNoiseTex); - _ssaoShader->setInt1(1, "gNoise"); - - // Camera matrices and parameters - _ssaoShader->setMat4(v.cam->getProjection(), "projection"); - _ssaoShader->setMat4(glm::inverse(v.cam->getProjection()), "invProjection"); - _ssaoShader->setVec2(glm::vec2((float)ssaoW / 4.0f, (float)ssaoH / 4.0f), "noiseScale"); - _ssaoShader->setInt1(kernelSize, "kernelSize"); - _ssaoShader->setFlt1(0.25f, "radius"); - _ssaoShader->setFlt1(0.035f, "bias"); - _ssaoShader->setFlt1(owner._settingsCurrent.ssaoStrength, "strength"); - - // SSAO kernel samples - for (int i = 0; i < kernelSize; ++i) { _ssaoShader->setVec3(_ssaoKernel[i], "samples[" + std::to_string(i) + "]"); } - - glBindVertexArray(_fullscreenVAO); - glDrawArrays(GL_TRIANGLES, 0, 3); - - // Blur pass - glBindFramebuffer(GL_FRAMEBUFFER, _ssaoBlurFBO); - glViewport(0, 0, ssaoW, ssaoH); - glClear(GL_COLOR_BUFFER_BIT); - - // No need for depth test or blending for a simple fullscreen blur - _ssaoBlurShader->use(); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, _ssaoTex); - _ssaoBlurShader->setInt1(0, "ssaoInput"); - glDrawArrays(GL_TRIANGLES, 0, 3); - glBindVertexArray(0); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - } - - // Renders the given viewport to its framebuffer, handling dynamic resizing of the framebuffer and camera aspect ratio based on the provided display size - void renderView(SimManager& owner, Viewport& v, int displayW, int displayH) { - displayW = std::max(1, displayW); - displayH = std::max(1, displayH); - - // Internal render target size - glm::ivec2 rt(std::max(1, (int)owner._internalSize.x), std::max(1, (int)owner._internalSize.y)); - - // In quad mode, render at the cell's display resolution to avoid scaling artifacts - if (owner._impl->viewMode == Impl::ViewMode::Quad) { - rt.x = std::max(1, displayW); - rt.y = std::max(1, displayH); - } - const int rtW = std::max(1, (int)rt.x); - const int rtH = std::max(1, (int)rt.y); - - LOG_INFO_ONCE("Rendering Viewport: RT Size = %dx%d, Display Size = %dx%d", rtW, rtH, displayW, displayH); - - // Resize only when internal RT changes OR display changes (post buffer) - const bool rtChanged = (v.w != rtW) || (v.h != rtH); - const bool displayChanged = (v.displayW != displayW) || (v.displayH != displayH); - const bool msaaChanged = false; - - if (rtChanged || displayChanged || msaaChanged) { - // Store internal RT size - v.w = rtW; - v.h = rtH; - - // Store display size - v.displayW = displayW; - v.displayH = displayH; - - // Adjust MSAA based on internal RT size - int msaa = std::max(1, owner._settingsCurrent.msaaSamples); - - // Scene framebuffer: INTERNAL resolution (rtW x rtH) - v.fb->deleteBuffers(); - v.fb->createBuffers(rtW, rtH, msaa); - - // Post-processing framebuffer: DISPLAY resolution (displayW x displayH) - v.post->deleteBuffers(); - v.post->createBuffers(displayW, displayH, 1); - - // Camera aspect must match DISPLAY aspect - v.cam->setAspect((float)displayW / (float)displayH); - } - - v.fb->bind(); - glViewport(0, 0, v.w, v.h); - glEnable(GL_DEPTH_TEST); - glDepthMask(GL_TRUE); - glDepthFunc(GL_LESS); - - glClearColor(owner._backgroundColour.r, owner._backgroundColour.g, owner._backgroundColour.b, owner._backgroundAlpha); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - if (owner._settingsCurrent.msaaSamples > 1) { - glEnable(GL_MULTISAMPLE); - } - else { - glDisable(GL_MULTISAMPLE); - } - - // Update Follow Target: use the explicitly bound target (e.g. end-effector from loadRobot), - // only fall back to _selectedObject if no explicit target was set via setViewFollowTarget - if (&v == &_views[(size_t)gui::ViewID::Follow]) { - scene::Object* target = v.followEnabled ? v.followTarget : _selectedObject; - - if (target && target->getMesh()) { - glm::mat4 M = target->transform.toMatrix() * target->getMesh()->localTransform; - glm::vec3 worldPos = glm::vec3(M[3]); - v.cam->setFollowTarget(worldPos, target->transform.rotQ); - } - } - - // Skybox (renders before all opaque geometry, doesn't write depth) - if (owner.skyboxEnabled) { - glDepthMask(GL_FALSE); - glDepthFunc(GL_LEQUAL); - owner.SkyboxRender(v.cam.get()); - glDepthMask(GL_TRUE); - glDepthFunc(GL_LESS); - } - - // Meshes & World Grid - GLint sampleBuffers = 0, samples = 0; - glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers); - glGetIntegerv(GL_SAMPLES, &samples); - LOG_INFO_ONCE("FB MSAA state: GL_SAMPLE_BUFFERS=%d GL_SAMPLES=%d", sampleBuffers, samples); - - owner.MeshRender(v.cam.get()); - if (owner._settingsCurrent.grid) { owner.WorldGridRender(v.cam.get(), v.w); } - - v.fb->unbind(); - - // SSAO pass (reads resolved depth, writes to _ssaoBlurTex) - renderSSAO(owner, v); - - // Only valid if you allocated mip levels for _texID (via glTexStorage2D) - glBindTexture(GL_TEXTURE_2D, v.fb->getTexture()); - glGenerateMipmap(GL_TEXTURE_2D); - glBindTexture(GL_TEXTURE_2D, 0); - - // Post-Processing - v.post->bind(); - glDisable(GL_MULTISAMPLE); - glViewport(0, 0, v.displayW, v.displayH); - - glDisable(GL_DEPTH_TEST); - glDisable(GL_BLEND); - glClear(GL_COLOR_BUFFER_BIT); - - _postShader->use(); - _postShader->setInt1(0, "hdrScene"); - _postShader->setInt1(1, "ssaoTex"); - _postShader->setBool(owner._settingsCurrent.ssao, "ssaoEnabled"); - _postShader->setFlt1(owner._settingsCurrent.exposure, "exposure"); - _postShader->setFlt1(owner._settingsCurrent.whitePoint, "whitePoint"); - _postShader->setVec2(glm::vec2(v.w, v.h), "uRes"); - - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, v.fb->getTexture()); - - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, owner._settingsCurrent.ssao ? _ssaoBlurTex : 0); - - glBindVertexArray(_fullscreenVAO); - glDrawArrays(GL_TRIANGLES, 0, 3); - if (owner._settingsCurrent.axisOrientator) { _axisOrientator->render(v.cam->getViewMatrix()); } - glBindVertexArray(0); - - v.post->unbind(); - } - - static bool icontains(const std::string& s, const char* sub) { - if (sub == nullptr || *sub == '\0') { return false; } - - // Case-insensitive search using std::search with a custom comparator - auto it = std::search( - s.begin(), s.end(), - sub, sub + std::strlen(sub), - [](char a, char b) { - return std::tolower((unsigned char)a) == std::tolower((unsigned char)b); - } - ); - return it != s.end(); - } - - scene::Object* findEndEffectorFromRange(size_t startIdx) { - for (size_t i = startIdx; i < _objects.size(); ++i) { - scene::Object* o = _objects[i].get(); - if (!o) continue; - - const std::string& n = o->name; - if (icontains(n, "end") || icontains(n, "eff") || icontains(n, "ee") || icontains(n, "tool") || icontains(n, "tcp") || icontains(n, "gripper")) { - return o; - } - } - - // Fallback: last object added (usually the last link) - if (_objects.size() > startIdx) return _objects.back().get(); - return nullptr; - } - }; - // Helper: create CorePtr (unique_ptr with std::function deleter) static CorePtr makeCoreFactory() { core::ISimulationCore* raw = CreateSimulationCore_v1(); @@ -726,11 +121,6 @@ namespace gui { if (_impl && _impl->_mesh) { _impl->_mesh->clean(); } } - // Helper to get the next ObjectID - static inline scene::ObjectID next(scene::ObjectID id) { - return static_cast(static_cast(id) + 1); - } - // -------------------------------------------------- // THREAD-SAFE SIMULATION RESULTS // -------------------------------------------------- @@ -763,287 +153,6 @@ namespace gui { return copy; } - - // -------------------------------------------------- - // LIGHT & SKYBOX - // -------------------------------------------------- - scene::Light* SimManager::getLight() { return _impl->_light.get(); } - - void SimManager::loadNewHDR(const std::string& path) { - D_INFO("Loading new HDR: %s", path.c_str()); - - // Make sure IBL system exists - if (!_impl->_ibl) { - LOG_ERROR("Cannot load HDR because IBL system is not initialised."); - D_FAIL("Cannot load HDR because IBL system is not initialised."); - return; - } - - _impl->_ibl->init(path); // rebuild envCubemap, irradiance, prefilter, brdfLUT - D_SUCCESS("IBL rebuilt successfully."); - - // Update skybox - _impl->_skybox->setEnvironmentTexture(_impl->_ibl->getEnvCubemap()); - - _activeHDRPath = path; - - D_SUCCESS("Loaded HDR successfully."); - } - - void SimManager::loadNewHDR_UI(const std::string& path) { - loadNewHDR(path); - _hdrUserOverride = true; - } - - void SimManager::loadNewHDR_Preset(const std::string& path) { - loadNewHDR(path); - _hdrUserOverride = false; - } - - // -------------------------------------------------- - // CONTROL MODES & CAMERA - // -------------------------------------------------- - - // Get the active view camera - scene::Camera* SimManager::getCamera() { return _impl->_views[static_cast(_impl->activeView)].cam.get(); } - // Reset the active view camera to default position - void SimManager::resetView() { - auto& v = _impl->_views[static_cast(_impl->activeView)]; - - glm::vec3 pos = { 0.0f, 0.25f, 1.0f }; - float fov = 60.0f; - - switch (_impl->activeView) { - case gui::ViewID::Top: pos = { 0, 5, 0 }; fov = 20.0f; break; - case gui::ViewID::Right: pos = { 5, 0, 0 }; fov = 20.0f; break; - case gui::ViewID::Front: pos = { 0, 0, 5 }; fov = 20.0f; break; - case gui::ViewID::Follow: fov = 20.0f; break; - case gui::ViewID::Manual: fov = 20.0f; break; - default: break; - } - - float aspect = (float)std::max(1, v.w) / (float)std::max(1, v.h); - v.cam = std::make_unique(pos, fov, aspect, 0.1f, 5000.0f); - - v.cam->setFocus(glm::vec3(0.0f)); - - switch (_impl->activeView) { - case gui::ViewID::Top: - v.cam->setYaw(-glm::half_pi()); - v.cam->setPitch(-glm::half_pi() + 0.001f); - break; - case gui::ViewID::Right: - v.cam->setYaw(glm::pi()); - v.cam->setPitch(0.0f); - break; - case gui::ViewID::Front: - v.cam->setYaw(-glm::half_pi()); - v.cam->setPitch(0.0f); - break; - default: - break; - } - - v.cam->updateViewMatrix(); - } - - // Attach camera to an object and start following it. The camera will maintain a fixed offset from the object's position and orientation. - void SimManager::attachCameraToObject(scene::Object* obj) { - if (!obj) return; - scene::Camera* cam = _impl->_views[static_cast(_impl->activeView)].cam.get(); - - _impl->_cameraFollowTarget = obj; - - glm::vec3 pos = obj->transform.position; - glm::quat rot = obj->transform.rotQ; - - cam->startFollow(pos, rot, glm::vec3(0, 2, 5)); - } - - // Detach camera from any object and stop following - void SimManager::detachCameraFromObject() { - scene::Camera* cam = _impl->_views[static_cast(_impl->activeView)].cam.get(); - _impl->_cameraFollowTarget = nullptr; - cam->clearFollow(); - } - - // Set the follow target for a specific view - void SimManager::setViewFollowTarget(ViewID view, scene::Object* obj, const glm::vec3& offset) { - if (view < ViewID::Manual || view >= ViewID::COUNT) { return; } - auto& v = _impl->_views[static_cast(view)]; - v.followTarget = obj; - v.followOffset = offset; - v.followEnabled = (obj != nullptr); - - if (v.followEnabled && v.cam && obj) { - v.cam->startFollow(obj->transform.position, obj->transform.rotQ, offset); - } - } - - // Clear the follow target for a specific view - void SimManager::clearViewFollowTarget(ViewID view) { - if (view < ViewID::Manual || view >= ViewID::COUNT) { return; } - auto& v = _impl->_views[static_cast(view)]; - v.followTarget = nullptr; - v.followEnabled = false; - if (v.cam) { v.cam->clearFollow(); } - } - - // Convenience for Follow view: set the follow target to the object attached to a robot joint (e.g. end-effector) - bool SimManager::setViewFollowRobotJoint(ViewID view, const std::string& jointName, const glm::vec3& offset) { - if (!hasRobot()) { - LOG_WARN("setViewFollowRobotJoint: no robot loaded"); - return false; - } - - robots::RobotSystem* rs = robotSystem(); - if (!rs) return false; - - auto& joints = rs->joints(); - auto& links = rs->links(); - - // 1) Find joint by name - const robots::RobotJoint* jPtr = nullptr; - for (auto& j : joints) { - if (j.name == jointName) { jPtr = &j; break; } - } - if (!jPtr) { - LOG_WARN("setViewFollowRobotJoint: joint not found: %s", jointName.c_str()); - return false; - } - - // 2) Find child link -> attached object - scene::Object* targetObj = nullptr; - if (auto it = _impl->_primaryLinkObject.find(jPtr->child); - it != _impl->_primaryLinkObject.end()) { - targetObj = it->second; - } - - if (!targetObj) { - LOG_WARN("setViewFollowRobotJoint: no attached object for joint=%s child=%s", - jointName.c_str(), jPtr->child.c_str()); - return false; - } - - // 3) Bind the view follow target - setViewFollowTarget(view, targetObj, offset); - - /*LOG_INFO("Follow view=%d bound to joint='%s' -> child='%s' -> obj='%s'", - (int)view, jointName.c_str(), jPtr->child.c_str(), targetObj->name.c_str());*/ - - return true; - } - - // Convenience for Follow view - bool SimManager::followRobotJoint(const std::string& jointName, const glm::vec3& offset) { - return setViewFollowRobotJoint(gui::ViewID::Follow, jointName, offset); - } - - // -------------------------------------------------- - // MESH LOADING & GEOMETRY - // -------------------------------------------------- - - // Load a mesh from file and create one Object per submesh. The last loaded mesh becomes the active selection. - void SimManager::loadMesh(const std::string& filepath) { - assets::MeshLoader loader; - auto meshes = loader.load(filepath); - - if (meshes.empty()) { - LOG_WARN("No meshes imported from %s", filepath.c_str()); - D_WARN("No meshes imported from %s", filepath.c_str()); - return; - } - - // For now: spawn one Object per submesh - for (auto& m : meshes) { - auto obj = std::make_unique(m); - obj->id = next(_nextObjectID); - obj->source.filename = filepath; - obj->name = m->getName().empty() ? "Object_" + std::to_string(scene::toUInt32(obj->id)) : m->getName(); - - // initialise physics state - obj->state.q = Quat(1.0, 0.0, 0.0, 0.0); - obj->state.angularVelocity = Vec3::Zero(); - obj->state.linearVelocity = Vec3::Zero(); - obj->state.mass = 1.0; - obj->state.damping = 0.0; - obj->state.inertia = Mat3::Identity(); - obj->state.forces = Vec3::Zero(); - obj->state.torques = Vec3::Zero(); - - _impl->_selectedObject = obj.get(); - _impl->_objects.push_back(std::move(obj)); - } - - LOG_INFO("Loaded %zu submeshes from %s", meshes.size(), filepath.c_str()); - D_INFO("Loaded %zu submeshes from %s", meshes.size(), filepath.c_str()); - } - - // Returns the loaded objects so they can be used as targets for robot joints in the same frame (e.g. end-effector) - std::vector SimManager::loadMeshReturn(const std::string& filepath) { - assets::MeshLoader loader; - auto meshes = loader.load(filepath); - std::vector result; - for (auto& m : meshes) { - auto obj = std::make_unique(m); - auto raw = obj.get(); - raw->internal = true; - _impl->_objects.push_back(std::move(obj)); - result.push_back(raw); - } - return result; - } - - // Setter and Getter for the active mesh - void SimManager::setMesh(std::shared_ptr mesh) { _impl->_mesh = mesh; } - std::shared_ptr SimManager::getMesh() { return _impl->_mesh; } - - // Set the currently selected object (can be nullptr to deselect) - void SimManager::setSelectedObject(scene::Object* obj) { _impl->_selectedObject = obj; } - // Add a new object to the scene and select it - void SimManager::addObject(std::unique_ptr obj) { _impl->_objects.push_back(std::move(obj)); } // Cache the unique_ptr - - // Remove an object by index - void SimManager::deleteObject(int index) { - if (index < 0 || index >= _impl->_objects.size()) { return; } - if (_impl->_selectedObject == _impl->_objects[index].get()) { _impl->_selectedObject = nullptr; } - _impl->_objects.erase(_impl->_objects.begin() + index); - } - - // Remove an object by pointer - void SimManager::removeObject(scene::Object* obj) { - if (!obj) return; - - auto it = std::remove_if( - _impl->_objects.begin(), - _impl->_objects.end(), - [obj](const std::unique_ptr& o) { - return o.get() == obj; - } - ); - - _impl->_objects.erase(it, _impl->_objects.end()); - } - - // Access the objects as raw pointers for use in the rest of the codebase, while maintaining ownership in SimManager - std::vector>& SimManager::getObjects() { return _impl->_objects; } - // Get the currently selected object (can be nullptr) - scene::Object* SimManager::getObject() { return _impl->_selectedObject; } - - // Helper to find an object by its ID (returns nullptr if not found) - scene::Object* SimManager::getObjectByID(scene::ObjectID id) { - for (auto& obj : _impl->_objects) { - if (obj && obj->id == id) { - return obj.get(); - } - } - return nullptr; - } - - // -------------------------------------------------- - // RENDERING ENTRY POINTS - // -------------------------------------------------- - // Main render function called by the application void SimManager::render() { if (hasCompletedStudy()) { @@ -1057,7 +166,7 @@ namespace gui { ImGuiIO& io = ImGui::GetIO(); - _core->tick(io.DeltaTime); + tick(io.DeltaTime); if (_impl->_robotSystem && hasRobot()) { _impl->_robotRenderer->applyTransforms( @@ -1077,6 +186,8 @@ namespace gui { drawViewportWindow(); } + void SimManager::tick(double dt) { _core->tick(dt); } + void SimManager::syncRobotToScene() { if (!hasRobot()) return; auto* rs = robotSystem(); @@ -1103,159 +214,6 @@ namespace gui { } } - // --- UI Elements --- - - // Main Dockspace with Menu Bar - void SimManager::drawMainDockspace() { - ImGuiWindowFlags flags = - ImGuiWindowFlags_NoDocking | - ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoBringToFrontOnFocus | - ImGuiWindowFlags_NoNavFocus; - - const ImGuiViewport* vp = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(vp->Pos); - ImGui::SetNextWindowSize(vp->Size); - ImGui::SetNextWindowViewport(vp->ID); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - - // Must be a window so DockSpace has somewhere to live - ImGui::Begin("##MainDockspace", nullptr, flags); - - ImGui::PopStyleVar(2); - - beginSimManager("##MainDockspaceChild"); - - ImGuiID dock_id = ImGui::GetID("MainDockspaceID"); - ImGui::DockSpace(dock_id, ImVec2(0, 0), ImGuiDockNodeFlags_PassthruCentralNode); - - endSimManager(); - - ImGui::End(); - } - - // Viewport Window - void SimManager::drawViewportWindow() { - ImGui::Begin("Viewport", nullptr, - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse); - - beginSimManager("##ViewportBody"); - - // --- Tabs: Single / Quad --- - if (ImGui::BeginTabBar("ViewportTabs", ImGuiTabBarFlags_None)) { - const bool singleSelected = ImGui::BeginTabItem("Single"); - if (singleSelected) { - // Restore to Manual view when switching back from Quad - if (_impl->viewMode == Impl::ViewMode::Quad) { - _impl->activeView = gui::ViewID::Manual; - } - _impl->viewMode = Impl::ViewMode::Single; - ImGui::EndTabItem(); - } - - const bool quadSelected = ImGui::BeginTabItem("Quad"); - if (quadSelected) { - _impl->viewMode = Impl::ViewMode::Quad; - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - } - - // Everything below tabs is render output - _isHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); - - ImVec2 panel = ImGui::GetContentRegionAvail(); - - // Pixel size (framebuffer coords) - int vpW = (int)(panel.x); - int vpH = (int)(panel.y); - vpW = std::max(1, vpW); - vpH = std::max(1, vpH); - - // Update DISPLAY size only - if ((int)_displaySize.x != vpW || (int)_displaySize.y != vpH) { - _displaySize = { (float)vpW, (float)vpH }; - - // invalidate only display cached sizes so post buffers resize - for (auto& v : _impl->_views) { - v.displayW = 0; - v.displayH = 0; - } - //LOG_INFO("Viewport display size updated to %dx%d", vpW, vpH); - } - - // --- Render + Present --- - if (_impl->viewMode == Impl::ViewMode::Quad) { - int halfW = std::max(1, vpW / 2); - int halfH = std::max(1, vpH / 2); - - _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Top], halfW, halfH); - _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Front], halfW, halfH); - _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Right], halfW, halfH); - _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Follow], halfW, halfH); - - // Stable 2x2 layout - ImVec2 avail = ImGui::GetContentRegionAvail(); - ImVec2 cell = ImVec2(avail.x * 0.5f, avail.y * 0.5f); - - // Helper to draw each cell with the same pattern - auto drawCell = [&](const char* childId, gui::ViewID id, bool sameLine) { - if (sameLine) ImGui::SameLine(); - ImGui::BeginChild(childId, cell, false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); - - auto& v = _impl->_views[(size_t)id]; - ImVec2 inner = ImGui::GetContentRegionAvail(); - - ImGui::Image((ImTextureID)(intptr_t)v.post->getTexture(), inner, ImVec2(0, 1), ImVec2(1, 0)); - - // Set active view when clicking inside the quad cell - if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - _impl->activeView = id; - } - - // Handle scroll zoom on hovered quad cell - if (ImGui::IsWindowHovered()) { - float scrollY = ImGui::GetIO().MouseWheel; - if (scrollY != 0.0f) { - v.cam->onMouseWheel((double)scrollY); - } - } - - // Visual indication: draw a border around the active cell - if (_impl->activeView == id) { - ImDrawList* dl = ImGui::GetWindowDrawList(); - ImVec2 p0 = ImGui::GetWindowPos(); - ImVec2 p1 = ImVec2(p0.x + ImGui::GetWindowSize().x, p0.y + ImGui::GetWindowSize().y); - dl->AddRect(p0, p1, IM_COL32(255, 200, 0, 180), 4.0f, 0, 2.0f); - } - - ImGui::EndChild(); - }; - - drawCell("##Top", gui::ViewID::Top, false); - drawCell("##Front", gui::ViewID::Front, true); - drawCell("##Right", gui::ViewID::Right, false); - drawCell("##Follow", gui::ViewID::Follow, true); - } - else { - auto& v = _impl->_views[static_cast(_impl->activeView)]; - _impl->renderView(*this, v, vpW, vpH); - - ImGui::Image((ImTextureID)(intptr_t)v.post->getTexture(), panel, ImVec2(0, 1), ImVec2(1, 0)); - } - - endSimManager(); - - ImGui::End(); - } - void SimManager::resize(int32_t width, int32_t height) { if (width <= 0 || height <= 0) return; _internalSize = { (float)width, (float)height }; @@ -1268,571 +226,6 @@ namespace gui { //LOG_INFO("Resized SimManager INTERNAL RT to %dx%d", width, height); } - - // Load a robot by name from the robot system - void SimManager::loadRobot(const std::string& name) { - // Clear any existing robot first - if (hasRobot()) { clearRobot(); } - _bodyLoaded = true; - - setSelectedObject(nullptr); - detachCameraFromObject(); - clearViewFollowTarget(gui::ViewID::Follow); - _impl->eeFollowBound = false; - _impl->eeObject = nullptr; - - if (!_impl->_robotSystem) { return; } - _core->loadRobotInternal(name); - - size_t startIdx = _impl->_objects.size(); - - _impl->buildRobotPresentationFromModel(_impl->_robotSystem->model(), *this); - - _impl->_robotRenderer->applyTransforms( - _impl->_robotSystem->model(), - _impl->_robotSystem->worldTransforms() - ); - - if (auto* simInteg = _impl->_robotSystem->getIntegrator()) { - simInteg->resetAdaptiveState(); - } - - // Attempt to find an end-effector candidate among the newly added objects and bind the Follow view to it - scene::Object* ee = _impl->findEndEffectorFromRange(startIdx); - if (ee) { - _impl->eeObject = ee; - setViewFollowTarget(gui::ViewID::Follow, ee, glm::vec3(0.0f, 0.2f, 0.6f)); - _impl->eeFollowBound = true; - LOG_INFO("Follow view bound to end-effector candidate: %s", ee->name.c_str()); - } - } - void SimManager::setRobotLinkRotation(const std::string& linkName, double angle) { - if (_impl->_robotSystem) { _impl->_robotSystem->setRobotLinkRotation(linkName, angle); } - } - void SimManager::setRobotRootPose(const Vec3& pos, Quat& rot) { - if (_impl->_robotSystem) { _impl->_robotSystem->setRobotRootPose(pos, rot); } - } - void SimManager::setRobotRootHome(const Vec3& pos, Quat& rot) { - if (_impl->_robotSystem) { _impl->_robotSystem->setRobotRootHome(pos, rot); } - } - void SimManager::resetRobot() { - if (_impl->_robotSystem) { _impl->_robotSystem->resetRobot(); } - } - void SimManager::clearRobot() { - setSelectedObject(nullptr); // deselect any selected object - _impl->clearRobotPresentation(); - _bodyLoaded = false; - - clearViewFollowTarget(gui::ViewID::Follow); - _impl->eeFollowBound = false; - _impl->eeObject = nullptr; - } - const bool SimManager::hasRobot() const { return _impl->_robotSystem && _impl->_robotSystem->hasRobot(); } - - // Access the robot system (non-const and const versions) - robots::RobotSystem* SimManager::robotSystem() { return _core->robotSystem(); } - const robots::RobotSystem* SimManager::robotSystem() const { return _core->robotSystem(); } - - // Access the trajectory manager (non-const and const versions) - control::TrajectoryManager* SimManager::traj() { return _core->trajectoryManager(); } - const control::TrajectoryManager* SimManager::traj() const { return _core->trajectoryManager(); } - - // Start the simulation - void SimManager::startSimulation() { - if (!hasRobot()) { - LOG_WARN("Cannot start simulation: no robot loaded"); - return; - } - if (hasRobot() && !_bodyLoaded) { - loadRobot(_impl->_robotSystem->robotName()); - return; - } - _core->startSimulation(); - } - // Stop the simulation - void SimManager::stopSimulation() { _core->stopSimulation(); } - - // Check if the simulation is currently running - const bool SimManager::isSimRunning() const { return _core->isSimRunning(); } - - // Setter for current simulation time (in seconds) - void SimManager::setSimTime(double time) { _core->setSimTime(time); } - const double SimManager::simTime() const { return _core->simTime(); } - - // Setter and gettter for fixed timstep (in seconds) - void SimManager::setFixedDt(double dt) { _core->setFixedDt(dt); } - const double SimManager::fixedDt() const { return _core->fixedDt(); } - - // Setter and getter for telemetry frequency (in Hz) - void SimManager::setTelemetryHz(double hz) { _core->setTelemetryHz(hz); } - const double SimManager::telemetryHz() const { return _core->telemetryHz(); } - - // Set whether a script is currently running (used to disable UI elements, etc.) - void SimManager::setScriptRunning(bool running) { _core->setScriptRunning(running); } - const bool SimManager::isScriptRunning() const { return _core->isScriptRunning(); } - - // Setters and getters for last script text - void SimManager::setLastScriptText(const std::string& text) { _core->setLastScriptText(text); } - const std::string& SimManager::lastScriptText() const { return _core->lastScriptText(); } - - // Accessors for the Simulation Core's telemetry data - diagnostics::TelemetryRecorder& SimManager::telemetry() { return _core->telemetry(); } - const diagnostics::TelemetryRecorder& SimManager::telemetry() const { return _core->telemetry(); } - - // Accesors for the active program (if any) - void SimManager::setActiveProgram(interpreter::IStoredProgram* program) { _core->setActiveProgram(program); } - interpreter::IStoredProgram* SimManager::activeProgram() { return _core->activeProgram(); } - const interpreter::IStoredProgram* SimManager::activeProgram() const { return _core->activeProgram(); } - - // Access the simulation core interface (non-const and const versions) - core::ISimulationCore* SimManager::simCoreInterface() { return _core.get(); } - const core::ISimulationCore* SimManager::simCoreInterface() const { return _core.get(); } - - // Access the concrete simulation core (non-const and const versions) - core::SimulationCore* SimManager::simCore() { return _core.get(); } - const core::SimulationCore* SimManager::simCore() const { return _core.get(); } - - // Set the integrator method for the current simulation run - void SimManager::setIntegrationMethod(integration::eIntegrationMethod method) { - _core->setIntegrationMethod(method); - } - const integration::eIntegrationMethod SimManager::integrationMethod() const { - return _core->integrationMethod(); - } - void SimManager::setADIntegrationMethod(integration::eAutoDiffIntegrationMethod method) { - _core->setADIntegrationMethod(method); - } - const integration::eAutoDiffIntegrationMethod SimManager::autoDiffIntegrationMethod() const { - return _core->autoDiffIntegrationMethod(); - } - - // Helper to replace the integrator method in the script text - // A hacky approach my idea, but it works for me and honestly im starting to write up the dissertation so IT WILL DO :) - // PS:If anyone has any better solution msg me - - // This seems to be the better solution? - static std::string replaceIntegratorInScript(const std::string& script, const std::string& methodName) { - std::regex re(R"((?i)set\s*\(\s*integrator\s*,\s*([a-z0-9_]+)\s*\))"); // case-insensitive regex to match my DSL command -> set(integrator, method) - std::string replacement = "set(integrator, " + methodName + ")"; - return std::regex_replace(script, re, replacement); - } - - // Run a script to completion synchronously with a specific integrator - bool SimManager::runScriptToCompletion(const std::string& scriptText, integration::eIntegrationMethod method) { - if (!hasRobot()) { return false; } - - // Map method enum to string name - static const char* names[] = { "euler", "midpoint", "heun", "ralston", "rk4", "rk45", "implicit_euler", "implicit_midpoint", "glrk2", "glrk3" }; - const std::string methodName = names[static_cast(method)]; - - // Replace the integrator method in the script text - std::string modifiedScript = replaceIntegratorInScript(scriptText, methodName); - - // Create program and parser (bound to headless core) - auto program = std::make_unique(_core.get()); - //if (scene::Object* o = getObject()) program->setDefaultObject(o); - auto parser = std::make_unique(program.get()); - - // Parse the modified script and start the program - parser->parse(modifiedScript); - program->start(); - return _core->runScriptToCompletion(program.get(), method); // this will block until the script finishes - } - - // -------------------------------------------------- - // INTERNAL REDNDERING PIPELINE - // -------------------------------------------------- - - // Initialize shadow map resources for cascaded shadow mapping - void SimManager::InitShadowResource(int baseRes) { - if (_shadowsInit) { - glDeleteFramebuffers(SimManager::NUM_CASCADES, _impl->_cascadeFBO); - glDeleteTextures(SimManager::NUM_CASCADES, _impl->_cascadeDepth); - } - - glGenFramebuffers(SimManager::NUM_CASCADES, _impl->_cascadeFBO); - glGenTextures(SimManager::NUM_CASCADES, _impl->_cascadeDepth); - - for (int i = 0; i < SimManager::NUM_CASCADES; i++) { - const int res = (i == 0) ? baseRes : (baseRes / 2); // 8192, 4096, 2048, 1024, 512, 256, 128 - - glBindTexture(GL_TEXTURE_2D, _impl->_cascadeDepth[i]); - glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, res, res, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); - const float border[] = { 1.0f, 1.0f, 1.0f, 1.0f }; - glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); - - glBindFramebuffer(GL_FRAMEBUFFER, _impl->_cascadeFBO[i]); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, _impl->_cascadeDepth[i], 0); - - glDrawBuffer(GL_NONE); - glReadBuffer(GL_NONE); - } - glBindFramebuffer(GL_FRAMEBUFFER, 0); - - _shadowsInit = true; - } - - // Initialize the IBL system with a default HDR environment map - void SimManager::InitIBL() { - _impl->_ibl = std::make_unique(); - _impl->_ibl->init((paths::assets() / "hdr" / "default_white.hdr").string()); - } - - // Render the world grid overlay in the viewport - void SimManager::WorldGridRender(scene::Camera* cam, int rtW) { - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LEQUAL); - glDepthMask(GL_FALSE); - - const int msaa = std::max(1, _settingsCurrent.msaaSamples); - - if (msaa > 1) { - glDisable(GL_BLEND); - glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE); - glEnable(GL_MULTISAMPLE); - - glEnable(GL_POLYGON_OFFSET_FILL); - glPolygonOffset(-0.2f, -0.2f); - } - else { - glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE); - glDisable(GL_MULTISAMPLE); - glEnable(GL_BLEND); - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - } - - // Scale grid line thickness so smaller RTs (quad mode) match single view appearance - const float fullW = std::max(1.0f, _internalSize.x); - const float internalScale = (float)std::max(1, rtW) / fullW; - - _impl->_worldGridShader->use(); - _impl->_worldGridShader->setMat4(cam->getViewProjection(), "gVP"); - _impl->_worldGridShader->setMat4(cam->getViewMatrix(), "gView"); - _impl->_worldGridShader->setVec3(cam->getPosition(), "gCameraWorldPos"); - _impl->_worldGridShader->setFlt1(_settingsCurrent.renderScale, "gRenderScale"); - _impl->_worldGridShader->setFlt1(internalScale, "gInternalScale"); - _impl->_worldGridShader->setFlt1(2500.0f, "gGridSize"); - - glBindVertexArray(_impl->_worldGridVAO); - glDrawArrays(GL_TRIANGLES, 0, 6); - glBindVertexArray(0); - - glDisable(GL_POLYGON_OFFSET_FILL); - glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE); - - glDepthMask(GL_TRUE); - glDisable(GL_BLEND); - glDepthFunc(GL_LESS); - } - - // Render the meshes in the scene using the currently selected shader mode, setting appropriate uniforms for each mode - void SimManager::MeshRender(scene::Camera* cam) { - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LESS); - glDepthMask(GL_TRUE); - glDisable(GL_BLEND); - - shaders::Shader* shader = nullptr; - - switch (currentShaderMode) { - case ShaderMode::Basic: - shader = _impl->_shaderBasic.get(); - break; - case ShaderMode::Lit: - shader = _impl->_shaderLit.get(); - break; - case ShaderMode::PBR: - shader = _impl->_shaderPBR.get(); - break; - } - - if (!shader) { - LOG_ERROR("Shader is NULL after switch!"); - return; - } - - shader->use(); - shader->setBool(false, "isFloor"); - - // Only PBR know about cascades & those uniforms - if (currentShaderMode == ShaderMode::PBR) { - for (int i = 0; i < NUM_CASCADES; i++) { - glActiveTexture(GL_TEXTURE5 + i); - glBindTexture(GL_TEXTURE_2D, _impl->_cascadeDepth[i]); - shader->setInt1(5 + i, "cascadeShadowMap[" + std::to_string(i) + "]"); - shader->setMat4(_impl->_lightSpaceMatrixCascade[i], "lightSpaceMatrix[" + std::to_string(i) + "]"); - } - - // Convert fractional splits (0..1 of far) into world-space distances - const float nearPlane = cam->getNear(); - const float farPlane = cam->getFar(); - - const float splitFrac0 = _cascadeSplits[0]; - const float splitFrac1 = _cascadeSplits[1]; - - const float splitDist0 = nearPlane + splitFrac0 * farPlane; - const float splitDist1 = nearPlane + splitFrac1 * farPlane; - - shader->setFlt2(splitDist0, splitDist1, "cascadeSplits"); - } - - // Camera / SunLight / light common to all mesh shaders - cam->update(shader); - _impl->_light->update(shader); - - // Main mesh rendering loop - for (auto& obj : _impl->_objects) { - if (!obj || !obj->getMesh()) continue; - - if (_impl->_cameraFollowTarget == obj.get()) { cam->setFollowTarget(obj->transform.position, obj->transform.rotQ); } - - glm::mat4 model = obj->transform.toMatrix() * obj->getMesh()->localTransform; - shader->setMat4(model, "model"); - - // Per-mode material uniforms - switch (currentShaderMode) { - case ShaderMode::Basic: - // (IMPORTANT) mesh_basic.frag needs: uniform vec3 color; - shader->setVec3(obj->material.albedo, "albedo"); - break; - - case ShaderMode::Lit: - // (IMPORTANT) mesh_lit.frag needs: albedo, lightPosition, lightColour, lightIntensity, camPos - shader->setVec3(obj->material.albedo, "albedo"); - shader->setVec3(_impl->_light->getPosition(), "lightPosition"); - shader->setFlt1(_impl->_light->getIntensity(), "lightIntensity"); - shader->setVec3(_impl->_light->getColour(), "lightColour"); - shader->setVec3(cam->getPosition(), "camPos"); - break; - - case ShaderMode::PBR: - // Per-mesh PBR material properties - shader->setVec3(obj->material.albedo, "albedo"); - shader->setFlt1(obj->material.metallic, "metallic"); - shader->setFlt1(obj->material.roughness, "roughness"); - shader->setFlt1(1.0f, "ao"); - shader->setFlt1(_settingsCurrent.ambientStrength, "ambientStrength"); - - shader->setVec3(glm::normalize(_impl->_light->getDirection()), "lightDirection"); - shader->setFlt1(_impl->_light->getIntensity(), "lightIntensity"); - shader->setVec3(_impl->_light->getColour(), "lightColour"); - shader->setVec3(cam->getPosition(), "camPos"); - - shader->setInt1(0, "irradianceMap"); - shader->setInt1(1, "prefilterMap"); - shader->setInt1(2, "brdfLUT"); - - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_CUBE_MAP, _impl->_ibl->getIrradianceMap()); - - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_CUBE_MAP, _impl->_ibl->getPrefilterMap()); - - glActiveTexture(GL_TEXTURE2); - glBindTexture(GL_TEXTURE_2D, _impl->_ibl->getBRDFLUT()); - - break; - } - - _impl->currentShader = shader; // for external access - obj->getMesh()->render(); - } - } - - // Render the shadow maps for each cascade by rendering the scene from the light's perspective into the depth textures - void SimManager::ShadowPass(scene::Camera* cam) { - float nearPlane = cam->getNear(); - float farPlane = cam->getFar(); - - float cascadeNear[NUM_CASCADES]{}; - float cascadeFar[NUM_CASCADES]{}; - - cascadeNear[0] = nearPlane; - cascadeFar[0] = nearPlane + _cascadeSplits[0] * (farPlane); - - cascadeNear[1] = cascadeFar[0]; - cascadeFar[1] = nearPlane + _cascadeSplits[1] * (farPlane); - - glEnable(GL_POLYGON_OFFSET_FILL); - glPolygonOffset(2.0f, 4.0f); - - for (int i = 0; i < NUM_CASCADES; i++) { - _impl->_lightSpaceMatrixCascade[i] = LightSpaceMatrix(cam, cascadeNear[i], cascadeFar[i]); - - // set viewport to shadow map size - int baseRes = _settingsCurrent.shadowMapRes; - int res = (i == 0) ? baseRes : (baseRes / 2); // 4096, 2048, 1024, 512, 256, 128 - glViewport(0, 0, res, res); - - // render to cascade FBO - glBindFramebuffer(GL_FRAMEBUFFER, _impl->_cascadeFBO[i]); - glClear(GL_DEPTH_BUFFER_BIT); - - // render scene from light's point of view - _impl->_shadowShader->use(); - _impl->_shadowShader->setMat4(_impl->_lightSpaceMatrixCascade[i], "lightSpaceMatrix"); - - // main mesh - for (auto& obj : _impl->_objects) { - if (!obj || !obj->getMesh()) continue; - - glm::mat4 model = obj->transform.toMatrix() * obj->getMesh()->localTransform; - - _impl->_shadowShader->setMat4(model, "model"); - obj->getMesh()->render(); - } - } - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glViewport(0, 0, (int)_internalSize.x, (int)_internalSize.y); - - glDisable(GL_POLYGON_OFFSET_FILL); - } - - // Compute the light-space matrix for a given camera frustum slice (cascade) - glm::mat4 SimManager::LightSpaceMatrix(scene::Camera* cam, float nearPlane, float farPlane) { - std::array corners = cam->getFrustumCornersWorldSpace(nearPlane, farPlane); - - glm::vec3 lightDir = glm::normalize(_impl->_light->getDirection()); - - // Fake camera position far along direction - glm::vec3 lightPos = -lightDir * 50.0f; - - glm::mat4 lightView = glm::lookAt( - lightPos, - glm::vec3(0.0f), - glm::vec3(0, 1, 0) - ); - - float minX = FLT_MAX, maxX = -FLT_MAX; - float minY = FLT_MAX, maxY = -FLT_MAX; - float minZ = FLT_MAX, maxZ = -FLT_MAX; - - for (auto& corner : corners) { - glm::vec4 trf = lightView * glm::vec4(corner); - minX = std::min(minX, trf.x); - maxX = std::max(maxX, trf.x); - minY = std::min(minY, trf.y); - maxY = std::max(maxY, trf.y); - minZ = std::min(minZ, trf.z); - maxZ = std::max(maxZ, trf.z); - } - - // Compute cascade center in light space - glm::vec3 center = { - 0.5f * (minX + maxX), - 0.5f * (minY + maxY), - 0.5f * (minZ + maxZ) - }; - - // Cascade radius (half-size of the bounding sphere) - float radius = glm::length(glm::vec3(maxX - minX, maxY - minY, 0.0f)) * 0.5f; - - int shadowMapResolution = _settingsCurrent.shadowMapRes; - - // The size of one texel in world-space - float worldUnitsPerTexel = (radius * 2.0f) / shadowMapResolution; - - // Snap X and Y (Z never snapped) - center.x = std::floor(center.x / worldUnitsPerTexel) * worldUnitsPerTexel; - center.y = std::floor(center.y / worldUnitsPerTexel) * worldUnitsPerTexel; - - // Recompute min/max using snapped centre - minX = center.x - radius; - maxX = center.x + radius; - minY = center.y - radius; - maxY = center.y + radius; - - glm::mat4 lightProj = glm::ortho(minX, maxX, minY, maxY, minZ - 20.0f, maxZ + 20.0f); - - return lightProj * lightView; - } - - // Render the skybox using the IBL environment cubemap - void SimManager::SkyboxRender(scene::Camera* cam) { - glm::mat4 view = cam->getViewMatrix(); - glm::mat4 projection = cam->getProjection(); - - _impl->_skybox->setEnvironmentTexture(_impl->_ibl->getEnvCubemap()); - _impl->_skybox->render(projection, view); - } - - // Load a new HDR environment map for IBL - std::string SimManager::getDefaultHDR() const { return (paths::assets() / "hdr"/ "default_white.hdr").string(); } - - // Load a new HDR environment map for IBL - const shaders::Shader* SimManager::getCurrentShader() const { return _impl->currentShader; } - void SimManager::applyRenderSettings(const render::RenderSettings& s, render::ResolutionPreset r) { applyRenderProfile(s, r); } - - void SimManager::applyRenderProfile(const render::RenderSettings& s, render::ResolutionPreset r) { - const bool first = !_settingsValid; - - const bool shadowResChanged = first || (s.shadowMapRes != _settingsCurrent.shadowMapRes); - const bool msaaChanged = first || (s.msaaSamples != _settingsCurrent.msaaSamples); - const bool renderScaleChanged = first || (s.renderScale != _settingsCurrent.renderScale); - const bool presetChanged = first || (r != _resCurrent); - - if (shadowResChanged) { - InitShadowResource(s.shadowMapRes); - } - - _settingsCurrent = s; - _resCurrent = r; - - if (msaaChanged || renderScaleChanged || presetChanged) { - for (auto& v : _impl->_views) { v.w = v.h = 0; v.displayW = v.displayH = 0; } - } - - glm::vec2 px = getPresetResolutionPx(); - _internalSize = px; - for (auto& v : _impl->_views) { v.w = v.h = 0; } // internal invalidation - - //LOG_INFO("Render settings applied: resPreset=%d shadowRes=%d msaa=%d renderScale=%.2f", (int)r, _settingsCurrent.shadowMapRes, _settingsCurrent.msaaSamples, _settingsCurrent.renderScale); - D_RUNTIME("Render settings applied: resPreset=%d shadowRes=%d msaa=%d renderScale=%.2f", (int)r, _settingsCurrent.shadowMapRes, _settingsCurrent.msaaSamples, _settingsCurrent.renderScale); - - _settingsValid = true; - } - - // Get the pixel dimensions for the current resolution preset - glm::vec2 SimManager::getPresetResolutionPx() const { - switch (_resCurrent) { - case render::ResolutionPreset::R_720p: return { 1280, 720 }; - case render::ResolutionPreset::R_1080p: return { 1920, 1080 }; - case render::ResolutionPreset::R_1440p: return { 2560, 1440 }; - case render::ResolutionPreset::R_4K: return { 3840, 2160 }; - default: return { 1920, 1080 }; - } - } - - glm::vec2 SimManager::getInternalResolutionSizePx() const { - return getPresetResolutionPx(); // no scale - } - - void SimManager::resetHDRToPreset() { - _hdrUserOverride = false; - const std::string hdr = getDefaultHDR(); - if (hdr != _activeHDRPath) { loadNewHDR(hdr); } - } - - void SimManager::reloadAllShaders() { - _impl->_shaderBasic->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_basic.frag.glsl").string()); - _impl->_shaderLit->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_lit.frag.glsl").string()); - _impl->_shaderPBR->load((paths::assets() / "shaders" / "vs_pbr.vert.glsl").string(), (paths::assets() / "shaders" / "mesh_pbr.frag.glsl").string()); - _impl->_ssaoShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "ssao.frag.glsl").string()); - _impl->_ssaoBlurShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "ssao_blur.frag.glsl").string()); - - //LOG_INFO("All shaders reloaded from disk."); - D_INFO_ONCE("All shaders reloaded from disk."); - } - - void SimManager::setLightColour(const glm::vec3& colour) { _impl->_light->_colour = colour; } // -------------------------------------------------- // INPUT HANDLING @@ -1921,23 +314,4 @@ namespace gui { void gui::SimManager::resetMouseDelta() { _firstMouse = true; } // --- Helpers --- - - // Begin Control Panel Helper - void SimManager::beginSimManager(const char* id) { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 10.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 5.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 8.0f)); - - ImGui::BeginChild(id, ImVec2(0, 0), true, - ImGuiChildFlags_AlwaysUseWindowPadding | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse); - } - - // End Control Panel Helper - void SimManager::endSimManager() { - ImGui::EndChild(); - ImGui::PopStyleVar(4); - } } \ No newline at end of file From c3f782c961f29d83ffe3730cd699d7bf180d2ff2 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Thu, 4 Jun 2026 19:36:09 +0100 Subject: [PATCH 008/110] refactor: Port application from GLFW to Qt (QOpenGLWidget) NOTES (partially generated): * Refactored main window and rendering to use Qt (QApplication, QMainWindow, QOpenGLWidget) instead of GLFW. * Updated `Application`, `DSFE_MainWindow`, `ProjectPage`, and `ViewportWidget` to support Qt-based management and input. * Introduced eKeyCode for unified key handling. * Adapted `SimulationManager` and rendering pipeline for Qt, including new `present.frag.glsl` shader. * Removed GLFW-specific code and update CMakeLists.txt accordingly. * Added KeyCode.h for cross-platform input. TODO: * Test loading of dynamic systems * Start to really remove GLFW from code * Cleanup existing code * Replicate process for ControlPanel, DSL editor, and DebugPanel(CLI) * Remove hardcoded plugin path and cmake path, replace with permanent solution --- .../assets/shaders/present.frag.glsl | 11 ++ DSFE_App/DSFE_Engine/src/Main.cpp | 3 +- DSFE_App/DSFE_GUI/CMakeLists.txt | 14 +- DSFE_App/DSFE_GUI/include/Application.h | 38 +++-- .../include/MainWindow/DSFE_MainWindow.h | 4 +- .../MainWindow/DockWidgets/ViewportDock.h | 13 -- .../MainWindow/Widgets/ViewportWidget.h | 38 ++++- .../MainWindow/Workspace/ProjectPage.h | 4 +- DSFE_App/DSFE_GUI/include/Platform/KeyCode.h | 13 ++ DSFE_App/DSFE_GUI/include/Scene/Camera.h | 8 + .../include/Scene/Manager/SimImplementation.h | 15 +- .../include/Scene/SimulationManager.h | 16 +- DSFE_App/DSFE_GUI/src/Application.cpp | 85 +++++------ .../src/MainWindow/DSFE_MainWindow.cpp | 9 +- .../MainWindow/DockWidgets/ViewportDock.cpp | 11 -- .../src/MainWindow/Widgets/ViewportWidget.cpp | 140 +++++++++++++++++- .../src/MainWindow/Workspace/ProjectPage.cpp | 8 +- .../DSFE_GUI/src/Platform/WindowManager.cpp | 3 +- .../src/Rendering/OpenGLBufferManager.cpp | 11 +- DSFE_App/DSFE_GUI/src/Scene/Camera.cpp | 14 +- .../DSFE_GUI/src/Scene/Manager/SimBackend.cpp | 2 +- .../DSFE_GUI/src/Scene/SimulationManager.cpp | 90 ++++++++--- 22 files changed, 408 insertions(+), 142 deletions(-) create mode 100644 DSFE_App/DSFE_Engine/assets/shaders/present.frag.glsl delete mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/DockWidgets/ViewportDock.h create mode 100644 DSFE_App/DSFE_GUI/include/Platform/KeyCode.h delete mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/DockWidgets/ViewportDock.cpp diff --git a/DSFE_App/DSFE_Engine/assets/shaders/present.frag.glsl b/DSFE_App/DSFE_Engine/assets/shaders/present.frag.glsl new file mode 100644 index 00000000..054f2500 --- /dev/null +++ b/DSFE_App/DSFE_Engine/assets/shaders/present.frag.glsl @@ -0,0 +1,11 @@ +#version 460 core + +in vec2 uv; + +out vec4 FragColour; + +uniform sampler2D screenTexture; + +void main() { + FragColour = vec4(1,0,0,1); +} \ No newline at end of file diff --git a/DSFE_App/DSFE_Engine/src/Main.cpp b/DSFE_App/DSFE_Engine/src/Main.cpp index 39994ac8..571390d0 100644 --- a/DSFE_App/DSFE_Engine/src/Main.cpp +++ b/DSFE_App/DSFE_Engine/src/Main.cpp @@ -79,8 +79,7 @@ int main(int argc, char** argv) { } Application app("DSFE"); - app.run(); - return 0; + return app.run(); } // Batch mode: parse batch-specific arguments diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 8e04f4ce..2fbad3fc 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -40,7 +40,6 @@ target_include_directories(stb INTERFACE set(MAIN_WINDOW_SRC src/MainWindow/DSFE_MainWindow.cpp src/MainWindow/Workspace/ProjectPage.cpp - src/MainWindow/DockWidgets/ViewportDock.cpp src/MainWindow/Widgets/ViewportWidget.cpp ) @@ -64,6 +63,17 @@ set(SCENE_SRC src/Scene/SimulationManager.cpp ) +set(MANAGER_SRC + src/Scene/Manager/SimBackend.cpp + src/Scene/Manager/SimCamera.cpp + src/Scene/Manager/SimObjects.cpp + src/Scene/Manager/SimPostFX.cpp + src/Scene/Manager/SimRendering.cpp + src/Scene/Manager/SimRobots.cpp + src/Scene/Manager/SimUI.cpp + src/Scene/Manager/SimViewport.cpp +) + set(UI_SRC src/ui/CommandScriptEditor.cpp src/ui/ControlPanel.cpp @@ -92,6 +102,7 @@ target_sources(DSFE_GUI PRIVATE ${MAIN_WINDOW_SRC} ${RENDER_SRC} ${SCENE_SRC} + ${MANAGER_SRC} ${UI_SRC} ${ROBOT_SRC} ${PLATFORM_SRC} @@ -132,6 +143,7 @@ target_include_directories(DSFE_GUI ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/include/MainWindow + ${CMAKE_CURRENT_SOURCE_DIR}/include/Scene PRIVATE ${imfilebrowser_ROOT} diff --git a/DSFE_App/DSFE_GUI/include/Application.h b/DSFE_App/DSFE_GUI/include/Application.h index 994129f2..f2ddd436 100644 --- a/DSFE_App/DSFE_GUI/include/Application.h +++ b/DSFE_App/DSFE_GUI/include/Application.h @@ -5,10 +5,14 @@ #include #include +#include +#include "Platform/Logger.h" // forward declarations -namespace window { class GLWindow; } -namespace scene { class Camera; } +//namespace window { class GLWindow; } +class QApplication; +namespace gui { class SimManager; } +namespace window { class DSFE_MainWindow; } class DSFE_GUI_API Application { public: @@ -16,18 +20,28 @@ class DSFE_GUI_API Application { ~Application(); static Application& Instance() { return *sInstance; } - void run(); + int run(); private: static Application* sInstance; -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable: 4251) // Suppress C4251 for private members -#endif - std::unique_ptr _window; - std::unique_ptr _camera; -#ifdef _MSC_VER -#pragma warning(pop) -#endif + std::string _name; + + int _qtArgc = 0; + std::vector _qtArgStorage; + std::vector _qtArgv; + + std::unique_ptr _qtApp; + std::unique_ptr _sim; + std::unique_ptr _mainW; + +//#ifdef _MSC_VER +//#pragma warning(push) +//#pragma warning(disable: 4251) // Suppress C4251 for private members +//#endif +// std::unique_ptr _window; +// std::unique_ptr _camera; +//#ifdef _MSC_VER +//#pragma warning(pop) +//#endif }; \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h index 5ed46a4c..94264db7 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h @@ -3,9 +3,11 @@ #include +namespace gui { class SimManager; } + namespace window { class DSFE_MainWindow : public QMainWindow { public: - explicit DSFE_MainWindow(QWidget* parent = nullptr); + explicit DSFE_MainWindow(gui::SimManager* sim, QWidget* parent = nullptr); }; } diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/DockWidgets/ViewportDock.h b/DSFE_App/DSFE_GUI/include/MainWindow/DockWidgets/ViewportDock.h deleted file mode 100644 index 7ea44d8a..00000000 --- a/DSFE_App/DSFE_GUI/include/MainWindow/DockWidgets/ViewportDock.h +++ /dev/null @@ -1,13 +0,0 @@ -// DSFE_GUI ViewportDock.h -#pragma once - -#include - -class ViewportWidget; - -namespace dockwidgets { - class ViewportDock : public QDockWidget { - public: - explicit ViewportDock(QWidget* parent = nullptr); - }; -} // namespace dockwidgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h index 51868f9e..006de699 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h @@ -2,10 +2,44 @@ #pragma once #include +#include +#include +#include +#include +#include +#include + +#include + +namespace gui { class SimManager; enum class eKeyCode; } namespace widgets { - class ViewportWidget : public QOpenGLWidget { + class ViewportWidget : public QOpenGLWidget, protected QOpenGLFunctions_4_5_Core { public: - explicit ViewportWidget(QWidget* parent = nullptr); + explicit ViewportWidget(gui::SimManager* sim, QWidget* parent = nullptr); + + protected: + void initializeGL() override; + void resizeGL(int w, int h) override; + void paintGL() override; + + void keyPressEvent(QKeyEvent* event) override; + void keyReleaseEvent(QKeyEvent* event) override; + + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + + void wheelEvent(QWheelEvent* event) override; + + private: + gui::SimManager* _sim = nullptr; + QElapsedTimer _frameTimer; + qint64 _lastNs = 0; + QTimer _updateTimer; + QPoint _screenCenter; + + bool _mouseCaptured = false; + std::unordered_set _pressedKeys; }; } \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h b/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h index bed7285a..eac14829 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h @@ -3,9 +3,11 @@ #include +namespace gui { class SimManager; } + namespace Workspace { class ProjectPage : public QWidget { public: - explicit ProjectPage(QWidget* parent = nullptr); + explicit ProjectPage(gui::SimManager* sim, QWidget* parent = nullptr); }; } // namespace Workspace \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Platform/KeyCode.h b/DSFE_App/DSFE_GUI/include/Platform/KeyCode.h new file mode 100644 index 00000000..fcd1356d --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/Platform/KeyCode.h @@ -0,0 +1,13 @@ +// DSFE_GUI KeyCode.h +#pragma once + +namespace gui { + enum class eKeyCode { + W = 0, A = 1, S = 2, D = 3, + LShift = 4, Ctrl = 5, + Space = 6, + Tab = 7, + Esc = 8, + Unknown = 9 + }; +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Scene/Camera.h b/DSFE_App/DSFE_GUI/include/Scene/Camera.h index d738b281..bc36f9ed 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/Camera.h +++ b/DSFE_App/DSFE_GUI/include/Scene/Camera.h @@ -138,6 +138,14 @@ namespace scene { void setMinDistance(float d) { _minDistance = glm::max(0.05f, d); } private: + enum class eKeyCode { + W, A, S, D, + LShift, Ctrl, + Space, + Tab, + Unknown + }; + void rebuildAxesFromFrontUp_(const glm::vec3& front, const glm::vec3& upHint); void updateProjectionMatrix() { if (!std::isfinite(_FOV) || _FOV <= 0.001f) { _FOV = glm::radians(70.0f); } diff --git a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h index 6b4303d3..12ef54e7 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h +++ b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h @@ -49,7 +49,7 @@ namespace gui { enum class ViewMode { Single, Quad }; // Current View Mode - ViewMode viewMode = ViewMode::Single; + ViewMode viewMode; // Viewport Structure struct Viewport { @@ -69,7 +69,7 @@ namespace gui { // Viewports std::array _views; - VID activeView = VID::Manual; + VID activeView; // Skybox & IBL std::unique_ptr _ibl; @@ -77,6 +77,7 @@ namespace gui { // Post-Processing Shader std::unique_ptr _postShader; + std::unique_ptr _presentShader; std::shared_ptr _shaderBasic; std::shared_ptr _shaderLit; std::shared_ptr _shaderPBR; @@ -131,8 +132,15 @@ namespace gui { std::vector _ssaoKernel; Impl(SimManager& owner) { + activeView = VID::Manual; + viewMode = ViewMode::Single; + } + + void initGLResources(SimManager& owner) { _postShader = std::make_unique(); _postShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "post.frag.glsl").string()); + _presentShader = std::make_unique(); + _presentShader->load((paths::assets() / "shaders" / "post.vert.glsl").string(), (paths::assets() / "shaders" / "present.frag.glsl").string()); glGenVertexArrays(1, &_fullscreenVAO); @@ -254,7 +262,6 @@ namespace gui { // Robot system with mesh loading (for normal simulation) _robotSystem = std::make_unique(); - _robotRenderer = std::make_unique(); // SSAO shaders @@ -497,6 +504,8 @@ namespace gui { glDepthMask(GL_TRUE); glDepthFunc(GL_LESS); + LOG_INFO_ONCE("Background = %.3f %.3f %.3f", owner._backgroundColour.r, owner._backgroundColour.g, owner._backgroundColour.b); + glClearColor(owner._backgroundColour.r, owner._backgroundColour.g, owner._backgroundColour.b, owner._backgroundAlpha); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); diff --git a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h index b0dda2f2..a86bc4d5 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h +++ b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "Platform/StudyRunner.h" #include "Rendering/ModelGroup.h" @@ -13,6 +14,8 @@ #include "ui/RenderPreset.h" #include "FpsCounter.h" +#include "Platform/KeyCode.h" + #include "Analysis/Telemetry.h" #include "Analysis/MetricLogger.h" @@ -52,6 +55,8 @@ namespace gui { // Forward Declarations for Axis Orientator class AxisOrientator; + // Forward Declarations for eKeyCode + enum class eKeyCode; // Control Modes & Camera enum class ControlMode { @@ -69,6 +74,9 @@ namespace gui { // OpenGL Initialisation void initGL(); + void renderViewport(int w, int h); + void setDisplaySize(int w, int h); + // Light scene::Light* getLight(); void setLightColour(const glm::vec3& c); @@ -164,6 +172,8 @@ namespace gui { void tick(double dt); void resize(int32_t width, int32_t height); + void setPresentationFBO(GLuint fbo) { _presentationFBO = fbo; } + void syncRobotToScene(); // Scene Objects Management @@ -261,8 +271,8 @@ namespace gui { // Input Handling void processMovementKey(int key, float delta); - void handleContinuousMovement(GLFWwindow* window, float dt); - void handleMouseLook(GLFWwindow* window, double xpos, double ypos); + void handleContinuousMovement(const std::unordered_set& pressedKeys, float dt); + void handleMouseLook(double xpos, double ypos, bool mouseCaptured); void onMouseMove(double x, double y, scene::eInputButton button); void onMouseWheel(double delta); void resetMouseDelta(); @@ -330,6 +340,8 @@ namespace gui { robots::TrajRefBuffer _trajRefBuffer; // Buffer for logging trajectory reference data each step bool _telemetryBegun = false; + GLuint _presentationFBO = 0; // FBO for final post-processed output to the screen + // Environment & Lighting render::RenderSettings _settingsCurrent{}; render::ResolutionPreset _resCurrent = render::ResolutionPreset::R_1080p; diff --git a/DSFE_App/DSFE_GUI/src/Application.cpp b/DSFE_App/DSFE_GUI/src/Application.cpp index 5e95f7e1..5df7a13a 100644 --- a/DSFE_App/DSFE_GUI/src/Application.cpp +++ b/DSFE_App/DSFE_GUI/src/Application.cpp @@ -1,77 +1,68 @@ // DSFE_GUI Application.cpp #include "Application.h" -#include "Platform/WindowManager.h" #include "MainWindow/DSFE_MainWindow.h" -#include "Scene/Camera.h" - -#ifdef __gl_h_ -#undef __gl_h_ -#endif -#include - -#include +#include "Scene/SimulationManager.h" #include "Platform/Paths.h" #include "EngineLib/LogMacros.h" +#include #include -#include -#include +#include +#include +#include namespace fs = std::filesystem; // Initialise the static instance pointer to nullptr Application* Application::sInstance = nullptr; -static QApplication* gQtApp = nullptr; -static window::DSFE_MainWindow* gQtWindow = nullptr; - // Constructor: Initialises paths, sets up data manager, and creates the main application window -Application::Application(const std::string& appName) { +Application::Application(const std::string& appName) : _name(appName) { paths::init(); - LOG_INFO("Root path: %s", paths::root().string().c_str()); LOG_INFO("Assets path: %s", paths::assets().string().c_str()); LOG_INFO("Configs path: %s", paths::configs().string().c_str()); LOG_INFO("Logs path: %s", paths::logs().string().c_str()); LOG_INFO("Runs path: %s", paths::runs().string().c_str()); - int winW = 1920, winH = 1080; + qputenv("QSG_RHI_BACKEND", "opengl"); +#if defined(Q_OS_WIN) + QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); +#endif + QCoreApplication::addLibraryPath("C:/Qt/6.11.1/msvc2022_64/plugins"); - if (glfwInit()) { - const GLFWvidmode* mode = glfwGetVideoMode(glfwGetPrimaryMonitor()); - if (mode) { - winH = (int)(mode->height * 0.8f); - winW = (winH * 16) / 9; - // Clamp width to 90% of monitor width in case ultra-wide - if (winW > (int)(mode->width * 0.9f)) { - winW = (int)(mode->width * 0.9f); - winH = (winW * 9) / 16; - } - LOG_INFO("Monitor: %dx%d -> Window: %dx%d (16:9)", mode->width, mode->height, winW, winH); - } - glfwTerminate(); // OpenGLContext::init will call glfwInit again - } + _qtArgStorage.clear(); + _qtArgStorage.emplace_back(_name.empty() ? "DSFE" : _name); + _qtArgc = static_cast(_qtArgStorage.size()); + _qtArgv.clear(); + _qtArgv.reserve(_qtArgStorage.size()); + for (std::string& arg : _qtArgStorage) { _qtArgv.push_back(arg.data()); } + + _qtApp = std::make_unique(_qtArgc, _qtArgv.data()); + + QSurfaceFormat format; + format.setProfile(QSurfaceFormat::CoreProfile); + format.setVersion(4, 5); + format.setDepthBufferSize(24); + format.setStencilBufferSize(8); + QSurfaceFormat::setDefaultFormat(format); - if (!gQtApp) { - QCoreApplication::addLibraryPath("C:/Qt/6.11.1/msvc2022_64/plugins"); - int argc = 0; - gQtApp = new QApplication(argc, nullptr); - gQtWindow = new window::DSFE_MainWindow(); - gQtWindow->show(); + _sim = std::make_unique(); + + int winW = 1920, winH = 1080; + if (QScreen* screen = QGuiApplication::primaryScreen()) { + const QRect g = screen->geometry(); + winH = int(g.height() * 0.8f); // maintain 16:9 aspect ratio and fit within 90% of screen height + winW = (winH * 16) / 9; // maintain 16:9 aspect ratio } - _window = std::make_unique(); - _window->init(winW, winH, appName); + _mainW = std::make_unique(_sim.get()); + _mainW->resize(winW, winH); + _mainW->show(); } Application::~Application() = default; -// Main application loop: Continues running until the window signals to close, updating and rendering each frame -void Application::run() { - while (_window->isRunning() && !_window->shouldClose()) { - QCoreApplication::processEvents(); - - _window->update(); - _window->render(); - } +int Application::run() { + return _qtApp ? _qtApp->exec() : 0; } \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index 8c2ea55a..3a06c138 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -1,14 +1,15 @@ //DSFE_GUI DSFE_MainWindow.cpp #include "MainWindow/DSFE_MainWindow.h" +#include "Scene/SimulationManager.h" #include "Workspace/ProjectPage.h" namespace window { - DSFE_MainWindow::DSFE_MainWindow(QWidget* parent) : QMainWindow(parent) { + DSFE_MainWindow::DSFE_MainWindow(gui::SimManager* sim, QWidget* parent) : QMainWindow(parent) { setWindowTitle("DSFE"); - resize(800, 600); + resize(1280, 720); - auto* page = new Workspace::ProjectPage(this); + auto* page = new Workspace::ProjectPage(sim, this); setCentralWidget(page); - page->show(); } + } // namespace window \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DockWidgets/ViewportDock.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DockWidgets/ViewportDock.cpp deleted file mode 100644 index 732f92c1..00000000 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DockWidgets/ViewportDock.cpp +++ /dev/null @@ -1,11 +0,0 @@ -// DSFE_GUI ViewportDock.cpp -#include "DockWidgets/ViewportDock.h" -#include "Widgets/ViewportWidget.h" - -namespace dockwidgets { - ViewportDock::ViewportDock(QWidget* parent) : QDockWidget("Viewport", parent) { - setAllowedAreas(Qt::AllDockWidgetAreas); - setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); - setWidget(new widgets::ViewportWidget(this)); - } -} // namespace dockwidgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp index 5cae3be8..cb0c3110 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp @@ -1,10 +1,148 @@ // DSFE_GUI ViewportWidget.cpp #include "Widgets/ViewportWidget.h" +#include "Scene/SimulationManager.h" + +#ifdef __gl_h_ +#undef __gl_h_ +#endif +#include +#include #include #include +#include "Platform/KeyCode.h" + namespace widgets { - ViewportWidget::ViewportWidget(QWidget* parent) : QOpenGLWidget(parent) { + ViewportWidget::ViewportWidget(gui::SimManager* sim, QWidget* parent) : QOpenGLWidget(parent), _sim(sim) { + setFocusPolicy(Qt::StrongFocus); + setMouseTracking(true); + + connect(&_updateTimer, &QTimer::timeout, this, [this]() {update(); }); + _updateTimer.start(7); // ~144 FPS + } + + void ViewportWidget::initializeGL() { + initializeOpenGLFunctions(); + + auto* ctx = QOpenGLContext::currentContext(); + + if (!ctx) { + LOG_ERROR("No current OpenGL context"); + return; + } + + const int gladResult = gladLoadGLLoader([](const char* name) -> void* { + auto* ctx = QOpenGLContext::currentContext(); + if (!ctx) { return nullptr; } + return reinterpret_cast( ctx->getProcAddress(name)); + }); + + if (!gladResult) { + LOG_ERROR("Failed to initialise GLAD"); + return; + } + + _frameTimer.start(); + if (_sim) { _sim->initGL(); } + } + void ViewportWidget::resizeGL(int w, int h) { + if (_sim) { _sim->setDisplaySize(w, h); } + } + void ViewportWidget::paintGL() { + LOG_INFO_ONCE("Qt default FBO = %u", defaultFramebufferObject()); + GLint qtFBO = defaultFramebufferObject(); + + const qint64 now = _frameTimer.nsecsElapsed(); + const float dt = (_lastNs == 0) ? (1.0f / 144.0f) : static_cast(now - _lastNs) * 1e-9f; + _lastNs = now; + if (!_sim) { + glClearColor(0.1f, 0.1f, 0.1f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + return; + } + + _sim->setPresentationFBO(static_cast(qtFBO)); + _sim->tick(dt); + _sim->renderViewport(width(), height()); + + static float smoothedDt = (1.0f / 144.0f); + smoothedDt = glm::mix(smoothedDt, dt, 0.5f); + if (_mouseCaptured) { _sim->handleContinuousMovement(_pressedKeys, smoothedDt); } + } + + void ViewportWidget::keyPressEvent(QKeyEvent* event) { + switch (event->key()) { + case Qt::Key_W: _pressedKeys.insert(gui::eKeyCode::W); break; + case Qt::Key_A: _pressedKeys.insert(gui::eKeyCode::A); break; + case Qt::Key_S: _pressedKeys.insert(gui::eKeyCode::S); break; + case Qt::Key_D: _pressedKeys.insert(gui::eKeyCode::D); break; + case Qt::Key_Space: _pressedKeys.insert(gui::eKeyCode::Space); break; + case Qt::Key_Control: _pressedKeys.insert(gui::eKeyCode::Ctrl); break; + case Qt::Key_Shift: _pressedKeys.insert(gui::eKeyCode::LShift); break; + case Qt::Key_Escape: + _mouseCaptured = !_mouseCaptured; + if (_mouseCaptured) { + setFocus(); + setCursor(Qt::BlankCursor); + _screenCenter = mapToGlobal(rect().center()); + QCursor::setPos(_screenCenter); + grabMouse(); + if (_sim) { _sim->resetMouseDelta(); } + } + else { + releaseMouse(); + unsetCursor(); + if (_sim) { _sim->resetMouseDelta(); } + } + break; + } + QOpenGLWidget::keyPressEvent(event); + } + + void ViewportWidget::keyReleaseEvent(QKeyEvent* event) { + switch (event->key()) { + case Qt::Key_W: _pressedKeys.erase(gui::eKeyCode::W); break; + case Qt::Key_A: _pressedKeys.erase(gui::eKeyCode::A); break; + case Qt::Key_S: _pressedKeys.erase(gui::eKeyCode::S); break; + case Qt::Key_D: _pressedKeys.erase(gui::eKeyCode::D); break; + case Qt::Key_Space: _pressedKeys.erase(gui::eKeyCode::Space); break; + case Qt::Key_Control: _pressedKeys.erase(gui::eKeyCode::Ctrl); break; + case Qt::Key_Shift: _pressedKeys.erase(gui::eKeyCode::LShift); break; + } + QOpenGLWidget::keyReleaseEvent(event); + } + + void ViewportWidget::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::RightButton) { + _mouseCaptured = true; + setFocus(); + setCursor(Qt::BlankCursor); + QCursor::setPos(mapToGlobal(rect().center())); + grabMouse(); + if (_sim) { _sim->resetMouseDelta(); } + } } + + void ViewportWidget::mouseReleaseEvent(QMouseEvent* event) { + if (event->button() == Qt::RightButton) { + _mouseCaptured = false; + releaseMouse(); + unsetCursor(); + } + } + + void ViewportWidget::mouseMoveEvent(QMouseEvent* event) { + if (!_sim || !_mouseCaptured) { return; } + QPoint current = QCursor::pos(); + QPoint delta = current - _screenCenter; + _sim->handleMouseLook(delta.x(), -delta.y(), true); + QCursor::setPos(_screenCenter); + } + + void ViewportWidget::wheelEvent(QWheelEvent* event) { + if (!_sim) { return; } + _sim->onMouseWheel(event->angleDelta().y() / 120.0); + } + } // namespace widgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp index dc4feb7e..25efa15d 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp @@ -1,5 +1,6 @@ // DSFE_GUI ProjectPage.cpp #include "Workspace/ProjectPage.h" +#include "Scene/SimulationManager.h" #include "Widgets/ViewportWidget.h" #include @@ -7,13 +8,14 @@ #include namespace Workspace { - ProjectPage::ProjectPage(QWidget* parent) : QWidget(parent) { + ProjectPage::ProjectPage(gui::SimManager* sim, QWidget* parent) : QWidget(parent) { auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); auto* splitter = new QSplitter(Qt::Horizontal, this); - splitter->addWidget(new widgets::ViewportWidget(splitter)); + splitter->addWidget(new widgets::ViewportWidget(sim, splitter)); splitter->addWidget(new QLabel("Properties(PlaceHolder)", splitter)); + layout->addWidget(splitter); splitter->setStretchFactor(0, 4); // Viewport takes 4/5 of space splitter->setStretchFactor(1, 1); // Properties takes 1/5 of space - layout->addWidget(splitter); } } // namespace Workspace \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Platform/WindowManager.cpp b/DSFE_App/DSFE_GUI/src/Platform/WindowManager.cpp index 967a6c07..ca5ecebe 100644 --- a/DSFE_App/DSFE_GUI/src/Platform/WindowManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Platform/WindowManager.cpp @@ -142,7 +142,7 @@ namespace window { smoothedDt = glm::mix(smoothedDt, dt, 0.2f); if (_sim) { - _sim->handleContinuousMovement(_window, smoothedDt); + //_sim->handleContinuousMovement(_window, smoothedDt); //_sim->getCamera()->applyGravity(smoothedDt, _sim->getPlaneHeight()); } } @@ -192,7 +192,6 @@ namespace window { // Forward window resize events to the SimManager to adjust the internal rendering resolution and aspect ratio void window::GLWindow::onCursorPos(double xpos, double ypos) { // LOG_INFO("Mouse moved to: X=%.2f, Y=%.2f", xpos, ypos); - if (_sim) { _sim->handleMouseLook(_window, xpos, ypos); } } // Window states diff --git a/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp b/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp index d7cb7032..02d46bc6 100644 --- a/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp @@ -242,11 +242,10 @@ namespace render { // CRITICAL: reset ALL framebuffer targets, not just GL_FRAMEBUFFER glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - // Restore default backbuffer state - glDrawBuffer(GL_BACK); - glReadBuffer(GL_BACK); + //// Restore default backbuffer state + //glDrawBuffer(GL_BACK); + //glReadBuffer(GL_BACK); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glDisable(GL_SCISSOR_TEST); @@ -256,8 +255,8 @@ namespace render { void OpenGLFrameBuffer::endSetup() { glBindFramebuffer(GL_FRAMEBUFFER, 0); - glDrawBuffer(GL_BACK); - glReadBuffer(GL_BACK); + //glDrawBuffer(GL_BACK); + //glReadBuffer(GL_BACK); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glDisable(GL_SCISSOR_TEST); } diff --git a/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp b/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp index 93134865..bfa327ab 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp @@ -8,6 +8,8 @@ #include #include "Scene/Camera.h" +#include "Platform/KeyCode.h" + #include "EngineLib/LogMacros.h" namespace scene { @@ -26,12 +28,12 @@ namespace scene { float velocity = _currentSpeed * dt; switch (key) { - case GLFW_KEY_W: moveForward(velocity); break; - case GLFW_KEY_S: moveBackward(velocity); break; - case GLFW_KEY_A: moveLeft(velocity); break; - case GLFW_KEY_D: moveRight(velocity); break; - case GLFW_KEY_SPACE: moveUp(velocity); break; - case GLFW_KEY_LEFT_SHIFT: moveDown(velocity); break; + case static_cast(gui::eKeyCode::W): moveForward(velocity); break; + case static_cast(gui::eKeyCode::A): moveLeft(velocity); break; + case static_cast(gui::eKeyCode::S): moveBackward(velocity); break; + case static_cast(gui::eKeyCode::D): moveRight(velocity); break; + case static_cast(gui::eKeyCode::Space): moveUp(velocity); break; + case static_cast(gui::eKeyCode::LShift): moveDown(velocity); break; } updateViewMatrix(); diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimBackend.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimBackend.cpp index c746a575..ab59ff42 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimBackend.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimBackend.cpp @@ -1,6 +1,6 @@ // DSFE_GUI SimBackend.cpp -#include "Scene/SimulationManager.h" #include "Scene/SimulationCore.h" +#include "Scene/SimulationManager.h" #ifdef __gl_h_ #undef __gl_h_ #endif diff --git a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp index 6d0279c7..6b2612a0 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp @@ -19,6 +19,8 @@ extern "C" void DestroySimulationCore(core::ISimulationCore*); #include +#include "Platform/KeyCode.h" + #include #include "Platform/Paths.h" @@ -90,6 +92,8 @@ namespace gui { if (_glReady) return; _glReady = true; + _impl->initGLResources(*this); + InitShadowResource(_settingsCurrent.shadowMapRes); InitIBL(); @@ -186,7 +190,42 @@ namespace gui { drawViewportWindow(); } - void SimManager::tick(double dt) { _core->tick(dt); } + void SimManager::tick(double dt) { + if (!hasRobot()) { return; } + _core->tick(dt); + } + + void SimManager::renderViewport(int w, int h) { + LOG_INFO_ONCE("renderViewport entered"); + if (!_glReady || !_impl) { return; } + if (w <= 0 || h <= 0) { return; } + if (_impl->_robotSystem && hasRobot()) { + _impl->_robotRenderer->applyTransforms( + _impl->_robotSystem->model(), + _impl->_robotSystem->worldTransforms() + ); + } + if (_core->robotPresentationDirty()) { + loadRobot(_core->robotSystem()->robotName()); + _core->clearRobotPresentationDirty(); + } + _fpsCounter.update(); + auto& view = _impl->_views[static_cast(_impl->activeView)]; + _impl->renderView(*this, view, w, h); + + glBindFramebuffer(GL_FRAMEBUFFER, _presentationFBO); + glViewport(0, 0, w, h); + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + _impl->_presentShader->use(); + _impl->_presentShader->setInt1(0, "screenTexture"); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, view.post->getTexture()); + glBindVertexArray(_impl->_fullscreenVAO); + glDrawArrays(GL_TRIANGLES, 0, 3); + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + } void SimManager::syncRobotToScene() { if (!hasRobot()) return; @@ -227,6 +266,14 @@ namespace gui { //LOG_INFO("Resized SimManager INTERNAL RT to %dx%d", width, height); } + void SimManager::setDisplaySize(int w, int h) { + if (w <= 0.0f || h <= 0.0f) return; + _displaySize = { w, h }; + for (auto& v : _impl->_views) { + v.displayW = 0; v.displayH = 0; // Force per-view reallocation next frame + } + } + // -------------------------------------------------- // INPUT HANDLING // -------------------------------------------------- @@ -237,45 +284,40 @@ namespace gui { else if (ctrlMode == ControlMode::Object && _impl->_mesh) { /*idea is to add multiple angles to switch between!*/ } } - void gui::SimManager::handleContinuousMovement(GLFWwindow* window, float dt) { - auto* win = static_cast(glfwGetWindowUserPointer(window)); - if (!win || !win->isMouseCaptured()) return; + void gui::SimManager::handleContinuousMovement(const std::unordered_set& pressedKeys, float dt) { + if (_impl->viewMode == Impl::ViewMode::Quad) { return; } // No keyboard movement in quad view float kspd = 0.2f * dt; // base speed m/s - if (scene::Input::IsKeyPressed(window, GLFW_KEY_W)) { processMovementKey(GLFW_KEY_W, kspd); } - if (scene::Input::IsKeyPressed(window, GLFW_KEY_S)) { processMovementKey(GLFW_KEY_S, kspd); } - if (scene::Input::IsKeyPressed(window, GLFW_KEY_A)) { processMovementKey(GLFW_KEY_A, kspd); } - if (scene::Input::IsKeyPressed(window, GLFW_KEY_D)) { processMovementKey(GLFW_KEY_D, kspd); } - if (scene::Input::IsKeyPressed(window, GLFW_KEY_SPACE)) { processMovementKey(GLFW_KEY_SPACE, kspd); } - if (scene::Input::IsKeyPressed(window, GLFW_KEY_LEFT_SHIFT)) { processMovementKey(GLFW_KEY_LEFT_SHIFT, kspd); } + LOG_INFO("dt = %f", dt); + + if (pressedKeys.contains(eKeyCode::W)) { processMovementKey((int)eKeyCode::W, kspd); } + if (pressedKeys.contains(eKeyCode::A)) { processMovementKey((int)eKeyCode::A, kspd); } + if (pressedKeys.contains(eKeyCode::S)) { processMovementKey((int)eKeyCode::S, kspd); } + if (pressedKeys.contains(eKeyCode::D)) { processMovementKey((int)eKeyCode::D, kspd); } + if (pressedKeys.contains(eKeyCode::Space)) { processMovementKey((int)eKeyCode::Space, kspd); } + if (pressedKeys.contains(eKeyCode::LShift)) { processMovementKey((int)eKeyCode::LShift, kspd); } } - void gui::SimManager::handleMouseLook(GLFWwindow* window, double xpos, double ypos) { + void gui::SimManager::handleMouseLook(double xpos, double ypos, bool mouseCaptured) { if (_impl->viewMode == Impl::ViewMode::Quad) { return; } // No mouse look in quad view scene::Camera* cam = _impl->_views[static_cast(_impl->activeView)].cam.get(); - auto* win = static_cast(glfwGetWindowUserPointer(window)); - if (!win || !win->isMouseCaptured()) { return; } - - bool captured = true; - if (win == static_cast(glfwGetWindowUserPointer(window))) { captured = win->isMouseCaptured(); } - - if (!captured && !_isHovered) { - _lastMousePos = { (float)xpos, (float)ypos }; + if (!mouseCaptured) { + _lastMousePos = { static_cast(xpos), static_cast(ypos) }; _firstMouse = true; return; } if (_firstMouse) { - _lastMousePos = { (float)xpos, (float)ypos }; + _lastMousePos = { static_cast(xpos), static_cast(ypos) }; _firstMouse = false; } - double xoffset = xpos - _lastMousePos.x; - double yoffset = _lastMousePos.y - ypos; - _lastMousePos = { (float)xpos, (float)ypos }; + double xoffset = xpos; + double yoffset = ypos; + _lastMousePos = { static_cast(xpos), static_cast(ypos) }; - if (ctrlMode == ControlMode::Camera) { cam->processMouseMovement((float)xoffset, (float)yoffset); } + if (ctrlMode == ControlMode::Camera) { cam->processMouseMovement(static_cast(xoffset), static_cast(yoffset)); } else if (ctrlMode == ControlMode::Object && _impl->_selectedObject) { _impl->_selectedObject->onMouseMove(xpos, ypos, scene::eInputButton::Right); } } From c20a3d7a4ac8e03c45f0dcac6fb1b3851452c685 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Fri, 5 Jun 2026 16:24:16 +0100 Subject: [PATCH 009/110] feat: Added control panel and robot selector UI integration --- DSFE_App/DSFE_GUI/CMakeLists.txt | 2 + .../MainWindow/Widgets/ControlPanelWidget.h | 15 +++++++ .../MainWindow/Widgets/RobotSelectorWidget.h | 16 +++++++ .../MainWindow/Widgets/ControlPanelWidget.cpp | 23 ++++++++++ .../Widgets/RobotSelectorWidget.cpp | 43 +++++++++++++++++++ 5 files changed, 99 insertions(+) create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/Widgets/RobotSelectorWidget.h create mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp create mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/Widgets/RobotSelectorWidget.cpp diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 2fbad3fc..80286bb5 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -41,6 +41,8 @@ set(MAIN_WINDOW_SRC src/MainWindow/DSFE_MainWindow.cpp src/MainWindow/Workspace/ProjectPage.cpp src/MainWindow/Widgets/ViewportWidget.cpp + src/MainWindow/Widgets/RobotSelectorWidget.cpp + src/MainWindow/Widgets/ControlPanelWidget.cpp ) set(RENDER_SRC diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h new file mode 100644 index 00000000..57cb7192 --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h @@ -0,0 +1,15 @@ +// DSFE_GUI ControlPanelWidget.h +#pragma once + +#include + +namespace gui { class SimManager; } + +namespace widgets { + class ControlPanelWidget : public QWidget { + public: + explicit ControlPanelWidget(gui::SimManager* sim, QWidget* parent = nullptr); + private: + gui::SimManager* _sim = nullptr; + }; +} // namespace widgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/RobotSelectorWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/RobotSelectorWidget.h new file mode 100644 index 00000000..8631f022 --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/RobotSelectorWidget.h @@ -0,0 +1,16 @@ +// DSFE_GUI RobotSelectorWidget.h +#pragma once + +#include + +namespace gui {class SimManager; } +class QPushButton; +namespace widgets { + class RobotSelectorWidget : public QWidget { + public: + explicit RobotSelectorWidget(gui::SimManager* sim, QWidget* parent = nullptr); + private: + gui::SimManager* _sim; + void addRobotButton(const QString& robotName, QString company); + }; +} // namespace widgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp new file mode 100644 index 00000000..64e7d7e2 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp @@ -0,0 +1,23 @@ +// DSFE_GUI ControlPanelWidget.cpp +#include "Widgets/ControlPanelWidget.h" +#include "Scene/SimulationManager.h" +#include "Widgets/RobotSelectorWidget.h" + +#include +#include + +namespace widgets { + ControlPanelWidget::ControlPanelWidget(gui::SimManager* sim, QWidget* parent) : QWidget(parent), _sim(sim) { + auto* rootLayout = new QVBoxLayout(this); + rootLayout->setContentsMargins(4, 4, 4, 4); + auto* scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + auto* content = new QWidget(scrollArea); + auto* contentLayout = new QVBoxLayout(content); + contentLayout->addWidget(new RobotSelectorWidget(sim, content)); + contentLayout->addStretch(); // push widgets to top + content->setLayout(contentLayout); + scrollArea->setWidget(content); + rootLayout->addWidget(scrollArea); + } +} // namespace widgets} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/RobotSelectorWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/RobotSelectorWidget.cpp new file mode 100644 index 00000000..683a5861 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/RobotSelectorWidget.cpp @@ -0,0 +1,43 @@ +// DSFE_GUI RobotSelectorWidget.cpp +#include "Widgets/RobotSelectorWidget.h" +#include "Scene/SimulationManager.h" + +#include +#include "Platform/SystemMap.h" + +#include +#include +#include + +namespace widgets { + RobotSelectorWidget::RobotSelectorWidget(gui::SimManager* sim, QWidget* parent) : QWidget(parent), _sim(sim) { + setWindowTitle("Choose Robotic Arm"); + setMinimumWidth(300); + auto* layout = new QVBoxLayout(this); + if (!_sim) { + layout->addWidget(new QLabel("No simulation manager available", this)); + return; + } + + const std::unordered_map& robotMap = platform::getRoboticArmMap(); + + if (robotMap.empty()) { + layout->addWidget(new QLabel("No robotic arms available", this)); + return; + } + + for (const auto& [arm, family] : robotMap) { + const QString robotName = QString::fromStdString(platform::RoboticArms().toString(arm)); + const QString familyName = QString::fromStdString(platform::RoboticArms().toString(family)); + addRobotButton(robotName, familyName); + } + } + void RobotSelectorWidget::addRobotButton(const QString& robotName, QString company) { + auto* button = new QPushButton(robotName + "\n" + company); + layout()->addWidget(button); + connect(button, &QPushButton::clicked, this, [this, robotName]() { + LOG_INFO("Selected robot: %s", robotName.toStdString().c_str()); + if (_sim) { _sim->loadRobot(robotName.toStdString()); } + }); + } +} // namespace widgets \ No newline at end of file From 69eb91f1baf353797974b9a63aae8b1918d212c6 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Fri, 5 Jun 2026 16:24:46 +0100 Subject: [PATCH 010/110] feat: Added SystemMap.h for robotic arm enums and mappings --- .../DSFE_GUI/include/Platform/SystemMap.h | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 DSFE_App/DSFE_GUI/include/Platform/SystemMap.h diff --git a/DSFE_App/DSFE_GUI/include/Platform/SystemMap.h b/DSFE_App/DSFE_GUI/include/Platform/SystemMap.h new file mode 100644 index 00000000..ff88305b --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/Platform/SystemMap.h @@ -0,0 +1,60 @@ +// DSFE_GUI SystemMap.h +#pragma once + +#include +#include + +namespace platform { + enum class eRoboticArms { + Z1 = 0, + UR5e = 1, + Panda = 2, + iiwa14 = 3, + VISPA = 4, + H1 = 5 + }; + + enum class eRoboticArmFamilies { + Unitree = 0, + Universal = 1, + Franka = 2, + KUKA = 3, + Airbus = 4 + }; + + struct RoboticArms { + std::unordered_map armMap = { + { eRoboticArms::Z1, eRoboticArmFamilies::Unitree }, + { eRoboticArms::UR5e, eRoboticArmFamilies::Universal }, + { eRoboticArms::Panda, eRoboticArmFamilies::Franka }, + { eRoboticArms::iiwa14, eRoboticArmFamilies::KUKA }, + { eRoboticArms::VISPA, eRoboticArmFamilies::Airbus }, + { eRoboticArms::H1, eRoboticArmFamilies::Unitree } + }; + + inline std::string toString(eRoboticArms arm) { + switch (arm) { + case eRoboticArms::Z1: return "Z1"; + case eRoboticArms::UR5e: return "UR5e"; + case eRoboticArms::Panda: return "Panda"; + case eRoboticArms::iiwa14: return "iiwa14"; + case eRoboticArms::VISPA: return "VISPA"; + case eRoboticArms::H1: return "H1"; + default: return "Unknown"; + } + } + + inline std::string toString(eRoboticArmFamilies family) { + switch (family) { + case eRoboticArmFamilies::Unitree: return "Unitree Robotics"; + case eRoboticArmFamilies::Universal: return "Universal Robots"; + case eRoboticArmFamilies::Franka: return "Franka Robotics"; + case eRoboticArmFamilies::KUKA: return "KUKA"; + case eRoboticArmFamilies::Airbus: return "Airbus"; + default: return "Unknown"; + } + } + }; + + std::unordered_map getRoboticArmMap() { return RoboticArms().armMap; } +} \ No newline at end of file From 00f78c03dada3064157edf65b503d8149edff810 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Fri, 5 Jun 2026 16:28:33 +0100 Subject: [PATCH 011/110] fixes: Removed QOpenGLFunctions from ViewportWidget, add ControlPanel --- DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h | 3 +-- DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h index 006de699..a9e39bcb 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h @@ -2,7 +2,6 @@ #pragma once #include -#include #include #include #include @@ -14,7 +13,7 @@ namespace gui { class SimManager; enum class eKeyCode; } namespace widgets { - class ViewportWidget : public QOpenGLWidget, protected QOpenGLFunctions_4_5_Core { + class ViewportWidget : public QOpenGLWidget { public: explicit ViewportWidget(gui::SimManager* sim, QWidget* parent = nullptr); diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp index 25efa15d..4790322d 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp @@ -2,6 +2,7 @@ #include "Workspace/ProjectPage.h" #include "Scene/SimulationManager.h" #include "Widgets/ViewportWidget.h" +#include "Widgets/ControlPanelWidget.h" #include #include @@ -13,7 +14,7 @@ namespace Workspace { layout->setContentsMargins(0, 0, 0, 0); auto* splitter = new QSplitter(Qt::Horizontal, this); splitter->addWidget(new widgets::ViewportWidget(sim, splitter)); - splitter->addWidget(new QLabel("Properties(PlaceHolder)", splitter)); + splitter->addWidget(new widgets::ControlPanelWidget(sim, splitter)); layout->addWidget(splitter); splitter->setStretchFactor(0, 4); // Viewport takes 4/5 of space splitter->setStretchFactor(1, 1); // Properties takes 1/5 of space From 421db1018d3ef3a5b098938319d80c9711488c99 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Fri, 5 Jun 2026 16:30:15 +0100 Subject: [PATCH 012/110] fixes: Added setFltArray2, cleanup includes, and FBO update * Introduced setFltArray2 for float array uniforms in shaders and update usage for cascade splits. * Removed unused GLFW includes and redundant headers. * Adjusted QApplication initialization order. * Changed shadow pass FBO binding to _presentationFBO. --- DSFE_App/DSFE_GUI/include/Rendering/ShaderUtil.h | 1 + DSFE_App/DSFE_GUI/src/Application.cpp | 3 +-- .../DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp | 3 --- DSFE_App/DSFE_GUI/src/Rendering/ShaderUtil.cpp | 5 +++++ DSFE_App/DSFE_GUI/src/Scene/Camera.cpp | 1 - DSFE_App/DSFE_GUI/src/Scene/Manager/SimRendering.cpp | 10 ++++------ DSFE_App/DSFE_GUI/src/Scene/Mesh.cpp | 4 ++-- DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp | 4 ---- 8 files changed, 13 insertions(+), 18 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/Rendering/ShaderUtil.h b/DSFE_App/DSFE_GUI/include/Rendering/ShaderUtil.h index 4daf22ec..3fae8d15 100644 --- a/DSFE_App/DSFE_GUI/include/Rendering/ShaderUtil.h +++ b/DSFE_App/DSFE_GUI/include/Rendering/ShaderUtil.h @@ -26,6 +26,7 @@ namespace shaders { void setFlt1(float a, const std::string& name); void setFlt2(float a, float b, const std::string& name); void setFlt3(float a, float b, float c, const std::string& name); + void setFltArray2(float a, float b, const std::string& name); void setVec2(const glm::vec2& vec2, const std::string& name); void setVec3(const glm::vec3& vec3, const std::string& name); void setVec4(const glm::vec4& vec4, const std::string& name); diff --git a/DSFE_App/DSFE_GUI/src/Application.cpp b/DSFE_App/DSFE_GUI/src/Application.cpp index 5df7a13a..566b5e38 100644 --- a/DSFE_App/DSFE_GUI/src/Application.cpp +++ b/DSFE_App/DSFE_GUI/src/Application.cpp @@ -38,8 +38,6 @@ Application::Application(const std::string& appName) : _name(appName) { _qtArgv.reserve(_qtArgStorage.size()); for (std::string& arg : _qtArgStorage) { _qtArgv.push_back(arg.data()); } - _qtApp = std::make_unique(_qtArgc, _qtArgv.data()); - QSurfaceFormat format; format.setProfile(QSurfaceFormat::CoreProfile); format.setVersion(4, 5); @@ -47,6 +45,7 @@ Application::Application(const std::string& appName) : _name(appName) { format.setStencilBufferSize(8); QSurfaceFormat::setDefaultFormat(format); + _qtApp = std::make_unique(_qtArgc, _qtArgv.data()); _sim = std::make_unique(); int winW = 1920, winH = 1080; diff --git a/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp b/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp index 02d46bc6..cd60aa44 100644 --- a/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp @@ -4,7 +4,6 @@ #undef __gl_h_ #endif #include -#include #include "Rendering/OpenGLBufferManager.h" #include "EngineLib/LogMacros.h" @@ -38,8 +37,6 @@ namespace render { glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(assets::VertexHolder), (void*)offsetof(assets::VertexHolder, _texCoord)); glBindVertexArray(0); - - //LOG_INFO("OpenGLVertexIndexBuffer buffers created successfully"); } // Deletes the VAO, VBO, and EBO associated with this buffer diff --git a/DSFE_App/DSFE_GUI/src/Rendering/ShaderUtil.cpp b/DSFE_App/DSFE_GUI/src/Rendering/ShaderUtil.cpp index 7c44d4b3..c15ce99d 100644 --- a/DSFE_App/DSFE_GUI/src/Rendering/ShaderUtil.cpp +++ b/DSFE_App/DSFE_GUI/src/Rendering/ShaderUtil.cpp @@ -127,6 +127,11 @@ namespace shaders { GLint matLoc = glGetUniformLocation(getProgramID(), name.c_str()); glUniform3f(matLoc, a, b, c); } + void Shader::setFltArray2(float a, float b, const std::string& name) { + GLint loc = glGetUniformLocation(getProgramID(), name.c_str()); + float vals[2] = { a, b }; + glUniform1fv(loc, 2, vals); + } // Set a vec4 uniform in the shader program void Shader::setVec2(const glm::vec2& vec2, const std::string& name) { GLint matLoc = glGetUniformLocation(getProgramID(), name.c_str()); diff --git a/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp b/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp index bfa327ab..5ec66667 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp @@ -5,7 +5,6 @@ #undef __gl_h_ #endif #include -#include #include "Scene/Camera.h" #include "Platform/KeyCode.h" diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRendering.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRendering.cpp index 2e3dda89..210f7c98 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRendering.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRendering.cpp @@ -178,19 +178,17 @@ namespace gui { const float splitDist0 = nearPlane + splitFrac0 * farPlane; const float splitDist1 = nearPlane + splitFrac1 * farPlane; - shader->setFlt2(splitDist0, splitDist1, "cascadeSplits"); + shader->setFltArray2(splitDist0, splitDist1, "cascadeSplits"); } - // Camera / SunLight / light common to all mesh shaders + // Update camera and light uniforms (only those relevant to the current shader mode) cam->update(shader); _impl->_light->update(shader); // Main mesh rendering loop for (auto& obj : _impl->_objects) { - if (!obj || !obj->getMesh()) continue; - + if (!obj || !obj->getMesh()) { continue; } if (_impl->_cameraFollowTarget == obj.get()) { cam->setFollowTarget(obj->transform.position, obj->transform.rotQ); } - glm::mat4 model = obj->transform.toMatrix() * obj->getMesh()->localTransform; shader->setMat4(model, "model"); @@ -287,7 +285,7 @@ namespace gui { obj->getMesh()->render(); } } - glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, _presentationFBO); glViewport(0, 0, (int)_internalSize.x, (int)_internalSize.y); glDisable(GL_POLYGON_OFFSET_FILL); diff --git a/DSFE_App/DSFE_GUI/src/Scene/Mesh.cpp b/DSFE_App/DSFE_GUI/src/Scene/Mesh.cpp index fc98ea39..7610fc20 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Mesh.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Mesh.cpp @@ -11,7 +11,7 @@ #include "EngineLib/LogMacros.h" namespace scene { - // Mesh Initialization + // Mesh Initialisation void Mesh::init() { _rndrBffrMngr = std::make_unique(); createBuffers(); @@ -39,7 +39,7 @@ namespace scene { void Mesh::unbind() { _rndrBffrMngr->unbind(); } // Render the mesh using the current GPU buffers - void Mesh::render() { _rndrBffrMngr->draw((int) _indices.size()); } + void Mesh::render() { _rndrBffrMngr->draw((int)_indices.size()); } // Clean up CPU and GPU buffers void Mesh::clean() { diff --git a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp index 6b2612a0..42b97400 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp @@ -13,12 +13,8 @@ extern "C" void DestroySimulationCore(core::ISimulationCore*); #undef __gl_h_ #endif #include -#include #include "Manager/SimImplementation.h" - -#include - #include "Platform/KeyCode.h" #include From ac242cac5b42faf31ac20ef63e70e42216bfa218 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Fri, 5 Jun 2026 16:43:01 +0100 Subject: [PATCH 013/110] feat: Added RAII OpenGL context management and context hooks * Introduced ScopeGLContext for RAII-based OpenGL context management using user-provided hooks (this was like 6 hours of debugging btw, knowing it was context but not knowing how to fix it) * Added context hook support to SimManager and set hooks from ViewportWidget. * Added ScopeGLContext for current use in robot loading to ensure correct context. * Added GL_CHECKPOINT macro for error checking and perform minor code clean-ups when debugging (REMOVE FOR RELEASE). --- .../DSFE_GUI/include/Platform/ScopeGLContext.h | 14 ++++++++++++++ .../include/Scene/Manager/SimImplementation.h | 13 +++++-------- .../DSFE_GUI/include/Scene/SimulationManager.h | 17 ++++++++++++++++- .../src/MainWindow/Widgets/ViewportWidget.cpp | 5 ++--- .../DSFE_GUI/src/Scene/Manager/SimRobots.cpp | 2 ++ 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 DSFE_App/DSFE_GUI/include/Platform/ScopeGLContext.h diff --git a/DSFE_App/DSFE_GUI/include/Platform/ScopeGLContext.h b/DSFE_App/DSFE_GUI/include/Platform/ScopeGLContext.h new file mode 100644 index 00000000..756c45d4 --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/Platform/ScopeGLContext.h @@ -0,0 +1,14 @@ +// DSFE_GUI ScopeGLContext.h +#pragma once + +#include + +namespace platform { + class ScopeGLContext { + public: + ScopeGLContext(std::function make, std::function done) : _done(std::move(done)) { if (make) { make(); } } + ~ScopeGLContext() { if (_done) { _done(); } } + private: + std::function _done; + }; +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h index 12ef54e7..b7d7b8b4 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h +++ b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h @@ -134,6 +134,9 @@ namespace gui { Impl(SimManager& owner) { activeView = VID::Manual; viewMode = ViewMode::Single; + + // Robot system with mesh loading (for normal simulation) + _robotSystem = std::make_unique(); } void initGLResources(SimManager& owner) { @@ -260,8 +263,6 @@ namespace gui { _mesh = std::make_shared(); _mesh->init(); - // Robot system with mesh loading (for normal simulation) - _robotSystem = std::make_unique(); _robotRenderer = std::make_unique(); // SSAO shaders @@ -509,12 +510,8 @@ namespace gui { glClearColor(owner._backgroundColour.r, owner._backgroundColour.g, owner._backgroundColour.b, owner._backgroundAlpha); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - if (owner._settingsCurrent.msaaSamples > 1) { - glEnable(GL_MULTISAMPLE); - } - else { - glDisable(GL_MULTISAMPLE); - } + if (owner._settingsCurrent.msaaSamples > 1) { glEnable(GL_MULTISAMPLE); } + else { glDisable(GL_MULTISAMPLE); } // Update Follow Target: use the explicitly bound target (e.g. end-effector from loadRobot), // only fall back to _selectedObject if no explicit target was set via setViewFollowTarget diff --git a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h index a86bc4d5..eae0291d 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h +++ b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h @@ -74,6 +74,11 @@ namespace gui { // OpenGL Initialisation void initGL(); + void setContextHooks(std::function makeCurrentHook, std::function doneCurrentHook) { + _makeCurrentHook = makeCurrentHook; + _doneCurrentHook = doneCurrentHook; + } + void renderViewport(int w, int h); void setDisplaySize(int w, int h); @@ -278,6 +283,9 @@ namespace gui { void resetMouseDelta(); private: + std::function _makeCurrentHook; + std::function _doneCurrentHook; + std::unique_ptr _core = nullptr; std::unique_ptr _studyRunner = nullptr; // Background worker for running batch studies bool _hasCompletedStudy = false; @@ -370,4 +378,11 @@ namespace gui { // Camera & Mouse glm::vec2 _lastMousePos{ 0.f, 0.f }; }; -} // namespace gui \ No newline at end of file +} // namespace gui + +#define GL_CHECKPOINT(name) \ +do { \ + GLenum err = glGetError(); \ + if (err != GL_NO_ERROR) \ + LOG_ERROR("%s -> 0x%X", name, err); \ +} while (0) \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp index cb0c3110..2b89a681 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp @@ -11,6 +11,7 @@ #include #include +#include #include "Platform/KeyCode.h" namespace widgets { @@ -23,10 +24,8 @@ namespace widgets { } void ViewportWidget::initializeGL() { - initializeOpenGLFunctions(); - auto* ctx = QOpenGLContext::currentContext(); - + if (_sim) { _sim->setContextHooks([this]() { this->makeCurrent(); }, [this]() { this->doneCurrent(); }); } if (!ctx) { LOG_ERROR("No current OpenGL context"); return; diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp index ff40df7d..24013153 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp @@ -5,6 +5,7 @@ #undef __gl_h_ #endif #include "Manager/SimImplementation.h" +#include "Platform/ScopeGLContext.h" namespace gui { // Load a robot by name from the robot system @@ -24,6 +25,7 @@ namespace gui { size_t startIdx = _impl->_objects.size(); + platform::ScopeGLContext guard(_makeCurrentHook, _doneCurrentHook); _impl->buildRobotPresentationFromModel(_impl->_robotSystem->model(), *this); _impl->_robotRenderer->applyTransforms( From 37ba554337d7cda190ad71db21f4efe87db07a9f Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 08:05:46 +0100 Subject: [PATCH 014/110] fixes: Corrected stale FragColour assignment in `present.frag.glsl` --- DSFE_App/DSFE_Engine/assets/shaders/present.frag.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSFE_App/DSFE_Engine/assets/shaders/present.frag.glsl b/DSFE_App/DSFE_Engine/assets/shaders/present.frag.glsl index 054f2500..51d0d135 100644 --- a/DSFE_App/DSFE_Engine/assets/shaders/present.frag.glsl +++ b/DSFE_App/DSFE_Engine/assets/shaders/present.frag.glsl @@ -7,5 +7,5 @@ out vec4 FragColour; uniform sampler2D screenTexture; void main() { - FragColour = vec4(1,0,0,1); + FragColour = texture(screenTexture, uv); } \ No newline at end of file From 2ba6b4bb3bd82176d8a44faae0327fe80a6f9af7 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 08:06:03 +0100 Subject: [PATCH 015/110] feat: Added visual outline for top task bar. --- .../include/MainWindow/DSFE_MainWindow.h | 4 + .../src/MainWindow/DSFE_MainWindow.cpp | 102 +++++++++++++++++- .../MainWindow/Widgets/ControlPanelWidget.cpp | 2 +- 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h index 94264db7..15c9d823 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h @@ -9,5 +9,9 @@ namespace window { class DSFE_MainWindow : public QMainWindow { public: explicit DSFE_MainWindow(gui::SimManager* sim, QWidget* parent = nullptr); + + private: + void buildMenuBar(); + gui::SimManager* _sim; }; } diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index 3a06c138..b4bb949e 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -3,13 +3,113 @@ #include "Scene/SimulationManager.h" #include "Workspace/ProjectPage.h" +#include +#include +#include +#include + namespace window { - DSFE_MainWindow::DSFE_MainWindow(gui::SimManager* sim, QWidget* parent) : QMainWindow(parent) { + DSFE_MainWindow::DSFE_MainWindow(gui::SimManager* sim, QWidget* parent) : QMainWindow(parent), _sim(sim) { setWindowTitle("DSFE"); resize(1280, 720); + buildMenuBar(); + auto* page = new Workspace::ProjectPage(sim, this); setCentralWidget(page); } + void DSFE_MainWindow::buildMenuBar() { + auto* fileMenu = menuBar()->addMenu("&File"); + auto* editMenu = menuBar()->addMenu("&Project"); + auto* viewMenu = menuBar()->addMenu("&View"); + auto* ToolsMenu = menuBar()->addMenu("&Tools"); + auto* helpMenu = menuBar()->addMenu("&Help"); + + // File menu + { + auto* openAction = new QAction("Open", this); + connect(openAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> Open"); + }); + auto* saveAction = new QAction("Save", this); + connect(saveAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> Save"); + }); + fileMenu->addSeparator(); + auto* exitAction = new QAction("Exit", this); + connect(exitAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> Exit"); + QApplication::quit(); + }); + } + // Project menu + { + auto* newProjectAction = new QAction("New Project", this); + connect(newProjectAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: Project -> New Project"); + }); + auto* loadProjectAction = new QAction("Load Project", this); + connect(loadProjectAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: Project -> Load Project"); + }); + auto* saveProjectAction = new QAction("Save Project", this); + connect(saveProjectAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: Project -> Save Project"); + }); + fileMenu->addSeparator(); + auto* loadRobotAction = new QAction("Load Robot", this); + connect(loadRobotAction, &QAction::triggered, this, [this]() { + LOG_INFO("Menu clicked: Project -> Load Robot"); + _sim->loadRobot("panda"); + }); + auto* loadMeshAction = new QAction("Load Mesh", this); + connect(loadMeshAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: Project -> Load Mesh"); + }); + auto* loadHDRAction = new QAction("Load HDRI", this); + connect(loadHDRAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: Project -> Load HDRI"); + }); + } + // View menu + { + auto* resetCameraAction = new QAction("Reset Camera", this); + connect(resetCameraAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: View -> Reset Camera"); + }); + auto* toggleGridAction = new QAction("Toggle Grid", this); + toggleGridAction->setCheckable(true); + toggleGridAction->setChecked(true); + connect(toggleGridAction, &QAction::toggled, this, [](bool checked) { + LOG_INFO("Menu toggled: View -> Toggle Grid -> %s", checked ? "On" : "Off"); + }); + } + // Tools menu + { + auto* physicsDebugAction = new QAction("Toggle Physics Debug", this); + physicsDebugAction->setCheckable(true); + physicsDebugAction->setChecked(false); + connect(physicsDebugAction, &QAction::toggled, this, [](bool checked) { + LOG_INFO("Menu toggled: Tools -> Toggle Physics Debug -> %s", checked ? "On" : "Off"); + }); + auto* reloadShadersAction = new QAction("Reload Shaders", this); + connect(reloadShadersAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: Tools -> Reload Shaders"); + }); + auto* diagnosticsAction = new QAction("Run Diagnostics", this); + } + // Help menu + { + auto* aboutAction = new QAction("About", this); + connect(aboutAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: Help -> About"); + }); + auto* docsAction = new QAction("Documentation", this); + connect(docsAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: Help -> Documentation"); + }); + } + } + } // namespace window \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp index 64e7d7e2..8dad56ff 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp @@ -20,4 +20,4 @@ namespace widgets { scrollArea->setWidget(content); rootLayout->addWidget(scrollArea); } -} // namespace widgets} \ No newline at end of file +} // namespace widgets \ No newline at end of file From e14bfe32e8892e3ab82b4b40147fa91d634c303f Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 08:41:17 +0100 Subject: [PATCH 016/110] feat: Reintroduced old layout style from imgui dimesions --- .../src/MainWindow/Workspace/ProjectPage.cpp | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp index 4790322d..c6d6bc5e 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp @@ -12,11 +12,20 @@ namespace Workspace { ProjectPage::ProjectPage(gui::SimManager* sim, QWidget* parent) : QWidget(parent) { auto* layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); - auto* splitter = new QSplitter(Qt::Horizontal, this); - splitter->addWidget(new widgets::ViewportWidget(sim, splitter)); - splitter->addWidget(new widgets::ControlPanelWidget(sim, splitter)); - layout->addWidget(splitter); - splitter->setStretchFactor(0, 4); // Viewport takes 4/5 of space - splitter->setStretchFactor(1, 1); // Properties takes 1/5 of space + auto* rootSplitter = new QSplitter(Qt::Horizontal, this); + auto* centreSplitter = new QSplitter(Qt::Vertical); + auto* rightSplitter = new QSplitter(Qt::Vertical); + rootSplitter->addWidget(new QLabel("Script Editor (TODO)", this)); + centreSplitter->addWidget(new widgets::ViewportWidget(sim, this)); + centreSplitter->addWidget(new QLabel("Console Output (TODO)", this)); + rightSplitter->addWidget(new widgets::ControlPanelWidget(sim, this)); + rightSplitter->addWidget(new QLabel("Scene Object? (TODO)", this)); + rootSplitter->addWidget(centreSplitter); + rootSplitter->addWidget(rightSplitter); + layout->addWidget(rootSplitter); + // Sizing reused from the old imgui layout + rootSplitter->setSizes({ 635, 1016, 393 }); + centreSplitter->setSizes({ 733, 396 }); + rightSplitter->setSizes({ 733, 396 }); } } // namespace Workspace \ No newline at end of file From 5c0acf6c54f19d1e5377ca03fea5b3a6e825b50f Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 09:16:49 +0100 Subject: [PATCH 017/110] feat: Updated menu items in `buildMenuBar` to have actual actions, including loading of robotic systems --- .../include/MainWindow/DSFE_MainWindow.h | 4 +- .../DSFE_GUI/include/Platform/SystemMap.h | 53 ++++++------ .../src/MainWindow/DSFE_MainWindow.cpp | 81 +++++++++++++------ .../Widgets/RobotSelectorWidget.cpp | 9 +-- .../src/Rendering/OpenGLBufferManager.cpp | 2 +- 5 files changed, 90 insertions(+), 59 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h index 15c9d823..da9520c5 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h @@ -2,9 +2,9 @@ #pragma once #include +#include namespace gui { class SimManager; } - namespace window { class DSFE_MainWindow : public QMainWindow { public: @@ -13,5 +13,7 @@ namespace window { private: void buildMenuBar(); gui::SimManager* _sim; + + void buildRobotMenu(QMenu* projectMenu); }; } diff --git a/DSFE_App/DSFE_GUI/include/Platform/SystemMap.h b/DSFE_App/DSFE_GUI/include/Platform/SystemMap.h index ff88305b..b21bd609 100644 --- a/DSFE_App/DSFE_GUI/include/Platform/SystemMap.h +++ b/DSFE_App/DSFE_GUI/include/Platform/SystemMap.h @@ -5,7 +5,7 @@ #include namespace platform { - enum class eRoboticArms { + enum class eRoboticSystems { Z1 = 0, UR5e = 1, Panda = 2, @@ -14,47 +14,48 @@ namespace platform { H1 = 5 }; - enum class eRoboticArmFamilies { + enum class eRoboticSystemFamilies { Unitree = 0, Universal = 1, Franka = 2, KUKA = 3, - Airbus = 4 + Airbus = 4, + Othjer = 5 }; - struct RoboticArms { - std::unordered_map armMap = { - { eRoboticArms::Z1, eRoboticArmFamilies::Unitree }, - { eRoboticArms::UR5e, eRoboticArmFamilies::Universal }, - { eRoboticArms::Panda, eRoboticArmFamilies::Franka }, - { eRoboticArms::iiwa14, eRoboticArmFamilies::KUKA }, - { eRoboticArms::VISPA, eRoboticArmFamilies::Airbus }, - { eRoboticArms::H1, eRoboticArmFamilies::Unitree } + struct RoboticSystems { + std::unordered_map robotMap = { + { eRoboticSystems::Z1, eRoboticSystemFamilies::Unitree }, + { eRoboticSystems::UR5e, eRoboticSystemFamilies::Universal }, + { eRoboticSystems::Panda, eRoboticSystemFamilies::Franka }, + { eRoboticSystems::iiwa14, eRoboticSystemFamilies::KUKA }, + { eRoboticSystems::VISPA, eRoboticSystemFamilies::Airbus }, + { eRoboticSystems::H1, eRoboticSystemFamilies::Unitree } }; - inline std::string toString(eRoboticArms arm) { - switch (arm) { - case eRoboticArms::Z1: return "Z1"; - case eRoboticArms::UR5e: return "UR5e"; - case eRoboticArms::Panda: return "Panda"; - case eRoboticArms::iiwa14: return "iiwa14"; - case eRoboticArms::VISPA: return "VISPA"; - case eRoboticArms::H1: return "H1"; + inline std::string toString(eRoboticSystems sys) { + switch (sys) { + case eRoboticSystems::Z1: return "Z1"; + case eRoboticSystems::UR5e: return "UR5e"; + case eRoboticSystems::Panda: return "Panda"; + case eRoboticSystems::iiwa14: return "iiwa14"; + case eRoboticSystems::VISPA: return "VISPA"; + case eRoboticSystems::H1: return "H1"; default: return "Unknown"; } } - inline std::string toString(eRoboticArmFamilies family) { + inline std::string toString(eRoboticSystemFamilies family) { switch (family) { - case eRoboticArmFamilies::Unitree: return "Unitree Robotics"; - case eRoboticArmFamilies::Universal: return "Universal Robots"; - case eRoboticArmFamilies::Franka: return "Franka Robotics"; - case eRoboticArmFamilies::KUKA: return "KUKA"; - case eRoboticArmFamilies::Airbus: return "Airbus"; + case eRoboticSystemFamilies::Unitree: return "Unitree Robotics"; + case eRoboticSystemFamilies::Universal: return "Universal Robots"; + case eRoboticSystemFamilies::Franka: return "Franka Robotics"; + case eRoboticSystemFamilies::KUKA: return "KUKA"; + case eRoboticSystemFamilies::Airbus: return "Airbus"; default: return "Unknown"; } } }; - std::unordered_map getRoboticArmMap() { return RoboticArms().armMap; } + inline std::unordered_map getRobotSystemMap() { return RoboticSystems().robotMap; } } \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index b4bb949e..ce06bc42 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -3,10 +3,12 @@ #include "Scene/SimulationManager.h" #include "Workspace/ProjectPage.h" +#include "Platform/SystemMap.h" + #include #include -#include #include +#include namespace window { DSFE_MainWindow::DSFE_MainWindow(gui::SimManager* sim, QWidget* parent) : QMainWindow(parent), _sim(sim) { @@ -21,23 +23,23 @@ namespace window { void DSFE_MainWindow::buildMenuBar() { auto* fileMenu = menuBar()->addMenu("&File"); - auto* editMenu = menuBar()->addMenu("&Project"); + auto* projectMenu = menuBar()->addMenu("&Project"); auto* viewMenu = menuBar()->addMenu("&View"); - auto* ToolsMenu = menuBar()->addMenu("&Tools"); + auto* toolsMenu = menuBar()->addMenu("&Tools"); auto* helpMenu = menuBar()->addMenu("&Help"); // File menu { - auto* openAction = new QAction("Open", this); + auto* openAction = fileMenu->addAction("Open"); connect(openAction, &QAction::triggered, this, []() { LOG_INFO("Menu clicked: File -> Open"); }); - auto* saveAction = new QAction("Save", this); + auto* saveAction = fileMenu->addAction("Save"); connect(saveAction, &QAction::triggered, this, []() { LOG_INFO("Menu clicked: File -> Save"); }); fileMenu->addSeparator(); - auto* exitAction = new QAction("Exit", this); + auto* exitAction = fileMenu->addAction("Exit"); connect(exitAction, &QAction::triggered, this, []() { LOG_INFO("Menu clicked: File -> Exit"); QApplication::quit(); @@ -45,40 +47,46 @@ namespace window { } // Project menu { - auto* newProjectAction = new QAction("New Project", this); + auto* newProjectAction = projectMenu->addAction("New Project"); connect(newProjectAction, &QAction::triggered, this, []() { LOG_INFO("Menu clicked: Project -> New Project"); }); - auto* loadProjectAction = new QAction("Load Project", this); + auto* loadProjectAction = projectMenu->addAction("Load Project"); connect(loadProjectAction, &QAction::triggered, this, []() { LOG_INFO("Menu clicked: Project -> Load Project"); }); - auto* saveProjectAction = new QAction("Save Project", this); + auto* saveProjectAction = projectMenu->addAction("Save Project"); connect(saveProjectAction, &QAction::triggered, this, []() { LOG_INFO("Menu clicked: Project -> Save Project"); }); - fileMenu->addSeparator(); - auto* loadRobotAction = new QAction("Load Robot", this); - connect(loadRobotAction, &QAction::triggered, this, [this]() { - LOG_INFO("Menu clicked: Project -> Load Robot"); - _sim->loadRobot("panda"); + projectMenu->addSeparator(); + auto* robotMenu = projectMenu->addMenu("Load Robot"); + connect(robotMenu, &QMenu::aboutToShow, this, [this, robotMenu]() { + robotMenu->clear(); + buildRobotMenu(robotMenu); }); - auto* loadMeshAction = new QAction("Load Mesh", this); - connect(loadMeshAction, &QAction::triggered, this, []() { + auto* loadMeshAction = projectMenu->addAction("Load Mesh"); + connect(loadMeshAction, &QAction::triggered, this, [this]() { LOG_INFO("Menu clicked: Project -> Load Mesh"); + QString path = QFileDialog::getOpenFileName(nullptr, "Select Mesh File", "", "Mesh Files (*.obj *.fbx *.gltf *.dae *.stl"); + if (path.isEmpty()) { return; } + _sim->loadMesh(path.toStdString()); }); - auto* loadHDRAction = new QAction("Load HDRI", this); - connect(loadHDRAction, &QAction::triggered, this, []() { + auto* loadHDRAction = projectMenu->addAction("Load HDRI"); + connect(loadHDRAction, &QAction::triggered, this, [this]() { LOG_INFO("Menu clicked: Project -> Load HDRI"); + QString path = QFileDialog::getOpenFileName(nullptr, "Select HDRI File", "", "HDRI Files (*.hdr *.exr)"); + if (path.isEmpty()) { return; } + _sim->loadNewHDR(path.toStdString()); }); } // View menu { - auto* resetCameraAction = new QAction("Reset Camera", this); + auto* resetCameraAction = viewMenu->addAction("Reset Camera"); connect(resetCameraAction, &QAction::triggered, this, []() { LOG_INFO("Menu clicked: View -> Reset Camera"); }); - auto* toggleGridAction = new QAction("Toggle Grid", this); + auto* toggleGridAction = viewMenu->addAction("Toggle Grid"); toggleGridAction->setCheckable(true); toggleGridAction->setChecked(true); connect(toggleGridAction, &QAction::toggled, this, [](bool checked) { @@ -87,29 +95,50 @@ namespace window { } // Tools menu { - auto* physicsDebugAction = new QAction("Toggle Physics Debug", this); + auto* physicsDebugAction = toolsMenu->addAction("Toggle Physics Debug"); physicsDebugAction->setCheckable(true); physicsDebugAction->setChecked(false); connect(physicsDebugAction, &QAction::toggled, this, [](bool checked) { LOG_INFO("Menu toggled: Tools -> Toggle Physics Debug -> %s", checked ? "On" : "Off"); }); - auto* reloadShadersAction = new QAction("Reload Shaders", this); - connect(reloadShadersAction, &QAction::triggered, this, []() { + auto* reloadShadersAction = toolsMenu->addAction("Reload Shaders"); + connect(reloadShadersAction, &QAction::triggered, this, [this]() { LOG_INFO("Menu clicked: Tools -> Reload Shaders"); + _sim->reloadAllShaders(); + }); + auto* diagnosticsAction = toolsMenu->addAction("Run Diagnostics"); + connect(diagnosticsAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: Tools -> Run Diagnostics"); }); - auto* diagnosticsAction = new QAction("Run Diagnostics", this); } // Help menu { - auto* aboutAction = new QAction("About", this); + auto* aboutAction = helpMenu->addAction("About"); connect(aboutAction, &QAction::triggered, this, []() { LOG_INFO("Menu clicked: Help -> About"); }); - auto* docsAction = new QAction("Documentation", this); + auto* docsAction = helpMenu->addAction("Documentation"); connect(docsAction, &QAction::triggered, this, []() { LOG_INFO("Menu clicked: Help -> Documentation"); }); } } + void DSFE_MainWindow::buildRobotMenu(QMenu* projectMenu) { + const auto& robotMap = platform::getRobotSystemMap(); + std::unordered_map familyMenus; + for (const auto& [sys, family] : robotMap) { + if (!familyMenus.contains(family)) { + QString familyName = QString::fromStdString(platform::RoboticSystems().toString(family)); + familyMenus[family] = projectMenu->addMenu(familyName); + } + QString robotName = QString::fromStdString(platform::RoboticSystems().toString(sys)); + QAction* robotAction = familyMenus[family]->addAction(robotName); + connect(robotAction, &QAction::triggered, this, [this, robotName]() { + LOG_INFO("Menu clicked: Project -> Load Robot -> %s", robotName.toStdString().c_str()); + _sim->loadRobot(robotName.toStdString()); + }); + } + } + } // namespace window \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/RobotSelectorWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/RobotSelectorWidget.cpp index 683a5861..8129530f 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/RobotSelectorWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/RobotSelectorWidget.cpp @@ -2,7 +2,6 @@ #include "Widgets/RobotSelectorWidget.h" #include "Scene/SimulationManager.h" -#include #include "Platform/SystemMap.h" #include @@ -19,16 +18,16 @@ namespace widgets { return; } - const std::unordered_map& robotMap = platform::getRoboticArmMap(); + const std::unordered_map& robotMap = platform::getRobotSystemMap(); if (robotMap.empty()) { layout->addWidget(new QLabel("No robotic arms available", this)); return; } - for (const auto& [arm, family] : robotMap) { - const QString robotName = QString::fromStdString(platform::RoboticArms().toString(arm)); - const QString familyName = QString::fromStdString(platform::RoboticArms().toString(family)); + for (const auto& [sys, family] : robotMap) { + const QString robotName = QString::fromStdString(platform::RoboticSystems().toString(sys)); + const QString familyName = QString::fromStdString(platform::RoboticSystems().toString(family)); addRobotButton(robotName, familyName); } } diff --git a/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp b/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp index cd60aa44..b9ed8165 100644 --- a/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Rendering/OpenGLBufferManager.cpp @@ -174,7 +174,7 @@ namespace render { // Deletes the framebuffer and its associated attachments void OpenGLFrameBuffer::deleteBuffers() { if (_FBO) { - LOG_INFO("Deleting framebuffer buffers"); + /*LOG_INFO("Deleting framebuffer buffers");*/ if (_msaaFBO) glDeleteFramebuffers(1, &_msaaFBO); if (_msaaColour) glDeleteTextures(1, &_msaaColour); if (_msaaDepthRBO) glDeleteRenderbuffers(1, &_msaaDepthRBO); From 88ab656f2bf5b61de33ed616a8627969cdaf8056 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 11:05:30 +0100 Subject: [PATCH 018/110] feat: Implemented Qt6-based version of old imgui integrator selection logic in `simPropertiesPanel` and `buildIntegratorCombos` --- .../MainWindow/Widgets/ControlPanelWidget.h | 112 +++++++++++++++++ .../MainWindow/Widgets/ControlPanelWidget.cpp | 116 ++++++++++++++++-- 2 files changed, 220 insertions(+), 8 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h index 57cb7192..3196e462 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h @@ -2,14 +2,126 @@ #pragma once #include +#include +#include +#include +#include +#include +#include + +#include "Platform/SimulationState.h" +#include "Numerics/IntegrationMethods.h" +#include "Platform/Logger.h" + +namespace scene { + class Mesh; + class Object; + class Light; + class Camera; +} +namespace render { + enum class ResolutionPreset; + enum class QualityPreset; +} + +namespace robots { class RobotSystem; } +namespace diagnostics { class TelemetryRecorder; } namespace gui { class SimManager; } +class QVBoxLayout; +class QCheckBox; +class QComboBox; +class QGroupBox; + namespace widgets { class ControlPanelWidget : public QWidget { public: explicit ControlPanelWidget(gui::SimManager* sim, QWidget* parent = nullptr); private: + struct IntegratorEntry { + integration::eIntegrationMethod method; + const char* name; + }; + + struct ADIntegratorEntry { + integration::eAutoDiffIntegrationMethod method; + const char* name; + }; + + void simPropertiesPanel(); + void buildIntegratorCombos(); + + void jointInfoPanel(); + void displayPanel(); + void selectJointAndFollow(int jointIdx); + gui::SimManager* _sim = nullptr; + + QVBoxLayout* _contentLayout = nullptr; + QGroupBox* _simPropertiesGroup = nullptr; + QCheckBox* _useAutoDiffCheck = nullptr; + QComboBox* _integratorCombo = nullptr; + + static constexpr IntegratorEntry integrators[] = { + { integration::eIntegrationMethod::Euler, "Euler" }, + { integration::eIntegrationMethod::Midpoint, "Midpoint" }, + { integration::eIntegrationMethod::Heun, "Heun" }, + { integration::eIntegrationMethod::Ralston, "Ralston" }, + { integration::eIntegrationMethod::RK4, "RK4" }, + { integration::eIntegrationMethod::RK45, "RK45" }, + { integration::eIntegrationMethod::ImplicitEuler, "Implicit Euler" }, + { integration::eIntegrationMethod::ImplicitMidpoint, "Implicit Midpoint" }, + { integration::eIntegrationMethod::GLRK2, "GLRK2" }, + { integration::eIntegrationMethod::GLRK3, "GLRK3" } + }; + + static constexpr ADIntegratorEntry adIntegrators[] = { + { integration::eAutoDiffIntegrationMethod::AD_ImplicitEuler, "Implicit Euler (AutoDiff)" }, + { integration::eAutoDiffIntegrationMethod::AD_ImplicitMidpoint, "Implicit Midpoint (AutoDiff)" }, + { integration::eAutoDiffIntegrationMethod::AD_GLRK2, "GLRK2 (AutoDiff)" }, + { integration::eAutoDiffIntegrationMethod::AD_GLRK3, "GLRK3 (AutoDiff)" } + }; + + // Current selection state + Selection _selection; + + // Current items selected + std::string _requestedRobot; // name of requested robot to load + std::string _currentObjectName; // name of currently selected object + std::string _currentLinkName; // name of currently selected link + std::string _currentJointName; // name of currently selected joint + std::string _lastLinkName; // name of last selected link + + // Storage of joint angles + std::unordered_map _linkAngles; + + // Internal states + bool simulationRunning = false; + bool _jointSelected = false; + bool diagRunning = false; + bool _robotRequested = false; + bool _hasRobot = false; + bool _openStats = true; + bool _useAutoDiff = false; + // Simulation and diagnostics timing + float simLength = 30.0f; // ~30 seconds default + float diagLength = 15.0f; // ~15 seconds default + double deltaTime = 1.0f / 180.0f; // ~180 FPS default + float simTime = 0.0f; // current simulation time + float diagTime = 0.0f; // current diagnostic time + // Camera properties + int povMode = 0; + float fov = 60.0f; + // Internal Physics + float velocity = 0.0f; + float torque = 0.0f; + float linkLength = 1.0f; + float damping = 0.1f; + float position = 0.0f; + + // Time tracking for simulation updates + std::chrono::high_resolution_clock::time_point simLastUpdateTime = std::chrono::high_resolution_clock::now(); + std::chrono::high_resolution_clock::time_point diagLastUpdateTime = std::chrono::high_resolution_clock::now(); }; } // namespace widgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp index 8dad56ff..8de97d97 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp @@ -1,23 +1,123 @@ // DSFE_GUI ControlPanelWidget.cpp #include "Widgets/ControlPanelWidget.h" -#include "Scene/SimulationManager.h" -#include "Widgets/RobotSelectorWidget.h" -#include #include +#include +#include +#include +#include + +#include "Scene/Mesh.h" +#include "Scene/Object.h" +#include "Scene/Light.h" +#include "Scene/Camera.h" + +#include "Scene/SimulationManager.h" +#include "Robots/RobotSystem.h" + +#include "Analysis/Telemetry.h" +#include "Platform/Paths.h" +#include "EngineLib/LogMacros.h" namespace widgets { - ControlPanelWidget::ControlPanelWidget(gui::SimManager* sim, QWidget* parent) : QWidget(parent), _sim(sim) { + ControlPanelWidget::ControlPanelWidget(gui::SimManager* sim, QWidget* parent) + : QWidget(parent), _sim(sim) + { auto* rootLayout = new QVBoxLayout(this); rootLayout->setContentsMargins(4, 4, 4, 4); auto* scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); auto* content = new QWidget(scrollArea); - auto* contentLayout = new QVBoxLayout(content); - contentLayout->addWidget(new RobotSelectorWidget(sim, content)); - contentLayout->addStretch(); // push widgets to top - content->setLayout(contentLayout); + _contentLayout = new QVBoxLayout(content); + content->setLayout(_contentLayout); scrollArea->setWidget(content); rootLayout->addWidget(scrollArea); + + simPropertiesPanel(); + + _contentLayout->addStretch(); + } + + // SimSetupPanel for + void ControlPanelWidget::simPropertiesPanel() { + auto* robot = _sim->robotSystem(); + _simPropertiesGroup = new QGroupBox("Simulation Properties"); + auto* layout = new QVBoxLayout(_simPropertiesGroup); + _useAutoDiffCheck = new QCheckBox("Enable Automatic Differentiable Integrators"); + layout->addWidget(_useAutoDiffCheck); + _integratorCombo = new QComboBox(); + layout->addWidget(_integratorCombo); + _contentLayout->addWidget(_simPropertiesGroup); + + buildIntegratorCombos(); + connect(_useAutoDiffCheck, &QCheckBox::toggled, this, [this, robot](bool checked) { + _useAutoDiff = checked; + robot->enableAutoDiff(checked); + buildIntegratorCombos(); + }); + + connect(_integratorCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int) { + if (_useAutoDiff) { + auto selectedMethod = static_cast(_integratorCombo->currentData().toInt()); + _sim->setADIntegrationMethod(selectedMethod); + } + else { + auto selectedMethod = static_cast(_integratorCombo->currentData().toInt()); + _sim->setIntegrationMethod(selectedMethod); + } + }); + } + + void ControlPanelWidget::buildIntegratorCombos() { + QSignalBlocker blocker(_integratorCombo); + _integratorCombo->clear(); + if (_useAutoDiff) { + for (const auto& entry : adIntegrators) { + _integratorCombo->addItem(entry.name, static_cast(entry.method)); + } + auto currentMethod = _sim->autoDiffIntegrationMethod(); + int index = _integratorCombo->findData(static_cast(currentMethod)); + if (index != -1) { + _integratorCombo->setCurrentIndex(index); + } + } + else { + for (const auto& entry : integrators) { + _integratorCombo->addItem(entry.name, static_cast(entry.method)); + } + auto currentMethod = _sim->integrationMethod(); + int index = _integratorCombo->findData(static_cast(currentMethod)); + if (index != -1) { + _integratorCombo->setCurrentIndex(index); + } + } + } + + void ControlPanelWidget::jointInfoPanel() { + } + + void ControlPanelWidget::displayPanel() { + } + + void ControlPanelWidget::selectJointAndFollow(int jointIdx) { + if (!_sim || !_sim->hasRobot()) { return; } + + robots::RobotSystem* robot = _sim->robotSystem(); + if (!robot) { return; } + + auto& joints = robot->joints(); + auto& links = robot->links(); + if (joints.empty()) { return; } + + jointIdx = std::clamp(jointIdx, 0, (int)joints.size() - 1); + + const auto& joint = joints[jointIdx]; + _currentJointName = joint.name; + + _selection.type = SelectionType::JOINT; + _selection.index = jointIdx; + _selection.source = SelectionSource::CONTROL_PANEL; + + _sim->followRobotJoint(_currentJointName, glm::vec3(0.0f, 0.2f, 0.6f)); } } // namespace widgets \ No newline at end of file From b0abf123a76e364baacdc10e0d49cc7bab038a44 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 11:05:53 +0100 Subject: [PATCH 019/110] fixes: Small code corrections to compliment new logic --- .../include/Platform/SimulationState.h | 10 ++- .../src/MainWindow/DSFE_MainWindow.cpp | 61 +++++++++++++++++-- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/DSFE_App/DSFE_Core/include/Platform/SimulationState.h b/DSFE_App/DSFE_Core/include/Platform/SimulationState.h index 50173444..446bbee0 100644 --- a/DSFE_App/DSFE_Core/include/Platform/SimulationState.h +++ b/DSFE_App/DSFE_Core/include/Platform/SimulationState.h @@ -38,4 +38,12 @@ struct DSFE_API modes { eRunMode _runMode = eRunMode::Interactive; }; - +// Struct to hold comparison results for integrator analysis +struct ComparisonSnapshot { + std::string integratorName; + std::vector time; // time samples + std::vector errRms; // RMS error time series + std::vector errMax; // Max error time series + std::vector> jointErr; // [joint][sample] + int jointCount = 0; +}; \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index ce06bc42..dc3de915 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -30,13 +30,62 @@ namespace window { // File menu { - auto* openAction = fileMenu->addAction("Open"); - connect(openAction, &QAction::triggered, this, []() { + auto* newMenu = fileMenu->addMenu("New"); + connect(newMenu, &QMenu::aboutToShow, this, [this, newMenu]() { + LOG_INFO("Menu clicked: File -> New"); + auto* newProjectAction = newMenu->addAction("Project"); + connect(newProjectAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> New -> New Project"); + }); + newMenu->addSeparator(); + auto* newScriptAction = newMenu->addAction("Script"); + connect(newScriptAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> New -> New Script"); + }); + }); + auto* openMenu = fileMenu->addMenu("Open"); + connect(openMenu, &QMenu::aboutToShow, this, [this, openMenu]() { LOG_INFO("Menu clicked: File -> Open"); + auto* openProjectAction = openMenu->addAction("Project"); + connect(openProjectAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> Open -> Project"); + }); + openMenu->addSeparator(); + auto* openScriptAction = openMenu->addAction("Script (*.dsl *.txt)"); + connect(openScriptAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> Open -> Script"); + }); }); - auto* saveAction = fileMenu->addAction("Save"); - connect(saveAction, &QAction::triggered, this, []() { + fileMenu->addSeparator(); + auto* saveMenu = fileMenu->addMenu("Save"); + connect(saveMenu, &QMenu::aboutToShow, this, [this, saveMenu]() { LOG_INFO("Menu clicked: File -> Save"); + auto* saveProjectAction = saveMenu->addAction("Project"); + connect(saveProjectAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> Save -> Project"); + }); + auto* saveScriptAction = saveMenu->addAction("Script"); + connect(saveScriptAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> Save -> Script"); + QString path = QFileDialog::getSaveFileName(nullptr, "Save Script", "", "DSL Script Files (*.dsl);;Text Files (*.txt)"); + if (path.isEmpty()) { return; } + LOG_INFO("Selected path: %s", path.toStdString().c_str()); + }); + }); + auto* saveAsMenu = fileMenu->addMenu("Save As"); + connect(saveAsMenu, &QMenu::aboutToShow, this, [this, saveAsMenu]() { + LOG_INFO("Menu clicked: File -> Save As"); + auto* saveProjectAsAction = saveAsMenu->addAction("Project"); + connect(saveProjectAsAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> Save As -> Project"); + }); + auto* saveScriptAsAction = saveAsMenu->addAction("Script"); + connect(saveScriptAsAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> Save As -> Script"); + QString path = QFileDialog::getSaveFileName(nullptr, "Save Script As", "", "DSL Script Files (*.dsl);;Text Files (*.txt)"); + if (path.isEmpty()) { return; } + LOG_INFO("Selected path: %s", path.toStdString().c_str()); + }); }); fileMenu->addSeparator(); auto* exitAction = fileMenu->addAction("Exit"); @@ -70,14 +119,14 @@ namespace window { LOG_INFO("Menu clicked: Project -> Load Mesh"); QString path = QFileDialog::getOpenFileName(nullptr, "Select Mesh File", "", "Mesh Files (*.obj *.fbx *.gltf *.dae *.stl"); if (path.isEmpty()) { return; } - _sim->loadMesh(path.toStdString()); + _sim->loadMesh(path.toStdString()); // Crashes at the moment, TODO fix crash }); auto* loadHDRAction = projectMenu->addAction("Load HDRI"); connect(loadHDRAction, &QAction::triggered, this, [this]() { LOG_INFO("Menu clicked: Project -> Load HDRI"); QString path = QFileDialog::getOpenFileName(nullptr, "Select HDRI File", "", "HDRI Files (*.hdr *.exr)"); if (path.isEmpty()) { return; } - _sim->loadNewHDR(path.toStdString()); + _sim->loadNewHDR_UI(path.toStdString()); }); } // View menu From 171739c2aadd86c4d02fe9c8aaefd335aeb6b653 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 12:37:26 +0100 Subject: [PATCH 020/110] feat: Added basic joint telemetry in the new control panel widget via `jointInfoPanel` and `displayJointInfo` * Not really sure why I did this before getting the scripts working. * Going to add the dt fraction next, then focus on just getting script functioning back! --- .../MainWindow/Widgets/ControlPanelWidget.h | 11 ++ .../MainWindow/Widgets/ControlPanelWidget.cpp | 134 ++++++++++++++++++ DSFE_App/DSFE_GUI/src/ui/ControlPanel.cpp | 27 ---- 3 files changed, 145 insertions(+), 27 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h index 3196e462..6e9b56e2 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h @@ -33,6 +33,8 @@ class QVBoxLayout; class QCheckBox; class QComboBox; class QGroupBox; +class QLabel; +class QSlider; namespace widgets { class ControlPanelWidget : public QWidget { @@ -51,8 +53,11 @@ namespace widgets { void simPropertiesPanel(); void buildIntegratorCombos(); + void buildDtFractions(); void jointInfoPanel(); + void displayJointInfo(const diagnostics::TelemetryRecorder& rec, int& selectedJoint, QVBoxLayout* layout); + void displayPanel(); void selectJointAndFollow(int jointIdx); @@ -62,6 +67,12 @@ namespace widgets { QGroupBox* _simPropertiesGroup = nullptr; QCheckBox* _useAutoDiffCheck = nullptr; QComboBox* _integratorCombo = nullptr; + QLabel* _currentIntegratorLabel = nullptr; + + QGroupBox* _jointInfoGroup = nullptr; + QSlider* _jointIdxSlider = nullptr; + QLabel* _currentSimTimeJointLabel = nullptr; + static constexpr IntegratorEntry integrators[] = { { integration::eIntegrationMethod::Euler, "Euler" }, diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp index 8de97d97..cc36d600 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp @@ -6,6 +6,10 @@ #include #include #include +#include +#include +#include +#include #include "Scene/Mesh.h" #include "Scene/Object.h" @@ -34,6 +38,7 @@ namespace widgets { rootLayout->addWidget(scrollArea); simPropertiesPanel(); + jointInfoPanel(); _contentLayout->addStretch(); } @@ -48,6 +53,7 @@ namespace widgets { _integratorCombo = new QComboBox(); layout->addWidget(_integratorCombo); _contentLayout->addWidget(_simPropertiesGroup); + _currentIntegratorLabel = new QLabel(); buildIntegratorCombos(); connect(_useAutoDiffCheck, &QCheckBox::toggled, this, [this, robot](bool checked) { @@ -60,12 +66,18 @@ namespace widgets { if (_useAutoDiff) { auto selectedMethod = static_cast(_integratorCombo->currentData().toInt()); _sim->setADIntegrationMethod(selectedMethod); + _currentIntegratorLabel->setText(QString("Current Integrator: ") + _integratorCombo->currentText()); } else { auto selectedMethod = static_cast(_integratorCombo->currentData().toInt()); _sim->setIntegrationMethod(selectedMethod); + _currentIntegratorLabel->setText(QString("Current Integrator: ") + _integratorCombo->currentText()); } }); + _currentIntegratorLabel->setWordWrap(true); + layout->addWidget(_currentIntegratorLabel); + + // TODO reintroduce old dt selection logic here, my aim is to still use the visual fraction selection as it looks way better (and its cool). } void ControlPanelWidget::buildIntegratorCombos() { @@ -94,6 +106,128 @@ namespace widgets { } void ControlPanelWidget::jointInfoPanel() { + _jointInfoGroup = new QGroupBox("Robot Joint Information"); + auto* layout = new QVBoxLayout(_jointInfoGroup); + _contentLayout->addWidget(_jointInfoGroup); + + if (!_sim->hasRobot()) { + layout->addWidget(new QLabel("No Robot Loaded.")); + return; + } + robots::RobotSystem* robot = _sim->robotSystem(); + if (!robot) { + layout->addWidget(new QLabel("Robotic system unavailable")); + return; + } + auto& joints = robot->joints(); + auto& links = robot->links(); + + + _jointIdxSlider = new QSlider(Qt::Orientation::Horizontal); + layout->addWidget(_jointIdxSlider); + + static int currentJointIndex = 0; + currentJointIndex = std::clamp(currentJointIndex, 0, (int)joints.size() - 1); + + auto& j = joints[currentJointIndex]; + int linkIndex = currentJointIndex; + linkIndex = std::clamp(linkIndex, 0, (int)links.size() - 1); + auto& l = links[linkIndex]; + + const auto& rec = _sim->telemetry(); + displayJointInfo(rec, currentJointIndex, layout); + layout->addSpacing(5); + layout->addWidget(new QLabel("Selected Joint: " + QString::fromStdString(j.name) + " - Child Link: " + QString::fromStdString(l.name))); + } + + void ControlPanelWidget::displayJointInfo(const diagnostics::TelemetryRecorder& rec, int& selectedJoint, QVBoxLayout* layout) { + const auto& ring = rec.ring; + if (ring.size() < 1) { return; } + const diagnostics::TelemetrySample& s = ring.at(ring.size() - 1); // Get the most recent sample + if (selectedJoint < 0) { selectedJoint = 0; } + if (selectedJoint >= (int)s.j.size()) { selectedJoint = (int)s.j.size() - 1; } + + _currentSimTimeJointLabel = new QLabel(QString("Current Simulation Time: ") + QString::number(s.timeSec) + " s"); + layout->addWidget(_currentSimTimeJointLabel); + + int currentJ = selectedJoint + 1; + if (!_jointIdxSlider) { + selectedJoint = currentJ - 1; + + _jointIdxSlider->setMinimum(1); + _jointIdxSlider->setMaximum((int)s.j.size()); + _jointIdxSlider->setValue(currentJ); + connect(_jointIdxSlider, &QSlider::valueChanged, this, [this](int value) { + int jointIdx = value - 1; + selectJointAndFollow(jointIdx); + }); + } + else { + selectedJoint = currentJ - 1; + _jointIdxSlider->setMaximum((int)s.j.size()); + _jointIdxSlider->setValue(currentJ); + } + + const diagnostics::JointTelemetry& j = s.j[selectedJoint]; + const float e = static_cast(j.q_ref - j.q); + + layout->addSpacing(10); + + auto headerFont = [](QLabel* label) { + QFont font = label->font(); + font.setBold(true); + font.setPointSize(font.pointSize() + 2); + label->setFont(font); + }; + + + auto* stateLabel = new QLabel(QString("State: ")); + headerFont(stateLabel); + auto* refLabel = new QLabel(QString("Reference: ")); + headerFont(refLabel); + auto* trajLabel = new QLabel(QString("Trajectory: ")); + headerFont(trajLabel); + auto* clampLabel = new QLabel(QString("Clamped: ")); + headerFont(clampLabel); + auto* constLabel = new QLabel(QString("Constants: ")); + headerFont(constLabel); + + // Joint State Telemetry + layout->addWidget(stateLabel); + layout->addWidget(new QLabel(QString("pos: ") + QString::number(j.q) + " rad")); + layout->addWidget(new QLabel(QString("vel: ") + QString::number(j.qd) + " rad/s")); + layout->addWidget(new QLabel(QString("torque: ") + QString::number(j.torqueNm) + " Nm")); + + layout->addSpacing(5); + + // Joint Reference Telemetry + layout->addWidget(refLabel); + layout->addWidget(new QLabel(QString("pos_ref: ") + QString::number(j.q_ref) + " rad")); + layout->addWidget(new QLabel(QString("vel_ref: ") + QString::number(j.qd_ref) + " rad/s")); + layout->addWidget(new QLabel(QString("acc_ref: ") + QString::number(j.qdd_ref) + " rad/s²")); + layout->addWidget(new QLabel(QString("error: ") + QString::number(e) + " rad")); + + layout->addSpacing(5); + + // Joint Trajectory Telemetry + layout->addWidget(trajLabel); + layout->addWidget(new QLabel(QString("pos_traj: ") + QString::number(j.traj_q) + " rad")); + layout->addWidget(new QLabel(QString("vel_traj: ") + QString::number(j.traj_qd) + " rad/s")); + layout->addWidget(new QLabel(QString("acc_traj: ") + QString::number(j.traj_qdd) + " rad/s²")); + + layout->addSpacing(5); + + // Joint Clamping Telemetry + layout->addWidget(clampLabel); + layout->addWidget(new QLabel(QString("pos_clamped: ") + QString(j.clampTheta ? "true" : "false"))); + layout->addWidget(new QLabel(QString("vel_clamped: ") + QString(j.clampOmega ? "true" : "false"))); + + layout->addSpacing(5); + + // Joint Constants Telemetry + layout->addWidget(constLabel); + layout->addWidget(new QLabel(QString("damping: ") + QString::number(j.damping) + " kg·m²/s")); + layout->addWidget(new QLabel(QString("friction: ") + QString::number(j.friction) + " N·m")); } void ControlPanelWidget::displayPanel() { diff --git a/DSFE_App/DSFE_GUI/src/ui/ControlPanel.cpp b/DSFE_App/DSFE_GUI/src/ui/ControlPanel.cpp index c3122a33..50f2b096 100644 --- a/DSFE_App/DSFE_GUI/src/ui/ControlPanel.cpp +++ b/DSFE_App/DSFE_GUI/src/ui/ControlPanel.cpp @@ -518,33 +518,6 @@ namespace gui { } } - //// Torque mode combo box - //ImGui::SectionHeader("Torque Mode"); - //ImGui::SetNextItemWidth(150.0f); - //if (ImGui::BeginCombo("##tau_mode", currentTorqueMode)) { - // for (int n = 0; n < IM_ARRAYSIZE(torqueModeNames); ++n) { - // bool isSelected = (n == static_cast(currentTauEnum)); - - // // When a new mode is selected, update the robot's torque mode and log the change - // if (ImGui::Selectable(torqueModeNames[n], isSelected)) { - // auto updatedMode = static_cast(n); - // robot->setTorqueMode(updatedMode); - - // switch (updatedMode) { - // case robots::eTorqueMode::NONE: - // D_INFO("Torque mode set to None"); break; - // case robots::eTorqueMode::PASSIVE: - // D_INFO("Torque mode set to Passive"); break; - // case robots::eTorqueMode::CONTROLLED: - // D_INFO("Torque mode set to Controlled"); break; - // default: - // break; - // } - // } - // if (isSelected) { ImGui::SetItemDefaultFocus(); } - // } - // ImGui::EndCombo(); - //} ImGui::EndDisabled(); // Delta time controls From 51add8503d0c2aa00f80979dbd0977831f08e8ba Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 12:55:00 +0100 Subject: [PATCH 021/110] feat: refactor old fraction logic from imgui widgets into Qt6 * With help from AI for this, only for the actual Qt6 syntax (still learning). --- DSFE_App/DSFE_GUI/CMakeLists.txt | 1 + .../Widgets/FractionSelectorWidget.h | 27 +++++++++ .../Widgets/FractionSelectorWidget.cpp | 55 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/Widgets/FractionSelectorWidget.h create mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/Widgets/FractionSelectorWidget.cpp diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 80286bb5..8b8572de 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -43,6 +43,7 @@ set(MAIN_WINDOW_SRC src/MainWindow/Widgets/ViewportWidget.cpp src/MainWindow/Widgets/RobotSelectorWidget.cpp src/MainWindow/Widgets/ControlPanelWidget.cpp + src/MainWindow/Widgets/FractionSelectorWidget.cpp ) set(RENDER_SRC diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/FractionSelectorWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/FractionSelectorWidget.h new file mode 100644 index 00000000..03c9b854 --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/FractionSelectorWidget.h @@ -0,0 +1,27 @@ +// DSFE_GUI FractionSelectorWidget.h +#pragma once + +#include + +namespace widgets { + class FractionSelectorWidget : public QWidget { + public: + explicit FractionSelectorWidget(bool telemetryMode = false, QWidget* parent = nullptr); + double dt() const; + + signals: + void valueChanged(double denom); + + protected: + void paintEvent(QPaintEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + + private: + int _k = 6; + int _minK = 1; + int _lastMouseX = 0; + bool _telemetryMode = false; // If true, the widget is being used to select a telemetry recording fraction, so it should display "Telemetry: 1/k" instead of "dt: 1/k" + }; +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/FractionSelectorWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/FractionSelectorWidget.cpp new file mode 100644 index 00000000..72c0f45e --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/FractionSelectorWidget.cpp @@ -0,0 +1,55 @@ +// DSFE_GUI FractionSelectorWidget.cpp +#include "Widgets/FractionSelectorWidget.h" + +#include +#include +#include + +namespace widgets { + FractionSelectorWidget::FractionSelectorWidget(bool telemetryMode, QWidget* parent) + : QWidget(parent), _telemetryMode(telemetryMode) + { + setMinimumSize(100, 70); + } + + double FractionSelectorWidget::dt() const { + return 1.0 / static_cast(10 * _k); + } + + void FractionSelectorWidget::paintEvent(QPaintEvent* event) { + QPainter p(this); + QString numer = "1"; + QString denom = QString::number(10 * _k); + QFontMetrics fm(p.font()); + + int centreX = width() / 2; + int numerW = fm.horizontalAdvance(numer); + int denomW = fm.horizontalAdvance(denom); + + p.drawText(centreX - numerW / 2, 20, numer); + p.drawLine(centreX - std::max(numerW, denomW) / 2 - 2, 30, centreX + std::max(numerW, denomW) / 2 + 2, 30); + p.drawText(centreX - denomW / 2, 50, denom); + } + + void FractionSelectorWidget::mousePressEvent(QMouseEvent* event) { _lastMouseX = event->pos().x(); } + + void FractionSelectorWidget::mouseMoveEvent(QMouseEvent* event) { + int dx = event->pos().x() - _lastMouseX; + if (dx != 0) { + _k += dx; + int max = _telemetryMode ? 100 : 5000; + _k = std::clamp(_k, _minK, max); + _lastMouseX = event->pos().x(); + update(); + emit valueChanged(1.0 / static_cast(10 * _k)); + } + } + + void FractionSelectorWidget::wheelEvent(QWheelEvent* event) { + int delta = event->angleDelta().y() > 0 ? 1 : -1; + int max = _telemetryMode ? 100 : 5000; + _k = std::clamp(_k + delta, _minK, max); + update(); + emit valueChanged(1.0 / static_cast(10 * _k)); + } +} \ No newline at end of file From e6b0a923bf96ba1d48c8c6911c678fdcdc53ffaf Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 13:26:07 +0100 Subject: [PATCH 022/110] feat: Implemented timestepping fractions into `simPropertiesPanel()` --- .../MainWindow/Widgets/ControlPanelWidget.h | 4 +++ .../MainWindow/Widgets/ControlPanelWidget.cpp | 25 ++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h index 6e9b56e2..b0892c54 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h @@ -37,6 +37,8 @@ class QLabel; class QSlider; namespace widgets { + class FractionSelectorWidget; + class ControlPanelWidget : public QWidget { public: explicit ControlPanelWidget(gui::SimManager* sim, QWidget* parent = nullptr); @@ -68,6 +70,8 @@ namespace widgets { QCheckBox* _useAutoDiffCheck = nullptr; QComboBox* _integratorCombo = nullptr; QLabel* _currentIntegratorLabel = nullptr; + FractionSelectorWidget* _simDtSelector = nullptr; + FractionSelectorWidget* _telemetryDtSelector = nullptr; QGroupBox* _jointInfoGroup = nullptr; QSlider* _jointIdxSlider = nullptr; diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp index cc36d600..37d9d133 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp @@ -11,6 +11,8 @@ #include #include +#include "Widgets/FractionSelectorWidget.h" + #include "Scene/Mesh.h" #include "Scene/Object.h" #include "Scene/Light.h" @@ -46,14 +48,28 @@ namespace widgets { // SimSetupPanel for void ControlPanelWidget::simPropertiesPanel() { auto* robot = _sim->robotSystem(); + _simPropertiesGroup = new QGroupBox("Simulation Properties"); auto* layout = new QVBoxLayout(_simPropertiesGroup); + _useAutoDiffCheck = new QCheckBox("Enable Automatic Differentiable Integrators"); layout->addWidget(_useAutoDiffCheck); _integratorCombo = new QComboBox(); layout->addWidget(_integratorCombo); - _contentLayout->addWidget(_simPropertiesGroup); + _currentIntegratorLabel = new QLabel(); + layout->addWidget(_currentIntegratorLabel); + + layout->addSpacing(5); + + _simDtSelector = new FractionSelectorWidget(false); + layout->addWidget(new QLabel("Simulation Time Step (dt):")); + layout->addWidget(_simDtSelector); + _telemetryDtSelector = new FractionSelectorWidget(true); + layout->addWidget(new QLabel("Telemetry Time Step (dt):")); + layout->addWidget(_telemetryDtSelector); + + _contentLayout->addWidget(_simPropertiesGroup); buildIntegratorCombos(); connect(_useAutoDiffCheck, &QCheckBox::toggled, this, [this, robot](bool checked) { @@ -66,18 +82,19 @@ namespace widgets { if (_useAutoDiff) { auto selectedMethod = static_cast(_integratorCombo->currentData().toInt()); _sim->setADIntegrationMethod(selectedMethod); + _currentIntegratorLabel->setWordWrap(true); _currentIntegratorLabel->setText(QString("Current Integrator: ") + _integratorCombo->currentText()); } else { auto selectedMethod = static_cast(_integratorCombo->currentData().toInt()); _sim->setIntegrationMethod(selectedMethod); + _currentIntegratorLabel->setWordWrap(true); _currentIntegratorLabel->setText(QString("Current Integrator: ") + _integratorCombo->currentText()); } }); - _currentIntegratorLabel->setWordWrap(true); - layout->addWidget(_currentIntegratorLabel); - // TODO reintroduce old dt selection logic here, my aim is to still use the visual fraction selection as it looks way better (and its cool). + connect(_simDtSelector, &FractionSelectorWidget::valueChanged, this, [this](double dt) { _sim->setFixedDt(dt); }); + connect(_telemetryDtSelector, &FractionSelectorWidget::valueChanged, this, [this](double dt) { _sim->setTelemetryHz(1.0/dt); }); } void ControlPanelWidget::buildIntegratorCombos() { From c41f5cea57b1fd8fc619746eaac88681e4710780 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 13:26:43 +0100 Subject: [PATCH 023/110] fixes: Added declaration of QObject for class def, hardcoded header location in CMAKE for MOC --- DSFE_App/DSFE_GUI/CMakeLists.txt | 1 + .../DSFE_GUI/include/MainWindow/Widgets/FractionSelectorWidget.h | 1 + 2 files changed, 2 insertions(+) diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 8b8572de..9ba5c59f 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -44,6 +44,7 @@ set(MAIN_WINDOW_SRC src/MainWindow/Widgets/RobotSelectorWidget.cpp src/MainWindow/Widgets/ControlPanelWidget.cpp src/MainWindow/Widgets/FractionSelectorWidget.cpp + include/MainWindow/Widgets/FractionSelectorWidget.h ) set(RENDER_SRC diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/FractionSelectorWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/FractionSelectorWidget.h index 03c9b854..5ebc5946 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/FractionSelectorWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/FractionSelectorWidget.h @@ -5,6 +5,7 @@ namespace widgets { class FractionSelectorWidget : public QWidget { + Q_OBJECT public: explicit FractionSelectorWidget(bool telemetryMode = false, QWidget* parent = nullptr); double dt() const; From a760994f8d71af04a51fd6fc4e68f35a9532dd95 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 15:09:08 +0100 Subject: [PATCH 024/110] feat(WIP): Added basic DSL script loading and running (which needs fixing) * The running is malformed somewhere (need to fix now) * I believe Qt6 means I can add syntax highlighting!! * Need to coordinate colours --- DSFE_App/DSFE_GUI/CMakeLists.txt | 2 + .../include/MainWindow/DSFE_MainWindow.h | 3 + .../MainWindow/Widgets/DSLEditorWidget.h | 64 ++++++ .../MainWindow/Workspace/ProjectPage.h | 5 + .../src/MainWindow/DSFE_MainWindow.cpp | 28 ++- .../MainWindow/Widgets/DSLEditorWidget.cpp | 201 ++++++++++++++++++ .../src/MainWindow/Workspace/ProjectPage.cpp | 7 +- 7 files changed, 298 insertions(+), 12 deletions(-) create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h create mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 9ba5c59f..3f8d1e6c 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -43,6 +43,8 @@ set(MAIN_WINDOW_SRC src/MainWindow/Widgets/ViewportWidget.cpp src/MainWindow/Widgets/RobotSelectorWidget.cpp src/MainWindow/Widgets/ControlPanelWidget.cpp + src/MainWindow/Widgets/DSLEditorWidget.cpp + src/MainWindow/Widgets/FractionSelectorWidget.cpp include/MainWindow/Widgets/FractionSelectorWidget.h ) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h index da9520c5..9f381d08 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h @@ -5,6 +5,8 @@ #include namespace gui { class SimManager; } +namespace widgets { class DSLEditorWidget; } + namespace window { class DSFE_MainWindow : public QMainWindow { public: @@ -13,6 +15,7 @@ namespace window { private: void buildMenuBar(); gui::SimManager* _sim; + widgets::DSLEditorWidget* _dslEditor = nullptr; void buildRobotMenu(QMenu* projectMenu); }; diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h new file mode 100644 index 00000000..9ae11d6d --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h @@ -0,0 +1,64 @@ +// DSFE_GUI DSLEditorWidget.h +#pragma once + +#include + +#include +#include +#include +#include + +class QTextEdit; +class QLabel; +class QPushButton; +class QTabWidget; +class QTimer; + +namespace gui { class SimManager; } +namespace interpreter { + class Parser; + class IStoredProgram; + class RunWrapper; +} + +namespace widgets { + class DSLEditorWidget : public QWidget { + public: + explicit DSLEditorWidget(gui::SimManager* sim, QWidget* parent = nullptr); + + bool loadScript(const QString& fileName); + bool saveScript(const QString& fileName); + + private: + void runScript(); + void stopScript(); + + void buildEditorTab(); + void buildHelpTab(); + + void terminateScript(const char* reason, bool fault); + void pollScriptState(); + void updateButtonState(bool running); + void setStatusMessage(const QString& message); + + void setScript(const std::string& s) { _scriptText = s; } + std::string getScript() const { return _scriptText; } + + std::string _scriptText; + + gui::SimManager* _sim = nullptr; + interpreter::Parser* _parser; + interpreter::IStoredProgram* _program; + interpreter::RunWrapper* _wrapper; + + QTabWidget* _tabs = nullptr; + QWidget* _editorTab = nullptr; + QWidget* _helpTab = nullptr; + QTextEdit* _scriptEditor = nullptr; + QTextEdit* _dslHelp = nullptr; + QPushButton* _runStopButton = nullptr; + QLabel* _statusLabel = nullptr; + QString _currentScriptPath; + QTimer* _stateTimer = nullptr; + }; +} // namespace widgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h b/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h index eac14829..bc338d3a 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h @@ -4,10 +4,15 @@ #include namespace gui { class SimManager; } +namespace widgets { class DSLEditorWidget; } namespace Workspace { class ProjectPage : public QWidget { public: explicit ProjectPage(gui::SimManager* sim, QWidget* parent = nullptr); + widgets::DSLEditorWidget* editor() const; + + private: + widgets::DSLEditorWidget* _editor = nullptr; }; } // namespace Workspace \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index dc3de915..7f42a8f6 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -2,6 +2,7 @@ #include "MainWindow/DSFE_MainWindow.h" #include "Scene/SimulationManager.h" #include "Workspace/ProjectPage.h" +#include "Widgets/DSLEditorWidget.h" #include "Platform/SystemMap.h" @@ -11,13 +12,16 @@ #include namespace window { - DSFE_MainWindow::DSFE_MainWindow(gui::SimManager* sim, QWidget* parent) : QMainWindow(parent), _sim(sim) { + DSFE_MainWindow::DSFE_MainWindow(gui::SimManager* sim, QWidget* parent) + : QMainWindow(parent), _sim(sim), _dslEditor(nullptr) + { setWindowTitle("DSFE"); resize(1280, 720); buildMenuBar(); auto* page = new Workspace::ProjectPage(sim, this); + _dslEditor = page->editor(); setCentralWidget(page); } @@ -52,8 +56,10 @@ namespace window { }); openMenu->addSeparator(); auto* openScriptAction = openMenu->addAction("Script (*.dsl *.txt)"); - connect(openScriptAction, &QAction::triggered, this, []() { - LOG_INFO("Menu clicked: File -> Open -> Script"); + connect(openScriptAction, &QAction::triggered, this, [this]() { + QString fileName = QFileDialog::getOpenFileName(nullptr, "Open Script", "", "DSL Script Files (*.dsl);;Text Files (*.txt)"); + if (fileName.isEmpty()) { return; } + _dslEditor->loadScript(fileName); }); }); fileMenu->addSeparator(); @@ -65,11 +71,11 @@ namespace window { LOG_INFO("Menu clicked: File -> Save -> Project"); }); auto* saveScriptAction = saveMenu->addAction("Script"); - connect(saveScriptAction, &QAction::triggered, this, []() { + connect(saveScriptAction, &QAction::triggered, this, [this]() { LOG_INFO("Menu clicked: File -> Save -> Script"); - QString path = QFileDialog::getSaveFileName(nullptr, "Save Script", "", "DSL Script Files (*.dsl);;Text Files (*.txt)"); - if (path.isEmpty()) { return; } - LOG_INFO("Selected path: %s", path.toStdString().c_str()); + QString fileName = QFileDialog::getSaveFileName(nullptr, "Save Script", "", "DSL Script Files (*.dsl);;Text Files (*.txt)"); + if (fileName.isEmpty()) { return; } + _dslEditor->saveScript(fileName); }); }); auto* saveAsMenu = fileMenu->addMenu("Save As"); @@ -80,11 +86,11 @@ namespace window { LOG_INFO("Menu clicked: File -> Save As -> Project"); }); auto* saveScriptAsAction = saveAsMenu->addAction("Script"); - connect(saveScriptAsAction, &QAction::triggered, this, []() { + connect(saveScriptAsAction, &QAction::triggered, this, [this]() { LOG_INFO("Menu clicked: File -> Save As -> Script"); - QString path = QFileDialog::getSaveFileName(nullptr, "Save Script As", "", "DSL Script Files (*.dsl);;Text Files (*.txt)"); - if (path.isEmpty()) { return; } - LOG_INFO("Selected path: %s", path.toStdString().c_str()); + QString fileName = QFileDialog::getSaveFileName(nullptr, "Save Script As", "", "DSL Script Files (*.dsl);;Text Files (*.txt)"); + if (fileName.isEmpty()) { return; } + _dslEditor->saveScript(fileName); }); }); fileMenu->addSeparator(); diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp new file mode 100644 index 00000000..f2ed6d59 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp @@ -0,0 +1,201 @@ +// DSFE_GUI DSLEditorWidget.cpp +#include "Widgets/DSLEditorWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Platform/StudyRunner.h" +#include "Interpreter/RunWrapper.h" + +#include +#include +#include + +#include "Scene/SimulationManager.h" +#include "Platform/Paths.h" + +#include "Numerics/IntegrationMethods.h" +#include "Interpreter/StoredProgram.h" + +#include "EngineLib/LogMacros.h" + +namespace widgets { + DSLEditorWidget::DSLEditorWidget(gui::SimManager* sim, QWidget* parent) + : QWidget(parent), _sim(sim), _parser(nullptr), _program(nullptr), _wrapper(nullptr) + { + auto* rootLayout = new QVBoxLayout(this); + auto* buttonLayout = new QHBoxLayout(); + + _runStopButton = new QPushButton("Run", this); // May also move to the menu, but I want to test the logic first + + buttonLayout->addStretch(); + buttonLayout->addWidget(_runStopButton); + + rootLayout->addLayout(buttonLayout); + + _tabs = new QTabWidget(this); + rootLayout->addWidget(_tabs); + + buildEditorTab(); + buildHelpTab(); + + _statusLabel = new QLabel("Idle", this); + rootLayout->addWidget(_statusLabel); + + _stateTimer = new QTimer(this); + connect(_stateTimer, &QTimer::timeout, this, [this]() { pollScriptState(); }); + _stateTimer->start(100); // Poll every 100 ms + + connect(_runStopButton, &QPushButton::clicked, this, [this]() { + if (_sim->isScriptRunning()) { stopScript(); } + else { runScript(); } + }); + + pollScriptState(); + } + + bool DSLEditorWidget::loadScript(const QString& fileName) { + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + LOG_ERROR("Failed to open script file: %s", fileName.toStdString().c_str()); + return false; + } + _scriptEditor->setPlainText(file.readAll()); + file.close(); + _currentScriptPath = fileName; + + LOG_INFO("DSL script loaded from file: %s", fileName.toStdString().c_str()); + D_INFO("DSL script loaded from file: %s", fileName.toStdString().c_str()); + + return true; + } + + bool DSLEditorWidget::saveScript(const QString& fileName) { + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + LOG_ERROR("Failed to open script file for writing: %s", fileName.toStdString().c_str()); + return false; + } + file.write(_scriptEditor->toPlainText().toUtf8()); + file.close(); + _currentScriptPath = fileName; + + LOG_INFO("DSL script saved to file: %s", fileName.toStdString().c_str()); + D_INFO("DSL script saved to file: %s", fileName.toStdString().c_str()); + + return true; + } + + void DSLEditorWidget::setStatusMessage(const QString& message) { if (_statusLabel) { _statusLabel->setText(message); } } + + void DSLEditorWidget::buildEditorTab() { + _editorTab = new QWidget(this); + auto* layout = new QVBoxLayout(_editorTab); + _scriptEditor = new QTextEdit(_editorTab); + layout->addWidget(_scriptEditor); + _tabs->addTab(_editorTab, "Script Editor"); + } + + void DSLEditorWidget::buildHelpTab() { + _helpTab = new QWidget(this); + auto* layout = new QVBoxLayout(_helpTab); + _dslHelp = new QTextEdit(_helpTab); + _dslHelp->setReadOnly(true); + + + _dslHelp->setPlainText( + "trajSet(...)\n" + "trajClear()\n" + "rotateJointTo(...)\n" + "rotateJointBy(...)\n" + "load(...)\n" + "set(...)\n" + "start()\n" + "stop()\n" + "wait(...)\n" + ); + + + layout->addWidget(_dslHelp); + _tabs->addTab(_helpTab, "DSL Help"); + } + + void DSLEditorWidget::runScript() { + if (_sim->isScriptRunning()) { return; } + LOG_INFO("DSL script started."); D_INFO("DSL script started."); + + _sim->setActiveProgram(nullptr); + delete _wrapper; _wrapper = nullptr; + delete _parser; _parser = nullptr; + delete _program; _program = nullptr; + + _program = new interpreter::StoredProgram(_sim->simCoreInterface()); + _parser = new interpreter::Parser(_program); + _wrapper = new interpreter::RunWrapper(_parser, _program); + + _scriptText = _scriptEditor->toPlainText().toStdString(); + + _sim->setActiveProgram(_program); + _sim->setScriptRunning(true); + _sim->setLastScriptText(_scriptText); + + std::string code = _scriptText; + if (!code.empty() && code.back() == '\0') { code.pop_back(); } + + _wrapper->runProgram(_scriptText); + + updateButtonState(true); + } + + void DSLEditorWidget::stopScript() { + if (!_sim->isScriptRunning()) { return; } + if (_sim->activeProgram()) { + _sim->activeProgram()->stop(); + _sim->activeProgram()->reset(); + } + _sim->setActiveProgram(nullptr); + _sim->stopSimulation(); + _sim->setScriptRunning(false); + LOG_INFO("DSL script stopped."); D_INFO("DSL script stopped."); + + updateButtonState(false); + } + + void DSLEditorWidget::terminateScript(const char* reason, bool fault) { + _sim->setActiveProgram(nullptr); + _sim->stopSimulation(); + _sim->setScriptRunning(false); + + if (fault) { LOG_WARN("%s", reason); D_FAIL("%s", reason); } + else { LOG_INFO("%s", reason); D_SUCCESS("%s", reason); } + + delete _wrapper; _wrapper = nullptr; + delete _parser; _parser = nullptr; + delete _program; _program = nullptr; + updateButtonState(false); + setStatusMessage(reason); + _statusLabel->setStyleSheet(fault ? "color: rgb(180,40,40);" : "color: rgb(40,180,40);"); + } + + void DSLEditorWidget::pollScriptState() { + if (!_sim->isScriptRunning()) { return; } + auto* prog = _sim->activeProgram(); + if (!prog) { terminateScript("No active program found in simulation manager.", true); return; } + if (prog->isEmpty()) { terminateScript("Command script stopped -> program is empty.", true); return; } + if (prog->isFaulted()) { terminateScript("Command script stopped due to fault.", true); return; } + if (prog->isCompleted()) { terminateScript("Command script completed.", false); return; } + if (prog->isStopped() && !prog->isCompleted()) { terminateScript("Command script stopped.", true); return; } + } + + void DSLEditorWidget::updateButtonState(bool running) { + _runStopButton->setText(running ? "Stop" : "Run"); + _runStopButton->setStyleSheet(running ? "background-color: rgb(180,40,40);" : "background-color: rgb(40,180,40);"); + } + +} // namespace widgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp index c6d6bc5e..70f7d168 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp @@ -3,6 +3,7 @@ #include "Scene/SimulationManager.h" #include "Widgets/ViewportWidget.h" #include "Widgets/ControlPanelWidget.h" +#include "Widgets/DSLEditorWidget.h" #include #include @@ -15,7 +16,8 @@ namespace Workspace { auto* rootSplitter = new QSplitter(Qt::Horizontal, this); auto* centreSplitter = new QSplitter(Qt::Vertical); auto* rightSplitter = new QSplitter(Qt::Vertical); - rootSplitter->addWidget(new QLabel("Script Editor (TODO)", this)); + _editor = new widgets::DSLEditorWidget(sim, this); + rootSplitter->addWidget(_editor); centreSplitter->addWidget(new widgets::ViewportWidget(sim, this)); centreSplitter->addWidget(new QLabel("Console Output (TODO)", this)); rightSplitter->addWidget(new widgets::ControlPanelWidget(sim, this)); @@ -28,4 +30,7 @@ namespace Workspace { centreSplitter->setSizes({ 733, 396 }); rightSplitter->setSizes({ 733, 396 }); } + + widgets::DSLEditorWidget* ProjectPage::editor() const { return _editor; } + } // namespace Workspace \ No newline at end of file From 40a0c30a5680d8d4a57e02c254f97f709128f44d Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 15:13:23 +0100 Subject: [PATCH 025/110] fixes --- DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp index 42b97400..5b0df1ad 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp @@ -285,8 +285,6 @@ namespace gui { float kspd = 0.2f * dt; // base speed m/s - LOG_INFO("dt = %f", dt); - if (pressedKeys.contains(eKeyCode::W)) { processMovementKey((int)eKeyCode::W, kspd); } if (pressedKeys.contains(eKeyCode::A)) { processMovementKey((int)eKeyCode::A, kspd); } if (pressedKeys.contains(eKeyCode::S)) { processMovementKey((int)eKeyCode::S, kspd); } From 0b79bfeb52f529e9153882cd7130b726a2b76cae Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 6 Jun 2026 15:33:01 +0100 Subject: [PATCH 026/110] chore: Updated relevant file paths for widget dialogs --- .../MainWindow/Widgets/DSLEditorWidget.h | 2 ++ .../src/MainWindow/DSFE_MainWindow.cpp | 12 +++++---- .../MainWindow/Widgets/DSLEditorWidget.cpp | 26 +++++++++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h index 9ae11d6d..794fb0b9 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h @@ -51,6 +51,8 @@ namespace widgets { interpreter::IStoredProgram* _program; interpreter::RunWrapper* _wrapper; + std::string _scriptWorkingDir; + QTabWidget* _tabs = nullptr; QWidget* _editorTab = nullptr; QWidget* _helpTab = nullptr; diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index 7f42a8f6..07ffa16a 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -11,6 +11,8 @@ #include #include +#include "Platform/Paths.h" + namespace window { DSFE_MainWindow::DSFE_MainWindow(gui::SimManager* sim, QWidget* parent) : QMainWindow(parent), _sim(sim), _dslEditor(nullptr) @@ -57,7 +59,7 @@ namespace window { openMenu->addSeparator(); auto* openScriptAction = openMenu->addAction("Script (*.dsl *.txt)"); connect(openScriptAction, &QAction::triggered, this, [this]() { - QString fileName = QFileDialog::getOpenFileName(nullptr, "Open Script", "", "DSL Script Files (*.dsl);;Text Files (*.txt)"); + QString fileName = QFileDialog::getOpenFileName(nullptr, "Open Script", QString::fromStdString((paths::assets() / "DSLScripts").string()), "DSL Script Files (*.dsl);;Text Files (*.txt)"); if (fileName.isEmpty()) { return; } _dslEditor->loadScript(fileName); }); @@ -73,7 +75,7 @@ namespace window { auto* saveScriptAction = saveMenu->addAction("Script"); connect(saveScriptAction, &QAction::triggered, this, [this]() { LOG_INFO("Menu clicked: File -> Save -> Script"); - QString fileName = QFileDialog::getSaveFileName(nullptr, "Save Script", "", "DSL Script Files (*.dsl);;Text Files (*.txt)"); + QString fileName = QFileDialog::getSaveFileName(nullptr, "Save Script", QString::fromStdString((paths::assets() / "DSLScripts").string()), "DSL Script Files (*.dsl);;Text Files (*.txt)"); // ARGS are if (fileName.isEmpty()) { return; } _dslEditor->saveScript(fileName); }); @@ -88,7 +90,7 @@ namespace window { auto* saveScriptAsAction = saveAsMenu->addAction("Script"); connect(saveScriptAsAction, &QAction::triggered, this, [this]() { LOG_INFO("Menu clicked: File -> Save As -> Script"); - QString fileName = QFileDialog::getSaveFileName(nullptr, "Save Script As", "", "DSL Script Files (*.dsl);;Text Files (*.txt)"); + QString fileName = QFileDialog::getSaveFileName(nullptr, "Save Script As", QString::fromStdString((paths::assets() / "DSLScripts").string()), "DSL Script Files (*.dsl);;Text Files (*.txt)"); if (fileName.isEmpty()) { return; } _dslEditor->saveScript(fileName); }); @@ -123,14 +125,14 @@ namespace window { auto* loadMeshAction = projectMenu->addAction("Load Mesh"); connect(loadMeshAction, &QAction::triggered, this, [this]() { LOG_INFO("Menu clicked: Project -> Load Mesh"); - QString path = QFileDialog::getOpenFileName(nullptr, "Select Mesh File", "", "Mesh Files (*.obj *.fbx *.gltf *.dae *.stl"); + QString path = QFileDialog::getOpenFileName(nullptr, "Select Mesh File", QString::fromStdString((paths::assets() / "objects" / "Shapes").string()), "Mesh Files (*.obj *.fbx *.gltf *.dae *.stl"); if (path.isEmpty()) { return; } _sim->loadMesh(path.toStdString()); // Crashes at the moment, TODO fix crash }); auto* loadHDRAction = projectMenu->addAction("Load HDRI"); connect(loadHDRAction, &QAction::triggered, this, [this]() { LOG_INFO("Menu clicked: Project -> Load HDRI"); - QString path = QFileDialog::getOpenFileName(nullptr, "Select HDRI File", "", "HDRI Files (*.hdr *.exr)"); + QString path = QFileDialog::getOpenFileName(nullptr, "Select HDRI File", QString::fromStdString((paths::assets() / "hdr").string()), "HDRI Files (*.hdr *.exr)"); if (path.isEmpty()) { return; } _sim->loadNewHDR_UI(path.toStdString()); }); diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp index f2ed6d59..92665434 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp @@ -27,18 +27,13 @@ namespace widgets { DSLEditorWidget::DSLEditorWidget(gui::SimManager* sim, QWidget* parent) - : QWidget(parent), _sim(sim), _parser(nullptr), _program(nullptr), _wrapper(nullptr) + : QWidget(parent), _sim(sim), _parser(nullptr), _program(nullptr), _wrapper(nullptr), _scriptWorkingDir((paths::assets() / "DSLScripts").string()) { auto* rootLayout = new QVBoxLayout(this); auto* buttonLayout = new QHBoxLayout(); _runStopButton = new QPushButton("Run", this); // May also move to the menu, but I want to test the logic first - buttonLayout->addStretch(); - buttonLayout->addWidget(_runStopButton); - - rootLayout->addLayout(buttonLayout); - _tabs = new QTabWidget(this); rootLayout->addWidget(_tabs); @@ -46,7 +41,11 @@ namespace widgets { buildHelpTab(); _statusLabel = new QLabel("Idle", this); + rootLayout->addWidget(_statusLabel); + buttonLayout->addStretch(); + buttonLayout->addWidget(_runStopButton); + rootLayout->addLayout(buttonLayout); _stateTimer = new QTimer(this); connect(_stateTimer, &QTimer::timeout, this, [this]() { pollScriptState(); }); @@ -128,6 +127,7 @@ namespace widgets { void DSLEditorWidget::runScript() { if (_sim->isScriptRunning()) { return; } + _sim->setScriptRunning(!_sim->isScriptRunning()); LOG_INFO("DSL script started."); D_INFO("DSL script started."); _sim->setActiveProgram(nullptr); @@ -139,15 +139,25 @@ namespace widgets { _parser = new interpreter::Parser(_program); _wrapper = new interpreter::RunWrapper(_parser, _program); + LOG_INFO("ScriptEditor content size = %d", _scriptEditor->toPlainText().size()); _scriptText = _scriptEditor->toPlainText().toStdString(); + LOG_INFO("Script size = %zu", _scriptText.size()); _sim->setActiveProgram(_program); _sim->setScriptRunning(true); + + LOG_INFO("Program = %p", _program); + LOG_INFO("Parser = %p", _parser); + LOG_INFO("Wrapper = %p", _wrapper); + _sim->setLastScriptText(_scriptText); std::string code = _scriptText; + LOG_INFO("Original script size = %zu", code.size()); if (!code.empty() && code.back() == '\0') { code.pop_back(); } + LOG_INFO("Running script:\n%s", code.c_str()); + _wrapper->runProgram(_scriptText); updateButtonState(true); @@ -180,7 +190,7 @@ namespace widgets { delete _program; _program = nullptr; updateButtonState(false); setStatusMessage(reason); - _statusLabel->setStyleSheet(fault ? "color: rgb(180,40,40);" : "color: rgb(40,180,40);"); + _statusLabel->setStyleSheet(fault ? "color: rgb(180,40,40);" : ""); } void DSLEditorWidget::pollScriptState() { @@ -195,7 +205,7 @@ namespace widgets { void DSLEditorWidget::updateButtonState(bool running) { _runStopButton->setText(running ? "Stop" : "Run"); - _runStopButton->setStyleSheet(running ? "background-color: rgb(180,40,40);" : "background-color: rgb(40,180,40);"); + _runStopButton->setStyleSheet(running ? "background-color: rgb(180,40,40);" : ""); } } // namespace widgets \ No newline at end of file From bf6f6aaadc0bdbf39927f36213e5417f3a06ca1e Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sun, 7 Jun 2026 11:33:28 +0100 Subject: [PATCH 027/110] fixes: Removed unsured file --- DSFE_App/DSFE_GUI/src/Scene/Manager/SimPostFX.cpp | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 DSFE_App/DSFE_GUI/src/Scene/Manager/SimPostFX.cpp diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimPostFX.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimPostFX.cpp deleted file mode 100644 index 94109812..00000000 --- a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimPostFX.cpp +++ /dev/null @@ -1,10 +0,0 @@ -// DSFE_GUI SimPostFX.cpp -#include "Scene/SimulationManager.h" -#ifdef __gl_h_ -#undef __gl_h_ -#endif -#include "Manager/SimImplementation.h" - -namespace gui { - -} \ No newline at end of file From 3dbe40263d4a0471462360f9481d36e955a39d46 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sun, 7 Jun 2026 11:33:55 +0100 Subject: [PATCH 028/110] feat: Added destructor to `ViewportWidget` --- DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h | 1 + DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h index a9e39bcb..9f9d8396 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ViewportWidget.h @@ -16,6 +16,7 @@ namespace widgets { class ViewportWidget : public QOpenGLWidget { public: explicit ViewportWidget(gui::SimManager* sim, QWidget* parent = nullptr); + ~ViewportWidget(); protected: void initializeGL() override; diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp index 2b89a681..e592c69a 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ViewportWidget.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include "Platform/KeyCode.h" @@ -23,9 +24,10 @@ namespace widgets { _updateTimer.start(7); // ~144 FPS } + ViewportWidget::~ViewportWidget() { if (_sim) _sim->setContextHooks({}, {}); } + void ViewportWidget::initializeGL() { auto* ctx = QOpenGLContext::currentContext(); - if (_sim) { _sim->setContextHooks([this]() { this->makeCurrent(); }, [this]() { this->doneCurrent(); }); } if (!ctx) { LOG_ERROR("No current OpenGL context"); return; From 45aca0519199b33f06d9e755cefefabc33f3f3b2 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sun, 7 Jun 2026 11:35:09 +0100 Subject: [PATCH 029/110] chore: Added code comments and small corrections --- DSFE_App/DSFE_GUI/include/Platform/ScopeGLContext.h | 11 ++++++++--- .../include/Scene/Manager/SimImplementation.h | 2 +- DSFE_App/DSFE_GUI/src/Robots/RobotRenderer.cpp | 9 ++++----- DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp | 2 +- DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp | 2 ++ 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/Platform/ScopeGLContext.h b/DSFE_App/DSFE_GUI/include/Platform/ScopeGLContext.h index 756c45d4..78209a5a 100644 --- a/DSFE_App/DSFE_GUI/include/Platform/ScopeGLContext.h +++ b/DSFE_App/DSFE_GUI/include/Platform/ScopeGLContext.h @@ -6,9 +6,14 @@ namespace platform { class ScopeGLContext { public: - ScopeGLContext(std::function make, std::function done) : _done(std::move(done)) { if (make) { make(); } } - ~ScopeGLContext() { if (_done) { _done(); } } + ScopeGLContext(std::function makeHook, std::function doneHook) + : _makeHook(std::move(makeHook)), _doneHook(std::move(doneHook)) + { + if (_makeHook) { _makeHook(); } + } + ~ScopeGLContext() { if (_doneHook) { _doneHook(); } } private: - std::function _done; + std::function _makeHook; + std::function _doneHook; }; } \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h index b7d7b8b4..e171722c 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h +++ b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h @@ -85,7 +85,7 @@ namespace gui { // World Grid & Shadow Shaders std::unique_ptr _worldGridShader; std::unique_ptr _shadowShader; - shaders::Shader* currentShader; + shaders::Shader* currentShader = nullptr; // Fullscreen Quad VAO GLuint _fullscreenVAO = 0; diff --git a/DSFE_App/DSFE_GUI/src/Robots/RobotRenderer.cpp b/DSFE_App/DSFE_GUI/src/Robots/RobotRenderer.cpp index 3f97d3c2..b2dba692 100644 --- a/DSFE_App/DSFE_GUI/src/Robots/RobotRenderer.cpp +++ b/DSFE_App/DSFE_GUI/src/Robots/RobotRenderer.cpp @@ -87,15 +87,14 @@ void RobotRenderer::bind(const RobotRenderBinding& binding) { void RobotRenderer::applyTransforms(const robots::RobotModel& robot, const std::vector& world) { const bool isAligned = robot.baseFrameIsEngineAligned; const size_t n = robot.links.size(); - - LOG_INFO_ONCE("applyTransforms world size = %zu", world.size()); - + if (world.size() < robot.links.size()) { + LOG_ERROR("Transform mismatch: links=%zu world=%zu", robot.links.size(), world.size()); + return; + } for (size_t i = 0; i < n; ++i) { const auto& link = robot.links[i]; - auto it = linkRenderMap.find(link.name); if (it == linkRenderMap.end()) { continue; } - glm::mat4 T = toGlm(world[i]); glm::vec3 pos = glm::vec3(T[3]); // Extract translation from the 4x4 matrix glm::quat q = glm::quat_cast(T); // Extract rotation as a quaternion diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp index 24013153..4a0555aa 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp @@ -25,7 +25,7 @@ namespace gui { size_t startIdx = _impl->_objects.size(); - platform::ScopeGLContext guard(_makeCurrentHook, _doneCurrentHook); + platform::ScopeGLContext guard(_makeCurrentHook, _doneCurrentHook); // Hooks may be empty under Qt: In that case the guard becomes a no-op. _impl->buildRobotPresentationFromModel(_impl->_robotSystem->model(), *this); _impl->_robotRenderer->applyTransforms( diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp index addd2a6b..36c20079 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp @@ -7,6 +7,8 @@ #include +// TODO: Convert logic to Qt6 and only ever use ImGui for debugging at most (or not at all) + namespace gui { // Main Dockspace with Menu Bar void SimManager::drawMainDockspace() { From 1d3ecd195c44002d4096337ce8aee45d12421082 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sun, 7 Jun 2026 11:37:27 +0100 Subject: [PATCH 030/110] refactor: Updated `tick()` to contain the dirty robot presentation checks rather than previous placement in `renderViewport` * Also removed old `render()` method as it is no decrepid --- .../include/Scene/SimulationManager.h | 9 +-- .../DSFE_GUI/src/Scene/SimulationManager.cpp | 60 +++++-------------- 2 files changed, 16 insertions(+), 53 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h index eae0291d..fd1b5c93 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h +++ b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h @@ -79,9 +79,6 @@ namespace gui { _doneCurrentHook = doneCurrentHook; } - void renderViewport(int w, int h); - void setDisplaySize(int w, int h); - // Light scene::Light* getLight(); void setLightColour(const glm::vec3& c); @@ -166,15 +163,13 @@ namespace gui { // Render Settings & Profiles void applyRenderSettings(const render::RenderSettings& s, render::ResolutionPreset r); void applyRenderProfile(const render::RenderSettings& s, render::ResolutionPreset r); - - // Reset HDR to default void resetHDRToPreset(); - // Reload shaders (e.g. after editing source files) void reloadAllShaders(); // Rendering Entry Points - void render(); void tick(double dt); + void renderViewport(int w, int h); + void setDisplaySize(int w, int h); void resize(int32_t width, int32_t height); void setPresentationFBO(GLuint fbo) { _presentationFBO = fbo; } diff --git a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp index 5b0df1ad..d11b1720 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp @@ -3,7 +3,6 @@ #include "Scene/SimulationManager.h" #include "Scene/SimulationCore.h" -#include #include extern "C" core::ISimulationCore* CreateSimulationCore_v1(); @@ -76,7 +75,7 @@ namespace gui { // CONSTRUCTOR & DESTRUCTOR // -------------------------------------------------- - SimManager::SimManager() : _internalSize(1920, 1080), _displaySize(1.0f, 1.0f), _backgroundColour(0.18f, 0.18f, 0.20f), + SimManager::SimManager() : _internalSize(1280, 720), _displaySize(1.0f, 1.0f), _backgroundColour(0.18f, 0.18f, 0.20f), _backgroundAlpha(1.0f), _impl(std::make_unique(*this)), _core(std::make_unique()), _studyRunner(std::make_unique(makeCoreFactory, std::thread::hardware_concurrency() > 1 ? std::thread::hardware_concurrency() - 1 : 1)) { _core->setRobotSystem(_impl->_robotSystem.get()); @@ -153,68 +152,37 @@ namespace gui { return copy; } - // Main render function called by the application - void SimManager::render() { - if (hasCompletedStudy()) { - auto results = consumeCompletedStudy(); - - for (const auto& r : results) { - LOG_INFO("Study completed: %s", r.tag.c_str()); - // TODO: update plots, telemetry graphs, UI panels here - } - } - - ImGuiIO& io = ImGui::GetIO(); - - tick(io.DeltaTime); - - if (_impl->_robotSystem && hasRobot()) { - _impl->_robotRenderer->applyTransforms( - _impl->_robotSystem->model(), - _impl->_robotSystem->worldTransforms() - ); - } - + void SimManager::tick(double dt) { + _core->tick(dt); if (_core->robotPresentationDirty()) { - loadRobot(_core->robotSystem()->robotName()); + loadRobot(_impl->_robotSystem->robotName()); _core->clearRobotPresentationDirty(); } - - _fpsCounter.update(); - - drawMainDockspace(); - drawViewportWindow(); - } - - void SimManager::tick(double dt) { - if (!hasRobot()) { return; } - _core->tick(dt); } void SimManager::renderViewport(int w, int h) { - LOG_INFO_ONCE("renderViewport entered"); if (!_glReady || !_impl) { return; } if (w <= 0 || h <= 0) { return; } - if (_impl->_robotSystem && hasRobot()) { - _impl->_robotRenderer->applyTransforms( - _impl->_robotSystem->model(), - _impl->_robotSystem->worldTransforms() - ); + if (hasCompletedStudy()) { + auto results = consumeCompletedStudy(); + for (const auto& r : results) { + LOG_INFO("Study completed: %s", r.tag.c_str()); + // TODO: update plots, telemetry graphs, UI panels here + } } - if (_core->robotPresentationDirty()) { - loadRobot(_core->robotSystem()->robotName()); - _core->clearRobotPresentationDirty(); + if (_impl->_robotSystem && hasRobot()) { + _impl->_robotRenderer->applyTransforms(_impl->_robotSystem->model(), _impl->_robotSystem->worldTransforms()); } _fpsCounter.update(); auto& view = _impl->_views[static_cast(_impl->activeView)]; _impl->renderView(*this, view, w, h); - glBindFramebuffer(GL_FRAMEBUFFER, _presentationFBO); glViewport(0, 0, w, h); glDisable(GL_DEPTH_TEST); glDisable(GL_BLEND); _impl->_presentShader->use(); _impl->_presentShader->setInt1(0, "screenTexture"); + glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, view.post->getTexture()); glBindVertexArray(_impl->_fullscreenVAO); @@ -224,7 +192,7 @@ namespace gui { } void SimManager::syncRobotToScene() { - if (!hasRobot()) return; + if (!hasRobot()) { return; } auto* rs = robotSystem(); const auto& model = rs->model(); const auto& T = rs->worldTransforms(); From 7f5d2858b472ccef7d52280ceb3a57f483d86646 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sun, 7 Jun 2026 11:39:06 +0100 Subject: [PATCH 031/110] feat: Added more alignments with previous DSL script editor in `DSLEditorWidget` * Still need to add a few more things in this, but they are purely visual and do not really improve functionality * One of these planned editions is syntax highlighting! * Need to move onto a more refined CLI or debug window and plot logging (not sure the best way to do that right now) --- .../MainWindow/Widgets/DSLEditorWidget.h | 34 ++++++-- .../MainWindow/Widgets/DSLEditorWidget.cpp | 86 ++++++++++++------- 2 files changed, 80 insertions(+), 40 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h index 794fb0b9..1ef4fd14 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h @@ -7,6 +7,11 @@ #include #include #include +#include "Platform/StudyRunner.h" +#include +#include +#include +#include "Interpreter/RunWrapper.h" class QTextEdit; class QLabel; @@ -18,7 +23,13 @@ namespace gui { class SimManager; } namespace interpreter { class Parser; class IStoredProgram; - class RunWrapper; +} + +namespace runs { + struct ActiveRun { + std::future fut; + std::string tag; + }; } namespace widgets { @@ -28,8 +39,17 @@ namespace widgets { bool loadScript(const QString& fileName); bool saveScript(const QString& fileName); + void runButtonHandler(); private: + std::mutex _activeRunsMutex; // Mutex for synchronizing access to active runs + std::vector _activeRuns; // Vector to hold active runs and their futures + + gui::SimManager* _sim = nullptr; + interpreter::Parser* _parser = nullptr; + interpreter::IStoredProgram* _program = nullptr; + interpreter::RunWrapper* _wrapper = nullptr; + void runScript(); void stopScript(); @@ -44,14 +64,12 @@ namespace widgets { void setScript(const std::string& s) { _scriptText = s; } std::string getScript() const { return _scriptText; } - std::string _scriptText; - - gui::SimManager* _sim = nullptr; - interpreter::Parser* _parser; - interpreter::IStoredProgram* _program; - interpreter::RunWrapper* _wrapper; + bool _loadedFromFile = false; + std::string _scriptText; std::string _scriptWorkingDir; + std::vector _script; + int lastClickedLine = -1; QTabWidget* _tabs = nullptr; QWidget* _editorTab = nullptr; @@ -60,6 +78,8 @@ namespace widgets { QTextEdit* _dslHelp = nullptr; QPushButton* _runStopButton = nullptr; QLabel* _statusLabel = nullptr; + QLabel* _scriptLinesLabel = nullptr; + QLabel* _scriptCharsLabel = nullptr; QString _currentScriptPath; QTimer* _stateTimer = nullptr; }; diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp index 92665434..453a1342 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp @@ -10,13 +10,6 @@ #include #include -#include "Platform/StudyRunner.h" -#include "Interpreter/RunWrapper.h" - -#include -#include -#include - #include "Scene/SimulationManager.h" #include "Platform/Paths.h" @@ -27,12 +20,10 @@ namespace widgets { DSLEditorWidget::DSLEditorWidget(gui::SimManager* sim, QWidget* parent) - : QWidget(parent), _sim(sim), _parser(nullptr), _program(nullptr), _wrapper(nullptr), _scriptWorkingDir((paths::assets() / "DSLScripts").string()) + : QWidget(parent), _sim(sim), _scriptWorkingDir((paths::assets() / "DSLScripts").string()) { auto* rootLayout = new QVBoxLayout(this); - auto* buttonLayout = new QHBoxLayout(); - - _runStopButton = new QPushButton("Run", this); // May also move to the menu, but I want to test the logic first + auto* scriptStateLayout = new QHBoxLayout(); _tabs = new QTabWidget(this); rootLayout->addWidget(_tabs); @@ -40,58 +31,81 @@ namespace widgets { buildEditorTab(); buildHelpTab(); - _statusLabel = new QLabel("Idle", this); - - rootLayout->addWidget(_statusLabel); - buttonLayout->addStretch(); - buttonLayout->addWidget(_runStopButton); - rootLayout->addLayout(buttonLayout); + _statusLabel = new QLabel("State: Idle", this); + auto* pipe1 = new QLabel("|", this); + auto* pipe2 = new QLabel("|", this); + _scriptLinesLabel = new QLabel("Lines: 0", this); + _scriptCharsLabel = new QLabel("Chars: 0", this); + _runStopButton = new QPushButton("Run", this); + + scriptStateLayout->addWidget(_statusLabel); + scriptStateLayout->addWidget(pipe1); + scriptStateLayout->addWidget(_scriptLinesLabel); + scriptStateLayout->addWidget(pipe2); + scriptStateLayout->addWidget(_scriptCharsLabel); + scriptStateLayout->addStretch(); + scriptStateLayout->addWidget(_runStopButton); + + rootLayout->addLayout(scriptStateLayout); + + scriptStateLayout->setContentsMargins(6, 6, 6, 6); + scriptStateLayout->setSpacing(8); + rootLayout->setContentsMargins(6, 6, 6, 6); + rootLayout->setSpacing(6); _stateTimer = new QTimer(this); connect(_stateTimer, &QTimer::timeout, this, [this]() { pollScriptState(); }); _stateTimer->start(100); // Poll every 100 ms - connect(_runStopButton, &QPushButton::clicked, this, [this]() { - if (_sim->isScriptRunning()) { stopScript(); } - else { runScript(); } - }); + connect(_runStopButton, &QPushButton::clicked, this, [this]() { _sim->isScriptRunning() ? stopScript() : runScript(); }); pollScriptState(); } + static std::string filenameFromPath(const std::string& path) { + size_t lastSlash = path.find_last_of("/\\"); + if (lastSlash == std::string::npos) { return path; } + return path.substr(lastSlash + 1); + } + bool DSLEditorWidget::loadScript(const QString& fileName) { QFile file(fileName); + std::string nameStr = filenameFromPath(fileName.toStdString()).c_str(); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - LOG_ERROR("Failed to open script file: %s", fileName.toStdString().c_str()); + LOG_ERROR("Failed to open script file: %s", nameStr); return false; } _scriptEditor->setPlainText(file.readAll()); file.close(); _currentScriptPath = fileName; - LOG_INFO("DSL script loaded from file: %s", fileName.toStdString().c_str()); - D_INFO("DSL script loaded from file: %s", fileName.toStdString().c_str()); + _scriptLinesLabel->setText(QString("Lines: %1").arg(_scriptEditor->toPlainText().split('\n').size())); + _scriptCharsLabel->setText(QString("Chars: %1").arg(_scriptEditor->toPlainText().size())); + + LOG_INFO("DSL script loaded from file: %s", nameStr); + D_INFO("DSL script loaded from file: %s", nameStr); return true; } bool DSLEditorWidget::saveScript(const QString& fileName) { QFile file(fileName); + std::string nameStr = filenameFromPath(fileName.toStdString()).c_str(); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - LOG_ERROR("Failed to open script file for writing: %s", fileName.toStdString().c_str()); + LOG_ERROR("Failed to open script file for writing: %s", nameStr); return false; } file.write(_scriptEditor->toPlainText().toUtf8()); file.close(); _currentScriptPath = fileName; - LOG_INFO("DSL script saved to file: %s", fileName.toStdString().c_str()); - D_INFO("DSL script saved to file: %s", fileName.toStdString().c_str()); + LOG_INFO("DSL script saved to file: %s", nameStr); + D_INFO("DSL script saved to file: %s", nameStr); return true; } - void DSLEditorWidget::setStatusMessage(const QString& message) { if (_statusLabel) { _statusLabel->setText(message); } } + void DSLEditorWidget::setStatusMessage(const QString& message) { if (_statusLabel) { _statusLabel->setText("State: " + message); } } void DSLEditorWidget::buildEditorTab() { _editorTab = new QWidget(this); @@ -106,8 +120,9 @@ namespace widgets { auto* layout = new QVBoxLayout(_helpTab); _dslHelp = new QTextEdit(_helpTab); _dslHelp->setReadOnly(true); - - + + // TODO: Use a markdown viewer or similar to format and display this help content in an accessible way, with sections, examples, etc. + // * (also means I can make a website with the same content) _dslHelp->setPlainText( "trajSet(...)\n" "trajClear()\n" @@ -120,31 +135,34 @@ namespace widgets { "wait(...)\n" ); - layout->addWidget(_dslHelp); _tabs->addTab(_helpTab, "DSL Help"); } void DSLEditorWidget::runScript() { if (_sim->isScriptRunning()) { return; } - _sim->setScriptRunning(!_sim->isScriptRunning()); - LOG_INFO("DSL script started."); D_INFO("DSL script started."); + if (_scriptEditor->toPlainText().isEmpty()) { setStatusMessage("Empty script"); return; } _sim->setActiveProgram(nullptr); delete _wrapper; _wrapper = nullptr; delete _parser; _parser = nullptr; delete _program; _program = nullptr; + LOG_INFO("Core = %p", _sim->simCoreInterface()); + _program = new interpreter::StoredProgram(_sim->simCoreInterface()); _parser = new interpreter::Parser(_program); _wrapper = new interpreter::RunWrapper(_parser, _program); + LOG_INFO("Program = %p", _program); + LOG_INFO("ScriptEditor content size = %d", _scriptEditor->toPlainText().size()); _scriptText = _scriptEditor->toPlainText().toStdString(); LOG_INFO("Script size = %zu", _scriptText.size()); _sim->setActiveProgram(_program); _sim->setScriptRunning(true); + LOG_INFO("DSL script started."); D_INFO("DSL script started."); LOG_INFO("Program = %p", _program); LOG_INFO("Parser = %p", _parser); @@ -163,6 +181,8 @@ namespace widgets { updateButtonState(true); } + void DSLEditorWidget::runButtonHandler() { _sim->isScriptRunning() ? stopScript() : runScript(); } + void DSLEditorWidget::stopScript() { if (!_sim->isScriptRunning()) { return; } if (_sim->activeProgram()) { From add31d0ddc226c22eb6559d1b214d40d1ad78fa1 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sun, 7 Jun 2026 11:39:57 +0100 Subject: [PATCH 032/110] refactor: Updated `buildMenuBar()` to call DSLEditorWidget methods for loading and saving files. --- DSFE_App/DSFE_GUI/CMakeLists.txt | 1 - DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 3f8d1e6c..5eb1cd28 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -73,7 +73,6 @@ set(MANAGER_SRC src/Scene/Manager/SimBackend.cpp src/Scene/Manager/SimCamera.cpp src/Scene/Manager/SimObjects.cpp - src/Scene/Manager/SimPostFX.cpp src/Scene/Manager/SimRendering.cpp src/Scene/Manager/SimRobots.cpp src/Scene/Manager/SimUI.cpp diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index 07ffa16a..2c978d9c 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -18,7 +18,7 @@ namespace window { : QMainWindow(parent), _sim(sim), _dslEditor(nullptr) { setWindowTitle("DSFE"); - resize(1280, 720); + resize(1920, 1080); buildMenuBar(); @@ -61,6 +61,7 @@ namespace window { connect(openScriptAction, &QAction::triggered, this, [this]() { QString fileName = QFileDialog::getOpenFileName(nullptr, "Open Script", QString::fromStdString((paths::assets() / "DSLScripts").string()), "DSL Script Files (*.dsl);;Text Files (*.txt)"); if (fileName.isEmpty()) { return; } + if (!_dslEditor) { LOG_ERROR("DSL Editor not found!"); return; } _dslEditor->loadScript(fileName); }); }); @@ -77,6 +78,7 @@ namespace window { LOG_INFO("Menu clicked: File -> Save -> Script"); QString fileName = QFileDialog::getSaveFileName(nullptr, "Save Script", QString::fromStdString((paths::assets() / "DSLScripts").string()), "DSL Script Files (*.dsl);;Text Files (*.txt)"); // ARGS are if (fileName.isEmpty()) { return; } + if (!_dslEditor) { LOG_ERROR("DSL Editor not found!"); return; } _dslEditor->saveScript(fileName); }); }); @@ -92,6 +94,7 @@ namespace window { LOG_INFO("Menu clicked: File -> Save As -> Script"); QString fileName = QFileDialog::getSaveFileName(nullptr, "Save Script As", QString::fromStdString((paths::assets() / "DSLScripts").string()), "DSL Script Files (*.dsl);;Text Files (*.txt)"); if (fileName.isEmpty()) { return; } + if (!_dslEditor) { LOG_ERROR("DSL Editor not found!"); return; } _dslEditor->saveScript(fileName); }); }); @@ -127,7 +130,7 @@ namespace window { LOG_INFO("Menu clicked: Project -> Load Mesh"); QString path = QFileDialog::getOpenFileName(nullptr, "Select Mesh File", QString::fromStdString((paths::assets() / "objects" / "Shapes").string()), "Mesh Files (*.obj *.fbx *.gltf *.dae *.stl"); if (path.isEmpty()) { return; } - _sim->loadMesh(path.toStdString()); // Crashes at the moment, TODO fix crash + _sim->loadMesh(path.toStdString()); }); auto* loadHDRAction = projectMenu->addAction("Load HDRI"); connect(loadHDRAction, &QAction::triggered, this, [this]() { From 7d4429193d760064f82a606048b56ad16b970661 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sun, 7 Jun 2026 14:38:50 +0100 Subject: [PATCH 033/110] feat: Added basic joint telemetry in `ControlPanelWidget` --- DSFE_App/DSFE_GUI/CMakeLists.txt | 20 +- .../include/MainWindow/DSFE_MainWindow.h | 2 + .../MainWindow/Widgets/ControlPanelWidget.h | 47 +- .../DSFE_GUI/include/Platform/WindowManager.h | 81 - .../DSFE_GUI/include/Platform/imguiWidgets.h | 298 --- .../DSFE_GUI/include/Rendering/GUIContext.h | 26 - .../include/Rendering/OpenGLContext.h | 21 - .../DSFE_GUI/include/ui/CommandScriptEditor.h | 104 - DSFE_App/DSFE_GUI/include/ui/ControlPanel.h | 232 -- DSFE_App/DSFE_GUI/include/ui/DebugPanel.h | 58 - DSFE_App/DSFE_GUI/include/ui/Styles.h | 15 - .../MainWindow/Widgets/ControlPanelWidget.cpp | 240 ++- .../MainWindow/Widgets/DSLEditorWidget.cpp | 2 +- .../DSFE_GUI/src/Platform/WindowManager.cpp | 200 -- .../DSFE_GUI/src/Rendering/GUIContext.cpp | 114 - .../DSFE_GUI/src/Rendering/OpenGLContext.cpp | 113 - .../DSFE_GUI/src/Scene/SimulationManager.cpp | 9 +- .../DSFE_GUI/src/ui/CommandScriptEditor.cpp | 703 ------- DSFE_App/DSFE_GUI/src/ui/ControlPanel.cpp | 1861 ----------------- DSFE_App/DSFE_GUI/src/ui/DebugPanel.cpp | 317 --- 20 files changed, 213 insertions(+), 4250 deletions(-) delete mode 100644 DSFE_App/DSFE_GUI/include/Platform/WindowManager.h delete mode 100644 DSFE_App/DSFE_GUI/include/Platform/imguiWidgets.h delete mode 100644 DSFE_App/DSFE_GUI/include/Rendering/GUIContext.h delete mode 100644 DSFE_App/DSFE_GUI/include/Rendering/OpenGLContext.h delete mode 100644 DSFE_App/DSFE_GUI/include/ui/CommandScriptEditor.h delete mode 100644 DSFE_App/DSFE_GUI/include/ui/ControlPanel.h delete mode 100644 DSFE_App/DSFE_GUI/include/ui/DebugPanel.h delete mode 100644 DSFE_App/DSFE_GUI/include/ui/Styles.h delete mode 100644 DSFE_App/DSFE_GUI/src/Platform/WindowManager.cpp delete mode 100644 DSFE_App/DSFE_GUI/src/Rendering/GUIContext.cpp delete mode 100644 DSFE_App/DSFE_GUI/src/Rendering/OpenGLContext.cpp delete mode 100644 DSFE_App/DSFE_GUI/src/ui/CommandScriptEditor.cpp delete mode 100644 DSFE_App/DSFE_GUI/src/ui/ControlPanel.cpp delete mode 100644 DSFE_App/DSFE_GUI/src/ui/DebugPanel.cpp diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 5eb1cd28..ba958df6 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -40,21 +40,12 @@ target_include_directories(stb INTERFACE set(MAIN_WINDOW_SRC src/MainWindow/DSFE_MainWindow.cpp src/MainWindow/Workspace/ProjectPage.cpp - src/MainWindow/Widgets/ViewportWidget.cpp - src/MainWindow/Widgets/RobotSelectorWidget.cpp - src/MainWindow/Widgets/ControlPanelWidget.cpp - src/MainWindow/Widgets/DSLEditorWidget.cpp - - src/MainWindow/Widgets/FractionSelectorWidget.cpp - include/MainWindow/Widgets/FractionSelectorWidget.h ) set(RENDER_SRC src/Rendering/CubeVertices.cpp - src/Rendering/GUIContext.cpp src/Rendering/IBL.cpp src/Rendering/OpenGLBufferManager.cpp - src/Rendering/OpenGLContext.cpp src/Rendering/ShaderUtil.cpp src/Rendering/SkyboxRenderer.cpp src/Rendering/Texture.cpp @@ -80,11 +71,13 @@ set(MANAGER_SRC ) set(UI_SRC - src/ui/CommandScriptEditor.cpp - src/ui/ControlPanel.cpp - src/ui/DebugPanel.cpp src/ui/RenderPreset.cpp - src/ui/Styles.cpp + src/MainWindow/Widgets/ViewportWidget.cpp + src/MainWindow/Widgets/RobotSelectorWidget.cpp + src/MainWindow/Widgets/ControlPanelWidget.cpp + src/MainWindow/Widgets/DSLEditorWidget.cpp + src/MainWindow/Widgets/FractionSelectorWidget.cpp + include/MainWindow/Widgets/FractionSelectorWidget.h ) set(ROBOT_SRC @@ -93,7 +86,6 @@ set(ROBOT_SRC ) set(PLATFORM_SRC - src/Platform/WindowManager.cpp src/Application.cpp ) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h index 9f381d08..771cad2e 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h @@ -1,6 +1,8 @@ // DSFE_GUI DSFE_MainWindow.h #pragma once +#include "GUIExports.h" + #include #include diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h index b0892c54..15b71b1c 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h @@ -25,8 +25,7 @@ namespace render { } namespace robots { class RobotSystem; } -namespace diagnostics { class TelemetryRecorder; } - +namespace diagnostics { class TelemetryRecorder; struct JointTelemetry; } namespace gui { class SimManager; } class QVBoxLayout; @@ -53,16 +52,53 @@ namespace widgets { const char* name; }; + struct TelemetryLabels { + // Headers + QLabel* stateHeader = nullptr; + QLabel* referenceHeader = nullptr; + QLabel* trajectoryHeader = nullptr; + QLabel* clampedHeader = nullptr; + QLabel* constantsHeader = nullptr; + + // State + QLabel* q = nullptr; + QLabel* qd = nullptr; + QLabel* tau = nullptr; + + // Reference + QLabel* qRef = nullptr; + QLabel* qdRef = nullptr; + QLabel* qddRef = nullptr; + QLabel* err = nullptr; + + // Trajectory + QLabel* qTraj = nullptr; + QLabel* qdTraj = nullptr; + QLabel* qddTraj = nullptr; + + // Clamping + QLabel* qClamped = nullptr; + QLabel* qdClamped = nullptr; + + // Constants + QLabel* damping = nullptr; + QLabel* friction = nullptr; + }; + void simPropertiesPanel(); void buildIntegratorCombos(); - void buildDtFractions(); void jointInfoPanel(); - void displayJointInfo(const diagnostics::TelemetryRecorder& rec, int& selectedJoint, QVBoxLayout* layout); + void updateTelemetryInfo(const diagnostics::JointTelemetry& j); + void buildTelemetryWidgets(QVBoxLayout* layout); + + void updateTelemetryDisplay(); void displayPanel(); void selectJointAndFollow(int jointIdx); + void updateSimClock(); + gui::SimManager* _sim = nullptr; QVBoxLayout* _contentLayout = nullptr; @@ -70,6 +106,7 @@ namespace widgets { QCheckBox* _useAutoDiffCheck = nullptr; QComboBox* _integratorCombo = nullptr; QLabel* _currentIntegratorLabel = nullptr; + QLabel* _simTimeLabel = nullptr; FractionSelectorWidget* _simDtSelector = nullptr; FractionSelectorWidget* _telemetryDtSelector = nullptr; @@ -98,6 +135,8 @@ namespace widgets { { integration::eAutoDiffIntegrationMethod::AD_GLRK3, "GLRK3 (AutoDiff)" } }; + TelemetryLabels _telemetryLabels; + // Current selection state Selection _selection; diff --git a/DSFE_App/DSFE_GUI/include/Platform/WindowManager.h b/DSFE_App/DSFE_GUI/include/Platform/WindowManager.h deleted file mode 100644 index a5a42f63..00000000 --- a/DSFE_App/DSFE_GUI/include/Platform/WindowManager.h +++ /dev/null @@ -1,81 +0,0 @@ -// DSFE_GUI WindowManager.h -#pragma once - -#include "GUIExports.h" - -#include "Platform/Window.h" -#include "Platform/Logger.h" - -// Forward Declarations -struct GLFWwindow; -namespace render { - class GUIContext; - class OpenGLContext; -} -namespace gui { - class SimManager; - class ControlPanel; - class DebugPanel; - class CommandScriptEditor; -} - -namespace window { - class GLWindow : public IWindow { - public: - GLWindow(); - ~GLWindow(); - - bool init(int width, int height, const std::string& title) override; - - // IWindow interface - bool isRunning() const override; - bool shouldClose() const override; - void pollEvents() override; - void swapBuffers() override; - - void* getNativeWin() override; - void setNativeWin(void* window) override; - - int getWidth() const override; - int getHeight() const override; - const std::string& getHeader() const override; - - // Input handling - void setMouseCaptured(bool captured); - bool isMouseCaptured() const { return _mouseCaptured; } - void onKey(int key, int scancode, int action, int mods) override; - void onScroll(double delta) override; - void onResize(int width, int height) override; - void onCursorPos(double xpos, double ypos) override; - void onClose() override; - - void update(); - void render(); - - private: - // UI State - struct UIState { - bool sceneViewOpen = true; - bool controlPanelOpen = true; - bool debugPanelOpen = true; - }; - - bool _isRunning = true; - GLFWwindow* _window = nullptr; - - std::unique_ptr _GUICntx; - std::unique_ptr _renderCntx; - std::unique_ptr _sim; - std::unique_ptr _controlPanel; - std::unique_ptr _debugPanel; - std::unique_ptr _cmdEditor; - - bool _isHovered = false; - bool _mouseCaptured = true; - - // Window properties - int _width = 0; - int _height = 0; - std::string *_header; - }; -} // namespace window \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Platform/imguiWidgets.h b/DSFE_App/DSFE_GUI/include/Platform/imguiWidgets.h deleted file mode 100644 index ccac8db6..00000000 --- a/DSFE_App/DSFE_GUI/include/Platform/imguiWidgets.h +++ /dev/null @@ -1,298 +0,0 @@ -// DSFE_GUI imguiWidgets.h -#pragma once - -#include -#include -#include "Platform/Logger.h" - -namespace ImGui { - // Draggable horizontal splitter for resizable panels - inline float hSplitter(const char* id, float* h, float minH = 200.0f, float maxH = 0.0f, float thickness = 2.0f) { - if (maxH <= 0.0f) { maxH = GetContentRegionAvail().y; } - - // Get the current cursor position in screen coordinates - ImVec2 cursor = GetCursorScreenPos(); - float w = GetContentRegionAvail().x; - - // Push a unique ID for this splitter to avoid conflicts with other widgets - PushID(id); - - // Position the invisible button for the splitter - if (w <= 0.0f) { - ImGui::Dummy(ImVec2(0.0f, thickness)); - PopID(); - return *h; - } - - InvisibleButton("##hsplit", ImVec2(w, thickness)); - - // Handle dragging - bool dragging = IsItemActive(); - if (dragging) { - *h += GetIO().MouseDelta.y; - } - - *h = ImClamp(*h, minH, maxH); - - // Draw the draggable splitter - if (IsItemHovered() || dragging) { - SetMouseCursor(ImGuiMouseCursor_ResizeNS); - ImVec2 p = GetItemRectMin(); - ImVec2 q = GetItemRectMax(); - GetWindowDrawList()->AddRectFilled( - ImVec2(p.x, q.y), ImVec2(p.x, p.y + thickness), - dragging ? IM_COL32(100, 150, 255, 200) // brighter color when dragging - : IM_COL32(150, 150, 150, 120) // default color - ); - } - - // Restore cursor position and ID stack - if (w > 0.0f) { ImGui::Dummy(ImVec2(0, thickness)); } - PopID(); - return *h; - } - - // Draggable vertical splitter for resizable panels - inline float vSplitter(const char* id, float* w, float minW = 200.0f, float maxW = 0.0f, float thickness = 2.0f) { - if (maxW <= 0.0f) { maxW = GetContentRegionAvail().x; } - - // Get the current cursor position in screen coordinates - ImVec2 cursor = GetCursorScreenPos(); - float h = GetContentRegionAvail().y; - - // Push a unique ID for this splitter to avoid conflicts with other widgets - PushID(id); - SetCursorScreenPos(ImVec2(cursor.x + *w, cursor.y)); - InvisibleButton("##vsplit", ImVec2(thickness, h)); - - // Handle dragging - bool dragging = IsItemActive(); - if (dragging) { - *w += GetIO().MouseDelta.x; - *w = ImClamp(*w, minW, maxW); - } - - // Draw the draggable splitter - if (IsItemHovered() || dragging) { - SetMouseCursor(ImGuiMouseCursor_ResizeEW); - ImVec2 p = GetItemRectMin(); - ImVec2 q = GetItemRectMax(); - GetWindowDrawList()->AddRectFilled( - ImVec2(p.x, p.y), ImVec2(p.x + thickness, q.y), - dragging ? IM_COL32(100, 150, 255, 200) // brighter color when dragging - : IM_COL32(150, 150, 150, 120) // default color - ); - } - - // Restore cursor position and ID stack - SetCursorScreenPos(cursor); - PopID(); - return *w; - } - - // Draw a section divider with spacing - inline void SectionDivider() { - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - } - - // Segmented Button Row Helper - inline bool SegmentedButtonRow(const char* label, const char* const* items, int itemCount, int& current, float buttonWidth) { - ImGui::TextUnformatted(label); - - // Track if selection changed - bool changed = false; - ImGui::PushID(label); - - // Draw buttons in a row - for (int i = 0; i < itemCount; ++i) { - if (i > 0) ImGui::SameLine(); - - // Highlight the currently selected button - const bool selected = (current == i); - if (selected) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.1f, 0.5f, 0.1f, 1.0f)); - } - - // Draw the button and check for clicks - if (ImGui::Button(items[i], ImVec2(buttonWidth, 0))) { - if (current != i) { - current = i; - changed = true; - } - } - - if (selected) { ImGui::PopStyleColor(3); } - } - - ImGui::PopID(); - return changed; - } - - // Reusable section header - inline void SectionHeader(const char* text, const ImVec4& color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f)) { - ImGui::Spacing(); - - // Get draw list and position for custom drawing - ImDrawList* dl = ImGui::GetWindowDrawList(); - ImVec2 p = ImGui::GetCursorScreenPos(); - - // Subtle left accent bar - dl->AddRectFilled( - ImVec2(p.x, p.y), - ImVec2(p.x + 3.0f, p.y + ImGui::GetTextLineHeightWithSpacing()), - ImGui::ColorConvertFloat4ToU32(color) - ); - - // Header text with indentation - ImGui::Indent(12.0f); - ImGui::TextColored(color, "%s", text); - ImGui::Unindent(12.0f); - ImGui::Spacing(); - } - - // Draw a simple fraction (numerator over denominator) with a horizontal line in between - inline void DrawFraction(const char* numerator, const char* denom) { - ImDrawList* draw = ImGui::GetWindowDrawList(); - ImVec2 pos = ImGui::GetCursorScreenPos(); - - // Calculate text sizes - ImVec2 numSize = ImGui::CalcTextSize(numerator); - ImVec2 denSize = ImGui::CalcTextSize(denom); - - // Calculate positions - float lineY = pos.y + numSize.y + 2.0f; - float width = std::max(numSize.x, denSize.x) + 10.0f; - - // Center numerator - draw->AddText( - ImVec2(pos.x + (width - numSize.x) * 0.5f, pos.y), - IM_COL32_WHITE, numerator - ); - - // Draw fraction bar - draw->AddLine( - ImVec2(pos.x, lineY), ImVec2(pos.x + width, lineY), - IM_COL32_WHITE, 1.5f - ); - - // Center denominator - draw->AddText( - ImVec2(pos.x + (width - denSize.x) * 0.5f, lineY + 2.0f), - IM_COL32_WHITE, denom - ); - - // Advance cursor so layout continues correctly - ImGui::Dummy(ImVec2(width, numSize.y + denSize.y + 6.0f)); - } - - // Interactive fraction where the denominator is 60 * k, and k can be adjusted by dragging horizontally - inline bool DragDtFraction(const char* id, int& k, bool isTelem = false) { - ImDrawList* draw = ImGui::GetWindowDrawList(); - ImVec2 pos = ImGui::GetCursorScreenPos(); - - // Ensure k is at least 1 to avoid zero or negative denominators - k = std::max(k, 1); - int denom = 30 * k; - - // Limits for k to prevent unreasonable values - const int MIN_K = 1; - const int MAX_K = isTelem ? 20 : 800; - - // Format denominator text - char denomBuf[32]; - snprintf(denomBuf, sizeof(denomBuf), "%d", denom); - - // Calculate text sizes - ImVec2 numSize = ImGui::CalcTextSize("1"); - ImVec2 denSize = ImGui::CalcTextSize(denomBuf); - - // Calculate width and positions - float width = std::max(numSize.x, denSize.x) + 10.0f; - float lineY = pos.y + numSize.y + 2.0f; - float totalHeight = numSize.y + denSize.y + 6.0f; - - // Numerator - draw->AddText( - ImVec2(pos.x + (width - numSize.x) * 0.5f, pos.y), - IM_COL32_WHITE, "1" - ); - - // Line - draw->AddLine( - ImVec2(pos.x, lineY), ImVec2(pos.x + width, lineY), - IM_COL32_WHITE, 1.5f - ); - - // Denominator position - ImVec2 denPos(pos.x + (width - denSize.x) * 0.5f, lineY + 2.0f); - - // Interactive region - ImGui::SetCursorScreenPos(denPos); - ImVec2 hitSize(denSize.x + 10.0f, denSize.y + 6.0f); - ImGui::InvisibleButton(id, hitSize); - - // Determine color based on interaction state - ImU32 colour = ImGui::IsItemActive() - ? IM_COL32(255, 220, 120, 255) // active color - : ImGui::IsItemHovered() - ? IM_COL32(200, 200, 255, 255) // hover color - : IM_COL32_WHITE; - - // Draw denominator with state-aware color - draw->AddText(denPos, colour, denomBuf); - - bool changed = false; - static float dragAccum = 0.0f; - - if (ImGui::IsItemActive()) { - ImGuiIO& io = ImGui::GetIO(); - dragAccum += io.MouseDelta.x; - int step = (int)dragAccum; - - // Update k if drag has accumulated enough to cross a step threshold - if (step != 0) { - dragAccum -= step * 1.0f; - int newK = k + step; - newK = std::clamp(newK, MIN_K, MAX_K); - // Only update if k actually changes - if (newK != k) { - k = newK; - changed = true; - } - } - } - else { dragAccum = 0.0f; } - - // Also allow adjusting k with the mouse wheel when hovering - if (ImGui::IsItemHovered()) { - ImGuiIO& io = ImGui::GetIO(); - - // Mouse wheel typically scrolls vertically, but we can interpret it as horizontal adjustment for this widget - if (io.MouseWheel != 0.0f) { - int newK = k + (int)io.MouseWheel; - newK = std::clamp(newK, MIN_K, MAX_K); - // Only update if k actually changes - if (newK != k) { - k = newK; - changed = true; - } - - // Reset mouse wheel to prevent affecting other widgets - io.MouseWheel = 0.0f; - } - } - - ImGui::NewLine(); - float dt = 1.0f / (float)denom; - ImGui::Text("dt: %.6f s", dt); - - // Advance cursor to account for the space taken by the fraction - ImGui::Dummy(ImVec2(width, totalHeight)); - return changed; - } -} - diff --git a/DSFE_App/DSFE_GUI/include/Rendering/GUIContext.h b/DSFE_App/DSFE_GUI/include/Rendering/GUIContext.h deleted file mode 100644 index 8acd3dd6..00000000 --- a/DSFE_App/DSFE_GUI/include/Rendering/GUIContext.h +++ /dev/null @@ -1,26 +0,0 @@ -// DSFE_Core GUIContext.h -#pragma once - -#include "RenderBase.h" -#include "ui/Styles.h" -#include "Platform/Logger.h" - -#include - -namespace render { - class GUIContext : public RenderContext { - public: - GUIContext() {} - - bool init(window::IWindow* win) override; - void preRender() override; - void postRender() override; - void end() override; - - void setMenuCallback(std::function callback) { _menuCallback = std::move(callback); } - - private: - std::unique_ptr _style; - std::function _menuCallback; - }; -} // namespace render \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Rendering/OpenGLContext.h b/DSFE_App/DSFE_GUI/include/Rendering/OpenGLContext.h deleted file mode 100644 index 1dc44015..00000000 --- a/DSFE_App/DSFE_GUI/include/Rendering/OpenGLContext.h +++ /dev/null @@ -1,21 +0,0 @@ -// DSFE_GUI OpenGLContext.h -#pragma once - -#include "RenderBase.h" -#include "ui/Styles.h" -#include "Platform/Logger.h" - -namespace render { - class OpenGLContext : public RenderContext { - public: - bool init(window::IWindow* window) override; - void preRender() override; - void postRender() override; - void end() override; - - GLFWwindow* getGLFWWindow() const { return _glfwWindow; } - - private: - GLFWwindow* _glfwWindow = nullptr; - }; -} // namespace render \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/ui/CommandScriptEditor.h b/DSFE_App/DSFE_GUI/include/ui/CommandScriptEditor.h deleted file mode 100644 index 112e9af6..00000000 --- a/DSFE_App/DSFE_GUI/include/ui/CommandScriptEditor.h +++ /dev/null @@ -1,104 +0,0 @@ -// DSFE_GUI CommandScriptEditor.h -#pragma once - -#include -#include -#include -#include "Platform/StudyRunner.h" - -#include -#include -#include -#include "Scene/SimulationManager.h" - -#include "Interpreter/RunWrapper.h" - -#include -#include "Platform/imguiWidgets.h" -#include - -#include "Platform/Logger.h" - -struct StudyResult; // forward declaration to avoid circular dependency - -namespace gui { - struct ActiveRun { - std::future fut; - std::string tag; - }; - - class CommandScriptEditor { - public: - CommandScriptEditor(gui::SimManager* sims); - ~CommandScriptEditor(); - - // Menu Render - void drawMenus(); - // Main render function - void render(); - - private: - // Active runs management - std::mutex _activeRunsMutex; // Mutex for synchronizing access to active runs - std::vector _activeRuns; // Vector to hold active runs and their futures - - // Simulation manager reference - gui::SimManager* _sim; - - // Interpreter components - interpreter::Parser* _parser; - interpreter::IStoredProgram* _program; - interpreter::RunWrapper* _wrapper; - - // File browser for loading/saving scripts - ImGui::FileBrowser _load; - ImGui::FileBrowser _save; - - // Current script file directory path - std::string _currentScriptPath; - // Current script file path - std::string _currentScriptFile; - - std::string _pendingSaveName = "Untitled.scl"; - std::string _pendingSavePath = "Engine/assets/scripts"; - bool _requestSaveAsPopup = false; - - void runButtonHandler(); - - void renderEnvironment(); - void renderCmdInstructions(); - - void terminateScript(const char* reason, bool fault); - - bool tryLoadFromDialog(); - bool trySaveScriptToFile(const std::string& filepath); - - void pollRuns(); - void launchBackgroundRun(const std::string& code, const std::string& tag); - - void renderSaveAsPopup(); - - void beginEditorPanel(const char* id); - void endEditorPanel(); - - void setScript(const std::string& s) { _scriptText = s; } - std::string getScript() const { return _scriptText; } - - // Command script contents as lines - std::vector _script; - // Full script text - std::string _scriptText; - - // For log selection - std::unordered_set selectedLines; - // Last clicked line index - int lastClickedLine = -1; - - // Colours - const ImVec4 CMD_COL = ImVec4(0.95f, 0.85f, 0.25f, 1.0f); // yellow - const ImVec4 VEC_COL = ImVec4(0.40f, 0.65f, 0.95f, 1.0f); // blue - const ImVec4 ARG_COL = ImVec4(0.95f, 0.55f, 0.25f, 1.0f); // orange - const ImVec4 DESC_COL = ImVec4(0.60f, 0.60f, 0.60f, 1.0f); // grey - - }; -} // namespace gui \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/ui/ControlPanel.h b/DSFE_App/DSFE_GUI/include/ui/ControlPanel.h deleted file mode 100644 index eec5ed36..00000000 --- a/DSFE_App/DSFE_GUI/include/ui/ControlPanel.h +++ /dev/null @@ -1,232 +0,0 @@ -// DSFE_GUI ControlPanel.h -#pragma once - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "Platform/SimulationState.h" -#include "Platform/Logger.h" - -// Forward Declarations -namespace scene { - class Mesh; - class Object; - class Light; - class Camera; -} -namespace render { - enum class ResolutionPreset; - enum class QualityPreset; -} - -namespace robots { class RobotSystem; } -namespace diagnostics { class TelemetryRecorder; } - -namespace gui { - // Forward Declaration for SimManager - class SimManager; - enum class ControlMode; - - // Gravity UI Modes - enum class GravityUIMode { - Preset, - SolarSystem, - Custom - }; - - // Gravity Presets (in m/s^2) - enum GravityPreset { - // Common Presets - PRESET_ZERO_G, - PRESET_MICRO_G, - PRESET_SOLAR_SYSTEM, - PRESET_CUSTOM, - // Solar System Bodies - PRESET_SUN, - PRESET_MERCURY, - PRESET_VENUS, - PRESET_EARTH, - PRESET_MARS, - PRESET_JUPITER, - PRESET_SATURN, - PRESET_URANUS, - PRESET_NEPTUNE, - PRESET_PLUTO, - // Moons - PRESET_MOON, - PRESET_TITAN, - PRESET_ENCELADUS, - PRESET_EUROPA, - PRESET_GANYMEDE, - PRESET_IO - }; - - // UI Navigation Levels for Gravity Presets - enum class GravityLevel { - Root, - SolarSystem, - Planets, - Moons - }; - - inline GravityLevel gravityLevel = GravityLevel::Root; - inline GravityUIMode gravityMode = GravityUIMode::Preset; - - // Struct to hold comparison results for integrator analysis - struct ComparisonSnapshot { - std::string integratorName; - std::vector time; // time samples - std::vector errRms; // RMS error time series - std::vector errMax; // Max error time series - std::vector> jointErr; // [joint][sample] - int jointCount = 0; - }; - - // ControlPanel Class - class ControlPanel { - public: - ControlPanel(SimManager* sim); - - void drawMenus(SimManager* sim); - void render(SimManager* sim); - void setSimulationCallback(const std::function& callback) { simCallback = callback; } - void setMeshLoadCallback(const std::function& callback) { meshLoadCallback = callback; } - - private: - // Comparison results management - std::vector _comparisonResults; - std::vector> _comparisonFutures; - std::atomic _comparisonPending{ 0 }; - std::mutex _comparisonMutex; - bool _comparisonReady = false; - - // Internal Pointers - SimManager* _sim = nullptr; - std::shared_ptr _mesh = nullptr; - scene::Light* _light = nullptr; - scene::Object* _obj = nullptr; - ImGui::FileBrowser _meshLoad; - ImGui::FileBrowser _hdrLoad; - std::string _currentMeshFile; - std::string _currentHDRFile; - - ControlMode* _controlMode; - - std::function meshLoadCallback; - std::function simCallback; - - Selection _selection; - - // Internal Methods - void simulationProperties(); - void objectProperties(); - void jointProperties(); - void displaySettings(); - - void tempLightControls(); - - void roboticArmSelector(); - void roboticCardDisplay(const char* name, const char* company); - - void sceneObjectsTable(); - - // Follow View Methods - void selectJointAndFollow(int jointIndex); - //void drawTelemetryPlots(const diagnostics::TelemetryRecorder& rec); - void drawTrajectoryInspector(const diagnostics::TelemetryRecorder& rec, int jointCount, int& selectedJoint); - void drawJointErrorPlot(); - - // Results Methods - void drawResultsWindow(); - void exportTelemetryCSV(const char* filepath); - void runComparisonAllIntegratorsAsync(); - void pollComparisonFutures(); - void drawComparisonPlots(); - - // Helper Methods - void beginControlPanel(const char* id, ImVec2 size = ImVec2(0, 0)); - void endControlPanel(); - - // Menu Button States - bool _showRobotSelector = false; - bool _showSimulationProperties = false; - bool _showCameraProperties = false; - bool _showDisplaySettings = false; - bool autoScroll = true; - bool scrollToBottom = false; - bool _useAutoDiff = false; - - // Render Presets - render::ResolutionPreset r; - render::QualityPreset q; - - bool _qualityChanged = false; - bool _resChanged = false; - - // Internal states - bool simulationRunning = false; - bool _jointSelected = false; - bool diagRunning = false; - bool _robotRequested = false; - bool _hasRobot = false; - bool _openStats = true; - - // Results tab/window state - bool _showResultsWindow = false; - bool _simWasRunningLastFrame = false; - bool _resultsFocusNeeded = false; - bool _selectResultsTab = false; - - // Currently selected items - std::string _requestedRobot; // name of requested robot to load - std::string _currentObjectName; // name of currently selected object - std::string _currentLinkName; // name of currently selected link - std::string _currentJointName; // name of currently selected joint - std::string _lastLinkName; // name of last selected link - - // Store joint angles for display - std::unordered_map _linkAngles; - - // Simulation and diagnostics timing - float simLength = 30.0f; // ~30 seconds default - float diagLength = 15.0f; // ~15 seconds default - double deltaTime = 1.0f / 180.0f; // ~180 FPS default - float simTime = 0.0f; // current simulation time - float diagTime = 0.0f; // current diagnostic time - - // Plot heights for comparison - float _cPlotH1 = 250.0f; - float _cPlotH2 = 250.0f; - - // Plot heights for results - float _plotH1 = 250.0f; - float _plotH2 = 250.0f; - - // Camera properties - int povMode = 0; - float fov = 60.0f; - - // Internal Physics - float velocity = 0.0f; - float torque = 0.0f; - float linkLength = 1.0f; - float damping = 0.1f; - float position = 0.0f; - - // Time tracking for simulation updates - std::chrono::high_resolution_clock::time_point simLastUpdateTime = std::chrono::high_resolution_clock::now(); - std::chrono::high_resolution_clock::time_point diagLastUpdateTime = std::chrono::high_resolution_clock::now(); - }; -} // namespace gui - -// Helper Macros - -#define INDENT() ImGui::Indent(20.0f) -#define UNINDENT() ImGui::Unindent(20.0f) \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/ui/DebugPanel.h b/DSFE_App/DSFE_GUI/include/ui/DebugPanel.h deleted file mode 100644 index 6af36ec2..00000000 --- a/DSFE_App/DSFE_GUI/include/ui/DebugPanel.h +++ /dev/null @@ -1,58 +0,0 @@ -// DSFE_GUI DebugPanel.h -#pragma once - -#include "imgui.h" -#include "Scene/SimulationManager.h" -#include "Scene/Camera.h" -#include - -#include "Platform/Logger.h" - -namespace gui { - struct debugLogEntry { - std::string level; - std::string desc; - bool isError = false; - }; - - struct TerminalLine { - enum class Level { Info, Warn, Error, Debug }; - Level level = Level::Info; - std::string text; - }; - - class DebugPanel { - public: - void render(); - void clearSimLog() { gLog.Instance().clearSimLog(); simSelectedLines.clear(); } - - private: - bool autoScroll = true; - std::vector _entries; - std::vector _simEnteries; - - void renderLog(); - void renderSimLog(); - void renderErrorTable(); - - void beginDebugPanel(const char* id, ImVec2 size = ImVec2(0, 0)); - void endDebugPanel(); - - std::unordered_set selectedLines; - std::unordered_set simSelectedLines; - int lastClickedLine = -1; - - // Colour Keys (I am colour deficient so GPT has been used to generate the colour values) - const ImVec4 traceCol = { 0.6275f, 0.6275f, 0.6275f, 1.0f }; - const ImVec4 debugCol = { 0.3137f, 0.6667f, 0.9412f, 1.0f }; - const ImVec4 infoCol = { 0.3137f, 0.6275f, 1.0f, 1.0f }; - const ImVec4 warnCol = { 1.0f, 0.7059f, 0.0f, 1.0f }; - const ImVec4 errorCol = { 0.8627f, 0.2353f, 0.2745f, 1.0f }; - const ImVec4 okCol = { 0.0f, 0.7451f, 0.3922f, 1.0f }; - const ImVec4 failCol = { 0.6667f, 0.0f, 0.1961f, 1.0f }; - const ImVec4 runtimeCol = { 0.7451f, 0.4706f, 1.0f, 1.0f }; - const ImVec4 outputCol = { 0.8627f, 0.8627f, 0.8627f, 1.0f }; - const ImVec4 rotateCol = { 0.4666f, 1.0f, 0.0f, 1.0f }; - const ImVec4 translateCol = { 0.2745f, 0.5098f, 0.7059f, 1.0f }; - }; -} // namespace gui \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/ui/Styles.h b/DSFE_App/DSFE_GUI/include/ui/Styles.h deleted file mode 100644 index e3ba75ee..00000000 --- a/DSFE_App/DSFE_GUI/include/ui/Styles.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -// File: Styles.h -// GitHub: SaltyJoss -#include "EngineCore.h" - -namespace gui { - class Styles { - public: - /// DARK MODE STYLE (VS 2022 esc) - void DarkMode(); - - /// LIGHT MODE STYLE (VS 2022 esc) - void LightMode(); - }; -} // namespace gui diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp index 37d9d133..36d5b7f8 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp @@ -10,6 +10,9 @@ #include #include #include +#include + +#include #include "Widgets/FractionSelectorWidget.h" @@ -42,6 +45,14 @@ namespace widgets { simPropertiesPanel(); jointInfoPanel(); + auto* timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, [this]() { + updateSimClock(); + jointInfoPanel(); + updateTelemetryDisplay(); + }); + timer->start(7); + _contentLayout->addStretch(); } @@ -68,6 +79,11 @@ namespace widgets { _telemetryDtSelector = new FractionSelectorWidget(true); layout->addWidget(new QLabel("Telemetry Time Step (dt):")); layout->addWidget(_telemetryDtSelector); + _simTimeLabel = new QLabel(); + _simTimeLabel->setTextFormat(Qt::RichText); + _simTimeLabel->setWordWrap(true); + layout->addWidget(new QLabel("Simulation Time:")); + layout->addWidget(_simTimeLabel); _contentLayout->addWidget(_simPropertiesGroup); @@ -123,73 +139,70 @@ namespace widgets { } void ControlPanelWidget::jointInfoPanel() { - _jointInfoGroup = new QGroupBox("Robot Joint Information"); - auto* layout = new QVBoxLayout(_jointInfoGroup); - _contentLayout->addWidget(_jointInfoGroup); + if (!_jointInfoGroup) { + _jointInfoGroup = new QGroupBox("Robot Joint Information"); + auto* layout = new QVBoxLayout(_jointInfoGroup); + _jointInfoGroup->setLayout(layout); + _currentSimTimeJointLabel = new QLabel(_jointInfoGroup); + layout->addWidget(_currentSimTimeJointLabel); + + _jointIdxSlider = new QSlider(Qt::Horizontal, _jointInfoGroup); + _jointIdxSlider->setMinimum(1); + _jointIdxSlider->setValue(1); + connect(_jointIdxSlider, &QSlider::valueChanged, this, [this](int value) { selectJointAndFollow(value - 1); }); + layout->addWidget(_jointIdxSlider); + + buildTelemetryWidgets(layout); + + _contentLayout->addWidget(_jointInfoGroup); + } - if (!_sim->hasRobot()) { - layout->addWidget(new QLabel("No Robot Loaded.")); + if (!_sim || !_sim->hasRobot()) { + _jointInfoGroup->setVisible(false); return; } robots::RobotSystem* robot = _sim->robotSystem(); if (!robot) { - layout->addWidget(new QLabel("Robotic system unavailable")); + _jointInfoGroup->setVisible(false); return; } auto& joints = robot->joints(); auto& links = robot->links(); - - _jointIdxSlider = new QSlider(Qt::Orientation::Horizontal); - layout->addWidget(_jointIdxSlider); + _jointInfoGroup->setVisible(!joints.empty() && !links.empty()); + if (joints.empty() || links.empty()) { _jointInfoGroup->setVisible(false); return; } + _jointInfoGroup->setVisible(true); + _jointIdxSlider->setMaximum(static_cast(joints.size())); static int currentJointIndex = 0; currentJointIndex = std::clamp(currentJointIndex, 0, (int)joints.size() - 1); - - auto& j = joints[currentJointIndex]; - int linkIndex = currentJointIndex; - linkIndex = std::clamp(linkIndex, 0, (int)links.size() - 1); - auto& l = links[linkIndex]; - - const auto& rec = _sim->telemetry(); - displayJointInfo(rec, currentJointIndex, layout); - layout->addSpacing(5); - layout->addWidget(new QLabel("Selected Joint: " + QString::fromStdString(j.name) + " - Child Link: " + QString::fromStdString(l.name))); } - void ControlPanelWidget::displayJointInfo(const diagnostics::TelemetryRecorder& rec, int& selectedJoint, QVBoxLayout* layout) { - const auto& ring = rec.ring; - if (ring.size() < 1) { return; } - const diagnostics::TelemetrySample& s = ring.at(ring.size() - 1); // Get the most recent sample - if (selectedJoint < 0) { selectedJoint = 0; } - if (selectedJoint >= (int)s.j.size()) { selectedJoint = (int)s.j.size() - 1; } + void ControlPanelWidget::updateTelemetryInfo(const diagnostics::JointTelemetry& j) { + auto& t = _telemetryLabels; + const float e = static_cast(j.q_ref - j.q); - _currentSimTimeJointLabel = new QLabel(QString("Current Simulation Time: ") + QString::number(s.timeSec) + " s"); - layout->addWidget(_currentSimTimeJointLabel); + t.q->setText(QString("pos:\t%1 rad").arg(j.q)); + t.qd->setText(QString("vel:\t%1 rad/s").arg(j.qd)); + t.tau->setText(QString("torque:\t%1 Nm").arg(j.torqueNm)); - int currentJ = selectedJoint + 1; - if (!_jointIdxSlider) { - selectedJoint = currentJ - 1; - - _jointIdxSlider->setMinimum(1); - _jointIdxSlider->setMaximum((int)s.j.size()); - _jointIdxSlider->setValue(currentJ); - connect(_jointIdxSlider, &QSlider::valueChanged, this, [this](int value) { - int jointIdx = value - 1; - selectJointAndFollow(jointIdx); - }); - } - else { - selectedJoint = currentJ - 1; - _jointIdxSlider->setMaximum((int)s.j.size()); - _jointIdxSlider->setValue(currentJ); - } + t.qRef->setText(QString("pos_ref:\t%1 rad").arg(j.q_ref)); + t.qdRef->setText(QString("vel_ref:\t%1 rad/s").arg(j.qd_ref)); + t.qddRef->setText(QString("acc_ref:\t%1 rad/s²").arg(j.qdd_ref)); + t.err->setText(QString("error:\t%1 rad").arg(e)); - const diagnostics::JointTelemetry& j = s.j[selectedJoint]; - const float e = static_cast(j.q_ref - j.q); + t.qTraj->setText(QString("pos_traj:\t%1 rad").arg(j.traj_q)); + t.qdTraj->setText(QString("vel_traj:\t%1 rad/s").arg(j.traj_qd)); + t.qddTraj->setText(QString("acc_traj:\t%1 rad/s²").arg(j.traj_qdd)); + + t.qClamped->setText(QString("pos_clamped:\t%1").arg(j.clampTheta ? "true" : "false")); + t.qdClamped->setText(QString("vel_clamped:\t%1").arg(j.clampOmega ? "true" : "false")); - layout->addSpacing(10); + t.damping->setText(QString("damping:\t%1 kg·m²/s").arg(j.damping)); + t.friction->setText(QString("friction:\t%1 N·m").arg(j.friction)); + } + void ControlPanelWidget::buildTelemetryWidgets(QVBoxLayout* layout) { auto headerFont = [](QLabel* label) { QFont font = label->font(); font.setBold(true); @@ -197,54 +210,90 @@ namespace widgets { label->setFont(font); }; - - auto* stateLabel = new QLabel(QString("State: ")); - headerFont(stateLabel); - auto* refLabel = new QLabel(QString("Reference: ")); - headerFont(refLabel); - auto* trajLabel = new QLabel(QString("Trajectory: ")); - headerFont(trajLabel); - auto* clampLabel = new QLabel(QString("Clamped: ")); - headerFont(clampLabel); - auto* constLabel = new QLabel(QString("Constants: ")); - headerFont(constLabel); - - // Joint State Telemetry - layout->addWidget(stateLabel); - layout->addWidget(new QLabel(QString("pos: ") + QString::number(j.q) + " rad")); - layout->addWidget(new QLabel(QString("vel: ") + QString::number(j.qd) + " rad/s")); - layout->addWidget(new QLabel(QString("torque: ") + QString::number(j.torqueNm) + " Nm")); + auto& t = _telemetryLabels; + + t.stateHeader = new QLabel("State:"); + t.referenceHeader = new QLabel("Reference:"); + t.trajectoryHeader = new QLabel("Trajectory:"); + t.clampedHeader = new QLabel("Clamped:"); + t.constantsHeader = new QLabel("Constants:"); + + headerFont(t.stateHeader); + headerFont(t.referenceHeader); + headerFont(t.trajectoryHeader); + headerFont(t.clampedHeader); + headerFont(t.constantsHeader); + + t.q = new QLabel(); + t.qd= new QLabel(); + t.tau = new QLabel(); + + t.qRef = new QLabel(); + t.qdRef = new QLabel(); + t.qddRef = new QLabel(); + t.err = new QLabel(); + + t.qTraj = new QLabel(); + t.qdTraj = new QLabel(); + t.qddTraj = new QLabel(); + + t.qClamped = new QLabel(); + t.qdClamped = new QLabel(); + + t.damping = new QLabel(); + t.friction = new QLabel(); layout->addSpacing(5); - // Joint Reference Telemetry - layout->addWidget(refLabel); - layout->addWidget(new QLabel(QString("pos_ref: ") + QString::number(j.q_ref) + " rad")); - layout->addWidget(new QLabel(QString("vel_ref: ") + QString::number(j.qd_ref) + " rad/s")); - layout->addWidget(new QLabel(QString("acc_ref: ") + QString::number(j.qdd_ref) + " rad/s²")); - layout->addWidget(new QLabel(QString("error: ") + QString::number(e) + " rad")); + layout->addWidget(t.stateHeader); + layout->addWidget(t.q); + layout->addWidget(t.qd); + layout->addWidget(t.tau); layout->addSpacing(5); - // Joint Trajectory Telemetry - layout->addWidget(trajLabel); - layout->addWidget(new QLabel(QString("pos_traj: ") + QString::number(j.traj_q) + " rad")); - layout->addWidget(new QLabel(QString("vel_traj: ") + QString::number(j.traj_qd) + " rad/s")); - layout->addWidget(new QLabel(QString("acc_traj: ") + QString::number(j.traj_qdd) + " rad/s²")); + layout->addWidget(t.referenceHeader); + layout->addWidget(t.qRef); + layout->addWidget(t.qdRef); + layout->addWidget(t.qddRef); + layout->addWidget(t.err); layout->addSpacing(5); - // Joint Clamping Telemetry - layout->addWidget(clampLabel); - layout->addWidget(new QLabel(QString("pos_clamped: ") + QString(j.clampTheta ? "true" : "false"))); - layout->addWidget(new QLabel(QString("vel_clamped: ") + QString(j.clampOmega ? "true" : "false"))); + layout->addWidget(t.trajectoryHeader); + layout->addWidget(t.qTraj); + layout->addWidget(t.qdTraj); + layout->addWidget(t.qddTraj); layout->addSpacing(5); - // Joint Constants Telemetry - layout->addWidget(constLabel); - layout->addWidget(new QLabel(QString("damping: ") + QString::number(j.damping) + " kg·m²/s")); - layout->addWidget(new QLabel(QString("friction: ") + QString::number(j.friction) + " N·m")); + layout->addWidget(t.clampedHeader); + layout->addWidget(t.qClamped); + layout->addWidget(t.qdClamped); + + layout->addSpacing(5); + + layout->addWidget(t.constantsHeader); + layout->addWidget(t.damping); + layout->addWidget(t.friction); + } + + void ControlPanelWidget::updateTelemetryDisplay() { + if (!_sim) { return; } + const auto& rec = _sim->telemetry(); + const auto& ring = rec.ring; + if (ring.size() < 1) { return; } + const auto& s = ring.at(ring.size() - 1); + if (s.j.empty()) { return; } + int jointIdx = _selection.type == SelectionType::JOINT ? _selection.index : 0; + jointIdx = std::clamp(jointIdx, 0, static_cast(s.j.size()) - 1); + if (_jointIdxSlider) { + QSignalBlocker blocker(_jointIdxSlider); + _jointIdxSlider->setMaximum(static_cast(s.j.size())); + _jointIdxSlider->setValue(jointIdx + 1); + } + updateTelemetryInfo(s.j[jointIdx]); + if (_currentSimTimeJointLabel) { _currentSimTimeJointLabel->setText(QString("Current Simulation Time: %1 s").arg(simTime, 0, 'f', 3)); } } void ControlPanelWidget::displayPanel() { @@ -271,4 +320,29 @@ namespace widgets { _sim->followRobotJoint(_currentJointName, glm::vec3(0.0f, 0.2f, 0.6f)); } + + void ControlPanelWidget::updateSimClock() { + if (!_sim || !_simTimeLabel) { return; } + + const double elapsed = _sim->simTime(); + const bool running = _sim->isSimRunning(); + + if (!running && elapsed <= 0.0) { + _simTimeLabel->clear(); + _simTimeLabel->setVisible(false); + return; + } + + _simTimeLabel->setVisible(true); + _simTimeLabel->setTextFormat(Qt::RichText); + _simTimeLabel->setWordWrap(true); + + if (_sim->isSimRunning()) { + simTime = _sim->simTime(); + _simTimeLabel->setText(QString("Elapsed Time: %1 s").arg(_sim->simTime(), 0, 'f', 3)); + } + else { + _simTimeLabel->setText(QString("Elapsed Time: %1 s").arg(_sim->simTime(), 0, 'f', 3)); + } + } } // namespace widgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp index 453a1342..77233689 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp @@ -58,7 +58,6 @@ namespace widgets { _stateTimer->start(100); // Poll every 100 ms connect(_runStopButton, &QPushButton::clicked, this, [this]() { _sim->isScriptRunning() ? stopScript() : runScript(); }); - pollScriptState(); } @@ -214,6 +213,7 @@ namespace widgets { } void DSLEditorWidget::pollScriptState() { + updateButtonState(_sim->isScriptRunning()); if (!_sim->isScriptRunning()) { return; } auto* prog = _sim->activeProgram(); if (!prog) { terminateScript("No active program found in simulation manager.", true); return; } diff --git a/DSFE_App/DSFE_GUI/src/Platform/WindowManager.cpp b/DSFE_App/DSFE_GUI/src/Platform/WindowManager.cpp deleted file mode 100644 index ca5ecebe..00000000 --- a/DSFE_App/DSFE_GUI/src/Platform/WindowManager.cpp +++ /dev/null @@ -1,200 +0,0 @@ -// DSFE_GUI WindowManager.cpp -#include "pch.h" - -#ifdef __gl_h_ -#undef __gl_h_ -#endif -#include -#include - -#include "Platform/Window.h" - -#include "Rendering/GUIContext.h" -#include "Rendering/OpenGLContext.h" -#include "Rendering/OpenGLBufferManager.h" -#include "Rendering/ShaderUtil.h" - -#include "Scene/Camera.h" -#include "Scene/Light.h" -#include "Scene/Input.h" -#include "Scene/Mesh.h" - -#include "Scene/SimulationManager.h" -#include "ui/DebugPanel.h" -#include "ui/ControlPanel.h" -#include "ui/CommandScriptEditor.h" - -#include "Platform/WindowManager.h" - -#include "EngineLib/LogMacros.h" - -bool controlPanelOpen = false; - -namespace window { - // Constructor - GLWindow::GLWindow() { _header = new std::string(); } - // Destructor - GLWindow::~GLWindow() { - _renderCntx->end(); - _GUICntx->end(); - - if (_window) { glfwDestroyWindow(_window); } - - glfwTerminate(); - delete _header; - - LOG_INFO("GLWindow destroyed, rendering and GUI contexts ended"); - } - - void GLWindow::render() { - _renderCntx->preRender(); - _GUICntx->preRender(); - - if (_sim) _sim->render(); - if (_controlPanel) _controlPanel->render(_sim.get()); - if (_debugPanel) _debugPanel->render(); - if (_cmdEditor) _cmdEditor->render(); - - - // Menu Callback - _GUICntx->setMenuCallback([this]() { - - ImGuiStyle& style = ImGui::GetStyle(); - // Vertical spacing between menu items - style.ItemSpacing = ImVec2(10.0f, 6.0f); - // Padding inside menus - style.WindowPadding = ImVec2(12.0f, 12.0f); // (left/right, top/bottom) - // Padding inside each menu item - style.FramePadding = ImVec2(8.0f, 4.0f); - - _controlPanel->drawMenus(_sim.get()); - _cmdEditor->drawMenus(); - }); - - _GUICntx->postRender(); - _renderCntx->postRender(); - } - - // Initialisation: Sets up the OpenGL context, GUI context, and simulation manager - bool GLWindow::init(int width, int height, const std::string& header) { - _width = width; - _height = height; - *_header = header; - - // Context layers - _renderCntx = std::make_unique(); - _renderCntx->init(this); - - _window = _renderCntx->getGLFWWindow(); - - _GUICntx = std::make_unique(); - _GUICntx->init(this); - - // UI + scene - _sim = std::make_unique(); - _sim->initGL(); - _controlPanel = std::make_unique(_sim.get()); - _debugPanel = std::make_unique(); - _cmdEditor = std::make_unique(_sim.get()); - - _controlPanel->setMeshLoadCallback([this](std::string path) { - _sim->loadMesh(path); - LOG_INFO("Mesh loaded: %s", path.c_str()); - } - ); - - _isRunning = true; - return true; - } - - // Window resize callback: Enforces 16:9 aspect ratio and updates the OpenGL viewport - void GLWindow::onResize(int width, int height) { - if (width <= 0 || height <= 0) return; - - _width = width; - _height = height; - - // Update the GL viewport for the default framebuffer - glViewport(0, 0, width, height); - } - - // Miscellaneous - bool GLWindow::shouldClose() const { return glfwWindowShouldClose(_window); } - void GLWindow::pollEvents() { glfwPollEvents(); } - void GLWindow::swapBuffers() { glfwSwapBuffers(_window); } - void* window::GLWindow::getNativeWin() { return static_cast(_window); } - void window::GLWindow::setNativeWin(void* window) { _window = static_cast(window);} - int window::GLWindow::getWidth() const { return _width; } - int window::GLWindow::getHeight() const { return _height; } - const std::string& window::GLWindow::getHeader() const { return *_header; } - - // Update loop: Handles input and updates the simulation state - void GLWindow::update() { - pollEvents(); - - static double lastFrame = glfwGetTime(); - double currentFrame = glfwGetTime(); - float dt = static_cast(currentFrame - lastFrame); - lastFrame = currentFrame; - - // minimal smoothing (optional) - static float smoothedDt = 0.016f; - smoothedDt = glm::mix(smoothedDt, dt, 0.2f); - - if (_sim) { - //_sim->handleContinuousMovement(_window, smoothedDt); - //_sim->getCamera()->applyGravity(smoothedDt, _sim->getPlaneHeight()); - } - } - - // Input handling - void window::GLWindow::setMouseCaptured(bool captured) { - _mouseCaptured = captured; - GLFWwindow* w = _window; - if (!w) { return; } - - // When mouse is captured, disable the cursor and enable raw mouse motion for high-precision input - if (_mouseCaptured) { - // When mouse is captured, disable the cursor and enable raw mouse motion for high-precision input - glfwSetInputMode(w, GLFW_CURSOR, GLFW_CURSOR_DISABLED); - if (glfwRawMouseMotionSupported()) { - glfwSetInputMode(w, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); - } - ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouse; - // tell SimManager to reset its first-mouse state - if (_sim) { _sim->resetMouseDelta(); } - } - else { - // When mouse is released, show the cursor and disable raw mouse motion - glfwSetInputMode(w, GLFW_CURSOR, GLFW_CURSOR_NORMAL); - if (glfwRawMouseMotionSupported()) { - glfwSetInputMode(w, GLFW_RAW_MOUSE_MOTION, GLFW_FALSE); - } - ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouse; - } - } - - // Toggle mouse capture on Escape key press, and also toggle the control panel visibility - void window::GLWindow::onKey(int key, int /*scancode*/, int action, int /*mods*/) { - if (action == GLFW_PRESS && key == GLFW_KEY_ESCAPE) { - setMouseCaptured(!_mouseCaptured); - controlPanelOpen = !controlPanelOpen; - ImGui::SetWindowFocus(nullptr); - return; - } - } - - // Forward scroll events to the SimManager for zooming or other scroll-based interactions - void window::GLWindow::onScroll(double delta) { - if (_sim) { _sim->onMouseWheel(delta); } - } - - // Forward window resize events to the SimManager to adjust the internal rendering resolution and aspect ratio - void window::GLWindow::onCursorPos(double xpos, double ypos) { - // LOG_INFO("Mouse moved to: X=%.2f, Y=%.2f", xpos, ypos); - } - - // Window states - bool GLWindow::isRunning() const { return _isRunning; } - void GLWindow::onClose() { _isRunning = false; } -} diff --git a/DSFE_App/DSFE_GUI/src/Rendering/GUIContext.cpp b/DSFE_App/DSFE_GUI/src/Rendering/GUIContext.cpp deleted file mode 100644 index 50631e21..00000000 --- a/DSFE_App/DSFE_GUI/src/Rendering/GUIContext.cpp +++ /dev/null @@ -1,114 +0,0 @@ -// DSFE_GUI GUIContext.cpp -#include "Utils.h" - -#ifdef __gl_h_ -#undef __gl_h_ -#endif -#include -#include - -#include "Rendering/GUIContext.h" -#include "EngineLib/LogMacros.h" - -// ImGui -#include -#include -#include - -// ImPlot -#include - -namespace render { - bool render::GUIContext::init(window::IWindow* window) { - __super::init(window); - - const char* glslVersion = "#version 460 core"; - - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImPlot::CreateContext(); - - ImGuiIO& io = ImGui::GetIO(); (void)io; - io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard - | ImGuiConfigFlags_DockingEnable - | ImGuiConfigFlags_ViewportsEnable; - - try { - io.IniFilename = "config/imgui.ini"; - } - catch (const std::exception& e) { - LOG_ERROR("Failed to set ImGui ini file path: %s, loading default", e.what()); - io.IniFilename = nullptr; // fallback to default in-memory ini - } - - _style = std::make_unique(); - _style->DarkMode(); - - ImGui_ImplGlfw_InitForOpenGL((GLFWwindow*)_window->getNativeWin(), true); - ImGui_ImplOpenGL3_Init(glslVersion); - - LOG_INFO("ImGui context initialised (GLSL %s)", glslVersion); - return true; - } - - void render::GUIContext::preRender() { - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - - ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar - | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize - | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus - | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground; - - ImGuiViewport* viewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(viewport->Pos); - ImGui::SetNextWindowSize(viewport->Size); - ImGui::SetNextWindowViewport(viewport->ID); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - ImGui::Begin("Invisible-Window", nullptr, windowFlags | ImGuiWindowFlags_MenuBar); - - // Menu from control panel, moved here -> need to find a way to connect the two! - if (ImGui::BeginMenuBar()) { - if (_menuCallback) { _menuCallback(); } - ImGui::EndMenuBar(); - } - - ImGui::PopStyleVar(3); - - ImGuiID dockingSpaceID = ImGui::GetID("Invisible-Window-Docking-Space"); - - ImGui::DockSpace(dockingSpaceID, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_PassthruCentralNode); - ImGui::End(); - - LOG_INFO_ONCE("ImGui preRender frame prepared with docking space ID %u", dockingSpaceID); - } - - void render::GUIContext::postRender() { - ImGui::Render(); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - - ImGuiIO& io = ImGui::GetIO(); - - if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { - GLFWwindow* backupCurrentContext = glfwGetCurrentContext(); - ImGui::UpdatePlatformWindows(); - ImGui::RenderPlatformWindowsDefault(); - glfwMakeContextCurrent(backupCurrentContext); - - LOG_INFO_ONCE("ImGui platform windows rendered (viewports enabled)"); - } - } - - void render::GUIContext::end() { - ImPlot::DestroyContext(); - ImGui_ImplOpenGL3_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); - - LOG_INFO("ImGui context shutdown completed"); - } -} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Rendering/OpenGLContext.cpp b/DSFE_App/DSFE_GUI/src/Rendering/OpenGLContext.cpp deleted file mode 100644 index 0ab0ccfd..00000000 --- a/DSFE_App/DSFE_GUI/src/Rendering/OpenGLContext.cpp +++ /dev/null @@ -1,113 +0,0 @@ -#include "pch.h" -// File: OpenGLContext.cpp -// GitHub: SaltyJoss -#ifdef __gl_h_ -#undef __gl_h_ -#endif -#include -#include -#include "Rendering/OpenGLContext.h" -#include - -#include "EngineLib/LogMacros.h" - -namespace render { - // GLFW Callback for key events - static void onKey_Callback(GLFWwindow* win, int key, int scancode, int action, int mods) { - auto currentWindow = static_cast(glfwGetWindowUserPointer(win)); - currentWindow->onKey(key, scancode, action, mods); - } - - // GLFW Callback for cursor position events - static void CursorPos_Callback(GLFWwindow* win, double xpos, double ypos) { - auto currentWindow = static_cast(glfwGetWindowUserPointer(win)); - currentWindow->onCursorPos(xpos, ypos); - } - - // GLFW Callback for scroll events - static void onScroll_Callback(GLFWwindow* win, double /*xoffset*/, double yoffset) { - auto currentWindow = static_cast(glfwGetWindowUserPointer(win)); - currentWindow->onScroll(yoffset); - } - - // GLFW Callback for window resize events - static void onResize_Callback(GLFWwindow* win, int width, int height) { - auto currentWindow = static_cast(glfwGetWindowUserPointer(win)); - currentWindow->onResize(width, height); - } - - // GLFW Callback for window close events - static void onClose_Callback(GLFWwindow* win) { - window::IWindow* currentWindow = static_cast(glfwGetWindowUserPointer(win)); - currentWindow->onClose(); - } - - // Initialize OpenGL context and create GLFW window - bool render::OpenGLContext::init(window::IWindow* window) { - __super::init(window); - - if (!window->getWidth() || !window->getHeight()) { - LOG_ERROR("Window dimensions not set!"); - return false; - } - - if (!glfwInit()) { - LOG_ERROR("Failed to initialize GLFW -> ", glfwGetError(NULL)); - return false; - } - - // Set GLFW window hints for OpenGL version and profile - auto glWindow = glfwCreateWindow(window->getWidth(), window->getHeight(), window->getHeader().c_str(), nullptr, nullptr); - glfwSetInputMode(glWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED); - glfwSetInputMode(glWindow, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); - - // Check if window creation succeeded - if (!glWindow) { - LOG_ERROR("Failed to create GLFW window -> ", glfwGetError(NULL)); - glfwTerminate(); - return false; - } - - window->setNativeWin(glWindow); - _glfwWindow = glWindow; - - // Set up OpenGL context and callbacks - glfwSwapInterval(1); - glfwSetWindowUserPointer(glWindow, window); // Set user pointer to access window instance in callbacks - glfwSetWindowSizeLimits(glWindow, 800, 450, GLFW_DONT_CARE, GLFW_DONT_CARE); // Minimum 16:9 at 800x450, no maximum - glfwSetWindowAspectRatio(glWindow, 16, 9); // Enforce 16:9 aspect ratio - glfwSetCursorPosCallback(glWindow, CursorPos_Callback); - glfwSetKeyCallback(glWindow, onKey_Callback); - glfwSetScrollCallback(glWindow, onScroll_Callback); - glfwSetWindowSizeCallback(glWindow, onResize_Callback); - glfwSetWindowCloseCallback(glWindow, onClose_Callback); - glfwMakeContextCurrent(glWindow); - - // Load OpenGL function pointers using GLAD - if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { - LOG_ERROR("Failed to initialise GLAD"); - return false; - } - - glEnable(GL_DEPTH_TEST); - return true; - } - - // Set viewport and clear buffers before rendering - void render::OpenGLContext::preRender() { - glViewport(0, 0, _window->getWidth(), _window->getHeight()); - glClearColor(0.33f, 0.33f, 0.33f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - } - - // Swap buffers after rendering - void render::OpenGLContext::postRender() { - glfwSwapBuffers((GLFWwindow*)_window->getNativeWin()); - } - - // Clean up GLFW resources and terminate context - void render::OpenGLContext::end() { - glfwDestroyWindow((GLFWwindow*)_window->getNativeWin()); - glfwTerminate(); - } -} diff --git a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp index d11b1720..3f2b6845 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp @@ -3,7 +3,10 @@ #include "Scene/SimulationManager.h" #include "Scene/SimulationCore.h" +#include +#include #include +#include extern "C" core::ISimulationCore* CreateSimulationCore_v1(); extern "C" void DestroySimulationCore(core::ISimulationCore*); @@ -64,9 +67,7 @@ namespace gui { // Helper: create CorePtr (unique_ptr with std::function deleter) static CorePtr makeCoreFactory() { core::ISimulationCore* raw = CreateSimulationCore_v1(); - if (!raw) { - return CorePtr(nullptr, [](core::ISimulationCore*) {}); - } + if (!raw) { return CorePtr(nullptr, [](core::ISimulationCore*) {}); } // std::function deleter is constructed from the lambda implicitly return CorePtr(raw, [](core::ISimulationCore* p) { DestroySimulationCore(p); }); } @@ -316,6 +317,4 @@ namespace gui { } void gui::SimManager::resetMouseDelta() { _firstMouse = true; } - - // --- Helpers --- } \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/ui/CommandScriptEditor.cpp b/DSFE_App/DSFE_GUI/src/ui/CommandScriptEditor.cpp deleted file mode 100644 index 104a65b0..00000000 --- a/DSFE_App/DSFE_GUI/src/ui/CommandScriptEditor.cpp +++ /dev/null @@ -1,703 +0,0 @@ -// DSFE_GUI CommandScriptEditor.cpp -#include -#include "ui/CommandScriptEditor.h" - -#include "Numerics/IntegrationMethods.h" -#include "Interpreter/StoredProgram.h" - -#include - -#include "Platform/Paths.h" -#include "EngineLib/LogMacros.h" - -namespace gui { - CommandScriptEditor::CommandScriptEditor(gui::SimManager* sim) : _sim(sim), _parser(nullptr), _program(nullptr), _wrapper(nullptr) { - _script = std::vector(); - _sim->setScriptRunning(false); - - // Preallocate script text buffer - _scriptText.reserve(8192); - - _currentScriptPath = (paths::assets() / "DSLScripts").string(); - _currentScriptFile = "<...>"; - - _load.SetTitle("Load Command Script"); - _load.SetDirectory(_currentScriptPath); - _load.SetTypeFilters({ ".dsl", ".txt" }); - _load.SetCurrentTypeFilterIndex(1); // default to .dsl - - _save.SetTitle("Save Command Script"); - _save.SetDirectory(_currentScriptPath); - _save.SetTypeFilters({ ".dsl", ".txt" }); - _save.SetCurrentTypeFilterIndex(1); // default to .dsl - }// .dsl ([Dynamical [S]ystems [L]anguage), a domain-specific language for defining constrained dynamical systems in MGRE - - // Destructor - CommandScriptEditor::~CommandScriptEditor() { - _script.clear(); - _sim->setScriptRunning(false); - } - - static int textResizeCallback(ImGuiInputTextCallbackData* data) { - if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { - auto* str = static_cast(data->UserData); - // Resize string callback - str->resize(data->BufTextLen + 1); - data->Buf = str->data(); - // Ensure string size matches buffer length - str->resize(data->BufTextLen); - } - return 0; - } - - static std::string filenameOnly(const std::string& path) { - if (path.empty()) return "<...>"; - return std::filesystem::path(path).filename().string(); - } - - // Draw the menu items - void CommandScriptEditor::drawMenus() { - // File menu for loading/saving scripts - if (ImGui::BeginMenu("Script")) { - if (ImGui::MenuItem("Load")) { - _load.Open(); - } - if (ImGui::MenuItem("Save")) { - if (_currentScriptFile == "Engine/assets/scripts") { - ImGui::OpenPopup("Save Command Script"); - } - else { - std::string path = _currentScriptFile; - trySaveScriptToFile(path); - } - } - if (ImGui::MenuItem("Save As")) { - _pendingSavePath = _currentScriptPath; - _pendingSaveName = "unititledScript.dsl"; - _requestSaveAsPopup = true; - } - - ImGui::EndMenu(); - } - - // AFTER menu closes - if (_requestSaveAsPopup) { - ImGui::OpenPopup("Save Command Script"); - _requestSaveAsPopup = false; - } - const bool loadedThisFrame = tryLoadFromDialog(); - renderSaveAsPopup(); - - if (loadedThisFrame) { - _script.clear(); - - std::string tmp; - tmp.reserve(_scriptText.size()); - - for (char c : _scriptText) { - if (c == '\0') break; - if (c == '\n') { - if (!tmp.empty() && tmp.back() == '\r') { tmp.pop_back(); } - _script.push_back(tmp); - tmp.clear(); - } - else { tmp.push_back(c); } - } - if (!tmp.empty()) { _script.push_back(tmp); } - - selectedLines.clear(); - lastClickedLine = -1; - - LOG_INFO("Command script loaded from file: %s", _currentScriptFile.c_str()); - D_INFO("Command script loaded from file: %s", _currentScriptFile.c_str()); - } - - // Run/Stop button for the command script - - const bool wasRunning = _sim->isScriptRunning(); // snapshot - - if (wasRunning) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.80f, 0.15f, 0.15f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.90f, 0.20f, 0.20f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.70f, 0.10f, 0.10f, 1.0f)); - } - - if (ImGui::Button(_sim->isScriptRunning() ? "Stop" : "Run")) { - _sim->setScriptRunning(!_sim->isScriptRunning()); - LOG_INFO("Command script %s.", _sim->isScriptRunning() ? "started" : "stopped"); - D_INFO("Command script %s.", _sim->isScriptRunning() ? "started" : "stopped"); - - if (!_sim->isScriptRunning()) { - if (_program) _program->stop(); - _sim->setActiveProgram(nullptr); - _sim->stopSimulation(); - _sim->setScriptRunning(false); - } - else { - // If a script is already running, stop it and clean up before starting a new one - _sim->setActiveProgram(nullptr); - delete _wrapper; _wrapper = nullptr; - delete _parser; _parser = nullptr; - delete _program; _program = nullptr; - - // Create new program, parser, and wrapper instances - _program = new interpreter::StoredProgram(_sim->simCoreInterface()); - //_program->setDefaultObject(_sim->getObject()); - _parser = new interpreter::Parser(_program); - _wrapper = new interpreter::RunWrapper(_parser, _program); - - // Set the active program in the simulation manager before running - _sim->setActiveProgram(_program); - _sim->setScriptRunning(true); - _sim->setLastScriptText(_scriptText); - - // Remove trailing null character if present - std::string code = _scriptText; - if (!code.empty() && code.back() == '\0') code.pop_back(); - - _wrapper->runProgram(_scriptText); - } - } - ImGui::SameLine(); - // Background run button - if (ImGui::Button("Run (background)")) { - std::string code = _scriptText; - if (!code.empty() && code.back() == '\0') { code.pop_back(); } - std::string tag = filenameOnly(_currentScriptFile); - launchBackgroundRun(code, tag); - } - // Pop button style colors if we pushed them - if (wasRunning) { ImGui::PopStyleColor(3); } - // Check script status and handle termination conditions - if (_sim->isScriptRunning()) { - auto* prog = _sim->activeProgram(); - if (!prog) { terminateScript("Command script stopped -> active program is null.", true); return; } - - if (prog->isEmpty()) { terminateScript("Command script stopped -> program is empty.", true); return; } - if (prog->isFaulted()) { terminateScript("Command script stopped due to fault.", true); return; } - if (prog->isCompleted()) { terminateScript("Command script completed.", false); return; } - if (prog->isStopped() && !prog->isCompleted()) { terminateScript("Command script stopped.", true); return; } - } - } - - // Handler for the Run button -> launches the script in a background thread using the StudyRunner - void CommandScriptEditor::runButtonHandler() { - if (!ImGui::Button("Run (background)")) { return; } - - // Snapshot script text - std::string code = _scriptText; - if (!code.empty() && code.back() == '\0') { code.pop_back(); } - std::string tag = filenameOnly(_currentScriptFile); - - // Build single config (or multiple if desired) - std::vector configs; - StudyRunner::config c; - - // Read integration method and dt from the sim core (defaults will be used if not set in the sim core) - c.method = _sim->simCoreInterface()->integrationMethod(); - c.dt = _sim->simCoreInterface()->fixedDt(); - c.len_min = 1.0; - c.tag = tag; - configs.push_back(c); - - // Launch background thread - std::thread([this, configs, code]() { - auto results = _sim->studyRunner()->runStudies(configs, code); - _sim->pushCompletedStudies(std::move(results)); - }).detach(); - } - - void CommandScriptEditor::terminateScript(const char* reason, bool fault) { - _sim->setActiveProgram(nullptr); - _sim->stopSimulation(); - _sim->setScriptRunning(false); - - if (fault) { LOG_WARN("%s", reason); D_FAIL("%s", reason); } - else { LOG_INFO("%s", reason); D_SUCCESS("%s", reason); } - - delete _wrapper; _wrapper = nullptr; - delete _parser; _parser = nullptr; - delete _program; _program = nullptr; - } - - void CommandScriptEditor::render() { - ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 310, ImGui::GetIO().DisplaySize.y - 200), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(300, 200), ImGuiCond_FirstUseEver); - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.129f, 0.129f, 0.129f, 0.8f)); - ImGui::Begin("Command Code Editor", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoTitleBar); - - beginEditorPanel("ScriptEditorPanel"); - - if (ImGui::BeginTabBar("Debug Tabs")) { - if (ImGui::BeginTabItem("Code Editor")) { - renderEnvironment(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Syntax List")) { - renderCmdInstructions(); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); - } - - endEditorPanel(); - - ImGui::End(); - ImGui::PopStyleColor(); - } - - void CommandScriptEditor::renderEnvironment() { - const std::string fileLabel = filenameOnly(_currentScriptFile); - - // Header info - ImGui::AlignTextToFramePadding(); - ImGui::Text("Script:"); - ImGui::SameLine(); - ImGui::TextDisabled("%s", fileLabel.c_str()); - - ImGui::Separator(); - - const float statusH = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y; - ImGui::BeginChild("EditorScroll", ImVec2(0, -statusH), false, - ImGuiWindowFlags_AlwaysHorizontalScrollbar); - - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.1f, 0.1f, 0.1f, 0.925f)); - // Ensure _scriptText has at least 1 char so data() is valid for ImGui - if (_scriptText.empty()) _scriptText.push_back('\0'); - - - ImGuiInputTextFlags flags = - ImGuiInputTextFlags_AllowTabInput | - ImGuiInputTextFlags_CallbackResize; - - if (_scriptText.empty() || _scriptText.back() != '\0') { _scriptText.push_back('\0'); } - - ImGui::InputTextMultiline( - "##editor", - _scriptText.data(), - _scriptText.capacity() + 1, - ImVec2(-FLT_MIN, -FLT_MIN), - flags, - textResizeCallback, - &_scriptText - ); - - ImGui::PopStyleColor(); - ImGui::EndChild(); - - // Footer status - ImGui::Separator(); - - // Count lines - int lineCount = 0; - for (char c : _scriptText) { - if (c == '\0') break; - if (c == '\n') ++lineCount; - } - // If there's any content, lines = newlines + 1 - if (!_scriptText.empty() && _scriptText[0] != '\0') ++lineCount; - - - // Render status info - ImGui::TextDisabled("Lines: %d", lineCount); - ImGui::SameLine(); ImGui::TextDisabled(" | "); - ImGui::SameLine(); ImGui::TextDisabled("Chars: %d", (int)_scriptText.size()); - ImGui::SameLine(); ImGui::TextDisabled(" | "); - ImGui::SameLine(); ImGui::TextDisabled("State: %s", _sim->isScriptRunning() ? "Running" : "Idle"); - } - - bool CommandScriptEditor::tryLoadFromDialog() { - _load.Display(); - if (!_load.HasSelected()) { return false; } - - const std::string filePath = _load.GetSelected().string(); - _load.ClearSelected(); - - FILE* file = nullptr; - if (fopen_s(&file, filePath.c_str(), "r") != 0 || !file) { - LOG_ERROR("Failed to open script file: %s", filePath.c_str()); - return false; - } - - _scriptText.clear(); - - char lineBuf[1024]; - while (fgets(lineBuf, sizeof(lineBuf), file)) { - _scriptText += lineBuf; - } - fclose(file); - - // Ensure null-termination - if (_scriptText.empty() || _scriptText.back() != '\0') { - _scriptText.push_back('\0'); - } - - _currentScriptFile = filePath; - _currentScriptPath = filePath.substr(0, filePath.find_last_of("/\\")); - - _pendingSavePath = _currentScriptPath; - - LOG_INFO("Command script loaded from file: %s", _currentScriptFile.c_str()); - D_SUCCESS("Command script loaded from file: %s", _currentScriptFile.c_str()); - - return true; - } - - bool CommandScriptEditor::trySaveScriptToFile(const std::string& filepath) { - FILE* file = nullptr; - if (fopen_s(&file, filepath.c_str(), "w") != 0 || !file) { - LOG_ERROR("Failed to save to script file for writing: %s", filepath.c_str()); - return false; - } - - size_t n = _scriptText.size(); - if (n > 0 && _scriptText.back() == '\0') n -= 1; - fwrite(_scriptText.data(), 1, n, file); - fclose(file); - - LOG_INFO("Command script saved to file: %s", filepath.c_str()); - D_SUCCESS("Command script saved to file: %s", filepath.c_str()); - - return true; - } - - // Poll active background runs for completion and forward results to SimManager - void CommandScriptEditor::pollRuns() { - std::lock_guard lk(_activeRunsMutex); - // Iterate over active runs and check for completion - for (auto it = _activeRuns.begin(); it != _activeRuns.end(); ) { - auto& ar = *it; - if (ar.fut.valid() && ar.fut.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) { - StudyResult r; - try { r = ar.fut.get(); } - catch (const std::exception& e) { - // convert exception into a failed StudyResult - r.success = false; - r.tag = ar.tag; - } - // forward to SimManager (thread-safe push) - if (_sim) { _sim->pushCompletedStudy(std::move(r)); } - it = _activeRuns.erase(it); - } - else { - ++it; - } - } - } - - // Launch a background run of the current script (for studies) - captures code and tag by value for thread safety - void CommandScriptEditor::launchBackgroundRun(const std::string& code, const std::string& tag) { - // capture code and tag by value -> worker owns its own SimulationCore and program - std::future f = std::async(std::launch::async, [code, tag]() -> StudyResult { - StudyResult r{}; - r.tag = tag; - - // Create fresh simulation core for worker thread - core::ISimulationCore* raw = CreateSimulationCore_v1(); - if (!raw) { r.success = false; return r; } - CorePtr core(raw, [](core::ISimulationCore* p) { DestroySimulationCore(p); }); - - // bind program+parser to the new core - auto program = std::make_unique(core.get()); - interpreter::Parser parser(program.get()); - parser.parse(code); - program->start(); - - // Choose integration method (could be read from script or set as default) - integration::eIntegrationMethod method = integration::eIntegrationMethod::RK4; // NOTE: we need to infer integrator from script or choose a default (RK4 is default everywhere!) - - // Block inside worker thread until completion - bool ok = core->runScriptToCompletion(program.get(), method); - - // Fill results - r.success = ok; - r.intName = core->integrationMethodName(); - r.simTime = core->simTime(); - try { r.samples = core->telemetrySampleCount(); } - catch (...) { r.samples = 0; } - return r; - }); - - // register active run - { - std::lock_guard lk(_activeRunsMutex); - _activeRuns.push_back(ActiveRun{ std::move(f), tag }); - } - } - - void CommandScriptEditor::renderSaveAsPopup() { - // Window Padding - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16.0f, 12.0f)); - - // Save As popup - if (!ImGui::BeginPopupModal("Save Command Script", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::PopStyleVar(); return; } - - // Display current directory - ImGui::Text("Directory: "); - ImGui::SameLine(); - ImGui::TextUnformatted(_pendingSavePath.c_str()); - - ImGui::Spacing(); - - if (ImGui::Button("Choose Folder...")) { _save.Open(); } - - _save.Display(); - if (_save.HasSelected()) { - _pendingSavePath = _save.GetSelected().string(); - _save.ClearSelected(); - } - - ImGui::Spacing(); - - // Filename input - static char filenameBuf[256] = {}; - if (_pendingSaveName.empty()) { - _pendingSaveName = "script.scl"; - std::snprintf(filenameBuf, sizeof(filenameBuf), "%s", _pendingSaveName.c_str()); - } - - if (ImGui::InputText("Filename", filenameBuf, sizeof(filenameBuf))) { _pendingSaveName = filenameBuf; } - - ImGui::SectionDivider(); - - if (ImGui::Button("Save")) { - std::string fullPath = _pendingSavePath; - - if (!fullPath.empty() && fullPath.back() != '/' && fullPath.back() != '\\') { fullPath += "/"; } - - fullPath += _pendingSaveName; - - if (trySaveScriptToFile(fullPath)) { - _currentScriptFile = fullPath; - _currentScriptPath = _pendingSavePath; - LOG_INFO("Command script saved to file: %s", _currentScriptFile.c_str()); - D_SUCCESS("Command script saved to file: %s", _currentScriptFile.c_str()); - - ImGui::CloseCurrentPopup(); - } - else { LOG_ERROR("Failed to save command script to file: %s", fullPath.c_str()); } - } - - ImGui::SameLine(); - if (ImGui::Button("Cancel")) { ImGui::CloseCurrentPopup(); } - - ImGui::EndPopup(); - ImGui::PopStyleVar(); - - } - - // Helper function to render inline colored text - static void TextInlineColored(const ImVec4& color, const char* text) { - ImGui::SameLine(0.0f, 0.0f); - ImGui::TextColored(color, "%s", text); - } - - // Render the command syntax instructions - void CommandScriptEditor::renderCmdInstructions() { - ImGui::BeginChild("CmdInstructions", ImVec2(0, -30), true, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysVerticalScrollbar); - - ImGui::SectionHeader("DSL Command Format:"); - ImGui::Spacing(); - - ImGui::TextColored(CMD_COL, "command"); - TextInlineColored(ARG_COL, "(identifier, arg1, arg2, ...)"); - TextInlineColored(DESC_COL, " \'#\' Denotes a comment"); - - ImGui::Spacing(); - - ImGui::TextDisabled("Notes:"); - ImGui::BulletText("Commands and identifiers are case-insensitive (parser lowercases)."); - ImGui::BulletText("Strings can be quoted: \"...\" to allow spaces in paths."); - ImGui::BulletText("Inline comments use '#': rotate(obj, 30, 0, 90) # quarter turn"); - - ImGui::Spacing(); - ImGui::SectionHeader("DSL Command List:"); - ImGui::Spacing(); - - // --- TRAJSET --- - ImGui::TextColored(CMD_COL, "trajSet"); - TextInlineColored(ARG_COL, "(, , )"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Sets a trajectory for robotic arm joint (J_i) of some type with parameters"); - ImGui::TextDisabled("Types and parameters:"); - ImGui::TextDisabled(" • : TRAP, SINE, MSINE"); - ImGui::TextDisabled(" • TRAP, : q(°), vmax(°/s), amax(°/s²)"); - ImGui::TextDisabled(" • SINE, : duration(s), center(°), amp(°), freq(Hz) [, phase(°)]"); - ImGui::TextDisabled(" • MSINE, : duration(s), center(°) amp1(°), f1(Hz), ph1(°), amp2, f2, ph2, ..."); - ImGui::EndTooltip(); - } - - ImGui::SectionDivider(); - - // --- TRAJCLEAR --- - ImGui::TextColored(CMD_COL, "trajClear"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextDisabled("No arguments."); - TextInlineColored(DESC_COL, "# Clears any trajectory set for robotic arm joints"); - ImGui::EndTooltip(); - } - TextInlineColored(ARG_COL, "()"); - - // --- ROTATEJOINTTO --- - ImGui::TextColored(CMD_COL, "rotateJointTo"); - TextInlineColored(ARG_COL, "(, <°w^(-1)>, )"); - - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Rotates robotic arm joint (J) to some angle (°) by some max angular velocity (°w^(-1))"); - ImGui::EndTooltip(); - } - - ImGui::SectionDivider(); - - // --- ROTATEJOINTBY --- - ImGui::TextColored(CMD_COL, "rotateJointBy"); - TextInlineColored(ARG_COL, "(, <°w^(-1)>, )"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Rotates robotic arm joint (J_i) by some delta angle (°) by some angular velocity (°w^(-1))"); - ImGui::EndTooltip(); - } - - ImGui::SectionDivider(); - - // --- ROTATETO --- - ImGui::TextColored(CMD_COL, "rotateTo"); - TextInlineColored(ARG_COL, "(, <{x,y,z}>, <°ω⁻¹>, )"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Rotates a rigid body (B) to some angle (°) by some max angular velocity (°w^(-1)) across some axis ({x,y,z})"); - ImGui::EndTooltip(); - } - - ImGui::SectionDivider(); - - // --- ROTATEBY --- - ImGui::TextColored(CMD_COL, "rotateBy"); - TextInlineColored(ARG_COL, "(, <{x,y,z}>, <°w^(-1)>, )"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Rotates a rigid body (B) by some delta angle (°) at some angular velocity (°w^(-1)) across some axis ({x,y,z})"); - ImGui::EndTooltip(); - } - - ImGui::SectionDivider(); - - // --- LOAD --- - ImGui::TextColored(CMD_COL, "load"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Load some single or chain rigid body model"); - ImGui::EndTooltip(); - } - TextInlineColored(ARG_COL, "(, )"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextDisabled("Identifiers & Arguments:"); - ImGui::TextDisabled(" • \"Robot\" -> args: \"Z1\", \"UR5e\", \"Panda\", \"iiwa14\", \"VISPA\", \"H1\", \"some\\path\\to\\robot.json\""); - ImGui::TextDisabled(" • \"Obj\" -> args: \"cube\", \"circle\", \"some\\path\\to\\object\\location.fbx\""); - ImGui::EndTooltip(); - } - - ImGui::SectionDivider(); - - // --- SET --- - ImGui::TextColored(CMD_COL, "set"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Set some identifier by a value in its respective format"); - ImGui::EndTooltip(); - } - TextInlineColored(ARG_COL, "(, )"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextDisabled("Identifiers & Arguments:"); - ImGui::TextDisabled(" • \"integrator\" -> args:\n\t\t\t\t # (Explicit) \"euler\", \"midpoint\", \"heun\", \"ralston\", \"rk4\", \"rk45\"\n\t\t\t\t # (Implicit) \"implicit_euler\", \"implicit_midpoint\", \"glrk2\", \"glrk3\","); - ImGui::TextDisabled(" • \"dt\" -> args: \"numerical val\""); - ImGui::TextDisabled(" • \"omega\" -> args: \"numerical val\""); - ImGui::TextDisabled(" • \"colour\" -> args: \"{0.0-1.0, 0.0-1.0, 0.0-1.0}\", \"#ffffff\", \"red\""); - ImGui::EndTooltip(); - } - - ImGui::SectionDivider(); - - // --- START --- - ImGui::TextColored(CMD_COL, "start"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Start the simulation run"); - ImGui::TextDisabled("No arguments."); - ImGui::TextDisabled("Note: 'start' must be called to begin a simulation run after loading models and setting parameters."); - ImGui::EndTooltip(); - } - TextInlineColored(ARG_COL, "()"); - - ImGui::SectionDivider(); - - // --- STOP --- - ImGui::TextColored(CMD_COL, "stop"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Stops the simulation"); - ImGui::TextDisabled("No arguments."); - ImGui::TextDisabled("Note: \'stop\' halts the simulation run but does not reset loaded models or parameters."); - ImGui::EndTooltip(); - } - TextInlineColored(ARG_COL, "()"); - - ImGui::SectionDivider(); - - // --- WAIT --- - ImGui::TextColored(CMD_COL, "wait"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Pauses script execution for some time (s)"); - ImGui::TextDisabled("Argument: time in seconds (s)."); - ImGui::EndTooltip(); - } - TextInlineColored(ARG_COL, "()"); - - ImGui::SectionDivider(); - - // --- SELECT --- - ImGui::TextColored(CMD_COL, "select"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Selects a rigid body or joint by its identifier"); - ImGui::TextDisabled("Argument: identifier string of the object to select."); - ImGui::EndTooltip(); - } - TextInlineColored(ARG_COL, "()"); - - ImGui::Text("DSL Parallel Execution:"); - - // --- PARALLEL --- - ImGui::TextColored(CMD_COL, "parallel"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - TextInlineColored(DESC_COL, "# Begins a parallel block where commands run concurrently"); - ImGui::TextDisabled("Argument: timeout in seconds (s) for the parallel block to auto-complete."); - ImGui::EndTooltip(); - } - TextInlineColored(ARG_COL, "()"); - - ImGui::EndChild(); - } - - void CommandScriptEditor::beginEditorPanel(const char* id) { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 10.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 6.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 8.0f)); - ImGui::BeginChild(id, ImVec2(0, 0), true, ImGuiChildFlags_AlwaysUseWindowPadding); - } - - void CommandScriptEditor::endEditorPanel() { - ImGui::EndChild(); - ImGui::PopStyleVar(4); - } -} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/ui/ControlPanel.cpp b/DSFE_App/DSFE_GUI/src/ui/ControlPanel.cpp deleted file mode 100644 index 50f2b096..00000000 --- a/DSFE_App/DSFE_GUI/src/ui/ControlPanel.cpp +++ /dev/null @@ -1,1861 +0,0 @@ -// DSFE_GUI ControlPanel.cpp -#include "ui/ControlPanel.h" - -#ifdef __gl_h_ -#undef __gl_h_ -#endif -#include -#include - -#include - -#include "Scene/Mesh.h" -#include "Scene/Object.h" -#include "Scene/Light.h" -#include "Scene/Camera.h" - -#include "Scene/SimulationManager.h" -#include "Robots/RobotSystem.h" -#include "Interpreter/Parser.h" - -#include "Platform/imguiWidgets.h" -#include - -#include "Analysis/Telemetry.h" -#include "Platform/Paths.h" -#include "EngineLib/LogMacros.h" - -namespace gui { - // --- Helper Functions --- - - // Get human-readable name for gravity level - static const char* gravityLevelName(GravityLevel level) { - switch (level) { - case GravityLevel::Root: return "Presets"; - case GravityLevel::SolarSystem: return "Solar System"; - case GravityLevel::Planets: return "Planets"; - case GravityLevel::Moons: return "Moons"; - default: return ""; - } - } - - // Gravity value lookup from preset - static double gravityFromPreset(GravityPreset p) { - switch (p) { - case PRESET_ZERO_G: return constants::g_zero; - case PRESET_MICRO_G: return constants::g_micro; - - case PRESET_SUN: return constants::g_Sun; - case PRESET_MERCURY: return constants::g_Mercury; - case PRESET_VENUS: return constants::g_Venus; - case PRESET_EARTH: return constants::g_Earth; - case PRESET_MARS: return constants::g_Mars; - case PRESET_JUPITER: return constants::g_Jupiter; - case PRESET_SATURN: return constants::g_Saturn; - case PRESET_URANUS: return constants::g_Uranus; - case PRESET_NEPTUNE: return constants::g_Neptune; - case PRESET_PLUTO: return constants::g_Pluto; - - case PRESET_MOON: return constants::g_Moon; - case PRESET_TITAN: return constants::g_Titan; - case PRESET_ENCELADUS: return constants::g_Enceladus; - case PRESET_EUROPA: return constants::g_Europa; - case PRESET_GANYMEDE: return constants::g_Ganymede; - case PRESET_IO: return constants::g_Io; - - default: return constants::g_Earth; - } - } - - // Draw the gravity preset combo menu - static void drawGravityChoiceMenu(GravityPreset& preset, double& g) { - // Combo label shows navigation state - char label[64]; - snprintf(label, sizeof(label), "Gravity / %s", gravityLevelName(gravityLevel)); - - ImGui::Text("Gravity Preset"); - ImGui::SetNextItemWidth(175.0f); - - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.8f, 1.0f, 1.0f)); - - if (ImGui::BeginCombo("##GravityCombo", label)) { - - // ---------------- ROOT ---------------- - if (gravityLevel == GravityLevel::Root) { - - if (ImGui::Selectable("Zero-G")) { - preset = PRESET_ZERO_G; - g = gravityFromPreset(preset); - ImGui::CloseCurrentPopup(); - } - - if (ImGui::Selectable("Micro-G")) { - preset = PRESET_MICRO_G; - g = gravityFromPreset(preset); - ImGui::CloseCurrentPopup(); - } - - ImGui::Separator(); - - if (ImGui::Selectable("Solar System >", false, ImGuiSelectableFlags_DontClosePopups)) { - gravityLevel = GravityLevel::SolarSystem; - } - - if (ImGui::Selectable("Custom")) { - preset = PRESET_CUSTOM; - gravityMode = GravityUIMode::Custom; - ImGui::CloseCurrentPopup(); - } - } - - // ---------------- SOLAR SYSTEM ---------------- - else if (gravityLevel == GravityLevel::SolarSystem) { - - if (ImGui::Selectable("< Back", false, ImGuiSelectableFlags_DontClosePopups)) { - gravityLevel = GravityLevel::Root; - } - - if (ImGui::Selectable("Planets >", false, ImGuiSelectableFlags_DontClosePopups)) { - gravityLevel = GravityLevel::Planets; - } - - if (ImGui::Selectable("Moons >", false, ImGuiSelectableFlags_DontClosePopups)) { - gravityLevel = GravityLevel::Moons; - } - } - - // ---------------- PLANETS ---------------- - else if (gravityLevel == GravityLevel::Planets) { - - if (ImGui::Selectable("< Back")) { - gravityLevel = GravityLevel::SolarSystem; - } - - ImGui::Separator(); - - struct { const char* name; GravityPreset p; } planets[] = { - { "Sun", PRESET_SUN }, - { "Mercury", PRESET_MERCURY }, - { "Venus", PRESET_VENUS }, - { "Earth", PRESET_EARTH }, - { "Mars", PRESET_MARS }, - { "Jupiter", PRESET_JUPITER }, - { "Saturn", PRESET_SATURN }, - { "Uranus", PRESET_URANUS }, - { "Neptune", PRESET_NEPTUNE }, - { "Pluto", PRESET_PLUTO } - }; - - // List planets - for (auto& p : planets) { - if (ImGui::Selectable(p.name)) { - preset = p.p; - g = gravityFromPreset(preset); - } - } - } - - // ---------------- MOONS ---------------- - else if (gravityLevel == GravityLevel::Moons) { - if (ImGui::Selectable("< Back")) { - gravityLevel = GravityLevel::SolarSystem; - } - - ImGui::Separator(); - - struct { const char* name; GravityPreset p; } moons[] = { - { "Moon (Earth)", PRESET_MOON }, - { "Titan", PRESET_TITAN }, - { "Enceladus", PRESET_ENCELADUS }, - { "Europa", PRESET_EUROPA }, - { "Ganymede", PRESET_GANYMEDE }, - { "Io", PRESET_IO } - }; - - // List moons - for (auto& m : moons) { - if (ImGui::Selectable(m.name)) { - preset = m.p; - g = gravityFromPreset(preset); - } - } - } - ImGui::EndCombo(); - } - ImGui::PopStyleColor(); - } - - // Begin Control Panel Helper - void ControlPanel::beginControlPanel(const char* id, ImVec2 size) { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 10.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 5.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 8.0f)); - - ImGui::BeginChild(id, size, true, ImGuiChildFlags_AlwaysUseWindowPadding); - } - - // End Control Panel Helper - void ControlPanel::endControlPanel() { - ImGui::EndChild(); - ImGui::PopStyleVar(4); - } - - // --- ControlPanel Implementation --- - - ControlPanel::ControlPanel(SimManager* sim) : - _sim(sim), _controlMode(&sim->ctrlMode), _obj(nullptr), _light(nullptr), - r(render::ResolutionPreset::R_1080p), q(render::QualityPreset::Medium), - _meshLoad(ImGuiFileBrowserFlags_CloseOnEsc | ImGuiFileBrowserFlags_NoModal), - _hdrLoad(ImGuiFileBrowserFlags_CloseOnEsc | ImGuiFileBrowserFlags_NoModal), - _useAutoDiff(false) - { - diagTime = 0.0f; // initialise diagnostic time - simTime = 0.0f; // initialise simulation time - - diagRunning = false; - simulationRunning = false; - - // File browsers - _currentMeshFile = "<...>"; - _currentHDRFile = "<...>"; - // Mesh loader - _meshLoad.SetTitle("Open Object Model"); - _meshLoad.SetDirectory((paths::assets() / "objects").string()); - _meshLoad.SetTypeFilters({ ".fbx", ".obj", ".dae", ".stl"}); - // HDR loader - _hdrLoad.SetTitle("Load HDR Environment"); - _hdrLoad.SetDirectory((paths::assets() / "hdr").string()); - _hdrLoad.SetTypeFilters({ ".hdr", ".exr" }); - - // One-time ImPlot styling - static bool plotStyled = false; - if (!plotStyled) { - ImPlotStyle& style = ImPlot::GetStyle(); - ImVec4* colors = style.Colors; - - style.LineWeight = 1.1f; - style.PlotPadding = ImVec2(14, 12); - style.LabelPadding = ImVec2(6, 4); - style.LegendPadding = ImVec2(6, 4); - style.FitPadding = ImVec2(0.05f, 0.05f); - - colors[ImPlotCol_PlotBg] = ImVec4(0.129f, 0.129f, 0.129f, 1.0f); - colors[ImPlotCol_PlotBorder] = ImVec4(0.3f, 0.3f, 0.3f, 1.0f); - colors[ImPlotCol_AxisGrid] = ImVec4(0.32f, 0.32f, 0.32f, 0.7f); - colors[ImPlotCol_AxisText] = ImVec4(0.85f, 0.85f, 0.85f, 1.0f); - colors[ImPlotCol_AxisTick] = ImVec4(0.75f, 0.75f, 0.75f, 1.0f); - colors[ImPlotCol_LegendBg] = ImVec4(0.129f, 0.129f, 0.129f, 0.9f); - colors[ImPlotCol_LegendBorder] = ImVec4(0.4f, 0.4f, 0.4f, 0.6f); - - plotStyled = true; - } - } - - void ControlPanel::drawMenus(SimManager* sim) { - _sim = sim; - _obj = _sim->getObject(); - - if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("Save Layout")) { - ImGui::SaveIniSettingsToDisk((paths::configs() / "imgui.ini").string().c_str()); - ImGui::SaveIniSettingsToDisk("imgui.ini"); - } - if (ImGui::MenuItem("Load Layout")) { - ImGui::LoadIniSettingsFromDisk((paths::configs() / "imgui.ini").string().c_str()); - } - ImGui::Separator(); - if (ImGui::MenuItem("Load HDR")) { _hdrLoad.Open(); /*LOG_INFO("HDR file dialog opened");*/ } - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Edit")) { - if (ImGui::MenuItem("Reset View")) { - _sim->resetView(); - LOG_INFO("Scene view reset to default position and orientation."); - } - if (ImGui::MenuItem("Reset HDR")) { - _sim->resetHDRToPreset(); - } - ImGui::Separator(); - if (ImGui::MenuItem("Properties")) { - // Placeholder for future properties dialog - } - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Project")) { - if (ImGui::MenuItem("Load Obj")) { _meshLoad.Open(); /*LOG_INFO("File dialog opened");*/ } - if (ImGui::MenuItem("Load Robotic Arm")) { _showRobotSelector = true; /*LOG_INFO("Robotic Arm Menu Opened");*/ } - - ImGui::Separator(); - const bool canShowResults = (_sim->telemetry().ring.size() >= 2); - if (ImGui::MenuItem("View Results", nullptr, false, canShowResults)) { - _showResultsWindow = true; - _resultsFocusNeeded = true; - } - - ImGui::EndMenu(); - } - } - - void ControlPanel::render(SimManager* sceneView) { - _sim = sceneView; // update internal pointer to current SimManager - pollComparisonFutures(); // poll any pending comparison results and update state accordingly - fov = _sim->getCamera()->getFOVRadians(); - - // Update pointers to current scene data - _mesh = _sim->getMesh(); - _obj = _sim->getObject(); - _light = _sim->getLight(); - _hasRobot = _sim->hasRobot(); - - // Begin Control Panel Window - ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(300, 400), ImGuiCond_FirstUseEver); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.129f, 0.129f, 0.129f, 0.8f)); - ImGui::Begin("Control Panel", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); - const bool wasRunning = _sim->isSimRunning(); // snapshot - - // Simulation Start/Stop Button - if (_sim->isSimRunning()) { - if (wasRunning && !_sim->isSimRunning()) { _sim->setSimTime(0.0f); } // reset time if just stopped - LOG_INFO_ONCE("Simulation %s", _sim->isSimRunning() ? "started" : "stopped"); - D_RUNTIME_ONCE("Simulation %s", _sim->isSimRunning() ? "started" : "stopped"); - } - - beginControlPanel("ControlPanel"); // Begin Decorative Child Panel - - // Tab bar for organizing control sections - roboticArmSelector(); // Draw robotic arm selector (if enabled) - if (ImGui::BeginTabBar("ControlPanelTabs")) { - if (ImGui::BeginTabItem("Rigid Body Properties")) { - simulationProperties(); - objectProperties(); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Multi-Body Properties")) { - simulationProperties(); - jointProperties(); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Display Settings")) { - displaySettings(); - // Temporary light controls (for testing) - // tempLightControls(); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); - } - endControlPanel(); // End Decorative Child Panel - - ImGui::End(); - ImGui::PopStyleColor(); - - sceneObjectsTable(); - - // Detect simulation completion: was running last frame, stopped this frame - { - const bool runningNow = _sim->isSimRunning(); - if (_simWasRunningLastFrame && !runningNow) { - LOG_INFO("Simulation stopped detected (edge). Ring size: %zu", _sim->telemetry().ring.size()); - if (_sim->telemetry().ring.size() >= 2) { - _selectResultsTab = true; - _showResultsWindow = true; - _resultsFocusNeeded = true; - LOG_INFO("Auto-opening Results window."); - } - } - _simWasRunningLastFrame = runningNow; - } - drawResultsWindow(); // Draw separate results window (if open) - - // Handle file dialogs for mesh loading - _meshLoad.Display(); - if (_meshLoad.HasSelected()) { - auto file_path = _meshLoad.GetSelected().string(); - _currentMeshFile = file_path.substr(file_path.find_last_of("/\\") + 1); - meshLoadCallback(file_path); - LOG_INFO("Mesh loaded from file: %s", _currentMeshFile.c_str()); - D_SUCCESS("Mesh loaded from file: %s", _currentMeshFile.c_str()); - - _meshLoad.ClearSelected(); - } - // Handle file dialog for HDR loading - _hdrLoad.Display(); - if (_hdrLoad.HasSelected()) { - auto file_path = _hdrLoad.GetSelected().string(); - _currentHDRFile = file_path.substr(file_path.find_last_of("/\\") + 1); - _sim->loadNewHDR_UI(file_path); - LOG_INFO("HDR loaded from file: %s", _currentHDRFile.c_str()); - D_SUCCESS("HDR loaded from file: %s", _currentHDRFile.c_str()); - _hdrLoad.ClearSelected(); - } - } - - // Temporary light controls for testing and debugging - void ControlPanel::tempLightControls() { - if (!_light) return; - ImGui::SeparatorText("Light Settings:"); - ImGui::Text("Intensity"); - ImGui::SetNextItemWidth(150.0f); - ImGui::DragFloat("##intensity", &_light->_intensity, 0.1f, 0.0f, 100.0f, "%.1f"); - ImGui::Text("Color"); - ImGui::SetNextItemWidth(150.0f); - ImGui::ColorEdit3("##Colour", glm::value_ptr(_light->_colour)), ImGui::SameLine(); - ImGui::Separator(); - ImGui::Text("Direction"); - ImGui::SetNextItemWidth(150.0f); - ImGui::DragFloat3("##direction", &_light->_direction.x, 0.1f, -25.0f, 25.0f, "%.2f"); - ImGui::Separator(); - ImGui::Text("Position"); - ImGui::SetNextItemWidth(150.0f); - ImGui::DragFloat3("##position", &_light->_position.x, 0.1f, -100.0f, 100.0f, "%.1f"); - ImGui::Separator(); - } - - // Simulation properties controls, including integration method, torque mode, and delta time settings - void ControlPanel::simulationProperties() { - ImGui::SectionHeader("Simulation Settings"); - ImGui::SectionDivider(); - - robots::RobotSystem* robot = _sim->robotSystem(); - auto currentIntEnum = _sim->integrationMethod(); - auto currentIntEnum_AD = _sim->autoDiffIntegrationMethod(); - auto currentTauEnum = robot->getTorqueMode(); - - static const char* intMethodNames[] = { "Euler", "Midpoint", "Heun", "Ralston", "RK4", "RK45", "Implicit Euler", "Implicit Midpoint", "GLRK2", "GLRK3" }; - const char* currentIntMethod = intMethodNames[static_cast(currentIntEnum)]; - - static const char* intMethodNames_AD[] = { "Implicit Euler (AutoDiff)", "Implicit Midpoint (AutoDiff)", "GLRK2 (AutoDiff)", "GLRK3 (AutoDiff)" }; - const char* currentIntMethod_AD = intMethodNames_AD[static_cast(currentIntEnum_AD)]; - - static const char* torqueModeNames[] = { "None", "Passive", "Controlled" }; - const char* currentTorqueMode = torqueModeNames[static_cast(currentTauEnum)]; - - - // Disable controls while sim is running to prevent conflicts and ensure stability of the simulations - ImGui::BeginDisabled(_sim->isSimRunning()); - - // Integration method combo box - ImGui::SectionHeader("Integration Methods"); - - ImGui::SetNextItemWidth(150.0f); - ImGui::Checkbox("Enable Automatic Differentiable Integrators", &_useAutoDiff); - robot->enableAutoDiff(_useAutoDiff); - - ImGui::SetNextItemWidth(150.0f); - if (_useAutoDiff) { - if (ImGui::BeginCombo("##", currentIntMethod_AD)) { - for (int n = 0; n < IM_ARRAYSIZE(intMethodNames_AD); ++n) { - bool isSelected = (n == static_cast(currentIntEnum_AD)); - - // When a new method is selected, update the robot's integration method and log the change - if (ImGui::Selectable(intMethodNames_AD[n], isSelected)) { - auto updatedMethod_AD = static_cast(n); - auto state = robot->runtimeIntegratorState(); - state->autoDiff = true; // ensure autodiff flag is set in state - _sim->setADIntegrationMethod(updatedMethod_AD); - - switch (updatedMethod_AD) { - case integration::eAutoDiffIntegrationMethod::AD_ImplicitEuler: - D_INFO("Integrator set to Implicit Euler (AutoDiff)"); break; - case integration::eAutoDiffIntegrationMethod::AD_ImplicitMidpoint: - D_INFO("Integrator set to Implicit Midpoint (AutoDiff)"); break; - case integration::eAutoDiffIntegrationMethod::AD_GLRK2: - D_INFO("Integrator set to GLRK2 (AutoDiff Gauss-Legendre Runge-Kutta 2-stage)"); break; - case integration::eAutoDiffIntegrationMethod::AD_GLRK3: - D_INFO("Integrator set to GLRK3 (AutoDiff Gauss-Legendre Runge-Kutta 3-stage)"); break; - default: - break; - } - } - if (isSelected) { ImGui::SetItemDefaultFocus(); } - } - ImGui::EndCombo(); - } - } - else { - if (ImGui::BeginCombo("##", currentIntMethod)) { - for (int n = 0; n < IM_ARRAYSIZE(intMethodNames); ++n) { - bool isSelected = (n == static_cast(currentIntEnum)); - - // When a new method is selected, update the robot's integration method and log the change - if (ImGui::Selectable(intMethodNames[n], isSelected)) { - auto updatedMethod = static_cast(n); - const auto state = robot->runtimeIntegratorState(); - state->autoDiff = false; // ensure autodiff flag is set in state - _sim->setIntegrationMethod(updatedMethod); - - switch (updatedMethod) { - case integration::eIntegrationMethod::Euler: - D_INFO("Integrator set to Euler"); break; - case integration::eIntegrationMethod::Midpoint: - D_INFO("Integrator set to RK2 (Midpoint)"); break; - case integration::eIntegrationMethod::Heun: - D_INFO("Integrator set to RK2 (Heun)"); break; - case integration::eIntegrationMethod::Ralston: - D_INFO("Integrator set to RK2 (Ralston)"); break; - case integration::eIntegrationMethod::RK4: - D_INFO("Integrator set to RK4"); break; - case integration::eIntegrationMethod::RK45: - D_INFO("Integrator set to RK45 (Dormand-Prince)"); break; - case integration::eIntegrationMethod::ImplicitEuler: - D_INFO("Integrator set to Implicit Euler"); break; - case integration::eIntegrationMethod::ImplicitMidpoint: - D_INFO("Integrator set to Implicit Midpoint"); break; - case integration::eIntegrationMethod::GLRK2: - D_INFO("Integrator set to GLRK2 (Gauss-Legendre Runge-Kutta 2-stage)"); break; - case integration::eIntegrationMethod::GLRK3: - D_INFO("Integrator set to GLRK3 (Gauss-Legendre Runge-Kutta 3-stage)"); break; - default: - break; - } - } - if (isSelected) { ImGui::SetItemDefaultFocus(); } - } - ImGui::EndCombo(); - } - } - - ImGui::EndDisabled(); - - // Delta time controls - ImGui::SectionHeader("Delta Time (dt) Settings:"); - ImGui::Spacing(); - - ImGui::BeginDisabled(_sim->isSimRunning()); - // Simulation dt controls - ImGui::BeginGroup(); - ImGui::Text("Simulation dt:"); - - static int k = 6; - if (ImGui::DragDtFraction("##simDtDrag", k, false)) { - int x = 30 * k; - double dt = 1.0 / (double)x; - _sim->setFixedDt(dt); - } - ImGui::EndGroup(); - - // Add spacing between the two groups of controls (40px) - ImGui::SameLine(0.0f, 40.0f); - - // Telemetry dt controls - ImGui::BeginGroup(); - ImGui::Text("Telemetry dt:"); - - static int k_tel = 4; - if (ImGui::DragDtFraction("##telDtDrag", k_tel, true)) { - int x = 30 * k_tel; - _sim->setTelemetryHz(x); - } - ImGui::EndGroup(); - ImGui::EndDisabled(); - - // Deals with simulation time tracking using chrono - if (_sim->isSimRunning()) { - ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "Simulation Running..."); - ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "Elapsed Time: %.3f", _sim->simTime()); - - // make sure to stop sim when commands are finished - if (!_sim->isSimRunning()) { - ImGui::Text("Simulation Stopped."); - ImGui::Text("Elapsed Time: %.3f", _sim->simTime()); - } - } - ImGui::Separator(); - } - - // Object properties implementation - void ControlPanel::objectProperties() { - // Prevent editing properties while sim is running - if (_sim->isSimRunning()) { - ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "Cannot edit object properties while simulation is running."); - return; - } - // Ensure we have an object to edit - if (!_obj) { - ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "No object selected."); - return; - } - - // Display object name - ImGui::SectionHeader("Physics Settings:"); - ImGui::Separator(); - - // Mass, Damping, Gravity Controls - double minMass = 0.25; double maxMass = 100.0; // mass limits - float minDamping = 0.0; float maxDamping = 1.0; // damping limits - double minGravity = 0.0; double maxGravity = 10.0; // gravity limits - - // Disable controls while sim is running to prevent conflicts - ImGui::BeginDisabled(_sim->isSimRunning()); - - // Mass Control - ImGui::Text("Mass:"); - ImGui::SetNextItemWidth(150.0f); ImGui::DragScalar("kg##mass", ImGuiDataType_Double, &_obj->state.mass, 0.025f, &minMass, &maxMass); - ImGui::Spacing(); - - // Damping Control - ImGui::Text("Damping:"); - ImGui::SetNextItemWidth(150.0f); ImGui::DragScalar("kg/s##damp", ImGuiDataType_Double, &_obj->state.damping, 0.001f, &minDamping, &maxDamping); - ImGui::Spacing(); - - // Gravity Control - ImGui::Text("Gravity:"); - ImGui::SetNextItemWidth(150.0f); ImGui::DragScalar("m/s^2##g", ImGuiDataType_Double, &_obj->state.gravity, 0.00005f, &minGravity, &maxGravity); - ImGui::Spacing(); - - ImGui::Separator(); - - // Scale Controls - ImGui::Text("Scale:"); - float minScale = 0.0001f; float maxScale = 100.0f; - ImGui::SetNextItemWidth(150.0f); ImGui::DragFloat("(x)", &_obj->transform.scale.x, 0.001f, minScale, maxScale); - ImGui::SetNextItemWidth(150.0f); ImGui::DragFloat("(y)", &_obj->transform.scale.y, 0.001f, minScale, maxScale); - ImGui::SetNextItemWidth(150.0f); ImGui::DragFloat("(z)", &_obj->transform.scale.z, 0.001f, minScale, maxScale); - - ImGui::Spacing(); - - // Linear Velocity Controls - ImGui::Text("Linear Velocity:"); - double minVelocity = -100.0; double maxVelocity = 100.0; - ImGui::SetNextItemWidth(150.0f); ImGui::DragScalar("X##linVelX", ImGuiDataType_Double, &_obj->state.linearVelocity.x(), 0.0025f, &minVelocity, &maxVelocity); - ImGui::SetNextItemWidth(150.0f); ImGui::DragScalar("Y##linVelY", ImGuiDataType_Double, &_obj->state.linearVelocity.y(), 0.0025f, &minVelocity, &maxVelocity); - ImGui::SetNextItemWidth(150.0f); ImGui::DragScalar("Z##linVelZ", ImGuiDataType_Double, &_obj->state.linearVelocity.z(), 0.0025f, &minVelocity, &maxVelocity); - - ImGui::Spacing(); - - // Angular Velocity Controls - ImGui::Text("Angular Velocity:"); - double minTorque = -100.0; double maxTorque = 100.0; - ImGui::SetNextItemWidth(150.0f); ImGui::DragScalar("X##angVelX", ImGuiDataType_Double, &_obj->state.angularVelocity.x(), 0.0025f, &minTorque, &maxTorque); - ImGui::SetNextItemWidth(150.0f); ImGui::DragScalar("Y##angVelY", ImGuiDataType_Double, &_obj->state.angularVelocity.y(), 0.0025f, &minTorque, &maxTorque); - ImGui::SetNextItemWidth(150.0f); ImGui::DragScalar("Z##angVelZ", ImGuiDataType_Double, &_obj->state.angularVelocity.z(), 0.0025f, &minTorque, &maxTorque); - - ImGui::Separator(); - ImGui::EndDisabled(); - - // Reset Object Button - ImGui::Text("Reset Object:"); - if (ImGui::Button("Reset")) { - // Should not happen since button is disabled when no object - if (!_obj) { - LOG_WARN("No object selected to reset."); - return; - } - // Reset object state to initial conditions - _obj->reset(); - LOG_INFO("Object reset to initial position and orientation."); - D_INFO("Reset %s", _obj); - } - ImGui::Separator(); - } - - // Joint properties implementation - void ControlPanel::jointProperties() { - // Prevent editing properties while sim is running - if (!_hasRobot) { ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "No robot model loaded."); return; } - robots::RobotSystem* robot = _sim->robotSystem(); - - // Get references to robot's links and joints for easy access - auto& links = robot->links(); - auto& joints = robot->joints(); - - // Ensure we have joints to edit - if (joints.empty()) { - ImGui::TextDisabled("Robot has no joints."); - return; - } - // Display joint properties header - float minDamping = 0.0; float maxDamping = 1.0; // damping limits - float minFriction = 0.0; float maxFriction = 10.0; // friction limits - double minGravity = 0.0; double maxGravity = 100.0; // gravity limits - - // Clamp current joint index to valid range to prevent out-of-bounds access - static int currentJointIndex = 0; - currentJointIndex = std::clamp(currentJointIndex, 0, (int)joints.size() - 1); - - // Get references to currently selected joint and its corresponding link - auto& j = joints[currentJointIndex]; - int linkIndex = currentJointIndex; - linkIndex = std::clamp(linkIndex, 0, (int)links.size() - 1); - auto& L = links[linkIndex]; - - // Copy joint properties to local variables for editing - float c = (float)j.dynamics.damping; - float f = (float)j.dynamics.friction; - double g = (double)robot->getGravity(); - - const auto& rec = _sim->telemetry(); - ImGui::BeginDisabled(_sim->isSimRunning()); - - ImGui::Text("Joint Dynamics:"); - // Damping display (just displays current val from model) - ImGui::SetNextItemWidth(150.0f); - ImGui::TextDisabled("Damping: %.3f", c); - ImGui::SetNextItemWidth(150.0f); - ImGui::TextDisabled("Friction: %.3f", f); - ImGui::Spacing(); - - // Gravity controls with preset menu - ImGui::Text("Gravity:"); - static GravityPreset gravityPreset = PRESET_MICRO_G; // default preset - - // Draw gravity preset menu and update gravity value based on selection - drawGravityChoiceMenu(gravityPreset, g); - // If in custom gravity mode, allow direct editing of gravity value - if (gravityMode == GravityUIMode::Custom) { - ImGui::SetNextItemWidth(150.0f); - if (ImGui::DragScalar("m/s²##g", ImGuiDataType_Double, &g, 0.005f, &minGravity, &maxGravity)) { - robot->setGravity(g); - } - } - else { robot->setGravity(g); } - ImGui::TextDisabled("Gravity: %.3f", g); - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::EndDisabled(); - - // Telemetry inspector for currently selected joint - ImGui::SectionHeader("Joint Telemetry:"); - // Draw telemetry inspector for the selected joint, showing its state over time - drawTrajectoryInspector(rec, (int)robot->joints().size(), _selection.index); - ImGui::TextDisabled("Selected Joint: %s - Child Link: %s", j.name.c_str(), L.name.c_str()); - ImGui::Spacing(); - ImGui::Text("Joint (1-%d) Errors:", (int)robot->joints().size()); - drawJointErrorPlot(); - ImGui::Spacing(); - // Reset joint properties to initial conditions - ImGui::Spacing(); - ImGui::Text("Reset Robot:"); - ImGui::Spacing(); - - // Reset button to reset the entire robot to its initial state, including all joints and links - ImGui::SetNextItemWidth(150.0f); - if (ImGui::Button("Reset")) { - if (!_hasRobot) { LOG_WARN("No robot selected to reset."); return; } // should not happen - _sim->robotSystem()->resetRobot(); - - // Clear selection - _selection.type = SelectionType::NONE; - _selection.index = -1; - _selection.source = SelectionSource::NONE; - _currentJointName = ""; - _currentLinkName = ""; - - D_INFO("Reset Robot to initial position and orientation."); - return; - } - } - - // Display settings implementation - void ControlPanel::displaySettings() { - ImGui::SectionHeader("Display Settings"); - ImGui::Spacing(); - - static float fovDeg = 70.0f; - ImGui::BeginDisabled(_sim->isSimRunning()); - - ImGui::Spacing(); - ImGui::Text("Camera Field of View (FOV):"); - - ImGui::SetNextItemWidth(150.0f); - bool edited = ImGui::SliderFloat("Field of View", &fovDeg, 25.0f, 125.0f, "%.f"); - bool active = ImGui::IsItemActive(); - - if (!active && !edited) { fovDeg = _sim->getCamera()->getFOVDegrees(); } - if (edited) { _sim->getCamera()->setFOVDegrees(fovDeg); } - ImGui::EndDisabled(); - - ImGui::Spacing(); - - // Graphics Quality Presets - static int graphicsIndx = 1; - const char* qualityOptions[] = { "Low", "Medium", "High", "Ultra" }; - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, ImGui::GetStyle().ItemSpacing.y)); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); - - // Quality Buttons - bool qualityChanged = ImGui::SegmentedButtonRow("Graphics Settings:", qualityOptions, IM_ARRAYSIZE(qualityOptions), graphicsIndx, 70.0f); - - ImGui::PopStyleVar(2); - - // Apply quality changes if needed - if (qualityChanged) { - switch (graphicsIndx) { - case 0: q = render::QualityPreset::Low; break; - case 1: q = render::QualityPreset::Medium; break; - case 2: q = render::QualityPreset::High; break; - case 3: q = render::QualityPreset::Ultra; break; - default: break; - } - - auto s = render::MakeSettings(r, q); - _sim->applyRenderProfile(s, r); - - LOG_INFO("Render quality preset changed to %s", - graphicsIndx == 0 ? "Low" : - graphicsIndx == 1 ? "Medium" : - graphicsIndx == 2 ? "High" : "Ultra"); - D_INFO("Render quality preset changed to %s", - graphicsIndx == 0 ? "Low" : - graphicsIndx == 1 ? "Medium" : - graphicsIndx == 2 ? "High" : "Ultra"); - } - - ImGui::Spacing(); - - // Resolution Presets - static int resIndx = 1; - const char* resOptions[] = { "1280x720", "1920x1080", "2560x1440", "3840x2160" }; - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, ImGui::GetStyle().ItemSpacing.y)); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); - - // Resolution Buttons - bool resChanged = ImGui::SegmentedButtonRow("Resolution Presets:", resOptions, IM_ARRAYSIZE(resOptions), resIndx, 90.0f); - - ImGui::PopStyleVar(2); - - // Apply resolution changes if needed - if (resChanged) { - switch (resIndx) { - case 0: r = render::ResolutionPreset::R_720p; break; - case 1: r = render::ResolutionPreset::R_1080p; break; - case 2: r = render::ResolutionPreset::R_1440p; break; - case 3: r = render::ResolutionPreset::R_4K; break; - default: break; - } - - auto s = render::MakeSettings(r, q); - _sim->applyRenderProfile(s, r); - - LOG_INFO("Render resolution preset changed to %dx%d", - (int)(_sim->size().x * s.renderScale), - (int)(_sim->size().y * s.renderScale)); - - D_INFO("Render resolution preset changed to %dx%d", - (int)(_sim->size().x * s.renderScale), - (int)(_sim->size().y * s.renderScale)); - } - - ImGui::Spacing(); - - // Shader Mode Selector - static int shaderIndx = 2; - const char* shaderOptions[] = { "Basic Shader", "Lit Shader", "PBR Shader" }; - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, ImGui::GetStyle().ItemSpacing.y)); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); - - // Shader Buttons - bool shaderChanged = ImGui::SegmentedButtonRow("Shader Mode:", shaderOptions, IM_ARRAYSIZE(shaderOptions), shaderIndx, 110.0f); - - ImGui::PopStyleVar(2); - - // Apply shader changes if needed - if (shaderChanged) { - switch (shaderIndx) { - case 0: _sim->currentShaderMode = SimManager::ShaderMode::Basic; D_INFO("Shader -> Basic Shader"); break; - case 1: _sim->currentShaderMode = SimManager::ShaderMode::Lit; D_INFO("Shader -> Lit Shader"); break; - case 2: _sim->currentShaderMode = SimManager::ShaderMode::PBR; D_INFO("Shader -> PBR Shader"); break; - default: break; - } - } - - if (ImGui::Button("Reload Shaders")) { - LOG_INFO("Shader reload requested."); - _sim->reloadAllShaders(); - } - } - - // Robotic Arm Selector - void ControlPanel::roboticArmSelector() { - if (!_showRobotSelector) return; - - ImGui::Begin("Choose Robotic Arm", &_showRobotSelector, ImGuiWindowFlags_NoDocking); - - ImGui::SectionHeader("Select a robotic arm model:"); - ImGui::Separator(); - ImGui::Spacing(); - - roboticCardDisplay("Z1", "Unitree Robotics"); - roboticCardDisplay("UR5e", "Universal Robots"); - roboticCardDisplay("Panda", "Franka Robotics"); - roboticCardDisplay("iiwa14", "KUKA"); - roboticCardDisplay("VISPA", "Airbus"); - roboticCardDisplay("H1", "Unitree Robotics"); - - ImGui::End(); - } - - // Robotic Arm Card for Selector (lists robotic arms to choose from) - void ControlPanel::roboticCardDisplay(const char* name, const char* company) { - ImGui::PushID(name); - - ImGui::BeginChild("robot_card", ImVec2(0, 55), true, ImGuiWindowFlags_NoScrollbar); - - // Loads robot - if (ImGui::Selectable(name, false, ImGuiSelectableFlags_AllowDoubleClick)) { - LOG_INFO("Selected robot: %s", name); - _showRobotSelector = false; - - _requestedRobot = name; - _robotRequested = true; - - _sim->loadRobot(_requestedRobot); - _hasRobot = true; - } - - ImGui::TextDisabled("Company: %s", company); - - ImGui::EndChild(); - ImGui::Spacing(); - - ImGui::PopID(); - } - - // Scene Objects List - void ControlPanel::sceneObjectsTable() { - ImGui::SetNextWindowPos(ImVec2(0, 250), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(300, 200), ImGuiCond_FirstUseEver); - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.129f, 0.129f, 0.129f, 0.8f)); - ImGui::Begin("SceneObjectsTable", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); - - beginControlPanel("Rigid Body Table"); - - auto& objs = _sim->getObjects(); - int indexToDelete = -1; - - ImGui::SectionHeader("Active Rigid-Bodies"); - ImGui::SectionDivider(); - - ImGuiTableFlags tableFlags = - ImGuiTableFlags_BordersV | - ImGuiTableFlags_BordersOuterH | - ImGuiTableFlags_Resizable | - ImGuiTableFlags_RowBg | - ImGuiTableFlags_NoBordersInBody; - - if (ImGui::BeginTable("SceneTable", 3, tableFlags)) { - ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_NoHide); - ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed); - ImGui::TableSetupColumn("Child / Action"); - ImGui::TableHeadersRow(); - - float rowH = 20.0f; - - // Robot section - if (_hasRobot) { - robots::RobotSystem* robotSys = _sim->robotSystem(); - if (robotSys && robotSys->hasRobot()) { - const auto& links = robotSys->links(); - const auto& joints = robotSys->joints(); - - // Root label - std::string rootName = robotSys->robotName(); // or robotSys->robotName() - if (rootName.empty()) rootName = "Robot"; - - ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH); - ImGui::TableSetColumnIndex(0); - - ImGuiTreeNodeFlags rootFlags = - ImGuiTreeNodeFlags_SpanAllColumns | - ImGuiTreeNodeFlags_OpenOnArrow; - - bool openRoot = ImGui::TreeNodeEx(rootName.c_str(), rootFlags); - - bool rowHovered = ImGui::IsItemHovered(); - if (rowHovered) { ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(ImGuiCol_HeaderHovered)); } - - // Column 1: centered "ROOT" - ImGui::TableSetColumnIndex(1); - { - const char* txt = "ROOT"; - float columnWidth = ImGui::GetColumnWidth(); - float textWidth = ImGui::CalcTextSize(txt).x; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (columnWidth - textWidth) * 0.5f); - ImGui::TextUnformatted(txt); - } - - // Column 2: centered Remove button - ImGui::TableSetColumnIndex(2); - { - float columnWidth = ImGui::GetColumnWidth(); - float buttonWidth = 80.0f; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (columnWidth - buttonWidth) * 0.5f); - - if (ImGui::Button(("Remove##robot_" + rootName).c_str(), ImVec2(buttonWidth, 0))) { - _sim->clearRobot(); - _hasRobot = false; - } - } - - if (openRoot) { - for (int i = 0; i < (int)joints.size(); ++i) { - auto& joint = joints[i]; - scene::Object* attachedObj = nullptr; - rowH = 10.0f; - - // Find attached object for this joint's child link - /*for (auto& l : links) { if (l.name == joint.child) { attachedObj = l.attachedObject; break; } }*/ - - bool jointSelected = (_selection.type == SelectionType::JOINT && _selection.index == i); - - ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH); - ImGui::TableSetColumnIndex(0); - - ImGui::PushID(i); - - bool rowClicked = ImGui::Selectable("##joint_row", jointSelected, - ImGuiSelectableFlags_SpanAllColumns, ImVec2(0.0f, rowH) - ); - - rowHovered = ImGui::IsItemHovered(); - if (rowHovered) { ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(ImGuiCol_HeaderHovered)); } - - ImGui::SameLine(0.0f, 6.0f); - - ImGuiTreeNodeFlags leafFlags = ImGuiTreeNodeFlags_Leaf - | ImGuiTreeNodeFlags_NoTreePushOnOpen - | ImGuiTreeNodeFlags_NoAutoOpenOnLog - | ImGuiTreeNodeFlags_SpanAllColumns - | ImGuiTreeNodeFlags_NoTreePushOnOpen; - - ImGui::TreeNodeEx("##leaf", leafFlags); - - // Center joint name *within column 0* - { - float columnWidth = ImGui::GetColumnWidth(); - float textWidth = ImGui::CalcTextSize(joint.name.c_str()).x; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (columnWidth - textWidth) * 0.5f); - } - - ImGui::SameLine(); - - if (jointSelected) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.95f, 0.6f, 1.0f)); - ImGui::TextUnformatted(joint.name.c_str()); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextDisabled("Angle(rad): %.2f\nOmega(rad/s): %.2f\nDamping: %.2f\nFriction: %.2f", - joint.q, joint.qd, joint.dynamics.damping, joint.dynamics.friction); - ImGui::EndTooltip(); - } - if (jointSelected) ImGui::PopStyleColor(); - - if (rowClicked) { - _currentJointName = joint.name; - _selection.type = SelectionType::JOINT; - _selection.index = i; - _selection.source = SelectionSource::CONTROL_PANEL; - if (attachedObj) { _sim->setSelectedObject(attachedObj); } - - _sim->followRobotJoint(_currentJointName, glm::vec3(0.0f, 0.2f, 0.6f)); - } - - ImGui::PopID(); - - // Column 1: centered "Joint" - ImGui::TableSetColumnIndex(1); - { - const char* txt = "Joint"; - float columnWidth = ImGui::GetColumnWidth(); - float textWidth = ImGui::CalcTextSize(txt).x; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (columnWidth - textWidth) * 0.5f); - ImGui::TextUnformatted(txt); - } - - // Column 2: centered child link name - ImGui::TableSetColumnIndex(2); - { - float columnWidth = ImGui::GetColumnWidth(); - float textWidth = ImGui::CalcTextSize(joint.child.c_str()).x; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (columnWidth - textWidth) * 0.5f); - ImGui::TextUnformatted(joint.child.c_str()); - } - } - ImGui::TreePop(); - } - _currentObjectName = "Robot: " + rootName; - } - } - - // General objects - rowH = 20.0f; - ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH); - - // General Object Loop - for (int i = 0; i < objs.size(); i++) { - auto* obj = objs[i].get(); - rowH = 10.0f; - bool isSelected = (_selection.type == SelectionType::BODY && _selection.index == i); - - if (obj->category != scene::ObjectCategory::General) { continue; } // skip non-general objects - - ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH); - ImGui::TableSetColumnIndex(0); - std::string label = "Object " + std::to_string(i); - - if (ImGui::Selectable(label.c_str(), isSelected)) { - _currentObjectName = label; - _selection.type = SelectionType::BODY; - _selection.index = i; - _selection.source = SelectionSource::CONTROL_PANEL; - _sim->setSelectedObject(obj); // fine to keep for inspector - LOG_INFO("Selected Object: %s", label.c_str()); - } - - // Column 1: centered "OBJECT" - ImGui::TableSetColumnIndex(1); - { - const char* txt = "OBJECT"; - float columnWidth = ImGui::GetColumnWidth(); - float textWidth = ImGui::CalcTextSize(txt).x; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (columnWidth - textWidth) * 0.5f); - ImGui::TextUnformatted(txt); - } - - // Column 2: centered Delete button - ImGui::TableSetColumnIndex(2); - { - float columnWidth = ImGui::GetColumnWidth(); - float buttonWidth = 80.0f; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (columnWidth - buttonWidth) * 0.5f); - if (ImGui::Button(("Delete##" + std::to_string(i)).c_str())) { indexToDelete = i; } - } - } - - ImGui::EndTable(); - } - - if (indexToDelete != -1) { - _sim->deleteObject(indexToDelete); - LOG_INFO("Deleted object at index %d", indexToDelete); - } - - endControlPanel(); - - ImGui::End(); - ImGui::PopStyleColor(); - } - - // Select joint from table and set camera to follow it - void ControlPanel::selectJointAndFollow(int jointIdx) { - if (!_sim || !_sim->hasRobot()) return; - - robots::RobotSystem* robot = _sim->robotSystem(); - if (!robot) return; - - auto& joints = robot->joints(); - auto& links = robot->links(); - if (joints.empty()) return; - - jointIdx = std::clamp(jointIdx, 0, (int)joints.size() - 1); - - const auto& joint = joints[jointIdx]; - _currentJointName = joint.name; - - _selection.type = SelectionType::JOINT; - _selection.index = jointIdx; - _selection.source = SelectionSource::CONTROL_PANEL; - - //// find attached object for child link - //scene::Object* attachedObj = nullptr; - //for (auto& l : links) { - // if (l.name == joint.child) { attachedObj = l.attachedObject; break; } - //} - //if (attachedObj) { - // _sim->setSelectedObject(attachedObj); - //} - - // camera follow - _sim->followRobotJoint(_currentJointName, glm::vec3(0.0f, 0.2f, 0.6f)); - } - // --- Telemetry Plots --- - - // Trajectory Inspector - void ControlPanel::drawTrajectoryInspector(const diagnostics::TelemetryRecorder& rec, int /*jointCount*/, int& selectedJoint) { - const auto& ring = rec.ring; - if (ring.size() < 1) { ImGui::TextUnformatted("No trajectory telemetry yet."); return; } - - const diagnostics::TelemetrySample& s = ring.at(ring.size() - 1); - if (selectedJoint < 0) { selectedJoint = 0; } - if (selectedJoint >= (int)s.j.size()) { selectedJoint = (int)s.j.size() - 1; } - - ImGui::Text("t = %.3f s", s.timeSec); - - int currentJ = selectedJoint + 1; - - if (ImGui::SliderInt("Joint index", ¤tJ, 1, (int)s.j.size())) { - selectedJoint = currentJ - 1; - selectJointAndFollow(selectedJoint); - } - else { - selectedJoint = currentJ - 1; - } - - const diagnostics::JointTelemetry& j = s.j[selectedJoint]; - const float e = (float)(j.q_ref - j.q); - - ImGui::Separator(); - ImGui::TextDisabled("Robot loaded: %s", _requestedRobot.c_str()); - ImGui::TextDisabled("Selected Joint: %s", _currentJointName.c_str()); - - // --------------- Joint Inspector ---------------- - ImGui::Spacing(); - // Joint Info - ImGui::SectionHeader("State:"); - ImGui::Text("theta: %.6f rad", j.q); - ImGui::Text("omega: %.6f rad/s", j.qd); - ImGui::Text("damping: %.6f kg·m^2/s", j.damping); - ImGui::Text("friction: %.6f N·m", j.friction); - ImGui::Text("torque: %.6f N·m", j.torqueNm); - - ImGui::Spacing(); - // Reference Info - ImGui::SectionHeader("Reference:"); - ImGui::Text("theta_ref: %.6f rad", j.q_ref); - ImGui::Text("omega_ref: %.6f rad/s", j.qd_ref); - ImGui::Text("alpha_ref: %.6f rad/s^2", j.qdd_ref); - ImGui::Text("error e: %.6f drad", e); - - ImGui::Spacing(); - // Control Info - ImGui::SectionHeader("Control:"); - ImGui::Text("Active: %s", j.traj_active ? "Yes" : "No"); - if (j.traj_active) { - ImGui::Text("traj q: %.6f", j.traj_q); - ImGui::Text("traj qd: %.6f", j.traj_qd); - ImGui::Text("traj qdd: %.6f", j.traj_qdd); - } - - ImGui::SectionDivider(); - // Limit Info - ImGui::SectionHeader("Limits:"); - ImGui::Text("Clamp_theta: %s", j.clampTheta ? "Yes" : "No"); - ImGui::Text("Clamp_omega: %s", j.clampOmega ? "Yes" : "No"); - - // Find and show worst joint button - if (ImGui::Button("Show Worst Joint")) { - float worstErr = 0.0f; - int worstIdx = 0; - for (int i = 0; i < (int)s.j.size(); ++i) { - float err = (float)std::abs(s.j[i].q_ref - s.j[i].q); - if (err > worstErr) { worstErr = err; worstIdx = i; } - } - selectedJoint = worstIdx; - selectJointAndFollow(selectedJoint); - } - } - - void ControlPanel::drawJointErrorPlot() { - // Get the latest telemetry record - const auto& rec = _sim->telemetry(); - const auto& ring = rec.ring; - - if (ring.size() < 2) { ImGui::TextUnformatted("No telemetry data yet."); return; } - - const auto& last = ring.at(ring.size() - 1); - - static std::vector rX; - static std::vector> rY; - - const size_t sampleCount = ring.size(); - const size_t jointCount = last.j.size(); - - rX.resize(sampleCount); - - if (rY.size() != jointCount) { rY.resize(jointCount); } - for (size_t j = 0; j < jointCount; ++j) { rY[j].resize(sampleCount); } - - for (int k = 0; k < sampleCount; ++k) { - const auto& s = ring.at(k); - rX[k] = (float)s.timeSec; - - const size_t m = std::min(jointCount, s.j.size()); - for (size_t j = 0; j < m; ++j) { - rY[j][k] = (float)(s.j[j].q_ref - s.j[j].q); - } - for (size_t j = m; j < jointCount; ++j) { - rY[j][k] = 0.0f; - } - } - - // --- Joint Error Overlay --- - const ImVec2 plotSz(ImGui::GetContentRegionAvail().x, 250.0f); - if (ImPlot::BeginPlot("Joint Error Overlay##results", plotSz)) { - ImPlot::SetupAxes("t (s)", "e (rad)", ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit); - ImPlot::SetupLegend(ImPlotLocation_NorthEast); - for (size_t j = 0; j < jointCount; ++j) { - char label[16]; - snprintf(label, sizeof(label), "J%02d", (int)j); - ImPlot::PlotLine(label, rX.data(), rY[j].data(), (int)sampleCount); - } - ImPlot::EndPlot(); - } - } - - // --- CSV Export --- - - // Export telemetry data to CSV for external analysis (e.g. Python, Excel) - void ControlPanel::exportTelemetryCSV(const char* filepath) { - const auto& rec = _sim->telemetry(); - const auto& ring = rec.ring; - if (ring.size() < 2) return; - - FILE* f = nullptr; - if (fopen_s(&f, filepath, "w") != 0 || !f) { - D_FAIL("Failed to export CSV to: %s", filepath); - LOG_ERROR("Failed to export CSV to: %s", filepath); - return; - } - - const size_t sampleCount = ring.size(); - const size_t jointCount = ring.at(sampleCount - 1).j.size(); - - // Header - fprintf(f, "time_s,err_rms,err_max,clamp_sum"); - for (size_t j = 0; j < jointCount; ++j) { - fprintf(f, ",J%02d_theta,J%02d_omega,J%02d_torque,J%02d_theta_ref,J%02d_err", (int)j+1, (int)j+1, (int)j+1, (int)j+1, (int)j+1); - } - fprintf(f, "\n"); - - // Data rows - for (size_t k = 0; k < sampleCount; ++k) { - const auto& s = ring.at(k); - fprintf(f, "%.6f,%.9f,%.9f,%d", s.timeSec, s.err_rms, s.err_max, s.clamp_sum); - const size_t m = std::min(jointCount, s.j.size()); - for (size_t j = 0; j < m; ++j) { - const auto& jt = s.j[j]; - fprintf(f, ",%.9f,%.9f,%.9f,%.9f,%.9f", - jt.q, jt.qd, jt.torqueNm, jt.q_ref, - jt.q_ref - jt.q); - } - fprintf(f, "\n"); - } - - fclose(f); - D_SUCCESS("Telemetry CSV exported to: %s", filepath); - LOG_INFO("Telemetry CSV exported to: %s", filepath); - } - - // --- Integrator Comparison --- - - // Run the loaded script with all integrators and capture telemetry for comparison plots - void ControlPanel::runComparisonAllIntegratorsAsync() { - const std::string scriptText = _sim->lastScriptText(); - if (scriptText.empty()) { - D_FAIL("No script loaded. Run a script first, then compare."); - return; - } - if (!_sim->hasRobot()) { - D_FAIL("No robot loaded. Cannot run comparison."); - return; - } - - static const integration::eIntegrationMethod methods[] = { - integration::eIntegrationMethod::Euler, - integration::eIntegrationMethod::Midpoint, - integration::eIntegrationMethod::Heun, - integration::eIntegrationMethod::Ralston, - integration::eIntegrationMethod::RK4, - integration::eIntegrationMethod::RK45, - integration::eIntegrationMethod::ImplicitEuler, - integration::eIntegrationMethod::ImplicitMidpoint, - integration::eIntegrationMethod::GLRK2, - integration::eIntegrationMethod::GLRK3 - }; - static const char* names[] = { "Euler", "Midpoint", "Heun", "Ralston", "RK4", "RK45", "ImplicitEuler", "ImplicitMidpoint", "GLRK2", "GLRK3" }; - - // Clear previous state - { - std::lock_guard lk(_comparisonMutex); - _comparisonResults.clear(); - _comparisonReady = false; - _comparisonFutures.clear(); - _comparisonPending = 0; - } - - D_INFO("Starting integrator comparison (parallel, %zu methods)...", (size_t)(sizeof(methods) / sizeof(methods[0]))); - - const size_t methodCount = sizeof(methods) / sizeof(methods[0]); - - // Launch one async worker per integrator - for (size_t i = 0; i < methodCount; ++i) { - const auto method = methods[i]; - const std::string methodName = names[i]; - _comparisonPending.fetch_add(1, std::memory_order_relaxed); - - // Capture by value: scriptText and methodName - _comparisonFutures.emplace_back(std::async(std::launch::async, [scriptText, method, methodName]() -> ComparisonSnapshot { - ComparisonSnapshot snap; - snap.integratorName = methodName; - try { - // Create fresh simulation core local to this worker - core::ISimulationCore* raw = CreateSimulationCore_v1(); - if (!raw) { D_FAIL("Worker: failed to CreateSimulationCore_v1 for %s", methodName.c_str()); return snap; } - CorePtr core(raw, [](core::ISimulationCore* p) { DestroySimulationCore(p); }); - - // Program and parser bound to new core - auto program = std::make_unique(core.get()); - interpreter::Parser parser(program.get()); - parser.parse(scriptText); - program->start(); - - // Run to completion with this integrator - bool ok = core->runScriptToCompletion(program.get(), method); - if (!ok) { - D_WARN("Worker: integrator %s returned failure.", methodName.c_str()); - // still try to gather telemetry if any - } - - // Snapshot telemetry from the worker core - const auto& ring = core->telemetry().ring; - if (ring.size() < 2) { return snap; } - - const size_t sampleCount = ring.size(); - snap.time.assign(sampleCount, 0.0f); - snap.errRms.assign(sampleCount, 0.0f); - snap.errMax.assign(sampleCount, 0.0f); - - // infer joint count from last sample - size_t jointCount = ring.at(ring.size() - 1).j.size(); - snap.jointCount = (int)jointCount; - snap.jointErr.resize(jointCount); - for (size_t j = 0; j < jointCount; ++j) snap.jointErr[j].assign(sampleCount, 0.0f); - - for (size_t k = 0; k < sampleCount; ++k) { - const auto& s = ring.at(k); - snap.time[k] = (float)s.timeSec; - snap.errRms[k] = (float)s.err_rms; - snap.errMax[k] = (float)s.err_max; - const size_t m = std::min(jointCount, s.j.size()); - for (size_t j = 0; j < m; ++j) { - snap.jointErr[j][k] = (float)(s.j[j].q_ref - s.j[j].q); - } - } - - // optional: record integrator name already set - return snap; - } - catch (const std::exception& ex) { - D_FAIL("Worker exception for %s: %s", methodName.c_str(), ex.what()); - return snap; - } - catch (...) { - D_FAIL("Worker unknown exception for %s", methodName.c_str()); - return snap; - } - })); - } - // Results are polled by pollComparisonFutures() - } - - // Poll the async comparison workers for completion, gather results, and mark ready when all done - void ControlPanel::pollComparisonFutures() { - if (_comparisonFutures.empty()) return; - // Check all futures for completion, gather results, and remove completed ones from the list - for (auto it = _comparisonFutures.begin(); it != _comparisonFutures.end();) { - auto& fut = *it; - if (fut.valid() && fut.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) { - try { - auto snap = fut.get(); - // If snapshot has data, add to results for plotting - { - std::lock_guard lk(_comparisonMutex); - _comparisonResults.emplace_back(std::move(snap)); - } - } - catch (const std::exception& e) { D_FAIL("pollComparisonFutures: future.get() threw: %s", e.what()); } - catch (...) { D_FAIL("pollComparisonFutures: unknown exception from future.get()"); } - // Remove this future from the list - it = _comparisonFutures.erase(it); - _comparisonPending.fetch_sub(1, std::memory_order_relaxed); - } - else ++it; - } - - // When all workers finished, mark ready so UI plotting code can run - if (_comparisonFutures.empty()) { - // sort results into canonical integrator order so UI & CSV are deterministic - if (!_comparisonResults.empty()) { - // canonical order for display - static const std::vector canonical = { "Euler", "Midpoint", "Heun", "Ralston", "RK4", "RK45", "ImplicitEuler", "ImplicitMidpoint", "GLRK2", "GLRK3"}; - std::unordered_map order; - for (int i = 0; i < (int)canonical.size(); ++i) order[canonical[i]] = i; - // sort by canonical order if both integrators are known, otherwise keep order of completion - std::stable_sort(_comparisonResults.begin(), _comparisonResults.end(), - [&order](const ComparisonSnapshot& a, const ComparisonSnapshot& b) { - auto ia = order.find(a.integratorName); - auto ib = order.find(b.integratorName); - if (ia == order.end() && ib == order.end()) return false; - if (ia == order.end()) return false; - if (ib == order.end()) return true; - return ia->second < ib->second; - }); - // mark ready for plotting - _comparisonReady = true; - D_INFO("Integrator comparison complete: %d methods captured.", (int)_comparisonResults.size()); - } - // if no results, still mark ready to avoid indefinite "loading" state in UI - else { - _comparisonReady = true; - D_WARN("Integrator comparison complete: no usable results."); - } - } - } - - // Draw comparison plots (overlays of all integrators) - void ControlPanel::drawComparisonPlots() { - if (!_comparisonReady || _comparisonResults.empty()) { - ImGui::TextDisabled("No comparison data. Click 'Compare All Integrators' first."); - return; - } - - ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Integrator Comparison (%d methods)", (int)_comparisonResults.size()); - ImGui::Separator(); - - float totalAvail = ImGui::GetWindowHeight() - 200.0f; - float splitterThickness = 4.0f; - - float reserved = 2.0f * splitterThickness; - float usable = totalAvail - reserved; - float minH = 150.0f; - - _cPlotH1 = ImClamp(_cPlotH1, minH, usable - minH); - _cPlotH2 = ImClamp(_cPlotH2, minH, usable - _cPlotH1 - minH); - - float _cPlotH3 = usable - _cPlotH1 - _cPlotH2; - - float _plotWidth = -1; - - // --- RMS Error Overlay --- - const ImVec2 cPlotSz1(_plotWidth, _cPlotH1); - if (ImPlot::BeginPlot("RMS Error - All Integrators##cmp", cPlotSz1)) { - ImPlot::SetupAxes("t (s)", "RMS error (rad)", ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit); - ImPlot::SetupLegend(ImPlotLocation_NorthEast); - for (const auto& res : _comparisonResults) { - ImPlot::PlotLine(res.integratorName.c_str(), res.time.data(), res.errRms.data(), (int)res.time.size()); - } - ImPlot::EndPlot(); - } - - // --- Splitter A --- - ImGui::hSplitter("##cSplit1", &_plotH2, minH, usable - _cPlotH2 - minH, splitterThickness); - - // --- Max Error Overlay --- - const ImVec2 cPlotSz2(_plotWidth, _cPlotH2); - if (ImPlot::BeginPlot("Max Error - All Integrators##cmp", cPlotSz2)) { - ImPlot::SetupAxes("t (s)", "Max error (rad)", ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit); - ImPlot::SetupLegend(ImPlotLocation_NorthEast); - for (const auto& res : _comparisonResults) { - ImPlot::PlotLine(res.integratorName.c_str(), res.time.data(), res.errMax.data(), (int)res.time.size()); - } - ImPlot::EndPlot(); - } - - // --- Splitter B --- - ImGui::hSplitter("##cSplit2", &_cPlotH2, minH, usable - _cPlotH1 - minH, splitterThickness); - - // --- Per-joint error for worst joint (joint with max final error) --- - const ImVec2 cPlotSz3(_plotWidth, _cPlotH3); - if (_comparisonResults.front().jointCount > 0) { - // Find which joint has the most variation across integrators - static int selectedCmpJoint = 0; - ImGui::SetNextItemWidth(150.0f); - ImGui::SliderInt("Compare Joint##cmp", &selectedCmpJoint, 0, (int)_comparisonResults.front().jointCount - 1, "J%02d"); - - char plotLabel[64]; - snprintf(plotLabel, sizeof(plotLabel), "J%02d Error - All Integrators##cmpjoint", selectedCmpJoint + 1); - if (ImPlot::BeginPlot(plotLabel, cPlotSz3)) { - ImPlot::SetupAxes("t (s)", "error (rad)", ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit); - ImPlot::SetupLegend(ImPlotLocation_NorthEast); - for (const auto& res : _comparisonResults) { - if (selectedCmpJoint < (int)res.jointErr.size()) { - ImPlot::PlotLine(res.integratorName.c_str(), res.time.data(), res.jointErr[selectedCmpJoint].data(), (int)res.time.size()); - } - } - ImPlot::EndPlot(); - } - } - - // --- Export comparison CSV --- - if (ImGui::Button("Export Comparison CSV")) { - auto now = std::chrono::system_clock::now(); - auto epoch = std::chrono::duration_cast(now.time_since_epoch()).count(); - - std::string filename = _requestedRobot.empty() ? "comparison" : _requestedRobot; - filename += "_comparison_" + std::to_string(epoch) + ".csv"; - - auto outPath = paths::runs() / filename; - std::filesystem::create_directories(paths::runs()); - - FILE* f = nullptr; - if (fopen_s(&f, outPath.string().c_str(), "w") == 0 && f) { - // Header: time, then rms/max for each integrator - fprintf(f, "time_s"); - for (const auto& res : _comparisonResults) { - fprintf(f, ",%s_rms,%s_max", res.integratorName.c_str(), res.integratorName.c_str()); - } - fprintf(f, "\n"); - - // Use longest time series - size_t maxSamples = 0; - for (const auto& res : _comparisonResults) { maxSamples = std::max(maxSamples, res.time.size()); } - - for (size_t k = 0; k < maxSamples; ++k) { - // Use first result's time as reference - float t = (k < _comparisonResults[0].time.size()) ? _comparisonResults[0].time[k] : 0.0f; - fprintf(f, "%.6f", t); - for (const auto& res : _comparisonResults) { - if (k < (int)res.time.size()) { - fprintf(f, ",%.9f,%.9f", res.errRms[k], res.errMax[k]); - } else { - fprintf(f, ",,"); - } - } - fprintf(f, "\n"); - } - fclose(f); - D_SUCCESS("Comparison CSV exported to: %s", outPath.string().c_str()); - } - } - ImGui::SameLine(); - ImGui::TextDisabled("Saves to: LocalAppData/DSFE/runs/"); - } - - // --- Results Window (post-simulation) --- - - // When a simulation completes, this modal window pops up with the telemetry plots and export options - void ControlPanel::drawResultsWindow() { - if (!_showResultsWindow) return; - - // Get the latest telemetry record - const auto& rec = _sim->telemetry(); - const auto& ring = rec.ring; - - // If telemetry is too short, don't show the window - if (ring.size() < 2) { - _showResultsWindow = false; - return; - } - - // Force focus on first frame the window opens - if (_resultsFocusNeeded) { ImGui::SetNextWindowFocus(); } - // Get display size for dynamic window sizing - ImVec2 display = ImGui::GetIO().DisplaySize; - - // Use 75% of screen size, clamped to reasonable limits - ImVec2 desiredSize(display.x * 0.75f, display.y * 0.75f); - - // Safety clamp for very large monitors (>4K or ultrawides) - desiredSize.x = ImClamp(desiredSize.x, 700.0f, 1200.0f); - desiredSize.y = ImClamp(desiredSize.y, 500.0f, 900.0f); - - // Set the window size on first appearance - ImGui::SetNextWindowSize(desiredSize, ImGuiCond_Appearing); - - // Center the window on screen - ImVec2 center((display.x - desiredSize.x) * 0.5f, (display.y - desiredSize.y) * 0.5f); - ImGui::SetNextWindowPos(center, ImGuiCond_FirstUseEver); - - // Clear focus flag after using it - if (_resultsFocusNeeded) { _resultsFocusNeeded = false; } - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.95f)); - - bool open = true; - - // Rounded corners + accent title bar - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16, 12)); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 4)); - ImGui::PushStyleColor(ImGuiCol_TitleBg, ImVec4(0.08f, 0.08f, 0.12f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.15f, 0.18f, 0.28f, 1.0f)); - ImGui::Begin("Simulation Results", &open, ImGuiWindowFlags_NoTitleBar); - - if (!open) { - _showResultsWindow = false; - ImGui::End(); - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(3); - ImGui::PopStyleColor(); - return; - } - - // --- Header --- - const bool runningNow = _sim->isSimRunning(); - if (runningNow) { - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.4f, 1.0f), "Running..."); - } - else if (_comparisonReady && !runningNow) { - ImGui::TextColored(ImVec4(1.0f, 0.749f, 0.0f, 1.0f), "Comparison Ready"); - } - else { - ImGui::TextColored(ImVec4(0.9f, 0.6f, 0.2f, 1.0f), "Comparison Complete"); - } - - // Show robot name if available - if (!_requestedRobot.empty()) { - ImGui::SameLine(); - ImGui::TextDisabled(" Robot: %s", _requestedRobot.c_str()); - } - - const auto& last = ring.at(ring.size() - 1); // get the latest sample for summary info - - ImGui::TextDisabled("Total Time: %.3f s | Samples: %d", last.timeSec, (int)ring.size()); - ImGui::Separator(); - ImGui::Spacing(); - - // Track the window rect for glReadPixels - static ImVec2 capturePos = { 0, 0 }; - static ImVec2 captureSize = { 0, 0 }; - - if (ImGui::Button("Export as CSV")) { - auto now = std::chrono::system_clock::now(); - auto epoch = std::chrono::duration_cast(now.time_since_epoch()).count(); - - std::string filename = _requestedRobot.empty() ? "results" : _requestedRobot; - filename += "_results_" + std::to_string(epoch) + ".csv"; - - auto outPath = paths::runs() / filename; - std::filesystem::create_directories(paths::runs()); - - exportTelemetryCSV(outPath.string().c_str()); - } - - ImGui::SameLine(); - ImGui::TextDisabled("Saves to: LocalAppData/DSFE/runs/"); - - ImGui::Spacing(); - ImGui::Separator(); - - // --- Rebuild series from ring (reuse the same helpers) --- - static std::vector rX, rRms, rMax, rCs, rCst, rCso; - static std::vector> rY; - - const size_t sampleCount = ring.size(); - const size_t jointCount = last.j.size(); - - rX.resize(sampleCount); - rRms.resize(sampleCount); - rMax.resize(sampleCount); - rCs.resize(sampleCount); - rCst.resize(sampleCount); - rCso.resize(sampleCount); - - if (rY.size() != jointCount) { rY.resize(jointCount); } - for (size_t j = 0; j < jointCount; ++j) { rY[j].resize(sampleCount); } - - for (int k = 0; k < sampleCount; ++k) { - const auto& s = ring.at(k); - rX[k] = (float)s.timeSec; - rRms[k] = (float) s.err_rms; - rMax[k] = (float)s.err_max; - rCs[k] = (float)s.clamp_sum; - rCst[k] = (float)s.clamp_theta; - rCso[k] = (float)s.clamp_omega; - - const size_t m = std::min(jointCount, s.j.size()); - for (size_t j = 0; j < m; ++j) { - rY[j][k] = (float)(s.j[j].q_ref - s.j[j].q); - } - for (size_t j = m; j < jointCount; ++j) { - rY[j][k] = 0.0f; - } - } - - float totalAvail = ImGui::GetWindowHeight() - 200.0f; - float splitterThickness = 4.0f; - - float reserved = 2.0f * splitterThickness; - float usable = totalAvail - reserved; - float minH = 150.0f; - - _plotH1 = ImClamp(_plotH1, minH, usable - minH); - _plotH2 = ImClamp(_plotH2, minH, usable - _plotH1 - minH); - - float _plotH3 = usable - _plotH1 - _plotH2; - - float _plotWidth = -1; - - // --- Error Plot --- - const ImVec2 plotSz1(_plotWidth, _plotH1); - if (ImPlot::BeginPlot("Error (RMS, Max)##results", plotSz1)) { - ImPlot::SetupAxes("t (s)", "error (rad)", ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit); - ImPlot::SetupLegend(ImPlotLocation_NorthEast); - ImPlot::PlotLine("RMS", rX.data(), rRms.data(), (int)sampleCount); - ImPlot::PlotLine("Max", rX.data(), rMax.data(), (int)sampleCount); - ImPlot::EndPlot(); - } - - // --- Splitter A --- - ImGui::hSplitter("##split1", &_plotH1, minH, usable - _plotH2 - minH, splitterThickness); - - - // --- Clamp Events --- - const ImVec2 tPlotSz2(_plotWidth, _plotH2); - if (ImPlot::BeginPlot("Clamp Events##results", tPlotSz2)) { - ImPlot::SetupAxes("t (s)", "Sum", ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit); - ImPlot::SetupLegend(ImPlotLocation_NorthEast); - ImPlot::PlotStairs("Clamp Theta", rX.data(), rCst.data(), (int)sampleCount); - ImPlot::PlotStairs("Clamp Omega", rX.data(), rCso.data(), (int)sampleCount); - - ImPlot::PushStyleColor(ImPlotCol_Line, ImVec4(0.9f, 0.1f, 0.1f, 1.0f)); - ImPlot::PlotStairs("Clamp Sum", rX.data(), rCs.data(), (int)sampleCount); - ImPlot::PopStyleColor(); - - ImPlot::EndPlot(); - } - - // --- Splitter B --- - ImGui::hSplitter("##split2", &_plotH2, minH, usable - _plotH1 - minH, splitterThickness); - - // --- Joint Error Overlay --- - const ImVec2 plotSz3(_plotWidth, _plotH3); - if (ImPlot::BeginPlot("Joint Error Overlay##results", plotSz3)) { - ImPlot::SetupAxes("t (s)", "e (rad)", ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit); - ImPlot::SetupLegend(ImPlotLocation_NorthEast); - for (size_t j = 0; j < jointCount; ++j) { - char label[16]; - snprintf(label, sizeof(label), "J%02d", (int)j + 1); - ImPlot::PlotLine(label, rX.data(), rY[j].data(), (int)sampleCount); - } - ImPlot::EndPlot(); - } - - // --- Integrator Comparison Section --- - ImGui::SectionDivider(); - ImGui::SectionHeader("Integrator Comparison:", ImVec4(0.4f, 0.8f, 1.0f, 1.0f)); - - const bool hasScript = !_sim->lastScriptText().empty(); - ImGui::BeginDisabled(!hasScript || _sim->isSimRunning()); - if (ImGui::Button("Compare All Integrators")) { runComparisonAllIntegratorsAsync(); } - ImGui::EndDisabled(); - ImGui::SameLine(); - ImGui::TextDisabled("Runs All integration methods sequentially"); - if (!hasScript) { - ImGui::SameLine(); - ImGui::TextDisabled("(run a script first)"); - } - - if (_comparisonReady) { - ImGui::Spacing(); - drawComparisonPlots(); - } - - // Capture window rect for next-frame export - capturePos = ImGui::GetWindowPos(); - captureSize = ImGui::GetWindowSize(); - - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(3); - - ImGui::End(); - ImGui::PopStyleColor(); - } -} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/ui/DebugPanel.cpp b/DSFE_App/DSFE_GUI/src/ui/DebugPanel.cpp deleted file mode 100644 index 3c67d17f..00000000 --- a/DSFE_App/DSFE_GUI/src/ui/DebugPanel.cpp +++ /dev/null @@ -1,317 +0,0 @@ -// DSFE_GUI DebugPanel.cpp -#include -#include "ui/DebugPanel.h" - -#include - -#include "EngineLib/LogMacros.h" - -namespace gui { - void gui::DebugPanel::render() { - ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 310, ImGui::GetIO().DisplaySize.y - 200), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(300, 200), ImGuiCond_FirstUseEver); - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.129f, 0.129f, 0.129f, 0.8f)); - ImGui::Begin("Output", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoTitleBar); - - beginDebugPanel("Output Panel"); - - if (ImGui::BeginTabBar("Debug Tabs")) { - if (ImGui::BeginTabItem("Terminal Log")) { - renderLog(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Simulation Log")) { - renderSimLog(); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); - } - - endDebugPanel(); - - ImGui::End(); - ImGui::PopStyleColor(); - } - - void DebugPanel::renderErrorTable() { - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.925f)); - ImGui::BeginChild("ErrorTableChild", ImVec2(0, 0), true, ImGuiWindowFlags_AlwaysVerticalScrollbar); - - ImGuiTableFlags tableFlags = - ImGuiTableFlags_BordersV | - ImGuiTableFlags_BordersOuterH | - ImGuiTableFlags_Resizable | - ImGuiTableFlags_RowBg | - ImGuiTableFlags_NoBordersInBody; - - if (ImGui::BeginTable("ErrorTable", 3, tableFlags)) - { - ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed); - ImGui::TableSetupColumn(" ", ImGuiTableColumnFlags_WidthFixed); - ImGui::TableSetupColumn("Description"); - ImGui::TableHeadersRow(); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - - ImGuiTreeNodeFlags rootFlags = - ImGuiTreeNodeFlags_SpanAllColumns | - ImGuiTreeNodeFlags_DefaultOpen; - - bool openError = ImGui::TreeNodeEx("Errors:", rootFlags); - bool openWarn = ImGui::TreeNodeEx("Warnings:", rootFlags); - - ImGui::TableSetColumnIndex(1); - ImGui::TextUnformatted(""); - - if (openError) { - for (const auto& e : _entries) { - ImGui::TableNextRow(); - - // Column 0 -> level type - ImGui::TableSetColumnIndex(0); - ImGuiTreeNodeFlags leafFlags = - ImGuiTreeNodeFlags_Leaf | - ImGuiTreeNodeFlags_NoTreePushOnOpen | - ImGuiTreeNodeFlags_OpenOnArrow; - - ImGui::Text(" ", leafFlags); - - // Column 1 -> Description - ImGui::TableSetColumnIndex(1); - if (_entries.back().isError) - ImGui::TextColored({ 1,0,0,1 }, e.level.c_str()); - - ImGui::TableSetColumnIndex(2); - if (_entries.back().isError) - ImGui::TextUnformatted(e.desc.c_str()); - - ImGui::TableNextRow(); - } - ImGui::TreePop(); - } - - ImGui::TableNextRow(); - - if (openWarn) { - for (const auto& e : _entries) { - ImGui::TableNextRow(); - - // Column 0 -> level type - ImGui::TableSetColumnIndex(0); - ImGuiTreeNodeFlags leafFlags = - ImGuiTreeNodeFlags_Leaf | - ImGuiTreeNodeFlags_NoTreePushOnOpen | - ImGuiTreeNodeFlags_OpenOnArrow; - - ImGui::TreeNodeEx(" ", leafFlags); - - // Column 1 -> Description - ImGui::TableSetColumnIndex(1); - if (!_entries.back().isError) - ImGui::TextColored({ 1,1,0,1 }, e.level.c_str()); - - ImGui::TableSetColumnIndex(2); - if (!_entries.back().isError) - ImGui::TextUnformatted(e.desc.c_str()); - } - ImGui::TreePop(); - } - - ImGui::EndTable(); - } - - ImGui::EndChild(); - ImGui::PopStyleColor(); - } - - void DebugPanel::renderLog() { - ImGuiWindowFlags window_flags = ImGuiWindowFlags_HorizontalScrollbar - | ImGuiWindowFlags_AlwaysVerticalScrollbar; - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.925f)); - ImGui::BeginChild("LogChild", ImVec2(0, -30), true, window_flags); - - const auto& entries = gLog.Instance().Entries(); - - for (int i = 0; i < (int)entries.size(); ++i) { - const auto& e = entries[i]; - - const char* levelStr = ""; - ImVec4 levelColour{ 1, 1, 1, 1 }; // Colour associated log levelling! (I think its useful) - - // Determine log level string and colour - switch (e.level) { - // trace level for detailed debugging information - case LogLevel::Trace: levelStr = "TRACE"; levelColour = traceCol; break; - // debug level for general debugging information - case LogLevel::Debug: levelStr = "DEBUG"; levelColour = debugCol; break; - // info level for informational messages - case LogLevel::Info: levelStr = "INFO"; levelColour = infoCol; break; - // warning level for potential issues - case LogLevel::Warning: levelStr = "WARN"; levelColour = warnCol; break; - // error level for error messages - case LogLevel::Error: levelStr = "ERROR"; levelColour = errorCol; break; - // success level for successful operations - case LogLevel::Success: levelStr = "SUCCESS"; levelColour = okCol; break; - // fail level for failed operations - case LogLevel::Fail: levelStr = "FAIL"; levelColour = failCol; break; - // Runtime level for runtime specific messages - case LogLevel::Runtime: levelStr = "RUNTIME"; levelColour = runtimeCol; break; - // Output level for general output messages - case LogLevel::Output: levelStr = "OUTPUT"; levelColour = outputCol; break; - // Default level for general output messages - default: levelStr = "OUTPUT"; levelColour = outputCol; break; - } - - bool selected = selectedLines.count(i) > 0; - - ImGui::PushID(i); - if (ImGui::Selectable("##logline", selected, ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_SpanAllColumns)) { - if (ImGui::GetIO().KeyShift && lastClickedLine != -1) { - int a = std::min(lastClickedLine, i); - int b = std::max(lastClickedLine, i); - selectedLines.clear(); - for (int j = a; j <= b; ++j) - selectedLines.insert(j); - } - else if (ImGui::GetIO().KeyCtrl) { - if (selected) selectedLines.erase(i); - else selectedLines.insert(i); - lastClickedLine = i; - } - else { - selectedLines.clear(); - selectedLines.insert(i); - lastClickedLine = i; - } - } - - // Render coloured text on top of selectable - ImGui::SameLine(); - ImGui::TextUnformatted("["); - ImGui::SameLine(0, 0); - ImGui::TextColored(levelColour, "%s", levelStr); - ImGui::SameLine(0, 0); - ImGui::TextUnformatted("]: "); - ImGui::SameLine(0, 0); - ImGui::TextWrapped("%s", e.message.c_str()); - - ImGui::PopID(); - } - - - ImGui::EndChild(); - ImGui::PopStyleColor(); - - if (ImGui::IsWindowFocused() && ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C)) { - std::string clip; - for (int idx : selectedLines) { - const auto& e = entries[idx]; - clip += e.message; - clip += "\n"; - } - ImGui::SetClipboardText(clip.c_str()); - } - } - - void DebugPanel::renderSimLog() { - ImGuiWindowFlags window_flags = ImGuiWindowFlags_HorizontalScrollbar - | ImGuiWindowFlags_AlwaysVerticalScrollbar; - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.925f)); - ImGui::BeginChild("simLogChild", ImVec2(0, -30), true, window_flags); - - const auto& entries = gLog.Instance().SimEntries(); - - for (int i = 0; i < (int)entries.size(); ++i) { - const auto& e = entries[i]; - - const char* levelStr = ""; - ImVec4 levelColour{ 1, 1, 1, 1 }; // Colour associated log levelling! (I think its useful) - - // Determine log level string and colour - switch (e.level) { - // debug level for general debugging information - case simLogLevel::Rotate: levelStr = "ROTATE"; levelColour = rotateCol; break; - // translate level for general debugging information - case simLogLevel::Translate: levelStr = "TRANSLATE"; levelColour = translateCol; break; - // error level for error messages - case simLogLevel::Error: levelStr = "ERROR"; levelColour = errorCol; break; - // fail level for failed operations - case simLogLevel::Fail: levelStr = "FAIL"; levelColour = failCol; break; - // success level for successful operations - case simLogLevel::Success: levelStr = "SUCCESS"; levelColour = okCol; break; - // Runtime level for runtime specific messages - case simLogLevel::Runtime: - default: levelStr = "RUNTIME"; levelColour = runtimeCol; break; - } - - bool selected = simSelectedLines.count(i) > 0; - - ImGui::PushID(i); - if (ImGui::Selectable("##logline", selected, ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_SpanAllColumns)) { - if (ImGui::GetIO().KeyShift && lastClickedLine != -1) { - int a = std::min(lastClickedLine, i); - int b = std::max(lastClickedLine, i); - simSelectedLines.clear(); - for (int j = a; j <= b; ++j) - simSelectedLines.insert(j); - } - else if (ImGui::GetIO().KeyCtrl) { - if (selected) simSelectedLines.erase(i); - else simSelectedLines.insert(i); - lastClickedLine = i; - } - else { - simSelectedLines.clear(); - simSelectedLines.insert(i); - lastClickedLine = i; - } - } - - // Render coloured text on top of selectable - ImGui::SameLine(); - ImGui::TextUnformatted("["); - ImGui::SameLine(0, 0); - ImGui::TextColored(levelColour, "%s", levelStr); - ImGui::SameLine(0, 0); - ImGui::TextUnformatted("]: "); - ImGui::SameLine(0, 0); - ImGui::TextWrapped("%s", e.message.c_str()); - - ImGui::PopID(); - } - - ImGui::EndChild(); - ImGui::PopStyleColor(); - - if (ImGui::IsWindowFocused() && ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C)) { - std::string clip; - for (int idx : simSelectedLines) { - const auto& e = entries[idx]; - clip += e.message; - clip += "\n"; - } - ImGui::SetClipboardText(clip.c_str()); - } - - // button to clear simulation log - if (ImGui::Button("Clear Simulation Log")) { clearSimLog(); } - } - - void DebugPanel::beginDebugPanel(const char* id, ImVec2 size) { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 10.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 5.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 6.0f)); - ImGui::BeginChild(id, size, true, ImGuiChildFlags_AlwaysUseWindowPadding); - } - - void DebugPanel::endDebugPanel() { - ImGui::EndChild(); - ImGui::PopStyleVar(4); - } -} From 5ac799e3bf91e9de66558b19941c78a9cd1c2696 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Mon, 8 Jun 2026 15:51:20 +0100 Subject: [PATCH 034/110] feat: Added the console and simulation logging window in `ConsoleOutputWidget` --- DSFE_App/DSFE_GUI/CMakeLists.txt | 5 +- .../MainWindow/Widgets/ConsoleOutputWidget.h | 45 ++++++ .../Widgets/ConsoleOutputWidget.cpp | 141 ++++++++++++++++++ 3 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ConsoleOutputWidget.h create mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index ba958df6..dcd44548 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -70,12 +70,13 @@ set(MANAGER_SRC src/Scene/Manager/SimViewport.cpp ) -set(UI_SRC +set(WIDGETS_SRC src/ui/RenderPreset.cpp src/MainWindow/Widgets/ViewportWidget.cpp src/MainWindow/Widgets/RobotSelectorWidget.cpp src/MainWindow/Widgets/ControlPanelWidget.cpp src/MainWindow/Widgets/DSLEditorWidget.cpp + src/MainWindow/Widgets/ConsoleOutputWidget.cpp src/MainWindow/Widgets/FractionSelectorWidget.cpp include/MainWindow/Widgets/FractionSelectorWidget.h ) @@ -100,7 +101,7 @@ target_sources(DSFE_GUI PRIVATE ${RENDER_SRC} ${SCENE_SRC} ${MANAGER_SRC} - ${UI_SRC} + ${WIDGETS_SRC} ${ROBOT_SRC} ${PLATFORM_SRC} ${ASSETS_SRC} diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ConsoleOutputWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ConsoleOutputWidget.h new file mode 100644 index 00000000..39938b00 --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ConsoleOutputWidget.h @@ -0,0 +1,45 @@ +// DSFE_GUI ConsoleOutputWidget.h +#pragma once + +#include +#include + +#include "Platform/Logger.h" + +class QTextEdit; +class QTabWidget; +class QPushButton; + +// Colours for log levels (Will eventually move to a separate file for global UI constants) +#define TRACE_COLOUR QColor(160, 160, 160) +#define DEBUG_COLOUR QColor(80, 170, 240) +#define INFO_COLOUR QColor(80, 160, 255) +#define WARN_COLOUR QColor(255, 180, 0) +#define ERROR_COLOUR QColor(220, 60, 70) +#define OK_COLOUR QColor(0, 190, 100) +#define FAIL_COLOUR QColor(170, 0, 50) +#define RUNTIME_COLOUR QColor(190, 120, 225) +#define OUTPUT_COLOUR QColor(220, 220, 220) + +#define ROTATE_COLOUR QColor(120, 180, 255) +#define TRANSLATE_COLOUR QColor(120, 255, 180) + +namespace widgets { + class ConsoleOutputWidget : public QWidget { + public: + explicit ConsoleOutputWidget(QWidget* parent = nullptr); + void clearSimLog(); + void updateLog(); + + private: + QTabWidget* _tabs = nullptr; + QTextEdit* _terminalLog = nullptr; + QTextEdit* _simLog = nullptr; + QPushButton* _clearTerminalButton = nullptr; + + size_t _lastTerminalCount = 0; + size_t _lastSimCount = 0; + + bool autoScroll = true; + }; +} // namespace widgets \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp new file mode 100644 index 00000000..e0faa1e1 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp @@ -0,0 +1,141 @@ +// DSFE_GUI ConsoleOutputWidget.cpp +#include "Widgets/ConsoleOutputWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "EngineLib/LogMacros.h" + +namespace widgets { + QColor levelColour(LogLevel level) { + switch (level) { + case LogLevel::Trace: return TRACE_COLOUR; + case LogLevel::Debug: return DEBUG_COLOUR; + case LogLevel::Info: return INFO_COLOUR; + case LogLevel::Warning: return WARN_COLOUR; + case LogLevel::Error: return ERROR_COLOUR; + case LogLevel::Success: return OK_COLOUR; + case LogLevel::Fail: return FAIL_COLOUR; + case LogLevel::Runtime: return RUNTIME_COLOUR; + case LogLevel::Output: + default: return OUTPUT_COLOUR; + } + } + QString levelString(LogLevel level) { + switch (level) { + case LogLevel::Trace: return "TRACE"; + case LogLevel::Debug: return "DEBUG"; + case LogLevel::Info: return "INFO"; + case LogLevel::Warning: return "WARN"; + case LogLevel::Error: return "ERROR"; + case LogLevel::Success: return "OK"; + case LogLevel::Fail: return "FAIL"; + case LogLevel::Runtime: return "RUNTIME"; + case LogLevel::Output: + default: return "OUTPUT"; + } + } + QColor simColour(simLogLevel level) { + switch (level) { + case simLogLevel::Rotate: return ROTATE_COLOUR; + case simLogLevel::Translate: return TRANSLATE_COLOUR; + case simLogLevel::Fail: return FAIL_COLOUR; + case simLogLevel::Error: return ERROR_COLOUR; + case simLogLevel::Success: return OK_COLOUR; + case simLogLevel::Runtime: + default: return RUNTIME_COLOUR; + } + } + + QString simString(simLogLevel level) { + switch (level) { + case simLogLevel::Rotate: return "ROTATE"; + case simLogLevel::Translate: return "TRANSLATE"; + case simLogLevel::Fail: return "FAIL"; + case simLogLevel::Error: return "ERROR"; + case simLogLevel::Success: return "SUCCESS"; + case simLogLevel::Runtime: + default: return "RUNTIME"; + } + } + + ConsoleOutputWidget::ConsoleOutputWidget(QWidget* parent) : QWidget(parent) { + auto* rootLayout = new QVBoxLayout(this); + + _tabs = new QTabWidget(this); + _terminalLog = new QTextEdit(this); + _terminalLog->setReadOnly(true); + _simLog = new QTextEdit(this); + _simLog->setReadOnly(true); + _tabs->addTab(_terminalLog, "Terminal"); + _tabs->addTab(_simLog, "Simulation"); + rootLayout->addWidget(_tabs); + + auto* buttonLayout = new QHBoxLayout(); + _clearTerminalButton = new QPushButton("Clear Terminal Log", this); + buttonLayout->addWidget(_clearTerminalButton); + rootLayout->addLayout(buttonLayout); + + connect(_clearTerminalButton, &QPushButton::clicked, this, [this]() { + gLog.Instance().clear(); + _terminalLog->clear(); + _lastTerminalCount = 0; + }); + + auto* timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, &ConsoleOutputWidget::updateLog); + timer->start(100); // Update every 100 ms + } + + void ConsoleOutputWidget::clearSimLog() { + gLog.Instance().clearSimLog(); + _simLog->clear(); + _lastSimCount = 0; + } + + void ConsoleOutputWidget::updateLog() { + const auto& entries = gLog.Instance().Entries(); + while (_lastTerminalCount < entries.size()) { + const auto& e = entries[_lastTerminalCount]; + QTextCursor cursor = _terminalLog->textCursor(); + cursor.movePosition(QTextCursor::End); + QTextCharFormat lvlFmt; + lvlFmt.setForeground(levelColour(e.level)); + cursor.insertText(QString("[%1] ").arg(levelString(e.level)), lvlFmt); + QTextCharFormat msgFmt; + msgFmt.setForeground(QColor(220, 220, 220)); + cursor.insertText(QString::fromStdString(e.message), msgFmt); + cursor.insertBlock(); + ++_lastTerminalCount; + } + + const auto& simEntries = gLog.Instance().SimEntries(); + while (_lastSimCount < simEntries.size()) { + const auto& e = simEntries[_lastSimCount]; + QTextCursor cursor = _simLog->textCursor(); + cursor.movePosition(QTextCursor::End); + QTextCharFormat lvlFmt; + lvlFmt.setForeground(simColour(e.level)); + cursor.insertText(QString("[%1] ").arg(simString(e.level)), lvlFmt); + QTextCharFormat msgFmt; + msgFmt.setForeground(QColor(220, 220, 220)); + cursor.insertText(QString::fromStdString(e.message), msgFmt); + cursor.insertBlock(); + ++_lastSimCount; + } + + if (autoScroll) { + auto* tBar = _terminalLog->verticalScrollBar(); + tBar->setValue(tBar->maximum()); + auto* sBar = _simLog->verticalScrollBar(); + sBar->setValue(sBar->maximum()); + } + } +} // namespace widgets \ No newline at end of file From 87d89d65ea98c5c8455f36188e08b84cec680436 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Mon, 8 Jun 2026 15:51:34 +0100 Subject: [PATCH 035/110] feat: Linked up automatic sim log clearning with script execution --- .../include/MainWindow/Widgets/DSLEditorWidget.h | 6 +++++- .../include/MainWindow/Workspace/ProjectPage.h | 6 +++++- .../src/MainWindow/Widgets/DSLEditorWidget.cpp | 7 +++++-- .../DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp | 10 ++++++---- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h index 1ef4fd14..6af42e4b 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h @@ -33,9 +33,11 @@ namespace runs { } namespace widgets { + class ConsoleOutputWidget; + class DSLEditorWidget : public QWidget { public: - explicit DSLEditorWidget(gui::SimManager* sim, QWidget* parent = nullptr); + explicit DSLEditorWidget(gui::SimManager* sim, ConsoleOutputWidget* _log, QWidget* parent = nullptr); bool loadScript(const QString& fileName); bool saveScript(const QString& fileName); @@ -50,6 +52,8 @@ namespace widgets { interpreter::IStoredProgram* _program = nullptr; interpreter::RunWrapper* _wrapper = nullptr; + ConsoleOutputWidget* _log = nullptr; + void runScript(); void stopScript(); diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h b/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h index bc338d3a..07e10138 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Workspace/ProjectPage.h @@ -4,7 +4,10 @@ #include namespace gui { class SimManager; } -namespace widgets { class DSLEditorWidget; } +namespace widgets { + class ConsoleOutputWidget; + class DSLEditorWidget; +} namespace Workspace { class ProjectPage : public QWidget { @@ -13,6 +16,7 @@ namespace Workspace { widgets::DSLEditorWidget* editor() const; private: + widgets::ConsoleOutputWidget* _log = nullptr; widgets::DSLEditorWidget* _editor = nullptr; }; } // namespace Workspace \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp index 77233689..e7d9b153 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp @@ -16,11 +16,13 @@ #include "Numerics/IntegrationMethods.h" #include "Interpreter/StoredProgram.h" +#include "Widgets/ConsoleOutputWidget.h" + #include "EngineLib/LogMacros.h" namespace widgets { - DSLEditorWidget::DSLEditorWidget(gui::SimManager* sim, QWidget* parent) - : QWidget(parent), _sim(sim), _scriptWorkingDir((paths::assets() / "DSLScripts").string()) + DSLEditorWidget::DSLEditorWidget(gui::SimManager* sim, ConsoleOutputWidget* log, QWidget* parent) + : QWidget(parent), _sim(sim), _log(log), _scriptWorkingDir((paths::assets() / "DSLScripts").string()) { auto* rootLayout = new QVBoxLayout(this); auto* scriptStateLayout = new QHBoxLayout(); @@ -159,6 +161,7 @@ namespace widgets { _scriptText = _scriptEditor->toPlainText().toStdString(); LOG_INFO("Script size = %zu", _scriptText.size()); + if (_log) { _log->clearSimLog(); } _sim->setActiveProgram(_program); _sim->setScriptRunning(true); LOG_INFO("DSL script started."); D_INFO("DSL script started."); diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp index 70f7d168..341093c6 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp @@ -1,9 +1,11 @@ // DSFE_GUI ProjectPage.cpp #include "Workspace/ProjectPage.h" #include "Scene/SimulationManager.h" + +#include "Widgets/DSLEditorWidget.h" #include "Widgets/ViewportWidget.h" #include "Widgets/ControlPanelWidget.h" -#include "Widgets/DSLEditorWidget.h" +#include "Widgets/ConsoleOutputWidget.h" #include #include @@ -16,16 +18,16 @@ namespace Workspace { auto* rootSplitter = new QSplitter(Qt::Horizontal, this); auto* centreSplitter = new QSplitter(Qt::Vertical); auto* rightSplitter = new QSplitter(Qt::Vertical); - _editor = new widgets::DSLEditorWidget(sim, this); + _log = new widgets::ConsoleOutputWidget(this); + _editor = new widgets::DSLEditorWidget(sim, _log, this); rootSplitter->addWidget(_editor); centreSplitter->addWidget(new widgets::ViewportWidget(sim, this)); - centreSplitter->addWidget(new QLabel("Console Output (TODO)", this)); + centreSplitter->addWidget(_log); rightSplitter->addWidget(new widgets::ControlPanelWidget(sim, this)); rightSplitter->addWidget(new QLabel("Scene Object? (TODO)", this)); rootSplitter->addWidget(centreSplitter); rootSplitter->addWidget(rightSplitter); layout->addWidget(rootSplitter); - // Sizing reused from the old imgui layout rootSplitter->setSizes({ 635, 1016, 393 }); centreSplitter->setSizes({ 733, 396 }); rightSplitter->setSizes({ 733, 396 }); From 99a551a37b6e449d8894ad77cf721ff5ed827c63 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Mon, 8 Jun 2026 15:59:43 +0100 Subject: [PATCH 036/110] fixes: Removed scene objects * May revamp this at another time, but not needed for now. --- DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp index 341093c6..180eb944 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Workspace/ProjectPage.cpp @@ -24,7 +24,6 @@ namespace Workspace { centreSplitter->addWidget(new widgets::ViewportWidget(sim, this)); centreSplitter->addWidget(_log); rightSplitter->addWidget(new widgets::ControlPanelWidget(sim, this)); - rightSplitter->addWidget(new QLabel("Scene Object? (TODO)", this)); rootSplitter->addWidget(centreSplitter); rootSplitter->addWidget(rightSplitter); layout->addWidget(rootSplitter); From 03b6979f98057cb9cd0958817475a3e63f7a6816 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Mon, 8 Jun 2026 16:04:35 +0100 Subject: [PATCH 037/110] feat: Added hard cap of terminal enteries --- .../MainWindow/Widgets/ConsoleOutputWidget.h | 26 +++++++++---------- .../Widgets/ConsoleOutputWidget.cpp | 20 +++++++------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ConsoleOutputWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ConsoleOutputWidget.h index 39938b00..5eec1936 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ConsoleOutputWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ConsoleOutputWidget.h @@ -11,18 +11,19 @@ class QTabWidget; class QPushButton; // Colours for log levels (Will eventually move to a separate file for global UI constants) -#define TRACE_COLOUR QColor(160, 160, 160) -#define DEBUG_COLOUR QColor(80, 170, 240) -#define INFO_COLOUR QColor(80, 160, 255) -#define WARN_COLOUR QColor(255, 180, 0) -#define ERROR_COLOUR QColor(220, 60, 70) -#define OK_COLOUR QColor(0, 190, 100) -#define FAIL_COLOUR QColor(170, 0, 50) -#define RUNTIME_COLOUR QColor(190, 120, 225) -#define OUTPUT_COLOUR QColor(220, 220, 220) - -#define ROTATE_COLOUR QColor(120, 180, 255) -#define TRANSLATE_COLOUR QColor(120, 255, 180) +constexpr QColor TRACE_COLOUR(160, 160, 160); +constexpr QColor DEBUG_COLOUR(80, 170, 240); +constexpr QColor INFO_COLOUR(80, 160, 255); +constexpr QColor WARN_COLOUR(255, 180, 0); +constexpr QColor ERROR_COLOUR(220, 60, 70); +constexpr QColor OK_COLOUR(0, 190, 100); +constexpr QColor FAIL_COLOUR(170, 0, 50); +constexpr QColor RUNTIME_COLOUR(190, 120, 225); +constexpr QColor OUTPUT_COLOUR(220, 220, 220); +constexpr QColor ROTATE_COLOUR(120, 180, 255); +constexpr QColor TRANSLATE_COLOUR(120, 255, 180); + +constexpr size_t MAX_TERMINAL_ENTRIES = 10000; // Maximum number of terminal entries to keep in memory for display namespace widgets { class ConsoleOutputWidget : public QWidget { @@ -35,7 +36,6 @@ namespace widgets { QTabWidget* _tabs = nullptr; QTextEdit* _terminalLog = nullptr; QTextEdit* _simLog = nullptr; - QPushButton* _clearTerminalButton = nullptr; size_t _lastTerminalCount = 0; size_t _lastSimCount = 0; diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp index e0faa1e1..a1992f4f 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp @@ -78,17 +78,6 @@ namespace widgets { _tabs->addTab(_simLog, "Simulation"); rootLayout->addWidget(_tabs); - auto* buttonLayout = new QHBoxLayout(); - _clearTerminalButton = new QPushButton("Clear Terminal Log", this); - buttonLayout->addWidget(_clearTerminalButton); - rootLayout->addLayout(buttonLayout); - - connect(_clearTerminalButton, &QPushButton::clicked, this, [this]() { - gLog.Instance().clear(); - _terminalLog->clear(); - _lastTerminalCount = 0; - }); - auto* timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &ConsoleOutputWidget::updateLog); timer->start(100); // Update every 100 ms @@ -131,6 +120,15 @@ namespace widgets { ++_lastSimCount; } + while (_terminalLog->document()->blockCount() > MAX_TERMINAL_ENTRIES) { + QTextCursor cursor = _terminalLog->textCursor(); + cursor.movePosition(QTextCursor::Start); + cursor.select(QTextCursor::BlockUnderCursor); + cursor.removeSelectedText(); + cursor.deleteChar(); // Remove the block itself + --_lastTerminalCount; // Adjust count since we've removed an entry + } + if (autoScroll) { auto* tBar = _terminalLog->verticalScrollBar(); tBar->setValue(tBar->maximum()); From 93a8e0044df4aaae3b0e96e4ba29f3c67be0dc29 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Mon, 8 Jun 2026 16:21:52 +0100 Subject: [PATCH 038/110] fixes: Corrected taskbar duplciation error --- .../src/MainWindow/DSFE_MainWindow.cpp | 50 ++++++++----------- .../Widgets/ConsoleOutputWidget.cpp | 7 ++- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index 2c978d9c..e9aa08f4 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -50,37 +50,31 @@ namespace window { }); }); auto* openMenu = fileMenu->addMenu("Open"); - connect(openMenu, &QMenu::aboutToShow, this, [this, openMenu]() { - LOG_INFO("Menu clicked: File -> Open"); - auto* openProjectAction = openMenu->addAction("Project"); - connect(openProjectAction, &QAction::triggered, this, []() { - LOG_INFO("Menu clicked: File -> Open -> Project"); - }); - openMenu->addSeparator(); - auto* openScriptAction = openMenu->addAction("Script (*.dsl *.txt)"); - connect(openScriptAction, &QAction::triggered, this, [this]() { - QString fileName = QFileDialog::getOpenFileName(nullptr, "Open Script", QString::fromStdString((paths::assets() / "DSLScripts").string()), "DSL Script Files (*.dsl);;Text Files (*.txt)"); - if (fileName.isEmpty()) { return; } - if (!_dslEditor) { LOG_ERROR("DSL Editor not found!"); return; } - _dslEditor->loadScript(fileName); - }); + auto* openProjectAction = openMenu->addAction("Project"); + connect(openProjectAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> Open -> Project"); + }); + openMenu->addSeparator(); + auto* openScriptAction = openMenu->addAction("Script (*.dsl *.txt)"); + connect(openScriptAction, &QAction::triggered, this, [this]() { + QString fileName = QFileDialog::getOpenFileName(nullptr, "Open Script", QString::fromStdString((paths::assets() / "DSLScripts").string()), "DSL Script Files (*.dsl);;Text Files (*.txt)"); + if (fileName.isEmpty()) { return; } + if (!_dslEditor) { LOG_ERROR("DSL Editor not found!"); return; } + _dslEditor->loadScript(fileName); }); fileMenu->addSeparator(); auto* saveMenu = fileMenu->addMenu("Save"); - connect(saveMenu, &QMenu::aboutToShow, this, [this, saveMenu]() { - LOG_INFO("Menu clicked: File -> Save"); - auto* saveProjectAction = saveMenu->addAction("Project"); - connect(saveProjectAction, &QAction::triggered, this, []() { - LOG_INFO("Menu clicked: File -> Save -> Project"); - }); - auto* saveScriptAction = saveMenu->addAction("Script"); - connect(saveScriptAction, &QAction::triggered, this, [this]() { - LOG_INFO("Menu clicked: File -> Save -> Script"); - QString fileName = QFileDialog::getSaveFileName(nullptr, "Save Script", QString::fromStdString((paths::assets() / "DSLScripts").string()), "DSL Script Files (*.dsl);;Text Files (*.txt)"); // ARGS are - if (fileName.isEmpty()) { return; } - if (!_dslEditor) { LOG_ERROR("DSL Editor not found!"); return; } - _dslEditor->saveScript(fileName); - }); + auto* saveProjectAction = saveMenu->addAction("Project"); + connect(saveProjectAction, &QAction::triggered, this, []() { + LOG_INFO("Menu clicked: File -> Save -> Project"); + }); + auto* saveScriptAction = saveMenu->addAction("Script"); + connect(saveScriptAction, &QAction::triggered, this, [this]() { + LOG_INFO("Menu clicked: File -> Save -> Script"); + QString fileName = QFileDialog::getSaveFileName(nullptr, "Save Script", QString::fromStdString((paths::assets() / "DSLScripts").string()), "DSL Script Files (*.dsl);;Text Files (*.txt)"); // ARGS are + if (fileName.isEmpty()) { return; } + if (!_dslEditor) { LOG_ERROR("DSL Editor not found!"); return; } + _dslEditor->saveScript(fileName); }); auto* saveAsMenu = fileMenu->addMenu("Save As"); connect(saveAsMenu, &QMenu::aboutToShow, this, [this, saveAsMenu]() { diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp index a1992f4f..3a6a6713 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ConsoleOutputWidget.cpp @@ -106,6 +106,7 @@ namespace widgets { } const auto& simEntries = gLog.Instance().SimEntries(); + if (_lastSimCount > simEntries.size()) { _lastSimCount = 0; _simLog->clear(); } while (_lastSimCount < simEntries.size()) { const auto& e = simEntries[_lastSimCount]; QTextCursor cursor = _simLog->textCursor(); @@ -126,7 +127,11 @@ namespace widgets { cursor.select(QTextCursor::BlockUnderCursor); cursor.removeSelectedText(); cursor.deleteChar(); // Remove the block itself - --_lastTerminalCount; // Adjust count since we've removed an entry + } + + if (_lastTerminalCount > entries.size()) { + _terminalLog->clear(); + _lastTerminalCount = 0; } if (autoScroll) { From 44b3d0f1e92f82ed0d7ecb4982b95c637125f66b Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Mon, 8 Jun 2026 18:54:06 +0100 Subject: [PATCH 039/110] feat: Added rudamentery syntax highlighting of DSL scripts --- .../include/MainWindow/DSL/DSLSyntaxColours.h | 14 +++++ .../MainWindow/DSL/DSLSyntaxHighlighter.h | 32 ++++++++++ .../MainWindow/DSL/DSLSyntaxHighlighter.cpp | 59 +++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/DSL/DSLSyntaxColours.h create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/DSL/DSLSyntaxHighlighter.h create mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/DSL/DSLSyntaxHighlighter.cpp diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/DSL/DSLSyntaxColours.h b/DSFE_App/DSFE_GUI/include/MainWindow/DSL/DSLSyntaxColours.h new file mode 100644 index 00000000..8c66af28 --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/DSL/DSLSyntaxColours.h @@ -0,0 +1,14 @@ +// DSFE_GUI DSLSyntaxColours.h +#pragma once + +#include + +const std::vector COMMAND_COL{ 86, 156, 214 }; +const std::vector COMMENT_COL{ 106, 153, 85 }; +const std::vector NUMBER_COL{ 220, 170, 90 }; +const std::vector TYPE_COL{ 197, 134, 192 }; +const std::vector ID_COL{ 78, 201, 176}; +const std::vector BRACKETS_COL{ 23, 159, 255 }; +const std::vector KEYWORD_COL{ 255, 215, 0 }; +const std::vector STRING_COL{ 214, 157, 133 }; +const std::vector ERROR_COL{ 255, 80, 80 }; \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/DSL/DSLSyntaxHighlighter.h b/DSFE_App/DSFE_GUI/include/MainWindow/DSL/DSLSyntaxHighlighter.h new file mode 100644 index 00000000..3742bba9 --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/DSL/DSLSyntaxHighlighter.h @@ -0,0 +1,32 @@ +// DSFE_GUI DSLSyntaxHighlighter.h +#pragma once + +#include +#include + +class QColor; + +namespace widgets { + + class DSLSyntaxHighlighter : public QSyntaxHighlighter { + public: + explicit DSLSyntaxHighlighter(QTextDocument* parent = nullptr); + + protected: + void highlightBlock(const QString& text) override; + + private: + template + QColor makeQColor(const Container& c) { + if (c.size() < 3) return QColor(); // Returns an invalid color safely + return QColor(c[0], c[1], c[2]); + } + + struct HighlightingRule { + QRegularExpression pattern; + QTextCharFormat format; + }; + std::vector _rules; + }; + +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSL/DSLSyntaxHighlighter.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSL/DSLSyntaxHighlighter.cpp new file mode 100644 index 00000000..f2e5bace --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSL/DSLSyntaxHighlighter.cpp @@ -0,0 +1,59 @@ +// DSFE_GUI DSLSyntaxHighlighter.cpp +#include "DSL/DSLSyntaxHighlighter.h" +#include "DSL/DSLSyntaxColours.h" + +namespace widgets { + DSLSyntaxHighlighter::DSLSyntaxHighlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { + QTextCharFormat commentFmt; + commentFmt.setForeground(makeQColor(COMMENT_COL)); + _rules.push_back({ QRegularExpression("#.*$"), commentFmt }); + + QTextCharFormat cmdFmt; + cmdFmt.setForeground(makeQColor(COMMAND_COL)); + cmdFmt.setFontWeight(QFont::Bold); + + QStringList commands = { + "load", + "start", + "stop", + "trajSet", + "trajClear", + "rotateJointTo", + "rotateJointBy", + "parallel", + "set" + }; + + for (const auto& cmd : commands) { _rules.push_back({ QRegularExpression("\\b" + cmd + "\\b"), cmdFmt }); } + + QTextCharFormat numFmt; + numFmt.setForeground(makeQColor(NUMBER_COL)); + _rules.push_back({ QRegularExpression(R"(\b[-+]?[0-9]*\.?[0-9]+\b)"), numFmt }); + + QTextCharFormat trajFmt; + trajFmt.setForeground(makeQColor(TYPE_COL)); + QStringList trajTypes = { "TRAP", "SINE", "MSINE", "MULTISINE" }; + for (const auto& type : trajTypes) { _rules.push_back({ QRegularExpression("\\b" + type + "\\b"), trajFmt }); } + + QTextCharFormat idFmt; + idFmt.setForeground(makeQColor(ID_COL)); + _rules.push_back({ QRegularExpression(R"(\blink[0-9]+\b)"), idFmt }); + } + + void DSLSyntaxHighlighter::highlightBlock(const QString& text) { + int commentPos = text.indexOf('#'); + QString codeText = (commentPos >= 0) ? text.left(commentPos) : text; + + for (const auto& rule : _rules) { + auto it = rule.pattern.globalMatch(codeText); + while (it.hasNext()) { + auto match = it.next(); + setFormat(match.capturedStart(), match.capturedLength(), rule.format); + } + } + + if (commentPos >= 0) { + setFormat(commentPos, text.length() - commentPos, _rules[0].format); // Comment format is the first rule + } + } +} \ No newline at end of file From 7b48fb0099aecd1cbce7334d4264e3539a4543d0 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Mon, 8 Jun 2026 18:56:31 +0100 Subject: [PATCH 040/110] feat: Integrated DSL syntax highlighting into `DSLEditorWidget` --- DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h | 2 ++ DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h index 6af42e4b..f5a03f1e 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/DSLEditorWidget.h @@ -34,6 +34,7 @@ namespace runs { namespace widgets { class ConsoleOutputWidget; + class DSLSyntaxHighlighter; class DSLEditorWidget : public QWidget { public: @@ -53,6 +54,7 @@ namespace widgets { interpreter::RunWrapper* _wrapper = nullptr; ConsoleOutputWidget* _log = nullptr; + DSLSyntaxHighlighter* _highlighter = nullptr; void runScript(); void stopScript(); diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp index e7d9b153..d77e3540 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/DSLEditorWidget.cpp @@ -17,6 +17,7 @@ #include "Interpreter/StoredProgram.h" #include "Widgets/ConsoleOutputWidget.h" +#include "DSL/DSLSyntaxHighlighter.h" #include "EngineLib/LogMacros.h" @@ -112,6 +113,7 @@ namespace widgets { _editorTab = new QWidget(this); auto* layout = new QVBoxLayout(_editorTab); _scriptEditor = new QTextEdit(_editorTab); + _highlighter = new DSLSyntaxHighlighter(_scriptEditor->document()); layout->addWidget(_scriptEditor); _tabs->addTab(_editorTab, "Script Editor"); } From 4d79bd677bf4deae26b684d146a524aedf1a50d4 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Mon, 8 Jun 2026 19:40:01 +0100 Subject: [PATCH 041/110] feat: Added new styling format, using .qss style file utilised by `qresource` --- DSFE_App/DSFE_GUI/CMakeLists.txt | 8 + .../MainWindow/Widgets/ControlPanelWidget.h | 1 - .../include/MainWindow/style/DSFETheme.h | 7 + DSFE_App/DSFE_GUI/resources/DSFE_GUI.qrc | 5 + .../DSFE_GUI/resources/style/dsfe_dark.qss | 142 +++++++++++++ DSFE_App/DSFE_GUI/src/Application.cpp | 2 + .../MainWindow/Widgets/ControlPanelWidget.cpp | 200 ++++++++++++------ .../src/MainWindow/style/DSFETheme.cpp | 30 +++ 8 files changed, 324 insertions(+), 71 deletions(-) create mode 100644 DSFE_App/DSFE_GUI/include/MainWindow/style/DSFETheme.h create mode 100644 DSFE_App/DSFE_GUI/resources/DSFE_GUI.qrc create mode 100644 DSFE_App/DSFE_GUI/resources/style/dsfe_dark.qss create mode 100644 DSFE_App/DSFE_GUI/src/MainWindow/style/DSFETheme.cpp diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index dcd44548..5b6ce6b6 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -40,6 +40,7 @@ target_include_directories(stb INTERFACE set(MAIN_WINDOW_SRC src/MainWindow/DSFE_MainWindow.cpp src/MainWindow/Workspace/ProjectPage.cpp + src/MainWindow/style/DSFETheme.cpp ) set(RENDER_SRC @@ -79,6 +80,7 @@ set(WIDGETS_SRC src/MainWindow/Widgets/ConsoleOutputWidget.cpp src/MainWindow/Widgets/FractionSelectorWidget.cpp include/MainWindow/Widgets/FractionSelectorWidget.h + src/MainWindow/DSL/DSLSyntaxHighlighter.cpp ) set(ROBOT_SRC @@ -96,6 +98,10 @@ set(ASSETS_SRC src/Assets/importObj.cpp ) +set(QT_RESOURCES + resources/DSFE_GUI.qrc +) + target_sources(DSFE_GUI PRIVATE ${MAIN_WINDOW_SRC} ${RENDER_SRC} @@ -105,6 +111,7 @@ target_sources(DSFE_GUI PRIVATE ${ROBOT_SRC} ${PLATFORM_SRC} ${ASSETS_SRC} + ${QT_RESOURCES} ) set(imfilebrowser_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/include/imfilebrowser) @@ -142,6 +149,7 @@ target_include_directories(DSFE_GUI ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/include/MainWindow ${CMAKE_CURRENT_SOURCE_DIR}/include/Scene + ${CMAKE_CURRENT_SOURCE_DIR}/include/MainWindow/style PRIVATE ${imfilebrowser_ROOT} diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h index 15b71b1c..81f77b2c 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/Widgets/ControlPanelWidget.h @@ -112,7 +112,6 @@ namespace widgets { QGroupBox* _jointInfoGroup = nullptr; QSlider* _jointIdxSlider = nullptr; - QLabel* _currentSimTimeJointLabel = nullptr; static constexpr IntegratorEntry integrators[] = { diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/style/DSFETheme.h b/DSFE_App/DSFE_GUI/include/MainWindow/style/DSFETheme.h new file mode 100644 index 00000000..7e870b2e --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/MainWindow/style/DSFETheme.h @@ -0,0 +1,7 @@ +// DSFE_GUI DSFETheme.h +#pragma once + +class QApplication; +namespace style { + void applyTheme(QApplication& app); +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/resources/DSFE_GUI.qrc b/DSFE_App/DSFE_GUI/resources/DSFE_GUI.qrc new file mode 100644 index 00000000..34a1a146 --- /dev/null +++ b/DSFE_App/DSFE_GUI/resources/DSFE_GUI.qrc @@ -0,0 +1,5 @@ + + + style/dsfe_dark.qss + + \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/resources/style/dsfe_dark.qss b/DSFE_App/DSFE_GUI/resources/style/dsfe_dark.qss new file mode 100644 index 00000000..fa160758 --- /dev/null +++ b/DSFE_App/DSFE_GUI/resources/style/dsfe_dark.qss @@ -0,0 +1,142 @@ +QMainWindow { + background-color: rgb(36,36,36); +} + +QWidget { + background-color: rgb(36,36,36); + color: rgb(220,220,220); +} + +QGroupBox { + background-color: rgb(40,40,40); + border: 1px solid rgb(70,70,70); + border-radius: 4px; + margin-top: 8px; + padding-top: 10px; + font-weight: bold; +} + +QGroupBox::title { + subcontrol-origin: margin; + left: 8px; + padding: 0px 4px; + color: rgb(220,220,220); +} + +QTextEdit { + background-color: rgb(26,26,26); + border: 1px solid rgb(70,70,70); + color: rgb(220,220,220); + selection-background-color: rgb(86,156,214); + font-family: Consolas, monospace; + font-size: 12px; +} + +QPlainTextEdit { + background-color: rgb(26,26,26); + border: 1px solid rgb(70,70,70); + color: rgb(220,220,220); + selection-background-color: rgb(86,156,214); + font-family: Consolas, monospace; + font-size: 12px; +} + +QTabWidget::pane { + border: 1px solid rgb(70,70,70); + background-color: rgb(36,36,36); +} + +QTabBar::tab { + background-color: rgb(40,40,40); + border: 1px solid rgb(70,70,70); + padding: 4px 8px; +} + +QTabBar::tab:selected { + background-color: rgb(55,55,55); +} + +QMenuBar { + background-color: rgb(36,36,36); +} + +QMenuBar::item { + background: transparent; + padding: 4px 8px; +} + +QMenuBar::item:selected { + background-color: rgb(55,55,55); +} + +QMenu { + background-color: rgb(40,40,40); + border: 1px solid rgb(70,70,70); +} + +QMenu::item:selected { + background-color: rgb(86,156,214); +} + +QComboBox { + background-color: rgb(40,40,40); + border: 1px solid rgb(70,70,70); + border-radius: 3px; + padding: 2px 6px; + min-height: 20px; +} + +QComboBox::drop-down { + border: none; +} + +QPushButton { + background-color: rgb(50,50,50); + border: 1px solid rgb(70,70,70); + border-radius: 3px; + padding: 4px 10px; +} + +QPushButton:hover { + background-color: rgb(65,65,65); +} + +QPushButton:pressed { + background-color: rgb(86,156,214); +} + +QScrollBar:vertical { + background-color: rgb(30,30,30); + width: 12px; +} + +QScrollBar::handle:vertical { + background-color: rgb(80,80,80); + min-height: 20px; +} + +QSlider::groove:horizontal { + height: 4px; + background-color: rgb(70,70,70); + border-radius: 2px; +} + +QSlider::sub-page:horizontal { + background-color: rgb(86,156,214); + border-radius: 2px; +} + +QSlider::handle:horizontal { + background-color: rgb(220,220,220); + width: 12px; + margin: -5px 0px; + border-radius: 6px; +} + +QCheckBox { + spacing: 6px; +} + +QLabel { + background-color: transparent; +} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Application.cpp b/DSFE_App/DSFE_GUI/src/Application.cpp index 566b5e38..424b0761 100644 --- a/DSFE_App/DSFE_GUI/src/Application.cpp +++ b/DSFE_App/DSFE_GUI/src/Application.cpp @@ -11,6 +11,7 @@ #include #include #include +#include "MainWindow/style/DSFETheme.h" namespace fs = std::filesystem; // Initialise the static instance pointer to nullptr @@ -46,6 +47,7 @@ Application::Application(const std::string& appName) : _name(appName) { QSurfaceFormat::setDefaultFormat(format); _qtApp = std::make_unique(_qtArgc, _qtArgv.data()); + style::applyTheme(*_qtApp); _sim = std::make_unique(); int winW = 1920, winH = 1080; diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp index 36d5b7f8..f8ebd9bb 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/Widgets/ControlPanelWidget.cpp @@ -11,8 +11,8 @@ #include #include #include - -#include +#include +#include #include "Widgets/FractionSelectorWidget.h" @@ -62,32 +62,51 @@ namespace widgets { _simPropertiesGroup = new QGroupBox("Simulation Properties"); auto* layout = new QVBoxLayout(_simPropertiesGroup); - - _useAutoDiffCheck = new QCheckBox("Enable Automatic Differentiable Integrators"); - layout->addWidget(_useAutoDiffCheck); - _integratorCombo = new QComboBox(); - layout->addWidget(_integratorCombo); - - _currentIntegratorLabel = new QLabel(); - layout->addWidget(_currentIntegratorLabel); - layout->addSpacing(5); + _useAutoDiffCheck = new QCheckBox(); + _integratorCombo = new QComboBox(); + _integratorCombo->setMaximumWidth(175); _simDtSelector = new FractionSelectorWidget(false); - layout->addWidget(new QLabel("Simulation Time Step (dt):")); - layout->addWidget(_simDtSelector); _telemetryDtSelector = new FractionSelectorWidget(true); - layout->addWidget(new QLabel("Telemetry Time Step (dt):")); - layout->addWidget(_telemetryDtSelector); + _simTimeLabel = new QLabel(); - _simTimeLabel->setTextFormat(Qt::RichText); _simTimeLabel->setWordWrap(true); - layout->addWidget(new QLabel("Simulation Time:")); - layout->addWidget(_simTimeLabel); + auto* form = new QFormLayout(); + + form->addRow("Auto Diff", _useAutoDiffCheck); + form->addRow("Integrator", _integratorCombo); + + layout->addLayout(form); + + layout->addSpacing(8); + + auto* dtHeaderRow = new QHBoxLayout(); + + auto* simDtLabel = new QLabel("Simulation dt"); + simDtLabel->setAlignment(Qt::AlignCenter); + + auto* telemetryDtLabel = new QLabel("Telemetry dt"); + telemetryDtLabel->setAlignment(Qt::AlignCenter); + + dtHeaderRow->addWidget(simDtLabel); + dtHeaderRow->addWidget(telemetryDtLabel); + + layout->addLayout(dtHeaderRow); + + auto* dtValueRow = new QHBoxLayout(); + + dtValueRow->addWidget(_simDtSelector); + dtValueRow->addWidget(_telemetryDtSelector); + layout->addLayout(dtValueRow); + layout->addSpacing(8); + + layout->addWidget(_simTimeLabel); _contentLayout->addWidget(_simPropertiesGroup); buildIntegratorCombos(); + connect(_useAutoDiffCheck, &QCheckBox::toggled, this, [this, robot](bool checked) { _useAutoDiff = checked; robot->enableAutoDiff(checked); @@ -98,19 +117,15 @@ namespace widgets { if (_useAutoDiff) { auto selectedMethod = static_cast(_integratorCombo->currentData().toInt()); _sim->setADIntegrationMethod(selectedMethod); - _currentIntegratorLabel->setWordWrap(true); - _currentIntegratorLabel->setText(QString("Current Integrator: ") + _integratorCombo->currentText()); } else { auto selectedMethod = static_cast(_integratorCombo->currentData().toInt()); _sim->setIntegrationMethod(selectedMethod); - _currentIntegratorLabel->setWordWrap(true); - _currentIntegratorLabel->setText(QString("Current Integrator: ") + _integratorCombo->currentText()); } }); connect(_simDtSelector, &FractionSelectorWidget::valueChanged, this, [this](double dt) { _sim->setFixedDt(dt); }); - connect(_telemetryDtSelector, &FractionSelectorWidget::valueChanged, this, [this](double dt) { _sim->setTelemetryHz(1.0/dt); }); + connect(_telemetryDtSelector, &FractionSelectorWidget::valueChanged, this, [this](double dt) { _sim->setTelemetryHz(1.0 / dt); }); } void ControlPanelWidget::buildIntegratorCombos() { @@ -143,13 +158,14 @@ namespace widgets { _jointInfoGroup = new QGroupBox("Robot Joint Information"); auto* layout = new QVBoxLayout(_jointInfoGroup); _jointInfoGroup->setLayout(layout); - _currentSimTimeJointLabel = new QLabel(_jointInfoGroup); - layout->addWidget(_currentSimTimeJointLabel); + layout->addWidget(new QLabel("Joint")); _jointIdxSlider = new QSlider(Qt::Horizontal, _jointInfoGroup); _jointIdxSlider->setMinimum(1); _jointIdxSlider->setValue(1); + connect(_jointIdxSlider, &QSlider::valueChanged, this, [this](int value) { selectJointAndFollow(value - 1); }); + layout->addWidget(_jointIdxSlider); buildTelemetryWidgets(layout); @@ -182,41 +198,42 @@ namespace widgets { auto& t = _telemetryLabels; const float e = static_cast(j.q_ref - j.q); - t.q->setText(QString("pos:\t%1 rad").arg(j.q)); - t.qd->setText(QString("vel:\t%1 rad/s").arg(j.qd)); - t.tau->setText(QString("torque:\t%1 Nm").arg(j.torqueNm)); + t.q->setText(QString("%1 rad").arg(j.q)); + t.qd->setText(QString("%1 rad/s").arg(j.qd)); + t.tau->setText(QString("%1 Nm").arg(j.torqueNm)); - t.qRef->setText(QString("pos_ref:\t%1 rad").arg(j.q_ref)); - t.qdRef->setText(QString("vel_ref:\t%1 rad/s").arg(j.qd_ref)); - t.qddRef->setText(QString("acc_ref:\t%1 rad/s²").arg(j.qdd_ref)); - t.err->setText(QString("error:\t%1 rad").arg(e)); + t.qRef->setText(QString("%1 rad").arg(j.q_ref)); + t.qdRef->setText(QString("%1 rad/s").arg(j.qd_ref)); + t.qddRef->setText(QString("%1 rad/s²").arg(j.qdd_ref)); + t.err->setText(QString("%1 rad").arg(e)); - t.qTraj->setText(QString("pos_traj:\t%1 rad").arg(j.traj_q)); - t.qdTraj->setText(QString("vel_traj:\t%1 rad/s").arg(j.traj_qd)); - t.qddTraj->setText(QString("acc_traj:\t%1 rad/s²").arg(j.traj_qdd)); + t.qTraj->setText(QString("%1 rad").arg(j.traj_q)); + t.qdTraj->setText(QString("%1 rad/s").arg(j.traj_qd)); + t.qddTraj->setText(QString("%1 rad/s²").arg(j.traj_qdd)); - t.qClamped->setText(QString("pos_clamped:\t%1").arg(j.clampTheta ? "true" : "false")); - t.qdClamped->setText(QString("vel_clamped:\t%1").arg(j.clampOmega ? "true" : "false")); + t.qClamped->setText(j.clampTheta ? "On" : "Off"); + t.qdClamped->setText(j.clampOmega ? "On" : "Off"); - t.damping->setText(QString("damping:\t%1 kg·m²/s").arg(j.damping)); - t.friction->setText(QString("friction:\t%1 N·m").arg(j.friction)); + t.damping->setText(QString("%1 kg·m²/s").arg(j.damping)); + t.friction->setText(QString("%1 N·m").arg(j.friction)); } void ControlPanelWidget::buildTelemetryWidgets(QVBoxLayout* layout) { auto headerFont = [](QLabel* label) { QFont font = label->font(); font.setBold(true); - font.setPointSize(font.pointSize() + 2); + font.setPointSize(font.pointSize() + 1); label->setFont(font); + label->setStyleSheet("color: rgb(220,220,220);"); }; auto& t = _telemetryLabels; - t.stateHeader = new QLabel("State:"); - t.referenceHeader = new QLabel("Reference:"); - t.trajectoryHeader = new QLabel("Trajectory:"); - t.clampedHeader = new QLabel("Clamped:"); - t.constantsHeader = new QLabel("Constants:"); + t.stateHeader = new QLabel("STATE"); + t.referenceHeader = new QLabel("REFERENCE"); + t.trajectoryHeader = new QLabel("TRAJECTORY"); + t.clampedHeader = new QLabel("LIMITS"); + t.constantsHeader = new QLabel("PHYSICAL"); headerFont(t.stateHeader); headerFont(t.referenceHeader); @@ -225,7 +242,7 @@ namespace widgets { headerFont(t.constantsHeader); t.q = new QLabel(); - t.qd= new QLabel(); + t.qd = new QLabel(); t.tau = new QLabel(); t.qRef = new QLabel(); @@ -243,39 +260,83 @@ namespace widgets { t.damping = new QLabel(); t.friction = new QLabel(); - layout->addSpacing(5); - - layout->addWidget(t.stateHeader); - layout->addWidget(t.q); - layout->addWidget(t.qd); - layout->addWidget(t.tau); + auto* stateGrid = new QGridLayout(); + stateGrid->addWidget(new QLabel("Position"), 0, 0); + stateGrid->addWidget(t.q, 0, 1); + stateGrid->addWidget(new QLabel("Velocity"), 1, 0); + stateGrid->addWidget(t.qd, 1, 1); + stateGrid->addWidget(new QLabel("Torque"), 2, 0); + stateGrid->addWidget(t.tau, 2, 1); + + stateGrid->setHorizontalSpacing(12); + stateGrid->setColumnStretch(0, 0); + stateGrid->setColumnStretch(1, 1); + + auto* refGrid = new QGridLayout(); + refGrid->addWidget(new QLabel("Target Pos"), 0, 0); + refGrid->addWidget(t.qRef, 0, 1); + refGrid->addWidget(new QLabel("Target Vel"), 1, 0); + refGrid->addWidget(t.qdRef, 1, 1); + refGrid->addWidget(new QLabel("Target Acc"), 2, 0); + refGrid->addWidget(t.qddRef, 2, 1); + refGrid->addWidget(new QLabel("Error"), 3, 0); + refGrid->addWidget(t.err, 3, 1); + + refGrid->setHorizontalSpacing(12); + refGrid->setColumnStretch(0, 0); + refGrid->setColumnStretch(1, 1); + + auto* trajGrid = new QGridLayout(); + trajGrid->addWidget(new QLabel("Position"), 0, 0); + trajGrid->addWidget(t.qTraj, 0, 1); + trajGrid->addWidget(new QLabel("Velocity"), 1, 0); + trajGrid->addWidget(t.qdTraj, 1, 1); + trajGrid->addWidget(new QLabel("Acceleration"), 2, 0); + trajGrid->addWidget(t.qddTraj, 2, 1); + + trajGrid->setHorizontalSpacing(12); + trajGrid->setColumnStretch(0, 0); + trajGrid->setColumnStretch(1, 1); + + auto* limitGrid = new QGridLayout(); + limitGrid->addWidget(new QLabel("Position Clamp"), 0, 0); + limitGrid->addWidget(t.qClamped, 0, 1); + limitGrid->addWidget(new QLabel("Velocity Clamp"), 1, 0); + limitGrid->addWidget(t.qdClamped, 1, 1); + + limitGrid->setHorizontalSpacing(12); + limitGrid->setColumnStretch(0, 0); + limitGrid->setColumnStretch(1, 1); + + auto* physicalGrid = new QGridLayout(); + physicalGrid->addWidget(new QLabel("Damping"), 0, 0); + physicalGrid->addWidget(t.damping, 0, 1); + physicalGrid->addWidget(new QLabel("Friction"), 1, 0); + physicalGrid->addWidget(t.friction, 1, 1); + + physicalGrid->setHorizontalSpacing(12); + physicalGrid->setColumnStretch(0, 0); + physicalGrid->setColumnStretch(1, 1); layout->addSpacing(5); + layout->addWidget(t.stateHeader); + layout->addLayout(stateGrid); + layout->addSpacing(8); layout->addWidget(t.referenceHeader); - layout->addWidget(t.qRef); - layout->addWidget(t.qdRef); - layout->addWidget(t.qddRef); - layout->addWidget(t.err); - - layout->addSpacing(5); + layout->addLayout(refGrid); + layout->addSpacing(8); layout->addWidget(t.trajectoryHeader); - layout->addWidget(t.qTraj); - layout->addWidget(t.qdTraj); - layout->addWidget(t.qddTraj); - - layout->addSpacing(5); + layout->addLayout(trajGrid); + layout->addSpacing(8); layout->addWidget(t.clampedHeader); - layout->addWidget(t.qClamped); - layout->addWidget(t.qdClamped); - - layout->addSpacing(5); + layout->addLayout(limitGrid); + layout->addSpacing(8); layout->addWidget(t.constantsHeader); - layout->addWidget(t.damping); - layout->addWidget(t.friction); + layout->addLayout(physicalGrid); } void ControlPanelWidget::updateTelemetryDisplay() { @@ -293,7 +354,6 @@ namespace widgets { _jointIdxSlider->setValue(jointIdx + 1); } updateTelemetryInfo(s.j[jointIdx]); - if (_currentSimTimeJointLabel) { _currentSimTimeJointLabel->setText(QString("Current Simulation Time: %1 s").arg(simTime, 0, 'f', 3)); } } void ControlPanelWidget::displayPanel() { diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/style/DSFETheme.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/style/DSFETheme.cpp new file mode 100644 index 00000000..a810ff68 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/MainWindow/style/DSFETheme.cpp @@ -0,0 +1,30 @@ +// DSFE_GUI DSFETheme.cpp +#include "DSFETheme.h" + +#include +#include +#include +#include + + +namespace style { + void applyTheme(QApplication& app) { + QPalette palette; + palette.setColor(QPalette::Window, QColor(30, 30, 30)); + palette.setColor(QPalette::WindowText, QColor(220, 220, 220)); + palette.setColor(QPalette::Base, QColor(30, 30, 30)); + palette.setColor(QPalette::AlternateBase, QColor(40, 40, 40)); + palette.setColor(QPalette::Text, QColor(220, 220, 220)); + palette.setColor(QPalette::Button, QColor(40, 40, 40)); + palette.setColor(QPalette::ButtonText, QColor(220, 220, 220)); + palette.setColor(QPalette::Highlight, QColor(86, 156, 214)); + palette.setColor(QPalette::HighlightedText, QColor(255, 255, 255)); + app.setPalette(palette); + + QFile file(":/style/dsfe_dark.qss"); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; } + + app.setStyleSheet(QString::fromUtf8(file.readAll())); + } + +} \ No newline at end of file From 2042d468c9c3bd1fe11886c32efe0768b96c8940 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Wed, 10 Jun 2026 17:36:07 +0100 Subject: [PATCH 042/110] chore: Updated minimum cmake version to 3.17 from 3.10 --- CMakeLists.txt | 7 +- DSFE_App/CMakeLists.txt | 2 +- DSFE_App/DSFE_Core/CMakeLists.txt | 2 +- DSFE_App/DSFE_Engine/CMakeLists.txt | 2 +- DSFE_App/DSFE_GUI/CMakeLists.txt | 2 +- .../include/MainWindow/DSFE_MainWindow.h | 13 +- DSFE_App/DSFE_GUI/src/ui/Styles.cpp | 154 ------------------ PxMLib/CMakeLists.txt | 2 +- PxMLib/MathLib/CMakeLists.txt | 3 +- .../CMakeLists.txt | 2 +- PxMLib/MathLib_Unit/CMakeLists.txt | 2 +- .../DualNumberTests/CMakeLists.txt | 2 +- .../IntegratorTests/CMakeLists.txt | 2 +- 13 files changed, 22 insertions(+), 173 deletions(-) delete mode 100644 DSFE_App/DSFE_GUI/src/ui/Styles.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a41f29ab..a4c4881a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,5 @@ # CMakeLists.txt for DSFE project (root CMakeLists.txt) -cmake_minimum_required(VERSION 3.10) - -if (POLICY CMP0141) - cmake_policy(SET CMP0141 NEW) - set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$,$>,$<$:EditAndContinue>,$<$:ProgramDatabase>>") -endif() +cmake_minimum_required(VERSION 3.17) project(DSFE LANGUAGES C CXX) diff --git a/DSFE_App/CMakeLists.txt b/DSFE_App/CMakeLists.txt index 1fd34a64..6f40143c 100644 --- a/DSFE_App/CMakeLists.txt +++ b/DSFE_App/CMakeLists.txt @@ -1,5 +1,5 @@ # CMakeLists.txt for the DSFE_App project -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.17) project(DSFE_App LANGUAGES C CXX) # Add projects within the DSFE_App directory diff --git a/DSFE_App/DSFE_Core/CMakeLists.txt b/DSFE_App/DSFE_Core/CMakeLists.txt index a12a6709..8cf99bf2 100644 --- a/DSFE_App/DSFE_Core/CMakeLists.txt +++ b/DSFE_App/DSFE_Core/CMakeLists.txt @@ -1,5 +1,5 @@ # CMakeLists.txt for DSFE_Core library -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.17) project(DSFE_Core LANGUAGES C CXX) add_library(DSFE_Core SHARED) diff --git a/DSFE_App/DSFE_Engine/CMakeLists.txt b/DSFE_App/DSFE_Engine/CMakeLists.txt index f68e79b6..654bfe42 100644 --- a/DSFE_App/DSFE_Engine/CMakeLists.txt +++ b/DSFE_App/DSFE_Engine/CMakeLists.txt @@ -1,5 +1,5 @@ # CMakeLists.txt for the DSFE_Engine executable -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.17) project(DSFE_Engine LANGUAGES C CXX) find_package(Threads REQUIRED) diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 5b6ce6b6..5054bb97 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -1,7 +1,7 @@ # CMakeLists.txt for DSFE_GUI module set(CMAKE_POSITION_INDEPENDENT_CODE ON) -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.17) project(DSFE_GUI LANGUAGES C CXX) set(CMAKE_AUTOMOC ON) diff --git a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h index 771cad2e..aec3331b 100644 --- a/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h +++ b/DSFE_App/DSFE_GUI/include/MainWindow/DSFE_MainWindow.h @@ -9,6 +9,11 @@ namespace gui { class SimManager; } namespace widgets { class DSLEditorWidget; } +namespace render { + enum class ResolutionPreset; + enum class QualityPreset; +} + namespace window { class DSFE_MainWindow : public QMainWindow { public: @@ -16,9 +21,13 @@ namespace window { private: void buildMenuBar(); + void buildGraphicsMenu(QMenu* graphicsMenu); + void buildRobotMenu(QMenu* projectMenu); + + render::ResolutionPreset r; + render::QualityPreset q; + gui::SimManager* _sim; widgets::DSLEditorWidget* _dslEditor = nullptr; - - void buildRobotMenu(QMenu* projectMenu); }; } diff --git a/DSFE_App/DSFE_GUI/src/ui/Styles.cpp b/DSFE_App/DSFE_GUI/src/ui/Styles.cpp deleted file mode 100644 index 2b477560..00000000 --- a/DSFE_App/DSFE_GUI/src/ui/Styles.cpp +++ /dev/null @@ -1,154 +0,0 @@ -// DSFE_GUI Styles.cpp -#include "ui/Styles.h" -#include -#include -#include - -namespace gui { - void Styles::DarkMode() { - ImGuiStyle& style = ImGui::GetStyle(); - ImVec4* colors = style.Colors; - - // --- Rounding --- - style.WindowRounding = 0.0f; - style.FrameRounding = 0.0f; - style.ScrollbarRounding = 0.0f; - style.GrabRounding = 0.0f; - style.TabRounding = 0.0f; - - // --- Padding & spacing --- - style.WindowPadding = ImVec2(6, 6); - style.FramePadding = ImVec2(6, 4); - style.ItemSpacing = ImVec2(6, 4); - style.ItemInnerSpacing = ImVec2(4, 4); - - // --- Borders & separators --- - style.WindowBorderSize = 1.0f; - style.ChildBorderSize = 1.0f; - style.PopupBorderSize = 1.0f; - style.FrameBorderSize = 1.0f; - style.TabBorderSize = 1.0f; - style.SeparatorTextBorderSize = 1.0f; - - // --- Colors --- - colors[ImGuiCol_Text] = ImVec4(0.9f, 0.9f, 0.9f, 1.0f); - colors[ImGuiCol_WindowBg] = ImVec4(0.12f, 0.12f, 0.12f, 1.0f); - colors[ImGuiCol_ChildBg] = ImVec4(0.16f, 0.16f, 0.16f, 1.0f); - colors[ImGuiCol_PopupBg] = ImVec4(0.16f, 0.16f, 0.16f, 1.0f); - colors[ImGuiCol_Border] = ImVec4(0.29f, 0.29f, 0.29f, 1.0f); - colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0); - colors[ImGuiCol_FrameBg] = ImVec4(0.18f, 0.18f, 0.18f, 1.0f); - colors[ImGuiCol_FrameBgHovered] = ImVec4(0.28f, 0.28f, 0.28f, 1.0f); - colors[ImGuiCol_FrameBgActive] = ImVec4(0.22f, 0.22f, 0.22f, 1.0f); - colors[ImGuiCol_TitleBg] = ImVec4(0.12f, 0.12f, 0.12f, 1.0f); - colors[ImGuiCol_TitleBgActive] = ImVec4(0.18f, 0.18f, 0.18f, 1.0f); - colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.10f, 0.10f, 0.10f, 1.0f); - - colors[ImGuiCol_MenuBarBg] = ImVec4(0.16f, 0.16f, 0.16f, 1.0f); - colors[ImGuiCol_ScrollbarBg] = ImVec4(0.12f, 0.12f, 0.12f, 1.0f); - colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.28f, 0.28f, 0.28f, 1.0f); - colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.33f, 0.33f, 0.33f, 1.0f); - colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.38f, 0.38f, 0.38f, 1.0f); - colors[ImGuiCol_CheckMark] = ImVec4(0.25f, 0.5f, 0.9f, 1.0f); - colors[ImGuiCol_SliderGrab] = ImVec4(0.25f, 0.5f, 0.9f, 1.0f); - colors[ImGuiCol_SliderGrabActive] = ImVec4(0.20f, 0.45f, 0.85f, 1.0f); - colors[ImGuiCol_Button] = ImVec4(0.18f, 0.18f, 0.18f, 1.0f); - colors[ImGuiCol_ButtonHovered] = ImVec4(0.28f, 0.28f, 0.28f, 1.0f); - colors[ImGuiCol_ButtonActive] = ImVec4(0.22f, 0.22f, 0.22f, 1.0f); - - // Tabs like VS2022 - colors[ImGuiCol_Tab] = ImVec4(0.15f, 0.15f, 0.15f, 1.0f); - colors[ImGuiCol_TabHovered] = ImVec4(0.28f, 0.28f, 0.28f, 1.0f); - colors[ImGuiCol_TabActive] = ImVec4(0.22f, 0.22f, 0.22f, 1.0f); - colors[ImGuiCol_TabUnfocused] = ImVec4(0.14f, 0.14f, 0.14f, 1.0f); - colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.18f, 0.18f, 0.18f, 1.0f); - colors[ImGuiCol_Header] = ImVec4(0.18f, 0.18f, 0.18f, 1.0f); - colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.28f, 0.28f, 1.0f); - colors[ImGuiCol_HeaderActive] = ImVec4(0.22f, 0.22f, 0.22f, 1.0f); - - // Separator - colors[ImGuiCol_Separator] = ImVec4(0.29f, 0.29f, 0.29f, 1.0f); - colors[ImGuiCol_SeparatorHovered] = ImVec4(0.35f, 0.35f, 0.35f, 1.0f); - colors[ImGuiCol_SeparatorActive] = ImVec4(0.45f, 0.45f, 0.45f, 1.0f); - - // Tooltip - colors[ImGuiCol_PopupBg] = ImVec4(0.16f, 0.16f, 0.16f, 1.0f); - - // Optional: scrollbar rounding 0 to mimic VS - style.ScrollbarRounding = 0.0f; - style.GrabRounding = 0.0f; - } - - void Styles::LightMode() { - ImGuiStyle& style = ImGui::GetStyle(); - ImVec4* colors = style.Colors; - - // --- Rounding -- - style.WindowRounding = 0.0f; - style.FrameRounding = 0.0f; - style.ScrollbarRounding = 0.0f; - style.GrabRounding = 0.0f; - style.TabRounding = 0.0f; - - // --- Padding --- - style.FramePadding = ImVec2(6, 4); - style.ItemSpacing = ImVec2(6, 4); - - // --- Borders & separators --- - style.WindowBorderSize = 1.0f; - style.ChildBorderSize = 1.0f; - style.PopupBorderSize = 1.0f; - style.FrameBorderSize = 1.0f; - style.TabBorderSize = 1.0f; - style.SeparatorTextBorderSize = 1.0f; - - // --- Colors --- - colors[ImGuiCol_Text] = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); - colors[ImGuiCol_WindowBg] = ImVec4(0.94f, 0.94f, 0.94f, 1.0f); - colors[ImGuiCol_ChildBg] = ImVec4(0.96f, 0.96f, 0.96f, 1.0f); - colors[ImGuiCol_PopupBg] = ImVec4(0.96f, 0.96f, 0.96f, 1.0f); - colors[ImGuiCol_Border] = ImVec4(0.65f, 0.65f, 0.65f, 1.0f); - colors[ImGuiCol_BorderShadow] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); - colors[ImGuiCol_FrameBg] = ImVec4(0.90f, 0.90f, 0.90f, 1.0f); - colors[ImGuiCol_FrameBgHovered] = ImVec4(0.82f, 0.82f, 0.82f, 1.0f); - colors[ImGuiCol_FrameBgActive] = ImVec4(0.78f, 0.78f, 0.78f, 1.0f); - colors[ImGuiCol_TitleBg] = ImVec4(0.94f, 0.94f, 0.94f, 1.0f); - colors[ImGuiCol_TitleBgActive] = ImVec4(0.90f, 0.90f, 0.90f, 1.0f); - colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.96f, 0.96f, 0.96f, 1.0f); - - colors[ImGuiCol_MenuBarBg] = ImVec4(0.92f, 0.92f, 0.92f, 1.0f); - colors[ImGuiCol_ScrollbarBg] = ImVec4(0.94f, 0.94f, 0.94f, 1.0f); - colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.82f, 0.82f, 0.82f, 1.0f); - colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.78f, 0.78f, 0.78f, 1.0f); - colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.73f, 0.73f, 0.73f, 1.0f); - colors[ImGuiCol_CheckMark] = ImVec4(0.25f, 0.5f, 0.9f, 1.0f); - colors[ImGuiCol_SliderGrab] = ImVec4(0.25f, 0.5f, 0.9f, 1.0f); - colors[ImGuiCol_SliderGrabActive] = ImVec4(0.2f, 0.45f, 0.85f, 1.0f); - colors[ImGuiCol_Button] = ImVec4(0.90f, 0.90f, 0.90f, 1.0f); - colors[ImGuiCol_ButtonHovered] = ImVec4(0.82f, 0.82f, 0.82f, 1.0f); - colors[ImGuiCol_ButtonActive] = ImVec4(0.78f, 0.78f, 0.78f, 1.0f); - - // Tabs like VS2022 - colors[ImGuiCol_Tab] = ImVec4(0.92f, 0.92f, 0.92f, 1.0f); - colors[ImGuiCol_TabHovered] = ImVec4(0.82f, 0.82f, 0.82f, 1.0f); - colors[ImGuiCol_TabActive] = ImVec4(0.78f, 0.78f, 0.78f, 1.0f); - colors[ImGuiCol_TabUnfocused] = ImVec4(0.94f, 0.94f, 0.94f, 1.0f); - colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.90f, 0.90f, 0.90f, 1.0f); - colors[ImGuiCol_Header] = ImVec4(0.90f, 0.90f, 0.90f, 1.0f); - colors[ImGuiCol_HeaderHovered] = ImVec4(0.82f, 0.82f, 0.82f, 1.0f); - colors[ImGuiCol_HeaderActive] = ImVec4(0.78f, 0.78f, 0.78f, 1.0f); - - // Separator - colors[ImGuiCol_Separator] = ImVec4(0.65f, 0.65f, 0.65f, 1.0f); - colors[ImGuiCol_SeparatorHovered] = ImVec4(0.75f, 0.75f, 0.75f, 1.0f); - colors[ImGuiCol_SeparatorActive] = ImVec4(0.85f, 0.85f, 0.85f, 1.0f); - - // Tooltip - colors[ImGuiCol_PopupBg] = ImVec4(0.96f, 0.96f, 0.96f, 1.0f); - - // Optional: scrollbar rounding 0 to mimic VS - style.ScrollbarRounding = 0.0f; - style.GrabRounding = 0.0f; - } -} - diff --git a/PxMLib/CMakeLists.txt b/PxMLib/CMakeLists.txt index 73994c9f..988b2907 100644 --- a/PxMLib/CMakeLists.txt +++ b/PxMLib/CMakeLists.txt @@ -1,5 +1,5 @@ # CMakeLists.txt for PxMLib project -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.17) project(PxMLib LANGUAGES CXX) # Add projects within the DSFE_App directory diff --git a/PxMLib/MathLib/CMakeLists.txt b/PxMLib/MathLib/CMakeLists.txt index 495cd98e..9059084d 100644 --- a/PxMLib/MathLib/CMakeLists.txt +++ b/PxMLib/MathLib/CMakeLists.txt @@ -1,6 +1,5 @@ # CMakeLists.txt for MathLib - -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.17) project(MathLib LANGUAGES CXX) add_library(MathLib STATIC diff --git a/PxMLib/MathLib_Unit/AutomaticDifferentiationTests/CMakeLists.txt b/PxMLib/MathLib_Unit/AutomaticDifferentiationTests/CMakeLists.txt index 4652f2d8..547e2f60 100644 --- a/PxMLib/MathLib_Unit/AutomaticDifferentiationTests/CMakeLists.txt +++ b/PxMLib/MathLib_Unit/AutomaticDifferentiationTests/CMakeLists.txt @@ -1,5 +1,5 @@ # CMakeLists.txt for MathLib_Unit_Tests executable -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.17) project(MathLib_ADTests LANGUAGES C CXX) enable_testing() diff --git a/PxMLib/MathLib_Unit/CMakeLists.txt b/PxMLib/MathLib_Unit/CMakeLists.txt index 0910ece5..9bf06a0e 100644 --- a/PxMLib/MathLib_Unit/CMakeLists.txt +++ b/PxMLib/MathLib_Unit/CMakeLists.txt @@ -1,5 +1,5 @@ # CMakeLists.txt for MathLib_Unit_Tests executable -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.17) project(MathLib_Unit_Tests LANGUAGES C CXX) add_subdirectory(IntegratorTests) diff --git a/PxMLib/MathLib_Unit/DualNumberTests/CMakeLists.txt b/PxMLib/MathLib_Unit/DualNumberTests/CMakeLists.txt index 08c8e969..abc162ed 100644 --- a/PxMLib/MathLib_Unit/DualNumberTests/CMakeLists.txt +++ b/PxMLib/MathLib_Unit/DualNumberTests/CMakeLists.txt @@ -1,5 +1,5 @@ # CMakeLists.txt for MathLib_Unit_Tests executable -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.17) project(MathLib_DualNumberTests LANGUAGES C CXX) enable_testing() diff --git a/PxMLib/MathLib_Unit/IntegratorTests/CMakeLists.txt b/PxMLib/MathLib_Unit/IntegratorTests/CMakeLists.txt index a4f82d1d..f04ac90a 100644 --- a/PxMLib/MathLib_Unit/IntegratorTests/CMakeLists.txt +++ b/PxMLib/MathLib_Unit/IntegratorTests/CMakeLists.txt @@ -1,5 +1,5 @@ # CMakeLists.txt for MathLib_Unit_Tests executable -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.17) project(MathLib_IntegratorTests LANGUAGES C CXX) enable_testing() From 130ad1149d206a33f094369fcb869c052a176277 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Wed, 10 Jun 2026 17:37:05 +0100 Subject: [PATCH 043/110] feat: Introduced quality, resolution, and shader switches in the view menu bar --- .../src/MainWindow/DSFE_MainWindow.cpp | 142 +++++++++++++++++- 1 file changed, 137 insertions(+), 5 deletions(-) diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index e9aa08f4..af18e8ef 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -4,9 +4,12 @@ #include "Workspace/ProjectPage.h" #include "Widgets/DSLEditorWidget.h" +#include "ui/RenderPreset.h" + #include "Platform/SystemMap.h" #include +#include #include #include #include @@ -136,6 +139,8 @@ namespace window { } // View menu { + auto* graphicsMenu = viewMenu->addMenu("Graphics Options"); + buildGraphicsMenu(graphicsMenu); auto* resetCameraAction = viewMenu->addAction("Reset Camera"); connect(resetCameraAction, &QAction::triggered, this, []() { LOG_INFO("Menu clicked: View -> Reset Camera"); @@ -155,11 +160,6 @@ namespace window { connect(physicsDebugAction, &QAction::toggled, this, [](bool checked) { LOG_INFO("Menu toggled: Tools -> Toggle Physics Debug -> %s", checked ? "On" : "Off"); }); - auto* reloadShadersAction = toolsMenu->addAction("Reload Shaders"); - connect(reloadShadersAction, &QAction::triggered, this, [this]() { - LOG_INFO("Menu clicked: Tools -> Reload Shaders"); - _sim->reloadAllShaders(); - }); auto* diagnosticsAction = toolsMenu->addAction("Run Diagnostics"); connect(diagnosticsAction, &QAction::triggered, this, []() { LOG_INFO("Menu clicked: Tools -> Run Diagnostics"); @@ -178,6 +178,138 @@ namespace window { } } + void DSFE_MainWindow::buildGraphicsMenu(QMenu* graphicsMenu) { + // Quality submenu + auto* qualityMenu = graphicsMenu->addMenu("Quality"); + auto* lowQualityAction = qualityMenu->addAction("Low"); + auto* mediumQualityAction = qualityMenu->addAction("Medium"); + auto* highQualityAction = qualityMenu->addAction("High"); + auto* ultraQualityAction = qualityMenu->addAction("Ultra"); + lowQualityAction->setCheckable(true); + mediumQualityAction->setCheckable(true); + highQualityAction->setCheckable(true); + ultraQualityAction->setCheckable(true); + auto* qualityGroup = new QActionGroup(this); + qualityGroup->setExclusive(true); + qualityGroup->addAction(lowQualityAction); + qualityGroup->addAction(mediumQualityAction); + qualityGroup->addAction(highQualityAction); + qualityGroup->addAction(ultraQualityAction); + mediumQualityAction->setChecked(true); + // LOW + connect(lowQualityAction, &QAction::triggered, this, [this]() { + q = render::QualityPreset::Low; + auto s = render::MakeSettings(r, q); + _sim->applyRenderProfile(s, r); + LOG_INFO("Graphics Quality -> Low"); + }); + // MEDIUM + connect(mediumQualityAction, &QAction::triggered, this, [this]() { + q = render::QualityPreset::Medium; + auto s = render::MakeSettings(r, q); + _sim->applyRenderProfile(s, r); + LOG_INFO("Graphics Quality -> Medium"); + }); + // HIGH + connect(highQualityAction, &QAction::triggered, this, [this]() { + q = render::QualityPreset::High; + auto s = render::MakeSettings(r, q); + _sim->applyRenderProfile(s, r); + LOG_INFO("Graphics Quality -> High"); + }); + // ULTRA + connect(ultraQualityAction, &QAction::triggered, this, [this]() { + q = render::QualityPreset::Ultra; + auto s = render::MakeSettings(r, q); + _sim->applyRenderProfile(s, r); + LOG_INFO("Graphics Quality -> Ultra"); + }); + + // Resolution submenu + auto* resolutionMenu = graphicsMenu->addMenu("Resolution"); + auto* r720Action = resolutionMenu->addAction("720p"); + auto* r1080Action = resolutionMenu->addAction("1080p"); + auto* r1440Action = resolutionMenu->addAction("1440p"); + auto* r4kAction = resolutionMenu->addAction("4K"); + r720Action->setCheckable(true); + r1080Action->setCheckable(true); + r1440Action->setCheckable(true); + r4kAction->setCheckable(true); + auto* resolutionGroup = new QActionGroup(this); + resolutionGroup->setExclusive(true); + resolutionGroup->addAction(r720Action); + resolutionGroup->addAction(r1080Action); + resolutionGroup->addAction(r1440Action); + resolutionGroup->addAction(r4kAction); + r1080Action->setChecked(true); + // 720p + connect(r720Action, &QAction::triggered, this, [this]() { + r = render::ResolutionPreset::R_720p; + auto s = render::MakeSettings(r, q); + _sim->applyRenderProfile(s, r); + LOG_INFO("Resolution -> 720p"); + }); + // 1080p + connect(r1080Action, &QAction::triggered, this, [this]() { + r = render::ResolutionPreset::R_1080p; + auto s = render::MakeSettings(r, q); + _sim->applyRenderProfile(s, r); + LOG_INFO("Resolution -> 1080p"); + }); + // 1440p + connect(r1440Action, &QAction::triggered, this, [this]() { + r = render::ResolutionPreset::R_1440p; + auto s = render::MakeSettings(r, q); + _sim->applyRenderProfile(s, r); + LOG_INFO("Resolution -> 1440p"); + }); + // 4K + connect(r4kAction, &QAction::triggered, this, [this]() { + r = render::ResolutionPreset::R_4K; + auto s = render::MakeSettings(r, q); + _sim->applyRenderProfile(s, r); + LOG_INFO("Resolution -> 4K"); + }); + + // Shader submenu + auto* shaderMenu = graphicsMenu->addMenu("Shaders"); + auto* basicShaderAction = shaderMenu->addAction("Basic"); + auto* litShaderAction = shaderMenu->addAction("Lit"); + auto* pbrShaderAction = shaderMenu->addAction("PBR"); + basicShaderAction->setCheckable(true); + litShaderAction->setCheckable(true); + pbrShaderAction->setCheckable(true); + auto* shaderGroup = new QActionGroup(this); + shaderGroup->setExclusive(true); + shaderGroup->addAction(basicShaderAction); + shaderGroup->addAction(litShaderAction); + shaderGroup->addAction(pbrShaderAction); + pbrShaderAction->setChecked(true); + // BASIC + connect(basicShaderAction, &QAction::triggered, this, [this]() { + _sim->currentShaderMode = gui::SimManager::ShaderMode::Basic; + LOG_INFO("Shader Mode -> Basic"); + }); + // LIT + connect(litShaderAction, &QAction::triggered, this, [this]() { + _sim->currentShaderMode = gui::SimManager::ShaderMode::Lit; + LOG_INFO("Shader Mode -> Lit"); + }); + // PBR + connect(pbrShaderAction, &QAction::triggered, this, [this]() { + _sim->currentShaderMode = gui::SimManager::ShaderMode::PBR; + LOG_INFO("Shader Mode -> PBR"); + }); + + shaderMenu->addSeparator(); + + auto* reloadShadersAction = shaderMenu->addAction("Reload Shaders"); + connect(reloadShadersAction, &QAction::triggered, this, [this]() { + LOG_INFO("Menu clicked: Reload Shaders"); + _sim->reloadAllShaders(); + }); + } + void DSFE_MainWindow::buildRobotMenu(QMenu* projectMenu) { const auto& robotMap = platform::getRobotSystemMap(); std::unordered_map familyMenus; From 29598dee707ef241f33aae7497f9fc09128fee47 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 08:38:46 +0100 Subject: [PATCH 044/110] fixes: Removed stale files referring to `imgui.h`, `implot.h`, `imfilebrowser.h`, `input.h`, and `glfw.h` --- DSFE_App/DSFE_GUI/include/Platform/Window.h | 41 - .../DSFE_GUI/include/Rendering/RenderBase.h | 9 - DSFE_App/DSFE_GUI/include/Scene/Camera.h | 35 +- DSFE_App/DSFE_GUI/include/Scene/FpsCounter.h | 35 - DSFE_App/DSFE_GUI/include/Scene/Input.h | 28 - .../include/Scene/Manager/SimImplementation.h | 1 - .../include/Scene/SimulationManager.h | 12 +- .../DSFE_GUI/include/imfilebrowser/LICENSE | 21 - .../include/imfilebrowser/imfilebrowser.h | 1347 ----------------- .../DSFE_GUI/src/Scene/AxisOrientator.cpp | 2 - DSFE_App/DSFE_GUI/src/Scene/Input.cpp | 41 - DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp | 182 --- DSFE_App/DSFE_GUI/src/Scene/Object.cpp | 27 +- .../DSFE_GUI/src/Scene/SimulationManager.cpp | 21 +- 14 files changed, 43 insertions(+), 1759 deletions(-) delete mode 100644 DSFE_App/DSFE_GUI/include/Platform/Window.h delete mode 100644 DSFE_App/DSFE_GUI/include/Scene/FpsCounter.h delete mode 100644 DSFE_App/DSFE_GUI/include/Scene/Input.h delete mode 100644 DSFE_App/DSFE_GUI/include/imfilebrowser/LICENSE delete mode 100644 DSFE_App/DSFE_GUI/include/imfilebrowser/imfilebrowser.h delete mode 100644 DSFE_App/DSFE_GUI/src/Scene/Input.cpp delete mode 100644 DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp diff --git a/DSFE_App/DSFE_GUI/include/Platform/Window.h b/DSFE_App/DSFE_GUI/include/Platform/Window.h deleted file mode 100644 index 861ec64b..00000000 --- a/DSFE_App/DSFE_GUI/include/Platform/Window.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once -// File: Window.h -// GitHub: SaltyJoss -#include "EngineCore.h" - -#include -#include "Platform/Logger.h" - -//Basic window interface -namespace window { - class IWindow { - public: - // Destructor - virtual ~IWindow() = default; - - // Initialisation - virtual bool init(int width, int height, const std::string& title) = 0; - - // Core windowing - virtual bool isRunning() const = 0; - virtual bool shouldClose() const = 0; - virtual void pollEvents() = 0; - virtual void swapBuffers() = 0; - - // Native window access - virtual void* getNativeWin() = 0; - virtual void setNativeWin(void* window) = 0; - - // Callbacks - virtual void onKey(int key, int scancode, int action, int mods) = 0; - virtual void onScroll(double delta) = 0; - virtual void onResize(int width, int height) = 0; - virtual void onCursorPos(double xpos, double ypos) = 0; - virtual void onClose() = 0; - - // Getters - virtual int getWidth() const = 0; - virtual int getHeight() const = 0; - virtual const std::string& getHeader() const = 0; - }; -} // namespace window \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Rendering/RenderBase.h b/DSFE_App/DSFE_GUI/include/Rendering/RenderBase.h index dde79adc..9dab8b53 100644 --- a/DSFE_App/DSFE_GUI/include/Rendering/RenderBase.h +++ b/DSFE_App/DSFE_GUI/include/Rendering/RenderBase.h @@ -1,7 +1,6 @@ // DSFE_GUI RenderBase.h #pragma once -#include "Platform/Window.h" #include "Assets/VertexHolder.h" #include @@ -65,18 +64,10 @@ namespace render { virtual ~RenderContext() = default; // Centeralised way to gain context on the renders' process -> see OpenGLContext - RenderContext() : _window(nullptr) {} - - virtual bool init(window::IWindow* win) { - _window = win; - return true; - } virtual void preRender() = 0; virtual void postRender() = 0; virtual void end() = 0; - protected: - window::IWindow* _window; }; } // namespace render \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Scene/Camera.h b/DSFE_App/DSFE_GUI/include/Scene/Camera.h index bc36f9ed..e45acbb1 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/Camera.h +++ b/DSFE_App/DSFE_GUI/include/Scene/Camera.h @@ -6,7 +6,6 @@ #include #include "Scene/Element.h" -#include "Scene/Input.h" #include "Platform/Logger.h" @@ -63,30 +62,30 @@ namespace scene { setDistance((float)(-delta * 0.5f)); } - void onMouseMove(double x, double y, eInputButton button) { - glm::vec2 pos2d{ x, y }; + //void onMouseMove(double x, double y) { + // glm::vec2 pos2d{ x, y }; - if (button == eInputButton::Right) { - glm::vec2 delta = (pos2d - _currentPos2D) * 0.004f; + // if () { + // glm::vec2 delta = (pos2d - _currentPos2D) * 0.004f; - float sign = getUp().y < 0 ? -1.0f : 1.0f; + // float sign = getUp().y < 0 ? -1.0f : 1.0f; - _yaw += sign * delta.x * _rotationSpeed; - _pitch += delta.y * _rotationSpeed; + // _yaw += sign * delta.x * _rotationSpeed; + // _pitch += delta.y * _rotationSpeed; - updateViewMatrix(); - } - else if (button == eInputButton::Left) { - glm::vec2 delta = (pos2d - _currentPos2D) * 0.003f; + // updateViewMatrix(); + // } + // else if (false) { + // glm::vec2 delta = (pos2d - _currentPos2D) * 0.003f; - _focus += -getRight() * delta.x * _distance; - _focus += getUp() * delta.y * _distance; + // _focus += -getRight() * delta.x * _distance; + // _focus += getUp() * delta.y * _distance; - updateViewMatrix(); - } + // updateViewMatrix(); + // } - _currentPos2D = pos2d; - } + // _currentPos2D = pos2d; + //} void startFollow(const glm::vec3& pos, const glm::quat& rot, const glm::vec3& offset); void clearFollow() { _following = false; } diff --git a/DSFE_App/DSFE_GUI/include/Scene/FpsCounter.h b/DSFE_App/DSFE_GUI/include/Scene/FpsCounter.h deleted file mode 100644 index 15569a27..00000000 --- a/DSFE_App/DSFE_GUI/include/Scene/FpsCounter.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once -// File: FpsCounter.h -// GitHub: SaltyJoss -#include "EngineCore.h" -#include - -#include "Platform/Logger.h" - -namespace gui { - class FpsCounter { - public: - void update() { - double now = glfwGetTime(); - double dt = now - last; - - last = now; - frames++; - accum += dt; - if (accum >= 1.0) { - fps = frames / accum; - frames = 0; - accum = 0.0; - } - } - - double getFPS() const { return fps; } - - private: - bool init = false; - double last = glfwGetTime(); - double accum = 0.0; - int frames = 0; - double fps = 0.0; - }; -} // namespace gui \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Scene/Input.h b/DSFE_App/DSFE_GUI/include/Scene/Input.h deleted file mode 100644 index 9373fcfe..00000000 --- a/DSFE_App/DSFE_GUI/include/Scene/Input.h +++ /dev/null @@ -1,28 +0,0 @@ -// DSFE_GUI Input.h -#pragma once - -#include -#include - -#include "Platform/Logger.h" - -namespace scene { - enum class eInputButton { - Left = 0, - Right = 1, - Middle = 2, - None = 9 - }; - - class Input { - public: - static eInputButton GetPressedButton(GLFWwindow* window); - - static bool IsKeyPressed(GLFWwindow* window, int key); - static bool IsMouseButtonPressed(GLFWwindow* window, eInputButton button); - - private: - Input() = default; - }; - -} // namespace scene \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h index e171722c..b0f7bed2 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h +++ b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h @@ -10,7 +10,6 @@ #include #include -#include "Scene/Input.h" #include "Scene/Camera.h" #include "Scene/Mesh.h" #include "Assets/MeshLoader.h" diff --git a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h index fd1b5c93..f433a54b 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h +++ b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h @@ -1,6 +1,10 @@ // DSFE_GUI SimulationManager.h #pragma once +#ifdef __gl_h_ +#undef __gl_h_ +#endif +#include #include #include #include @@ -12,7 +16,6 @@ #include "Rendering/ModelGroup.h" #include "Scene/ObjectID.h" #include "ui/RenderPreset.h" -#include "FpsCounter.h" #include "Platform/KeyCode.h" @@ -31,10 +34,8 @@ namespace shaders { class Shader; } // Forward Declarations for Scene namespace scene { - enum class eInputButton; class Light; class Camera; - class Input; class Mesh; class Object; class SceneRenderer; @@ -273,7 +274,7 @@ namespace gui { void processMovementKey(int key, float delta); void handleContinuousMovement(const std::unordered_set& pressedKeys, float dt); void handleMouseLook(double xpos, double ypos, bool mouseCaptured); - void onMouseMove(double x, double y, scene::eInputButton button); + void onMouseMove(double x, double y); void onMouseWheel(double delta); void resetMouseDelta(); @@ -361,9 +362,6 @@ namespace gui { bool _shadowsInit = false; bool _hdrUserOverride = false; - // Editor & UI - gui::FpsCounter _fpsCounter; - // View management bool _isHovered = false; bool skyboxEnabled = true; diff --git a/DSFE_App/DSFE_GUI/include/imfilebrowser/LICENSE b/DSFE_App/DSFE_GUI/include/imfilebrowser/LICENSE deleted file mode 100644 index a656d732..00000000 --- a/DSFE_App/DSFE_GUI/include/imfilebrowser/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019-2025 Zhuang Guan - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/DSFE_App/DSFE_GUI/include/imfilebrowser/imfilebrowser.h b/DSFE_App/DSFE_GUI/include/imfilebrowser/imfilebrowser.h deleted file mode 100644 index 3b472823..00000000 --- a/DSFE_App/DSFE_GUI/include/imfilebrowser/imfilebrowser.h +++ /dev/null @@ -1,1347 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef IMGUI_VERSION -# error "include imgui.h before this header" -#endif - -using ImGuiFileBrowserFlags = std::uint32_t; - -enum ImGuiFileBrowserFlags_ : std::uint32_t -{ - ImGuiFileBrowserFlags_SelectDirectory = 1 << 0, // select directory instead of regular file - ImGuiFileBrowserFlags_EnterNewFilename = 1 << 1, // allow user to enter new filename when selecting regular file - ImGuiFileBrowserFlags_NoModal = 1 << 2, // file browsing window is modal by default. specify this to use a popup window - ImGuiFileBrowserFlags_NoTitleBar = 1 << 3, // hide window title bar - ImGuiFileBrowserFlags_NoStatusBar = 1 << 4, // hide status bar at the bottom of browsing window - ImGuiFileBrowserFlags_CloseOnEsc = 1 << 5, // close file browser when pressing 'ESC' - ImGuiFileBrowserFlags_CreateNewDir = 1 << 6, // allow user to create new directory - ImGuiFileBrowserFlags_MultipleSelection = 1 << 7, // allow user to select multiple files. this will hide ImGuiFileBrowserFlags_EnterNewFilename - ImGuiFileBrowserFlags_HideRegularFiles = 1 << 8, // hide regular files when ImGuiFileBrowserFlags_SelectDirectory is enabled - ImGuiFileBrowserFlags_ConfirmOnEnter = 1 << 9, // confirm selection when pressing 'ENTER' - ImGuiFileBrowserFlags_SkipItemsCausingError = 1 << 10, // when entering a new directory, any error will interrupt the process, causing the file browser to fall back to the working directory. - // with this flag, if an error is caused by a specific item in the directory, that item will be skipped, allowing the process to continue. - ImGuiFileBrowserFlags_EditPathString = 1 << 11, // allow user to directly edit the whole path string -}; - -namespace ImGui -{ - class FileBrowser - { - public: - - explicit FileBrowser( - ImGuiFileBrowserFlags flags = 0, - std::filesystem::path defaultDirectory = std::filesystem::current_path()); - - FileBrowser(const FileBrowser ©From); - - FileBrowser &operator=(const FileBrowser ©From); - - // set the window position (in pixels) - // default is centered - void SetWindowPos(int posX, int posY) noexcept; - - // set the window size (in pixels) - // default is (700, 450) - void SetWindowSize(int width, int height) noexcept; - - // set the window title text - void SetTitle(std::string title); - - // open the browsing window - void Open(); - - // close the browsing window - void Close(); - - // the browsing window is opened or not - bool IsOpened() const noexcept; - - // display the browsing window if opened - void Display(); - - // returns true when there is a selected filename - bool HasSelected() const noexcept; - - // set current browsing directory - bool SetDirectory(const std::filesystem::path &dir = std::filesystem::current_path()); - - // legacy interface. use SetDirectory instead. - bool SetPwd(const std::filesystem::path &dir = std::filesystem::current_path()) - { - return SetDirectory(dir); - } - - // get current browsing directory - const std::filesystem::path &GetDirectory() const noexcept; - - // legacy interface. use GetDirectory instead. - const std::filesystem::path &GetPwd() const noexcept - { - return GetDirectory(); - } - - // returns selected filename. make sense only when HasSelected returns true - // when ImGuiFileBrowserFlags_MultipleSelection is enabled, only one of - // selected filename will be returned - std::filesystem::path GetSelected() const; - - // returns all selected filenames. - // when ImGuiFileBrowserFlags_MultipleSelection is enabled, use this - // instead of GetSelected - std::vector GetMultiSelected() const; - - // set selected filename to empty - void ClearSelected(); - - // (optional) set file type filters. eg. { ".h", ".cpp", ".hpp" } - // ".*" matches any file types - void SetTypeFilters(const std::vector &typeFilters); - - // set currently applied type filter - // default value is 0 (the first type filter) - void SetCurrentTypeFilterIndex(int index); - - // when ImGuiFileBrowserFlags_EnterNewFilename is set - // this function will pre-fill the input dialog with a filename. - void SetInputName(std::string_view input); - - private: - - template - struct ScopeGuard - { - ScopeGuard(Functor&& t) : func(std::move(t)) { } - - ~ScopeGuard() { func(); } - - private: - - Functor func; - }; - - struct FileRecord - { - bool isDir = false; - std::filesystem::path name; - std::string showName; - std::filesystem::path extension; - }; - - static std::string ToLower(const std::string &s); - - void ToolTip(const std::string_view &s); - - void UpdateFileRecords(); - - void SetCurrentDirectoryUncatched(const std::filesystem::path &pwd); - - bool SetCurrentDirectoryInternal( - const std::filesystem::path &dir, - const std::filesystem::path &preferredFallback); - - bool IsExtensionMatched(const std::filesystem::path &extension) const; - - void ClearRangeSelectionState(); - - static void AssignToArrayStyleString(std::vector &arr, std::string_view content); - - static int ExpandInputBuffer(ImGuiInputTextCallbackData *callbackData); - -#ifdef _WIN32 - static std::uint32_t GetDrivesBitMask(); -#endif - - // for c++17 compatibility - -#if defined(__cpp_lib_char8_t) - static std::string u8StrToStr(std::u8string s); -#endif - static std::string u8StrToStr(std::string s); - - static std::filesystem::path u8StrToPath(const char *str); - - int width_; - int height_; - int posX_; - int posY_; - ImGuiFileBrowserFlags flags_; - std::filesystem::path defaultDirectory_; - - std::string title_; - std::string openLabel_; - - bool shouldOpen_; - bool shouldClose_; - bool isOpened_; - bool isOk_; - bool isPosSet_; - - std::string statusStr_; - - std::vector typeFilters_; - unsigned int typeFilterIndex_; - bool hasAllFilter_; - - std::filesystem::path currentDirectory_; - std::vector fileRecords_; - - unsigned int rangeSelectionStart_; // enable range selection when shift is pressed - std::set selectedFilenames_; - - std::string openNewDirLabel_; - std::vector newDirNameBuffer_; - std::vector inputNameBuffer_; - std::string customizedInputName_; - - bool editDir_; - bool setFocusToEditDir_; - std::vector currDirBuffer_; - -#ifdef _WIN32 - std::uint32_t drives_; -#endif - }; -} // namespace ImGui - -inline ImGui::FileBrowser::FileBrowser(ImGuiFileBrowserFlags flags, std::filesystem::path defaultDirectory) - : width_(700) - , height_(450) - , posX_(0) - , posY_(0) - , flags_(flags) - , defaultDirectory_(std::move(defaultDirectory)) - , shouldOpen_(false) - , shouldClose_(false) - , isOpened_(false) - , isOk_(false) - , isPosSet_(false) - , rangeSelectionStart_(0) - , editDir_(false) - , setFocusToEditDir_(false) -{ - assert(!((flags_ & ImGuiFileBrowserFlags_SelectDirectory) && (flags_ & ImGuiFileBrowserFlags_EnterNewFilename)) && - "'EnterNewFilename' doesn't work when 'SelectDirectory' is enabled"); - if(flags_ & ImGuiFileBrowserFlags_CreateNewDir) - { - newDirNameBuffer_.resize(32, '\0'); - } - - SetTitle("file browser"); - SetDirectory(defaultDirectory_); - - typeFilters_.clear(); - typeFilterIndex_ = 0; - hasAllFilter_ = false; - -#ifdef _WIN32 - drives_ = GetDrivesBitMask(); -#endif -} - -inline ImGui::FileBrowser::FileBrowser(const FileBrowser ©From) - : FileBrowser() -{ - *this = copyFrom; -} - -inline ImGui::FileBrowser &ImGui::FileBrowser::operator=( - const FileBrowser ©From) -{ - width_ = copyFrom.width_; - height_ = copyFrom.height_; - - posX_ = copyFrom.posX_; - posY_ = copyFrom.posY_; - - flags_ = copyFrom.flags_; - SetTitle(copyFrom.title_); - - shouldOpen_ = copyFrom.shouldOpen_; - shouldClose_ = copyFrom.shouldClose_; - isOpened_ = copyFrom.isOpened_; - isOk_ = copyFrom.isOk_; - isPosSet_ = copyFrom.isPosSet_; - - statusStr_ = ""; - - typeFilters_ = copyFrom.typeFilters_; - typeFilterIndex_ = copyFrom.typeFilterIndex_; - hasAllFilter_ = copyFrom.hasAllFilter_; - - selectedFilenames_ = copyFrom.selectedFilenames_; - rangeSelectionStart_ = copyFrom.rangeSelectionStart_; - - currentDirectory_ = copyFrom.currentDirectory_; - fileRecords_ = copyFrom.fileRecords_; - - openNewDirLabel_ = copyFrom.openNewDirLabel_; - newDirNameBuffer_ = copyFrom.newDirNameBuffer_; - inputNameBuffer_ = copyFrom.inputNameBuffer_; - customizedInputName_ = copyFrom.customizedInputName_; - - editDir_ = copyFrom.editDir_; - currDirBuffer_ = copyFrom.currDirBuffer_; - -#ifdef _WIN32 - drives_ = copyFrom.drives_; -#endif - - return *this; -} - -inline void ImGui::FileBrowser::SetWindowPos(int posX, int posY) noexcept -{ - posX_ = posX; - posY_ = posY; - isPosSet_ = true; -} - -inline void ImGui::FileBrowser::SetWindowSize(int width, int height) noexcept -{ - assert(width > 0 && height > 0); - width_ = width; - height_ = height; -} - -inline void ImGui::FileBrowser::SetTitle(std::string title) -{ - title_ = std::move(title); - - const std::string thisPtrStr = std::to_string(reinterpret_cast(this)); - openLabel_ = title_ + "##filebrowser_" + thisPtrStr; - openNewDirLabel_ = "new dir##new_dir_" + thisPtrStr; -} - -inline void ImGui::FileBrowser::Open() -{ - UpdateFileRecords(); - ClearSelected(); - statusStr_ = std::string(); - shouldOpen_ = true; - shouldClose_ = false; - if((flags_ & ImGuiFileBrowserFlags_EnterNewFilename) && !customizedInputName_.empty()) - { - AssignToArrayStyleString(inputNameBuffer_, customizedInputName_); - selectedFilenames_ = { u8StrToPath(inputNameBuffer_.data()) }; - } -} - -inline void ImGui::FileBrowser::Close() -{ - ClearSelected(); - statusStr_ = std::string(); - shouldClose_ = true; - shouldOpen_ = false; -} - -inline bool ImGui::FileBrowser::IsOpened() const noexcept -{ - return isOpened_; -} - -inline void ImGui::FileBrowser::Display() -{ - PushID(this); - ScopeGuard exitThis([this] - { - shouldOpen_ = false; - shouldClose_ = false; - PopID(); - }); - - if(shouldOpen_) - { - OpenPopup(openLabel_.c_str()); - } - isOpened_ = false; - - // open the popup window - - if(shouldOpen_ && (flags_ & ImGuiFileBrowserFlags_NoModal)) - { - if(isPosSet_) - { - SetNextWindowPos(ImVec2(static_cast(posX_), static_cast(posY_))); - } - SetNextWindowSize(ImVec2(static_cast(width_), static_cast(height_))); - } - else - { - if(isPosSet_) - { - SetNextWindowPos(ImVec2(static_cast(posX_), static_cast(posY_)), ImGuiCond_FirstUseEver); - } - SetNextWindowSize(ImVec2(static_cast(width_), static_cast(height_)), ImGuiCond_FirstUseEver); - } - if(flags_ & ImGuiFileBrowserFlags_NoModal) - { - if(!BeginPopup(openLabel_.c_str())) - { - return; - } - } - else if(!BeginPopupModal(openLabel_.c_str(), nullptr, - flags_ & ImGuiFileBrowserFlags_NoTitleBar ? ImGuiWindowFlags_NoTitleBar : 0)) - { - return; - } - - isOpened_ = true; - ScopeGuard endPopup([] { EndPopup(); }); - - std::filesystem::path newDir; bool shouldSetNewDir = false; - - if(editDir_) - { - if(setFocusToEditDir_) // Automatically set the text box to be focused on appearing - { - SetKeyboardFocusHere(); - } - - PushItemWidth(-1); - const bool enter = InputText( - "##directory", currDirBuffer_.data(), currDirBuffer_.size(), - ImGuiInputTextFlags_CallbackResize | ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll, - ExpandInputBuffer, &currDirBuffer_); - PopItemWidth(); - - if(!IsItemActive() && !setFocusToEditDir_) - { - editDir_ = false; - } - setFocusToEditDir_ = false; - - if(enter) - { - std::filesystem::path enteredDir = u8StrToPath(currDirBuffer_.data()); - if(is_directory(enteredDir)) - { - newDir = std::move(enteredDir); - shouldSetNewDir = true; - } - else if(is_directory(enteredDir.parent_path())) - { - newDir = enteredDir.parent_path(); - shouldSetNewDir = true; - } - else - { - statusStr_ = "[" + std::string(currDirBuffer_.data()) + "] is not a valid directory"; - } - } - } - else - { - // display elements in pwd - -#ifdef _WIN32 - const char currentDrive = static_cast(currentDirectory_.c_str()[0]); - const char driveStr[] = { currentDrive, ':', '\0' }; - - PushItemWidth(4 * GetFontSize()); - if(BeginCombo("##select_drive", driveStr)) - { - ScopeGuard guard([&] { EndCombo(); }); - - for(int i = 0; i < 26; ++i) - { - if(!(drives_ & (1 << i))) - { - continue; - } - - const char driveCh = static_cast('A' + i); - const char selectableStr[] = { driveCh, ':', '\0' }; - const bool selected = currentDrive == driveCh; - - if(Selectable(selectableStr, selected) && !selected) - { - char newPwd[] = { driveCh, ':', '\\', '\0' }; - SetDirectory(newPwd); - } - } - } - PopItemWidth(); - - SameLine(); -#endif - - int secIdx = 0, newDirLastSecIdx = -1; - for(const auto &sec : currentDirectory_) - { -#ifdef _WIN32 - if(secIdx == 1) - { - ++secIdx; - continue; - } -#endif - - PushID(secIdx); - if(secIdx > 0) - { - SameLine(); - } - if(SmallButton(u8StrToStr(sec.u8string()).c_str())) - { - newDirLastSecIdx = secIdx; - } - PopID(); - - ++secIdx; - } - - if(newDirLastSecIdx >= 0) - { - int i = 0; - std::filesystem::path dstDir; - for(const auto &sec : currentDirectory_) - { - if(i++ > newDirLastSecIdx) - { - break; - } - dstDir /= sec; - } - -#ifdef _WIN32 - if(newDirLastSecIdx == 0) - { - dstDir /= "\\"; - } -#endif - - SetDirectory(dstDir); - } - - if(flags_ & ImGuiFileBrowserFlags_EditPathString) - { - SameLine(); - - if(SmallButton("#")) - { - const auto currDirStr = u8StrToStr(currentDirectory_.u8string()); - currDirBuffer_.resize(currDirStr.size() + 1); - std::memcpy(currDirBuffer_.data(), currDirStr.data(), currDirStr.size()); - currDirBuffer_.back() = '\0'; - - editDir_ = true; - setFocusToEditDir_ = true; - } - else - { - ToolTip("Edit the current path"); - } - } - } - - SameLine(); - if(SmallButton("*")) - { -#ifdef _WIN32 - drives_ = GetDrivesBitMask(); -#endif - - UpdateFileRecords(); - - std::set newSelectedFilenames; - for(auto &name : selectedFilenames_) - { - const auto it = std::find_if( - fileRecords_.begin(), fileRecords_.end(), [&](const FileRecord &record) - { - return name == record.name; - }); - if(it != fileRecords_.end()) - { - newSelectedFilenames.insert(name); - } - } - - if((flags_ & ImGuiFileBrowserFlags_EnterNewFilename) && !inputNameBuffer_.empty() && inputNameBuffer_[0]) - { - newSelectedFilenames.insert(u8StrToPath(inputNameBuffer_.data())); - } - } - else - { - ToolTip("Refresh"); - } - - bool focusOnInputText = false; - if(flags_ & ImGuiFileBrowserFlags_CreateNewDir) - { - SameLine(); - if(SmallButton("+")) - { - OpenPopup(openNewDirLabel_.c_str()); - newDirNameBuffer_[0] = '\0'; - } - else - { - ToolTip("Create a new directory"); - } - - if(BeginPopup(openNewDirLabel_.c_str())) - { - ScopeGuard endNewDirPopup([] { EndPopup(); }); - - InputText( - "name", newDirNameBuffer_.data(), newDirNameBuffer_.size(), - ImGuiInputTextFlags_CallbackResize, ExpandInputBuffer, &newDirNameBuffer_); - focusOnInputText |= IsItemFocused(); - SameLine(); - - if(Button("ok") && newDirNameBuffer_[0] != '\0') - { - ScopeGuard closeNewDirPopup([] { CloseCurrentPopup(); }); - if(create_directory(currentDirectory_ / u8StrToPath(newDirNameBuffer_.data()))) - { - UpdateFileRecords(); - } - else - { - statusStr_ = "failed to create " + std::string(newDirNameBuffer_.data()); - } - } - } - } - - // browse files in a child window - - float reserveHeight = GetFrameHeightWithSpacing(); - if(flags_ & ImGuiFileBrowserFlags_EnterNewFilename) - { - reserveHeight += GetFrameHeightWithSpacing(); - } - - { - BeginChild("ch", ImVec2(0, -reserveHeight), true, - (flags_ & ImGuiFileBrowserFlags_NoModal) ? ImGuiWindowFlags_AlwaysHorizontalScrollbar : 0); - ScopeGuard endChild([] { EndChild(); }); - - const bool shouldHideRegularFiles = - (flags_ & ImGuiFileBrowserFlags_HideRegularFiles) && (flags_ & ImGuiFileBrowserFlags_SelectDirectory); - - for(unsigned int rscIndex = 0; rscIndex < fileRecords_.size(); ++rscIndex) - { - const auto &rsc = fileRecords_[rscIndex]; - if(!rsc.isDir && shouldHideRegularFiles) - { - continue; - } - if(!rsc.isDir && !IsExtensionMatched(rsc.extension)) - { - continue; - } - if(!rsc.name.empty() && rsc.name.c_str()[0] == '$') - { - continue; - } - - const bool selected = selectedFilenames_.find(rsc.name) != selectedFilenames_.end(); - -#if IMGUI_VERSION_NUM >= 19100 - const ImGuiSelectableFlags selectableFlag = ImGuiSelectableFlags_NoAutoClosePopups; -#else - const ImGuiSelectableFlags selectableFlag = ImGuiSelectableFlags_DontClosePopups; -#endif - - if(Selectable(rsc.showName.c_str(), selected, selectableFlag)) - { - const bool wantDir = flags_ & ImGuiFileBrowserFlags_SelectDirectory; - const bool canSelect = rsc.name != ".." && rsc.isDir == wantDir; - const bool rangeSelect = - canSelect && GetIO().KeyShift && - rangeSelectionStart_ < fileRecords_.size() && - (flags_ & ImGuiFileBrowserFlags_MultipleSelection) && - IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); - const bool multiSelect = - !rangeSelect && GetIO().KeyCtrl && - (flags_ & ImGuiFileBrowserFlags_MultipleSelection) && - IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); - - if(rangeSelect) - { - const unsigned int first = (std::min)(rangeSelectionStart_, rscIndex); - const unsigned int last = (std::max)(rangeSelectionStart_, rscIndex); - selectedFilenames_.clear(); - for(unsigned int i = first; i <= last; ++i) - { - if(fileRecords_[i].isDir != wantDir) - { - continue; - } - if(!wantDir && !IsExtensionMatched(fileRecords_[i].extension)) - { - continue; - } - selectedFilenames_.insert(fileRecords_[i].name); - } - } - else if(selected) - { - if(!multiSelect) - { - selectedFilenames_ = { rsc.name }; - rangeSelectionStart_ = rscIndex; - } - else - { - selectedFilenames_.erase(rsc.name); - } - if(flags_ & ImGuiFileBrowserFlags_EnterNewFilename) - { - AssignToArrayStyleString(inputNameBuffer_, ""); - } - } - else if(canSelect) - { - if(multiSelect) - { - selectedFilenames_.insert(rsc.name); - } - else - { - selectedFilenames_ = { rsc.name }; - } - if(flags_ & ImGuiFileBrowserFlags_EnterNewFilename) - { - const auto rscName = u8StrToStr(rsc.name.u8string()); - AssignToArrayStyleString(inputNameBuffer_, rscName); - } - rangeSelectionStart_ = rscIndex; - } - } - - if(IsMouseDoubleClicked(ImGuiMouseButton_Left) && IsItemHovered(ImGuiHoveredFlags_None)) - { - if(rsc.isDir) - { - shouldSetNewDir = true; - newDir = (rsc.name != "..") ? (currentDirectory_ / rsc.name) : currentDirectory_.parent_path(); - } - else if(!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) - { - selectedFilenames_ = { rsc.name }; - isOk_ = true; - CloseCurrentPopup(); - } - } - else if(IsKeyPressed(ImGuiKey_GamepadFaceDown) && IsItemHovered()) - { - if(rsc.isDir) - { - shouldSetNewDir = true; - newDir = (rsc.name != "..") ? (currentDirectory_ / rsc.name) : currentDirectory_.parent_path(); - SetKeyboardFocusHere(-1); - } - else if(!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) - { - selectedFilenames_ = { rsc.name }; - isOk_ = true; - CloseCurrentPopup(); - } - } - } - } - - if(shouldSetNewDir) - { - SetDirectory(newDir); - } - - if(flags_ & ImGuiFileBrowserFlags_EnterNewFilename) - { - PushID(this); - ScopeGuard popTextID([] { PopID(); }); - - if(inputNameBuffer_.empty()) - { - inputNameBuffer_.resize(1, '\0'); - } - - PushItemWidth(-1); - if(InputText( - "", inputNameBuffer_.data(), inputNameBuffer_.size(), - ImGuiInputTextFlags_CallbackResize, ExpandInputBuffer, &inputNameBuffer_)) - { - if(inputNameBuffer_[0] != '\0') - { - selectedFilenames_ = { u8StrToPath(inputNameBuffer_.data()) }; - } - else - { - selectedFilenames_.clear(); - } - } - focusOnInputText |= IsItemFocused(); - PopItemWidth(); - } - - if(!focusOnInputText && !editDir_) - { - const bool selectAll = (flags_ & ImGuiFileBrowserFlags_MultipleSelection) && - IsKeyPressed(ImGuiKey_A) && (IsKeyDown(ImGuiKey_LeftCtrl) || - IsKeyDown(ImGuiKey_RightCtrl)); - if(selectAll) - { - const bool needDir = flags_ & ImGuiFileBrowserFlags_SelectDirectory; - selectedFilenames_.clear(); - for(size_t i = 1; i < fileRecords_.size(); ++i) - { - auto &record = fileRecords_[i]; - if(record.isDir == needDir && - (needDir || IsExtensionMatched(record.extension))) - { - selectedFilenames_.insert(record.name); - } - } - } - } - - const bool isEnterPressed = - (flags_ & ImGuiFileBrowserFlags_ConfirmOnEnter) && - IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && - IsKeyPressed(ImGuiKey_Enter); - if(!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) - { - BeginDisabled(selectedFilenames_.empty()); - const bool ok = Button("ok"); - EndDisabled(); - if((ok || isEnterPressed) && !selectedFilenames_.empty()) - { - isOk_ = true; - CloseCurrentPopup(); - } - } - else - { - if(Button(" ok ") || isEnterPressed) - { - isOk_ = true; - CloseCurrentPopup(); - } - } - - SameLine(); - - const bool shouldClose = - Button("cancel") || shouldClose_ || - ((flags_ & ImGuiFileBrowserFlags_CloseOnEsc) && - IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && - IsKeyPressed(ImGuiKey_Escape)); - if(shouldClose) - { - CloseCurrentPopup(); - } - - if(!statusStr_.empty() && !(flags_ & ImGuiFileBrowserFlags_NoStatusBar)) - { - SameLine(); - Text("%s", statusStr_.c_str()); - if (ImGui::IsItemHovered()) - { - ImGui::BeginTooltip(); - ImGui::PushTextWrapPos(300.0f); - ImGui::Text("%s", statusStr_.c_str()); - ImGui::PopTextWrapPos(); - ImGui::EndTooltip(); - } - } - - if(!typeFilters_.empty()) - { - SameLine(); - PushItemWidth(8 * GetFontSize()); - if(BeginCombo( - "##type_filters", typeFilters_[typeFilterIndex_].c_str())) - { - ScopeGuard guard([&] { EndCombo(); }); - - for(size_t i = 0; i < typeFilters_.size(); ++i) - { - bool selected = i == typeFilterIndex_; - if(Selectable(typeFilters_[i].c_str(), selected) && !selected) - { - typeFilterIndex_ = static_cast(i); - } - } - } - PopItemWidth(); - } -} - -inline bool ImGui::FileBrowser::HasSelected() const noexcept -{ - return isOk_; -} - -inline bool ImGui::FileBrowser::SetDirectory(const std::filesystem::path &dir) -{ - const std::filesystem::path preferredFallback = this->GetDirectory(); - return SetCurrentDirectoryInternal(dir, preferredFallback); -} - -inline const std::filesystem::path &ImGui::FileBrowser::GetDirectory() const noexcept -{ - return currentDirectory_; -} - -inline std::filesystem::path ImGui::FileBrowser::GetSelected() const -{ - // when isOk_ is true, selectedFilenames_ may be empty if SelectDirectory - // is enabled. return pwd in that case. - if(selectedFilenames_.empty()) - { - return currentDirectory_; - } - return currentDirectory_ / *selectedFilenames_.begin(); -} - -inline std::vector ImGui::FileBrowser::GetMultiSelected() const -{ - if(selectedFilenames_.empty()) - { - return { currentDirectory_ }; - } - - std::vector ret; - ret.reserve(selectedFilenames_.size()); - for(auto &s : selectedFilenames_) - { - ret.push_back(currentDirectory_ / s); - } - - return ret; -} - -inline void ImGui::FileBrowser::ClearSelected() -{ - selectedFilenames_.clear(); - if((flags_ & ImGuiFileBrowserFlags_EnterNewFilename)) - { - AssignToArrayStyleString(inputNameBuffer_, ""); - } - isOk_ = false; -} - -inline void ImGui::FileBrowser::SetTypeFilters(const std::vector &_typeFilters) -{ - typeFilters_.clear(); - - // remove duplicate filter names due to case unsensitivity on windows - -#ifdef _WIN32 - - std::vector typeFilters; - for(auto &rawFilter : _typeFilters) - { - std::string lowerFilter = ToLower(rawFilter); - const auto it = std::find(typeFilters.begin(), typeFilters.end(), lowerFilter); - if(it == typeFilters.end()) - { - typeFilters.push_back(std::move(lowerFilter)); - } - } - -#else - - auto &typeFilters = _typeFilters; - -#endif - - // insert auto-generated filter - hasAllFilter_ = false; - if(typeFilters.size() > 1) - { - hasAllFilter_ = true; - std::string allFiltersName = std::string(); - for(size_t i = 0; i < typeFilters.size(); ++i) - { - if(typeFilters[i] == std::string_view(".*")) - { - hasAllFilter_ = false; - break; - } - - if(i > 0) - { - allFiltersName += ","; - } - allFiltersName += typeFilters[i]; - } - - if(hasAllFilter_) - { - typeFilters_.push_back(std::move(allFiltersName)); - } - } - - std::copy(typeFilters.begin(), typeFilters.end(), std::back_inserter(typeFilters_)); - typeFilterIndex_ = 0; -} - -inline void ImGui::FileBrowser::SetCurrentTypeFilterIndex(int index) -{ - typeFilterIndex_ = static_cast(index); -} - -inline void ImGui::FileBrowser::SetInputName(std::string_view input) -{ - assert((flags_ & ImGuiFileBrowserFlags_EnterNewFilename) && - "SetInputName can only be called when ImGuiFileBrowserFlags_EnterNewFilename is enabled"); - customizedInputName_ = input; -} - -inline std::string ImGui::FileBrowser::ToLower(const std::string &s) -{ - std::string ret = s; - for(char &c : ret) - { - c = static_cast(std::tolower(c)); - } - return ret; -} - -inline void ImGui::FileBrowser::ToolTip(const std::string_view &s) -{ - if (!ImGui::IsItemHovered()) - { - return; - } - ImGui::SetTooltip("%s", s.data()); -} - -inline void ImGui::FileBrowser::UpdateFileRecords() -{ - fileRecords_ = { FileRecord{ true, "..", "[D] ..", "" } }; - - const auto getDirectoryIterator = [&]() -> std::filesystem::directory_iterator - { - try - { - return std::filesystem::directory_iterator(currentDirectory_); - } - catch (const std::filesystem::filesystem_error& err) - { - statusStr_ = std::string("error: ") + err.what(); - if (!(flags_ & ImGuiFileBrowserFlags_SkipItemsCausingError)) - { - throw; - } - return {}; - } - }; - - for(auto &p : getDirectoryIterator()) - { - FileRecord rcd; - try - { - if(p.is_regular_file()) - { - rcd.isDir = false; - } - else if(p.is_directory()) - { - rcd.isDir = true; - } - else - { - continue; - } - - rcd.name = p.path().filename(); - if(rcd.name.empty()) - { - continue; - } - - rcd.extension = p.path().filename().extension(); - rcd.showName = (rcd.isDir ? "[D] " : "[F] ") + u8StrToStr(p.path().filename().u8string()); - } - catch(...) - { - if(!(flags_ & ImGuiFileBrowserFlags_SkipItemsCausingError)) - { - throw; - } - continue; - } - fileRecords_.push_back(rcd); - } - - // The default lexicographical order does not meet our sorting requirements. - // We want [b0, a0, A1] to be sorted into something like [a0, A1, b0] instead of [a0, b0, A1]. - // Therefore, here we compute a custom key for each filename for sorting. - if(fileRecords_.size() > 2) - { - std::vector> keys; - keys.reserve(fileRecords_.size()); - for(auto &fileRecord : fileRecords_) - { - const auto name = u8StrToStr(fileRecord.name.u8string()); - auto& key = keys.emplace_back(); - key.reserve(name.size() + 1); - key.emplace_back(!fileRecord.isDir); - for(char c : name) - { - if('A' <= c && c <= 'Z') - { - key.emplace_back(2 * (c + 'a' - 'A') + 1); - } - else - { - key.emplace_back(2 * c); - } - } - } - - std::vector fileRecordRemapIndices; - fileRecordRemapIndices.reserve(fileRecords_.size()); - for(uint32_t i = 0; i < fileRecords_.size(); ++i) - { - fileRecordRemapIndices.push_back(i); - } - - std::sort( - fileRecordRemapIndices.begin() + 1, fileRecordRemapIndices.end(), [&](uint32_t li, uint32_t ri) - { - return keys[li] < keys[ri]; - }); - - std::vector remappedFileRecords; - remappedFileRecords.reserve(fileRecords_.size()); - for(const uint32_t index : fileRecordRemapIndices) - { - remappedFileRecords.emplace_back(std::move(fileRecords_[index])); - } - - fileRecords_ = std::move(remappedFileRecords); - } - - ClearRangeSelectionState(); -} - -inline void ImGui::FileBrowser::SetCurrentDirectoryUncatched(const std::filesystem::path &pwd) -{ - currentDirectory_ = absolute(pwd); - UpdateFileRecords(); - - bool shouldClearInputNameBuffer = true; - - if((flags_ & ImGuiFileBrowserFlags_EnterNewFilename) && - selectedFilenames_.size() == 1 && - !customizedInputName_.empty() && - !inputNameBuffer_.empty() && - std::strcmp(inputNameBuffer_.data(), customizedInputName_.data()) == 0) - { - shouldClearInputNameBuffer = false; - } - - if(shouldClearInputNameBuffer) - { - selectedFilenames_.clear(); - AssignToArrayStyleString(inputNameBuffer_, ""); - } -} - -inline bool ImGui::FileBrowser::SetCurrentDirectoryInternal( - const std::filesystem::path &dir, const std::filesystem::path &preferredFallback) -{ - try - { - SetCurrentDirectoryUncatched(dir); - return true; - } - catch(const std::exception &err) - { - statusStr_ = std::string("error: ") + err.what(); - } - catch(...) - { - statusStr_ = "unknown error"; - } - - if(preferredFallback != defaultDirectory_) - { - try - { - SetCurrentDirectoryUncatched(preferredFallback); - } - catch(...) - { - SetCurrentDirectoryUncatched(defaultDirectory_); - } - } - else - { - SetCurrentDirectoryUncatched(defaultDirectory_); - } - - return false; -} - -inline bool ImGui::FileBrowser::IsExtensionMatched(const std::filesystem::path &_extension) const -{ -#ifdef _WIN32 - std::filesystem::path extension = ToLower(u8StrToStr(_extension.u8string())); -#else - auto &extension = _extension; -#endif - - // no type filters - if(typeFilters_.empty()) - { - return true; - } - - // invalid type filter index - if(static_cast(typeFilterIndex_) >= typeFilters_.size()) - { - return true; - } - - // all type filters - if(hasAllFilter_ && typeFilterIndex_ == 0) - { - for(size_t i = 1; i < typeFilters_.size(); ++i) - { - if(extension == typeFilters_[i]) - { - return true; - } - } - return false; - } - - // universal filter - if(typeFilters_[typeFilterIndex_] == std::string_view(".*")) - { - return true; - } - - // regular filter - return extension == typeFilters_[typeFilterIndex_]; -} - -inline void ImGui::FileBrowser::ClearRangeSelectionState() -{ - rangeSelectionStart_ = 9999999; - const bool dir = flags_ & ImGuiFileBrowserFlags_SelectDirectory; - for(unsigned int i = 1; i < fileRecords_.size(); ++i) - { - if(fileRecords_[i].isDir == dir) - { - if(!dir && !IsExtensionMatched(fileRecords_[i].extension)) - { - continue; - } - rangeSelectionStart_ = i; - break; - } - } -} - -inline void ImGui::FileBrowser::AssignToArrayStyleString(std::vector &arr, std::string_view content) -{ - if(content.empty()) - { - if(!arr.empty()) - { - arr[0] = '\0'; - } - return; - } - - if(arr.size() < content.size() + 1) - { - arr.resize(content.size() + 1); - } - std::memcpy(arr.data(), content.data(), content.size()); - arr[content.size()] = '\0'; -} - -inline int ImGui::FileBrowser::ExpandInputBuffer(ImGuiInputTextCallbackData *callbackData) -{ - if(callbackData && callbackData->EventFlag & ImGuiInputTextFlags_CallbackResize) - { - auto buffer = static_cast*>(callbackData->UserData); - size_t newSize = buffer->size(); - while(newSize < static_cast(callbackData->BufSize)) - { - newSize <<= 1; - } - buffer->resize(newSize, '\0'); - callbackData->Buf = buffer->data(); - callbackData->BufDirty = true; - } - return 0; -} - -#if defined(__cpp_lib_char8_t) -inline std::string ImGui::FileBrowser::u8StrToStr(std::u8string s) -{ - std::string result; - result.resize(s.length()); - std::memcpy(result.data(), s.data(), s.length()); - return result; -} -#endif - -inline std::string ImGui::FileBrowser::u8StrToStr(std::string s) -{ - return s; -} - -inline std::filesystem::path ImGui::FileBrowser::u8StrToPath(const char *str) -{ -#if defined(__cpp_lib_char8_t) - // With C++20/23, it's impossible to efficiently convert a `char*` string to a `char8_t*` string without violating - // the strict aliasing rule. Bad joke! - const size_t len = std::strlen(str); - std::u8string u8Str; - u8Str.resize(len); - std::memcpy(u8Str.data(), str, len); - return std::filesystem::path(u8Str); -#else - // u8path is deprecated in C++20 - return std::filesystem::u8path(str); -#endif -} - -#ifdef _WIN32 - -inline std::uint32_t ImGui::FileBrowser::GetDrivesBitMask() -{ - std::uint32_t ret = 0; - for(int i = 0; i < 26; ++i) - { - const char rootName[4] = { static_cast('A' + i), ':', '\\', '\0' }; - try{ - if (std::filesystem::exists(rootName)) - { - ret |= (1 << i); - } - } - catch (const std::filesystem::filesystem_error &) - { - // Ignore invalid paths or inaccessible drives, e.g., empty CD drives or network shares - } - } - return ret; -} - -#endif diff --git a/DSFE_App/DSFE_GUI/src/Scene/AxisOrientator.cpp b/DSFE_App/DSFE_GUI/src/Scene/AxisOrientator.cpp index 8052b41d..e0bf0eac 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/AxisOrientator.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/AxisOrientator.cpp @@ -8,8 +8,6 @@ #include #include -#include // needed if you keep any ImGui usage - namespace gui { // --- Static Member Definitions --- GLuint AxisOrientator::g_VAO = 0; diff --git a/DSFE_App/DSFE_GUI/src/Scene/Input.cpp b/DSFE_App/DSFE_GUI/src/Scene/Input.cpp deleted file mode 100644 index df7743bc..00000000 --- a/DSFE_App/DSFE_GUI/src/Scene/Input.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "pch.h" -// File: Input.cpp -// GitHub: SaltyJoss -#include "Scene/Input.h" -#include - -#include "EngineLib/LogMacros.h" - -using namespace scene; - -// Get the currently pressed mouse button, if any -eInputButton Input::GetPressedButton(GLFWwindow* window) { - if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { return eInputButton::Left; } - if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS) { return eInputButton::Right; } - if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS) { return eInputButton::Middle; } - return eInputButton::None; -} - -// Check if a specific key is currently pressed -bool Input::IsKeyPressed(GLFWwindow* window, int key) { - return glfwGetKey(window, key) == GLFW_PRESS; -} - -// Check if a specific mouse button is currently pressed -bool Input::IsMouseButtonPressed(GLFWwindow* window, eInputButton button) { - int glfwButton; - switch (button) { - case eInputButton::Left: - glfwButton = GLFW_MOUSE_BUTTON_LEFT; - break; - case eInputButton::Right: - glfwButton = GLFW_MOUSE_BUTTON_RIGHT; - break; - case eInputButton::Middle: - glfwButton = GLFW_MOUSE_BUTTON_MIDDLE; - break; - default: - return false; - } - return glfwGetMouseButton(window, glfwButton) == GLFW_PRESS; -} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp deleted file mode 100644 index 36c20079..00000000 --- a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimUI.cpp +++ /dev/null @@ -1,182 +0,0 @@ -// DSFE_GUI SimUI.cpp -#include "Scene/SimulationManager.h" -#ifdef __gl_h_ -#undef __gl_h_ -#endif -#include "Manager/SimImplementation.h" - -#include - -// TODO: Convert logic to Qt6 and only ever use ImGui for debugging at most (or not at all) - -namespace gui { - // Main Dockspace with Menu Bar - void SimManager::drawMainDockspace() { - ImGuiWindowFlags flags = - ImGuiWindowFlags_NoDocking | - ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoBringToFrontOnFocus | - ImGuiWindowFlags_NoNavFocus; - - const ImGuiViewport* vp = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(vp->Pos); - ImGui::SetNextWindowSize(vp->Size); - ImGui::SetNextWindowViewport(vp->ID); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - - // Must be a window so DockSpace has somewhere to live - ImGui::Begin("##MainDockspace", nullptr, flags); - - ImGui::PopStyleVar(2); - - beginSimManager("##MainDockspaceChild"); - - ImGuiID dock_id = ImGui::GetID("MainDockspaceID"); - ImGui::DockSpace(dock_id, ImVec2(0, 0), ImGuiDockNodeFlags_PassthruCentralNode); - - endSimManager(); - - ImGui::End(); - } - - // Viewport Window - void SimManager::drawViewportWindow() { - ImGui::Begin("Viewport", nullptr, - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse); - - beginSimManager("##ViewportBody"); - - // --- Tabs: Single / Quad --- - if (ImGui::BeginTabBar("ViewportTabs", ImGuiTabBarFlags_None)) { - const bool singleSelected = ImGui::BeginTabItem("Single"); - if (singleSelected) { - // Restore to Manual view when switching back from Quad - if (_impl->viewMode == Impl::ViewMode::Quad) { - _impl->activeView = gui::ViewID::Manual; - } - _impl->viewMode = Impl::ViewMode::Single; - ImGui::EndTabItem(); - } - - const bool quadSelected = ImGui::BeginTabItem("Quad"); - if (quadSelected) { - _impl->viewMode = Impl::ViewMode::Quad; - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - } - - // Everything below tabs is render output - _isHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); - - ImVec2 panel = ImGui::GetContentRegionAvail(); - - // Pixel size (framebuffer coords) - int vpW = (int)(panel.x); - int vpH = (int)(panel.y); - vpW = std::max(1, vpW); - vpH = std::max(1, vpH); - - // Update DISPLAY size only - if ((int)_displaySize.x != vpW || (int)_displaySize.y != vpH) { - _displaySize = { (float)vpW, (float)vpH }; - - // invalidate only display cached sizes so post buffers resize - for (auto& v : _impl->_views) { - v.displayW = 0; - v.displayH = 0; - } - //LOG_INFO("Viewport display size updated to %dx%d", vpW, vpH); - } - - // --- Render + Present --- - if (_impl->viewMode == Impl::ViewMode::Quad) { - int halfW = std::max(1, vpW / 2); - int halfH = std::max(1, vpH / 2); - - _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Top], halfW, halfH); - _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Front], halfW, halfH); - _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Right], halfW, halfH); - _impl->renderView(*this, _impl->_views[(size_t)gui::ViewID::Follow], halfW, halfH); - - // Stable 2x2 layout - ImVec2 avail = ImGui::GetContentRegionAvail(); - ImVec2 cell = ImVec2(avail.x * 0.5f, avail.y * 0.5f); - - // Helper to draw each cell with the same pattern - auto drawCell = [&](const char* childId, gui::ViewID id, bool sameLine) { - if (sameLine) ImGui::SameLine(); - ImGui::BeginChild(childId, cell, false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); - - auto& v = _impl->_views[(size_t)id]; - ImVec2 inner = ImGui::GetContentRegionAvail(); - - ImGui::Image((ImTextureID)(intptr_t)v.post->getTexture(), inner, ImVec2(0, 1), ImVec2(1, 0)); - - // Set active view when clicking inside the quad cell - if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - _impl->activeView = id; - } - - // Handle scroll zoom on hovered quad cell - if (ImGui::IsWindowHovered()) { - float scrollY = ImGui::GetIO().MouseWheel; - if (scrollY != 0.0f) { - v.cam->onMouseWheel((double)scrollY); - } - } - - // Visual indication: draw a border around the active cell - if (_impl->activeView == id) { - ImDrawList* dl = ImGui::GetWindowDrawList(); - ImVec2 p0 = ImGui::GetWindowPos(); - ImVec2 p1 = ImVec2(p0.x + ImGui::GetWindowSize().x, p0.y + ImGui::GetWindowSize().y); - dl->AddRect(p0, p1, IM_COL32(255, 200, 0, 180), 4.0f, 0, 2.0f); - } - - ImGui::EndChild(); - }; - - drawCell("##Top", gui::ViewID::Top, false); - drawCell("##Front", gui::ViewID::Front, true); - drawCell("##Right", gui::ViewID::Right, false); - drawCell("##Follow", gui::ViewID::Follow, true); - } - else { - auto& v = _impl->_views[static_cast(_impl->activeView)]; - _impl->renderView(*this, v, vpW, vpH); - - ImGui::Image((ImTextureID)(intptr_t)v.post->getTexture(), panel, ImVec2(0, 1), ImVec2(1, 0)); - } - - endSimManager(); - - ImGui::End(); - } - - // Begin Control Panel Helper - void SimManager::beginSimManager(const char* id) { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 10.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 5.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 8.0f)); - - ImGui::BeginChild(id, ImVec2(0, 0), true, - ImGuiChildFlags_AlwaysUseWindowPadding | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse); - } - - // End Control Panel Helper - void SimManager::endSimManager() { - ImGui::EndChild(); - ImGui::PopStyleVar(4); - } -} \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Scene/Object.cpp b/DSFE_App/DSFE_GUI/src/Scene/Object.cpp index 8a4758e7..e2e78680 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Object.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Object.cpp @@ -9,7 +9,6 @@ #include "Physics/PhysicsState.h" #include "Scene/ObjectID.h" -#include "Scene/Input.h" #include "Scene/Mesh.h" using namespace mathlib; @@ -45,25 +44,25 @@ namespace scene { // Update method passes material properties to the shader void Object::update(shaders::Shader* shader) { if (_mesh) _mesh->update(shader); } - // Handle mouse movement for object manipulation + // Handle mouse movement for object manipulation **DECREPATED** void Object::onMouseMove(double x, double y, eInputButton button) { glm::vec2 pos2d{ x, y }; glm::vec2 delta = pos2d - _lastMousePos; _lastMousePos = pos2d; - if (button == eInputButton::Right) { - delta *= 0.004f; - float yaw = delta.x; - float pitch = -delta.y; + //if (button == eInputButton::Right) { + // delta *= 0.004f; + // float yaw = delta.x; + // float pitch = -delta.y; - glm::quat qYaw = glm::angleAxis(yaw, glm::vec3(0.0f, 1.0f, 0.0f)); - glm::vec3 right = transform.rotQ * glm::vec3(1.0f, 0.0f, 0.0f); - glm::quat qPitch = glm::angleAxis(pitch, glm::normalize(right)); + // glm::quat qYaw = glm::angleAxis(yaw, glm::vec3(0.0f, 1.0f, 0.0f)); + // glm::vec3 right = transform.rotQ * glm::vec3(1.0f, 0.0f, 0.0f); + // glm::quat qPitch = glm::angleAxis(pitch, glm::normalize(right)); - } - else if (button == eInputButton::Left) { - delta *= 0.003f; - transform.position += glm::vec3(delta.x * _distance, -delta.y * _distance, 0.0f); - } + //} + //else if (button == eInputButton::Left) { + // delta *= 0.003f; + // transform.position += glm::vec3(delta.x * _distance, -delta.y * _distance, 0.0f); + //} } } diff --git a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp index 3f2b6845..9487201d 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp @@ -11,11 +11,6 @@ extern "C" core::ISimulationCore* CreateSimulationCore_v1(); extern "C" void DestroySimulationCore(core::ISimulationCore*); -#ifdef __gl_h_ -#undef __gl_h_ -#endif -#include - #include "Manager/SimImplementation.h" #include "Platform/KeyCode.h" @@ -76,7 +71,7 @@ namespace gui { // CONSTRUCTOR & DESTRUCTOR // -------------------------------------------------- - SimManager::SimManager() : _internalSize(1280, 720), _displaySize(1.0f, 1.0f), _backgroundColour(0.18f, 0.18f, 0.20f), + SimManager::SimManager() : _internalSize(1920, 1080), _displaySize(1.0f, 1.0f), _backgroundColour(0.18f, 0.18f, 0.20f), _backgroundAlpha(1.0f), _impl(std::make_unique(*this)), _core(std::make_unique()), _studyRunner(std::make_unique(makeCoreFactory, std::thread::hardware_concurrency() > 1 ? std::thread::hardware_concurrency() - 1 : 1)) { _core->setRobotSystem(_impl->_robotSystem.get()); @@ -174,7 +169,6 @@ namespace gui { if (_impl->_robotSystem && hasRobot()) { _impl->_robotRenderer->applyTransforms(_impl->_robotSystem->model(), _impl->_robotSystem->worldTransforms()); } - _fpsCounter.update(); auto& view = _impl->_views[static_cast(_impl->activeView)]; _impl->renderView(*this, view, w, h); glBindFramebuffer(GL_FRAMEBUFFER, _presentationFBO); @@ -262,6 +256,8 @@ namespace gui { if (pressedKeys.contains(eKeyCode::LShift)) { processMovementKey((int)eKeyCode::LShift, kspd); } } +// Handle mouse look (camera rotation) based on mouse movement. **OLD LOGIC FOR IMGUI AND GLFW** +#pragma region DecrepatedInputLogic void gui::SimManager::handleMouseLook(double xpos, double ypos, bool mouseCaptured) { if (_impl->viewMode == Impl::ViewMode::Quad) { return; } // No mouse look in quad view scene::Camera* cam = _impl->_views[static_cast(_impl->activeView)].cam.get(); @@ -281,10 +277,9 @@ namespace gui { _lastMousePos = { static_cast(xpos), static_cast(ypos) }; if (ctrlMode == ControlMode::Camera) { cam->processMouseMovement(static_cast(xoffset), static_cast(yoffset)); } - else if (ctrlMode == ControlMode::Object && _impl->_selectedObject) { _impl->_selectedObject->onMouseMove(xpos, ypos, scene::eInputButton::Right); } + else if (ctrlMode == ControlMode::Object && _impl->_selectedObject) { return; /*_impl->_selectedObject->onMouseMove(xpos, ypos, scene::eInputButton::Right);*/ } } - - void SimManager::onMouseMove(double x, double y, scene::eInputButton button) { + void SimManager::onMouseMove(double x, double y) { scene::Camera* cam = _impl->_views[static_cast(_impl->activeView)].cam.get(); glm::vec2 pos2d{ x, y }; glm::vec2 delta = pos2d - _lastMousePos; @@ -298,10 +293,9 @@ namespace gui { return; } - if (ctrlMode == ControlMode::Camera) { cam->onMouseMove(x, y, button); } - else if (ctrlMode == ControlMode::Object && _impl->_selectedObject) { _impl->_selectedObject->onMouseMove(x, y, button); } + //if (ctrlMode == ControlMode::Camera) { cam->onMouseMove(x, y, button); } + //else if (ctrlMode == ControlMode::Object && _impl->_selectedObject) { _impl->_selectedObject->onMouseMove(x, y, button); } } - void SimManager::onMouseWheel(double delta) { scene::Camera* cam = _impl->_views[static_cast(_impl->activeView)].cam.get(); auto* obj = _impl->_selectedObject; @@ -317,4 +311,5 @@ namespace gui { } void gui::SimManager::resetMouseDelta() { _firstMouse = true; } +#pragma endregion } \ No newline at end of file From 45c3405a3024c26657e0b9ee0b8400470e728d40 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 08:39:19 +0100 Subject: [PATCH 045/110] fixes: Removed CMake fetch and includes of previously removed libraries --- CMakeLists.txt | 23 --------------------- DSFE_App/DSFE_GUI/CMakeLists.txt | 35 -------------------------------- 2 files changed, 58 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a4c4881a..2a712153 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,13 +22,6 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(eigen) -FetchContent_Declare( - glfw - GIT_REPOSITORY https://github.com/glfw/glfw.git - GIT_TAG 3.4 -) -FetchContent_MakeAvailable(glfw) - # Fetch GLM from GitHub FetchContent_Declare( glm @@ -57,22 +50,6 @@ FetchContent_Declare( set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) -FetchContent_Declare( - imgui - GIT_REPOSITORY https://github.com/ocornut/imgui.git - GIT_TAG docking - GIT_SHALLOW TRUE -) -FetchContent_MakeAvailable(imgui) - -FetchContent_Declare( - implot - GIT_REPOSITORY https://github.com/epezent/implot.git - GIT_TAG v0.17 - GIT_SHALLOW TRUE -) -FetchContent_MakeAvailable(implot) - FetchContent_Declare( nlohmann_json GIT_REPOSITORY https://github.com/nlohmann/json.git diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 5054bb97..f058115f 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -22,7 +22,6 @@ find_package(Qt6 REQUIRED COMPONENTS target_compile_definitions(DSFE_GUI PRIVATE DSFE_GUI_EXPORTS - IMGUI_ENABLE_DOCKING GLM_ENABLE_EXPERIMENTAL ) @@ -55,7 +54,6 @@ set(RENDER_SRC set(SCENE_SRC src/Scene/AxisOrientator.cpp src/Scene/Camera.cpp - src/Scene/Input.cpp src/Scene/Mesh.cpp src/Scene/Object.cpp src/Scene/SimulationManager.cpp @@ -67,7 +65,6 @@ set(MANAGER_SRC src/Scene/Manager/SimObjects.cpp src/Scene/Manager/SimRendering.cpp src/Scene/Manager/SimRobots.cpp - src/Scene/Manager/SimUI.cpp src/Scene/Manager/SimViewport.cpp ) @@ -114,35 +111,6 @@ target_sources(DSFE_GUI PRIVATE ${QT_RESOURCES} ) -set(imfilebrowser_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/include/imfilebrowser) - -add_library(imgui STATIC - ${imgui_SOURCE_DIR}/imgui.cpp - ${imgui_SOURCE_DIR}/imgui_draw.cpp - ${imgui_SOURCE_DIR}/imgui_tables.cpp - ${imgui_SOURCE_DIR}/imgui_widgets.cpp - - ${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp - ${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp -) - -target_include_directories(imgui PUBLIC - ${imgui_SOURCE_DIR} - ${imgui_SOURCE_DIR}/backends -) - -target_link_libraries(imgui PUBLIC glfw OpenGL::GL) - -add_library(implot STATIC - ${implot_SOURCE_DIR}/implot.cpp - ${implot_SOURCE_DIR}/implot_items.cpp -) - -target_include_directories(implot PUBLIC - ${implot_SOURCE_DIR} - ${imgui_SOURCE_DIR} -) - target_include_directories(DSFE_GUI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} @@ -163,13 +131,10 @@ target_link_libraries(DSFE_GUI Threads::Threads PRIVATE - implot stb assimp - glfw glad glm::glm - imgui OpenGL::GL Qt6::Core Qt6::Gui From 23bd1af1c0c231789c23d61ee6345dc7e60c189c Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 09:44:29 +0100 Subject: [PATCH 046/110] feat: Added new `MeshPresentationBuilder` class with a dedicated `build()` method for general objects --- DSFE_App/DSFE_GUI/CMakeLists.txt | 8 ++++--- .../include/Assets/MeshPresentationBuilder.h | 23 +++++++++++++++++++ .../src/Assets/MeshPresentationBuilder.cpp | 16 +++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 DSFE_App/DSFE_GUI/include/Assets/MeshPresentationBuilder.h create mode 100644 DSFE_App/DSFE_GUI/src/Assets/MeshPresentationBuilder.cpp diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index f058115f..031a2de9 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -85,6 +85,10 @@ set(ROBOT_SRC src/Robots/RobotRenderer.cpp ) +set(OBJECT_SRC + src/Assets/MeshPresentationBuilder.cpp +) + set(PLATFORM_SRC src/Application.cpp ) @@ -106,6 +110,7 @@ target_sources(DSFE_GUI PRIVATE ${MANAGER_SRC} ${WIDGETS_SRC} ${ROBOT_SRC} + ${OBJECT_SRC} ${PLATFORM_SRC} ${ASSETS_SRC} ${QT_RESOURCES} @@ -118,9 +123,6 @@ target_include_directories(DSFE_GUI ${CMAKE_CURRENT_SOURCE_DIR}/include/MainWindow ${CMAKE_CURRENT_SOURCE_DIR}/include/Scene ${CMAKE_CURRENT_SOURCE_DIR}/include/MainWindow/style - - PRIVATE - ${imfilebrowser_ROOT} ) # Link DSFE_Core against its dependencies diff --git a/DSFE_App/DSFE_GUI/include/Assets/MeshPresentationBuilder.h b/DSFE_App/DSFE_GUI/include/Assets/MeshPresentationBuilder.h new file mode 100644 index 00000000..f14fc7f6 --- /dev/null +++ b/DSFE_App/DSFE_GUI/include/Assets/MeshPresentationBuilder.h @@ -0,0 +1,23 @@ +// DSFE_GUI MeshPresentationBuilder.h +#pragma once + + +#include +#include + +namespace scene { + class Mesh; + class Object; +} + +namespace presentation { + struct MeshRenderBinding { + std::vector> owndObjs; + std::vector visObjs; + }; + + class MeshPresentationBuilder { + public: + MeshRenderBinding build(const std::vector>& meshes); + }; +} // namespace presentation \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/Assets/MeshPresentationBuilder.cpp b/DSFE_App/DSFE_GUI/src/Assets/MeshPresentationBuilder.cpp new file mode 100644 index 00000000..98ab2463 --- /dev/null +++ b/DSFE_App/DSFE_GUI/src/Assets/MeshPresentationBuilder.cpp @@ -0,0 +1,16 @@ +// DSFE_GUI MeshPresentationBuilder.cpp +#include "Assets/MeshPresentationBuilder.h" +#include "Scene/Object.h" + +namespace presentation { + MeshRenderBinding MeshPresentationBuilder::build(const std::vector>& meshes) { + MeshRenderBinding binding; + for (const auto& mesh : meshes) { + auto obj = std::make_unique(mesh); + obj->category = scene::ObjectCategory::General; + binding.visObjs.push_back(obj.get()); + binding.owndObjs.push_back(std::move(obj)); + } + return binding; + } +} // namespace presentation \ No newline at end of file From add51c7cf89395877742c6b5c5529f2cd23ec54c Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 11:58:38 +0100 Subject: [PATCH 047/110] chore: Code cleanup of unused / old code in GUI files * Need to actually RE-ADD physics logic for single objects, maybe add this into PxM rather than Core, and step the logic in Core only. --- DSFE_App/DSFE_GUI/include/Scene/Camera.h | 33 +----------- .../include/Scene/Manager/SimImplementation.h | 36 ++++++++----- DSFE_App/DSFE_GUI/include/Scene/Mesh.h | 2 +- DSFE_App/DSFE_GUI/include/Scene/Object.h | 3 -- .../include/Scene/SimulationManager.h | 1 - DSFE_App/DSFE_GUI/src/Assets/MeshLoader.cpp | 52 +++++-------------- .../src/MainWindow/DSFE_MainWindow.cpp | 4 +- DSFE_App/DSFE_GUI/src/Scene/Camera.cpp | 6 +-- DSFE_App/DSFE_GUI/src/Scene/Object.cpp | 24 +-------- .../DSFE_GUI/src/Scene/SimulationManager.cpp | 23 +------- 10 files changed, 44 insertions(+), 140 deletions(-) diff --git a/DSFE_App/DSFE_GUI/include/Scene/Camera.h b/DSFE_App/DSFE_GUI/include/Scene/Camera.h index e45acbb1..b9b05154 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/Camera.h +++ b/DSFE_App/DSFE_GUI/include/Scene/Camera.h @@ -4,9 +4,7 @@ #include #include #include - #include "Scene/Element.h" - #include "Platform/Logger.h" namespace scene { @@ -57,35 +55,7 @@ namespace scene { updateViewMatrix(); } - void onMouseWheel(double delta) { - // Negative delta = scroll forward = zoom in (decrease distance) - setDistance((float)(-delta * 0.5f)); - } - - //void onMouseMove(double x, double y) { - // glm::vec2 pos2d{ x, y }; - - // if () { - // glm::vec2 delta = (pos2d - _currentPos2D) * 0.004f; - - // float sign = getUp().y < 0 ? -1.0f : 1.0f; - - // _yaw += sign * delta.x * _rotationSpeed; - // _pitch += delta.y * _rotationSpeed; - - // updateViewMatrix(); - // } - // else if (false) { - // glm::vec2 delta = (pos2d - _currentPos2D) * 0.003f; - - // _focus += -getRight() * delta.x * _distance; - // _focus += getUp() * delta.y * _distance; - - // updateViewMatrix(); - // } - - // _currentPos2D = pos2d; - //} + void onMouseWheel(double delta) { setDistance((float)(-delta * 0.5f)); } void startFollow(const glm::vec3& pos, const glm::quat& rot, const glm::vec3& offset); void clearFollow() { _following = false; } @@ -95,7 +65,6 @@ namespace scene { } void updateViewMatrix(); - void fall(); void moveForward(float delta); diff --git a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h index b0f7bed2..2bd91cdb 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h +++ b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h @@ -109,7 +109,6 @@ namespace gui { // Robot System std::unique_ptr _robotSystem; // simulation - // Robot Rendering std::unique_ptr _robotRenderer; // rendering RobotRenderBinding _currentBinding; // current render binding @@ -292,31 +291,40 @@ namespace gui { void buildRobotPresentationFromModel(const robots::RobotModel& model, SimManager& owner) { clearRobotPresentation(); - RobotPresentationBuilder builder; RobotRenderBinding binding = builder.build(model); - - for (auto& owned : binding.ownedObjects) { - _objects.push_back(std::move(owned)); - } - + for (auto& owned : binding.ownedObjects) { _objects.push_back(std::move(owned)); } _robotRenderer->bind(binding); - for (const auto& [linkName, visuals] : binding.linkVisuals) { auto& target = _linkToObjects[linkName]; - for (auto* obj : visuals) { if (!obj) { continue; } - target.push_back(obj); - - if (!_primaryLinkObject.contains(linkName)) { - _primaryLinkObject[linkName] = obj; - } + if (!_primaryLinkObject.contains(linkName)) { _primaryLinkObject[linkName] = obj; } } } } + //void clearObjectPresentation() { _objects.clear(); } + //void buildObjectPresentation(const std::vector>& meshes) { + // clearObjectPresentation(); + // presentation::MeshPresentationBuilder builder; + // presentation::MeshRenderBinding binding = builder.build(meshes); + // for (auto& owned : binding.owndObjs) { + // owned->state.q = mathlib::Quat(1.0, 0.0, 0.0, 0.0); + // owned->state.linearVelocity = mathlib::Vec3::Zero(); + // owned->state.angularVelocity = mathlib::Vec3::Zero(); + // owned->state.forces = mathlib::Vec3::Zero(); + // owned->state.torques = mathlib::Vec3::Zero(); + // owned->state.mass = 1.0; + // owned->state.damping = 0.0; + // owned->state.inertia = mathlib::Mat3::Identity(); + // owned->visible = true; + // owned->internal = false; + // _objects.push_back(std::move(owned)); + // } + //} + // Random number generation for SSAO kernel and noise std::mt19937 _rng{ std::random_device{}() }; std::uniform_real_distribution _uni{ -1.0f, 1.0f }; diff --git a/DSFE_App/DSFE_GUI/include/Scene/Mesh.h b/DSFE_App/DSFE_GUI/include/Scene/Mesh.h index 11b1831e..57251122 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/Mesh.h +++ b/DSFE_App/DSFE_GUI/include/Scene/Mesh.h @@ -114,7 +114,7 @@ namespace scene { std::unique_ptr _rndrBffrMngr; int id = 0; - std::string _name = "obj" + id; + std::string _name = "obj_" + id; // Default material properties glm::vec3 _albedo = glm::vec3(0.4, 0.4, 0.4); diff --git a/DSFE_App/DSFE_GUI/include/Scene/Object.h b/DSFE_App/DSFE_GUI/include/Scene/Object.h index 3fb3966c..e2172bc7 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/Object.h +++ b/DSFE_App/DSFE_GUI/include/Scene/Object.h @@ -15,8 +15,6 @@ #include "EngineLib/LogMacros.h" namespace scene { - enum class eInputButton; - class Input; class Mesh; enum class ObjectCategory { General, RobotLink }; @@ -125,7 +123,6 @@ namespace scene { } void onMouseWheel(double delta) { _distance += (float)delta * 0.5f; } - void onMouseMove(double x, double y, eInputButton button); void setLastMousePos(const glm::vec2& pos) { _lastMousePos = pos; } glm::vec2 getLastMousePos() const { return _lastMousePos; } diff --git a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h index f433a54b..4da0d6cf 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h +++ b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h @@ -274,7 +274,6 @@ namespace gui { void processMovementKey(int key, float delta); void handleContinuousMovement(const std::unordered_set& pressedKeys, float dt); void handleMouseLook(double xpos, double ypos, bool mouseCaptured); - void onMouseMove(double x, double y); void onMouseWheel(double delta); void resetMouseDelta(); diff --git a/DSFE_App/DSFE_GUI/src/Assets/MeshLoader.cpp b/DSFE_App/DSFE_GUI/src/Assets/MeshLoader.cpp index 7187af35..d321f4c6 100644 --- a/DSFE_App/DSFE_GUI/src/Assets/MeshLoader.cpp +++ b/DSFE_App/DSFE_GUI/src/Assets/MeshLoader.cpp @@ -13,7 +13,6 @@ namespace assets { // Load a mesh from the specified file path and return a vector of shared pointers to Mesh objects std::vector> MeshLoader::load(const std::string& filepath) { _imported.clear(); - const uint32_t importFlags = aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | @@ -30,10 +29,8 @@ namespace assets { LOG_ERROR("Failed to load mesh from %s: %s", filepath.c_str(), importer.GetErrorString()); return {}; } - glm::mat4 rootTransform(1.0f); processNode(scene->mRootNode, scene, rootTransform); - return _imported; } @@ -48,22 +45,18 @@ namespace assets { a.a3, a.b3, a.c3, a.d3, a.a4, a.b4, a.c4, a.d4 ); - glm::mat4 globalTransform = parentTransform * nodeTransform; // Process meshes in this node - for (unsigned int i = 0; i < node->mNumMeshes; i++) - { + for (unsigned int i = 0; i < node->mNumMeshes; i++) { aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; - auto m = processMesh(mesh, scene); + auto m = processMesh(mesh, scene); m->localTransform = globalTransform; _imported.push_back(std::move(m)); } // Recursively process child nodes - for (unsigned int i = 0; i < node->mNumChildren; i++) { - processNode(node->mChildren[i], scene, globalTransform); - } + for (unsigned int i = 0; i < node->mNumChildren; i++) { processNode(node->mChildren[i], scene, globalTransform); } } // Helper method to process an Assimp mesh and convert it into a shared pointer to a scene::Mesh object @@ -79,21 +72,15 @@ namespace assets { ? glm::vec3(mesh->mNormals[i].x, mesh->mNormals[i].y, mesh->mNormals[i].z) : glm::vec3(0.0f, 1.0f, 0.0f); - if (mesh->mTextureCoords[0]) { - vh._texCoord = glm::vec2(mesh->mTextureCoords[0][i].x, - mesh->mTextureCoords[0][i].y); - } + if (mesh->mTextureCoords[0]) { vh._texCoord = glm::vec2(mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y); } else { vh._texCoord = glm::vec2(0.0f); } - result->addVertex(vh); } // Get the indices for the faces of the mesh and add them to the result mesh, applying the index offset to account for previously added vertices for (unsigned int i = 0; i < mesh->mNumFaces; ++i) { const aiFace& face = mesh->mFaces[i]; - for (unsigned int j = 0; j < face.mNumIndices; ++j) { - result->addVertexIndex(face.mIndices[j] + indexOffset); - } + for (unsigned int j = 0; j < face.mNumIndices; ++j) { result->addVertexIndex(face.mIndices[j] + indexOffset); } } // Extract material properties from Assimp @@ -102,42 +89,31 @@ namespace assets { // Albedo / diffuse colour aiColor4D diffuse(0.4f, 0.4f, 0.4f, 1.0f); - if (mat->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse) == AI_SUCCESS || - mat->Get(AI_MATKEY_BASE_COLOR, diffuse) == AI_SUCCESS) { + if (mat->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse) == AI_SUCCESS || mat->Get(AI_MATKEY_BASE_COLOR, diffuse) == AI_SUCCESS) { result->setAlbedo(glm::vec3(diffuse.r, diffuse.g, diffuse.b)); } // Metallic (GLTF/PBR key, fallback to reflectivity) float metallic = 0.0f; - if (mat->Get(AI_MATKEY_METALLIC_FACTOR, metallic) == AI_SUCCESS) { - result->setMetallic(metallic); - } else { + if (mat->Get(AI_MATKEY_METALLIC_FACTOR, metallic) == AI_SUCCESS) { result->setMetallic(metallic); } else { float reflectivity = 0.0f; - if (mat->Get(AI_MATKEY_REFLECTIVITY, reflectivity) == AI_SUCCESS) { - result->setMetallic(glm::clamp(reflectivity, 0.0f, 1.0f)); - } + if (mat->Get(AI_MATKEY_REFLECTIVITY, reflectivity) == AI_SUCCESS) { result->setMetallic(glm::clamp(reflectivity, 0.0f, 1.0f)); } } // Roughness (GLTF/PBR key, fallback from shininess) float roughness = 0.5f; - if (mat->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness) == AI_SUCCESS) { - result->setRoughness(roughness); - } else { + if (mat->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness) == AI_SUCCESS) { result->setRoughness(roughness); } + else { float shininess = 0.0f; - if (mat->Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS && shininess > 0.0f) { - // Convert Phong shininess to roughness (approximate) - result->setRoughness(glm::clamp(1.0f - glm::sqrt(shininess / 1000.0f), 0.05f, 1.0f)); + if (mat->Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS && shininess > 0.0f) { + result->setRoughness(glm::clamp(1.0f - glm::sqrt(shininess / 1000.0f), 0.05f, 1.0f)); // Convert Phong shininess to roughness (approximate) } } } // Set mesh name from Assimp - if (mesh->mName.length > 0) { - result->setName(std::string(mesh->mName.C_Str())); - } - - // Update the index offset for the next mesh - result->init(); + if (mesh->mName.length > 0) { result->setName(std::string(mesh->mName.C_Str())); } + result->init(); // Initialise the mesh (create GPU buffers, etc.) return result; } } \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index af18e8ef..4efb9e31 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -58,7 +58,7 @@ namespace window { LOG_INFO("Menu clicked: File -> Open -> Project"); }); openMenu->addSeparator(); - auto* openScriptAction = openMenu->addAction("Script (*.dsl *.txt)"); + auto* openScriptAction = openMenu->addAction("Script"); connect(openScriptAction, &QAction::triggered, this, [this]() { QString fileName = QFileDialog::getOpenFileName(nullptr, "Open Script", QString::fromStdString((paths::assets() / "DSLScripts").string()), "DSL Script Files (*.dsl);;Text Files (*.txt)"); if (fileName.isEmpty()) { return; } @@ -125,7 +125,7 @@ namespace window { auto* loadMeshAction = projectMenu->addAction("Load Mesh"); connect(loadMeshAction, &QAction::triggered, this, [this]() { LOG_INFO("Menu clicked: Project -> Load Mesh"); - QString path = QFileDialog::getOpenFileName(nullptr, "Select Mesh File", QString::fromStdString((paths::assets() / "objects" / "Shapes").string()), "Mesh Files (*.obj *.fbx *.gltf *.dae *.stl"); + QString path = QFileDialog::getOpenFileName(nullptr, "Select Mesh File", QString::fromStdString((paths::assets() / "objects" / "Shapes").string()), "Mesh Files(*.obj * .fbx * .gltf * .dae * .stl)"); if (path.isEmpty()) { return; } _sim->loadMesh(path.toStdString()); }); diff --git a/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp b/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp index 5ec66667..dffa27a9 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Camera.cpp @@ -6,9 +6,7 @@ #endif #include #include "Scene/Camera.h" - #include "Platform/KeyCode.h" - #include "EngineLib/LogMacros.h" namespace scene { @@ -187,8 +185,8 @@ namespace scene { if (len < 1e-6f) return; dir /= len; - // Match your yaw/pitch convention: - // Your default: _yaw = -pi/2 gives forward (0,0,-1). + // Match yaw/pitch convention: + // Default: _yaw = -pi/2 gives forward (0,0,-1). // That corresponds to: // forward.x = cos(yaw)*cos(pitch) // forward.y = sin(pitch) diff --git a/DSFE_App/DSFE_GUI/src/Scene/Object.cpp b/DSFE_App/DSFE_GUI/src/Scene/Object.cpp index e2e78680..db849f79 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Object.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Object.cpp @@ -25,7 +25,7 @@ namespace scene { // --- Object Implementation --- - // Constructor initializes transform and physics state + // Constructor initialises transform and physics state Object::Object(std::shared_ptr mesh) : _mesh(std::move(mesh)) { transform.position = glm::vec3(0.0f); transform.rotQ = glm::quat{ 1.0f, 0.0f, 0.0f, 0.0f }; @@ -43,26 +43,4 @@ namespace scene { // Update method passes material properties to the shader void Object::update(shaders::Shader* shader) { if (_mesh) _mesh->update(shader); } - - // Handle mouse movement for object manipulation **DECREPATED** - void Object::onMouseMove(double x, double y, eInputButton button) { - glm::vec2 pos2d{ x, y }; - glm::vec2 delta = pos2d - _lastMousePos; - _lastMousePos = pos2d; - - //if (button == eInputButton::Right) { - // delta *= 0.004f; - // float yaw = delta.x; - // float pitch = -delta.y; - - // glm::quat qYaw = glm::angleAxis(yaw, glm::vec3(0.0f, 1.0f, 0.0f)); - // glm::vec3 right = transform.rotQ * glm::vec3(1.0f, 0.0f, 0.0f); - // glm::quat qPitch = glm::angleAxis(pitch, glm::normalize(right)); - - //} - //else if (button == eInputButton::Left) { - // delta *= 0.003f; - // transform.position += glm::vec3(delta.x * _distance, -delta.y * _distance, 0.0f); - //} - } } diff --git a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp index 9487201d..281ee456 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp @@ -201,11 +201,9 @@ namespace gui { const glm::mat4& world = toGlm(T[i]); for (scene::Object* obj : it->second) { - if (!obj) continue; - + if (!obj) { continue; } glm::vec3 pos = glm::vec3(world[3]); glm::quat q = glm::quat_cast(world); - obj->transform.position = pos; obj->transform.rotQ = q; } @@ -257,7 +255,6 @@ namespace gui { } // Handle mouse look (camera rotation) based on mouse movement. **OLD LOGIC FOR IMGUI AND GLFW** -#pragma region DecrepatedInputLogic void gui::SimManager::handleMouseLook(double xpos, double ypos, bool mouseCaptured) { if (_impl->viewMode == Impl::ViewMode::Quad) { return; } // No mouse look in quad view scene::Camera* cam = _impl->_views[static_cast(_impl->activeView)].cam.get(); @@ -279,23 +276,6 @@ namespace gui { if (ctrlMode == ControlMode::Camera) { cam->processMouseMovement(static_cast(xoffset), static_cast(yoffset)); } else if (ctrlMode == ControlMode::Object && _impl->_selectedObject) { return; /*_impl->_selectedObject->onMouseMove(xpos, ypos, scene::eInputButton::Right);*/ } } - void SimManager::onMouseMove(double x, double y) { - scene::Camera* cam = _impl->_views[static_cast(_impl->activeView)].cam.get(); - glm::vec2 pos2d{ x, y }; - glm::vec2 delta = pos2d - _lastMousePos; - _lastMousePos = pos2d; - - if (_impl->viewMode == Impl::ViewMode::Quad) { return; } // No mouse drag in quad view - - if (!_isHovered) { - cam->setCurrentPos2D(pos2d); - _impl->_selectedObject->setLastMousePos(pos2d); - return; - } - - //if (ctrlMode == ControlMode::Camera) { cam->onMouseMove(x, y, button); } - //else if (ctrlMode == ControlMode::Object && _impl->_selectedObject) { _impl->_selectedObject->onMouseMove(x, y, button); } - } void SimManager::onMouseWheel(double delta) { scene::Camera* cam = _impl->_views[static_cast(_impl->activeView)].cam.get(); auto* obj = _impl->_selectedObject; @@ -311,5 +291,4 @@ namespace gui { } void gui::SimManager::resetMouseDelta() { _firstMouse = true; } -#pragma endregion } \ No newline at end of file From 1533216567ebdc21dd2a5e80c02346ce24343d93 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 12:00:01 +0100 Subject: [PATCH 048/110] fixes: Updated `buildRobotMenu` to directly call the core `loadRobot()` rather than the internal simMananger version, as it eliminates crashes from uninitialise GL pointers --- DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index 4efb9e31..afd39d0a 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -1,6 +1,7 @@ //DSFE_GUI DSFE_MainWindow.cpp #include "MainWindow/DSFE_MainWindow.h" #include "Scene/SimulationManager.h" +#include "Scene/SimulationCore.h" #include "Workspace/ProjectPage.h" #include "Widgets/DSLEditorWidget.h" @@ -322,7 +323,7 @@ namespace window { QAction* robotAction = familyMenus[family]->addAction(robotName); connect(robotAction, &QAction::triggered, this, [this, robotName]() { LOG_INFO("Menu clicked: Project -> Load Robot -> %s", robotName.toStdString().c_str()); - _sim->loadRobot(robotName.toStdString()); + _sim->simCore()->loadRobot(robotName.toStdString()); }); } } From 45ce847387e9ae1779eb6f0549f54de1925214f1 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 12:18:08 +0100 Subject: [PATCH 049/110] feat: Introduced new internal library in PxM, `PhysLib` * This will see some more work now, potentially moving over some of the "Robot" -> to be named `Multibody or something less specific but equally appropriate (or moving it into a multibody sub dir)" -> system methods --- PxMLib/CMakeLists.txt | 1 + PxMLib/MathLib/framework.h | 2 +- PxMLib/MathLib/pch.h | 1 - PxMLib/PhysLib/CMakeLists.txt | 22 ++++++++++++++++++++++ PxMLib/PhysLib/dllmain.cpp | 19 +++++++++++++++++++ PxMLib/PhysLib/framework.h | 6 ++++++ PxMLib/PhysLib/pch.cpp | 2 ++ PxMLib/PhysLib/pch.h | 24 ++++++++++++++++++++++++ 8 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 PxMLib/PhysLib/CMakeLists.txt create mode 100644 PxMLib/PhysLib/dllmain.cpp create mode 100644 PxMLib/PhysLib/framework.h create mode 100644 PxMLib/PhysLib/pch.cpp create mode 100644 PxMLib/PhysLib/pch.h diff --git a/PxMLib/CMakeLists.txt b/PxMLib/CMakeLists.txt index 988b2907..3f67732c 100644 --- a/PxMLib/CMakeLists.txt +++ b/PxMLib/CMakeLists.txt @@ -4,4 +4,5 @@ project(PxMLib LANGUAGES CXX) # Add projects within the DSFE_App directory add_subdirectory(MathLib) +add_subdirectory(PhysLib)` add_subdirectory(MathLib_Unit) diff --git a/PxMLib/MathLib/framework.h b/PxMLib/MathLib/framework.h index dca3ebf1..a2c75c3c 100644 --- a/PxMLib/MathLib/framework.h +++ b/PxMLib/MathLib/framework.h @@ -1,6 +1,6 @@ #pragma once -#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers // Windows Header Files #define NOMINMAX #include diff --git a/PxMLib/MathLib/pch.h b/PxMLib/MathLib/pch.h index 49d41c41..35608d93 100644 --- a/PxMLib/MathLib/pch.h +++ b/PxMLib/MathLib/pch.h @@ -7,7 +7,6 @@ #ifndef PCH_H #define PCH_H -// add headers that you want to pre-compile here #include "framework.h" // Standard Libraries diff --git a/PxMLib/PhysLib/CMakeLists.txt b/PxMLib/PhysLib/CMakeLists.txt new file mode 100644 index 00000000..e3231b64 --- /dev/null +++ b/PxMLib/PhysLib/CMakeLists.txt @@ -0,0 +1,22 @@ +# CMakeLists.txt for PhysLib +cmake_minimum_required(VERSION 3.17) +project(PhysLib LANGUAGES CXX) + +add_library(PhysLib STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/pch.cpp +) + +target_precompile_headers(PhysLib PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/pch.h +) + +target_include_directories(PhysLib + PUBLIC + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/ +) + +target_link_libraries(PhysLib PUBLIC + Eigen3::Eigen +) \ No newline at end of file diff --git a/PxMLib/PhysLib/dllmain.cpp b/PxMLib/PhysLib/dllmain.cpp new file mode 100644 index 00000000..f2665971 --- /dev/null +++ b/PxMLib/PhysLib/dllmain.cpp @@ -0,0 +1,19 @@ +// dllmain.cpp : Defines the entry point for the DLL application. +#include "pch.h" + +BOOL APIENTRY DllMain( HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved + ) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} + diff --git a/PxMLib/PhysLib/framework.h b/PxMLib/PhysLib/framework.h new file mode 100644 index 00000000..a2c75c3c --- /dev/null +++ b/PxMLib/PhysLib/framework.h @@ -0,0 +1,6 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +// Windows Header Files +#define NOMINMAX +#include diff --git a/PxMLib/PhysLib/pch.cpp b/PxMLib/PhysLib/pch.cpp new file mode 100644 index 00000000..c94d6873 --- /dev/null +++ b/PxMLib/PhysLib/pch.cpp @@ -0,0 +1,2 @@ +// pch.cpp: source file corresponding to the pre-compiled header +#include "pch.h" \ No newline at end of file diff --git a/PxMLib/PhysLib/pch.h b/PxMLib/PhysLib/pch.h new file mode 100644 index 00000000..35608d93 --- /dev/null +++ b/PxMLib/PhysLib/pch.h @@ -0,0 +1,24 @@ +// pch.h: This is a precompiled header file. +// Files listed below are compiled only once, improving build performance for future builds. +// This also affects IntelliSense performance, including code completion and many code browsing features. +// However, files listed here are ALL re-compiled if any one of them is updated between builds. +// Do not add files here that you will be updating frequently as this negates the performance advantage. + +#ifndef PCH_H +#define PCH_H + +#include "framework.h" + +// Standard Libraries +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif //PCH_H From c41e1064220b95ff39b40992c19db9d11970f2f2 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 12:18:54 +0100 Subject: [PATCH 050/110] feat: Initialised new `SingleBodySystem` method to allow PROPER modelling on single bodies * This is in preparation for something, but did not want to reuse the old logic from imgui days. --- DSFE_App/DSFE_Core/CMakeLists.txt | 38 ++++++++++++------- .../SingleBodySystem/SingleBodySystem.h | 38 +++++++++++++++++++ .../src/SingleBodySystem/SingleBodySystem.cpp | 1 + 3 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 DSFE_App/DSFE_Core/include/SingleBodySystem/SingleBodySystem.h create mode 100644 DSFE_App/DSFE_Core/src/SingleBodySystem/SingleBodySystem.cpp diff --git a/DSFE_App/DSFE_Core/CMakeLists.txt b/DSFE_App/DSFE_Core/CMakeLists.txt index 8cf99bf2..bec3de84 100644 --- a/DSFE_App/DSFE_Core/CMakeLists.txt +++ b/DSFE_App/DSFE_Core/CMakeLists.txt @@ -12,18 +12,34 @@ if (MSVC) target_compile_options(DSFE_Core PRIVATE /bigobj) endif() -set(CORE_SRC +set(DEP_CORE_SRC src/EngineCore.cpp - src/Numerics/IntegrationService.cpp +) + +set(SINGLE_BODY_SYS_SRC + src/SingleBodySystem/SingleBodySystem.cpp +) +set(MULTI_BODY_SYS_SRC src/Robots/RobotDynamics.cpp src/Robots/RobotKinematics.cpp src/Robots/RobotLoader.cpp src/Robots/RobotSystem.cpp src/Robots/TrajectoryManager.cpp src/Robots/RobotSimSnapshot.cpp +) + +set(PLATFORM_SRC + src/Platform/DataManager.cpp + src/Platform/Logger.cpp + src/Platform/Paths.cpp + src/Platform/StudyRunner.cpp + src/Analysis/Telemetry.cpp +) + +set(DSL_SRC src/Interpreter/Command.cpp src/Interpreter/CommandContext.cpp src/Interpreter/CommandFactory.cpp @@ -50,22 +66,16 @@ set(CORE_SRC src/Interpreter/Commands/WaitCmd.cpp ) -set(PLATFORM_SRC - src/Platform/DataManager.cpp - src/Platform/Logger.cpp - src/Platform/Paths.cpp - src/Platform/StudyRunner.cpp - - src/Analysis/Telemetry.cpp -) - -set(SCENE_SRC src/Scene/SimulationCore.cpp) +set(SIM_CORE_SRC src/Scene/SimulationCore.cpp) # Add sources for DSFE_Core, including ImGui files target_sources(DSFE_Core PRIVATE - ${CORE_SRC} + ${DEP_CORE_SRC} + ${SINGLE_BODY_SYS_SRC} + ${MULTI_BODY_SYS_SRC} ${PLATFORM_SRC} - ${SCENE_SRC} + ${DSL_SRC} + ${SIM_CORE_SRC} ) # Add Windows-specific source files diff --git a/DSFE_App/DSFE_Core/include/SingleBodySystem/SingleBodySystem.h b/DSFE_App/DSFE_Core/include/SingleBodySystem/SingleBodySystem.h new file mode 100644 index 00000000..1b458a5e --- /dev/null +++ b/DSFE_App/DSFE_Core/include/SingleBodySystem/SingleBodySystem.h @@ -0,0 +1,38 @@ +// DSFE_CORE SingleBodySystem.h +#pragma once + +#include "EngineCore.h" +#include +#include "Numerics/IntegrationService.h" + +namespace single_body_system { + class SingleBodySystem { + public: + SingleBodySystem() = default; + ~SingleBodySystem() = default; + + + + void step(double dt, double t); + + + integration::eIntegrationMethod getIntegrationMethod() const { return _curIntMethod; } + std::string getIntegratorName() const { return _integrator->IntegratorName(_curIntMethod); } + void setStandardIntegrator(integration::eIntegrationMethod m) { _curIntMethod = m; } + + integration::eAutoDiffIntegrationMethod AD_IntegrationMethod() const { return _curIntMethod_AD; } + std::string AD_integratorName() const { return _AD_integrator->IntegratorName(_curIntMethod_AD); } + void setADIntegrator(integration::eAutoDiffIntegrationMethod m) { _curIntMethod_AD = m; } + + private: + double _mass; + + /*std::unique_ptr _dynamics;*/ + + std::unique_ptr _integrator; + integration::eIntegrationMethod _curIntMethod{}; + + std::unique_ptr _AD_integrator; + integration::eAutoDiffIntegrationMethod _curIntMethod_AD{}; + }; +} \ No newline at end of file diff --git a/DSFE_App/DSFE_Core/src/SingleBodySystem/SingleBodySystem.cpp b/DSFE_App/DSFE_Core/src/SingleBodySystem/SingleBodySystem.cpp new file mode 100644 index 00000000..218a7124 --- /dev/null +++ b/DSFE_App/DSFE_Core/src/SingleBodySystem/SingleBodySystem.cpp @@ -0,0 +1 @@ +// DSFE_CORE SingleBodySystem.cpp \ No newline at end of file From 8c7334943d21877e8450fcf5d01ac88770e88aad Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 12:19:23 +0100 Subject: [PATCH 051/110] fixes: Removed accidental ` "`" ` --- PxMLib/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PxMLib/CMakeLists.txt b/PxMLib/CMakeLists.txt index 3f67732c..b2857283 100644 --- a/PxMLib/CMakeLists.txt +++ b/PxMLib/CMakeLists.txt @@ -4,5 +4,5 @@ project(PxMLib LANGUAGES CXX) # Add projects within the DSFE_App directory add_subdirectory(MathLib) -add_subdirectory(PhysLib)` +add_subdirectory(PhysLib) add_subdirectory(MathLib_Unit) From dd96bf26253040dfc64d8abb356a849357e6b69b Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 13:14:54 +0100 Subject: [PATCH 052/110] chore: Code cleanup --- DSFE_App/DSFE_Core/include/Robots/RobotModel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSFE_App/DSFE_Core/include/Robots/RobotModel.h b/DSFE_App/DSFE_Core/include/Robots/RobotModel.h index a3b6c4b7..3da80a1e 100644 --- a/DSFE_App/DSFE_Core/include/Robots/RobotModel.h +++ b/DSFE_App/DSFE_Core/include/Robots/RobotModel.h @@ -46,7 +46,7 @@ namespace robots { // Inertial properties of a link struct Inertial { - double mass = 0.0f; + double mass = 0.0; mathlib::Vec3 com_xyz{ 0.0,0.0,0.0 }; Inertia inertia{}; }; From d7287285ae406ff885c84fa4d90c122e68a1385e Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 13:15:25 +0100 Subject: [PATCH 053/110] feat: Added `SingleBodySystem` struct into PhysLib --- PxMLib/CMakeLists.txt | 2 +- PxMLib/PhysLib/CMakeLists.txt | 1 + .../SingleBodySystems/SingleBodySystem.h | 41 +++++++++++++++++++ PxMLib/PhysLib/pch.h | 2 + 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h diff --git a/PxMLib/CMakeLists.txt b/PxMLib/CMakeLists.txt index b2857283..43f5cd6a 100644 --- a/PxMLib/CMakeLists.txt +++ b/PxMLib/CMakeLists.txt @@ -4,5 +4,5 @@ project(PxMLib LANGUAGES CXX) # Add projects within the DSFE_App directory add_subdirectory(MathLib) -add_subdirectory(PhysLib) add_subdirectory(MathLib_Unit) +add_subdirectory(PhysLib) diff --git a/PxMLib/PhysLib/CMakeLists.txt b/PxMLib/PhysLib/CMakeLists.txt index e3231b64..0bb22ebc 100644 --- a/PxMLib/PhysLib/CMakeLists.txt +++ b/PxMLib/PhysLib/CMakeLists.txt @@ -19,4 +19,5 @@ target_include_directories(PhysLib target_link_libraries(PhysLib PUBLIC Eigen3::Eigen + MathLib ) \ No newline at end of file diff --git a/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h b/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h new file mode 100644 index 00000000..b17dd5da --- /dev/null +++ b/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h @@ -0,0 +1,41 @@ +// PxM/PhysLib SingleBodySystems/SingleBodySystem.h +#pragma once + +#include +#include + +using namespace constants; + +namespace single_body_system { + // Inertia struct representing the mass and inertia tensor of a rigid body + struct BodyInertia { + double mass = 0.0; + mathlib::Vec3 com_xyz{ 0.0, 0.0, 0.0 }; // Center of mass position in the body frame + mathlib::Mat3 inertiaTensor; // 3x3 inertia tensor matrix + }; + + struct DynamicsState { + mathlib::Vec3 x; // Position of the body in 3D space + mathlib::Vec3 xd; // Velocity of the body in 3D space + mathlib::Vec3 xdd; // Acceleration of the body in 3D space + mathlib::Quat q; // Orientation of the body (quaternion) + mathlib::Vec3 w; // Angular velocity of the body + mathlib::Vec3 wd; // Angular acceleration of the body + }; + + struct BodyLimits { + mathlib::Vec3 xdMin{ 0.0 }; // Minimum velocity limits (xd, yd, zd) + mathlib::Vec3 xdMax{ constants::c_0 }; // Maximum velocity limits (xd, yd, zd) + mathlib::Vec3 xddMin{ 0.0 }; // Minimum acceleration limits (xdd, ydd, zdd) + mathlib::Vec3 xddMax; // Maximum acceleration limits (xdd, ydd, zdd) + }; + + struct SingleBodySystem { + std::string name = "UnnamedBody"; + float scale = 1.0f; + + BodyInertia inertia; + DynamicsState state; + BodyLimits limits; + }; +} \ No newline at end of file diff --git a/PxMLib/PhysLib/pch.h b/PxMLib/PhysLib/pch.h index 35608d93..818dce77 100644 --- a/PxMLib/PhysLib/pch.h +++ b/PxMLib/PhysLib/pch.h @@ -21,4 +21,6 @@ #include #include +#include // All of PhysLib depends on MathLib, necessary here to avoid circular dependencies + #endif //PCH_H From 03ea214e44758c7ed5c8dc0be974be696d45a103 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 13:18:48 +0100 Subject: [PATCH 054/110] feat: Added basic `SingleBodyDynamics` class with methods to `computeDynamics` and `computeDerivatives` * This is insanely rushed, and I have not checked my math, I want to basically see if I can get the object recognised first, then worry about physical correctness after * I doubt ill need to compute the derivatives to begin with but we shall see. --- .../Include/SingleBodySystems/Dynamics.h | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h diff --git a/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h b/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h new file mode 100644 index 00000000..0571a126 --- /dev/null +++ b/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h @@ -0,0 +1,36 @@ +// PxM/PhysLib SingleBodySystems/Dynamics.h +#pragma once +#include "SingleBodySystem.h" + +namespace single_body_system::dynamics { + class SingleBodyDynamics { + public: + void computeDynamics(SingleBodySystem& body, mathlib::Vec3& externalForce, mathlib::Vec3& externalTorque, double dt) { + // Compute linear acceleration + body.state.xdd = (externalForce / body.inertia.mass).eval(); + // Compute angular acceleration + mathlib::Mat3 inertiaInv = body.inertia.inertiaTensor.inverse(); + body.state.wd = inertiaInv * (externalTorque - body.state.w.cross(body.inertia.inertiaTensor * body.state.w)); + // Update linear velocity and position + body.state.xd += body.state.xdd * dt; + body.state.x += body.state.xd * dt; + // Update angular velocity and orientation (using quaternion integration) + body.state.w += body.state.wd * dt; + mathlib::Quat dq = mathlib::Quat(0.0, body.state.w * dt); + body.state.q = (body.state.q.coeffs() + (0.5 * dq.coeffs() * body.state.q.coeffs())).normalized(); + } + + void derivatives(const SingleBodySystem& body, mathlib::Vec3& externalForce, mathlib::Vec3& externalTorque, mathlib::Mat3& dxdot_dx, mathlib::Mat3& dxdot_dxdot) { + // Compute the Jacobian of the dynamics with respect to state variables + dxdot_dx.setZero(); + dxdot_dxdot.setZero(); + // Linear acceleration derivatives + dxdot_dx.block<3, 3>(0, 0) = mathlib::Mat3::Zero(); // dx/dx = 0 + dxdot_dxdot.block<3, 3>(0, 3) = mathlib::Mat3::Identity() / body.inertia.mass; // dx/dxdot = 1/mass + // Angular acceleration derivatives + mathlib::Mat3 inertiaInv = body.inertia.inertiaTensor.inverse(); + dxdot_dx.block<3, 3>(3, 6) = -inertiaInv * skewSymmetric(body.state.w) * body.inertia.inertiaTensor; // dw/dq + dxdot_dxdot.block<3, 3>(3, 9) = inertiaInv; // dw/dwd = I^-1 + } + }; +} \ No newline at end of file From 3f6f42a78942ee058e9312cf68613f094df8dc2d Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 13:52:35 +0100 Subject: [PATCH 055/110] fixes: Updated SinlgeBody naming to `Body` --- DSFE_App/DSFE_Core/CMakeLists.txt | 8 ++++---- .../{SingleBodySystem.h => Body.h} | 18 ++++++++++-------- .../DSFE_Core/src/SingleBodySystem/Body.cpp | 16 ++++++++++++++++ .../src/SingleBodySystem/SingleBodySystem.cpp | 1 - .../Include/SingleBodySystems/Dynamics.h | 4 ++-- .../SingleBodySystems/SingleBodySystem.h | 2 +- 6 files changed, 33 insertions(+), 16 deletions(-) rename DSFE_App/DSFE_Core/include/SingleBodySystem/{SingleBodySystem.h => Body.h} (78%) create mode 100644 DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp delete mode 100644 DSFE_App/DSFE_Core/src/SingleBodySystem/SingleBodySystem.cpp diff --git a/DSFE_App/DSFE_Core/CMakeLists.txt b/DSFE_App/DSFE_Core/CMakeLists.txt index bec3de84..d8f452b8 100644 --- a/DSFE_App/DSFE_Core/CMakeLists.txt +++ b/DSFE_App/DSFE_Core/CMakeLists.txt @@ -18,7 +18,7 @@ set(DEP_CORE_SRC ) set(SINGLE_BODY_SYS_SRC - src/SingleBodySystem/SingleBodySystem.cpp + src/SingleBodySystem/Body.cpp ) set(MULTI_BODY_SYS_SRC @@ -35,7 +35,6 @@ set(PLATFORM_SRC src/Platform/Logger.cpp src/Platform/Paths.cpp src/Platform/StudyRunner.cpp - src/Analysis/Telemetry.cpp ) @@ -96,9 +95,10 @@ target_precompile_headers(DSFE_Core PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/pch.h") # Link DSFE_Core against its dependencies target_link_libraries(DSFE_Core - PUBLIC - MathLib + PUBLIC Eigen3::Eigen + MathLib + PhysLib hdf5::hdf5-shared Threads::Threads diff --git a/DSFE_App/DSFE_Core/include/SingleBodySystem/SingleBodySystem.h b/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h similarity index 78% rename from DSFE_App/DSFE_Core/include/SingleBodySystem/SingleBodySystem.h rename to DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h index 1b458a5e..9d8c3809 100644 --- a/DSFE_App/DSFE_Core/include/SingleBodySystem/SingleBodySystem.h +++ b/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h @@ -1,8 +1,9 @@ -// DSFE_CORE SingleBodySystem.h +// DSFE_CORE Body.h #pragma once #include "EngineCore.h" -#include +#include +#include "SingleBodySystems/Dynamics.h" #include "Numerics/IntegrationService.h" namespace single_body_system { @@ -11,11 +12,8 @@ namespace single_body_system { SingleBodySystem() = default; ~SingleBodySystem() = default; - - void step(double dt, double t); - integration::eIntegrationMethod getIntegrationMethod() const { return _curIntMethod; } std::string getIntegratorName() const { return _integrator->IntegratorName(_curIntMethod); } void setStandardIntegrator(integration::eIntegrationMethod m) { _curIntMethod = m; } @@ -25,14 +23,18 @@ namespace single_body_system { void setADIntegrator(integration::eAutoDiffIntegrationMethod m) { _curIntMethod_AD = m; } private: - double _mass; + void packState(mathlib::VecX& x) const; + void unpackState(const mathlib::VecX& x); - /*std::unique_ptr _dynamics;*/ + Body* _body; + std::unique_ptr _dynamics; std::unique_ptr _integrator; integration::eIntegrationMethod _curIntMethod{}; - std::unique_ptr _AD_integrator; integration::eAutoDiffIntegrationMethod _curIntMethod_AD{}; + + double _mass = 1.0; + mathlib::Vec3 _g{ 0.0, 0.0, 0.0 }; }; } \ No newline at end of file diff --git a/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp b/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp new file mode 100644 index 00000000..b889b500 --- /dev/null +++ b/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp @@ -0,0 +1,16 @@ +// DSFE_CORE Body.cpp +#include "pch.h" +#include "SingleBodySystem/Body.h" +#include "EngineLib/LogMacros.h" + +namespace single_body_system { + // Step the simulation forward by dt seconds at time t + void SingleBodySystem::step(double dt, double t) { + if (!_dynamics) { + _dynamics = std::make_unique(); + } + mathlib::Vec3 externalForce{ 0.0, 0.0, 0.0 }; // Placeholder for external forces + mathlib::Vec3 externalTorque{ 0.0, 0.0, 0.0 }; // Placeholder for external torques + _dynamics->computeDynamics(*this, externalForce, externalTorque, dt); + } +} \ No newline at end of file diff --git a/DSFE_App/DSFE_Core/src/SingleBodySystem/SingleBodySystem.cpp b/DSFE_App/DSFE_Core/src/SingleBodySystem/SingleBodySystem.cpp deleted file mode 100644 index 218a7124..00000000 --- a/DSFE_App/DSFE_Core/src/SingleBodySystem/SingleBodySystem.cpp +++ /dev/null @@ -1 +0,0 @@ -// DSFE_CORE SingleBodySystem.cpp \ No newline at end of file diff --git a/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h b/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h index 0571a126..75125274 100644 --- a/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h +++ b/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h @@ -5,7 +5,7 @@ namespace single_body_system::dynamics { class SingleBodyDynamics { public: - void computeDynamics(SingleBodySystem& body, mathlib::Vec3& externalForce, mathlib::Vec3& externalTorque, double dt) { + void computeDynamics(Body& body, mathlib::Vec3& externalForce, mathlib::Vec3& externalTorque, double dt) { // Compute linear acceleration body.state.xdd = (externalForce / body.inertia.mass).eval(); // Compute angular acceleration @@ -20,7 +20,7 @@ namespace single_body_system::dynamics { body.state.q = (body.state.q.coeffs() + (0.5 * dq.coeffs() * body.state.q.coeffs())).normalized(); } - void derivatives(const SingleBodySystem& body, mathlib::Vec3& externalForce, mathlib::Vec3& externalTorque, mathlib::Mat3& dxdot_dx, mathlib::Mat3& dxdot_dxdot) { + void derivatives(const Body& body, mathlib::Vec3& externalForce, mathlib::Vec3& externalTorque, mathlib::Mat3& dxdot_dx, mathlib::Mat3& dxdot_dxdot) { // Compute the Jacobian of the dynamics with respect to state variables dxdot_dx.setZero(); dxdot_dxdot.setZero(); diff --git a/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h b/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h index b17dd5da..f6ece898 100644 --- a/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h +++ b/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h @@ -30,7 +30,7 @@ namespace single_body_system { mathlib::Vec3 xddMax; // Maximum acceleration limits (xdd, ydd, zdd) }; - struct SingleBodySystem { + struct Body { std::string name = "UnnamedBody"; float scale = 1.0f; From 791a5caca8ef530ebf30dc977a200dd5ddd0793c Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 13:53:52 +0100 Subject: [PATCH 056/110] fixes: Removed unused and inappropriate MathLib files * Inappropriate meaning should not be within the library, not that the content is socially incorrect or misplaced. --- PxMLib/MathLib/include/dynamics/FrictionModels.h | 15 --------------- PxMLib/MathLib/include/dynamics/RigidBody.h | 14 -------------- 2 files changed, 29 deletions(-) delete mode 100644 PxMLib/MathLib/include/dynamics/FrictionModels.h delete mode 100644 PxMLib/MathLib/include/dynamics/RigidBody.h diff --git a/PxMLib/MathLib/include/dynamics/FrictionModels.h b/PxMLib/MathLib/include/dynamics/FrictionModels.h deleted file mode 100644 index 969ec102..00000000 --- a/PxMLib/MathLib/include/dynamics/FrictionModels.h +++ /dev/null @@ -1,15 +0,0 @@ -// PxM/MathLib FrictionModels.h - -#include - -namespace dynamics { - template - inline Scalar computeKarnoppFriction(const Scalar qd, const Scalar tau_active, Scalar c, Scalar b) { - Scalar Dv = Scalar(1e-4); // Small vel deadband - if (mathlib::abs(qd) > Dv) { return c * mathlib::sgn(qd) + b * qd; } // Viscous friction - else { - Scalar max_static_friction = c * Scalar(1.2); // Max static friction (20 percent higher than dynamic friction) - return mathlib::clamp(tau_active, -max_static_friction, max_static_friction); // Static friction with saturation - } - } -} // namespace dynamics \ No newline at end of file diff --git a/PxMLib/MathLib/include/dynamics/RigidBody.h b/PxMLib/MathLib/include/dynamics/RigidBody.h deleted file mode 100644 index a2c119ec..00000000 --- a/PxMLib/MathLib/include/dynamics/RigidBody.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include - -using namespace mathlib; - -namespace dynamics { - template - struct LinkInertia { - Scalar mass; - Vec3_T com; // center of mass - Mat3_T inertia; // inertia tensor - }; -} \ No newline at end of file From d8929ef26248dda4abac2a537b631db909e7b3ab Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 13:54:07 +0100 Subject: [PATCH 057/110] feat: Added new central `PhysLib` header --- PxMLib/PhysLib/Include/PhysLib.h | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 PxMLib/PhysLib/Include/PhysLib.h diff --git a/PxMLib/PhysLib/Include/PhysLib.h b/PxMLib/PhysLib/Include/PhysLib.h new file mode 100644 index 00000000..2741d1ad --- /dev/null +++ b/PxMLib/PhysLib/Include/PhysLib.h @@ -0,0 +1,9 @@ +// PxM/PhysLib PhysLib.h + +#ifdef PHYS_LIB_H +#define PHYS_LIB_H + +#include +#include "SingleBodySystems/SingleBodySystem.h" + +#endif // PHYS_LIB_H \ No newline at end of file From d54bfbd4bbbf706b6674c1e046933f6251e45803 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 15:12:05 +0100 Subject: [PATCH 058/110] feat: Added a simple stepping method into `SingleBodySystem` --- .../DSFE_Core/include/SingleBodySystem/Body.h | 20 +++- .../DSFE_Core/src/SingleBodySystem/Body.cpp | 98 +++++++++++++++++-- 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h b/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h index 9d8c3809..afc400d2 100644 --- a/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h +++ b/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h @@ -6,14 +6,19 @@ #include "SingleBodySystems/Dynamics.h" #include "Numerics/IntegrationService.h" +#include "Analysis/MetricLogger.h" + namespace single_body_system { - class SingleBodySystem { + class SingleBodySystem { // FOR NOW, this represents a single RIGID BODY, but in the near-near-near-future I will extend this for particles (or make a separate ParticleSystem class, changing this to RigidBodySystem) public: - SingleBodySystem() = default; - ~SingleBodySystem() = default; + SingleBodySystem(); + ~SingleBodySystem(); void step(double dt, double t); + Body* body() { return _body; } // return pointer to the body + const Body* body() const { return _body; } // return const pointer to the body + integration::eIntegrationMethod getIntegrationMethod() const { return _curIntMethod; } std::string getIntegratorName() const { return _integrator->IntegratorName(_curIntMethod); } void setStandardIntegrator(integration::eIntegrationMethod m) { _curIntMethod = m; } @@ -23,8 +28,8 @@ namespace single_body_system { void setADIntegrator(integration::eAutoDiffIntegrationMethod m) { _curIntMethod_AD = m; } private: - void packState(mathlib::VecX& x) const; - void unpackState(const mathlib::VecX& x); + mathlib::VecX packState() const; + void unpackState(const mathlib::VecX& s); Body* _body; std::unique_ptr _dynamics; @@ -34,6 +39,11 @@ namespace single_body_system { std::unique_ptr _AD_integrator; integration::eAutoDiffIntegrationMethod _curIntMethod_AD{}; + // precomputed clamp lookup tables + mutable std::vector _clampVel; + mutable std::vector _clampAngVel; + + double _simTime = 0.0; double _mass = 1.0; mathlib::Vec3 _g{ 0.0, 0.0, 0.0 }; }; diff --git a/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp b/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp index b889b500..ebe00b8a 100644 --- a/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp +++ b/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp @@ -4,13 +4,99 @@ #include "EngineLib/LogMacros.h" namespace single_body_system { + SingleBodySystem::SingleBodySystem() + : _body(new Body()), _dynamics(std::make_unique()), + _integrator(std::make_unique()), _AD_integrator(std::make_unique()) + { + } + + mathlib::VecX SingleBodySystem::packState() const { + if (!_body) { LOG_ERROR("Body not initialised!"); return; } + mathlib::VecX x(13); + x.segment<3>(0) = _body->state.p; // position + x.segment<3>(3) = _body->state.pd; // linear velocity + x.segment<4>(6) = _body->state.w; // angular velocity + x.segment<3>(9) = _body->state.q.coeffs(); // quaternion coefficients <- included because this should work for various single bodies, but in the case of a particle we can just use a different state representation via particle_packState() + return x; + } + + void SingleBodySystem::unpackState(const mathlib::VecX& x) { + if (!_body) { LOG_ERROR("Body not initialised!"); return; } + if (x.size() != 13) { LOG_ERROR("State vector size mismatch! Expected 13, got %d", (int)x.size()); return; } + + if (_clampVel.size() != 3) { _clampVel.resize(3, 0); } + if (_clampAngVel.size() != 3) { _clampAngVel.resize(3, 0); } + + auto& p_in = x.segment<3>(0); + auto& pd_in = x.segment<3>(3); + auto& w_in = x.segment<3>(6); + + for (int i = 0; i < 3; ++i) { + double pd_i = pd_in[i]; // Store the original value before clamping + double w_i = w_in[i]; // Store the original value before clamping + + double pdMax_hpd = std::abs(_body->limits.pdMax[i]); + double pd_i_out = pd_i; + double wMax_hw = std::abs(_body->limits.wMax[i]); + double w_i_out = w_i; + + const double eps = 0.05; // 5 % tolerance for clamping + // TODO: Check the correct way to clamp the velocities close the S.o.L + // * Not really sure if I have done this correctly + if (pdMax_hpd > 0.0) { + if (pdMax_hpd == constants::c_0) { std::clamp(pd_i, -pdMax_hpd, pdMax_hpd); } + else if (std::abs(pd_i) > (1.0 + eps) * pdMax_hpd) { pd_i_out = std::clamp(pd_i, -pdMax_hpd, pdMax_hpd); } + } + if (wMax_hw > 0.0) { + if (wMax_hw == constants::c_0) { std::clamp(w_i, -wMax_hw, wMax_hw); } + else if (std::abs(w_i) > (1.0 + eps) * wMax_hw) { w_i_out = std::clamp(w_i, -wMax_hw, wMax_hw); } + } + + _clampVel[i] = (pd_i_out != pd_i) ? 1 : 0; + _clampAngVel[i] = (w_i_out != w_i) ? 1 : 0; + + _body->state.p[i] = p_in[i]; + _body->state.pd[i] = pd_i_out; + _body->state.w[i] = w_i_out; + } + _body->state.q.coeffs() = x.segment<4>(9); + + + if (_clampVel[0] || _clampVel[1] || _clampVel[2]) { LOG_WARN("Linear velocity clamping applied: pd = [%f, %f, %f]", pd_in[0], pd_in[1], pd_in[2]); } + if (_clampAngVel[0] || _clampAngVel[1] || _clampAngVel[2]) { LOG_WARN("Angular velocity clamping applied: w = [%f, %f, %f]", w_in[0], w_in[1], w_in[2]); } + } + // Step the simulation forward by dt seconds at time t void SingleBodySystem::step(double dt, double t) { - if (!_dynamics) { - _dynamics = std::make_unique(); - } - mathlib::Vec3 externalForce{ 0.0, 0.0, 0.0 }; // Placeholder for external forces - mathlib::Vec3 externalTorque{ 0.0, 0.0, 0.0 }; // Placeholder for external torques - _dynamics->computeDynamics(*this, externalForce, externalTorque, dt); + if (!_body) { LOG_ERROR("Body not initialised!"); return; } + if (!_dynamics) { LOG_ERROR("Dynamics system not initialised!"); return; } + + _simTime = t; + VecX x = packState(); + // Not going to use the dynamics methods initially, just want a straight cut test first. + auto func = [this](const mathlib::VecX& x, mathlib::VecX& dxdt) { + // Unpack the state vector into the body state + unpackState(x); + // Compute the derivatives of the state (dx/dt) + mathlib::Vec3 externalForce{ 0.0, 0.0, 0.0 }; // Placeholder for external forces + mathlib::Vec3 externalTorque{ 0.0, 0.0, 0.0 }; // Placeholder for external torques + // Compute linear acceleration + if (externalForce == mathlib::Vec3(0.0, 0.0, 0.0)) { _body->state.pdd = mathlib::Vec3(0.0, 0.0, 0.0); } + else { _body->state.pdd = (externalForce / _body->inertia.mass).eval(); } + // Compute angular acceleration + mathlib::Mat3 inertiaInv = _body->inertia.inertiaTensor.inverse(); + _body->state.wd = inertiaInv * (externalTorque - _body->state.w.cross(_body->inertia.inertiaTensor * _body->state.w)); + // Fill in the derivative vector dxdt + dxdt.segment<3>(0) = _body->state.pd; // dp/dt = pd + dxdt.segment<3>(3) = _body->state.pdd; // dpd/dt = pdd + dxdt.segment<4>(6) = _body->state.w; // dw/dt = w + dxdt.segment<3>(10) = _body->state.wd; // dwd/dt = wd + }; + + auto step = _integrator->step(_curIntMethod, x, t, dt, func, nullptr); // Wont work with Implicit since no jacobian provided, but will work with RK4 and other explicit methods + // Also will not with adaptive (probably) since no rtol/atol provided, but will work with fixed step methods + VecX x_next = step.x_next; + + unpackState(x); } } \ No newline at end of file From acdddd283c9a973323aea3b1e9cf49931ca4742e Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 15:12:09 +0100 Subject: [PATCH 059/110] fixes --- DSFE_App/DSFE_Core/include/Robots/RobotDynamics.h | 1 - 1 file changed, 1 deletion(-) diff --git a/DSFE_App/DSFE_Core/include/Robots/RobotDynamics.h b/DSFE_App/DSFE_Core/include/Robots/RobotDynamics.h index ca7b0e92..c9a850b3 100644 --- a/DSFE_App/DSFE_Core/include/Robots/RobotDynamics.h +++ b/DSFE_App/DSFE_Core/include/Robots/RobotDynamics.h @@ -3,7 +3,6 @@ #include "EngineCore.h" #include -#include #include "Robots/DynamicsTypes.h" #include "Robots/RobotMetrics.h" From 25395a37d772ea667a10919200ff506e49aa5179 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 15:12:45 +0100 Subject: [PATCH 060/110] refactor: Updated `SingleBodyDynamics` and `Body` to reflect new pos, vel, and acc naming. --- .../Include/SingleBodySystems/Dynamics.h | 25 ++++++++++++++++--- .../SingleBodySystems/SingleBodySystem.h | 22 ++++++++-------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h b/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h index 75125274..d8a04311 100644 --- a/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h +++ b/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h @@ -5,15 +5,34 @@ namespace single_body_system::dynamics { class SingleBodyDynamics { public: + void buildInertiaTensor(Body& body) { + // Compute the inertia tensor based on the body's mass and center of mass + double m = body.inertia.mass; + mathlib::Vec3 com = body.inertia.com_xyz; + // Inertia tensor for a point mass at the center of mass + mathlib::Mat3 I = mathlib::Mat3::Zero(); // Local inertia tensor + I << + (1.0 / 12.0) * m * (com.y() * com.y() + com.z() * com.z()), 0, 0, + 0, (1.0 / 12.0)* m * (com.x() * com.x() + com.z() * com.z()), 0, + 0, 0, (1.0 / 12.0)* m * (com.x() * com.x() + com.y() * com.y()); + body.inertia.inertiaTensor = I; // Assign the computed inertia tensor to the body + } + + void buildParticleInertia(Body& body) { + // For a particle, in the simple case the body is treated as a point mass, so the inertia tensor is zero + body.inertia.inertiaTensor = mathlib::Mat3::Zero(); + } + void computeDynamics(Body& body, mathlib::Vec3& externalForce, mathlib::Vec3& externalTorque, double dt) { // Compute linear acceleration - body.state.xdd = (externalForce / body.inertia.mass).eval(); + if (externalForce == mathlib::Vec3(0.0, 0.0, 0.0)) { body.state.pdd = mathlib::Vec3(0.0, 0.0, 0.0); } + else { body.state.pdd = (externalForce / body.inertia.mass).eval(); } // Compute angular acceleration mathlib::Mat3 inertiaInv = body.inertia.inertiaTensor.inverse(); body.state.wd = inertiaInv * (externalTorque - body.state.w.cross(body.inertia.inertiaTensor * body.state.w)); // Update linear velocity and position - body.state.xd += body.state.xdd * dt; - body.state.x += body.state.xd * dt; + body.state.pd += body.state.pdd * dt; + body.state.p += body.state.pd * dt; // Update angular velocity and orientation (using quaternion integration) body.state.w += body.state.wd * dt; mathlib::Quat dq = mathlib::Quat(0.0, body.state.w * dt); diff --git a/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h b/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h index f6ece898..1606d0b4 100644 --- a/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h +++ b/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h @@ -15,19 +15,21 @@ namespace single_body_system { }; struct DynamicsState { - mathlib::Vec3 x; // Position of the body in 3D space - mathlib::Vec3 xd; // Velocity of the body in 3D space - mathlib::Vec3 xdd; // Acceleration of the body in 3D space - mathlib::Quat q; // Orientation of the body (quaternion) - mathlib::Vec3 w; // Angular velocity of the body - mathlib::Vec3 wd; // Angular acceleration of the body + mathlib::Vec3 p{ 0.0 }; // Position of the body in 3D space (meters) + mathlib::Vec3 pd{ 0.0 }; // Velocity of the body in 3D space (meters/second) + mathlib::Vec3 pdd{ 0.0 }; // Acceleration of the body in 3D space (meters/second^2) + mathlib::Quat q{ 1.0, 0.0, 0.0, 0.0 }; // Orientation of the body (quaternion) + mathlib::Vec3 w{ 0.0 }; // Angular velocity of the body (rads/second) + mathlib::Vec3 wd{ 0.0 }; // Angular acceleration of the body (rads/second^2) }; struct BodyLimits { - mathlib::Vec3 xdMin{ 0.0 }; // Minimum velocity limits (xd, yd, zd) - mathlib::Vec3 xdMax{ constants::c_0 }; // Maximum velocity limits (xd, yd, zd) - mathlib::Vec3 xddMin{ 0.0 }; // Minimum acceleration limits (xdd, ydd, zdd) - mathlib::Vec3 xddMax; // Maximum acceleration limits (xdd, ydd, zdd) + mathlib::Vec3 pdMin{ 0.0 }; // Minimum velocity limits (xd, yd, zd) + mathlib::Vec3 pdMax{ constants::c_0 }; // Maximum velocity limits (xd, yd, zd) + mathlib::Vec3 pddMin{ 0.0 }; // Minimum acceleration limits (xdd, ydd, zdd) + mathlib::Vec3 pddMax; // Maximum acceleration limits (xdd, ydd, zdd) + mathlib::Vec3 wMin{ 0.0 }; // Minimum angular velocity limits (wx, wy, wz) + mathlib::Vec3 wMax{ constants::c_0 }; // Maximum angular velocity limits (wx, wy, wz) }; struct Body { From 650e3d8b6e44c75e1a9f31235f38d91073bb26eb Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 17:10:38 +0100 Subject: [PATCH 061/110] fixes: Updated `Dynamics` to have correct matrix and vector computations * AGAIN NOT ENTIRELY SURE IF THIS IS EVEN CORRECT AT A SIMPLE LEVEL, IM NOT TRYING TO CORRECT IT RIGHT NOW, IM TRYING TO CHECK IF IT FUNCTIONS AS EXPECTED IN C++, THEN CORRECT. --- .../DSFE_Core/include/SingleBodySystem/Body.h | 3 + .../DSFE_Core/src/SingleBodySystem/Body.cpp | 36 +++------ .../Include/SingleBodySystems/Dynamics.h | 77 +++++++++++++------ .../SingleBodySystems/SingleBodySystem.h | 28 +++---- 4 files changed, 81 insertions(+), 63 deletions(-) diff --git a/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h b/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h index afc400d2..72b1c0df 100644 --- a/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h +++ b/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h @@ -39,6 +39,9 @@ namespace single_body_system { std::unique_ptr _AD_integrator; integration::eAutoDiffIntegrationMethod _curIntMethod_AD{}; + mathlib::Vec3 _F_ext{ 0.0, 0.0, 0.0 }; + mathlib::Vec3 _tau_ext{ 0.0, 0.0, 0.0 }; + // precomputed clamp lookup tables mutable std::vector _clampVel; mutable std::vector _clampAngVel; diff --git a/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp b/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp index ebe00b8a..00b91dd0 100644 --- a/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp +++ b/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp @@ -11,12 +11,12 @@ namespace single_body_system { } mathlib::VecX SingleBodySystem::packState() const { - if (!_body) { LOG_ERROR("Body not initialised!"); return; } mathlib::VecX x(13); x.segment<3>(0) = _body->state.p; // position x.segment<3>(3) = _body->state.pd; // linear velocity - x.segment<4>(6) = _body->state.w; // angular velocity - x.segment<3>(9) = _body->state.q.coeffs(); // quaternion coefficients <- included because this should work for various single bodies, but in the case of a particle we can just use a different state representation via particle_packState() + x.segment<4>(6) = _body->state.q.coeffs(); + x.segment<3>(10) = _body->state.w; // angular velocity + return x; } @@ -29,7 +29,7 @@ namespace single_body_system { auto& p_in = x.segment<3>(0); auto& pd_in = x.segment<3>(3); - auto& w_in = x.segment<3>(6); + auto& w_in = x.segment<3>(10); for (int i = 0; i < 3; ++i) { double pd_i = pd_in[i]; // Store the original value before clamping @@ -59,7 +59,7 @@ namespace single_body_system { _body->state.pd[i] = pd_i_out; _body->state.w[i] = w_i_out; } - _body->state.q.coeffs() = x.segment<4>(9); + _body->state.q.coeffs() = x.segment<4>(6); if (_clampVel[0] || _clampVel[1] || _clampVel[2]) { LOG_WARN("Linear velocity clamping applied: pd = [%f, %f, %f]", pd_in[0], pd_in[1], pd_in[2]); } @@ -73,27 +73,11 @@ namespace single_body_system { _simTime = t; VecX x = packState(); - // Not going to use the dynamics methods initially, just want a straight cut test first. - auto func = [this](const mathlib::VecX& x, mathlib::VecX& dxdt) { - // Unpack the state vector into the body state - unpackState(x); - // Compute the derivatives of the state (dx/dt) - mathlib::Vec3 externalForce{ 0.0, 0.0, 0.0 }; // Placeholder for external forces - mathlib::Vec3 externalTorque{ 0.0, 0.0, 0.0 }; // Placeholder for external torques - // Compute linear acceleration - if (externalForce == mathlib::Vec3(0.0, 0.0, 0.0)) { _body->state.pdd = mathlib::Vec3(0.0, 0.0, 0.0); } - else { _body->state.pdd = (externalForce / _body->inertia.mass).eval(); } - // Compute angular acceleration - mathlib::Mat3 inertiaInv = _body->inertia.inertiaTensor.inverse(); - _body->state.wd = inertiaInv * (externalTorque - _body->state.w.cross(_body->inertia.inertiaTensor * _body->state.w)); - // Fill in the derivative vector dxdt - dxdt.segment<3>(0) = _body->state.pd; // dp/dt = pd - dxdt.segment<3>(3) = _body->state.pdd; // dpd/dt = pdd - dxdt.segment<4>(6) = _body->state.w; // dw/dt = w - dxdt.segment<3>(10) = _body->state.wd; // dwd/dt = wd - }; - - auto step = _integrator->step(_curIntMethod, x, t, dt, func, nullptr); // Wont work with Implicit since no jacobian provided, but will work with RK4 and other explicit methods + // Not going to use the computeDynamics methods initially, just want a straight cut test first. + auto f_deriv = [&](auto t, const auto& x) { return _dynamics->derivatives(*_body, x, _F_ext, _tau_ext, dt); }; + auto f_jac = [&](const auto& x, auto& J_out) { _dynamics->jacobian(*_body, x, J_out); }; + + auto step = _integrator->step(_curIntMethod, x, t, dt, f_deriv, f_jac); // Wont work with Implicit since no jacobian provided, but will work with RK4 and other explicit methods // Also will not with adaptive (probably) since no rtol/atol provided, but will work with fixed step methods VecX x_next = step.x_next; diff --git a/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h b/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h index d8a04311..d2ca1c70 100644 --- a/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h +++ b/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h @@ -23,33 +23,64 @@ namespace single_body_system::dynamics { body.inertia.inertiaTensor = mathlib::Mat3::Zero(); } - void computeDynamics(Body& body, mathlib::Vec3& externalForce, mathlib::Vec3& externalTorque, double dt) { - // Compute linear acceleration - if (externalForce == mathlib::Vec3(0.0, 0.0, 0.0)) { body.state.pdd = mathlib::Vec3(0.0, 0.0, 0.0); } - else { body.state.pdd = (externalForce / body.inertia.mass).eval(); } - // Compute angular acceleration - mathlib::Mat3 inertiaInv = body.inertia.inertiaTensor.inverse(); - body.state.wd = inertiaInv * (externalTorque - body.state.w.cross(body.inertia.inertiaTensor * body.state.w)); + mathlib::VecX derivatives(Body& body, const mathlib::VecX& x, mathlib::Vec3& F_ext, mathlib::Vec3& tau_ext, double dt) { + mathlib::VecX dxdt(13); + + mathlib::Vec3 p = x.block<3, 1>(0, 0); + mathlib::Vec3 pd = x.block<3, 1>(3, 0); + mathlib::Vec4 v = x.block<4, 1>(6, 0); + mathlib::Quat q_coeffs(v); + mathlib::Vec3 w = x.block<3, 1>(10, 0); + + if (F_ext == mathlib::Vec3::Zero()) { body.state.pdd = F_ext; } + else { body.state.pdd = (F_ext / body.inertia.mass).eval(); } + // Update linear velocity and position - body.state.pd += body.state.pdd * dt; - body.state.p += body.state.pd * dt; - // Update angular velocity and orientation (using quaternion integration) - body.state.w += body.state.wd * dt; - mathlib::Quat dq = mathlib::Quat(0.0, body.state.w * dt); - body.state.q = (body.state.q.coeffs() + (0.5 * dq.coeffs() * body.state.q.coeffs())).normalized(); + body.state.pd = body.state.pdd * dt; + body.state.p = body.state.pd * dt; + + if (tau_ext == mathlib::Vec3::Zero()) { + body.state.wd = tau_ext; + body.state.w = tau_ext; + } + else { + mathlib::Mat3 inertiaInv = body.inertia.inertiaTensor.inverse(); + body.state.wd = inertiaInv * (tau_ext - body.state.w.cross(body.inertia.inertiaTensor * body.state.w)); + body.state.w = body.state.wd * dt; + } + + // Update orientation quaternion + if (q_coeffs.norm() > 1e-12) { + body.state.q = mathlib::Quat(q_coeffs).normalized(); + } + else { + body.state.q.coeffs() = mathlib::Quat(1.0, 0.0, 0.0, 0.0).coeffs(); + } + + double theta = body.state.w.norm() * dt; + if (theta > 1e-12) { + mathlib::Quat dq(Eigen::AngleAxis(theta, body.state.w.normalized())); + body.state.q = (body.state.q * dq).normalized(); + } + + dxdt.block<3, 1>(0, 0) = body.state.pd; // dp/dt = pd + dxdt.block<3, 1>(3, 0) = body.state.pdd; // dpd/dt = pdd + dxdt.block<4, 1>(6, 0) = body.state.q.coeffs(); // dq/dt = q (quaternion) + dxdt.block<3, 1>(10, 0) = body.state.w; // dw/dt = w + + return dxdt; } - void derivatives(const Body& body, mathlib::Vec3& externalForce, mathlib::Vec3& externalTorque, mathlib::Mat3& dxdot_dx, mathlib::Mat3& dxdot_dxdot) { - // Compute the Jacobian of the dynamics with respect to state variables - dxdot_dx.setZero(); - dxdot_dxdot.setZero(); - // Linear acceleration derivatives - dxdot_dx.block<3, 3>(0, 0) = mathlib::Mat3::Zero(); // dx/dx = 0 - dxdot_dxdot.block<3, 3>(0, 3) = mathlib::Mat3::Identity() / body.inertia.mass; // dx/dxdot = 1/mass - // Angular acceleration derivatives + void jacobian(Body& body, const mathlib::VecX& x, mathlib::MatX& J_out) { + J_out.setZero(13, 13); + // Partial derivatives for position and velocity + J_out.block<3, 3>(0, 3) = mathlib::Mat3::Identity(); // dp/dt = pd + J_out.block<3, 3>(3, 10) = mathlib::Mat3::Identity(); // dpd/dt = pdd + // Partial derivatives for orientation (quaternion) + J_out.block<4, 4>(6, 6) = mathlib::Mat4::Identity(); // dq/dt = q + // Partial derivatives for angular velocity mathlib::Mat3 inertiaInv = body.inertia.inertiaTensor.inverse(); - dxdot_dx.block<3, 3>(3, 6) = -inertiaInv * skewSymmetric(body.state.w) * body.inertia.inertiaTensor; // dw/dq - dxdot_dxdot.block<3, 3>(3, 9) = inertiaInv; // dw/dwd = I^-1 + J_out.block<3, 3>(10, 10) = inertiaInv; // dw/dt = inertiaInv * (tau_ext - w x (I * w)) } }; } \ No newline at end of file diff --git a/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h b/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h index 1606d0b4..d218396d 100644 --- a/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h +++ b/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h @@ -10,26 +10,26 @@ namespace single_body_system { // Inertia struct representing the mass and inertia tensor of a rigid body struct BodyInertia { double mass = 0.0; - mathlib::Vec3 com_xyz{ 0.0, 0.0, 0.0 }; // Center of mass position in the body frame - mathlib::Mat3 inertiaTensor; // 3x3 inertia tensor matrix + mathlib::Vec3 com_xyz = mathlib::Vec3::Zero();; // Center of mass position in the body frame + mathlib::Mat3 inertiaTensor = mathlib::Mat3::Zero();; // Inertia tensor in the body frame }; struct DynamicsState { - mathlib::Vec3 p{ 0.0 }; // Position of the body in 3D space (meters) - mathlib::Vec3 pd{ 0.0 }; // Velocity of the body in 3D space (meters/second) - mathlib::Vec3 pdd{ 0.0 }; // Acceleration of the body in 3D space (meters/second^2) - mathlib::Quat q{ 1.0, 0.0, 0.0, 0.0 }; // Orientation of the body (quaternion) - mathlib::Vec3 w{ 0.0 }; // Angular velocity of the body (rads/second) - mathlib::Vec3 wd{ 0.0 }; // Angular acceleration of the body (rads/second^2) + mathlib::Vec3 p = mathlib::Vec3::Zero(); // Position of the body in 3D space (meters) + mathlib::Vec3 pd = mathlib::Vec3::Zero(); // Velocity of the body in 3D space (meters/second) + mathlib::Vec3 pdd = mathlib::Vec3::Zero(); // Acceleration of the body in 3D space (meters/second^2) + mathlib::Quat q = mathlib::Quat::Identity(); // Orientation of the body as a quaternion + mathlib::Vec3 w = mathlib::Vec3::Zero(); // Angular velocity of the body (rads/second) + mathlib::Vec3 wd = mathlib::Vec3::Zero(); // Angular acceleration of the body (rads/second^2) }; struct BodyLimits { - mathlib::Vec3 pdMin{ 0.0 }; // Minimum velocity limits (xd, yd, zd) - mathlib::Vec3 pdMax{ constants::c_0 }; // Maximum velocity limits (xd, yd, zd) - mathlib::Vec3 pddMin{ 0.0 }; // Minimum acceleration limits (xdd, ydd, zdd) - mathlib::Vec3 pddMax; // Maximum acceleration limits (xdd, ydd, zdd) - mathlib::Vec3 wMin{ 0.0 }; // Minimum angular velocity limits (wx, wy, wz) - mathlib::Vec3 wMax{ constants::c_0 }; // Maximum angular velocity limits (wx, wy, wz) + mathlib::Vec3 pdMin = mathlib::Vec3::Zero(); // Minimum velocity limits (xd, yd, zd) + mathlib::Vec3 pdMax = mathlib::Vec3(c_0, c_0, c_0); // Maximum velocity limits (xd, yd, zd) + mathlib::Vec3 pddMin = mathlib::Vec3::Zero(); // Minimum acceleration limits (xdd, ydd, zdd) + mathlib::Vec3 pddMax = mathlib::Vec3::Zero(); // Maximum acceleration limits (xdd, ydd, zdd) + mathlib::Vec3 wMin = mathlib::Vec3::Zero(); // Minimum angular velocity limits (wx, wy, wz) + mathlib::Vec3 wMax = mathlib::Vec3(c_0, c_0, c_0); // Maximum angular velocity limits (wx, wy, wz) }; struct Body { From d8ae172ebb82c9c97251e52947a6e5097e68af40 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 17:35:10 +0100 Subject: [PATCH 062/110] feat: Added basic set of unit tests to test the `Dynamics.h` methods --- PxMLib/CMakeLists.txt | 1 + PxMLib/PhysLib_Unit/CMakeLists.txt | 5 + PxMLib/PhysLib_Unit/Common/TestHarness.h | 99 +++++++++++++++ .../SingleBodySystemTests/CMakeLists.txt | 27 +++++ .../SingleBodySystemDynamicsTests.cpp | 113 ++++++++++++++++++ .../SingleBodySystemTests/main.cpp | 32 +++++ 6 files changed, 277 insertions(+) create mode 100644 PxMLib/PhysLib_Unit/CMakeLists.txt create mode 100644 PxMLib/PhysLib_Unit/Common/TestHarness.h create mode 100644 PxMLib/PhysLib_Unit/SingleBodySystemTests/CMakeLists.txt create mode 100644 PxMLib/PhysLib_Unit/SingleBodySystemTests/SingleBodySystemDynamicsTests.cpp create mode 100644 PxMLib/PhysLib_Unit/SingleBodySystemTests/main.cpp diff --git a/PxMLib/CMakeLists.txt b/PxMLib/CMakeLists.txt index 43f5cd6a..af62eede 100644 --- a/PxMLib/CMakeLists.txt +++ b/PxMLib/CMakeLists.txt @@ -6,3 +6,4 @@ project(PxMLib LANGUAGES CXX) add_subdirectory(MathLib) add_subdirectory(MathLib_Unit) add_subdirectory(PhysLib) +add_subdirectory(PhysLib_Unit) diff --git a/PxMLib/PhysLib_Unit/CMakeLists.txt b/PxMLib/PhysLib_Unit/CMakeLists.txt new file mode 100644 index 00000000..7cc7e428 --- /dev/null +++ b/PxMLib/PhysLib_Unit/CMakeLists.txt @@ -0,0 +1,5 @@ +# CMakeLists.txt for PhysLib_Unit_Tests executable +cmake_minimum_required(VERSION 3.17) +project(PhysLib_Unit_Tests LANGUAGES C CXX) + +add_subdirectory(SingleBodySystemTests) \ No newline at end of file diff --git a/PxMLib/PhysLib_Unit/Common/TestHarness.h b/PxMLib/PhysLib_Unit/Common/TestHarness.h new file mode 100644 index 00000000..32665440 --- /dev/null +++ b/PxMLib/PhysLib_Unit/Common/TestHarness.h @@ -0,0 +1,99 @@ +#pragma once +// File: TestHarness.h +// GitHub: SaltyJoss +// Pretty basic console test runner for integration tests +#include +#include +#include +#include +#include +#include +#include + +namespace test { + // ANSI colour codes for console output + inline constexpr const char* GREEN = "\033[32m"; + inline constexpr const char* RED = "\033[31m"; + inline constexpr const char* YELLOW = "\033[33m"; + inline constexpr const char* CYAN = "\033[36m"; + inline constexpr const char* RESET = "\033[0m"; + + // assertTrue checks a condition and throws a runtime_error with the given message if it fails. + inline void assertTrue(bool condition, const char* msg) { + if (!condition) { throw std::runtime_error(msg); } + } + + // TestCase represents a single test with its group, name, and function to execute. + struct TestCase { + std::string group; + std::string name; + std::function fn; + }; + + // registry() returns a reference to the static vector of registered test cases. + inline std::vector& registry() { + static std::vector tests; + return tests; + } + + // AutoRegister is a helper that registers a test case at static initialization time. + struct AutoRegister { + AutoRegister(const char* group, const char* name, std::function fn) { + registry().push_back({ group, name, std::move(fn) }); + } + }; + + // Runs all registered tests, printing results and timing. Returns number of failed tests. + inline int runAll() { + int passed = 0, failed = 0; + std::string lastGroup; + // Iterate through all registered test cases, grouped by their group name. + for (const auto& tc : registry()) { + if (tc.group != lastGroup) { + std::cout << "\n" << CYAN << "=== " << tc.group << " ===" << RESET << "\n"; + lastGroup = tc.group; + } + // Time the test execution and catch any exceptions to report failures. + auto t0 = std::chrono::high_resolution_clock::now(); + try { + tc.fn(); + auto t1 = std::chrono::high_resolution_clock::now(); + double ms = std::chrono::duration(t1 - t0).count(); + std::cout << " " << GREEN << "PASS" << RESET + << " " << tc.name + << " (" << static_cast(ms) << " ms)\n"; + ++passed; + } + // If an exception is thrown, catch it and report as a failure with the error message. + catch (const std::exception& e) { + auto t1 = std::chrono::high_resolution_clock::now(); + double ms = std::chrono::duration(t1 - t0).count(); + std::cout << " " << RED << "FAIL" << RESET + << " " << tc.name + << " (" << static_cast(ms) << " ms)" + << " -> " << e.what() << "\n"; + ++failed; + } + } + + // Print a summary of the test results, showing total, passed, and failed counts with color coding. + std::cout << "\n" << std::string(50, '-') << "\n"; + std::cout << "Total: " << (passed + failed) + << " " << GREEN << "Passed: " << passed << RESET + << " " << (failed ? RED : GREEN) << "Failed: " << failed << RESET << "\n"; + + return failed; + } +} + +// TEST(group, name) registers a free function as a test case. +#define TEST(groupName, testName) \ + static void testName##_impl(); \ + namespace { static test::AutoRegister _reg_##testName( \ + groupName, #testName, testName##_impl); } \ + static void testName##_impl() + +#define ASSERT_TRUE(cond, msg) test::assertTrue((cond), (msg)) +#define ASSERT_EQ(a, b, tol, msg) test::assertTrue(std::abs((a) - (b)) < (tol)), (msg) +#define SUCCEED() return +#define ASSERT_NEAR(a, b, tol) test::assertTrue(std::abs((a) - (b)) < (tol), "Expected " #a " and " #b " to be within " #tol) \ No newline at end of file diff --git a/PxMLib/PhysLib_Unit/SingleBodySystemTests/CMakeLists.txt b/PxMLib/PhysLib_Unit/SingleBodySystemTests/CMakeLists.txt new file mode 100644 index 00000000..c9779dbe --- /dev/null +++ b/PxMLib/PhysLib_Unit/SingleBodySystemTests/CMakeLists.txt @@ -0,0 +1,27 @@ +# CMakeLists.txt for MathLib_Unit_Tests executable +cmake_minimum_required(VERSION 3.17) +project(PhysLib_SingleBodySystemTests LANGUAGES C CXX) + +enable_testing() + +add_executable(PhysLib_SingleBodySystemTests + main.cpp + SingleBodySystemDynamicsTests.cpp +) + +target_link_libraries(PhysLib_SingleBodySystemTests PRIVATE + PhysLib + gtest_main +) + +target_include_directories(PhysLib_SingleBodySystemTests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../Common +) + +include(GoogleTest) + +gtest_discover_tests( + PhysLib_SingleBodySystemTests + DISCOVERY_MODE PRE_TEST + DISCOVERY_TIMEOUT 10 +) diff --git a/PxMLib/PhysLib_Unit/SingleBodySystemTests/SingleBodySystemDynamicsTests.cpp b/PxMLib/PhysLib_Unit/SingleBodySystemTests/SingleBodySystemDynamicsTests.cpp new file mode 100644 index 00000000..0263345e --- /dev/null +++ b/PxMLib/PhysLib_Unit/SingleBodySystemTests/SingleBodySystemDynamicsTests.cpp @@ -0,0 +1,113 @@ +// PxM/PhysLib_Unit SingleBodySystemDynamicsTests.cpp +#include "TestHarness.h" + +#include "PhysLib.h" +#include "SingleBodySystems/Dynamics.h" + +#include "core/MathLib.h" +#include "integrators/numerical_integrators.h" + +namespace { + auto f = [](const mathlib::VecX& x, double t) -> mathlib::VecX { + mathlib::VecX dxdt(13); + dxdt.setZero(); + dxdt[3] = 1.0; // Constant velocity in x-direction + return dxdt; + }; +} + +// Test case for the derivatives function in SingleBodyDynamics +TEST("Single Body Dynamics Derivatives Method Test", SingleBodyDynamics_Derivatives) { + single_body_system::Body body; + body.inertia.mass = 1.0; + body.state.p = mathlib::Vec3(0.0, 0.0, 0.0); + body.state.pd = mathlib::Vec3(1.0, 0.0, 0.0); + body.state.q = mathlib::Quat::Identity(); + body.state.w = mathlib::Vec3(0.0, 0.0, 0.0); + mathlib::VecX x(13); + x.segment<3>(0) = body.state.p; + x.segment<3>(3) = body.state.pd; + x.segment<4>(6) = body.state.q.coeffs(); + x.segment<3>(10) = body.state.w; + mathlib::Vec3 F_ext(1.0, 0.0, 0.0); // External force in x-direction + mathlib::Vec3 tau_ext(0.0, 0.0, 1.0); // External torque around z-axis + double dt = 1.0; + single_body_system::dynamics::SingleBodyDynamics dynamics; + mathlib::VecX dxdt = dynamics.derivatives(body, x, F_ext, tau_ext, dt); + // Check that the derivatives are as expected + ASSERT_NEAR(dxdt[3], 1.0, 1e-6); // Velocity in x-direction should be constant + ASSERT_NEAR(dxdt[4], 0.0, 1e-6); // Velocity in y-direction should be zero + ASSERT_NEAR(dxdt[5], 0.0, 1e-6); // Velocity in z-direction should be zero +} +// Test case for the buildInertiaTensor function in SingleBodyDynamics +TEST("Single Body Dynamics Build Inertia Tensor Method Test", SingleBodyDynamics_BuildInertiaTensor) { + single_body_system::Body body; + body.inertia.mass = 2.0; + body.inertia.com_xyz = mathlib::Vec3(1.0, 1.0, 1.0); + single_body_system::dynamics::SingleBodyDynamics dynamics; + dynamics.buildInertiaTensor(body); + // Check that the inertia tensor is computed correctly + mathlib::Mat3 expectedInertia; + expectedInertia << (1.0 / 12.0) * body.inertia.mass * (body.inertia.com_xyz.y() * body.inertia.com_xyz.y() + body.inertia.com_xyz.z() * body.inertia.com_xyz.z()), 0, 0, + 0, (1.0 / 12.0)* body.inertia.mass* (body.inertia.com_xyz.x() * body.inertia.com_xyz.x() + body.inertia.com_xyz.z() * body.inertia.com_xyz.z()), 0, + 0, 0, (1.0 / 12.0)* body.inertia.mass* (body.inertia.com_xyz.x() * body.inertia.com_xyz.x() + body.inertia.com_xyz.y() * body.inertia.com_xyz.y()); + ASSERT_TRUE(body.inertia.inertiaTensor.isApprox(expectedInertia, 1e-6), "Inertia tensor is not computed correctly"); +} +// Test case for the buildParticleInertia function in SingleBodyDynamics +TEST("Single Body Dynamics Build Particle Inertia Method Test", SingleBodyDynamics_BuildParticleInertia) { + single_body_system::Body body; + body.inertia.mass = 1.0; + single_body_system::dynamics::SingleBodyDynamics dynamics; + dynamics.buildParticleInertia(body); + // Check that the inertia tensor is zero for a particle + ASSERT_TRUE(body.inertia.inertiaTensor.isZero(1e-6), "Inertia tensor for a particle should be zero"); +} +// Test case for the jacobian function in SingleBodyDynamics +TEST("Single Body Dynamics Jacobian Method Test", SingleBodyDynamics_Jacobian) { + single_body_system::Body body; + body.inertia.mass = 1.0; + body.state.p = mathlib::Vec3(0.0, 0.0, 0.0); + body.state.pd = mathlib::Vec3(1.0, 0.0, 0.0); + body.state.q = mathlib::Quat::Identity(); + body.state.w = mathlib::Vec3(0.0, 0.0, 0.0); + mathlib::VecX x(13); + x.segment<3>(0) = body.state.p; + x.segment<3>(3) = body.state.pd; + x.segment<4>(6) = body.state.q.coeffs(); + x.segment<3>(10) = body.state.w; + mathlib::MatX J_out(13, 13); + single_body_system::dynamics::SingleBodyDynamics dynamics; + dynamics.jacobian(body, x, J_out); + int rows = J_out.rows(); + int cols = J_out.cols(); + + // Check that the Jacobian is computed correctly (this is a simple check for the structure of the Jacobian) + ASSERT_TRUE(rows == 13, "Jacobian should have 13 rows"); + ASSERT_TRUE(cols == 13, "Jacobian should have 13 columns"); +} +// Test case for the derivatives function with zero external forces and torques +TEST("Single Body Dynamics Derivatives Method Test with Zero External Forces and Torques", SingleBodyDynamics_Derivatives_ZeroForcesTorques) { + single_body_system::Body body; + body.inertia.mass = 1.0; + body.state.p = mathlib::Vec3(0.0, 0.0, 0.0); + body.state.pd = mathlib::Vec3(1.0, 0.0, 0.0); + body.state.q = mathlib::Quat::Identity(); + body.state.w = mathlib::Vec3(0.0, 0.0, 0.0); + mathlib::VecX x(13); + x.segment<3>(0) = body.state.p; + x.segment<3>(3) = body.state.pd; + x.segment<4>(6) = body.state.q.coeffs(); + x.segment<3>(10) = body.state.w; + mathlib::Vec3 F_ext(0.0, 0.0, 0.0); // No external force + mathlib::Vec3 tau_ext(0.0, 0.0, 0.0); // No external torque + double dt = 1.0; + single_body_system::dynamics::SingleBodyDynamics dynamics; + mathlib::VecX dxdt = dynamics.derivatives(body, x, F_ext, tau_ext, dt); + // Check that the derivatives are as expected (no change in velocity or angular velocity) + ASSERT_NEAR(dxdt[3], 1.0, 1e-6); // Velocity in x-direction should remain constant + ASSERT_NEAR(dxdt[4], 0.0, 1e-6); // Velocity in y-direction should be zero + ASSERT_NEAR(dxdt[5], 0.0, 1e-6); // Velocity in z-direction should be zero + ASSERT_NEAR(dxdt[10], 0.0, 1e-6); // Angular velocity in x-direction should be zero + ASSERT_NEAR(dxdt[11], 0.0, 1e-6); // Angular velocity in y-direction should be zero + ASSERT_NEAR(dxdt[12], 0.0, 1e-6); // Angular velocity in z-direction should be zero +} diff --git a/PxMLib/PhysLib_Unit/SingleBodySystemTests/main.cpp b/PxMLib/PhysLib_Unit/SingleBodySystemTests/main.cpp new file mode 100644 index 00000000..505041d1 --- /dev/null +++ b/PxMLib/PhysLib_Unit/SingleBodySystemTests/main.cpp @@ -0,0 +1,32 @@ +// File: main.cpp +// GitHub: SaltyJoss +// Entry point for the integration test runner + +#include "TestHarness.h" + +#ifdef _WIN32 + #define NOMINMAX + #include +#endif + +int main() { +#ifdef _WIN32 + // Enable ANSI escape codes on Windows 10+ + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD mode = 0; + if (GetConsoleMode(hOut, &mode)) { + SetConsoleMode(hOut, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + } +#endif + + std::cout << "========================================\n"; + std::cout << " Single Body System Unit Tests\n"; + std::cout << "========================================\n"; + + int failures = test::runAll(); + + std::cout << "\nPress Enter to exit..."; + std::cin.get(); + + return failures; +} From 6f2a84bb43baca0eb55f0c9bb26b6555068aabdd Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Sat, 13 Jun 2026 18:23:00 +0100 Subject: [PATCH 063/110] fixes: Updated logic to correctly compute derivatives in single body systems --- .../Include/SingleBodySystems/Dynamics.h | 53 ++++++++----------- .../SingleBodySystems/SingleBodySystem.h | 2 +- .../SingleBodySystemDynamicsTests.cpp | 4 +- 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h b/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h index d2ca1c70..db38c6aa 100644 --- a/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h +++ b/PxMLib/PhysLib/Include/SingleBodySystems/Dynamics.h @@ -25,48 +25,39 @@ namespace single_body_system::dynamics { mathlib::VecX derivatives(Body& body, const mathlib::VecX& x, mathlib::Vec3& F_ext, mathlib::Vec3& tau_ext, double dt) { mathlib::VecX dxdt(13); + dxdt.setZero(); mathlib::Vec3 p = x.block<3, 1>(0, 0); mathlib::Vec3 pd = x.block<3, 1>(3, 0); - mathlib::Vec4 v = x.block<4, 1>(6, 0); - mathlib::Quat q_coeffs(v); + mathlib::Vec4 qv = x.block<4, 1>(6, 0); + mathlib::Quat q(qv); mathlib::Vec3 w = x.block<3, 1>(10, 0); - if (F_ext == mathlib::Vec3::Zero()) { body.state.pdd = F_ext; } - else { body.state.pdd = (F_ext / body.inertia.mass).eval(); } + const double m = body.inertia.mass; - // Update linear velocity and position - body.state.pd = body.state.pdd * dt; - body.state.p = body.state.pd * dt; + mathlib::Vec3 pdd; + if (m <= 1e-12) { pdd = mathlib::Vec3(0.0, 0.0, 0.0); } + else { pdd = F_ext / m; } - if (tau_ext == mathlib::Vec3::Zero()) { - body.state.wd = tau_ext; - body.state.w = tau_ext; - } - else { - mathlib::Mat3 inertiaInv = body.inertia.inertiaTensor.inverse(); - body.state.wd = inertiaInv * (tau_ext - body.state.w.cross(body.inertia.inertiaTensor * body.state.w)); - body.state.w = body.state.wd * dt; - } + mathlib::Vec3 wd = mathlib::Vec3::Zero(); + mathlib::Mat3 I = body.inertia.inertiaTensor; + mathlib::Mat3 I_inv = I.inverse(); - // Update orientation quaternion - if (q_coeffs.norm() > 1e-12) { - body.state.q = mathlib::Quat(q_coeffs).normalized(); - } - else { - body.state.q.coeffs() = mathlib::Quat(1.0, 0.0, 0.0, 0.0).coeffs(); + if (I.determinant() > 1e-12) { + wd = I_inv * (tau_ext - w.cross(I * w)); } - double theta = body.state.w.norm() * dt; - if (theta > 1e-12) { - mathlib::Quat dq(Eigen::AngleAxis(theta, body.state.w.normalized())); - body.state.q = (body.state.q * dq).normalized(); - } + dxdt.block<3, 1>(0, 0) = pd; // dp/dt = v + dxdt.block<3, 1>(3, 0) = pdd; // dpd/dt = a + + mathlib::Quat dq; + mathlib::Vec3 omega = w; + + mathlib::Quat omega_quat(0.0, omega.x(), omega.y(), omega.z()); + dq.coeffs() = 0.5 * (omega_quat * q).coeffs(); - dxdt.block<3, 1>(0, 0) = body.state.pd; // dp/dt = pd - dxdt.block<3, 1>(3, 0) = body.state.pdd; // dpd/dt = pdd - dxdt.block<4, 1>(6, 0) = body.state.q.coeffs(); // dq/dt = q (quaternion) - dxdt.block<3, 1>(10, 0) = body.state.w; // dw/dt = w + dxdt.block<4, 1>(6, 0) = dq.coeffs(); // dq/dt = 0.5 * q * w + dxdt.block<3, 1>(10, 0) = wd; return dxdt; } diff --git a/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h b/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h index d218396d..a9eca26b 100644 --- a/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h +++ b/PxMLib/PhysLib/Include/SingleBodySystems/SingleBodySystem.h @@ -9,7 +9,7 @@ using namespace constants; namespace single_body_system { // Inertia struct representing the mass and inertia tensor of a rigid body struct BodyInertia { - double mass = 0.0; + double mass = 1.0; mathlib::Vec3 com_xyz = mathlib::Vec3::Zero();; // Center of mass position in the body frame mathlib::Mat3 inertiaTensor = mathlib::Mat3::Zero();; // Inertia tensor in the body frame }; diff --git a/PxMLib/PhysLib_Unit/SingleBodySystemTests/SingleBodySystemDynamicsTests.cpp b/PxMLib/PhysLib_Unit/SingleBodySystemTests/SingleBodySystemDynamicsTests.cpp index 0263345e..7900dca3 100644 --- a/PxMLib/PhysLib_Unit/SingleBodySystemTests/SingleBodySystemDynamicsTests.cpp +++ b/PxMLib/PhysLib_Unit/SingleBodySystemTests/SingleBodySystemDynamicsTests.cpp @@ -7,6 +7,8 @@ #include "core/MathLib.h" #include "integrators/numerical_integrators.h" +#include + namespace { auto f = [](const mathlib::VecX& x, double t) -> mathlib::VecX { mathlib::VecX dxdt(13); @@ -104,7 +106,7 @@ TEST("Single Body Dynamics Derivatives Method Test with Zero External Forces and single_body_system::dynamics::SingleBodyDynamics dynamics; mathlib::VecX dxdt = dynamics.derivatives(body, x, F_ext, tau_ext, dt); // Check that the derivatives are as expected (no change in velocity or angular velocity) - ASSERT_NEAR(dxdt[3], 1.0, 1e-6); // Velocity in x-direction should remain constant + ASSERT_NEAR(dxdt[3], 0.0, 1e-6); // Velocity in x-direction should remain constant ASSERT_NEAR(dxdt[4], 0.0, 1e-6); // Velocity in y-direction should be zero ASSERT_NEAR(dxdt[5], 0.0, 1e-6); // Velocity in z-direction should be zero ASSERT_NEAR(dxdt[10], 0.0, 1e-6); // Angular velocity in x-direction should be zero From 5453401f07c2fec8b5c799fa5c79c6303f008aab Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 16 Jun 2026 09:29:56 +0100 Subject: [PATCH 064/110] feat: Integrated basic pointer referencing of `SingleBodySystem` into `SimulationCore` and `SimManager` * I am thinking that a `Systems` header (and compiler?) may be beneficial to centralise ALL systems, then compute which internally using state callbacks. --- .../include/Platform/ISimulationCore.h | 5 + .../DSFE_Core/include/Scene/SimulationCore.h | 12 +++ .../DSFE_Core/include/SingleBodySystem/Body.h | 23 ++++- .../DSFE_Core/src/Scene/SimulationCore.cpp | 92 +++++++++++++------ .../DSFE_Core/src/SingleBodySystem/Body.cpp | 33 ++++++- .../include/Scene/Manager/SimImplementation.h | 4 + .../include/Scene/SimulationManager.h | 7 +- .../src/MainWindow/DSFE_MainWindow.cpp | 3 + .../DSFE_GUI/src/Scene/Manager/SimRobots.cpp | 5 + .../DSFE_GUI/src/Scene/SimulationManager.cpp | 20 ++++ 10 files changed, 170 insertions(+), 34 deletions(-) diff --git a/DSFE_App/DSFE_Core/include/Platform/ISimulationCore.h b/DSFE_App/DSFE_Core/include/Platform/ISimulationCore.h index 27b7b6da..dec47dd0 100644 --- a/DSFE_App/DSFE_Core/include/Platform/ISimulationCore.h +++ b/DSFE_App/DSFE_Core/include/Platform/ISimulationCore.h @@ -9,6 +9,7 @@ // Forward Declarations namespace integration { enum class eIntegrationMethod; enum class eAutoDiffIntegrationMethod; } namespace robots { class RobotSystem; } +namespace single_body_system { class SingleBodySystem; } namespace control { class TrajectoryManager; } namespace diagnostics { class TelemetryRecorder; } namespace interpreter { class IStoredProgram; } @@ -47,7 +48,11 @@ namespace core { virtual void setRunTag(const std::string& tag) = 0; // Subsystems virtual robots::RobotSystem* robotSystem() = 0; + virtual single_body_system::SingleBodySystem* singleBodySystem() = 0; virtual control::TrajectoryManager* trajectoryManager() = 0; + // Body management + virtual bool hasSingleBody() const = 0; + virtual void loadSingleBody(const std::string& name) = 0; // Robot management virtual bool hasRobot() const = 0; virtual void loadRobot(const std::string& name) = 0; diff --git a/DSFE_App/DSFE_Core/include/Scene/SimulationCore.h b/DSFE_App/DSFE_Core/include/Scene/SimulationCore.h index 22d6bc17..2da971e7 100644 --- a/DSFE_App/DSFE_Core/include/Scene/SimulationCore.h +++ b/DSFE_App/DSFE_Core/include/Scene/SimulationCore.h @@ -22,6 +22,7 @@ // Forward Declarations namespace control { class TrajectoryManager; } namespace robots { class RobotSystem; } +namespace single_body_system { class SingleBodySystem; } namespace interpreter { class IStoredProgram; } namespace core { @@ -77,9 +78,16 @@ namespace core { // Subsystems access robots::RobotSystem* robotSystem() override; const robots::RobotSystem* robotSystem() const; + single_body_system::SingleBodySystem* singleBodySystem() override; + const single_body_system::SingleBodySystem* singleBodySystem() const; control::TrajectoryManager* trajectoryManager() override; const control::TrajectoryManager* trajectoryManager() const; + // Body management + bool hasSingleBody() const override; + void loadSingleBody(const std::string& name) override; + void loadSingleBodyInternal(const std::string& name); // Internal method that assumes ownership + // Robot management bool hasRobot() const override; void loadRobot(const std::string& name) override; @@ -95,6 +103,7 @@ namespace core { // Setters for subsystems and scene objects void setRobotSystem(robots::RobotSystem* robot); + void setSingleBodySystem(single_body_system::SingleBodySystem* singleBody); void setTrajectoryManager(control::TrajectoryManager* traj); void setJointLogBuffer(robots::JointLogBuffer* buffer); void setTrajRefBuffer(robots::TrajRefBuffer* buffer); @@ -146,11 +155,13 @@ namespace core { // Owning storage (used only in owning mode) // std::unique_ptr>> _objectsOwned; std::unique_ptr _robotOwned; + std::unique_ptr _singleBodyOwned; std::unique_ptr _trajOwned; // Non-owning access (always used by logic) // std::vector>* _objects = nullptr; robots::RobotSystem* _robot = nullptr; + single_body_system::SingleBodySystem* _singleBody = nullptr; control::TrajectoryManager* _traj = nullptr; mutable std::mutex _stateMutex; @@ -175,6 +186,7 @@ namespace core { // Active Script Program interpreter::IStoredProgram* _activeProgram = nullptr; bool _robotPresentationDirty = false; + bool _singleBodyPresentationDirty = false; // Telemetry diagnostics::TelemetryRecorder _telemetry; // Dynamic telemetry recorder diff --git a/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h b/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h index 72b1c0df..3b4224e0 100644 --- a/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h +++ b/DSFE_App/DSFE_Core/include/SingleBodySystem/Body.h @@ -9,16 +9,21 @@ #include "Analysis/MetricLogger.h" namespace single_body_system { - class SingleBodySystem { // FOR NOW, this represents a single RIGID BODY, but in the near-near-near-future I will extend this for particles (or make a separate ParticleSystem class, changing this to RigidBodySystem) + class DSFE_API SingleBodySystem { // FOR NOW, this represents a single RIGID BODY, but in the near-near-near-future I will extend this for particles (or make a separate ParticleSystem class, changing this to RigidBodySystem) public: SingleBodySystem(); - ~SingleBodySystem(); + ~SingleBodySystem() = default; void step(double dt, double t); Body* body() { return _body; } // return pointer to the body const Body* body() const { return _body; } // return const pointer to the body + void loadBody(const std::string& name); // this does not load the actual graphical model, just the physical properties of the body (mass, inertia, etc.) from the stl file or other source + void resetBody(); // reset the body to its initial state (position, orientation, velocity, etc.) + + bool hasBody() const { return _body != nullptr; } + integration::eIntegrationMethod getIntegrationMethod() const { return _curIntMethod; } std::string getIntegratorName() const { return _integrator->IntegratorName(_curIntMethod); } void setStandardIntegrator(integration::eIntegrationMethod m) { _curIntMethod = m; } @@ -27,6 +32,18 @@ namespace single_body_system { std::string AD_integratorName() const { return _AD_integrator->IntegratorName(_curIntMethod_AD); } void setADIntegrator(integration::eAutoDiffIntegrationMethod m) { _curIntMethod_AD = m; } + integration::IntegrationService* getIntegrator(); + const integration::IntegrationService* getIntegrator() const; + + integration::DifferentiableIntegrator* getADIntegrator(); + const integration::DifferentiableIntegrator* getADIntegrator() const; + + bool autoDiffEnabled() const { return _useAutoDiff; } + void enableAutoDiff(bool enable) { _useAutoDiff = enable; } + + std::shared_ptr runtimeIntegratorState(); + std::shared_ptr runtimeIntegratorState() const; + private: mathlib::VecX packState() const; void unpackState(const mathlib::VecX& s); @@ -42,6 +59,8 @@ namespace single_body_system { mathlib::Vec3 _F_ext{ 0.0, 0.0, 0.0 }; mathlib::Vec3 _tau_ext{ 0.0, 0.0, 0.0 }; + bool _useAutoDiff = false; + // precomputed clamp lookup tables mutable std::vector _clampVel; mutable std::vector _clampAngVel; diff --git a/DSFE_App/DSFE_Core/src/Scene/SimulationCore.cpp b/DSFE_App/DSFE_Core/src/Scene/SimulationCore.cpp index f0d18b8f..c7f9880b 100644 --- a/DSFE_App/DSFE_Core/src/Scene/SimulationCore.cpp +++ b/DSFE_App/DSFE_Core/src/Scene/SimulationCore.cpp @@ -5,6 +5,7 @@ #include "Robots/RobotSystem.h" #include "Robots/RobotModel.h" #include "Robots/TrajectoryManager.h" +#include "SingleBodySystem/Body.h" #include "Interpreter/StoredProgram.h" #include "Interpreter/Parser.h" @@ -15,10 +16,12 @@ namespace core { // Owned constructed subsystems (default) SimulationCore::SimulationCore() - : _trajOwned(std::make_unique()), _robotOwned(std::make_unique()) + : _trajOwned(std::make_unique()), _robotOwned(std::make_unique()), + _singleBodyOwned(std::make_unique()) { _traj = _trajOwned.get(); _robot = _robotOwned.get(); + _singleBody = _singleBodyOwned.get(); startExportThread(); } @@ -30,15 +33,23 @@ namespace core { // Non-owning constructor (used when subsystems are managed externally, e.g. by the SimulationManager) SimulationCore::SimulationCore(robots::RobotSystem& robot, control::TrajectoryManager& traj) - : _robot(&robot), _traj(&traj) { + : _robot(&robot), _traj(&traj), _singleBody(nullptr) { startExportThread(); } // Simulation System void SimulationCore::setupSimulationIntegrator() { - if (!_robot) { return; } - auto* intgr = _robot->getIntegrator(); - auto* adIntgr = _robot->getADIntegrator(); + if (!_robot && !_singleBody) { return; } + integration::IntegrationService* intgr; + integration::DifferentiableIntegrator* adIntgr; + if (_robot) { + intgr = _robot->getIntegrator(); + adIntgr = _robot->getADIntegrator(); + } + if (_singleBody) { + intgr = _singleBody->getIntegrator(); + adIntgr = _singleBody->getADIntegrator(); + } intgr->resetAdaptiveState(); intgr->setAdaptiveTolerances(1e-3, 1e-6); intgr->setMaxStep(_dt); @@ -47,37 +58,39 @@ namespace core { } // Set the integration method for the simulation (also updates the robot's integrator if it exists) void SimulationCore::setIntegrationMethod(integration::eIntegrationMethod method) { - if (!_robot) { return; } - _robot->setStandardIntegrator(method); + if (!_robot && !_singleBody) { return; } + if (_singleBody) { _singleBody->setStandardIntegrator(method); } + if (_robot) { _robot->setStandardIntegrator(method); } } // Set the auto-diff integration method for the simulation (also updates the robot's AD integrator if it exists) void SimulationCore::setADIntegrationMethod(integration::eAutoDiffIntegrationMethod method) { - if (!_robot) { return; } - _robot->setADIntegrator(method); + if (!_robot && !_singleBody) { return; } + if (_singleBody) { _singleBody->setADIntegrator(method); } + if (_robot) { _robot->setADIntegrator(method); } } // Get the name of the current integration method (returns "no_robot" if no robot is loaded) std::string SimulationCore::integrationMethodName() const { - if (!_robot) { return "no_robot"; } std::string intName; - if (_robot->autoDiffEnabled()) { - intName = _robot->getIntegratorName(); - } - else { - intName = _robot->AD_integratorName(); + if (_robot) { + if (_robot->autoDiffEnabled()) { intName = _robot->getIntegratorName(); } + else { intName = _robot->AD_integratorName(); } } + else if(_singleBody) { intName = _singleBody->getIntegratorName(); } + else { intName = "no_system"; } LOG_INFO("Integration Method: %s", intName.c_str()); - return intName; } // Get the current integration method integration::eIntegrationMethod SimulationCore::integrationMethod() const { - if (!_robot) { return integration::eIntegrationMethod::RK4; } - return _robot->getIntegrationMethod(); + if (_robot) { return _robot->getIntegrationMethod(); } + if (_singleBody) { return _singleBody->getIntegrationMethod(); } + return integration::eIntegrationMethod::RK4; } // Get the current auto-diff integration method integration::eAutoDiffIntegrationMethod SimulationCore::autoDiffIntegrationMethod() const { - if (!_robot) { return integration::eAutoDiffIntegrationMethod::AD_ImplicitEuler; } - return _robot->AD_IntegrationMethod(); + if (_robot) { return _robot->AD_IntegrationMethod(); } + if (_singleBody) { return _singleBody->AD_IntegrationMethod(); } + return integration::eAutoDiffIntegrationMethod::AD_ImplicitEuler; } void SimulationCore::enableAutoDiff(bool enable) { if (!_robot) { return; } @@ -135,6 +148,7 @@ namespace core { } _telemetry.update(simTime, *_robot, _traj, diagnostics::eTelemetryLevel::FULL); } + if (hasSingleBody()) { _singleBody->step(_dt, simTime); } } _accum -= _dt; // decrease accumulator by fixed timestep until we catch up to the current frame time } @@ -157,6 +171,8 @@ namespace core { _simTime.store(0.0, std::memory_order_relaxed); _accum = 0.0; + std::string intName; + // Reset simulation system if (_robot) { _robot->resetRobot(); @@ -182,11 +198,16 @@ namespace core { _trajRefBuffer.clear(); _trajRefBuffer.reserve(std::max(1024, total / (26 / 5))); // 26 to 5 entries, so reserving 1/(26/5) of total steps as a heuristic for ref buffer size _robot->setRefBuffer(&_trajRefBuffer); - } - - const auto state = _robot->runtimeIntegratorState(); - std::string intName = (_robot->autoDiffEnabled()) ? _robot->AD_integratorName() : _robot->getIntegratorName(); + const auto state = _robot->runtimeIntegratorState(); + intName = (_robot->autoDiffEnabled()) ? _robot->AD_integratorName() : _robot->getIntegratorName(); + } + if (_singleBody) { + _singleBody->resetBody(); + const auto state = _singleBody->runtimeIntegratorState(); + intName = (_singleBody->autoDiffEnabled()) ? _singleBody->AD_integratorName() : _singleBody->getIntegratorName(); + } + _data.setParentFolder(paths::runs().string()); // Ensure reference sim system have their integrators configured for the new run @@ -412,12 +433,31 @@ namespace core { loadRobotInternal(name); _robotPresentationDirty = true; } - + // Internal method to load a robot, assumes ownership of the robot system void SimulationCore::loadRobotInternal(const std::string& name) { if (!_robot) { LOG_ERROR("Cannot load robot: RobotSystem not set"); return; } _robot->loadRobot(name); } + // Accessor for the single body system (non-const and const versions) + single_body_system::SingleBodySystem* SimulationCore::singleBodySystem() { return _singleBody; } + const single_body_system::SingleBodySystem* SimulationCore::singleBodySystem() const { return _singleBody; } + + // Setter and checker for Single Body System + void SimulationCore::setSingleBodySystem(single_body_system::SingleBodySystem* singleBody) { _singleBody = singleBody; } + bool SimulationCore::hasSingleBody() const { return _singleBody && _singleBody->hasBody(); } + + // Loads a single body into the single body system by name + void SimulationCore::loadSingleBody(const std::string& name) { + loadSingleBodyInternal(name); + _singleBodyPresentationDirty = true; + } + // Internal method to load a single body, assumes ownership of the single body system + void SimulationCore::loadSingleBodyInternal(const std::string& name) { + if (!_singleBody) { LOG_ERROR("Cannot load single body: SingleBodySystem not set"); return; } + _singleBody->loadBody(name); + } + // Setter for the trajectory manager void SimulationCore::setTrajectoryManager(control::TrajectoryManager* traj) { _traj = traj; } @@ -440,8 +480,6 @@ namespace core { void SimulationCore::setActiveProgram(interpreter::IStoredProgram* p) { _activeProgram = p; } interpreter::IStoredProgram* SimulationCore::activeProgram() const { return _activeProgram; } - // Setter for the - // Thread-based methods // Starts the export thread if it's not already running diff --git a/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp b/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp index 00b91dd0..60fabd9e 100644 --- a/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp +++ b/DSFE_App/DSFE_Core/src/SingleBodySystem/Body.cpp @@ -44,11 +44,11 @@ namespace single_body_system { // TODO: Check the correct way to clamp the velocities close the S.o.L // * Not really sure if I have done this correctly if (pdMax_hpd > 0.0) { - if (pdMax_hpd == constants::c_0) { std::clamp(pd_i, -pdMax_hpd, pdMax_hpd); } + if (pdMax_hpd == constants::c_0) { pd_i_out = std::clamp(pd_i, -pdMax_hpd, pdMax_hpd); } else if (std::abs(pd_i) > (1.0 + eps) * pdMax_hpd) { pd_i_out = std::clamp(pd_i, -pdMax_hpd, pdMax_hpd); } } if (wMax_hw > 0.0) { - if (wMax_hw == constants::c_0) { std::clamp(w_i, -wMax_hw, wMax_hw); } + if (wMax_hw == constants::c_0) { w_i_out = std::clamp(w_i, -wMax_hw, wMax_hw); } else if (std::abs(w_i) > (1.0 + eps) * wMax_hw) { w_i_out = std::clamp(w_i, -wMax_hw, wMax_hw); } } @@ -76,11 +76,36 @@ namespace single_body_system { // Not going to use the computeDynamics methods initially, just want a straight cut test first. auto f_deriv = [&](auto t, const auto& x) { return _dynamics->derivatives(*_body, x, _F_ext, _tau_ext, dt); }; auto f_jac = [&](const auto& x, auto& J_out) { _dynamics->jacobian(*_body, x, J_out); }; - auto step = _integrator->step(_curIntMethod, x, t, dt, f_deriv, f_jac); // Wont work with Implicit since no jacobian provided, but will work with RK4 and other explicit methods - // Also will not with adaptive (probably) since no rtol/atol provided, but will work with fixed step methods VecX x_next = step.x_next; unpackState(x); + + for (int i = 0; i < x_next.size(); ++i) { + const auto v = mathlib::real(x_next[i]); + if (std::isnan(v) || std::isinf(v)) { LOG_ERROR("Non-finite x_next[%d] = %f", i, (double)v); } + } + } + + void SingleBodySystem::loadBody(const std::string& name) { + if (!_body) { LOG_ERROR("Body not initialised!"); return; } + _body->name = name; } + + void SingleBodySystem::resetBody() { + if (!_body) { LOG_ERROR("Body not initialised!"); return; } + _body->state.p = mathlib::Vec3::Zero(); + _body->state.pd = mathlib::Vec3::Zero(); + _body->state.q = mathlib::Quat::Identity(); + _body->state.w = mathlib::Vec3::Zero(); + } + + integration::IntegrationService* SingleBodySystem::getIntegrator() { return _integrator.get(); } + const integration::IntegrationService* SingleBodySystem::getIntegrator() const { return _integrator.get(); } + + integration::DifferentiableIntegrator* SingleBodySystem::getADIntegrator() { return _AD_integrator.get(); } + const integration::DifferentiableIntegrator* SingleBodySystem::getADIntegrator() const { return _AD_integrator.get(); } + + std::shared_ptr SingleBodySystem::runtimeIntegratorState() { return _useAutoDiff ? _AD_integrator->runtimeState() : _integrator->runtimeState(); } + std::shared_ptr SingleBodySystem::runtimeIntegratorState() const { return _useAutoDiff ? _AD_integrator->runtimeState() : _integrator->runtimeState(); } } \ No newline at end of file diff --git a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h index 2bd91cdb..0437666a 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h +++ b/DSFE_App/DSFE_GUI/include/Scene/Manager/SimImplementation.h @@ -19,6 +19,7 @@ #include "Robots/RobotSystem.h" #include "Robots/RobotModel.h" #include "Robots/TrajectoryManager.h" +#include "SingleBodySystem/Body.h" #include "Rendering/SkyboxRenderer.h" #include "Rendering/ShaderUtil.h" @@ -109,6 +110,8 @@ namespace gui { // Robot System std::unique_ptr _robotSystem; // simulation + // Single Body System + std::unique_ptr _singleBody; // simulation // Robot Rendering std::unique_ptr _robotRenderer; // rendering RobotRenderBinding _currentBinding; // current render binding @@ -135,6 +138,7 @@ namespace gui { // Robot system with mesh loading (for normal simulation) _robotSystem = std::make_unique(); + _singleBody = std::make_unique(); } void initGLResources(SimManager& owner) { diff --git a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h index 4da0d6cf..535ba764 100644 --- a/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h +++ b/DSFE_App/DSFE_GUI/include/Scene/SimulationManager.h @@ -176,6 +176,7 @@ namespace gui { void setPresentationFBO(GLuint fbo) { _presentationFBO = fbo; } void syncRobotToScene(); + void syncBodyToScene(); // Scene Objects Management void setSelectedObject(scene::Object* obj); @@ -194,16 +195,20 @@ namespace gui { void clearRobot(); const bool hasRobot() const; + const bool hasBody() const; + // Setters for robot joint states (angle in radians) void setRobotLinkRotation(const std::string& linkName, double angle); void setRobotRootPose(const mathlib::Vec3& pos, mathlib::Quat& rot); void setRobotRootHome(const mathlib::Vec3& pos, mathlib::Quat& rot); - // Accesors for the robot system (non-const and const versions) robots::RobotSystem* robotSystem(); const robots::RobotSystem* robotSystem() const; + single_body_system::SingleBodySystem* singleBodySystem(); + const single_body_system::SingleBodySystem* singleBodySystem() const; + // Accessors for the trajectory manager (non-const and const versions) control::TrajectoryManager* traj(); const control::TrajectoryManager* traj() const; diff --git a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp index afd39d0a..2fa785e9 100644 --- a/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp +++ b/DSFE_App/DSFE_GUI/src/MainWindow/DSFE_MainWindow.cpp @@ -128,6 +128,9 @@ namespace window { LOG_INFO("Menu clicked: Project -> Load Mesh"); QString path = QFileDialog::getOpenFileName(nullptr, "Select Mesh File", QString::fromStdString((paths::assets() / "objects" / "Shapes").string()), "Mesh Files(*.obj * .fbx * .gltf * .dae * .stl)"); if (path.isEmpty()) { return; } + // Covert path name to just the file name without extension or path + std::string bodyName = QFileInfo(path).baseName().toStdString(); + _sim->simCore()->loadSingleBody(bodyName); _sim->loadMesh(path.toStdString()); }); auto* loadHDRAction = projectMenu->addAction("Load HDRI"); diff --git a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp index 4a0555aa..51c977e9 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/Manager/SimRobots.cpp @@ -68,11 +68,16 @@ namespace gui { _impl->eeObject = nullptr; } const bool SimManager::hasRobot() const { return _impl->_robotSystem && _impl->_robotSystem->hasRobot(); } + const bool SimManager::hasBody() const { return _impl->_singleBody && _impl->_singleBody->hasBody(); } // Access the robot system (non-const and const versions) robots::RobotSystem* SimManager::robotSystem() { return _core->robotSystem(); } const robots::RobotSystem* SimManager::robotSystem() const { return _core->robotSystem(); } + // Access the single body system (non-const and const versions) + single_body_system::SingleBodySystem* SimManager::singleBodySystem() { return _core->singleBodySystem(); } + const single_body_system::SingleBodySystem* SimManager::singleBodySystem() const { return _core->singleBodySystem(); } + // Access the trajectory manager (non-const and const versions) control::TrajectoryManager* SimManager::traj() { return _core->trajectoryManager(); } const control::TrajectoryManager* SimManager::traj() const { return _core->trajectoryManager(); } diff --git a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp index 281ee456..58c1dbb6 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp @@ -12,6 +12,7 @@ extern "C" core::ISimulationCore* CreateSimulationCore_v1(); extern "C" void DestroySimulationCore(core::ISimulationCore*); #include "Manager/SimImplementation.h" +#include "SingleBodySystems/SingleBodySystem.h" #include "Platform/KeyCode.h" #include @@ -210,6 +211,25 @@ namespace gui { } } + void SimManager::syncBodyToScene() { + if (!hasBody()) { return; } + auto* sys = singleBodySystem(); + const auto& body = sys->body(); + ; + auto it = _impl->_linkToObjects.find(body->name); + if (it == _impl->_linkToObjects.end()) return; + + // For a single body, world transform are redundant, in fact the body->transform is already in world space so we can + auto& state = body->state; + for (scene::Object* obj : it->second) { + if (!obj) { continue; } + glm::vec3 pos = toGlm(state.p); + glm::quat q = toGlm(state.q); + obj->transform.position = pos; + obj->transform.rotQ = q; + } + } + void SimManager::resize(int32_t width, int32_t height) { if (width <= 0 || height <= 0) return; _internalSize = { (float)width, (float)height }; From beccf8ee1198afa8ebfb922d2e7a965f1671b14b Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 16 Jun 2026 11:42:32 +0100 Subject: [PATCH 065/110] refactor: Moved to `CMakePresets` focused build system, preparing for eventual extension to linux-based OS operation. --- .gitignore | 3 + CMakeLists.txt | 9 +- CMakePresets.json | 106 +++++++++--------- DSFE_App/CMakeLists.txt | 6 +- DSFE_App/DSFE_Core/CMakeLists.txt | 3 +- DSFE_App/DSFE_Engine/CMakeLists.txt | 2 - DSFE_App/DSFE_GUI/CMakeLists.txt | 6 +- .../DSFE_GUI/src/Scene/SimulationManager.cpp | 3 - .../CMakeLists.txt | 3 - PxMLib/MathLib_Unit/CMakeLists.txt | 3 - .../DualNumberTests/CMakeLists.txt | 3 - .../IntegratorTests/CMakeLists.txt | 3 - PxMLib/PhysLib_Unit/CMakeLists.txt | 3 - .../SingleBodySystemTests/CMakeLists.txt | 3 - 14 files changed, 68 insertions(+), 88 deletions(-) diff --git a/.gitignore b/.gitignore index a58b3aa9..6cdcb229 100644 --- a/.gitignore +++ b/.gitignore @@ -476,3 +476,6 @@ vcpkg_downloads/ !CMakeLists.txt !**/imgui.ini +/build-ninja +/CMakeFiles +/_deps diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a712153..427e7beb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,15 @@ # CMakeLists.txt for DSFE project (root CMakeLists.txt) cmake_minimum_required(VERSION 3.17) - project(DSFE LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") -# Set common output directories for all targets -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(OUTPUT_BASE ${CMAKE_BINARY_DIR}) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${OUTPUT_BASE}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${OUTPUT_BASE}/lib) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${OUTPUT_BASE}/lib) include(FetchContent) diff --git a/CMakePresets.json b/CMakePresets.json index 6c6c0187..6780ead5 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,81 +1,87 @@ { - "version": 3, + "version": 8, "configurePresets": [ { - "name": "windows-base", + "name": "base", "hidden": true, - "generator": "Ninja", "binaryDir": "${sourceDir}/out/build/${presetName}", - "installDir": "${sourceDir}/out/install/${presetName}", "cacheVariables": { - "CMAKE_C_COMPILER": "cl.exe", - "CMAKE_CXX_COMPILER": "cl.exe" - }, + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "windows-x64", + "displayName": "Windows x64", + "inherits": "base", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" - } - }, - { - "name": "x64-debug", - "displayName": "x64 Debug", - "inherits": "windows-base", + }, + "generator": "Visual Studio 18 2026", + "cacheVariables": { + "CMAKE_PREFIX_PATH": "C:/Qt/6.11.1/msvc2022_64" + }, "architecture": { "value": "x64", "strategy": "external" - }, - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" } }, { - "name": "x64-release", - "displayName": "x64 Release", - "inherits": "x64-debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - }, - { - "name": "x86-debug", - "displayName": "x86 Debug", - "inherits": "windows-base", - "architecture": { - "value": "x86", - "strategy": "external" + "name": "linux-x64-debug", + "displayName": "Linux x64 Debug", + "inherits": "base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" }, + "generator": "Ninja", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" + }, + "architecture": { + "value": "x64", + "strategy": "external" } }, { - "name": "x86-release", - "displayName": "x86 Release", - "inherits": "x86-debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - }, - { - "name": "linux-debug", - "displayName": "Linux Debug", - "generator": "Ninja", - "binaryDir": "${sourceDir}/out/build/${presetName}", - "installDir": "${sourceDir}/out/install/${presetName}", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - }, + "name": "linux-x64-release", + "displayName": "Linux x64 Release", + "inherits": "base", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" }, - "vendor": { - "microsoft.com/VisualStudioRemoteSettings/CMake/1.0": { - "sourceDir": "$env{HOME}/.vs/$ms{projectDirName}" - } + "generator": "Ninja", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + }, + "architecture": { + "value": "x64", + "strategy": "external" } } + ], + "buildPresets": [ + { + "name": "windows-x64-debug", + "configurePreset": "windows-x64", + "configuration": "Debug" + }, + { + "name": "windows-x64-release", + "configurePreset": "windows-x64", + "configuration": "Release" + }, + { + "name": "linux-x64-debug", + "configurePreset": "linux-x64-debug" + }, + { + "name": "linux-x64-release", + "configurePreset": "linux-x64-release" + } ] } diff --git a/DSFE_App/CMakeLists.txt b/DSFE_App/CMakeLists.txt index 6f40143c..6c33a113 100644 --- a/DSFE_App/CMakeLists.txt +++ b/DSFE_App/CMakeLists.txt @@ -1,8 +1,4 @@ -# CMakeLists.txt for the DSFE_App project -cmake_minimum_required(VERSION 3.17) -project(DSFE_App LANGUAGES C CXX) - -# Add projects within the DSFE_App directory +# CMakeLists.txt for the DSFE_App add_subdirectory(DSFE_Core) add_subdirectory(DSFE_GUI) add_subdirectory(DSFE_Engine) diff --git a/DSFE_App/DSFE_Core/CMakeLists.txt b/DSFE_App/DSFE_Core/CMakeLists.txt index d8f452b8..60357fd8 100644 --- a/DSFE_App/DSFE_Core/CMakeLists.txt +++ b/DSFE_App/DSFE_Core/CMakeLists.txt @@ -9,7 +9,7 @@ find_package(HDF5 CONFIG REQUIRED) target_compile_definitions(DSFE_Core PRIVATE DSFE_CORE_EXPORTS) if (MSVC) -target_compile_options(DSFE_Core PRIVATE /bigobj) + target_compile_options(DSFE_Core PRIVATE /bigobj) endif() set(DEP_CORE_SRC @@ -86,7 +86,6 @@ endif() target_include_directories(DSFE_Core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/include - ${HDF5_INCLUDE_DIRS} ) diff --git a/DSFE_App/DSFE_Engine/CMakeLists.txt b/DSFE_App/DSFE_Engine/CMakeLists.txt index 654bfe42..50beccb0 100644 --- a/DSFE_App/DSFE_Engine/CMakeLists.txt +++ b/DSFE_App/DSFE_Engine/CMakeLists.txt @@ -1,6 +1,4 @@ # CMakeLists.txt for the DSFE_Engine executable -cmake_minimum_required(VERSION 3.17) -project(DSFE_Engine LANGUAGES C CXX) find_package(Threads REQUIRED) diff --git a/DSFE_App/DSFE_GUI/CMakeLists.txt b/DSFE_App/DSFE_GUI/CMakeLists.txt index 031a2de9..92c52f02 100644 --- a/DSFE_App/DSFE_GUI/CMakeLists.txt +++ b/DSFE_App/DSFE_GUI/CMakeLists.txt @@ -1,13 +1,13 @@ # CMakeLists.txt for DSFE_GUI module -set(CMAKE_POSITION_INDEPENDENT_CODE ON) - cmake_minimum_required(VERSION 3.17) project(DSFE_GUI LANGUAGES C CXX) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) -set(CMAKE_PREFIX_PATH "C:/Qt/6.11.1/msvc2022_64") +#set(CMAKE_PREFIX_PATH "C:/Qt/6.11.1/msvc2022_64") add_library(DSFE_GUI SHARED) diff --git a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp index 58c1dbb6..b2f372f3 100644 --- a/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp +++ b/DSFE_App/DSFE_GUI/src/Scene/SimulationManager.cpp @@ -8,9 +8,6 @@ #include #include -extern "C" core::ISimulationCore* CreateSimulationCore_v1(); -extern "C" void DestroySimulationCore(core::ISimulationCore*); - #include "Manager/SimImplementation.h" #include "SingleBodySystems/SingleBodySystem.h" #include "Platform/KeyCode.h" diff --git a/PxMLib/MathLib_Unit/AutomaticDifferentiationTests/CMakeLists.txt b/PxMLib/MathLib_Unit/AutomaticDifferentiationTests/CMakeLists.txt index 547e2f60..9c86dbff 100644 --- a/PxMLib/MathLib_Unit/AutomaticDifferentiationTests/CMakeLists.txt +++ b/PxMLib/MathLib_Unit/AutomaticDifferentiationTests/CMakeLists.txt @@ -1,7 +1,4 @@ # CMakeLists.txt for MathLib_Unit_Tests executable -cmake_minimum_required(VERSION 3.17) -project(MathLib_ADTests LANGUAGES C CXX) - enable_testing() add_executable(MathLib_ADTests diff --git a/PxMLib/MathLib_Unit/CMakeLists.txt b/PxMLib/MathLib_Unit/CMakeLists.txt index 9bf06a0e..d8ede159 100644 --- a/PxMLib/MathLib_Unit/CMakeLists.txt +++ b/PxMLib/MathLib_Unit/CMakeLists.txt @@ -1,7 +1,4 @@ # CMakeLists.txt for MathLib_Unit_Tests executable -cmake_minimum_required(VERSION 3.17) -project(MathLib_Unit_Tests LANGUAGES C CXX) - add_subdirectory(IntegratorTests) add_subdirectory(DualNumberTests) add_subdirectory(AutomaticDifferentiationTests) \ No newline at end of file diff --git a/PxMLib/MathLib_Unit/DualNumberTests/CMakeLists.txt b/PxMLib/MathLib_Unit/DualNumberTests/CMakeLists.txt index abc162ed..5af97497 100644 --- a/PxMLib/MathLib_Unit/DualNumberTests/CMakeLists.txt +++ b/PxMLib/MathLib_Unit/DualNumberTests/CMakeLists.txt @@ -1,7 +1,4 @@ # CMakeLists.txt for MathLib_Unit_Tests executable -cmake_minimum_required(VERSION 3.17) -project(MathLib_DualNumberTests LANGUAGES C CXX) - enable_testing() add_executable(MathLib_DualNumberTests diff --git a/PxMLib/MathLib_Unit/IntegratorTests/CMakeLists.txt b/PxMLib/MathLib_Unit/IntegratorTests/CMakeLists.txt index f04ac90a..c50df7ce 100644 --- a/PxMLib/MathLib_Unit/IntegratorTests/CMakeLists.txt +++ b/PxMLib/MathLib_Unit/IntegratorTests/CMakeLists.txt @@ -1,7 +1,4 @@ # CMakeLists.txt for MathLib_Unit_Tests executable -cmake_minimum_required(VERSION 3.17) -project(MathLib_IntegratorTests LANGUAGES C CXX) - enable_testing() add_executable(MathLib_IntegratorTests diff --git a/PxMLib/PhysLib_Unit/CMakeLists.txt b/PxMLib/PhysLib_Unit/CMakeLists.txt index 7cc7e428..1629f64c 100644 --- a/PxMLib/PhysLib_Unit/CMakeLists.txt +++ b/PxMLib/PhysLib_Unit/CMakeLists.txt @@ -1,5 +1,2 @@ # CMakeLists.txt for PhysLib_Unit_Tests executable -cmake_minimum_required(VERSION 3.17) -project(PhysLib_Unit_Tests LANGUAGES C CXX) - add_subdirectory(SingleBodySystemTests) \ No newline at end of file diff --git a/PxMLib/PhysLib_Unit/SingleBodySystemTests/CMakeLists.txt b/PxMLib/PhysLib_Unit/SingleBodySystemTests/CMakeLists.txt index c9779dbe..0ed8ef16 100644 --- a/PxMLib/PhysLib_Unit/SingleBodySystemTests/CMakeLists.txt +++ b/PxMLib/PhysLib_Unit/SingleBodySystemTests/CMakeLists.txt @@ -1,7 +1,4 @@ # CMakeLists.txt for MathLib_Unit_Tests executable -cmake_minimum_required(VERSION 3.17) -project(PhysLib_SingleBodySystemTests LANGUAGES C CXX) - enable_testing() add_executable(PhysLib_SingleBodySystemTests From 40debc1a17718cf3f5da857b8917f2937670f6cd Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 16 Jun 2026 11:57:58 +0100 Subject: [PATCH 066/110] refactor: Updated `build.yml` to use preset logic * This is a test, if it works, I will move the toolchain logic into the presets too! --- .github/workflows/build.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a70e721..d18d3d3d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,20 +25,25 @@ jobs: run: | .\vcpkg\vcpkg.exe install hdf5:x64-windows + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + version: 6.11.1 + arch: win64_msvc2022_64 + - name: Configure shell: pwsh run: | - cmake -S . -B build ` - -A x64 ` + cmake --preset windows-x64 ` -DVCPKG_TARGET_TRIPLET=x64-windows ` -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake" - name: Build shell: pwsh run: | - cmake --build build --config Release --verbose + cmake --build --preset windows-x64-release --verbose - name: Run tests shell: pwsh run: | - ctest --test-dir build --output-on-failure + ctest --preset windows-x64-release --output-on-failure From cf0dd241895f69f08daa147d1bd8d246e72002a4 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 16 Jun 2026 12:08:26 +0100 Subject: [PATCH 067/110] fixes: Try different Qt6 version for workflow * Added a temporary CI step for checking the available versions --- .github/workflows/build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d18d3d3d..56ccfe0c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,10 +25,16 @@ jobs: run: | .\vcpkg\vcpkg.exe install hdf5:x64-windows + - name: List Qt versions (debug) + shell: pwsh + run: | + python -m pip install aqtinstall + python -m aqt list-qt windows desktop + - name: Install Qt uses: jurplel/install-qt-action@v4 with: - version: 6.11.1 + version: 6.6.2 arch: win64_msvc2022_64 - name: Configure From a473b311fcb8a10f52e617098afb24ec23fd9480 Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 16 Jun 2026 12:15:31 +0100 Subject: [PATCH 068/110] fixes --- .github/workflows/build.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 56ccfe0c..ed31bb47 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,18 +25,6 @@ jobs: run: | .\vcpkg\vcpkg.exe install hdf5:x64-windows - - name: List Qt versions (debug) - shell: pwsh - run: | - python -m pip install aqtinstall - python -m aqt list-qt windows desktop - - - name: Install Qt - uses: jurplel/install-qt-action@v4 - with: - version: 6.6.2 - arch: win64_msvc2022_64 - - name: Configure shell: pwsh run: | From 4bad85f7f10cb7f7da6f8b064dd2cf294b76f05f Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 16 Jun 2026 12:28:18 +0100 Subject: [PATCH 069/110] fixes --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed31bb47..fc7523c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,6 +25,11 @@ jobs: run: | .\vcpkg\vcpkg.exe install hdf5:x64-windows + - name: Install Qt + shell: pwsh + run: | + .\vcpkg\vcpkg.exe install qtbase:x64-windows + - name: Configure shell: pwsh run: | From 941c9ada428d09e0112c22111e38dad94a2bae3d Mon Sep 17 00:00:00 2001 From: saltyjoss Date: Tue, 16 Jun 2026 12:45:15 +0100 Subject: [PATCH 070/110] feat: Added `DSFE_ENABLE_GUI` compile time flag, influencing the inclusion of `DSFE_GUI` at runtime. --- DSFE_App/DSFE_Engine/CMakeLists.txt | 8 +++-- DSFE_App/DSFE_Engine/src/Main.cpp | 56 +++++++++++++++++------------ 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/DSFE_App/DSFE_Engine/CMakeLists.txt b/DSFE_App/DSFE_Engine/CMakeLists.txt index 50beccb0..9778b916 100644 --- a/DSFE_App/DSFE_Engine/CMakeLists.txt +++ b/DSFE_App/DSFE_Engine/CMakeLists.txt @@ -1,5 +1,5 @@ # CMakeLists.txt for the DSFE_Engine executable - +option(DSFE_ENABLE_GUI "Enable GUI module" ON) find_package(Threads REQUIRED) add_executable(DSFE_Engine @@ -12,11 +12,15 @@ target_include_directories(DSFE_Engine PRIVATE ) target_link_libraries(DSFE_Engine PRIVATE - DSFE_GUI DSFE_Core Threads::Threads ) +if (DSFE_ENABLE_GUI) + target_link_libraries(DSFE_Engine PRIVATE DSFE_GUI) + target_compile_definitions(DSFE_Engine PRIVATE DSFE_ENABLE_GUI) +endif() + add_custom_command(TARGET DSFE_Engine POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" diff --git a/DSFE_App/DSFE_Engine/src/Main.cpp b/DSFE_App/DSFE_Engine/src/Main.cpp index 571390d0..c477399a 100644 --- a/DSFE_App/DSFE_Engine/src/Main.cpp +++ b/DSFE_App/DSFE_Engine/src/Main.cpp @@ -4,10 +4,13 @@ #include #include #include -#include #include "BatchEntry.h" #include "BatchArgs.h" +#ifdef DSFE_ENABLE_GUI + #include +#endif + // Helper function to split a comma-separated string into a vector of strings, trimming whitespace static std::vector splitComma(const std::string& input) { std::vector result; @@ -46,40 +49,47 @@ int main(int argc, char** argv) { batchMode = true; break; } + else if (arg == "--about") { + std::cout + << "DSFE (Dynamic Systems Framework Engine)\n" + << " > A research-focused simulation engine for numerically modelling dynamic systems with different integration methods (explicit and implicit).\n" + << " > Version 0.9.1-alpha\n" + << " > Developed by Joss Salton\n"; + return 0; + } + else if (arg == "--help" || arg == "-h") { + std::cout + << "Usage:\n" + << " --batch -t