From 6deaee9427e6ae3fa6961ad11612a25b6665f343 Mon Sep 17 00:00:00 2001 From: Nicholas Sharp Date: Sat, 20 Dec 2025 23:13:56 -0800 Subject: [PATCH 1/5] overhaul transform gizmo, add ImGizmo, allow user-created gizmos --- .gitmodules | 3 + deps/imgui/CMakeLists.txt | 17 +- include/polyscope/color_bar.h | 1 + include/polyscope/context.h | 11 + include/polyscope/structure.h | 1 + include/polyscope/transformation_gizmo.h | 119 ++-- include/polyscope/widget.h | 3 +- src/color_bar.cpp | 4 + src/polyscope.cpp | 3 +- src/render/mock_opengl/mock_gl_engine.cpp | 11 +- src/render/opengl/gl_engine_egl.cpp | 1 + src/render/opengl/gl_engine_glfw.cpp | 8 +- src/slice_plane.cpp | 7 +- src/structure.cpp | 12 +- src/transformation_gizmo.cpp | 736 ++++++---------------- src/widget.cpp | 2 +- test/src/misc_test.cpp | 55 +- 17 files changed, 366 insertions(+), 628 deletions(-) diff --git a/.gitmodules b/.gitmodules index dd520028f..0722fbdf8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,3 +15,6 @@ [submodule "deps/imgui/implot"] path = deps/imgui/implot url = https://github.com/epezent/implot +[submodule "deps/imgui/ImGuizmo"] + path = deps/imgui/ImGuizmo + url = https://github.com/CedricGuillemet/ImGuizmo.git diff --git a/deps/imgui/CMakeLists.txt b/deps/imgui/CMakeLists.txt index 24576087c..ea97b9b47 100644 --- a/deps/imgui/CMakeLists.txt +++ b/deps/imgui/CMakeLists.txt @@ -20,13 +20,26 @@ endif() if("${POLYSCOPE_BACKEND_OPENGL3_GLFW}") # imgui sources - list(APPEND SRCS imgui/imgui.cpp imgui/imgui_draw.cpp imgui/imgui_tables.cpp imgui/imgui_widgets.cpp imgui/imgui_demo.cpp imgui/backends/imgui_impl_glfw.cpp imgui/backends/imgui_impl_opengl3.cpp) + list(APPEND SRCS + imgui/imgui.cpp + imgui/imgui_draw.cpp + imgui/imgui_tables.cpp + imgui/imgui_widgets.cpp + imgui/imgui_demo.cpp + imgui/backends/imgui_impl_glfw.cpp + imgui/backends/imgui_impl_opengl3.cpp + ) # implot sources list(APPEND SRCS implot/implot.cpp implot/implot_items.cpp ) + + # imguizmosources + list(APPEND SRCS + ImGuizmo/ImGuizmo.cpp + ) add_library( imgui @@ -35,7 +48,7 @@ if("${POLYSCOPE_BACKEND_OPENGL3_GLFW}") target_include_directories(imgui PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/imgui/") target_include_directories(imgui PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/implot/") - target_include_directories(imgui PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../glfw/include/") + target_include_directories(imgui PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/ImGuizmo/") target_include_directories(imgui PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../glad/include/") target_link_libraries(imgui PRIVATE glfw) diff --git a/include/polyscope/color_bar.h b/include/polyscope/color_bar.h index afca0996b..e4e391daf 100644 --- a/include/polyscope/color_bar.h +++ b/include/polyscope/color_bar.h @@ -81,6 +81,7 @@ class OnscreenColorBarWidget : public Widget { public: OnscreenColorBarWidget(ColorBar& parent_); virtual void draw() override; + std::string uniquePrefix() override; private: ColorBar& parent; diff --git a/include/polyscope/context.h b/include/polyscope/context.h index 825f9101d..b5902af63 100644 --- a/include/polyscope/context.h +++ b/include/polyscope/context.h @@ -28,6 +28,7 @@ class Structure; class Group; class SlicePlane; class Widget; +class TransformationGizmo; class FloatingQuantityStructure; namespace view { extern const double defaultNearClipRatio; @@ -115,6 +116,16 @@ struct Context { bool pointCloudEfficiencyWarningReported = false; FloatingQuantityStructure* globalFloatingQuantityStructure = nullptr; + + // ====================================================== + // === Other various global lists + // ====================================================== + + // Transformation gizmos that were created by hte user for the secne + // Note: this does _not_ include all gizmos, such as the one which exists + // for each structure. This is just storage for gizmos explicitly created + // like with addTransformationGizmo() + std::vector> createdTransformationGizmos; }; diff --git a/include/polyscope/structure.h b/include/polyscope/structure.h index a153e96f3..a79ae3ab0 100644 --- a/include/polyscope/structure.h +++ b/include/polyscope/structure.h @@ -84,6 +84,7 @@ class Structure : public render::ManagedBufferRegistry, public virtual WeakRefer void translate(glm::vec3 vec); // *adds* vec to the position glm::mat4x4 getTransform(); glm::vec3 getPosition(); + TransformationGizmo& getTransformGizmo(); void setStructureUniforms(render::ShaderProgram& p); bool wantsCullPosition(); diff --git a/include/polyscope/transformation_gizmo.h b/include/polyscope/transformation_gizmo.h index 9584ea892..9bb499427 100644 --- a/include/polyscope/transformation_gizmo.h +++ b/include/polyscope/transformation_gizmo.h @@ -7,6 +7,8 @@ #include "polyscope/utilities.h" #include "polyscope/widget.h" +#include "ImGuizmo.h" + namespace polyscope { // A visual widget with handles for translations/rotations @@ -16,7 +18,14 @@ class TransformationGizmo : public Widget { public: // == Constructors - TransformationGizmo(std::string name, glm::mat4& T, PersistentValue* Tpers = nullptr); + // Construct a gizmo. + // + // If T is null, this gizmo owns its transform matrix which can be accessed via set/getTransform(). + // If T is non-null, the gizmo will manipulate that external transform. Optionally, a pointer can also be passed to a + // PersistentValue, which will be updated as the transform is changed. + // + // Users creating additional gizmos should not call this, use addTransformationGizmo() instead. + TransformationGizmo(std::string name, glm::mat4* T = nullptr, PersistentValue* Tpers = nullptr); // == Key members @@ -24,68 +33,82 @@ class TransformationGizmo : public Widget { // a unique name const std::string name; - // the main transform encoded by the gizmo - PersistentValue enabled; - - // the main transform encoded by the gizmo - // note that this is a reference set on construction; the gizmo wraps a transform defined somewhere else - glm::mat4& T; - PersistentValue* Tpers; // optional, a persistent value defined elsewhere that goes with T - // == Member functions + // == Getters and setters - void prepare(); - void draw() override; - bool interact() override; + glm::mat4 getTransform(); + void setTransform(glm::mat4 newT); -protected: - enum class TransformHandle { None, Rotation, Translation, Scale }; + bool getEnabled(); + void setEnabled(bool newVal); + bool getAllowTranslation(); + void setAllowTranslation(bool newVal); - // parameters - const float gizmoSizeRel = 0.08; - const float diskWidthObj = 0.1; // in object coordinates, before transformation - const float vecLength = 1.5; - const float sphereRad = 0.32; - const std::string material = "wax"; + bool getAllowRotation(); + void setAllowRotation(bool newVal); - // state - int selectedDim = -1; // must be {0,1,2} if selectedType == Rotation/Translation - TransformHandle selectedType = TransformHandle::None; - bool currentlyDragging = false; - glm::vec3 dragPrevVec{1., 0., - 0.}; // the normal vector from the previous frame of the drag OR previous translation center + bool getAllowScaling(); + void setAllowScaling(bool newVal); - std::array niceRGB = {{glm::vec3{211 / 255., 45 / 255., 62 / 255.}, - glm::vec3{65 / 255., 121 / 255., 225 / 255.}, - glm::vec3{95 / 255., 175 / 255., 35 / 255.}}}; + // sadly this is not really supported by ImGuizmo + // bool getUniformScaling(); + // void setUniformScaling(bool newVal); - void markUpdated(); + bool getInteractInLocalSpace(); + void setInteractInLocalSpace(bool newVal); - // Render stuff - std::shared_ptr ringProgram; - std::shared_ptr arrowProgram; - std::shared_ptr sphereProgram; + // Size is relative, with 1.0 as the default size + float getGizmoSize(); + void setGizmoSize(float newVal); - // Geometry helpers used to test hits + // == Member functions - // returns - std::tuple circleTest(glm::vec3 raySource, glm::vec3 rayDir, glm::vec3 center, - glm::vec3 normal, float radius); - std::tuple lineTest(glm::vec3 raySource, glm::vec3 rayDir, glm::vec3 center, - glm::vec3 tangent, float length); - std::tuple sphereTest(glm::vec3 raySource, glm::vec3 rayDir, glm::vec3 center, float radius, - bool allowHitSurface = true); + std::string uniquePrefix() override; + void draw() override; + bool interact() override; + void buildUI() override; + void buildInlineTransformUI(); + void buildMenuItems(); + void markUpdated(); +protected: + // The main transform encoded by the gizmo + // This can be either a reference to an external wrapped transform, or the internal storage below + glm::mat4& Tref; - std::tuple, std::vector, std::vector, std::vector, - std::vector> - triplePlaneCoords(); + // Optional, a persistent value defined elsewhere that goes with T, which + // will be marked as updated when the gizmo changes the transform. + PersistentValue* Tpers; - std::tuple, std::vector, std::vector, std::vector> - tripleArrowCoords(); + // Local stoarage for a transformation. + // This may or may not be used; based on the constructor args this class may wrap an external transform, or simply use + // this one. + glm::mat4 Towned = glm::mat4(1.0); - // std::tuple, std::vector> unitCubeCoords(); + // options + PersistentValue enabled; + PersistentValue allowTranslation; + PersistentValue allowRotation; + PersistentValue allowScaling; + // PersistentValue uniformScaling; // not really supported by the ImGuizmo + PersistentValue interactInLocalSpace; + PersistentValue showUIWindow; + PersistentValue gizmoSize; + + // internal + bool lastInteractResult = false; }; +// Create a user-defined transformation gizmo in the scene. +// By default, the gizmo maintains its own transformation matrix which +// can be accessed by by .getTransform(). Optionally, it can instead wrap +// and existin transform passed as transformToWrap. +TransformationGizmo* addTransformationGizmo(std::string name = "", glm::mat4* transformToWrap = nullptr); + +// Remove a user-created transformation gizmo +void removeTransformationGizmo(TransformationGizmo* gizmo); +void removeTransformationGizmo(std::string name); +void removeAllTransformationGizmos(); + } // namespace polyscope diff --git a/include/polyscope/widget.h b/include/polyscope/widget.h index 5f3dfa249..8a2b0d305 100644 --- a/include/polyscope/widget.h +++ b/include/polyscope/widget.h @@ -20,7 +20,8 @@ class Widget : public virtual WeakReferrable { virtual void draw(); virtual bool interact(); // returns true if the mouse input was consumed - virtual void buildGUI(); + virtual void buildUI(); + virtual std::string uniquePrefix() = 0; }; // namespace polyscope } // namespace polyscope diff --git a/src/color_bar.cpp b/src/color_bar.cpp index 20e68ab27..b68066d4a 100644 --- a/src/color_bar.cpp +++ b/src/color_bar.cpp @@ -205,6 +205,10 @@ void ColorBar::prepareOnscreenColorBar() { OnscreenColorBarWidget::OnscreenColorBarWidget(ColorBar& parent_) : parent(parent_) {} +std::string OnscreenColorBarWidget::uniquePrefix() { + return "#widget#OnscreenColorBarWidget#" + parent.parent.uniquePrefix(); +} + void OnscreenColorBarWidget::draw() { if (!parent.parent.isEnabled()) return; if (!parent.getOnscreenColorbarEnabled()) return; diff --git a/src/polyscope.cpp b/src/polyscope.cpp index d6f91e817..ae45b4b44 100644 --- a/src/polyscope.cpp +++ b/src/polyscope.cpp @@ -1035,7 +1035,7 @@ void draw(bool withUI, bool withContextCallback) { for (WeakHandle wHandle : state::widgets) { if (wHandle.isValid()) { Widget& w = wHandle.get(); - w.buildGUI(); + w.buildUI(); } } } @@ -1174,6 +1174,7 @@ void shutdown(bool allowMidFrameShutdown) { removeAllStructures(); removeAllGroups(); removeAllSlicePlanes(); + removeAllTransformationGizmos(); clearMessages(); state::userCallback = nullptr; state::filesDroppedCallback = nullptr; diff --git a/src/render/mock_opengl/mock_gl_engine.cpp b/src/render/mock_opengl/mock_gl_engine.cpp index f6609d082..13471421c 100644 --- a/src/render/mock_opengl/mock_gl_engine.cpp +++ b/src/render/mock_opengl/mock_gl_engine.cpp @@ -1576,12 +1576,12 @@ void MockGLEngine::initialize() { void MockGLEngine::initializeImGui() { ImGui::CreateContext(); // must call once at start - ImPlot::CreateContext(); + ImPlot::CreateContext(); configureImGui(); } void MockGLEngine::configureImGui() { - + // don't both calling the style callbacks, there is no UI if (options::uiScale < 0) { @@ -1598,9 +1598,9 @@ void MockGLEngine::shutdown() { shutdownImGui(); } -void MockGLEngine::shutdownImGui() { - ImPlot::DestroyContext(); - ImGui::DestroyContext(); +void MockGLEngine::shutdownImGui() { + ImPlot::DestroyContext(); + ImGui::DestroyContext(); } void MockGLEngine::swapDisplayBuffers() {} @@ -1675,6 +1675,7 @@ void MockGLEngine::ImGuiNewFrame() { io.DisplaySize.y = view::bufferHeight; ImGui::NewFrame(); + ImGuizmo::BeginFrame(); } void MockGLEngine::ImGuiRender() { diff --git a/src/render/opengl/gl_engine_egl.cpp b/src/render/opengl/gl_engine_egl.cpp index 151916d4a..bca6a00b0 100644 --- a/src/render/opengl/gl_engine_egl.cpp +++ b/src/render/opengl/gl_engine_egl.cpp @@ -473,6 +473,7 @@ void GLEngineEGL::ImGuiNewFrame() { io.DisplaySize.y = view::bufferHeight; ImGui::NewFrame(); + ImGuizmo::BeginFrame(); } void GLEngineEGL::ImGuiRender() { diff --git a/src/render/opengl/gl_engine_glfw.cpp b/src/render/opengl/gl_engine_glfw.cpp index af7c29280..4783bfed3 100644 --- a/src/render/opengl/gl_engine_glfw.cpp +++ b/src/render/opengl/gl_engine_glfw.cpp @@ -10,6 +10,8 @@ #include "stb_image.h" +#include "ImGuizmo.h" + #include #include #include @@ -82,8 +84,7 @@ void GLEngineGLFW::initialize() { // Drag & drop support glfwSetDropCallback(mainWindow, [](GLFWwindow* window, int path_count, const char* paths[]) { - if (!state::filesDroppedCallback) - return; + if (!state::filesDroppedCallback) return; std::vector pathsVec(path_count); for (int i = 0; i < path_count; i++) { @@ -162,7 +163,7 @@ void GLEngineGLFW::initializeImGui() { bindDisplay(); ImGui::CreateContext(); // must call once at start - ImPlot::CreateContext(); + ImPlot::CreateContext(); // Set up ImGUI glfw bindings ImGui_ImplGlfw_InitForOpenGL(mainWindow, true); @@ -220,6 +221,7 @@ void GLEngineGLFW::ImGuiNewFrame() { ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + ImGuizmo::BeginFrame(); } void GLEngineGLFW::ImGuiRender() { diff --git a/src/slice_plane.cpp b/src/slice_plane.cpp index 604968ff2..2ea7ff4ac 100644 --- a/src/slice_plane.cpp +++ b/src/slice_plane.cpp @@ -117,7 +117,7 @@ SlicePlane::SlicePlane(std::string name_) color(uniquePrefix() + "#color", getNextUniqueColor()), gridLineColor(uniquePrefix() + "#gridLineColor", glm::vec3{.97, .97, .97}), transparency(uniquePrefix() + "#transparency", 0.5), shouldInspectMesh(false), inspectedMeshName(""), - transformGizmo(uniquePrefix() + "#transformGizmo", objectTransform.get(), &objectTransform), + transformGizmo(uniquePrefix() + "#transformGizmo", &objectTransform.get(), &objectTransform), sliceBufferArr{{{nullptr, uniquePrefix() + "#slice1", sliceBufferDataArr[0]}, {nullptr, uniquePrefix() + "#slice2", sliceBufferDataArr[1]}, {nullptr, uniquePrefix() + "#slice3", sliceBufferDataArr[2]}, @@ -125,7 +125,8 @@ SlicePlane::SlicePlane(std::string name_) { render::engine->addSlicePlane(postfix); - transformGizmo.enabled = true; + transformGizmo.setEnabled(true); + transformGizmo.setInteractInLocalSpace(true); prepare(); } @@ -425,7 +426,7 @@ glm::vec3 SlicePlane::getNormal() { void SlicePlane::updateWidgetEnabled() { bool enabled = getEnabled() && getDrawWidget(); - transformGizmo.enabled = enabled; + transformGizmo.setEnabled(enabled); } diff --git a/src/structure.cpp b/src/structure.cpp index 9cab497e8..39ef25950 100644 --- a/src/structure.cpp +++ b/src/structure.cpp @@ -12,7 +12,7 @@ Structure::Structure(std::string name_, std::string subtypeName_) : name(name_), subtypeName(subtypeName_), enabled(subtypeName + "#" + name + "#enabled", true), objectTransform(subtypeName + "#" + name + "#object_transform", glm::mat4(1.0)), transparency(subtypeName + "#" + name + "#transparency", 1.0), - transformGizmo(subtypeName + "#" + name + "#transform_gizmo", objectTransform.get(), &objectTransform), + transformGizmo(subtypeName + "#" + name + "#transform_gizmo", &objectTransform.get(), &objectTransform), cullWholeElements(subtypeName + "#" + name + "#cullWholeElements", false), ignoredSlicePlaneNames(subtypeName + "#" + name + "#ignored_slice_planes", {}), objectSpaceBoundingBox( @@ -72,8 +72,7 @@ void Structure::buildUI() { if (ImGui::MenuItem("Center")) centerBoundingBox(); if (ImGui::MenuItem("Unit Scale")) rescaleToUnit(); if (ImGui::MenuItem("Reset")) resetTransform(); - if (ImGui::MenuItem("Show Gizmo", NULL, &transformGizmo.enabled.get())) - transformGizmo.enabled.manuallyChanged(); + transformGizmo.buildMenuItems(); ImGui::EndMenu(); } @@ -200,6 +199,8 @@ glm::vec3 Structure::getPosition() { return glm::vec3{objectTransform.get()[3][0], objectTransform.get()[3][1], objectTransform.get()[3][2]}; } +TransformationGizmo& Structure::getTransformGizmo() { return transformGizmo; } + void Structure::resetTransform() { objectTransform = glm::mat4(1.0); updateStructureExtents(); @@ -314,11 +315,10 @@ bool Structure::getCullWholeElements() { return cullWholeElements.get(); } Structure* Structure::setTransformGizmoEnabled(bool newVal) { - transformGizmo.enabled = newVal; - requestRedraw(); + transformGizmo.setEnabled(newVal); return this; } -bool Structure::getTransformGizmoEnabled() { return transformGizmo.enabled.get(); } +bool Structure::getTransformGizmoEnabled() { return transformGizmo.getEnabled(); } Structure* Structure::setIgnoreSlicePlane(std::string name, bool newValue) { diff --git a/src/transformation_gizmo.cpp b/src/transformation_gizmo.cpp index ed66becae..8c93d3047 100644 --- a/src/transformation_gizmo.cpp +++ b/src/transformation_gizmo.cpp @@ -7,625 +7,249 @@ #include #include +#include "ImGuizmo.h" + #include namespace polyscope { -TransformationGizmo::TransformationGizmo(std::string name_, glm::mat4& T_, PersistentValue* Tpers_) - : name(name_), enabled(name + "#name", false), T(T_), Tpers(Tpers_) - +TransformationGizmo::TransformationGizmo(std::string name_, glm::mat4* externalTptr, PersistentValue* Tpers_) + : name(name_), Tref(externalTptr ? *externalTptr : Towned), Tpers(Tpers_), + // clang-format off + enabled(name + "#enabled", false), + allowTranslation(name + "#allowTranslation", true), + allowRotation(name + "#allowRotation", true), + allowScaling(name + "#allowScaling", false), + // uniformScaling(name + "#uniformScaling", true), + interactInLocalSpace(name + "#interactInLocalSpace", false), + showUIWindow(name + "#showUIWindow", false), + gizmoSize(name + "#gizmoSize", 1.0) +// clang-format on {} + +std::string TransformationGizmo::uniquePrefix() { return "#widget#TransformationGizmo#" + name; } + void TransformationGizmo::markUpdated() { if (Tpers != nullptr) { Tpers->manuallyChanged(); } -} - -void TransformationGizmo::prepare() { - - { // The rotation rings, drawn via textured quads - // clang-format off - ringProgram = render::engine->requestShader("TRANSFORMATION_GIZMO_ROT", - {}, - render::ShaderReplacementDefaults::Process); - // clang-format on - - std::vector coords; - std::vector normals; - std::vector colors; - std::vector texcoords; - std::vector components; - std::tie(coords, normals, colors, texcoords, components) = triplePlaneCoords(); - - ringProgram->setAttribute("a_position", coords); - ringProgram->setAttribute("a_normal", normals); - ringProgram->setAttribute("a_color", colors); - ringProgram->setAttribute("a_texcoord", texcoords); - ringProgram->setAttribute("a_component", components); - } - - { // Translation arrows - // clang-format off - arrowProgram = render::engine->requestShader("RAYCAST_VECTOR", - render::engine->addMaterialRules(material, - { - view::getCurrentProjectionModeRaycastRule(), - "VECTOR_PROPAGATE_COLOR", - "TRANSFORMATION_GIZMO_VEC", - "SHADE_COLOR", - } - ), - render::ShaderReplacementDefaults::Process); - // clang-format on - - std::vector vectors; - std::vector bases; - std::vector colors; - std::vector components; - std::tie(vectors, bases, colors, components) = tripleArrowCoords(); - - arrowProgram->setAttribute("a_vector", vectors); - arrowProgram->setAttribute("a_position", bases); - arrowProgram->setAttribute("a_color", colors); - arrowProgram->setAttribute("a_component", components); - - render::engine->setMaterial(*arrowProgram, material); - } - - { // Scale sphere - // clang-format off - sphereProgram = render::engine->requestShader("RAYCAST_SPHERE", - render::engine->addMaterialRules(material, - { - view::getCurrentProjectionModeRaycastRule(), - "SHADE_BASECOLOR", - "LIGHT_MATCAP" - } - ), - render::ShaderReplacementDefaults::Process); - // clang-format on - - render::engine->setMaterial(*sphereProgram, material); - - std::vector center = {glm::vec3(0., 0., 0.)}; - sphereProgram->setAttribute("a_position", center); - } + requestRedraw(); } void TransformationGizmo::draw() { if (!enabled.get()) return; - if (!ringProgram) prepare(); - - // == set uniforms - - float transScale = glm::length(glm::vec3(T[0])); - float gizmoSizePreTrans = gizmoSizeRel * state::lengthScale; - float gizmoSize = transScale * gizmoSizePreTrans; - glm::mat4 viewMat = - view::getCameraViewMatrix() * T * glm::scale(glm::vec3{gizmoSizePreTrans, gizmoSizePreTrans, gizmoSizePreTrans}); - ringProgram->setUniform("u_modelView", glm::value_ptr(viewMat)); - arrowProgram->setUniform("u_modelView", glm::value_ptr(viewMat)); - sphereProgram->setUniform("u_modelView", glm::value_ptr(viewMat)); + ImGuizmo::PushID(uniquePrefix().c_str()); - glm::mat4 projMat = view::getCameraPerspectiveMatrix(); - ringProgram->setUniform("u_projMatrix", glm::value_ptr(projMat)); - arrowProgram->setUniform("u_projMatrix", glm::value_ptr(projMat)); - sphereProgram->setUniform("u_projMatrix", glm::value_ptr(projMat)); + ImGuiIO& io = ImGui::GetIO(); + ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y); + + // styling options + ImGuizmo::SetGizmoSizeClipSpace(gizmoSize.get() * 0.08f * options::uiScale); + + ImGuizmo::Style& style = ImGuizmo::GetStyle(); + style.HatchedAxisLineThickness = 0.; // disable the hatching affect along the negative axis + // setting these causes some weird clipping behaviors in the gizmo, let's not mess with it + // style.TranslationLineThickness = 3.0f * options::uiScale; + // style.TranslationLineArrowSize = 6.0f * options::uiScale; + // style.RotationLineThickness = 2.0f * options::uiScale; + // style.RotationOuterLineThickness = 3.0f * options::uiScale; + // style.ScaleLineThickness = 3.0f * options::uiScale; + + int gizmoOpModeInt = 0; + if (allowTranslation.get()) gizmoOpModeInt |= ImGuizmo::TRANSLATE; + if (allowRotation.get()) gizmoOpModeInt |= ImGuizmo::ROTATE; + if (allowScaling.get()) { + gizmoOpModeInt |= ImGuizmo::SCALEU; + // if(uniformScaling.get()) { + // gizmoOpModeInt |= ImGuizmo::SCALEU; + // } else { + // gizmoOpModeInt |= ImGuizmo::SCALE; + // } + } + ImGuizmo::OPERATION gizmoOpMode = static_cast(gizmoOpModeInt); - ringProgram->setUniform("u_diskWidthRel", diskWidthObj); + ImGuizmo::MODE gizmoCoordSpace = interactInLocalSpace.get() ? ImGuizmo::LOCAL : ImGuizmo::WORLD; + glm::mat4 cameraView = view::getCameraViewMatrix(); + glm::mat4 cameraProjection = view::getCameraPerspectiveMatrix(); + ImGuizmo::SetOrthographic(view::getProjectionMode() == ProjectionMode::Orthographic); - render::engine->setMaterialUniforms(*arrowProgram, material); - render::engine->setMaterialUniforms(*sphereProgram, material); + // draw/process the actual widget + bool lastInteractResult = ImGuizmo::Manipulate(glm::value_ptr(cameraView), glm::value_ptr(cameraProjection), + gizmoOpMode, gizmoCoordSpace, glm::value_ptr(Tref)); - // set selections - glm::vec3 selectRot{0., 0., 0.}; - glm::vec3 selectTrans{0., 0., 0.}; - glm::vec3 sphereColor(0.85); - if (selectedType == TransformHandle::Rotation) { - selectRot[selectedDim] = 1.; - } - ringProgram->setUniform("u_active", selectRot); - if (selectedType == TransformHandle::Translation) { - selectTrans[selectedDim] = 1.; + if (lastInteractResult) { + markUpdated(); } - arrowProgram->setUniform("u_active", selectTrans); - if (selectedType == TransformHandle::Scale) { - sphereColor = glm::vec3(0.95); - } - - glm::mat4 P = view::getCameraPerspectiveMatrix(); - glm::mat4 Pinv = glm::inverse(P); - arrowProgram->setUniform("u_invProjMatrix", glm::value_ptr(Pinv)); - arrowProgram->setUniform("u_viewport", render::engine->getCurrentViewport()); - arrowProgram->setUniform("u_lengthMult", vecLength); - arrowProgram->setUniform("u_radius", 0.2 * gizmoSize); - - sphereProgram->setUniform("u_invProjMatrix", glm::value_ptr(Pinv)); - sphereProgram->setUniform("u_viewport", render::engine->getCurrentViewport()); - sphereProgram->setUniform("u_pointRadius", sphereRad * gizmoSize); - sphereProgram->setUniform("u_baseColor", sphereColor); - - render::engine->setDepthMode(DepthMode::Less); - render::engine->setBlendMode(BlendMode::AlphaOver); - render::engine->setBackfaceCull(false); + ImGuizmo::PopID(); +} - // == draw - ringProgram->draw(); - arrowProgram->draw(); - sphereProgram->draw(); +void TransformationGizmo::buildMenuItems() { + if (ImGui::MenuItem("Show Gizmo", NULL, &enabled.get())) enabled.manuallyChanged(); + ImGui::MenuItem("Show Transform Window", NULL, &showUIWindow.get()); + ImGui::MenuItem("Allow Translation", NULL, &allowTranslation.get()); + ImGui::MenuItem("Allow Rotation", NULL, &allowRotation.get()); + ImGui::MenuItem("Allow Scaling", NULL, &allowScaling.get()); + // ImGui::MenuItem("Uniform Scaling", NULL, &uniformScaling.get()); } - bool TransformationGizmo::interact() { if (!enabled.get()) return false; - ImGuiIO& io = ImGui::GetIO(); - - // end a drag, if needed - bool draggingAtStart = currentlyDragging; - if (currentlyDragging && (!ImGui::IsMouseDown(0) || !ImGui::IsMousePosValid())) { - currentlyDragging = false; - } - - // Get the mouse ray in world space - glm::vec3 raySource = view::getCameraWorldPosition(); - glm::vec2 mouseCoords{io.MousePos.x, io.MousePos.y}; - glm::vec3 ray = view::screenCoordsToWorldRay(mouseCoords); - - // Get data about the widget - // (there are much more efficient ways to do this) - glm::vec3 center(T * glm::vec4{0., 0., 0., 1.}); - glm::vec3 nX(T * glm::vec4{1., 0., 0., 0.}); - glm::vec3 nY(T * glm::vec4{0., 1., 0., 0.}); - glm::vec3 nZ(T * glm::vec4{0., 0., 1., 0.}); - std::array axNormals{glm::normalize(nX), glm::normalize(nY), glm::normalize(nZ)}; - - float transScale = glm::length(glm::vec3(T[0])); - float gizmoSize = transScale * gizmoSizeRel * state::lengthScale; - float diskRad = gizmoSize; // size 1 - float diskWidth = gizmoSize * diskWidthObj; - - if (currentlyDragging) { - - if (selectedType == TransformHandle::Rotation) { - - // Cast against the axis we are currently rotating - glm::vec3 normal = axNormals[selectedDim]; - float dist, tHit; - glm::vec3 nearestPoint; - std::tie(tHit, dist, nearestPoint) = circleTest(raySource, ray, center, normal, diskRad - diskWidth); - - if (dist != std::numeric_limits::infinity()) { - // if a collinear line (etc) causes an inf dist, just don't process - - // Compute the new nearest normal to the drag - glm::vec3 nearestDir = glm::normalize(nearestPoint - center); - float arg = glm::dot(normal, glm::cross(dragPrevVec, nearestDir)); - arg = glm::clamp(arg, -1.f, 1.f); - float angle = std::asin(arg); // could be fancier with atan, but that's fine - - // Split, transform, and recombine - glm::vec4 trans(T[3]); - T[3] = glm::vec4{0., 0., 0., 1.}; - // T.get() = glm::translate(glm::rotate(angle, normal) * glm::translate(T.get(), -trans), trans); - T = glm::rotate(angle, normal) * T; - T[3] = trans; - markUpdated(); - polyscope::requestRedraw(); - - dragPrevVec = nearestDir; // store this dir for the next time around - } - } else if (selectedType == TransformHandle::Translation) { - - // Cast against the axis we are currently translating - - glm::vec3 normal = axNormals[selectedDim]; - float dist, tHit; - glm::vec3 nearestPoint; - std::tie(tHit, dist, nearestPoint) = - lineTest(raySource, ray, center, normal, std::numeric_limits::infinity()); - - if (dist != std::numeric_limits::infinity()) { - // if a collinear line (etc) causes an inf dist, just don't process - - // Split, transform, and recombine - glm::vec3 trans = nearestPoint - dragPrevVec; - T[3][0] += trans.x; - T[3][1] += trans.y; - T[3][2] += trans.z; - markUpdated(); - polyscope::requestRedraw(); - - dragPrevVec = nearestPoint; // store this dir for the next time around - } - - } else if (selectedType == TransformHandle::Scale) { + // it might might be slightly more correct to invoke the ImGuizmo::Manipulate() or do a ImGuizmo::IsUsing() check + // here. But it seems mostly fine to just re-return the last value, and that is much simpler. Let's do this until we + // have a reason to change it. - // Cast against the scale sphere + return lastInteractResult; +} - float dist, tHit; - glm::vec3 nearestPoint; - float worldSphereRad = sphereRad * gizmoSize; - std::tie(tHit, dist, nearestPoint) = sphereTest(raySource, ray, center, worldSphereRad, false); - if (dist != std::numeric_limits::infinity()) { - // if a collinear line (etc) causes an inf dist, just don't process +void TransformationGizmo::buildUI() { + if (!showUIWindow.get()) return; - float newWorldRad = glm::length(nearestPoint - center); - float scaleRatio = newWorldRad / worldSphereRad; + bool showUIWindowBefore = showUIWindow.get(); + if (ImGui::Begin(name.c_str(), &showUIWindow.get())) { + buildInlineTransformUI(); + } + ImGui::End(); - // Split, transform, and recombine - glm::vec4 trans(T[3]); - T[3] = glm::vec4{0., 0., 0., 1.}; - T *= scaleRatio; - T[3] = trans; - markUpdated(); - polyscope::requestRedraw(); + if (showUIWindowBefore != showUIWindow.get()) { + showUIWindow.manuallyChanged(); + } +} - dragPrevVec = nearestPoint; // store this dir for the next time around - } - } - } else /* !currentlyDragging */ { +void TransformationGizmo::buildInlineTransformUI() { + + ImGui::PushID(uniquePrefix().c_str()); + + ImGui::Checkbox("Gizmo Enabled", &enabled.get()); + + ImGui::Checkbox("Translation", &allowTranslation.get()); + ImGui::SameLine(); + ImGui::Checkbox("Rotation", &allowRotation.get()); + ImGui::SameLine(); + ImGui::Checkbox("Scaling", &allowScaling.get()); + + // if (allowScaling.get()) { + // ImGui::Checkbox("Uniform Scaling", &uniformScaling.get()); + // } + + static ImGuizmo::OPERATION mCurrentGizmoOperation(ImGuizmo::ROTATE); + static ImGuizmo::MODE mCurrentGizmoMode(ImGuizmo::WORLD); + if (ImGui::RadioButton("Translate", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) + mCurrentGizmoOperation = ImGuizmo::TRANSLATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Rotate", mCurrentGizmoOperation == ImGuizmo::ROTATE)) + mCurrentGizmoOperation = ImGuizmo::ROTATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Scale", mCurrentGizmoOperation == ImGuizmo::SCALE)) mCurrentGizmoOperation = ImGuizmo::SCALE; + float matrixTranslation[3], matrixRotation[3], matrixScale[3]; + ImGuizmo::DecomposeMatrixToComponents(glm::value_ptr(Tref), matrixTranslation, matrixRotation, matrixScale); + ImGui::InputFloat3("Tr", matrixTranslation); + ImGui::InputFloat3("Rt", matrixRotation); + ImGui::InputFloat3("Sc", matrixScale); + ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, glm::value_ptr(Tref)); + + // TODO these don't seem to be doing anything? + if (ImGui::RadioButton("Local", interactInLocalSpace.get())) interactInLocalSpace = true; + ImGui::SameLine(); + if (ImGui::RadioButton("World", !interactInLocalSpace.get())) interactInLocalSpace = false; + + ImGui::PopID(); +} - // == Find the part of the widget that we are closest to +glm::mat4 TransformationGizmo::getTransform() { return Tref; } +void TransformationGizmo::setTransform(glm::mat4 newT) { + Tref = newT; + markUpdated(); +}; - float firstHit = std::numeric_limits::infinity(); - float hitDist = std::numeric_limits::infinity(); - int hitDim = -1; - TransformHandle hitType = TransformHandle::None; - glm::vec3 hitNearest(0., 0., 0.); +bool TransformationGizmo::getEnabled() { return enabled.get(); } +void TransformationGizmo::setEnabled(bool newVal) { enabled = newVal; } - // Test the three rotation directions - for (int dim = 0; dim < 3; dim++) { +bool TransformationGizmo::getAllowTranslation() { return allowTranslation.get(); } +void TransformationGizmo::setAllowTranslation(bool newVal) { allowTranslation = newVal; } - glm::vec3 normal = axNormals[dim]; +bool TransformationGizmo::getAllowRotation() { return allowRotation.get(); } +void TransformationGizmo::setAllowRotation(bool newVal) { allowRotation = newVal; } - float dist, tHit; - glm::vec3 nearestPoint; - std::tie(tHit, dist, nearestPoint) = circleTest(raySource, ray, center, normal, diskRad - diskWidth); +bool TransformationGizmo::getAllowScaling() { return allowScaling.get(); } +void TransformationGizmo::setAllowScaling(bool newVal) { allowScaling = newVal; } - if (dist < diskWidth && tHit < firstHit) { - firstHit = tHit; - hitDist = dist; - hitDim = dim; - hitType = TransformHandle::Rotation; - hitNearest = nearestPoint; - } - } +bool TransformationGizmo::getInteractInLocalSpace() { return interactInLocalSpace.get(); } +void TransformationGizmo::setInteractInLocalSpace(bool newVal) { interactInLocalSpace = newVal; } - // Test the three translation directions - for (int dim = 0; dim < 3; dim++) { - glm::vec3 normal = axNormals[dim]; +TransformationGizmo* addTransformationGizmo(std::string name, glm::mat4* transformToWrap) { - float dist, tHit; - glm::vec3 nearestPoint; - std::tie(tHit, dist, nearestPoint) = lineTest(raySource, ray, center, normal, vecLength * gizmoSize); - tHit -= diskWidth; // pull the hit outward, hackily simulates cylinder radius + std::vector>& gizmoList = state::globalContext.createdTransformationGizmos; - if (dist < diskWidth && tHit < firstHit) { - firstHit = tHit; - hitDist = dist; - hitDim = dim; - hitType = TransformHandle::Translation; - hitNearest = nearestPoint; + // If auto-naming, assign a unique name + if (name == "") { + // assign a name like transformation_gizmo_7 + int iName = 0; + bool taken = true; + name = "transformation_gizmo_" + std::to_string(iName); + while (taken) { + taken = false; + for (auto& gizmo : gizmoList) { + if (gizmo->name == name) { + taken = true; + break; + } } - } - - { // Test the scaling sphere - - float dist, tHit; - glm::vec3 nearestPoint; - std::tie(tHit, dist, nearestPoint) = sphereTest(raySource, ray, center, sphereRad * gizmoSize); - - if (dist == 0. && tHit < firstHit) { - firstHit = tHit; - hitDist = dist; - hitDim = 0; - hitType = TransformHandle::Scale; - hitNearest = nearestPoint; + if (taken) { + iName++; + name = "transformation_gizmo_" + std::to_string(iName); } } + } - // = Process the result - - // clear selection before proceeding - selectedType = TransformHandle::None; - selectedDim = -1; - - if (hitType == TransformHandle::Rotation && hitDist < diskWidth) { - // rotation is hovered - - // set new selection - selectedType = TransformHandle::Rotation; - selectedDim = hitDim; - - // if the mouse is clicked, start a drag - if (ImGui::IsMouseClicked(0) && !io.WantCaptureMouse) { - currentlyDragging = true; - - glm::vec3 nearestDir = glm::normalize(hitNearest - center); - dragPrevVec = nearestDir; - } - } else if (hitType == TransformHandle::Translation && hitDist < diskWidth) { - // translation is hovered - - // set new selection - selectedType = TransformHandle::Translation; - selectedDim = hitDim; - - // if the mouse is clicked, start a drag - if (ImGui::IsMouseClicked(0) && !io.WantCaptureMouse) { - currentlyDragging = true; - - dragPrevVec = hitNearest; - } - } else if (hitType == TransformHandle::Scale) { - // scaling is hovered - - // set new selection - selectedType = TransformHandle::Scale; - selectedDim = -1; - - // if the mouse is clicked, start a drag - if (ImGui::IsMouseClicked(0) && !io.WantCaptureMouse) { - currentlyDragging = true; - - dragPrevVec = hitNearest; - } + // Check that the name is unique + for (auto& gizmo : gizmoList) { + if (gizmo->name == name) { + error("Transformation Gizmo already exists with name " + name); + return nullptr; } } - return currentlyDragging || draggingAtStart; -} // namespace polyscope - -std::tuple TransformationGizmo::circleTest(glm::vec3 raySource, glm::vec3 rayDir, - glm::vec3 center, glm::vec3 normal, float radius) { + // create the gizmo + gizmoList.emplace_back(std::unique_ptr(new TransformationGizmo(name, transformToWrap))); - // used for explicit constructors below to make old compilers (gcc-5) happy - typedef std::tuple ret_t; + TransformationGizmo* newGizmo = gizmoList.back().get(); + newGizmo->setEnabled(true); - // Intersect the ray with the plane defined by the normal - float div = glm::dot(normal, rayDir); - if (std::fabs(div) < 1e-6) - return ret_t{-1, std::numeric_limits::infinity(), glm::vec3{0., 0., 0.}}; // parallel - - float tRay = glm::dot((center - raySource), normal) / div; - if (tRay < 0) return ret_t{-1, std::numeric_limits::infinity(), glm::vec3{0., 0., 0.}}; // behind the ray - - glm::vec3 hitPoint = raySource + tRay * rayDir; - - // Find the closest point on the circle - float hitRad = glm::length(hitPoint - center); - float ringDist = std::fabs(hitRad - radius); - glm::vec3 nearestPoint = center + (hitPoint - center) / hitRad * radius; - - return ret_t{tRay, ringDist, nearestPoint}; + return newGizmo; } -std::tuple TransformationGizmo::lineTest(glm::vec3 raySource, glm::vec3 rayDir, - glm::vec3 center, glm::vec3 tangent, float length) { - - // used for explicit constructors below to make old compilers (gcc-5) happy - typedef std::tuple ret_t; - - glm::vec3 nBetween = glm::cross(rayDir, tangent); - if (glm::length(nBetween) < 1e-6) - return ret_t{-1, std::numeric_limits::infinity(), glm::vec3{0., 0., 0.}}; // parallel +void removeTransformationGizmo(TransformationGizmo* gizmo) { + std::vector>& gizmoList = state::globalContext.createdTransformationGizmos; - glm::vec3 nTan = glm::cross(tangent, nBetween); - glm::vec3 nRay = glm::cross(rayDir, nBetween); - - float tRay = glm::dot(center - raySource, nTan) / glm::dot(rayDir, nTan); - float tLine = glm::dot(raySource - center, nRay) / glm::dot(tangent, nRay); - - if (tLine < -length || tLine > length || tRay < 0) - return ret_t{-1, std::numeric_limits::infinity(), glm::vec3{0., 0., 0.}}; // out of bounds or beind - - glm::vec3 pRay = raySource + tRay * rayDir; - glm::vec3 pLine = center + tLine * tangent; - - return ret_t{tRay, glm::length(pRay - pLine), pLine}; + // erase-remove the gizmo matching the pointer (assuming there is one) + gizmoList.erase(std::remove_if(gizmoList.begin(), gizmoList.end(), + [gizmo](const std::unique_ptr& g) { return g.get() == gizmo; }), + gizmoList.end()); } -std::tuple TransformationGizmo::sphereTest(glm::vec3 raySource, glm::vec3 rayDir, - glm::vec3 center, float radius, - bool allowHitSurface) { - - // used for explicit constructors below to make old compilers (gcc-5) happy - typedef std::tuple ret_t; - - glm::vec3 oc = raySource - center; - float b = 2. * dot(oc, rayDir); - float c = glm::dot(oc, oc) - radius * radius; - float disc = b * b - 4 * c; - if (disc < 1e-6 || !allowHitSurface) { - // miss, return nearest point - float tHit = glm::dot(rayDir, center - raySource); - if (tHit < 0.) { - // hit behind - return ret_t{-1, std::numeric_limits::infinity(), glm::vec3{0., 0., 0.}}; - } - glm::vec3 hitPoint = raySource + tHit * rayDir; - return ret_t{tHit, glm::length(hitPoint - center) - radius, hitPoint}; - } else { - // actual hit - float tHit = (-b - std::sqrt(disc)) / 2.; - if (tHit < 0.) { - // hit behind - return ret_t{-1, std::numeric_limits::infinity(), glm::vec3{0., 0., 0.}}; - } - return ret_t{tHit, 0, raySource + tHit * rayDir}; - } -} +void removeTransformationGizmo(std::string name) { + std::vector>& gizmoList = state::globalContext.createdTransformationGizmos; -std::tuple, std::vector, std::vector, std::vector, - std::vector> -TransformationGizmo::triplePlaneCoords() { - std::vector coords; - std::vector normals; - std::vector colors; - std::vector texcoords; - std::vector components; - - float s = 1.2; - - auto addPlane = [&](int i) { - int j = (i + 1) % 3; - int k = (i + 2) % 3; - - glm::vec3 lowerLeft(0., 0., 0.); - lowerLeft[j] = -s; - lowerLeft[k] = -s; - glm::vec2 lowerLeftT(-s, -s); - - glm::vec3 lowerRight(0., 0., 0.); - lowerRight[j] = s; - lowerRight[k] = -s; - glm::vec2 lowerRightT(s, -s); - - glm::vec3 upperLeft(0., 0., 0.); - upperLeft[j] = -s; - upperLeft[k] = s; - glm::vec2 upperLeftT(-s, s); - - glm::vec3 upperRight(0., 0., 0.); - upperRight[j] = s; - upperRight[k] = s; - glm::vec2 upperRightT(s, s); - - glm::vec3 normal(0., 0., 0.); - normal[i] = 1; - glm::vec3 component(0., 0., 0.); - component[i] = 1; - - // First triangle - coords.push_back(lowerLeft); - coords.push_back(lowerRight); - coords.push_back(upperRight); - for (int m = 0; m < 3; m++) { - normals.push_back(normal); - components.push_back(component); - colors.push_back(niceRGB[i]); + // find the gizmo with the matching name and remove it + for (auto& gizmo : gizmoList) { + if (gizmo->name == name) { + removeTransformationGizmo(gizmo.get()); + return; } - texcoords.push_back(lowerLeftT); - texcoords.push_back(lowerRightT); - texcoords.push_back(upperRightT); - - // Second triangle - coords.push_back(lowerLeft); - coords.push_back(upperRight); - coords.push_back(upperLeft); - for (int m = 0; m < 3; m++) { - normals.push_back(normal); - components.push_back(component); - colors.push_back(niceRGB[i]); - } - texcoords.push_back(lowerLeftT); - texcoords.push_back(upperRightT); - texcoords.push_back(upperLeftT); - }; - - // apparently this is how we write for loops in 2021... - addPlane(0); - addPlane(1); - addPlane(2); - - return std::make_tuple(coords, normals, colors, texcoords, components); -} - -std::tuple, std::vector, std::vector, std::vector> -TransformationGizmo::tripleArrowCoords() { - - std::vector vectors; - std::vector bases; - std::vector color; - std::vector components; - - for (int dim = 0; dim < 3; dim++) { - - // Forward vec - bases.push_back(glm::vec3{0., 0., 0.}); - glm::vec3 v(0., 0., 0.); - v[dim] = 1; - vectors.push_back(v); - color.push_back(niceRGB[dim]); - components.push_back(v); - - // Backward vec - bases.push_back(glm::vec3{0., 0., 0.}); - v[dim] = -1; - vectors.push_back(v); - v[dim] = 1; - color.push_back(niceRGB[dim]); - components.push_back(v); } - - return std::make_tuple(vectors, bases, color, components); } -/* -std::tuple, std::vector> TransformationGizmo::unitCubeCoords() { - std::vector coords; - std::vector normals; - - auto addCubeFace = [&](int iS, float s) { - int iU = (iS + 1) % 3; - int iR = (iS + 2) % 3; - - glm::vec3 lowerLeft(0., 0., 0.); - lowerLeft[iS] = s; - lowerLeft[iU] = -s; - lowerLeft[iR] = -s; - - glm::vec3 lowerRight(0., 0., 0.); - lowerRight[iS] = s; - lowerRight[iU] = -s; - lowerRight[iR] = s; - - glm::vec3 upperLeft(0., 0., 0.); - upperLeft[iS] = s; - upperLeft[iU] = s; - upperLeft[iR] = -s; - - glm::vec3 upperRight(0., 0., 0.); - upperRight[iS] = s; - upperRight[iU] = s; - upperRight[iR] = s; - - glm::vec3 normal(0., 0., 0.); - normal[iS] = s; - - // first triangle - coords.push_back(lowerLeft); - coords.push_back(lowerRight); - coords.push_back(upperRight); - normals.push_back(normal); - normals.push_back(normal); - normals.push_back(normal); - - // second triangle - coords.push_back(lowerLeft); - coords.push_back(upperRight); - coords.push_back(upperLeft); - normals.push_back(normal); - normals.push_back(normal); - normals.push_back(normal); - }; - - addCubeFace(0, +1.); - addCubeFace(0, -1.); - addCubeFace(1, +1.); - addCubeFace(1, -1.); - addCubeFace(2, +1.); - addCubeFace(2, -1.); - - return {coords, normals}; +void removeAllTransformationGizmos() { + std::vector>& gizmoList = state::globalContext.createdTransformationGizmos; + gizmoList.clear(); } -*/ + } // namespace polyscope diff --git a/src/widget.cpp b/src/widget.cpp index 0646f90ff..592f60ad6 100644 --- a/src/widget.cpp +++ b/src/widget.cpp @@ -12,6 +12,6 @@ Widget::~Widget() {} void Widget::draw() {} bool Widget::interact() { return false; } -void Widget::buildGUI() {} +void Widget::buildUI() {} } // namespace polyscope diff --git a/test/src/misc_test.cpp b/test/src/misc_test.cpp index 49aa454f1..8bbe42892 100644 --- a/test/src/misc_test.cpp +++ b/test/src/misc_test.cpp @@ -43,7 +43,7 @@ TEST_F(PolyscopeTest, TestScalarColormapQuantity) { q1->setOnscreenColorbarEnabled(true); EXPECT_TRUE(q1->getOnscreenColorbarEnabled()); polyscope::show(3); - + // set its location manually q1->setOnscreenColorbarLocation(glm::vec2(500.f, 500.f)); EXPECT_EQ(glm::vec2(500.f, 500.f), q1->getOnscreenColorbarLocation()); @@ -67,6 +67,57 @@ TEST_F(PolyscopeTest, FlatMaterialTest) { polyscope::removeAllStructures(); } +// ============================================================ +// =============== Transformation Gizmo Tests +// ============================================================ + +TEST_F(PolyscopeTest, TransformationGizmoTest) { + auto psMesh = registerTriangleMesh(); + + // try a bunch of options for the gizmo on a structure + psMesh->setTransformGizmoEnabled(true); + polyscope::show(3); + polyscope::TransformationGizmo& gizmo = psMesh->getTransformGizmo(); + gizmo.setAllowTranslation(true); + gizmo.setAllowRotation(true); + gizmo.setAllowScaling(true); + gizmo.setInteractInLocalSpace(false); + polyscope::show(3); + + polyscope::removeAllStructures(); +} + +TEST_F(PolyscopeTest, TransformationGizmoStandaloneTest) { + + polyscope::TransformationGizmo* gizmo1 = polyscope::addTransformationGizmo(); + polyscope::show(3); + gizmo1->setAllowTranslation(true); + gizmo1->setAllowRotation(true); + gizmo1->setAllowScaling(true); + gizmo1->setInteractInLocalSpace(false); + gizmo1->setGizmoSize(0.5f); + glm::mat4 T1 = gizmo1->getTransform(); + polyscope::show(3); + polyscope::removeTransformationGizmo(gizmo1); + + // create by name + polyscope::TransformationGizmo* gizmo2 = polyscope::addTransformationGizmo("my_gizmo"); + polyscope::show(3); + polyscope::removeTransformationGizmo("my_gizmo"); + + // create multiple + polyscope::TransformationGizmo* gizmo3 = polyscope::addTransformationGizmo(); + polyscope::TransformationGizmo* gizmo4 = polyscope::addTransformationGizmo(); + polyscope::show(3); + + // non-owned transform + glm::mat4 externalT = glm::mat4(1.0); + externalT[0][3] = 2.0; + polyscope::TransformationGizmo* gizmo5 = polyscope::addTransformationGizmo("my_gizmo_3", &externalT); + EXPECT_EQ(gizmo5->getTransform(), externalT); + + polyscope::removeAllTransformationGizmos(); +} // ============================================================ // =============== Slice Plane Tests @@ -93,7 +144,7 @@ TEST_F(PolyscopeTest, TestSlicePlane) { sp1->setTransparency(0.5); EXPECT_EQ(sp1->getTransparency(), 0.5); - + sp1->setGridLineColor(glm::vec3(0.5, 0.5, 0.5)); EXPECT_EQ(sp1->getGridLineColor(), glm::vec3(0.5, 0.5, 0.5)); From 866590189b1d6c5c448c4bc8ac121a85ec9d5dc7 Mon Sep 17 00:00:00 2001 From: Nicholas Sharp Date: Sat, 20 Dec 2025 23:41:25 -0800 Subject: [PATCH 2/5] second attempt at adding submodule --- deps/imgui/ImGuizmo | 1 + 1 file changed, 1 insertion(+) create mode 160000 deps/imgui/ImGuizmo diff --git a/deps/imgui/ImGuizmo b/deps/imgui/ImGuizmo new file mode 160000 index 000000000..df1c30142 --- /dev/null +++ b/deps/imgui/ImGuizmo @@ -0,0 +1 @@ +Subproject commit df1c30142e7c3fb13c171aaeb328bb338fa7aaa6 From 794e073e19b38235a78fdc9ce0f4ba01819b9bb4 Mon Sep 17 00:00:00 2001 From: Nicholas Sharp Date: Sun, 21 Dec 2025 00:19:31 -0800 Subject: [PATCH 3/5] more utility functions --- include/polyscope/transformation_gizmo.h | 7 +++++++ src/transformation_gizmo.cpp | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/include/polyscope/transformation_gizmo.h b/include/polyscope/transformation_gizmo.h index 9bb499427..4473019e6 100644 --- a/include/polyscope/transformation_gizmo.h +++ b/include/polyscope/transformation_gizmo.h @@ -33,6 +33,10 @@ class TransformationGizmo : public Widget { // a unique name const std::string name; + // NOTE: this is only meaningful to call on use-created gizmos from addTransformationGizmo(), + // it will have no effect on gizmos created other ways + // After removing, the object is destructed + void remove(); // == Getters and setters @@ -106,6 +110,9 @@ class TransformationGizmo : public Widget { // and existin transform passed as transformToWrap. TransformationGizmo* addTransformationGizmo(std::string name = "", glm::mat4* transformToWrap = nullptr); +// Get a user-created transformation gizmo by name +TransformationGizmo* getTransformationGizmo(std::string name); + // Remove a user-created transformation gizmo void removeTransformationGizmo(TransformationGizmo* gizmo); void removeTransformationGizmo(std::string name); diff --git a/src/transformation_gizmo.cpp b/src/transformation_gizmo.cpp index 8c93d3047..fd9f63a23 100644 --- a/src/transformation_gizmo.cpp +++ b/src/transformation_gizmo.cpp @@ -162,6 +162,8 @@ void TransformationGizmo::buildInlineTransformUI() { ImGui::PopID(); } +void TransformationGizmo::remove() { removeTransformationGizmo(this); } + glm::mat4 TransformationGizmo::getTransform() { return Tref; } void TransformationGizmo::setTransform(glm::mat4 newT) { Tref = newT; @@ -183,6 +185,8 @@ void TransformationGizmo::setAllowScaling(bool newVal) { allowScaling = newVal; bool TransformationGizmo::getInteractInLocalSpace() { return interactInLocalSpace.get(); } void TransformationGizmo::setInteractInLocalSpace(bool newVal) { interactInLocalSpace = newVal; } +float TransformationGizmo::getGizmoSize() { return gizmoSize.get(); } +void TransformationGizmo::setGizmoSize(float newVal) { gizmoSize = newVal; } TransformationGizmo* addTransformationGizmo(std::string name, glm::mat4* transformToWrap) { @@ -226,6 +230,18 @@ TransformationGizmo* addTransformationGizmo(std::string name, glm::mat4* transfo return newGizmo; } +TransformationGizmo* getTransformationGizmo(std::string name) { + std::vector>& gizmoList = state::globalContext.createdTransformationGizmos; + + for (auto& gizmo : gizmoList) { + if (gizmo->name == name) { + return gizmo.get(); + } + } + + return nullptr; +} + void removeTransformationGizmo(TransformationGizmo* gizmo) { std::vector>& gizmoList = state::globalContext.createdTransformationGizmos; From 9c746a8e60dadfef94b64248744b8610739d19ef Mon Sep 17 00:00:00 2001 From: Nicholas Sharp Date: Sun, 21 Dec 2025 00:45:15 -0800 Subject: [PATCH 4/5] add error for get if not present --- src/transformation_gizmo.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/transformation_gizmo.cpp b/src/transformation_gizmo.cpp index fd9f63a23..618bfe794 100644 --- a/src/transformation_gizmo.cpp +++ b/src/transformation_gizmo.cpp @@ -239,6 +239,7 @@ TransformationGizmo* getTransformationGizmo(std::string name) { } } + error("No Transformation Gizmo exists with name " + name); return nullptr; } From 033d42564654db0fc69ebc7bb0fa638408132d17 Mon Sep 17 00:00:00 2001 From: Nicholas Sharp Date: Sun, 21 Dec 2025 11:04:46 -0800 Subject: [PATCH 5/5] add position setter/getter --- include/polyscope/transformation_gizmo.h | 4 ++++ src/transformation_gizmo.cpp | 6 ++++++ test/src/misc_test.cpp | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/include/polyscope/transformation_gizmo.h b/include/polyscope/transformation_gizmo.h index 4473019e6..6b2a98d86 100644 --- a/include/polyscope/transformation_gizmo.h +++ b/include/polyscope/transformation_gizmo.h @@ -43,6 +43,10 @@ class TransformationGizmo : public Widget { glm::mat4 getTransform(); void setTransform(glm::mat4 newT); + // the are helpers which access/update only the position component of the transform + glm::vec3 getPosition(); + void setPosition(glm::vec3 newPos); + bool getEnabled(); void setEnabled(bool newVal); diff --git a/src/transformation_gizmo.cpp b/src/transformation_gizmo.cpp index 618bfe794..2ce6ecb61 100644 --- a/src/transformation_gizmo.cpp +++ b/src/transformation_gizmo.cpp @@ -170,6 +170,12 @@ void TransformationGizmo::setTransform(glm::mat4 newT) { markUpdated(); }; +glm::vec3 TransformationGizmo::getPosition() { return glm::vec3(Tref[3]); } +void TransformationGizmo::setPosition(glm::vec3 newPos) { + Tref[3] = glm::vec4(newPos, 1.0); + markUpdated(); +} + bool TransformationGizmo::getEnabled() { return enabled.get(); } void TransformationGizmo::setEnabled(bool newVal) { enabled = newVal; } diff --git a/test/src/misc_test.cpp b/test/src/misc_test.cpp index 8bbe42892..e7d2b00bb 100644 --- a/test/src/misc_test.cpp +++ b/test/src/misc_test.cpp @@ -116,6 +116,12 @@ TEST_F(PolyscopeTest, TransformationGizmoStandaloneTest) { polyscope::TransformationGizmo* gizmo5 = polyscope::addTransformationGizmo("my_gizmo_3", &externalT); EXPECT_EQ(gizmo5->getTransform(), externalT); + glm::mat4 T = gizmo5->getTransform(); + glm::vec3 pos = gizmo5->getPosition(); + pos.z += 4.0; + gizmo5->setPosition(pos); + polyscope::show(3); + polyscope::removeAllTransformationGizmos(); }