From 6f559251a54ce67bebcf5692753960833f0a9782 Mon Sep 17 00:00:00 2001 From: Nicholas Sharp Date: Sat, 19 Jul 2025 14:56:27 -0700 Subject: [PATCH 1/2] introduce viewCenter, improve logic around view changes --- include/polyscope/context.h | 1 + include/polyscope/view.h | 8 ++ src/view.cpp | 187 ++++++++++++++++++++++++++++-------- 3 files changed, 158 insertions(+), 38 deletions(-) diff --git a/include/polyscope/context.h b/include/polyscope/context.h index daa71101..9e8c99a4 100644 --- a/include/polyscope/context.h +++ b/include/polyscope/context.h @@ -91,6 +91,7 @@ struct Context { glm::mat4x4 viewMat; double fov = view::defaultFov; ProjectionMode projectionMode = ProjectionMode::Perspective; + glm::vec3 viewCenter; bool midflight = false; float flightStartTime = -1; float flightEndTime = -1; diff --git a/include/polyscope/view.h b/include/polyscope/view.h index b6654d96..f82af433 100644 --- a/include/polyscope/view.h +++ b/include/polyscope/view.h @@ -56,6 +56,7 @@ extern std::array& bgColor; extern glm::mat4x4& viewMat; extern double& fov; // in the y direction extern ProjectionMode& projectionMode; +extern glm::vec3& viewCenter; // center about which view transformations are performed // "Flying" view members extern bool& midflight; @@ -99,6 +100,13 @@ void lookAt(glm::vec3 cameraLocation, glm::vec3 target, glm::vec3 upDir, bool fl glm::mat4 computeHomeView(); void resetCameraToHomeView(); void flyToHomeView(); +void setViewCenter(glm::vec3 newCenter, bool flyTo = false); + +// These both set the new value, and project the current view as-needed to conform to the new setting +void updateViewAndChangeNavigationStyle(NavigateStyle newStyle, bool flyTo = false); +void updateViewAndChangeUpDir(UpDir newUpDir, bool flyTo = false); +void updateViewAndChangeFrontDir(FrontDir newFrontDir, bool flyTo = false); +void updateViewAndChangeCenter(glm::vec3 newCenter, bool flyTo = false); // Move the camera with a 'flight' where the camera's position is briefly animated void startFlightTo(const CameraParameters& p, float flightLengthInSeconds = .4); diff --git a/src/view.cpp b/src/view.cpp index 63c8824a..8ae37769 100644 --- a/src/view.cpp +++ b/src/view.cpp @@ -32,6 +32,7 @@ std::array& bgColor = state::globalContext.bgColor; glm::mat4x4& viewMat = state::globalContext.viewMat; double& fov = state::globalContext.fov; ProjectionMode& projectionMode = state::globalContext.projectionMode; +glm::vec3& viewCenter = state::globalContext.viewCenter; bool& midflight = state::globalContext.midflight; float& flightStartTime = state::globalContext.flightStartTime; float& flightEndTime = state::globalContext.flightEndTime; @@ -89,6 +90,30 @@ std::string to_string(NavigateStyle style) { return ""; // unreachable } +namespace { // anonymous helpers + + +// A default pairing of directions to fall back on when something goes wrong. +const std::vector> defaultUpFrontPairs{ + {UpDir::NegXUp, FrontDir::NegYFront}, {UpDir::XUp, FrontDir::YFront}, {UpDir::NegYUp, FrontDir::NegZFront}, + {UpDir::YUp, FrontDir::ZFront}, {UpDir::NegZUp, FrontDir::NegXFront}, {UpDir::ZUp, FrontDir::XFront}}; + +FrontDir defaultOrthogonalFrontDir(UpDir upDir) { + for (const std::pair& p : defaultUpFrontPairs) { + if (p.first == upDir) return p.second; + } + return FrontDir::ZFront; // fallthrough, should be unused +} + +UpDir defaultOrthogonalUpDir(FrontDir frontDir) { + for (const std::pair& p : defaultUpFrontPairs) { + if (p.second == frontDir) return p.first; + } + return UpDir::YUp; // fallthrough, should be unused +} + +}; // namespace + std::tuple screenCoordsToBufferInds(glm::vec2 screenCoords) { @@ -135,7 +160,7 @@ void processRotate(glm::vec2 startP, glm::vec2 endP) { float delPhi = 2.0 * dragDelta.y * moveScale; // Translate to center - viewMat = glm::translate(viewMat, state::center()); + viewMat = glm::translate(viewMat, view::viewCenter); // Rotation about the horizontal axis glm::mat4x4 phiCamR = glm::rotate(glm::mat4x4(1.0), -delPhi, frameRightDir); @@ -147,7 +172,7 @@ void processRotate(glm::vec2 startP, glm::vec2 endP) { viewMat = viewMat * thetaCamR; // Undo centering - viewMat = glm::translate(viewMat, -state::center()); + viewMat = glm::translate(viewMat, -view::viewCenter); break; } case NavigateStyle::Free: { @@ -156,7 +181,7 @@ void processRotate(glm::vec2 startP, glm::vec2 endP) { float delPhi = 2.0 * dragDelta.y * moveScale; // Translate to center - viewMat = glm::translate(viewMat, state::center()); + viewMat = glm::translate(viewMat, view::viewCenter); // Rotation about the vertical axis glm::mat4x4 thetaCamR = glm::rotate(glm::mat4x4(1.0), delTheta, frameUpDir); @@ -167,7 +192,7 @@ void processRotate(glm::vec2 startP, glm::vec2 endP) { viewMat = viewMat * phiCamR; // Undo centering - viewMat = glm::translate(viewMat, -state::center()); + viewMat = glm::translate(viewMat, -view::viewCenter); break; } case NavigateStyle::Planar: { @@ -363,7 +388,7 @@ void ensureViewValid() { glm::mat4 computeHomeView() { - glm::vec3 target = state::center(); + glm::vec3 target = view::viewCenter; glm::vec3 upDir = getUpVec(); glm::vec3 frontDir = getFrontVec(); if (std::fabs(glm::dot(upDir, frontDir)) > 0.01) { @@ -385,6 +410,7 @@ void resetCameraToHomeView() { return; } + view::viewCenter = state::center(); viewMat = computeHomeView(); fov = defaultFov; @@ -407,6 +433,119 @@ void flyToHomeView() { startFlightTo(T, Tfov); } +void updateViewAndChangeNavigationStyle(NavigateStyle newStyle, bool flyTo) { + NavigateStyle oldStyle = view::style; + view::style = newStyle; + + if (viewIsValid()) { + // for a few combinations of views, we can leave the camera where it is rather than resetting to the home view + if (newStyle == NavigateStyle::Free) { + // nothing needed + } else if (newStyle == NavigateStyle::FirstPerson && oldStyle == NavigateStyle::Turntable) { + // nothing needed + } else if (newStyle == NavigateStyle::Turntable) { + // leave the camera in the same location + lookAt(getCameraWorldPosition(), view::viewCenter, flyTo); + } else { + // General case, depending only on the target style + glm::mat4x4 T = computeHomeView(); + if (flyTo) { + startFlightTo(T, view::fov); + } else { + viewMat = T; + } + } + + requestRedraw(); + } +} + +void updateViewAndChangeUpDir(UpDir newUpDir, bool flyTo) { + view::upDir = newUpDir; + + if (std::fabs(dot(view::getUpVec(), view::getFrontVec())) > 0.1) { + // if the user has foolishly set upDir and frontDir to be along the same axis, fix it + view::frontDir = defaultOrthogonalFrontDir(view::upDir); + } + + if (viewIsValid()) { + switch (style) { + case NavigateStyle::Turntable: + case NavigateStyle::Planar: + case NavigateStyle::Arcball: + case NavigateStyle::FirstPerson: { + glm::vec3 lookDir = getCameraParametersForCurrentView().getLookDir(); + if (std::fabs(dot(view::getUpVec(), lookDir) < 0.01)) { + // if the new up direction is colinear with the direction we're currently looking + lookDir = getFrontVec(); + } + + lookAt(getCameraWorldPosition(), lookDir * state::lengthScale, flyTo); + + break; + } + case NavigateStyle::Free: + case NavigateStyle::None: + // No change needed + break; + } + + requestRedraw(); + } +} + +void updateViewAndChangeFrontDir(FrontDir newFrontDir, bool flyTo) { + view::frontDir = newFrontDir; + + if (std::fabs(dot(view::getUpVec(), view::getFrontVec())) > 0.1) { + // if the user has foolishly set upDir and frontDir to be along the same axis, fix it + view::upDir = defaultOrthogonalUpDir(view::frontDir); + } + + if (viewIsValid()) { + switch (style) { + case NavigateStyle::Turntable: + case NavigateStyle::Planar: + case NavigateStyle::Arcball: + case NavigateStyle::Free: + case NavigateStyle::FirstPerson: + case NavigateStyle::None: + // Currently no views require updating to conform to the front dir, it is just for the default pose + break; + } + + requestRedraw(); + } +} + +void updateViewAndChangeCenter(glm::vec3 newCenter, bool flyTo) { + + view::viewCenter = newCenter; + + if (viewIsValid()) { + // Update the view to be relative to the new center + // This is necessary for some view modes like Turntable, where the viewMat is in a constrained family with respect + // to the center. + switch (style) { + case NavigateStyle::Turntable: + case NavigateStyle::Planar: + case NavigateStyle::Arcball: + // this is a decent baseliny policy that always does _something_ sane + // might want nicer policies for certain cameras + lookAt(getCameraWorldPosition(), view::viewCenter, flyTo); + break; + case NavigateStyle::Free: + case NavigateStyle::FirstPerson: + case NavigateStyle::None: + // no change needed + break; + } + + requestRedraw(); + } +} + +void setViewCenter(glm::vec3 newCenter, bool flyTo) { updateViewAndChangeCenter(newCenter, flyTo); } void lookAt(glm::vec3 cameraLocation, glm::vec3 target, bool flyTo) { lookAt(cameraLocation, target, getUpVec(), flyTo); @@ -426,7 +565,8 @@ void lookAt(glm::vec3 cameraLocation, glm::vec3 target, glm::vec3 upDir, bool fl } } if (!isFinite) { - warning("lookAt() yielded an invalid view. Is the look direction collinear with the up direction?"); + warning("lookAt() yielded an invalid view. Is the location same as the target? Is the look direction collinear " + "with the up direction?"); // just continue after; our view handling will take care of the NaN and set it to the default view } @@ -1008,14 +1148,7 @@ void buildViewGui() { } } -void setUpDir(UpDir newUpDir, bool animateFlight) { - upDir = newUpDir; - if (animateFlight) { - flyToHomeView(); - } else { - resetCameraToHomeView(); - } -} +void setUpDir(UpDir newUpDir, bool animateFlight) { updateViewAndChangeUpDir(newUpDir, animateFlight); } UpDir getUpDir() { return upDir; } @@ -1039,15 +1172,7 @@ glm::vec3 getUpVec() { return glm::vec3{0., 0., 0.}; } -void setFrontDir(FrontDir newFrontDir, bool animateFlight) { - frontDir = newFrontDir; - if (animateFlight) { - flyToHomeView(); - } else { - resetCameraToHomeView(); - } - requestRedraw(); -} +void setFrontDir(FrontDir newFrontDir, bool animateFlight) { updateViewAndChangeFrontDir(newFrontDir, animateFlight); } FrontDir getFrontDir() { return frontDir; } @@ -1073,21 +1198,7 @@ glm::vec3 getFrontVec() { void setNavigateStyle(NavigateStyle newStyle, bool animateFlight) { - NavigateStyle oldStyle = style; - style = newStyle; - - // for a few combinations of views, we can leave the camera where it is rather than resetting to the home view - if (newStyle == NavigateStyle::Free || - (newStyle == NavigateStyle::FirstPerson && oldStyle == NavigateStyle::Turntable)) { - return; - } - - // reset to the home view - if (animateFlight) { - flyToHomeView(); - } else { - resetCameraToHomeView(); - } + updateViewAndChangeNavigationStyle(newStyle, animateFlight); } NavigateStyle getNavigateStyle() { return style; } From 5520195a537880da12ad7cbc4adb1ae083849cda Mon Sep 17 00:00:00 2001 From: Nicholas Sharp Date: Sat, 19 Jul 2025 16:18:46 -0700 Subject: [PATCH 2/2] set view center with click, ctrl/cmd fixes --- include/polyscope/view.h | 3 +- src/polyscope.cpp | 161 ++++++++++++++++++++++----------------- src/view.cpp | 22 +++++- 3 files changed, 113 insertions(+), 73 deletions(-) diff --git a/include/polyscope/view.h b/include/polyscope/view.h index f82af433..75288dc2 100644 --- a/include/polyscope/view.h +++ b/include/polyscope/view.h @@ -183,8 +183,9 @@ void ensureViewValid(); void processTranslate(glm::vec2 delta); void processRotate(glm::vec2 startP, glm::vec2 endP); void processClipPlaneShift(double amount); -void processZoom(double amount); +void processZoom(double amount, bool relativeToCenter = false); void processKeyboardNavigation(ImGuiIO& io); +void processSetCenter(glm::vec2 screenCoords); // deprecated, bad names, see variants above glm::vec3 bufferCoordsToWorldRay(glm::vec2 bufferCoords); diff --git a/src/polyscope.cpp b/src/polyscope.cpp index 5570f55b..c8846a2f 100644 --- a/src/polyscope.cpp +++ b/src/polyscope.cpp @@ -338,6 +338,9 @@ float dragDistSinceLastRelease = 0.0; void processInputEvents() { ImGuiIO& io = ImGui::GetIO(); + // RECALL: in ImGUI language, on MacOS "ctrl" == "cmd", so all the options + // below referring to ctrl really mean cmd on MacOS. + // If any mouse button is pressed, trigger a redraw if (ImGui::IsAnyMouseDown()) { requestRedraw(); @@ -358,90 +361,108 @@ void processInputEvents() { } } + // === Mouse inputs if (!io.WantCaptureMouse && !widgetCapturedMouse) { - double xoffset = io.MouseWheelH; - double yoffset = io.MouseWheel; - if (xoffset != 0 || yoffset != 0) { - requestRedraw(); + { // Process scroll via "mouse wheel" (which might be a touchpad) + double xoffset = io.MouseWheelH; + double yoffset = io.MouseWheel; - // On some setups, shift flips the scroll direction, so take the max - // scrolling in any direction - double maxScroll = xoffset; - if (std::abs(yoffset) > std::abs(xoffset)) { - maxScroll = yoffset; - } + if (xoffset != 0 || yoffset != 0) { + requestRedraw(); - // Pass camera commands to the camera - if (maxScroll != 0.0) { - bool scrollClipPlane = io.KeyShift; + // On some setups, shift flips the scroll direction, so take the max + // scrolling in any direction + double maxScroll = xoffset; + if (std::abs(yoffset) > std::abs(xoffset)) { + maxScroll = yoffset; + } + + // Pass camera commands to the camera + if (maxScroll != 0.0) { + bool scrollClipPlane = io.KeyShift && !io.KeyCtrl; + bool relativeZoom = io.KeyShift && io.KeyCtrl; - if (scrollClipPlane) { - view::processClipPlaneShift(maxScroll); - } else { - view::processZoom(maxScroll); + if (scrollClipPlane) { + view::processClipPlaneShift(maxScroll); + } else { + view::processZoom(maxScroll, relativeZoom); + } } } } - } - // === Mouse inputs - if (!io.WantCaptureMouse && !widgetCapturedMouse) { - // Process drags - bool dragLeft = ImGui::IsMouseDragging(0); - bool dragRight = !dragLeft && ImGui::IsMouseDragging(1); // left takes priority, so only one can be true - if (dragLeft || dragRight) { + { // Process drags + bool dragLeft = ImGui::IsMouseDragging(0); + bool dragRight = !dragLeft && ImGui::IsMouseDragging(1); // left takes priority, so only one can be true + if (dragLeft || dragRight) { - glm::vec2 dragDelta{io.MouseDelta.x / view::windowWidth, -io.MouseDelta.y / view::windowHeight}; - dragDistSinceLastRelease += std::abs(dragDelta.x); - dragDistSinceLastRelease += std::abs(dragDelta.y); + glm::vec2 dragDelta{io.MouseDelta.x / view::windowWidth, -io.MouseDelta.y / view::windowHeight}; + dragDistSinceLastRelease += std::abs(dragDelta.x); + dragDistSinceLastRelease += std::abs(dragDelta.y); - // exactly one of these will be true - bool isRotate = dragLeft && !io.KeyShift && !io.KeyCtrl; - bool isTranslate = (dragLeft && io.KeyShift && !io.KeyCtrl) || dragRight; - bool isDragZoom = dragLeft && io.KeyShift && io.KeyCtrl; + // exactly one of these will be true + bool isRotate = dragLeft && !io.KeyShift && !io.KeyCtrl; + bool isTranslate = (dragLeft && io.KeyShift && !io.KeyCtrl) || dragRight; + bool isDragZoom = dragLeft && io.KeyShift && io.KeyCtrl; - if (isDragZoom) { - view::processZoom(dragDelta.y * 5); - } - if (isRotate) { - glm::vec2 currPos{io.MousePos.x / view::windowWidth, - (view::windowHeight - io.MousePos.y) / view::windowHeight}; - currPos = (currPos * 2.0f) - glm::vec2{1.0, 1.0}; - if (std::abs(currPos.x) <= 1.0 && std::abs(currPos.y) <= 1.0) { - view::processRotate(currPos - 2.0f * dragDelta, currPos); + if (isDragZoom) { + view::processZoom(dragDelta.y * 5, true); + } + if (isRotate) { + glm::vec2 currPos{io.MousePos.x / view::windowWidth, + (view::windowHeight - io.MousePos.y) / view::windowHeight}; + currPos = (currPos * 2.0f) - glm::vec2{1.0, 1.0}; + if (std::abs(currPos.x) <= 1.0 && std::abs(currPos.y) <= 1.0) { + view::processRotate(currPos - 2.0f * dragDelta, currPos); + } + } + if (isTranslate) { + view::processTranslate(dragDelta); } - } - if (isTranslate) { - view::processTranslate(dragDelta); } } - // Click picks - float dragIgnoreThreshold = 0.01; - if (ImGui::IsMouseReleased(0)) { + { // Click picks + float dragIgnoreThreshold = 0.01; + bool anyModifierHeld = io.KeyShift || io.KeyCtrl || io.KeyAlt; + bool ctrlShiftHeld = io.KeyShift && io.KeyCtrl; + + if (!anyModifierHeld && io.MouseReleased[0]) { - // Don't pick at the end of a long drag - if (dragDistSinceLastRelease < dragIgnoreThreshold) { - ImVec2 p = ImGui::GetMousePos(); - PickResult pickResult = pickAtScreenCoords(glm::vec2{p.x, p.y}); - setSelection(pickResult); + // Don't pick at the end of a long drag + if (dragDistSinceLastRelease < dragIgnoreThreshold) { + glm::vec2 screenCoords{io.MousePos.x, io.MousePos.y}; + PickResult pickResult = pickAtScreenCoords(screenCoords); + setSelection(pickResult); + } } - // Reset the drag distance after any release - dragDistSinceLastRelease = 0.0; - } - // Clear pick - if (ImGui::IsMouseReleased(1)) { - if (dragDistSinceLastRelease < dragIgnoreThreshold) { - resetSelection(); + // Clear pick + if (!anyModifierHeld && io.MouseReleased[1]) { + if (dragDistSinceLastRelease < dragIgnoreThreshold) { + resetSelection(); + } + dragDistSinceLastRelease = 0.0; + } + + // Ctrl-shift left-click to set new center + if (ctrlShiftHeld && io.MouseReleased[0]) { + if (dragDistSinceLastRelease < dragIgnoreThreshold) { + glm::vec2 screenCoords{io.MousePos.x, io.MousePos.y}; + view::processSetCenter(screenCoords); + } } - dragDistSinceLastRelease = 0.0; } } } + // Reset the drag distance after any release + if (io.MouseReleased[0]) { + dragDistSinceLastRelease = 0.0; + } + // === Key-press inputs if (!io.WantCaptureKeyboard) { view::processKeyboardNavigation(io); @@ -634,15 +655,18 @@ void buildPolyscopeGui() { // clang-format off ImGui::Begin("Controls", NULL, ImGuiWindowFlags_NoTitleBar); - ImGui::TextUnformatted("View Navigation:"); - ImGui::TextUnformatted(" Rotate: [left click drag]"); - ImGui::TextUnformatted(" Translate: [shift] + [left click drag] OR [right click drag]"); - ImGui::TextUnformatted(" Zoom: [scroll] OR [ctrl] + [shift] + [left click drag]"); - ImGui::TextUnformatted(" Use [ctrl-c] and [ctrl-v] to save and restore camera poses"); + ImGui::TextUnformatted("View Navigation:"); + ImGui::TextUnformatted(" Rotate: [left click drag]"); + ImGui::TextUnformatted(" Translate: [shift] + [left click drag] OR [right click drag]"); + ImGui::TextUnformatted(" Zoom: [scroll] OR [ctrl/cmd] + [shift] + [left click drag]"); + ImGui::TextUnformatted(" Use [ctrl/cmd-c] and [ctrl/cmd-v] to save and restore camera poses"); ImGui::TextUnformatted(" via the clipboard."); - ImGui::TextUnformatted("\nMenu Navigation:"); + ImGui::TextUnformatted(" Hold [ctrl/cmd] + [shift] and [left click] in the scene to set the"); + ImGui::TextUnformatted(" orbit center."); + ImGui::TextUnformatted(" Hold [ctrl/cmd] + [shift] and scroll to zoom towards the center."); + ImGui::TextUnformatted("\nMenu Navigation:"); ImGui::TextUnformatted(" Menu headers with a '>' can be clicked to collapse and expand."); - ImGui::TextUnformatted(" Use [ctrl] + [left click] to manually enter any numeric value"); + ImGui::TextUnformatted(" Use [ctrl/cmd] + [left click] to manually enter any numeric value"); ImGui::TextUnformatted(" via the keyboard."); ImGui::TextUnformatted(" Press [space] to dismiss popup dialogs."); ImGui::TextUnformatted("\nSelection:"); @@ -1095,8 +1119,9 @@ bool hasStructure(std::string type, std::string name) { // Special automatic case, return any if (name == "") { if (sMap.size() != 1) { - exception("Cannot use automatic has-structure test with empty name unless there is exactly one structure of that type " - "registered"); + exception( + "Cannot use automatic has-structure test with empty name unless there is exactly one structure of that type " + "registered"); } return true; } diff --git a/src/view.cpp b/src/view.cpp index 8ae37769..921d515e 100644 --- a/src/view.cpp +++ b/src/view.cpp @@ -282,7 +282,7 @@ void processClipPlaneShift(double amount) { requestRedraw(); } -void processZoom(double amount) { +void processZoom(double amount, bool relativeToCenter) { if (amount == 0.0) return; if (getNavigateStyle() == NavigateStyle::None || getNavigateStyle() == NavigateStyle::FirstPerson) { return; @@ -292,7 +292,12 @@ void processZoom(double amount) { switch (projectionMode) { case ProjectionMode::Perspective: { - float movementScale = state::lengthScale * 0.1 * moveScale; + float movementScale; + if (relativeToCenter) { + movementScale = glm::length(view::viewCenter - view::getCameraWorldPosition()) * 0.3 * moveScale; + } else { + movementScale = state::lengthScale * 0.1 * moveScale; + } glm::mat4x4 camSpaceT = glm::translate(glm::mat4x4(1.0), glm::vec3(0., 0., movementScale * amount)); viewMat = camSpaceT * viewMat; break; @@ -359,6 +364,14 @@ void processKeyboardNavigation(ImGuiIO& io) { } } +void processSetCenter(glm::vec2 screenCoords) { + PickResult pickResult = pickAtScreenCoords(screenCoords); + + if (pickResult.isHit) { + setViewCenter(pickResult.position, true); + } +} + void invalidateView() { viewMat = glm::mat4x4(std::numeric_limits::quiet_NaN()); } bool viewIsValid() { @@ -424,6 +437,7 @@ void flyToHomeView() { // WARNING: Duplicated here and in resetCameraToHomeView() + view::viewCenter = state::center(); glm::mat4x4 T = computeHomeView(); float Tfov = defaultFov; @@ -881,8 +895,8 @@ void buildViewGui() { std::string viewStyleName = to_string(view::style); ImGui::PushItemWidth(120 * options::uiScale); - std::array styles{NavigateStyle::Turntable, NavigateStyle::Free, NavigateStyle::Planar, - NavigateStyle::None, NavigateStyle::FirstPerson}; + std::array styles{NavigateStyle::Turntable, NavigateStyle::FirstPerson, NavigateStyle::Free, + NavigateStyle::Planar, NavigateStyle::None}; if (ImGui::BeginCombo("##View Style", viewStyleName.c_str())) { for (NavigateStyle s : styles) {