From c977332008654ee69d98385a52b85027313c0855 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Tue, 17 Dec 2024 22:19:46 +0800 Subject: [PATCH 1/5] quickviz app: disabled main docking space resize --- src/app/panels/console_panel.cpp | 2 +- src/app/panels/main_docking_panel.cpp | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/panels/console_panel.cpp b/src/app/panels/console_panel.cpp index 8252bb9..a8be41f 100644 --- a/src/app/panels/console_panel.cpp +++ b/src/app/panels/console_panel.cpp @@ -13,7 +13,7 @@ namespace quickviz { ConsolePanel::ConsolePanel(std::string name) : Panel(name) { this->SetAutoLayout(false); - // this->SetNoResize(true); + this->SetNoResize(true); this->SetNoMove(true); this->SetWindowNoMenuButton(); diff --git a/src/app/panels/main_docking_panel.cpp b/src/app/panels/main_docking_panel.cpp index 695ce3e..107e91e 100644 --- a/src/app/panels/main_docking_panel.cpp +++ b/src/app/panels/main_docking_panel.cpp @@ -14,8 +14,8 @@ namespace quickviz { MainDockingPanel::MainDockingPanel(std::string name) : Panel(name) { this->SetAutoLayout(true); - this->SetNoMove(true); this->SetNoResize(true); + this->SetNoMove(true); this->SetNoTitleBar(true); this->SetNoBackground(true); @@ -31,9 +31,13 @@ void MainDockingPanel::Draw() { // set up layout Begin(); + + // set up dockspace + ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_NoResize; + ImGui::DockSpace(dockspace_id_, ImGui::GetWindowSize(), dockspace_flags); { if (!layout_initialized_) { - dockspace_id_ = ImGui::DockBuilderAddNode(); + dockspace_id_ = ImGui::DockBuilderAddNode(dockspace_id_, dockspace_flags); ImGui::DockBuilderSplitNode(dockspace_id_, ImGuiDir_Left, 0.2f, &config_panel_node_, &gl_scene_widget_node_); From 7b358e7271b431a87a7faf4182df14ddf347049a Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Tue, 17 Dec 2024 23:04:16 +0800 Subject: [PATCH 2/5] log: added log handler --- src/app/CMakeLists.txt | 2 - src/app/panels/console_panel.cpp | 31 ++++++------ src/app/panels/console_panel.hpp | 6 +-- src/app/panels/scene_panel.cpp | 7 +++ src/imview/CMakeLists.txt | 2 + .../component/logging/app_log_handler.hpp | 49 +++++++++++++++++++ .../component/logging}/log_processor.hpp | 0 .../src/component/logging/app_log_handler.cpp | 37 ++++++++++++++ .../src/component/logging}/log_processor.cpp | 4 +- 9 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 src/imview/include/imview/component/logging/app_log_handler.hpp rename src/{app/component => imview/include/imview/component/logging}/log_processor.hpp (100%) create mode 100644 src/imview/src/component/logging/app_log_handler.cpp rename src/{app/component => imview/src/component/logging}/log_processor.cpp (97%) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 57e4787..8fea69b 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,9 +1,7 @@ if (BUILD_QUICKVIZ_APP) message(STATUS "Build quickviz application") add_executable(quickviz main.cpp - # components quickviz_application.cpp - component/log_processor.cpp # panels panels/menu_bar.cpp panels/main_docking_panel.cpp diff --git a/src/app/panels/console_panel.cpp b/src/app/panels/console_panel.cpp index a8be41f..6adba1f 100644 --- a/src/app/panels/console_panel.cpp +++ b/src/app/panels/console_panel.cpp @@ -9,6 +9,7 @@ #include "panels/console_panel.hpp" #include "imview/fonts.hpp" +#include "imview/component/logging/app_log_handler.hpp" namespace quickviz { ConsolePanel::ConsolePanel(std::string name) : Panel(name) { @@ -17,24 +18,26 @@ ConsolePanel::ConsolePanel(std::string name) : Panel(name) { this->SetNoMove(true); this->SetWindowNoMenuButton(); - static int counter = 0; - const char* categories[3] = {"info", "warn", "error"}; - const char* words[] = {"Bumfuzzled", "Cattywampus", "Snickersnee", - "Abibliophobia", "Absquatulate", "Nincompoop", - "Pauciloquent"}; - for (int n = 0; n < 5; n++) { - const char* category = categories[counter % IM_ARRAYSIZE(categories)]; - const char* word = words[counter % IM_ARRAYSIZE(words)]; - log_.AddLog( - "[%05d] [%s] Hello, current time is %.1f, here's a word: '%s'\n", - ImGui::GetFrameCount(), category, ImGui::GetTime(), word); - counter++; - } + // static int counter = 0; + // const char* categories[3] = {"info", "warn", "error"}; + // const char* words[] = {"Bumfuzzled", "Cattywampus", "Snickersnee", + // "Abibliophobia", "Absquatulate", "Nincompoop", + // "Pauciloquent"}; + // for (int n = 0; n < 5; n++) { + // const char* category = categories[counter % IM_ARRAYSIZE(categories)]; + // const char* word = words[counter % IM_ARRAYSIZE(words)]; + // log_.AddLog( + // "[%05d] [%s] Hello, current time is %.1f, here's a word: '%s'\n", + // ImGui::GetFrameCount(), category, ImGui::GetTime(), word); + // counter++; + // } + AppLogHandler::GetInstance().Log(LogLevel::kInfo, "ConsolePanel initialized"); } void ConsolePanel::Draw() { Begin(); - log_.Draw("Example: Log"); + // log_.Draw("Example: Log"); + AppLogHandler::GetInstance().Draw(); End(); } } // namespace quickviz \ No newline at end of file diff --git a/src/app/panels/console_panel.hpp b/src/app/panels/console_panel.hpp index 03eb165..ddd9152 100644 --- a/src/app/panels/console_panel.hpp +++ b/src/app/panels/console_panel.hpp @@ -10,17 +10,13 @@ #define QUICKVIZ_CONSOLE_PANEL_HPP #include "imview/panel.hpp" -#include "component/log_processor.hpp" namespace quickviz { class ConsolePanel : public Panel { public: - ConsolePanel(std::string name = "Debug"); + ConsolePanel(std::string name = "Console"); void Draw() override; - - private: - LogProcessor log_; }; } // namespace quickviz diff --git a/src/app/panels/scene_panel.cpp b/src/app/panels/scene_panel.cpp index 8a5682c..899af8b 100644 --- a/src/app/panels/scene_panel.cpp +++ b/src/app/panels/scene_panel.cpp @@ -13,6 +13,8 @@ #include #include +#include "imview/component/logging/app_log_handler.hpp" + namespace quickviz { ScenePanel::ScenePanel(const std::string& panel_name) : GlWidget(panel_name) { this->SetNoMove(true); @@ -36,6 +38,11 @@ void ScenePanel::Draw() { glm::mat4 projection = camera_->GetProjectionMatrix(aspect_ratio); glm::mat4 view = camera_->GetViewMatrix(); + if (ImGui::IsWindowHovered()) { + std::cerr << "ScenePanel is hovered" << std::endl; + AppLogHandler::GetInstance().Log(LogLevel::kInfo, "ScenePanel is hovered"); + } + UpdateView(projection, view); GlWidget::Draw(); diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index 2f44070..f3d96eb 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -45,6 +45,8 @@ add_library(imview src/component/buffer/scrolling_plot_buffer.cpp src/component/event/event_dispatcher.cpp src/component/event/async_event_dispatcher.cpp + src/component/logging/log_processor.cpp + src/component/logging/app_log_handler.cpp ) target_link_libraries(imview PUBLIC imcore PkgConfig::Cairo PkgConfig::Fontconfig diff --git a/src/imview/include/imview/component/logging/app_log_handler.hpp b/src/imview/include/imview/component/logging/app_log_handler.hpp new file mode 100644 index 0000000..d539de9 --- /dev/null +++ b/src/imview/include/imview/component/logging/app_log_handler.hpp @@ -0,0 +1,49 @@ +/* + * @file app_log_handler.hpp + * @date 12/17/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef APP_LOG_HANDLER_HPP +#define APP_LOG_HANDLER_HPP + +#include "imview/component/logging/log_processor.hpp" + +#include +#include + +namespace quickviz { +enum class LogLevel { kDebug, kInfo, kWarn, kError, kFatal }; + +class AppLogHandler { + using Clock = std::chrono::steady_clock; + using TimePoint = Clock::time_point; + + AppLogHandler() { t0_ = Clock::now(); } + ~AppLogHandler() = default; + + public: + static AppLogHandler& GetInstance() { + static AppLogHandler instance; + return instance; + } + + void Log(LogLevel level, const char* fmt, ...); + void Draw(); + + private: + LogProcessor processor_; + TimePoint t0_; + + std::unordered_map level_str_map_ = { + {LogLevel::kDebug, "DEBUG"}, + {LogLevel::kInfo, "INFO "}, + {LogLevel::kWarn, "WARN "}, + {LogLevel::kError, "ERROR"}, + {LogLevel::kFatal, "FATAL"}}; +}; +} // namespace quickviz + +#endif // APP_LOG_HANDLER_HPP \ No newline at end of file diff --git a/src/app/component/log_processor.hpp b/src/imview/include/imview/component/logging/log_processor.hpp similarity index 100% rename from src/app/component/log_processor.hpp rename to src/imview/include/imview/component/logging/log_processor.hpp diff --git a/src/imview/src/component/logging/app_log_handler.cpp b/src/imview/src/component/logging/app_log_handler.cpp new file mode 100644 index 0000000..505a750 --- /dev/null +++ b/src/imview/src/component/logging/app_log_handler.cpp @@ -0,0 +1,37 @@ +/* + * @file app_log_handler.cpp + * @date 12/17/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "imview/component/logging/app_log_handler.hpp" + +#include +#include +#include + +namespace quickviz { +void AppLogHandler::Log(LogLevel level, const char* fmt, ...) { + auto duration = Clock::now() - t0_; + // Split into seconds and fractional nanoseconds + auto seconds = + std::chrono::duration_cast(duration).count(); + auto microseconds = + std::chrono::duration_cast(duration).count() % + 1'000'000; + + // Format into string + std::ostringstream timestamp; + timestamp << std::setfill('0') << std::setw(10) << seconds + << "." // Fixed-width seconds + << std::setfill('0') << std::setw(6) + << microseconds; // Microseconds + + processor_.AddLog("[%s] [%s] %s\n", timestamp.str().c_str(), + level_str_map_[level], fmt); +} + +void AppLogHandler::Draw() { processor_.Draw("AppLog"); } +} // namespace quickviz \ No newline at end of file diff --git a/src/app/component/log_processor.cpp b/src/imview/src/component/logging/log_processor.cpp similarity index 97% rename from src/app/component/log_processor.cpp rename to src/imview/src/component/logging/log_processor.cpp index a18a38d..1f92d7c 100644 --- a/src/app/component/log_processor.cpp +++ b/src/imview/src/component/logging/log_processor.cpp @@ -1,12 +1,12 @@ /* * @file log_processor.cpp * @date 11/3/24 - * @brief + * @brief adapted from imgui_demo.cpp * * @copyright Copyright (c) 2024 Ruixiang Du (rdu) */ -#include "component/log_processor.hpp" +#include "imview/component/logging/log_processor.hpp" namespace quickviz { LogProcessor::LogProcessor() { From b9a0bd78da13aa2a5fe9615916d4f3fde5034577 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Wed, 18 Dec 2024 00:00:04 +0800 Subject: [PATCH 3/5] log: fixed log to handle arguments properly --- src/app/panels/scene_panel.cpp | 24 +++++++++++++- .../component/logging/app_log_handler.hpp | 31 ++++++++++++++++++- .../src/component/logging/app_log_handler.cpp | 24 -------------- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/src/app/panels/scene_panel.cpp b/src/app/panels/scene_panel.cpp index 899af8b..273e13e 100644 --- a/src/app/panels/scene_panel.cpp +++ b/src/app/panels/scene_panel.cpp @@ -39,10 +39,32 @@ void ScenePanel::Draw() { glm::mat4 view = camera_->GetViewMatrix(); if (ImGui::IsWindowHovered()) { - std::cerr << "ScenePanel is hovered" << std::endl; AppLogHandler::GetInstance().Log(LogLevel::kInfo, "ScenePanel is hovered"); } + ImGuiIO& io = ImGui::GetIO(); + ImVec2 windowPos = ImGui::GetWindowPos(); + + ImVec2 localMousePos = + ImVec2(io.MousePos.x - windowPos.x, io.MousePos.y - windowPos.y); + + if (io.WantCaptureMouse) { + if (ImGui::IsMousePosValid()) { + AppLogHandler::GetInstance().Log(LogLevel::kInfo, "Mouse pos: (%f, %f)", + io.MousePos.x, io.MousePos.y); + } else { + AppLogHandler::GetInstance().Log(LogLevel::kInfo, "Mouse pos: "); + } + + for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) + if (ImGui::IsMouseDown(i)) { + ImGui::SameLine(); + // ImGui::Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]); + AppLogHandler::GetInstance().Log(LogLevel::kInfo, + "Mouse button %d down", i); + } + } + UpdateView(projection, view); GlWidget::Draw(); diff --git a/src/imview/include/imview/component/logging/app_log_handler.hpp b/src/imview/include/imview/component/logging/app_log_handler.hpp index d539de9..9a74e6c 100644 --- a/src/imview/include/imview/component/logging/app_log_handler.hpp +++ b/src/imview/include/imview/component/logging/app_log_handler.hpp @@ -12,6 +12,8 @@ #include "imview/component/logging/log_processor.hpp" #include +#include +#include #include namespace quickviz { @@ -30,7 +32,34 @@ class AppLogHandler { return instance; } - void Log(LogLevel level, const char* fmt, ...); + // void Log(LogLevel level, const char* fmt, ...); + template + void Log(LogLevel level, const char* fmt, Args&&... args) { + // generate timestamp + auto duration = Clock::now() - t0_; + auto seconds = + std::chrono::duration_cast(duration).count(); + auto microseconds = + std::chrono::duration_cast(duration) + .count() % + 1'000'000; + std::ostringstream timestamp; + timestamp << std::setfill('0') << std::setw(10) << seconds + << "." // Fixed-width seconds + << std::setfill('0') << std::setw(6) + << microseconds; // Microseconds + + // forward fixed arguments and variable arguments to AddLog + processor_.AddLog("[%s][%s] ", timestamp.str().c_str(), + level_str_map_[level]); + if constexpr (sizeof...(args) > 0) { + processor_.AddLog(fmt, std::forward(args)...); + } else { + processor_.AddLog("%s", fmt); // Treat fmt as a regular string + } + processor_.AddLog("\n"); + } + void Draw(); private: diff --git a/src/imview/src/component/logging/app_log_handler.cpp b/src/imview/src/component/logging/app_log_handler.cpp index 505a750..3c567ee 100644 --- a/src/imview/src/component/logging/app_log_handler.cpp +++ b/src/imview/src/component/logging/app_log_handler.cpp @@ -8,30 +8,6 @@ #include "imview/component/logging/app_log_handler.hpp" -#include -#include -#include - namespace quickviz { -void AppLogHandler::Log(LogLevel level, const char* fmt, ...) { - auto duration = Clock::now() - t0_; - // Split into seconds and fractional nanoseconds - auto seconds = - std::chrono::duration_cast(duration).count(); - auto microseconds = - std::chrono::duration_cast(duration).count() % - 1'000'000; - - // Format into string - std::ostringstream timestamp; - timestamp << std::setfill('0') << std::setw(10) << seconds - << "." // Fixed-width seconds - << std::setfill('0') << std::setw(6) - << microseconds; // Microseconds - - processor_.AddLog("[%s] [%s] %s\n", timestamp.str().c_str(), - level_str_map_[level], fmt); -} - void AppLogHandler::Draw() { processor_.Draw("AppLog"); } } // namespace quickviz \ No newline at end of file From d78503ee9dec3f926bb58b2e604e09083ab31162 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Wed, 18 Dec 2024 20:15:07 +0800 Subject: [PATCH 4/5] camera: fixed camera for quickviz app --- docs/opengl/camera_axes.png | Bin 0 -> 44565 bytes src/app/panels/console_panel.cpp | 15 +---- src/app/panels/main_docking_panel.cpp | 1 + src/app/panels/scene_panel.cpp | 56 ++++++++---------- src/app/panels/scene_panel.hpp | 8 +++ .../imview/component/opengl/camera.hpp | 1 + .../component/opengl/camera_controller.hpp | 10 +++- .../include/imview/widget/gl_widget.hpp | 1 + src/imview/src/component/opengl/camera.cpp | 16 +++++ .../component/opengl/camera_controller.cpp | 16 ++++- src/imview/src/widget/gl_widget.cpp | 51 ++++++++-------- 11 files changed, 100 insertions(+), 75 deletions(-) create mode 100644 docs/opengl/camera_axes.png diff --git a/docs/opengl/camera_axes.png b/docs/opengl/camera_axes.png new file mode 100644 index 0000000000000000000000000000000000000000..88f00753906c59ac3db306e5480684b790032753 GIT binary patch literal 44565 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 this->SetWindowNoMenuButton(); - // static int counter = 0; - // const char* categories[3] = {"info", "warn", "error"}; - // const char* words[] = {"Bumfuzzled", "Cattywampus", "Snickersnee", - // "Abibliophobia", "Absquatulate", "Nincompoop", - // "Pauciloquent"}; - // for (int n = 0; n < 5; n++) { - // const char* category = categories[counter % IM_ARRAYSIZE(categories)]; - // const char* word = words[counter % IM_ARRAYSIZE(words)]; - // log_.AddLog( - // "[%05d] [%s] Hello, current time is %.1f, here's a word: '%s'\n", - // ImGui::GetFrameCount(), category, ImGui::GetTime(), word); - // counter++; - // } - AppLogHandler::GetInstance().Log(LogLevel::kInfo, "ConsolePanel initialized"); + AppLogHandler::GetInstance().Log(LogLevel::kInfo, "app initialized"); } void ConsolePanel::Draw() { diff --git a/src/app/panels/main_docking_panel.cpp b/src/app/panels/main_docking_panel.cpp index 107e91e..b8496a2 100644 --- a/src/app/panels/main_docking_panel.cpp +++ b/src/app/panels/main_docking_panel.cpp @@ -33,6 +33,7 @@ void MainDockingPanel::Draw() { Begin(); // set up dockspace + dockspace_id_ = ImGui::GetID("MainDockingPanel"); ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_NoResize; ImGui::DockSpace(dockspace_id_, ImGui::GetWindowSize(), dockspace_flags); { diff --git a/src/app/panels/scene_panel.cpp b/src/app/panels/scene_panel.cpp index 273e13e..cc66fb9 100644 --- a/src/app/panels/scene_panel.cpp +++ b/src/app/panels/scene_panel.cpp @@ -22,51 +22,43 @@ ScenePanel::ScenePanel(const std::string& panel_name) : GlWidget(panel_name) { this->SetNoTitleBar(true); this->SetNoBackground(true); - camera_ = - std::make_unique(glm::vec3(0.0f, 3.0f, 8.0f), -90.0f, -25.0f); + camera_ = std::make_unique(); + camera_controller_ = std::make_unique( + *camera_, glm::vec3(0.0f, 6.0f, 8.0f), 0.0f, 25.0f); auto grid = std::make_unique(10.0f, 1.0f, glm::vec3(0.7f, 0.7f, 0.7f)); this->AddOpenGLObject("grid", std::move(grid)); } void ScenePanel::Draw() { - ImVec2 content_size = ImGui::GetContentRegionAvail(); - - // get view matrices from camera - float aspect_ratio = - static_cast(content_size.x) / static_cast(content_size.y); - glm::mat4 projection = camera_->GetProjectionMatrix(aspect_ratio); - glm::mat4 view = camera_->GetViewMatrix(); - - if (ImGui::IsWindowHovered()) { - AppLogHandler::GetInstance().Log(LogLevel::kInfo, "ScenePanel is hovered"); - } + Begin(); + // update view according to user input ImGuiIO& io = ImGui::GetIO(); - ImVec2 windowPos = ImGui::GetWindowPos(); - - ImVec2 localMousePos = - ImVec2(io.MousePos.x - windowPos.x, io.MousePos.y - windowPos.y); - - if (io.WantCaptureMouse) { - if (ImGui::IsMousePosValid()) { - AppLogHandler::GetInstance().Log(LogLevel::kInfo, "Mouse pos: (%f, %f)", - io.MousePos.x, io.MousePos.y); - } else { - AppLogHandler::GetInstance().Log(LogLevel::kInfo, "Mouse pos: "); + // only process mouse delta when mouse position is within the scene panel + if (ImGui::IsMousePosValid() && io.WantCaptureMouse && + ImGui::IsWindowHovered()) { + // track mouse move delta only when the mouse left button is pressed + if (ImGui::IsMouseDown(MouseButton::kLeft)) { + camera_controller_->ProcessMouseMovement(io.MouseDelta.x, + io.MouseDelta.y); } - for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) - if (ImGui::IsMouseDown(i)) { - ImGui::SameLine(); - // ImGui::Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]); - AppLogHandler::GetInstance().Log(LogLevel::kInfo, - "Mouse button %d down", i); - } + // track mouse wheel scroll + camera_controller_->ProcessMouseScroll(io.MouseWheel); } + // get view matrices from camera + ImVec2 content_size = ImGui::GetContentRegionAvail(); + float aspect_ratio = + static_cast(content_size.x) / static_cast(content_size.y); + glm::mat4 projection = camera_->GetProjectionMatrix(aspect_ratio); + glm::mat4 view = camera_->GetViewMatrix(); UpdateView(projection, view); - GlWidget::Draw(); + // finally draw the scene + DrawOpenGLObject(); + + End(); } } // namespace quickviz \ No newline at end of file diff --git a/src/app/panels/scene_panel.hpp b/src/app/panels/scene_panel.hpp index e6f8f9c..c56cbf1 100644 --- a/src/app/panels/scene_panel.hpp +++ b/src/app/panels/scene_panel.hpp @@ -14,9 +14,16 @@ #include "imview/widget/gl_widget.hpp" #include "imview/component/opengl/grid.hpp" #include "imview/component/opengl/camera.hpp" +#include "imview/component/opengl/camera_controller.hpp" namespace quickviz { class ScenePanel : public GlWidget { + enum MouseButton { + kLeft = 0, + kRight = 1, + kMiddle = 2, + }; + public: ScenePanel(const std::string& panel_name); @@ -24,6 +31,7 @@ class ScenePanel : public GlWidget { private: std::unique_ptr camera_; + std::unique_ptr camera_controller_; }; } // namespace quickviz diff --git a/src/imview/include/imview/component/opengl/camera.hpp b/src/imview/include/imview/component/opengl/camera.hpp index 48fd82d..16c43a4 100644 --- a/src/imview/include/imview/component/opengl/camera.hpp +++ b/src/imview/include/imview/component/opengl/camera.hpp @@ -43,6 +43,7 @@ class Camera { enum class Movement { kForward, kBackward, kLeft, kRight, kUp, kDown }; public: + explicit Camera(float fov = default_fov); Camera(glm::vec3 position, float yaw, float pitch, float fov = default_fov); // public methods diff --git a/src/imview/include/imview/component/opengl/camera_controller.hpp b/src/imview/include/imview/component/opengl/camera_controller.hpp index 5657533..153d979 100644 --- a/src/imview/include/imview/component/opengl/camera_controller.hpp +++ b/src/imview/include/imview/component/opengl/camera_controller.hpp @@ -18,7 +18,8 @@ class CameraController { using CameraMovement = Camera::Movement; public: - CameraController(Camera& camera); + CameraController(Camera& camera, glm::vec3 position = {0, 0, 0}, + float yaw = 0, float pitch = 0); void Reset(); void SetMode(Mode mode); @@ -27,13 +28,16 @@ class CameraController { void ProcessMouseScroll(float y_offset); private: + static constexpr float initial_orbit_distance = 10.0f; + static constexpr float initial_top_down_height = 10.0f; + void UpdateOrbitPosition(); Camera& camera_; Mode mode_ = Mode::kOrbit; glm::vec3 orbit_target_ = glm::vec3(0.0f, 0.0f, 0.0f); - float orbit_distance_ = 10.0f; - float top_down_height_ = 10.0f; + float orbit_distance_ = initial_orbit_distance; + float top_down_height_ = initial_top_down_height; }; } // namespace quickviz diff --git a/src/imview/include/imview/widget/gl_widget.hpp b/src/imview/include/imview/widget/gl_widget.hpp index 86dec84..ecbdc62 100644 --- a/src/imview/include/imview/widget/gl_widget.hpp +++ b/src/imview/include/imview/widget/gl_widget.hpp @@ -29,6 +29,7 @@ class GlWidget : public Panel { void RemoveOpenGLObject(const std::string& name); void ClearOpenGLObjects(); void UpdateView(const glm::mat4& projection, const glm::mat4& view); + void DrawOpenGLObject(); void Draw() override; diff --git a/src/imview/src/component/opengl/camera.cpp b/src/imview/src/component/opengl/camera.cpp index 7fc4243..ba17d7c 100644 --- a/src/imview/src/component/opengl/camera.cpp +++ b/src/imview/src/component/opengl/camera.cpp @@ -11,6 +11,22 @@ #include namespace quickviz { +Camera::Camera(float fov) : fov_(fov) { + current_state_.position = glm::vec3(0.0f, 0.0f, 0.0f); + current_state_.yaw = -90.0f; + current_state_.pitch = 0.0f; + + UpdateCameraVectors(); + + // save initial state for reset + initial_state_.position = current_state_.position; + initial_state_.yaw = current_state_.yaw; + initial_state_.pitch = current_state_.pitch; + initial_state_.front = current_state_.front; + initial_state_.up = current_state_.up; + initial_state_.right = current_state_.right; +} + Camera::Camera(glm::vec3 position, float yaw, float pitch, float fov) : fov_(fov) { current_state_.position = position; diff --git a/src/imview/src/component/opengl/camera_controller.cpp b/src/imview/src/component/opengl/camera_controller.cpp index 0c1d29a..c783e13 100644 --- a/src/imview/src/component/opengl/camera_controller.cpp +++ b/src/imview/src/component/opengl/camera_controller.cpp @@ -9,13 +9,25 @@ #include "imview/component/opengl/camera_controller.hpp" namespace quickviz { -CameraController::CameraController(Camera& camera) : camera_(camera) {} +CameraController::CameraController(Camera& camera, glm::vec3 position, + float yaw, float pitch) + : camera_(camera) { + camera_.SetPosition(position); + camera_.SetYaw(yaw); + camera_.SetPitch(pitch); + + orbit_distance_ = glm::length(camera_.GetPosition()); + + UpdateOrbitPosition(); +} void CameraController::Reset() { camera_.Reset(); } void CameraController::SetMode(CameraController::Mode mode) { + if (mode == mode_) return; + mode_ = mode; - if (mode == Mode::kTopDown) { + if (mode_ == Mode::kTopDown) { camera_.SetPosition(glm::vec3(0.0f, top_down_height_, 0.0f)); camera_.SetPitch(-90.0f); camera_.SetYaw(0.0f); diff --git a/src/imview/src/widget/gl_widget.cpp b/src/imview/src/widget/gl_widget.cpp index e2628e8..d5b0959 100644 --- a/src/imview/src/widget/gl_widget.cpp +++ b/src/imview/src/widget/gl_widget.cpp @@ -33,33 +33,36 @@ void GlWidget::UpdateView(const glm::mat4& projection, const glm::mat4& view) { view_ = view; } -void GlWidget::Draw() { - Begin(); - { - ImVec2 content_size = ImGui::GetContentRegionAvail(); - float width = content_size.x; - float height = content_size.y; +void GlWidget::DrawOpenGLObject() { + ImVec2 content_size = ImGui::GetContentRegionAvail(); + float width = content_size.x; + float height = content_size.y; - if (frame_buffer_ != nullptr) { - // render to frame buffer - frame_buffer_->Resize(width, height); - frame_buffer_->Bind(); - for (auto& obj : drawable_objects_) { - obj.second->OnDraw(projection_, view_); - } - frame_buffer_->Unbind(); - - // render frame buffer to ImGui - ImVec2 uv0 = ImVec2(0, 1); - ImVec2 uv1 = ImVec2(1, 0); - ImVec4 tint_col = ImVec4(1, 1, 1, 1); - ImVec4 border_col = ImVec4(0, 0, 0, 0); - ImGui::Image((void*)(intptr_t)frame_buffer_->GetTextureId(), - ImVec2(width, height), uv0, uv1, tint_col, border_col); - } else { - frame_buffer_ = std::make_unique(width, height); + if (frame_buffer_ != nullptr) { + // render to frame buffer + frame_buffer_->Resize(width, height); + frame_buffer_->Bind(); + frame_buffer_->Clear(); + for (auto& obj : drawable_objects_) { + obj.second->OnDraw(projection_, view_); } + frame_buffer_->Unbind(); + + // render frame buffer to ImGui + ImVec2 uv0 = ImVec2(0, 1); + ImVec2 uv1 = ImVec2(1, 0); + ImVec4 tint_col = ImVec4(1, 1, 1, 1); + ImVec4 border_col = ImVec4(0, 0, 0, 0); + ImGui::Image((void*)(intptr_t)frame_buffer_->GetTextureId(), + ImVec2(width, height), uv0, uv1, tint_col, border_col); + } else { + frame_buffer_ = std::make_unique(width, height); } +} + +void GlWidget::Draw() { + Begin(); + DrawOpenGLObject(); End(); } } // namespace quickviz \ No newline at end of file From cbedd76b2dfb65f984a827d56659a797623798f6 Mon Sep 17 00:00:00 2001 From: Ruixiang Du Date: Wed, 18 Dec 2024 23:21:34 +0800 Subject: [PATCH 5/5] imview: added popup support --- src/imview/CMakeLists.txt | 2 + .../imview/component/popup_manager.hpp | 57 ++++++++++++++ src/imview/include/imview/popup.hpp | 16 +++- src/imview/src/component/popup_manager.cpp | 75 +++++++++++++++++++ src/imview/src/popup.cpp | 42 ++++++++--- src/imview/test/feature/CMakeLists.txt | 3 + src/imview/test/feature/test_popup.cpp | 71 ++++++++++++++++++ 7 files changed, 252 insertions(+), 14 deletions(-) create mode 100644 src/imview/include/imview/component/popup_manager.hpp create mode 100644 src/imview/src/component/popup_manager.cpp create mode 100644 src/imview/test/feature/test_popup.cpp diff --git a/src/imview/CMakeLists.txt b/src/imview/CMakeLists.txt index f3d96eb..b95e1b1 100644 --- a/src/imview/CMakeLists.txt +++ b/src/imview/CMakeLists.txt @@ -19,6 +19,7 @@ add_library(imview # ui components src/window.cpp src/viewer.cpp + src/popup.cpp src/fonts.cpp src/scene_object.cpp src/panel.cpp @@ -31,6 +32,7 @@ add_library(imview src/widget/rt_line_plot_widget.cpp src/widget/gl_widget.cpp # components + src/component/popup_manager.cpp src/component/image_utils.cpp src/component/cairo_context.cpp src/component/cairo_draw.cpp diff --git a/src/imview/include/imview/component/popup_manager.hpp b/src/imview/include/imview/component/popup_manager.hpp new file mode 100644 index 0000000..1e32de9 --- /dev/null +++ b/src/imview/include/imview/component/popup_manager.hpp @@ -0,0 +1,57 @@ +/* + * @file popup_manager.hpp + * @date 12/18/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#ifndef POPUP_MANAGER_HPP +#define POPUP_MANAGER_HPP + +#include + +#include "imview/popup.hpp" + +namespace quickviz { +class PopupManager { + struct PopupDescriptor { + PopupType type; + std::string title; + std::string msg; + float width; + float height; + OnConfirmationCallback callback; + }; + + struct PopupState { + bool triggered; + PopupDescriptor descriptor; + }; + + public: + static PopupManager& GetInstance() { + static PopupManager instance; + return instance; + } + + void RegisterNotificationPopup(std::string title, std::string msg, + float width = 300, float height = 150); + void RegisterConfirmationPopup(std::string title, std::string msg, + OnConfirmationCallback callback = nullptr, + float width = 300, float height = 150); + + void TriggerPopup(std::string title); + void CancelPopup(std::string title); + void UpdatePopups(); + + private: + PopupManager() = default; + ~PopupManager() = default; + + std::mutex popup_mtx_; + std::unordered_map popups_; +}; +} // namespace quickviz + +#endif // POPUP_MANAGER_HPP \ No newline at end of file diff --git a/src/imview/include/imview/popup.hpp b/src/imview/include/imview/popup.hpp index 6464b4b..d8163d7 100644 --- a/src/imview/include/imview/popup.hpp +++ b/src/imview/include/imview/popup.hpp @@ -1,4 +1,4 @@ -/* +/* * popup.hpp * * Created on 4/5/22 11:08 PM @@ -12,10 +12,18 @@ #include #include +#include namespace quickviz { -bool ShowPopupNotification(std::string msg, std::string title, +enum class PopupType { kNotification, kConfirmation }; + +void ShowNotificationPopup(std::string title, std::string msg, + float width = 300, float height = 150); + +using OnConfirmationCallback = std::function; +void ShowConfirmationPopup(std::string title, std::string msg, + OnConfirmationCallback callback = nullptr, float width = 300, float height = 150); -} +} // namespace quickviz -#endif //ROBOSW_SRC_VISUALIZATION_IMVIEW_INCLUDE_IMVIEW_POPUP_HPP +#endif // ROBOSW_SRC_VISUALIZATION_IMVIEW_INCLUDE_IMVIEW_POPUP_HPP diff --git a/src/imview/src/component/popup_manager.cpp b/src/imview/src/component/popup_manager.cpp new file mode 100644 index 0000000..f4336f7 --- /dev/null +++ b/src/imview/src/component/popup_manager.cpp @@ -0,0 +1,75 @@ +/* + * @file popup_manager.cpp + * @date 12/18/24 + * @brief + * + * @copyright Copyright (c) 2024 Ruixiang Du (rdu) + */ + +#include "imview/component/popup_manager.hpp" + +#include "imgui.h" + +namespace quickviz { +void PopupManager::RegisterNotificationPopup(std::string title, std::string msg, + float width, float height) { + std::lock_guard lock(popup_mtx_); + popups_[title].triggered = false; + popups_[title].descriptor = PopupDescriptor{ + PopupType::kNotification, title, msg, width, height, nullptr}; +} + +void PopupManager::RegisterConfirmationPopup(std::string title, std::string msg, + OnConfirmationCallback callback, + float width, float height) { + std::lock_guard lock(popup_mtx_); + popups_[title].triggered = false; + popups_[title].descriptor = PopupDescriptor{ + PopupType::kConfirmation, title, msg, width, height, callback}; +} + +void PopupManager::TriggerPopup(std::string title) { + std::lock_guard lock(popup_mtx_); + if (popups_.find(title) != popups_.end()) { + popups_[title].triggered = true; + } +} + +void PopupManager::CancelPopup(std::string title) { + std::lock_guard lock(popup_mtx_); + if (popups_.find(title) != popups_.end()) { + popups_[title].triggered = false; + } +} + +void PopupManager::UpdatePopups() { + std::unordered_map popups; + + // update popup date + { + std::lock_guard lock(popup_mtx_); + popups = popups_; + for (auto& [title, state] : popups_) { + if (state.triggered) { + ImGui::OpenPopup(title.c_str()); + state.triggered = false; + } + } + } + + // show popups if triggered + for (auto& [title, state] : popups) { + switch (state.descriptor.type) { + case PopupType::kNotification: + ShowNotificationPopup(state.descriptor.title, state.descriptor.msg, + state.descriptor.width, state.descriptor.height); + break; + case PopupType::kConfirmation: + ShowConfirmationPopup(state.descriptor.title, state.descriptor.msg, + state.descriptor.callback, state.descriptor.width, + state.descriptor.height); + break; + } + } +} +} // namespace quickviz \ No newline at end of file diff --git a/src/imview/src/popup.cpp b/src/imview/src/popup.cpp index 117b705..bc86c8e 100644 --- a/src/imview/src/popup.cpp +++ b/src/imview/src/popup.cpp @@ -12,20 +12,14 @@ #include "imgui.h" namespace quickviz { -bool ShowPopupNotification(std::string msg, std::string title, float width, +void ShowNotificationPopup(std::string title, std::string msg, float width, float height) { - bool show_popup = true; - - ImGui::OpenPopup(title.c_str()); - // Always center this window when appearing ImVec2 center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowSize(ImVec2(width, height)); ImGui::SetNextWindowBgAlpha(0.75f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); - if (ImGui::BeginPopupModal( title.c_str(), NULL, ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoResize)) { @@ -38,13 +32,41 @@ bool ShowPopupNotification(std::string msg, std::string title, float width, ImGui::SetCursorPos(ImVec2((width - 120) / 2.0f, 100)); if (ImGui::Button("OK", ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); - show_popup = false; } + ImGui::SetItemDefaultFocus(); ImGui::EndPopup(); } +} - ImGui::PopStyleVar(1); +void ShowConfirmationPopup(std::string title, std::string msg, + OnConfirmationCallback callback, float width, + float height) { + // Always center this window when appearing + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(width, height)); + ImGui::SetNextWindowBgAlpha(0.75f); + + if (ImGui::BeginPopupModal( + title.c_str(), NULL, + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoResize)) { + ImGui::SetCursorPos(ImVec2(15, 40)); + ImGui::Text("%s", msg.c_str()); + ImGui::Text("\n"); + // ImGui::Separator(); - return show_popup; + ImGui::SetItemDefaultFocus(); + ImGui::SetCursorPos(ImVec2((width - 170) / 2.0f, 100)); + if (ImGui::Button("Yes", ImVec2(80, 0))) { + callback(true); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("No", ImVec2(80, 0))) { + callback(false); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } } } // namespace quickviz \ No newline at end of file diff --git a/src/imview/test/feature/CMakeLists.txt b/src/imview/test/feature/CMakeLists.txt index d4cff86..703fa31 100644 --- a/src/imview/test/feature/CMakeLists.txt +++ b/src/imview/test/feature/CMakeLists.txt @@ -20,3 +20,6 @@ target_link_libraries(test_implot_widget PRIVATE imview) add_executable(test_gl_widget test_gl_widget.cpp) target_link_libraries(test_gl_widget PRIVATE imview) + +add_executable(test_popup test_popup.cpp) +target_link_libraries(test_popup PRIVATE imview) diff --git a/src/imview/test/feature/test_popup.cpp b/src/imview/test/feature/test_popup.cpp new file mode 100644 index 0000000..1d5d6f0 --- /dev/null +++ b/src/imview/test/feature/test_popup.cpp @@ -0,0 +1,71 @@ +/* + * test_viewer.cpp + * + * Created on: Jul 27, 2021 09:07 + * Description: + * + * Copyright (c) 2021 Ruixiang Du (rdu) + */ + +#include + +#include "imview/viewer.hpp" +#include "imview/box.hpp" + +#include "imview/panel.hpp" +#include "imview/popup.hpp" + +#include "imview/component/popup_manager.hpp" + +using namespace quickviz; + +class MyPanel : public Panel { + public: + MyPanel(std::string name = "MyPanel") : Panel(name) { + this->SetAutoLayout(false); + + PopupManager::GetInstance().RegisterNotificationPopup( + "Notification", "This is a popup test"); + PopupManager::GetInstance().RegisterConfirmationPopup( + "Confirmation", "This is a confirmation test", [](bool confirm) { + if (confirm) { + std::cout << "Confirmed" << std::endl; + } else { + std::cout << "Not Confirmed" << std::endl; + } + }); + } + + void Draw() override { + Begin(); + { + static bool show_popup = false; + if (ImGui::Button("Show Notification")) { + PopupManager::GetInstance().TriggerPopup("Notification"); + } + if (ImGui::Button("Show Confirmation")) { + PopupManager::GetInstance().TriggerPopup("Confirmation"); + } + + PopupManager::GetInstance().UpdatePopups(); + + ImGui::PushFont(Fonts::GetFont(FontSize::kFont18)); + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 153, 153, 200)); + ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", + 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); + ImGui::PopStyleColor(); + ImGui::PopFont(); + } + End(); + } +}; + +int main(int argc, char* argv[]) { + Viewer viewer; + + auto obj1 = std::make_shared("Panel1"); + viewer.AddSceneObject(obj1); + + viewer.Show(); + return 0; +} \ No newline at end of file