diff --git a/SofaGLFW/CMakeLists.txt b/SofaGLFW/CMakeLists.txt index f4c8831c73..5c18ef7b2c 100644 --- a/SofaGLFW/CMakeLists.txt +++ b/SofaGLFW/CMakeLists.txt @@ -82,8 +82,41 @@ if(SofaPython3_FOUND) add_subdirectory(bindings) endif() +if(TARGET PkgConfig::FFMPEG) + target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::FFMPEG) + set(SOFAGLFW_HAVE_FFMPEG 1) +endif() + add_definitions("-DSOFAGLFW_RESOURCES_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/resources\"") +# FFMPEG_exec +sofa_find_package(FFMPEG_exec BOTH_SCOPES) +# FFMPEG +if(FFMPEG_EXEC_FOUND) + install(PROGRAMS "${FFMPEG_EXEC_FILE}" DESTINATION ${CMAKE_INSTALL_PREFIX}/bin COMPONENT applications) +endif() + +# Create build and install versions of .ini file for resources finding +## For build tree +set(FFMPEG_EXEC_PATH "${FFMPEG_EXEC_FILE}") # absolute path for build dir, see .ini file +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/etc/${PROJECT_NAME}.ini.in "${CMAKE_BINARY_DIR}/etc/${PROJECT_NAME}.ini") + +## For install tree +get_filename_component(FFMPEG_EXEC_FILENAME "${FFMPEG_EXEC_FILE}" NAME) +if(FFMPEG_EXEC_FILENAME) + set(FFMPEG_EXEC_PATH "../bin/${FFMPEG_EXEC_FILENAME}") +else() + #If ffmpeg hasn't been found, it'll not be shipped but we still want to offer the possibility for the user to use it + #easily by putting the executable alongside runSofa + set(FFMPEG_EXEC_PATH "../bin/ffmpeg") + if(WIN32) + set(FFMPEG_EXEC_PATH "${FFMPEG_EXEC_PATH}.exe") + endif() +endif() +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/etc/${PROJECT_NAME}.ini.in "${CMAKE_BINARY_DIR}/etc/installed${PROJECT_NAME}.ini") +#Hack relative tree to ../../etc because of Windows:NSIS that doesn't allow absolute install path. +install(FILES "${CMAKE_BINARY_DIR}/etc/installed${PROJECT_NAME}.ini" DESTINATION ../../etc RENAME ${PROJECT_NAME}.ini COMPONENT applications) + sofa_create_package_with_targets( PACKAGE_NAME ${PROJECT_NAME} PACKAGE_VERSION ${Sofa_VERSION} diff --git a/SofaGLFW/SofaGLFWConfig.cmake.in b/SofaGLFW/SofaGLFWConfig.cmake.in index 009e4de44a..09b9b4b0f9 100644 --- a/SofaGLFW/SofaGLFWConfig.cmake.in +++ b/SofaGLFW/SofaGLFWConfig.cmake.in @@ -3,14 +3,27 @@ @PACKAGE_GUARD@ @PACKAGE_INIT@ -find_package(Sofa.Config REQUIRED) -find_package(Sofa.Simulation.Graph REQUIRED) -find_package(Sofa.GL REQUIRED) -find_package(Sofa.Component.Visual REQUIRED) -find_package(Sofa.GUI.Common QUIET) +find_package(Sofa.Config QUIET REQUIRED) +find_package(Sofa.Simulation.Graph QUIET REQUIRED) +find_package(Sofa.GL QUIET REQUIRED) +find_package(Sofa.Component.Visual QUIET REQUIRED) + +if(NOT TARGET glfw) + sofa_find_package(glfw3 QUIET REQUIRED) +endif() + +set(SOFAGLFW_HAVE_SOFA_GUI_COMMON @SOFAGLFW_HAVE_SOFA_GUI_COMMON@) +if(SOFAGLFW_HAVE_SOFA_GUI_COMMON) + find_package(Sofa.GUI.Common QUIET REQUIRED) +endif() + +set(SOFAGLFW_HAVE_SOFAPYTHON3 @SOFAGLFW_HAVE_SOFAPYTHON3@) +if(SOFAGLFW_HAVE_SOFAPYTHON3) + find_package(SofaPython3 QUIET REQUIRED) +endif() if(NOT TARGET @PROJECT_NAME@) include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") endif() -check_required_components(@PROJECT_NAME@) +check_required_components(@PROJECT_NAME@) \ No newline at end of file diff --git a/SofaGLFW/etc/SofaGLFW.ini.in b/SofaGLFW/etc/SofaGLFW.ini.in new file mode 100644 index 0000000000..b235bf80a0 --- /dev/null +++ b/SofaGLFW/etc/SofaGLFW.ini.in @@ -0,0 +1 @@ +FFMPEG_EXEC_PATH=@FFMPEG_EXEC_PATH@ diff --git a/SofaGLFW/src/SofaGLFW/BaseGUIEngine.h b/SofaGLFW/src/SofaGLFW/BaseGUIEngine.h index f1648162d5..d42b412633 100644 --- a/SofaGLFW/src/SofaGLFW/BaseGUIEngine.h +++ b/SofaGLFW/src/SofaGLFW/BaseGUIEngine.h @@ -23,6 +23,9 @@ #include #include +#include +#include + struct GLFWwindow; namespace sofaglfw @@ -42,6 +45,7 @@ class SOFAGLFW_API BaseGUIEngine virtual void afterDraw() = 0; virtual void terminate() = 0; virtual bool dispatchMouseEvents() = 0; + virtual sofa::type::Vec2i getFrameBufferPixels(std::vector& pixels) = 0; virtual void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {SOFA_UNUSED(window); SOFA_UNUSED(key); SOFA_UNUSED(scancode); SOFA_UNUSED(action); SOFA_UNUSED(mods);} diff --git a/SofaGLFW/src/SofaGLFW/NullGUIEngine.cpp b/SofaGLFW/src/SofaGLFW/NullGUIEngine.cpp index 230015fd57..a3ac2bd21e 100644 --- a/SofaGLFW/src/SofaGLFW/NullGUIEngine.cpp +++ b/SofaGLFW/src/SofaGLFW/NullGUIEngine.cpp @@ -61,4 +61,14 @@ bool NullGUIEngine::dispatchMouseEvents() return true; } +sofa::type::Vec2i NullGUIEngine::getFrameBufferPixels(std::vector& pixels) +{ + GLint viewport[4]; + glGetIntegerv(GL_VIEWPORT, viewport); + pixels.resize(viewport[2] * viewport[3] * 3); + glReadPixels(viewport[0], viewport[1], viewport[2], viewport[3], GL_RGB, GL_UNSIGNED_BYTE, pixels.data()); + + return {viewport[2], viewport[3]}; +} + } // namespace sofaglfw diff --git a/SofaGLFW/src/SofaGLFW/NullGUIEngine.h b/SofaGLFW/src/SofaGLFW/NullGUIEngine.h index ddcabf5f9c..1b811385e8 100644 --- a/SofaGLFW/src/SofaGLFW/NullGUIEngine.h +++ b/SofaGLFW/src/SofaGLFW/NullGUIEngine.h @@ -41,6 +41,7 @@ class NullGUIEngine : public BaseGUIEngine void afterDraw() override {} void terminate() override; bool dispatchMouseEvents() override; + sofa::type::Vec2i getFrameBufferPixels(std::vector& pixels) override; }; } // namespace sofaglfw diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp index c504d539df..f93f80d6f1 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp @@ -20,33 +20,40 @@ * Contact information: contact@sofa-framework.org * ******************************************************************************/ -#include -#include - +#include #define GLFW_INCLUDE_NONE #include +#include +#include #include -#include -#include +#include + +#include #include #include -#include -#include #include #include + #include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include #include -#include -#include +#include + -#include -#include using namespace sofa; namespace sofaglfw @@ -59,7 +66,7 @@ SofaGLFWBaseGUI::SofaGLFWBaseGUI() m_showSelectedObjectBoundingBox = false; m_showSelectedObjectPositions = true; m_showSelectedObjectSurfaces = true; - m_selectionColor = type::RGBAColor::orange(); + m_selectionColor = type::RGBAColor(0.439, 0.588, 0.702, 1.); } SofaGLFWBaseGUI::~SofaGLFWBaseGUI() @@ -92,7 +99,7 @@ bool SofaGLFWBaseGUI::init(int nbMSAASamples) // max = 32 (MSAA with 32 samples) glfwWindowHint(GLFW_SAMPLES, std::clamp(nbMSAASamples, 0, 32) ); - m_glDrawTool = new sofa::gl::DrawToolGL(); + m_glDrawTool = std::make_unique(); m_bGlfwIsInitialized = true; return true; } @@ -113,7 +120,7 @@ void SofaGLFWBaseGUI::setSimulation(sofa::simulation::NodeSPtr groot, const std: m_groot = groot; m_filename = filename; - sofa::core::visual::VisualParams::defaultInstance()->drawTool() = m_glDrawTool; + sofa::core::visual::VisualParams::defaultInstance()->drawTool() = m_glDrawTool.get(); sofa::core::visual::VisualParams::defaultInstance()->setSupported(sofa::core::visual::API_OpenGL); setScene(groot, filename.c_str()); load(); @@ -451,6 +458,8 @@ std::size_t SofaGLFWBaseGUI::runLoop(std::size_t targetNbIterations) bool running = true; std::size_t currentNbIterations = 0; std::stringstream tmpStr; + std::vector pixels; + while (!s_mapWindows.empty() && running) { SIMULATION_LOOP_SCOPE @@ -476,10 +485,17 @@ std::size_t SofaGLFWBaseGUI::runLoop(std::size_t targetNbIterations) m_guiEngine->startFrame(this); m_guiEngine->endFrame(); - glfwSwapBuffers(glfwWindow); - m_viewPortHeight = m_vparams->viewport()[3]; m_viewPortWidth = m_vparams->viewport()[2]; + + // Read framebuffer + if(this->groot->getAnimate() && this->m_isVideoRecording) + { + const auto [width, height] = this->m_guiEngine->getFrameBufferPixels(pixels); + m_videoRecorderFFMPEG.addFrame(pixels.data(), width, height); + } + + glfwSwapBuffers(glfwWindow); } else { @@ -553,7 +569,7 @@ void SofaGLFWBaseGUI::initVisual() void SofaGLFWBaseGUI::runStep() { - if(simulationIsRunning()) + if(simulationIsRunning() && m_guiEngine && m_groot) { sofa::helper::AdvancedTimer::begin("Animate"); @@ -571,7 +587,11 @@ void SofaGLFWBaseGUI::terminate() if (!m_bGlfwIsInitialized) return; - m_guiEngine->terminate(); + if (m_guiEngine) + m_guiEngine->terminate(); + + if(m_isVideoRecording) + m_videoRecorderFFMPEG.finishVideo(); glfwTerminate(); } @@ -742,6 +762,15 @@ void SofaGLFWBaseGUI::moveRayPickInteractor(int eventX, int eventY) getPickHandler()->updateRay(position, direction); } +void SofaGLFWBaseGUI::setMousePos(int xpos, int ypos) { + if(m_firstWindow) + { + glfwSetInputMode(m_firstWindow, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); // Required on Wayland + glfwSetCursorPos(m_firstWindow, xpos, ypos); + glfwSetInputMode(m_firstWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } +} + void SofaGLFWBaseGUI::window_pos_callback(GLFWwindow* window, int xpos, int ypos) { SofaGLFWBaseGUI* gui = static_cast(glfwGetWindowUserPointer(window)); @@ -1008,4 +1037,97 @@ bool SofaGLFWBaseGUI::centerWindow(GLFWwindow* window) return true; } +bool SofaGLFWBaseGUI::toggleVideoRecording() +{ + if(m_isVideoRecording) + { + m_isVideoRecording = false; + m_videoRecorderFFMPEG.finishVideo(); + } + else + { + // Initialize recorder with default parameters + const int width = std::max(1, m_viewPortWidth); + const int height = std::max(1, m_viewPortHeight); + + if(initRecorder(width, height)) + { + m_isVideoRecording = true; + } + else + { + msg_error("SofaGLFWBaseGUI") << "Failed to initialize recorder."; + return false; + } + } + return true; +} + +bool SofaGLFWBaseGUI::initRecorder(int width, + int height, + unsigned int framerate, + unsigned int bitrate, + const std::string& codecExtension, + const std::string& codecName) +{ + // Validate parameters + if (width <= 0 || height <= 0) + { + msg_error("SofaGLFWBaseGUI") << "Invalid video dimensions: " << width << "x" << height; + return false; + } + + std::string ffmpeg_exec_path = ""; + const std::string ffmpegIniFilePath = sofa::helper::Utils::getSofaPathTo("etc/SofaGLFW.ini"); + std::map iniFileValues = sofa::helper::Utils::readBasicIniFile(ffmpegIniFilePath); + if (iniFileValues.find("FFMPEG_EXEC_PATH") != iniFileValues.end()) + { + // get absolute path of FFMPEG executable + ffmpeg_exec_path = sofa::helper::system::SetDirectory::GetRelativeFromProcess(iniFileValues["FFMPEG_EXEC_PATH"].c_str()); + msg_info("SofaGLFWBaseGUI") << " The file " << ffmpegIniFilePath << " points to " << ffmpeg_exec_path << " for the ffmpeg executable."; + } + else + { + msg_warning("SofaGLFWBaseGUI") << " The file " << helper::Utils::getSofaPathPrefix() <<"/etc/SofaGLFW.ini either doesn't exist or doesn't contain the string FFMPEG_EXEC_PATH." + " The initialization of the FFMPEG video recorder will likely fail." + " To fix this, provide a valid path to the ffmpeg executable inside this file using the syntax \"FFMPEG_EXEC_PATH=/usr/bin/ffmpeg\"."; + } + + std::string screenshotPath = sofa::gui::common::BaseGUI::getScreenshotDirectoryPath(); + m_videoFilePath = m_videoFilename.empty()? generateFilename("video", codecExtension): m_videoFilename; + m_videoFilePath = sofa::helper::system::FileSystem::append(screenshotPath, m_videoFilePath); + return m_videoRecorderFFMPEG.init(ffmpeg_exec_path, m_videoFilePath, width, height, framerate, bitrate, codecName); +} + +bool SofaGLFWBaseGUI::isVideoRecording() const +{ + return m_isVideoRecording; +} + +std::string SofaGLFWBaseGUI::generateFilename(const std::string& prefix, const std::string &extension) +{ + std::string filename = getSceneFileName(); + // Add the date and time to the filename + auto now = std::chrono::system_clock::now(); + auto localTime = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + + if (!prefix.empty()) + ss << prefix << "_"; + + if (!filename.empty()) + { + std::filesystem::path path(filename); + ss << path.filename().replace_extension("").string() << "_"; + } + + ss << std::put_time(std::localtime(&localTime), "%F_%H-%M-%S"); + + if (!extension.empty()) + ss << "." << extension; + + filename = ss.str(); + return filename; +} + } // namespace sofaglfw diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h index 9ebaef17c6..80997c826e 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h @@ -21,16 +21,16 @@ ******************************************************************************/ #pragma once #include +#include +#include #include -#include #include +#include #include + #include #include - -#include -#include #include #include @@ -72,7 +72,6 @@ class SOFAGLFW_API SofaGLFWBaseGUI : public sofa::gui::common::BaseViewer bool centerWindow(GLFWwindow* window = nullptr); void updateViewportPosition(float viewportPositionX, float viewportPositionY) ; - GLFWmonitor* getCurrentMonitor(GLFWwindow *window); void viewAll() override; void saveView() override ; @@ -92,47 +91,34 @@ class SOFAGLFW_API SofaGLFWBaseGUI : public sofa::gui::common::BaseViewer void setMouseInteractionEnabled(const bool& enabled) {m_isMouseInteractionEnabled=enabled;} bool isMouseInteractionEnabled() {return m_isMouseInteractionEnabled;} - virtual void setBackgroundColour(float r, float g, float b) override - { - setWindowBackgroundColor(sofa::type::RGBAColor{r, g, b, 1.0f}, 0); - } - virtual void setBackgroundImage(std::string imageFileName) override - { - setWindowBackgroundImage(imageFileName, 0); - } + virtual void setBackgroundColour(float r, float g, float b) override {setWindowBackgroundColor(sofa::type::RGBAColor{r, g, b, 1.0f}, 0);} + virtual void setBackgroundImage(std::string imageFileName) override {setWindowBackgroundImage(imageFileName, 0);} sofa::core::sptr getRootNode() const; bool hasWindow() const { return m_firstWindow != nullptr; } - [[nodiscard]] std::string getFilename() const - { - return m_filename; - } + [[nodiscard]] std::string getFilename() const {return m_filename;} sofa::component::visual::BaseCamera::SPtr findCamera(sofa::simulation::NodeSPtr groot); void changeCamera(sofa::component::visual::BaseCamera::SPtr newCamera); void setWindowIcon(GLFWwindow* glfwWindow); - void setGUIEngine(std::shared_ptr guiEngine) - { - m_guiEngine = guiEngine; - } - - std::shared_ptr getGUIEngine() - { - return m_guiEngine; - } + void setGUIEngine(std::shared_ptr guiEngine) {m_guiEngine = guiEngine;} + std::shared_ptr getGUIEngine() {return m_guiEngine;} void moveRayPickInteractor(int eventX, int eventY) override ; - - void setMousePos(int xpos, int ypos) { - if(m_firstWindow) - { - glfwSetInputMode(m_firstWindow, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); // Required on Wayland - glfwSetCursorPos(m_firstWindow, xpos, ypos); - glfwSetInputMode(m_firstWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL); - } - } + void setMousePos(int xpos, int ypos); + + bool initRecorder(int width, int height, + unsigned int framerate=60, unsigned int bitrate=2000000, + const std::string& codecExtension="mp4", const std::string& codecName="yuv420p"); + bool toggleVideoRecording(); + bool isVideoRecording() const; + const std::string& getVideoFilename() {return m_videoFilename;} + void setVideoFilename(const std::string filename) {m_videoFilename = filename;} + const std::string& getVideoFilePath() {return m_videoFilePath;} + + std::string generateFilename(const std::string &prefix, const std::string &extension); const char* getGUINodeName() {return "GUI";} sofa::core::objectmodel::Tag getGUITag() {return sofa::core::objectmodel::Tag("createdByGUI");} @@ -159,18 +145,16 @@ class SOFAGLFW_API SofaGLFWBaseGUI : public sofa::gui::common::BaseViewer void makeCurrentContext(GLFWwindow* sofaWindow); void runStep(); - // static members inline static std::map< GLFWwindow*, SofaGLFWWindow*> s_mapWindows{}; inline static std::map< GLFWwindow*, SofaGLFWBaseGUI*> s_mapGUIs{}; - //members bool m_bGlfwIsInitialized{ false }; bool m_bGlewIsInitialized{ false }; sofa::simulation::NodeSPtr m_groot{ nullptr }; bool m_simulationCanRun{ true }; std::string m_filename; - sofa::gl::DrawToolGL* m_glDrawTool{ nullptr }; + std::unique_ptr m_glDrawTool; sofa::core::visual::VisualParams* m_vparams{ nullptr }; GLFWwindow* m_firstWindow{ nullptr }; int m_windowWidth{ 0 }; @@ -188,8 +172,12 @@ class SOFAGLFW_API SofaGLFWBaseGUI : public sofa::gui::common::BaseViewer sofa::type::Vec2f m_viewPortPosition; sofa::type::Vec2f m_windowPosition; - std::shared_ptr m_guiEngine; + std::string m_videoFilePath; + std::string m_videoFilename; + bool m_isVideoRecording {false}; + sofa::gl::VideoRecorderFFMPEG m_videoRecorderFFMPEG; + std::shared_ptr m_guiEngine; }; } // namespace sofaglfw diff --git a/SofaGLFW/src/SofaGLFW/config.h.in b/SofaGLFW/src/SofaGLFW/config.h.in index 4cfd1493a6..c2d8818a9c 100644 --- a/SofaGLFW/src/SofaGLFW/config.h.in +++ b/SofaGLFW/src/SofaGLFW/config.h.in @@ -24,6 +24,7 @@ #define SOFAGLFW_VERSION @PROJECT_VERSION@ #cmakedefine01 SOFAGLFW_HAVE_SOFA_GUI_COMMON +#cmakedefine01 SOFAGLFW_HAVE_FFMPEG #define SOFAGLFW_HAS_IMGUI @SOFAGLFW_HAS_IMGUI_VALUE@ diff --git a/SofaImGui/CMakeLists.txt b/SofaImGui/CMakeLists.txt index 6757ffa420..beb75bce93 100644 --- a/SofaImGui/CMakeLists.txt +++ b/SofaImGui/CMakeLists.txt @@ -120,6 +120,7 @@ set(HEADER_FILES ${SOFAIMGUI_SOURCE_DIR}/windows/PluginsWindow.h ${SOFAIMGUI_SOURCE_DIR}/windows/ProgramWindow.h ${SOFAIMGUI_SOURCE_DIR}/windows/ProfilerWindow.h + ${SOFAIMGUI_SOURCE_DIR}/windows/RecordVideoWindow.h ${SOFAIMGUI_SOURCE_DIR}/windows/ViewportWindow.h ${SOFAIMGUI_SOURCE_DIR}/windows/WindowsSettingsName.h @@ -181,6 +182,7 @@ set(SOURCE_FILES ${SOFAIMGUI_SOURCE_DIR}/windows/PluginsWindow.cpp ${SOFAIMGUI_SOURCE_DIR}/windows/ProgramWindow.cpp ${SOFAIMGUI_SOURCE_DIR}/windows/ProfilerWindow.cpp + ${SOFAIMGUI_SOURCE_DIR}/windows/RecordVideoWindow.cpp ${SOFAIMGUI_SOURCE_DIR}/windows/ViewportWindow.cpp ${SOFAIMGUI_SOURCE_DIR}/widgets/ImGuiDataWidget.cpp diff --git a/SofaImGui/resources/Style.cpp b/SofaImGui/resources/Style.cpp index 0aab9b7ed4..668e70c632 100644 --- a/SofaImGui/resources/Style.cpp +++ b/SofaImGui/resources/Style.cpp @@ -227,4 +227,13 @@ void setStyle(const std::string& style) } } +ImVec4 blendColor(const ImVec4& color1, const ImVec4& color2, const float& w) +{ + return ImVec4((1 - w) * color1.x + w * color2.x, + (1 - w) * color1.y + w * color2.y, + (1 - w) * color1.z + w * color2.z, + (1 - w) * color1.w + w * color2.w + ); +} + } diff --git a/SofaImGui/resources/Style.h b/SofaImGui/resources/Style.h index e3886b5c64..e22196670e 100644 --- a/SofaImGui/resources/Style.h +++ b/SofaImGui/resources/Style.h @@ -28,5 +28,6 @@ namespace sofaimgui { void setStyle(const std::string& style); +ImVec4 blendColor(const ImVec4& color1, const ImVec4& color2, const float& w); } diff --git a/SofaImGui/src/SofaImGui/FooterStatusBar.cpp b/SofaImGui/src/SofaImGui/FooterStatusBar.cpp index 00b30d407a..c025df0fab 100644 --- a/SofaImGui/src/SofaImGui/FooterStatusBar.cpp +++ b/SofaImGui/src/SofaImGui/FooterStatusBar.cpp @@ -193,20 +193,24 @@ void FooterStatusBar::setTempMessage(const std::string &message, const MessageTy m_tempMessagePath = path; std::string from = "GUI"; + auto getFullMessage = [this] { + return m_tempMessage + m_tempMessagePath; + }; + switch (type) { case MessageType::MWARNING: { - msg_warning(from) << m_tempMessage; + msg_warning(from) << getFullMessage(); break; } case MessageType::MERROR: { - msg_error(from) << m_tempMessage; + msg_error(from) << getFullMessage(); break; } default: { - msg_info(from) << m_tempMessage; + msg_info(from) << getFullMessage(); break; } } diff --git a/SofaImGui/src/SofaImGui/ImGuiGUIEngine.cpp b/SofaImGui/src/SofaImGui/ImGuiGUIEngine.cpp index 80f8822297..ef8fb65400 100644 --- a/SofaImGui/src/SofaImGui/ImGuiGUIEngine.cpp +++ b/SofaImGui/src/SofaImGui/ImGuiGUIEngine.cpp @@ -27,6 +27,9 @@ #include #include +#include +#include + #include #include @@ -70,8 +73,6 @@ #include #include -#include -#include #include #include @@ -288,6 +289,8 @@ void ImGuiGUIEngine::initBackend(GLFWwindow* glfwWindow) io.Fonts->AddFontFromMemoryCompressedTTF(FA_SOLID_900_compressed_data, FA_SOLID_900_compressed_size, 12 * yscale, &config, icon_ranges); io.Fonts->AddFontFromMemoryCompressedTTF(DejaVuSans_compressed_data, DejaVuSans_compressed_size, 12 * yscale, &configDejaVu, icon_rangesDejaVu); } + + glGenBuffers(s_NB_PBOS, m_pbos); } void ImGuiGUIEngine::startFrame(sofaglfw::SofaGLFWBaseGUI* baseGUI) @@ -338,8 +341,9 @@ void ImGuiGUIEngine::startFrame(sofaglfw::SofaGLFWBaseGUI* baseGUI) m_baseGUI->setSimulationCanRun(workbench != Workbench::SCENE_EDITOR); showMainMenuBar(baseGUI); showSecondaryMenuBar(); - m_pluginsWindow.showWindow(baseGUI, ImGuiWindowFlags_None); - m_mouseManagerWindow.showWindow(baseGUI, ImGuiWindowFlags_None); + m_pluginsWindow.showWindow(baseGUI, ImGuiWindowFlags_NoDocking); + m_mouseManagerWindow.showWindow(baseGUI, ImGuiWindowFlags_NoDocking); + m_recordVideoWindow.showWindow(baseGUI, ImGuiWindowFlags_NoDocking); FooterStatusBar::getInstance().showFooterStatusBar(); FooterStatusBar::getInstance().showTempMessageOnStatusBar(); @@ -364,6 +368,8 @@ void ImGuiGUIEngine::startFrame(sofaglfw::SofaGLFWBaseGUI* baseGUI) ImGui::UpdatePlatformWindows(); ImGui::RenderPlatformWindowsDefault(); } + + m_frameCount++; } void ImGuiGUIEngine::beforeDraw(GLFWwindow*) @@ -407,6 +413,7 @@ void ImGuiGUIEngine::terminate() { saveSettings(); NFD_Quit(); + glDeleteBuffers(s_NB_PBOS, m_pbos); #if SOFAIMGUI_FORCE_OPENGL2 == 1 ImGui_ImplOpenGL2_Shutdown(); @@ -424,6 +431,54 @@ bool ImGuiGUIEngine::dispatchMouseEvents() return !ImGui::GetIO().WantCaptureMouse || m_viewportWindow.isMouseOnViewport(); } +type::Vec2i ImGuiGUIEngine::getFrameBufferPixels(std::vector& pixels) +{ + int readIndex = m_frameCount % s_NB_PBOS; + int processIndex = (m_frameCount + 1) % s_NB_PBOS; + + m_fbo->start(); + + GLint viewport[4]; + glGetIntegerv(GL_VIEWPORT, viewport); + + if(m_pboSize[0] != viewport[2] || m_pboSize[1] != viewport[3]) + { + // Size for your frame (e.g., 1920x1080 RGBA) + int size = viewport[2] * viewport[3] * 4; + + for (int i = 0; i < s_NB_PBOS; i++) { + glBindBuffer(GL_PIXEL_PACK_BUFFER, m_pbos[i]); + glBufferData(GL_PIXEL_PACK_BUFFER, size, NULL, GL_STREAM_READ); + } + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + + m_pboSize[0] = viewport[2]; + m_pboSize[1] = viewport[3]; + } + + glPixelStorei(GL_PACK_ALIGNMENT, 1); + + // Read to PBO (asynchronous) + glBindBuffer(GL_PIXEL_PACK_BUFFER, m_pbos[readIndex]); + glReadPixels(0, 0, viewport[2], viewport[3], GL_RGBA, GL_UNSIGNED_BYTE, 0); + + // Map and process previous frame + glBindBuffer(GL_PIXEL_PACK_BUFFER, m_pbos[processIndex]); + void* data = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY); + if (data) + { + int size = viewport[2] * viewport[3] * 4; + pixels.resize(size); + memcpy(pixels.data(), data, size); + glUnmapBuffer(GL_PIXEL_PACK_BUFFER); + } + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + + m_fbo->stop(); + + return {viewport[2], viewport[3]}; +} + void ImGuiGUIEngine::initDockSpace(const bool& firstTime) { ImGuiViewport* viewport = ImGui::GetMainViewport(); @@ -557,131 +612,149 @@ void ImGuiGUIEngine::showMainMenuBar(sofaglfw::SofaGLFWBaseGUI* baseGUI) { if (ImGui::BeginMainMenuBar()) { - std::string version = "v" + std::string(SOFA_VERSION_STR); - menus::FileMenu fileMenu(baseGUI); - fileMenu.addMenu(); - if (fileMenu.m_loadSimulation) { - saveProject(); - loadSimulation(false, fileMenu.getFilename()); - } - if(fileMenu.m_reloadSimulation) - loadSimulation(true, fileMenu.getFilename()); - - if(fileMenu.m_openPluginsManager) - m_pluginsWindow.setOpen(true); + { // File menu + menus::FileMenu fileMenu = menus::FileMenu(baseGUI); + fileMenu.addMenu(); + if (fileMenu.m_loadSimulation) + { + saveProject(); + loadSimulation(false, fileMenu.getFilename()); + } + if(fileMenu.m_reloadSimulation) + loadSimulation(true, fileMenu.getFilename()); - if(fileMenu.m_openMouseManager) - m_mouseManagerWindow.setOpen(true); + if(fileMenu.m_openPluginsManager) + m_pluginsWindow.setOpen(true); - menus::ViewMenu(baseGUI).addMenu(m_currentFBOSize, m_fbo->getColorTexture()); + if(fileMenu.m_openMouseManager) + m_mouseManagerWindow.setOpen(true); + } - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f)); - if (ImGui::BeginMenu("Workbench")) - { - bool disableWorkbench = Robot::getInstance().getConnection(); // Disable changing workbench if a robot is connected + { // View menu + menus::ViewMenu viewMenu = menus::ViewMenu(baseGUI); + viewMenu.addMenu(m_currentFBOSize, m_fbo->getColorTexture()); - if (disableWorkbench) - ImGui::BeginDisabled(); + if(menus::ViewMenu::openRecordVideoWindow) + { + m_recordVideoWindow.setOpen(true); + menus::ViewMenu::openRecordVideoWindow = false; + } + } - ImGui::PopStyleColor(); - int value = workbench; - for (int i=0; i6)? "next": version; - manualURL += "/Users/SOFARobotics/GUI-user-manual/"; - ImGui::LocalTextLinkOpenURL("Sofa Robotics Manual", manualURL.c_str()); + ImGui::EndMenu(); + } + else + { + ImGui::PopStyleColor(); + } + } - // Sofa Robotics GitHub - ImGui::LocalTextLinkOpenURL("Sofa Robotics GitHub", "https://github.com/SofaComplianceRobotics/SofaGLFW/tree/robotics"); + { // Help + std::string version = "v" + std::string(SOFA_VERSION_STR); + static bool isAboutOpen = false; + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f)); + if (ImGui::BeginMenu("Help")) + { + ImGui::PopStyleColor(); - // Compliance Robotics Website - ImGui::LocalTextLinkOpenURL("Compliance Robotics", "https://compliance-robotics.com/"); + // Manual + std::string manualURL = "https://docs-support.compliance-robotics.com/docs/"; + manualURL += (version.length()>6)? "next": version; + manualURL += "/Users/SOFARobotics/GUI-user-manual/"; + ImGui::LocalTextLinkOpenURL("Sofa Robotics Manual", manualURL.c_str()); - if (ImGui::MenuItem("\t About...", nullptr, false, true)) - isAboutOpen = true; - ImGui::EndMenu(); - } - else - { - ImGui::PopStyleColor(); - } + // Sofa Robotics GitHub + ImGui::LocalTextLinkOpenURL("Sofa Robotics GitHub", "https://github.com/SofaComplianceRobotics/SofaGLFW/tree/robotics"); - if (isAboutOpen) - { - ImGui::Begin("About##SofaComplianceRobotics", &isAboutOpen, ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_AlwaysAutoResize); - - auto windowWidth = ImGui::GetWindowSize().x; - std::vector texts = {"\n", "SOFA, Simulation Open-Framework Architecture \n (c) 2006 INRIA, USTL, UJF, CNRS, MGH", - "&", "(c) Compliance Robotics", "\n", - version, - "SOFA is an open-source framework for interactive physics simulation, \n" - "with an emphasis on soft body dynamics. After years of research and \n" - "development, the project remains open-source under the LGPL v2.1 license, \n" - "fostering both research and development."}; - for (const auto& text : texts) + // Compliance Robotics Website + ImGui::LocalTextLinkOpenURL("Compliance Robotics", "https://compliance-robotics.com/"); + + if (ImGui::MenuItem("\t About...", nullptr, false, true)) + isAboutOpen = true; + ImGui::EndMenu(); + } + else { - auto textWidth = ImGui::CalcTextSize(text.c_str()).x; - ImGui::SetCursorPosX((windowWidth - textWidth) * 0.5f); - ImGui::Text("%s", text.c_str()); + ImGui::PopStyleColor(); } - // Support SOFA - ImGui::LocalTextLinkOpenURL("Support SOFA", "https://www.sofa-framework.org/consortium/support-us/"); + if (isAboutOpen) + { + ImGui::Begin("About##SofaComplianceRobotics", &isAboutOpen, ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_AlwaysAutoResize); + + auto windowWidth = ImGui::GetWindowSize().x; + std::vector texts = {"\n", "SOFA, Simulation Open-Framework Architecture \n (c) 2006 INRIA, USTL, UJF, CNRS, MGH", + "&", "(c) Compliance Robotics", "\n", + version, + "SOFA is an open-source framework for interactive physics simulation, \n" + "with an emphasis on soft body dynamics. After years of research and \n" + "development, the project remains open-source under the LGPL v2.1 license, \n" + "fostering both research and development."}; + for (const auto& text : texts) + { + auto textWidth = ImGui::CalcTextSize(text.c_str()).x; + ImGui::SetCursorPosX((windowWidth - textWidth) * 0.5f); + ImGui::Text("%s", text.c_str()); + } - ImGui::End(); + // Support SOFA + ImGui::LocalTextLinkOpenURL("Support SOFA", "https://www.sofa-framework.org/consortium/support-us/"); + + ImGui::End(); + } } const auto posX = ImGui::GetCursorPosX(); diff --git a/SofaImGui/src/SofaImGui/ImGuiGUIEngine.h b/SofaImGui/src/SofaImGui/ImGuiGUIEngine.h index 95f8084f5b..7e601b29fb 100644 --- a/SofaImGui/src/SofaImGui/ImGuiGUIEngine.h +++ b/SofaImGui/src/SofaImGui/ImGuiGUIEngine.h @@ -41,11 +41,9 @@ #include #include #include - +#include #include -#include - #include #include @@ -77,6 +75,7 @@ class SOFAIMGUI_API ImGuiGUIEngine : public sofaglfw::BaseGUIEngine void afterDraw() override; void terminate() override; bool dispatchMouseEvents() override; + sofa::type::Vec2i getFrameBufferPixels(std::vector& pixels) override; void animateBeginEvent(sofa::simulation::Node* groot) override; void animateEndEvent(sofa::simulation::Node* groot) override; @@ -111,6 +110,7 @@ class SOFAIMGUI_API ImGuiGUIEngine : public sofaglfw::BaseGUIEngine windows::PluginsWindow m_pluginsWindow = windows::PluginsWindow("Plugins Manager", false); windows::MouseManagerWindow m_mouseManagerWindow = windows::MouseManagerWindow("Mouse Manager", false); + windows::RecordVideoWindow m_recordVideoWindow = windows::RecordVideoWindow("Record Video", false); protected: @@ -157,6 +157,11 @@ class SOFAIMGUI_API ImGuiGUIEngine : public sofaglfw::BaseGUIEngine bool m_darkMode{false}; sofaglfw::SofaGLFWBaseGUI* m_baseGUI{nullptr}; std::vector m_dockIDs; + + std::size_t m_frameCount{0}; + static inline constexpr int s_NB_PBOS = 2; + GLuint m_pbos[s_NB_PBOS]; + sofa::type::Vec2i m_pboSize; }; } // namespace sofaimgui diff --git a/SofaImGui/src/SofaImGui/Utils.cpp b/SofaImGui/src/SofaImGui/Utils.cpp index 2b69ce5f9f..6dbfacc3c1 100644 --- a/SofaImGui/src/SofaImGui/Utils.cpp +++ b/SofaImGui/src/SofaImGui/Utils.cpp @@ -20,14 +20,17 @@ * Contact information: contact@sofa-framework.org * ******************************************************************************/ -#include -#include +#include #include -#include #include +#include #include + #include +#include +#include + namespace sofaimgui::Utils { using sofaimgui::menus::ViewMenu; diff --git a/SofaImGui/src/SofaImGui/menus/ViewMenu.cpp b/SofaImGui/src/SofaImGui/menus/ViewMenu.cpp index 3617d9e6d1..789930926a 100644 --- a/SofaImGui/src/SofaImGui/menus/ViewMenu.cpp +++ b/SofaImGui/src/SofaImGui/menus/ViewMenu.cpp @@ -37,12 +37,13 @@ #include #include -#include #include #include namespace sofaimgui::menus { +bool ViewMenu::openRecordVideoWindow = false; + using sofaglfw::SofaGLFWWindow; ViewMenu::ViewMenu(sofaglfw::SofaGLFWBaseGUI *baseGUI) : m_baseGUI(baseGUI) @@ -83,6 +84,7 @@ void ViewMenu::addMenu(const std::pair& fboSize, ImGui::Separator(); + addRecordVideoMenuItem(); addSaveScreenShotMenuItem(fboSize, texture); ImGui::Separator(); @@ -523,22 +525,9 @@ void ViewMenu::addSaveScreenShotMenuItem(const std::pair filterItem{ {{"Image", "jpg,png"}} }; + std::array filterItem{{{"Image", "jpg,png"}}}; - // Add the date and time to the filename - auto now = std::chrono::system_clock::now(); - auto localTime = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; - - auto sceneFilename = m_baseGUI->getFilename(); - if (!sceneFilename.empty()) - { - std::filesystem::path path(sceneFilename); - ss << path.filename().replace_extension("").string() << "_" << std::put_time(std::localtime(&localTime), "%F_%H-%M-%S") << ".png"; - } else { - ss << "screenshot_" << std::put_time(std::localtime(&localTime), "%F_%H-%M-%S") << ".png"; - } - sceneFilename = ss.str(); + auto sceneFilename = m_baseGUI->generateFilename("screenshot", "png"); nfdresult_t result = NFD_SaveDialog(&outPath, filterItem.data(), filterItem.size(), screenshotPath.c_str(), sceneFilename.c_str()); if (result == NFD_OKAY) @@ -560,4 +549,15 @@ void ViewMenu::addSaveScreenShotMenuItem(const std::pair& fboSize, const GLuint& texture); + void addRecordVideoMenuItem(); + void showRecordVideo(); void addFullScreenMenuItem(); + static bool openRecordVideoWindow; + protected: void showGrid(const bool& show, const float &squareSize, const float &thickness, const sofa::type::RGBAColor& color); diff --git a/SofaImGui/src/SofaImGui/widgets/Gizmos.cpp b/SofaImGui/src/SofaImGui/widgets/Gizmos.cpp index d1a30d9f12..4be5ab7f7e 100644 --- a/SofaImGui/src/SofaImGui/widgets/Gizmos.cpp +++ b/SofaImGui/src/SofaImGui/widgets/Gizmos.cpp @@ -29,6 +29,7 @@ SOFTWARE. #include #include #include +#include namespace sofaimgui::widget { @@ -163,15 +164,6 @@ bool drawNegativeLine(const ImVec2 center, const ImVec2 axis, const ImVec4 color return isHovered; } -ImVec4 blendColor(const ImVec4& color1, const ImVec4& color2, const float& w) -{ - return ImVec4((1 - w) * color1.x + w * color2.x, - (1 - w) * color1.y + w * color2.y, - (1 - w) * color1.z + w * color2.z, - (1 - w) * color1.w + w * color2.w - ); -} - } // namespace internal static struct Config { @@ -237,22 +229,22 @@ void DrawFrameGizmo(float* const viewMatrix, const float* const projectionMatrix for (const auto& [fst, snd] : pairs) { switch (fst) { case 0: // +x axis - axisClicked[0] = internal::drawPositiveLine(center, ImVec2{ xAxis.x, -xAxis.y }, internal::blendColor(config.xCircleFrontColor, config.xCircleBackColor, xW), radius, lineThickness, "X"); + axisClicked[0] = internal::drawPositiveLine(center, ImVec2{ xAxis.x, -xAxis.y }, sofaimgui::blendColor(config.xCircleFrontColor, config.xCircleBackColor, xW), radius, lineThickness, "X"); continue; case 1: // +y axis - axisClicked[1] = internal::drawPositiveLine(center, ImVec2{ yAxis.x, -yAxis.y }, internal::blendColor(config.yCircleFrontColor, config.yCircleBackColor, yW), radius, lineThickness, "Y"); + axisClicked[1] = internal::drawPositiveLine(center, ImVec2{ yAxis.x, -yAxis.y }, sofaimgui::blendColor(config.yCircleFrontColor, config.yCircleBackColor, yW), radius, lineThickness, "Y"); continue; case 2: // +z axis - axisClicked[2] = internal::drawPositiveLine(center, ImVec2{ zAxis.x, -zAxis.y }, internal::blendColor(config.zCircleFrontColor, config.zCircleBackColor, zW), radius, lineThickness, "Z"); + axisClicked[2] = internal::drawPositiveLine(center, ImVec2{ zAxis.x, -zAxis.y }, sofaimgui::blendColor(config.zCircleFrontColor, config.zCircleBackColor, zW), radius, lineThickness, "Z"); continue; case 3: // -x axis - axisClicked[3] = internal::drawNegativeLine(center, ImVec2{ xAxis.x, -xAxis.y }, internal::blendColor(config.xCircleBackColor, config.xCircleFrontColor, xW), radius, lineThickness, "-X"); + axisClicked[3] = internal::drawNegativeLine(center, ImVec2{ xAxis.x, -xAxis.y }, sofaimgui::blendColor(config.xCircleBackColor, config.xCircleFrontColor, xW), radius, lineThickness, "-X"); continue; case 4: // -y axis - axisClicked[4] = internal::drawNegativeLine(center, ImVec2{ yAxis.x, -yAxis.y }, internal::blendColor(config.yCircleBackColor, config.yCircleFrontColor, yW), radius, lineThickness, "-Y"); + axisClicked[4] = internal::drawNegativeLine(center, ImVec2{ yAxis.x, -yAxis.y }, sofaimgui::blendColor(config.yCircleBackColor, config.yCircleFrontColor, yW), radius, lineThickness, "-Y"); continue; case 5: // -z axis - axisClicked[5] = internal::drawNegativeLine(center, ImVec2{ zAxis.x, -zAxis.y }, internal::blendColor(config.zCircleBackColor, config.zCircleFrontColor, zW), radius, lineThickness, "-Z"); + axisClicked[5] = internal::drawNegativeLine(center, ImVec2{ zAxis.x, -zAxis.y }, sofaimgui::blendColor(config.zCircleBackColor, config.zCircleFrontColor, zW), radius, lineThickness, "-Z"); continue; default: break; } diff --git a/SofaImGui/src/SofaImGui/widgets/Gizmos.h b/SofaImGui/src/SofaImGui/widgets/Gizmos.h index 0c76de1476..308f173cfa 100644 --- a/SofaImGui/src/SofaImGui/widgets/Gizmos.h +++ b/SofaImGui/src/SofaImGui/widgets/Gizmos.h @@ -46,8 +46,6 @@ bool drawPositiveLine(const ImVec2 center, const ImVec2 axis, const ImVec4 color bool drawNegativeLine(const ImVec2 center, const ImVec2 axis, const ImVec4 color, const float radius, const float thickness, const char* text); -ImVec4 blendColor(const ImVec4& color1, const ImVec4& color2, const float& w); - } // namespace internal /// @brief Set the center and size of the gizmo (square). Should be called before drawing the gizmo. diff --git a/SofaImGui/src/SofaImGui/windows/MouseManagerWindow.cpp b/SofaImGui/src/SofaImGui/windows/MouseManagerWindow.cpp index a5067b6b22..cc3f90a4bc 100644 --- a/SofaImGui/src/SofaImGui/windows/MouseManagerWindow.cpp +++ b/SofaImGui/src/SofaImGui/windows/MouseManagerWindow.cpp @@ -52,7 +52,7 @@ void MouseManagerWindow::showWindow(sofaglfw::SofaGLFWBaseGUI *baseGUI, const Im ImGuiIO& io = ImGui::GetIO(); const ImVec2 defaultSize = ImVec2(io.DisplaySize.x * 0.5, io.DisplaySize.y * 0.3); ImGui::SetNextWindowSize(defaultSize, ImGuiCond_Once); - ImGui::Begin(getName().c_str(), &m_isOpen, windowFlags | ImGuiWindowFlags_NoDocking); + ImGui::Begin(getName().c_str(), &m_isOpen, windowFlags); ImGui::TextDisabled("Mouse interaction with the simulation is enabled by pressing the left shift key."); diff --git a/SofaImGui/src/SofaImGui/windows/PluginsWindow.cpp b/SofaImGui/src/SofaImGui/windows/PluginsWindow.cpp index e4e65e6975..80abd287d5 100644 --- a/SofaImGui/src/SofaImGui/windows/PluginsWindow.cpp +++ b/SofaImGui/src/SofaImGui/windows/PluginsWindow.cpp @@ -64,7 +64,7 @@ void PluginsWindow::showWindow(sofaglfw::SofaGLFWBaseGUI *baseGUI, const ImGuiWi static std::string selectedPlugin; const auto& pluginMap = sofa::helper::system::PluginManager::getInstance().getPluginMap(); - if (ImGui::Begin(getName().c_str(), &m_isOpen, windowFlags | ImGuiWindowFlags_NoDocking)) + if (ImGui::Begin(getName().c_str(), &m_isOpen, windowFlags)) { if (ImGui::BeginChild("#LoadedPlugins", ImVec2(ImGui::GetContentRegionAvail().x * 0.5f, ImGui::GetContentRegionAvail().y), ImGuiChildFlags_None)) { diff --git a/SofaImGui/src/SofaImGui/windows/RecordVideoWindow.cpp b/SofaImGui/src/SofaImGui/windows/RecordVideoWindow.cpp new file mode 100644 index 0000000000..ef1d283231 --- /dev/null +++ b/SofaImGui/src/SofaImGui/windows/RecordVideoWindow.cpp @@ -0,0 +1,143 @@ +/****************************************************************************** + * SOFA, Simulation Open-Framework Architecture * + * (c) 2006 INRIA, USTL, UJF, CNRS, MGH * + * * + * This program is free software; you can redistribute it and/or modify it * + * under the terms of the GNU General Public License as published by the Free * + * Software Foundation; either version 2 of the License, or (at your option) * + * any later version. * + * * + * This program is distributed in the hope that it will be useful, but WITHOUT * + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * + * more details. * + * * + * You should have received a copy of the GNU General Public License along * + * with this program. If not, see . * + ******************************************************************************* + * Authors: The SOFA Team and external contributors (see Authors.txt) * + * * + * Contact information: contact@sofa-framework.org * + ******************************************************************************/ +#include "IconsFontAwesome6.h" +#include "Style.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sofaimgui::windows { + +RecordVideoWindow::RecordVideoWindow(const std::string& name, + const bool& isWindowOpen) +{ + m_defaultIsOpen = false; + m_name = name; + m_isOpen = isWindowOpen; +} + +std::string RecordVideoWindow::getDescription() +{ + return "Record video of the simulation."; +} + +void RecordVideoWindow::showWindow(sofaglfw::SofaGLFWBaseGUI *baseGUI, const ImGuiWindowFlags &windowFlags) +{ + SOFA_UNUSED(baseGUI); + + if (isOpen()) + { + ImGui::SetNextWindowSize(ImVec2(0., 0.), ImGuiCond_Once); + if (ImGui::Begin(getName().c_str(), &m_isOpen, windowFlags)) + { + // float rightPosition = ImGui::GetCursorPosX() + ImGui::GetWindowSize().x - ImGui::GetFrameHeightWithSpacing()*2.; + + static bool record = false; + ImVec2 buttonSize(ImGui::GetFrameHeight(), ImGui::GetFrameHeight()); + + if (record) + ImGui::BeginDisabled(); + + // Output file + ImGui::AlignTextToFramePadding(); + ImGui::Text("Output file"); + ImGui::SameLine(); + static std::string filename = baseGUI->generateFilename("video", ""); + ImGui::InputText("##OutputFile", &filename); + if (filename.empty()) + filename = baseGUI->generateFilename("video", ""); + ImGui::SameLine(); + ImGui::TextDisabled(".mp4"); + + // Interval time + ImGui::AlignTextToFramePadding(); + ImGui::Text("Interval time"); + ImGui::SameLine(); + + static float start_time = 0.; + ImGui::LocalInputFloat("##StartTime", &start_time); + + ImGui::SameLine(); + ImGui::Text("-"); + ImGui::SameLine(); + + static float end_time = std::numeric_limits::infinity(); + ImGui::LocalInputFloat("##EndTime", &end_time); + + // Current time + const auto& current_time = baseGUI->getRootNode()->getTime(); + + if (record) + ImGui::EndDisabled(); + + ImVec4 red = ImVec4(1., 0.3, 0.3, 1.); + ImGui::PushStyleColor(ImGuiCol_Button, red); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, sofaimgui::blendColor(red, ImVec4(0.5,0.,0.,1.), 0.1)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, sofaimgui::blendColor(red, ImVec4(0.5,0.,0.,1.), 0.3)); + + // ImGui::SetCursorPosX(rightPosition); // Set the position to the right of the area + static bool clicked = false; + if (ImGui::Button(record? ICON_FA_STOP " Stop": "\xE2\xAC\xA4 Record")) + { + clicked = true; + record = !record; + } + ImGui::SetItemTooltip(record? "Stop recording": "Start recording"); + + if ((clicked && current_time >= start_time) || (record && current_time >= end_time)) + { + clicked = false; + baseGUI->setVideoFilename(filename + ".mp4"); + if (baseGUI->toggleVideoRecording()) + showRecordingMessage(baseGUI); + else + FooterStatusBar::getInstance().setTempMessage("Something went wrong with the video, check the Log Window", FooterStatusBar::MERROR); + record = baseGUI->isVideoRecording(); + } + + ImGui::PopStyleColor(3); + } + ImGui::End(); + } +} + +void RecordVideoWindow::showRecordingMessage(sofaglfw::SofaGLFWBaseGUI *baseGUI) +{ + bool recording = baseGUI->isVideoRecording(); + std::string message = recording? "Start": "Finished"; + message += " recording to:" + (recording? " " + baseGUI->getVideoFilePath() : ""); + + FooterStatusBar::getInstance().setTempMessage(message, FooterStatusBar::MINFO, recording ? "" : baseGUI->getVideoFilePath()); + + if (!recording && !sofa::helper::system::FileSystem::exists(baseGUI->getVideoFilePath(), true)) // if there is no file after recording + FooterStatusBar::getInstance().setTempMessage("Something went wrong with the video, check the Log Window", FooterStatusBar::MERROR); + +} +} // namespace + + diff --git a/SofaImGui/src/SofaImGui/windows/RecordVideoWindow.h b/SofaImGui/src/SofaImGui/windows/RecordVideoWindow.h new file mode 100644 index 0000000000..7e62a85b6f --- /dev/null +++ b/SofaImGui/src/SofaImGui/windows/RecordVideoWindow.h @@ -0,0 +1,48 @@ +/****************************************************************************** + * SOFA, Simulation Open-Framework Architecture * + * (c) 2006 INRIA, USTL, UJF, CNRS, MGH * + * * + * This program is free software; you can redistribute it and/or modify it * + * under the terms of the GNU General Public License as published by the Free * + * Software Foundation; either version 2 of the License, or (at your option) * + * any later version. * + * * + * This program is distributed in the hope that it will be useful, but WITHOUT * + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * + * more details. * + * * + * You should have received a copy of the GNU General Public License along * + * with this program. If not, see . * + ******************************************************************************* + * Authors: The SOFA Team and external contributors (see Authors.txt) * + * * + * Contact information: contact@sofa-framework.org * + ******************************************************************************/ +#pragma once + +#include +#include +#include +#include +#include + +namespace sofaimgui::windows { + +class SOFAIMGUI_API RecordVideoWindow : public BaseWindow +{ +public: + + RecordVideoWindow(const std::string& name, const bool& isWindowOpen); + ~RecordVideoWindow() = default; + + void showWindow(sofaglfw::SofaGLFWBaseGUI *baseGUI, const ImGuiWindowFlags &windowFlags) override; + std::string getDescription() override; + +protected: + void showRecordingMessage(sofaglfw::SofaGLFWBaseGUI *baseGUI); +}; + +} + + diff --git a/SofaImGui/src/SofaImGui/windows/ViewportWindow.cpp b/SofaImGui/src/SofaImGui/windows/ViewportWindow.cpp index 4e11f376d3..d69e905e3c 100644 --- a/SofaImGui/src/SofaImGui/windows/ViewportWindow.cpp +++ b/SofaImGui/src/SofaImGui/windows/ViewportWindow.cpp @@ -19,7 +19,9 @@ * * * Contact information: contact@sofa-framework.org * ******************************************************************************/ +#define IMGUI_DEFINE_MATH_OPERATORS // import math operators +#include #include #include #include @@ -31,7 +33,7 @@ #include #include #include - +#include namespace sofaimgui::windows { @@ -54,9 +56,7 @@ void ViewportWindow::showWindow(sofaglfw::SofaGLFWBaseGUI* baseGUI, { if (isOpen()) { - if (baseGUI) - m_viewmenu.m_baseGUI = baseGUI; - else + if (!baseGUI) return; auto groot = baseGUI->getRootNode().get(); @@ -91,7 +91,10 @@ void ViewportWindow::showWindow(sofaglfw::SofaGLFWBaseGUI* baseGUI, } addCameraButtons(baseGUI, groot); - addContextMenu(texture); + ImVec4 red = ImVec4(1., 0.3, 0.3, 1.); // TODO create a stylesheet + if(baseGUI->isVideoRecording()) + addRecordingStatus(red); + addContextMenu(baseGUI, texture); } ImGui::EndChild(); } @@ -260,7 +263,7 @@ void ViewportWindow::addCameraButtons(sofaglfw::SofaGLFWBaseGUI* baseGUI, sofa:: { // 3D view display options if (ImGui::BeginPopup("##DisplayOptions")) { - m_viewmenu.addShowIn3DViewMenuItems(); + menus::ViewMenu(baseGUI).addShowIn3DViewMenuItems(); ImGui::EndPopup(); } @@ -444,16 +447,18 @@ void ViewportWindow::addCameraButtons(sofaglfw::SofaGLFWBaseGUI* baseGUI, sofa:: ImGui::EndChild(); } -void ViewportWindow::addContextMenu(const ImTextureID& texture) +void ViewportWindow::addContextMenu(sofaglfw::SofaGLFWBaseGUI *baseGUI, const ImTextureID& texture) { if (ImGui::BeginPopup("##ViewportContextMenu")) { - m_viewmenu.addSaveCameraMenuItem(); - m_viewmenu.addRestoreCameraMenuItem(); + menus::ViewMenu viewMenu(baseGUI); + viewMenu.addSaveCameraMenuItem(); + viewMenu.addRestoreCameraMenuItem(); ImGui::Separator(); - m_viewmenu.addSaveScreenShotMenuItem(std::pair(m_windowSize.first, m_windowSize.second), texture); + viewMenu.addRecordVideoMenuItem(); + viewMenu.addSaveScreenShotMenuItem(std::pair(m_windowSize.first, m_windowSize.second), texture); ImGui::EndPopup(); } @@ -468,6 +473,7 @@ void ViewportWindow::addContextMenu(const ImTextureID& texture) } } + bool ViewportWindow::addAnimateButton(bool *animate, const float &shift_x) { ImVec2 buttonSize = ImVec2(ImGui::GetFrameHeight(), ImGui::GetFrameHeight()); @@ -493,7 +499,6 @@ bool ViewportWindow::addAnimateButton(bool *animate, const float &shift_x) if (ImGui::Begin("ViewportChildMiddleButtons", &m_isOpen, ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove)) { - // ImGui::SameLine(); ImGui::Button(*animate ? ICON_FA_PAUSE : ICON_FA_PLAY, buttonSize); ImGui::SetItemTooltip(*animate ? "Stop simulation" : "Start simulation"); @@ -503,10 +508,11 @@ bool ViewportWindow::addAnimateButton(bool *animate, const float &shift_x) isItemClicked = true; } } - ImGui::EndChild(); ImGui::PopStyleColor(2); ImGui::PopStyleVar(); + + ImGui::EndChild(); } ImGui::End(); } @@ -611,5 +617,34 @@ void ViewportWindow::addSimulationTimeAndFPS(sofa::simulation::Node* groot) } } +void ViewportWindow::addRecordingStatus(const ImVec4& red) +{ + if (m_isOpen) + { + if (ImGui::Begin(getLabel().c_str(), &m_isOpen)) + { + if(ImGui::BeginChild("Render")) + { + // Recording + std::string icon = ICON_FA_CIRCLE_DOT; + std::string text = " Recording"; + auto position = ImGui::GetWindowWidth() - ImGui::CalcTextSize((icon+text).c_str()).x - ImGui::GetFrameHeight() * 0.5f; + ImGui::SetCursorPosX(position); + ImGui::SetCursorPosY(ImGui::GetStyle().ItemSpacing.y); + ImGui::PushStyleColor(ImGuiCol_Text, red); + ImGui::Text("%s", icon.c_str()); + ImGui::PopStyleColor(); + ImGui::SameLine(0.f, 0.f); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f)); + ImGui::Text("%s", text.c_str()); + ImGui::PopStyleColor(); + + ImGui::EndChild(); + } + } + ImGui::End(); + } +} + } diff --git a/SofaImGui/src/SofaImGui/windows/ViewportWindow.h b/SofaImGui/src/SofaImGui/windows/ViewportWindow.h index 4cddf22fcb..81904d37a5 100644 --- a/SofaImGui/src/SofaImGui/windows/ViewportWindow.h +++ b/SofaImGui/src/SofaImGui/windows/ViewportWindow.h @@ -39,7 +39,7 @@ class SOFAIMGUI_API ViewportWindow : public BaseWindow std::string getDescription() override; void addCameraButtons(sofaglfw::SofaGLFWBaseGUI *baseGUI, sofa::simulation::Node *groot); - bool addAnimateButton(bool *animate, const float& shift_x); + bool addAnimateButton(bool *animate, const float &shift_x); bool addStepButton(); bool addDrivingTabCombo(int *mode, const char *listModes[], const int &sizeListModes); @@ -50,7 +50,6 @@ class SOFAIMGUI_API ViewportWindow : public BaseWindow protected: - menus::ViewMenu m_viewmenu = menus::ViewMenu(nullptr); std::shared_ptr m_stateWindow; float m_fps{0.f}; @@ -61,8 +60,9 @@ class SOFAIMGUI_API ViewportWindow : public BaseWindow void addStateWindow(sofaglfw::SofaGLFWBaseGUI *baseGUI, const ImGuiWindowFlags &windowFlags); void addSimulationTimeAndFPS(sofa::simulation::Node *groot); + void addRecordingStatus(const ImVec4 &red); bool checkCamera(sofa::simulation::Node* groot); - void addContextMenu(const ImTextureID& texture); + void addContextMenu(sofaglfw::SofaGLFWBaseGUI *baseGUI, const ImTextureID& texture); }; }