#include "ViewportTile.h" #include "../utils/Logger.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace UsdLayerManager { // --------------------------------------------------------------------------- ViewportTile::ViewportTile() = default; ViewportTile::~ViewportTile() = default; // --------------------------------------------------------------------------- void ViewportTile::SetCommandHistory(CommandHistory* h) { m_commandHistory = h; // The TransformManipulator's command history is set by the container // (ViewportPanel), not by individual tiles. } void ViewportTile::SetTimeCodes(pxr::UsdTimeCode displayTime, pxr::UsdTimeCode editTime) { m_displayTime = displayTime; m_editTime = editTime; m_renderer.SetCurrentTimeCode(displayTime); m_renderer.SetForceRefresh(true); } void ViewportTile::SetStage(pxr::UsdStageRefPtr stage) { m_stage = stage; m_renderer.SetStage(stage); m_camera.SetStage(stage); m_selectedCameraIndex = 0; m_cameraListDirty = true; m_hasLastGfCamera = false; m_hasSavedFreeCameraState = false; m_isDrivingUsdCamPrim = false; m_drivenUsdCamPath = pxr::SdfPath(); m_camera.SwitchToFreeCamera(); if (stage) { pxr::TfTokenVector purposes = { pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy }; pxr::UsdGeomBBoxCache bboxCache(m_displayTime, purposes, true); pxr::GfBBox3d stageBBox = bboxCache.ComputeWorldBound(stage->GetPseudoRoot()); if (!stageBBox.GetRange().IsEmpty()) m_camera.FrameSelection(stageBBox, 1.1); } } // --------------------------------------------------------------------------- void ViewportTile::SetSelectedPaths(const pxr::SdfPathVector& paths, const std::string& primaryPath) { m_selectedSdfPaths = paths; m_selectedPrimPath = primaryPath; m_renderer.SetSelectedPaths(paths); } // --------------------------------------------------------------------------- void ViewportTile::FrameScene() { if (!m_stage) return; pxr::TfTokenVector purposes = { pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy }; pxr::UsdGeomBBoxCache bboxCache(m_displayTime, purposes, true); pxr::GfBBox3d stageBBox = bboxCache.ComputeWorldBound(m_stage->GetPseudoRoot()); if (!stageBBox.GetRange().IsEmpty()) { InitCameraNavigation(); m_camera.FrameSelection(stageBBox, 1.1); } } // --------------------------------------------------------------------------- void ViewportTile::RefreshCameraList() { m_cameraPaths.clear(); if (m_stage) { for (pxr::UsdPrim prim : m_stage->Traverse()) if (prim.IsA()) m_cameraPaths.push_back(prim.GetPath()); } m_cameraListDirty = false; } void ViewportTile::TrySwitchToFreeCamera() { if (m_camera.GetMode() != ViewportCamera::CameraMode::UsdCamera) return; m_selectedCameraIndex = 0; m_isDrivingUsdCamPrim = false; if (m_hasSavedFreeCameraState) m_camera.SwitchToFreeCamera(&m_savedFreeCameraState); else m_camera.SwitchToFreeCamera(m_hasLastGfCamera ? &m_lastComputedGfCamera : nullptr); } void ViewportTile::InitCameraNavigation() { if (m_camera.GetMode() != ViewportCamera::CameraMode::UsdCamera) return; pxr::SdfPath camPath = m_camera.GetUsdCameraPath(); if (!m_stage || camPath.IsEmpty()) { TrySwitchToFreeCamera(); return; } pxr::UsdPrim prim = m_stage->GetPrimAtPath(camPath); if (!prim || !prim.IsA()) { TrySwitchToFreeCamera(); return; } pxr::UsdGeomCamera usdCam(prim); pxr::GfCamera gfCam = usdCam.GetCamera(m_displayTime); m_camera.SwitchToFreeCamera(&gfCam); // SwitchToFreeCamera calls PullFromCameraTransform, which extracts orbital // theta/phi via Euler decomposition and uses GetFocusDistance() as m_dist. // Both are wrong for a USD camera prim: the focus distance is a DOF attribute // (defaults to 5 units) and the Euler decomposition produces gimbal-affected // angles when the camera has roll or an unusual orientation. // Fix: compute orbit center from the scene bbox, then call // InitOrbitFromEyeAndCenter to derive theta/phi from geometry with zero roll. { pxr::TfTokenVector purposes = { pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy }; pxr::UsdGeomBBoxCache bboxCache(m_displayTime, purposes, true); pxr::GfBBox3d stageBBox = bboxCache.ComputeWorldBound(m_stage->GetPseudoRoot()); if (!stageBBox.GetRange().IsEmpty()) { pxr::GfVec3d sceneCenter = stageBBox.ComputeCentroid(); pxr::GfVec3d camEye = gfCam.GetFrustum().GetPosition(); pxr::GfVec3d viewDir = gfCam.GetFrustum().ComputeViewDirection(); // Project scene centre onto the view ray for the orbit distance; // keeps the camera at its authored position. double dist = pxr::GfDot(sceneCenter - camEye, viewDir); if (dist > 0.01) m_camera.InitOrbitFromEyeAndCenter( camEye, camEye + dist * viewDir, dist); } } m_isDrivingUsdCamPrim = true; m_drivenUsdCamPath = camPath; } pxr::GfVec3d ViewportTile::ComputeGizmoPivot() const { if (m_selectedSdfPaths.empty() || !m_stage) return pxr::GfVec3d(0.0); pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_selectedSdfPaths.front()); if (!prim) return pxr::GfVec3d(0.0); pxr::UsdGeomXformCache xformCache(m_displayTime); // Primary: use XformCommonAPI to read the authored translate and pivot op. // The manipulator must sit at the pivot's world-space position: // worldPivot = parentToWorld.Transform(translate + pivot) // NOT at the bounding-box center (geometry centroid) and NOT at // worldMatrix.ExtractTranslation() which folds in the pivot-inverse op // and gives the geometry origin, not the authored pivot point. pxr::UsdGeomXformCommonAPI api(prim); pxr::GfVec3d translate; pxr::GfVec3f rotation, scale, pivot; pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder; if (api.GetXformVectors(&translate, &rotation, &scale, &pivot, &rotOrder, m_displayTime)) { pxr::GfVec3d localPivot = translate + pxr::GfVec3d(pivot[0], pivot[1], pivot[2]); pxr::UsdPrim parent = prim.GetParent(); if (parent && !parent.IsPseudoRoot()) { pxr::GfMatrix4d p2w = xformCache.GetLocalToWorldTransform(parent); return p2w.Transform(localPivot); } return localPivot; // root-level prim: parent space == world space } // Fallback for incompatible op stacks (e.g. xformOp:transform matrix ops): // use the prim's world-space origin from the local-to-world matrix. pxr::GfMatrix4d worldXform = xformCache.GetLocalToWorldTransform(prim); return worldXform.ExtractTranslation(); } // --------------------------------------------------------------------------- // BuildOrthoCamera // --------------------------------------------------------------------------- // Camera-to-world rotation matrices (row-major GfMatrix4d: rows are cam axes). // Right-hand rule verified: cross(cam_X, cam_Y) == cam_Z for every entry. // // Y-up (isZUp=false) // Top cam_X=(1,0,0) cam_Y=(0,0,-1) cam_Z=(0,1,0) eye=c+(0,+d,0) // Bottom cam_X=(1,0,0) cam_Y=(0,0,+1) cam_Z=(0,-1,0) eye=c+(0,-d,0) // Front cam_X=(1,0,0) cam_Y=(0,1,0) cam_Z=(0,0,+1) eye=c+(0,0,+d) // Back cam_X=(-1,0,0) cam_Y=(0,1,0) cam_Z=(0,0,-1) eye=c+(0,0,-d) // Left cam_X=(0,0,+1) cam_Y=(0,1,0) cam_Z=(-1,0,0) eye=c+(-d,0,0) // Right cam_X=(0,0,-1) cam_Y=(0,1,0) cam_Z=(+1,0,0) eye=c+(+d,0,0) // // Z-up (isZUp=true) // Top cam_X=(1,0,0) cam_Y=(0,1,0) cam_Z=(0,0,+1) eye=c+(0,0,+d) // Bottom cam_X=(1,0,0) cam_Y=(0,-1,0) cam_Z=(0,0,-1) eye=c+(0,0,-d) // Front cam_X=(1,0,0) cam_Y=(0,0,+1) cam_Z=(0,-1,0) eye=c+(0,-d,0) // Back cam_X=(-1,0,0) cam_Y=(0,0,+1) cam_Z=(0,+1,0) eye=c+(0,+d,0) // Left cam_X=(0,-1,0) cam_Y=(0,0,+1) cam_Z=(-1,0,0) eye=c+(-d,0,0) // Right cam_X=(0,+1,0) cam_Y=(0,0,+1) cam_Z=(+1,0,0) eye=c+(+d,0,0) // --------------------------------------------------------------------------- pxr::GfCamera ViewportTile::BuildOrthoCamera() const { const bool zUp = m_camera.IsZUp(); const double d = m_camera.GetDist(); const pxr::GfVec3d c = m_camera.GetFocalPoint(); const double aspect = (m_viewWidth > 0 && m_viewHeight > 0) ? static_cast(m_viewWidth) / static_cast(m_viewHeight) : 1.0; pxr::GfCamera cam; // Visible vertical world span = d*2. Scales with zoom/framing naturally. cam.SetOrthographicFromAspectRatioAndSize( static_cast(aspect), static_cast(d * 2.0), pxr::GfCamera::FOVVertical); // Large symmetric range: ortho cameras may need to see geometry behind // the nominal camera position (e.g., geometry above a Top-view camera). cam.SetClippingRange(pxr::GfRange1f( static_cast(-d * 10.0), static_cast( d * 100.0))); // Build camera-to-world matrix from three cam-space axes + eye position. auto M = []( double r00, double r01, double r02, double r10, double r11, double r12, double r20, double r21, double r22, const pxr::GfVec3d& e) -> pxr::GfMatrix4d { return pxr::GfMatrix4d( r00, r01, r02, 0, r10, r11, r12, 0, r20, r21, r22, 0, e[0], e[1], e[2], 1); }; pxr::GfMatrix4d xform(1.0); if (!zUp) { switch (m_orthoView) { case OrthoView::Top: xform = M( 1,0, 0, 0,0,-1, 0, 1, 0, c+pxr::GfVec3d( 0, d, 0)); break; case OrthoView::Bottom: xform = M( 1,0, 0, 0,0, 1, 0,-1, 0, c+pxr::GfVec3d( 0,-d, 0)); break; case OrthoView::Front: xform = M( 1,0, 0, 0,1, 0, 0, 0, 1, c+pxr::GfVec3d( 0, 0, d)); break; case OrthoView::Back: xform = M(-1,0, 0, 0,1, 0, 0, 0,-1, c+pxr::GfVec3d( 0, 0,-d)); break; case OrthoView::Left: xform = M( 0,0, 1, 0,1, 0, -1, 0, 0, c+pxr::GfVec3d(-d, 0, 0)); break; case OrthoView::Right: xform = M( 0,0,-1, 0,1, 0, 1, 0, 0, c+pxr::GfVec3d( d, 0, 0)); break; default: break; } } else { switch (m_orthoView) { case OrthoView::Top: xform = M( 1,0, 0, 0, 1, 0, 0, 0, 1, c+pxr::GfVec3d( 0, 0, d)); break; case OrthoView::Bottom: xform = M( 1,0, 0, 0,-1, 0, 0, 0,-1, c+pxr::GfVec3d( 0, 0,-d)); break; case OrthoView::Front: xform = M( 1,0, 0, 0, 0, 1, 0,-1, 0, c+pxr::GfVec3d( 0,-d, 0)); break; case OrthoView::Back: xform = M(-1,0, 0, 0, 0, 1, 0, 1, 0, c+pxr::GfVec3d( 0, d, 0)); break; case OrthoView::Left: xform = M( 0,-1,0, 0, 0, 1, -1, 0, 0, c+pxr::GfVec3d(-d, 0, 0)); break; case OrthoView::Right: xform = M( 0, 1,0, 0, 0, 1, 1, 0, 0, c+pxr::GfVec3d( d, 0, 0)); break; default: break; } } cam.SetTransform(xform); return cam; } // --------------------------------------------------------------------------- // ResolveCamera // --------------------------------------------------------------------------- pxr::GfCamera ViewportTile::ResolveCamera() { double aspect = static_cast(m_viewWidth) / static_cast(m_viewHeight); pxr::GfCamera gfCamera; // -- Orthographic preset (short-circuit) --------------------------------- if (m_orthoView != OrthoView::None) { gfCamera = BuildOrthoCamera(); m_lastComputedGfCamera = gfCamera; m_hasLastGfCamera = true; m_renderer.SetCameraStateFromGfCamera(gfCamera); return gfCamera; } if (m_camera.GetMode() == ViewportCamera::CameraMode::Free || m_isDrivingUsdCamPrim) { m_camera.SetAspectRatio(aspect); pxr::GfBBox3d emptyBBox; gfCamera = m_camera.ComputeGfCamera(emptyBBox, false); pxr::CameraUtilConformWindow( &gfCamera, pxr::CameraUtilMatchVertically, aspect); m_lastComputedGfCamera = gfCamera; m_hasLastGfCamera = true; m_renderer.SetCameraStateFromGfCamera(gfCamera); if (m_isDrivingUsdCamPrim) { pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_drivenUsdCamPath); if (prim && prim.IsA()) { // Author the full camera-to-world matrix as a single transform // op rather than decomposing to XYZ-euler translate/rotate. // The euler round-trip (Decompose(X,Y,Z) -> rotateXYZ op) is // lossy: the read-back orientation differs from what was // written, which made the view jump every time a drag finished // and the mode flipped back to reading the prim. A matrix op // round-trips exactly through UsdGeomCamera::GetCamera(). pxr::UsdGeomXformable xformable(prim); bool resets = false; std::vector ops = xformable.GetOrderedXformOps(&resets); pxr::UsdGeomXformOp matrixOp; if (ops.size() == 1 && ops[0].GetOpType() == pxr::UsdGeomXformOp::TypeTransform) matrixOp = ops[0]; else matrixOp = xformable.MakeMatrixXform(); if (matrixOp) matrixOp.Set(gfCamera.GetTransform(), m_editTime); } if (!m_isOrbiting && !m_isPanning && !m_isDollying) { m_isDrivingUsdCamPrim = false; m_camera.SetUsdCamera(m_drivenUsdCamPath); m_renderer.SetCameraPath(m_drivenUsdCamPath); } } } else { pxr::SdfPath camPath = m_camera.GetUsdCameraPath(); pxr::UsdPrim camPrim = m_stage->GetPrimAtPath(camPath); if (camPrim && camPrim.IsA()) { pxr::UsdGeomCamera usdCam(camPrim); gfCamera = usdCam.GetCamera(m_displayTime); pxr::CameraUtilConformWindow( &gfCamera, pxr::CameraUtilMatchVertically, aspect); m_lastComputedGfCamera = gfCamera; m_hasLastGfCamera = true; m_renderer.SetCameraStateFromGfCamera(gfCamera); } } return gfCamera; } // --------------------------------------------------------------------------- // HandleInput // --------------------------------------------------------------------------- void ViewportTile::HandleInput(bool isFocused, TransformManipulator& manipulator, bool dividerActive) { ImGuiIO& io = ImGui::GetIO(); ImVec2 mousePos = ImGui::GetMousePos(); // IsWindowHovered scopes to this child window bool hovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows); bool altHeld = io.KeyAlt; // Track click for focus update ??any mouse button press in this tile // makes it the focused (active) viewport. if (hovered && (ImGui::IsMouseClicked(ImGuiMouseButton_Left) || ImGui::IsMouseClicked(ImGuiMouseButton_Middle) || ImGui::IsMouseClicked(ImGuiMouseButton_Right))) m_wasClickedThisFrame = true; // --- Manipulator input (only in focused tile) --- bool manipConsumed = false; if (isFocused && manipulator.GetMode() != ManipulatorMode::Select && !m_selectedSdfPaths.empty() && m_stage && m_viewWidth > 0 && m_viewHeight > 0 && m_hasLastGfCamera) { pxr::GfVec3d pivot = ComputeGizmoPivot(); pxr::GfVec3d camEye = m_lastComputedGfCamera.GetFrustum().GetPosition(); pxr::GfFrustum frustum = m_lastComputedGfCamera.GetFrustum(); pxr::GfMatrix4d viewProj = frustum.ComputeViewMatrix() * frustum.ComputeProjectionMatrix(); manipConsumed = manipulator.HandleInput( viewProj, pivot, camEye, m_imageScreenPos, m_viewWidth, m_viewHeight, hovered); } // --- Start camera drags --- if (hovered && !manipConsumed) { if (altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { if (m_orthoView == OrthoView::None) { // Perspective free camera: orbit InitCameraNavigation(); m_isOrbiting = true; } else { // Ortho view: Alt+LMB pans (same as Alt+MMB in Maya) m_isPanning = true; } m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y; } if (altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) { InitCameraNavigation(); m_isPanning = true; m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y; } if (altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { InitCameraNavigation(); m_isDollying = true; m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y; } if (!altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !dividerActive && // don't start rect-select while a split handle is active m_stage && m_viewWidth > 0 && m_viewHeight > 0) { m_rectAnchor = mousePos; m_rectCurrent = mousePos; m_rectDragStarted = true; m_isRectSelecting = false; } } // --- Stop Alt-based camera drags when Alt is released --- // Alt+LMB / Alt+MMB / Alt+RMB navigation should end as soon as Alt is // released, even if the mouse button is still held. if (!altHeld) { m_isOrbiting = false; m_isPanning = false; m_isDollying = false; } // --- Process camera drags --- if (m_isOrbiting) { float dx = mousePos.x - m_lastMouseX; float dy = mousePos.y - m_lastMouseY; m_camera.Tumble(0.25 * dx, 0.25 * dy); m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y; } if (m_isPanning) { float dx = mousePos.x - m_lastMouseX; float dy = mousePos.y - m_lastMouseY; if (m_orthoView != OrthoView::None) { // Ortho pan: derive right/up from the ortho camera matrix so the // movement direction always matches the view, regardless of whatever // stale theta/phi the underlying free-camera orbital state carries. // Scale: visible world height = dist*2, so 1 pixel = (dist*2)/viewH. pxr::GfCamera orthoCam = BuildOrthoCamera(); pxr::GfMatrix4d xform = orthoCam.GetTransform(); // row0 = cam_X (screen right), row1 = cam_Y (screen up) in world space pxr::GfVec3d screenRight(xform[0][0], xform[0][1], xform[0][2]); pxr::GfVec3d screenUp (xform[1][0], xform[1][1], xform[1][2]); double factor = m_camera.GetDist() * 2.0 / static_cast(std::max(m_viewHeight, 1)); pxr::GfVec3d newCenter = m_camera.GetFocalPoint() - screenRight * (static_cast(dx) * factor) + screenUp * (static_cast(dy) * factor); m_camera.SetFocalPoint(newCenter); } else { double factor = m_camera.ComputePixelsToWorldFactor( static_cast(std::max(m_viewHeight, 1))); m_camera.Truck(static_cast(-dx) * factor, static_cast( dy) * factor); } m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y; } if (m_isDollying) { float dx = mousePos.x - m_lastMouseX; float dy = mousePos.y - m_lastMouseY; double zoomDelta = -0.002 * (dx + dy); m_camera.AdjustDistance(1.0 + zoomDelta); m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y; } // --- Update rect selection while LMB held --- if (m_rectDragStarted && !altHeld && ImGui::IsMouseDown(ImGuiMouseButton_Left) && !m_isOrbiting && !m_isPanning && !m_isDollying && !manipulator.IsDragging()) { m_rectCurrent = mousePos; float dx = m_rectCurrent.x - m_rectAnchor.x; float dy = m_rectCurrent.y - m_rectAnchor.y; if (std::sqrt(dx * dx + dy * dy) > kRectDragThreshold) m_isRectSelecting = true; } // --- LMB released --- if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { m_isOrbiting = false; if (m_rectDragStarted && !altHeld && m_stage && m_viewWidth > 0 && m_viewHeight > 0) { if (hovered) { int ax = static_cast(m_rectAnchor.x - m_imageScreenPos.x); int ay = static_cast(m_rectAnchor.y - m_imageScreenPos.y); int cx = static_cast(m_rectCurrent.x - m_imageScreenPos.x); int cy = static_cast(m_rectCurrent.y - m_imageScreenPos.y); if (m_isRectSelecting) { pxr::SdfPathVector hitPaths; if (m_renderer.PickObjectsInRect( ax, ay, cx, cy, m_viewWidth, m_viewHeight, &hitPaths)) { if (io.KeyShift) { for (const auto& hp : hitPaths) if (std::find(m_selectedSdfPaths.begin(), m_selectedSdfPaths.end(), hp) == m_selectedSdfPaths.end()) m_selectedSdfPaths.push_back(hp); } else { m_selectedSdfPaths = hitPaths; } m_selectedPrimPath = m_selectedSdfPaths.empty() ? "" : m_selectedSdfPaths.front().GetString(); m_renderer.SetSelectedPaths(m_selectedSdfPaths); if (OnPrimsPickedRect) { std::vector pathStrs; pathStrs.reserve(m_selectedSdfPaths.size()); for (const auto& p : m_selectedSdfPaths) pathStrs.push_back(p.GetString()); OnPrimsPickedRect(pathStrs); } } else if (!io.KeyShift) { m_renderer.ClearSelected(); m_selectedPrimPath.clear(); m_selectedSdfPaths.clear(); if (OnPrimsPickedRect) OnPrimsPickedRect({}); } } else { // Single click pick if (cx >= 0 && cy >= 0 && cx < m_viewWidth && cy < m_viewHeight) { pxr::SdfPath camPickPath; bool hitCamera = false; if (m_hasLastGfCamera) { pxr::GfFrustum fr = m_lastComputedGfCamera.GetFrustum(); pxr::GfMatrix4d vp = fr.ComputeViewMatrix() * fr.ComputeProjectionMatrix(); hitCamera = m_renderer.PickCameraAtPoint( m_stage, mousePos.x, mousePos.y, vp, m_imageScreenPos.x, m_imageScreenPos.y, m_viewWidth, m_viewHeight, m_camera.GetDist(), &camPickPath); } pxr::SdfPath hitPath = hitCamera ? camPickPath : pxr::SdfPath(); bool hitGeom = false; pxr::GfVec3d hitPoint; if (!hitCamera) hitGeom = m_renderer.PickObject( cx, cy, m_viewWidth, m_viewHeight, &hitPoint, &hitPath); if (hitCamera || hitGeom) { if (io.KeyShift) { auto it = std::find(m_selectedSdfPaths.begin(), m_selectedSdfPaths.end(), hitPath); if (it != m_selectedSdfPaths.end()) m_selectedSdfPaths.erase(it); else m_selectedSdfPaths.push_back(hitPath); m_selectedPrimPath = m_selectedSdfPaths.empty() ? "" : m_selectedSdfPaths.back().GetString(); m_renderer.ClearSelected(); for (const auto& p : m_selectedSdfPaths) m_renderer.AddSelected(p); if (OnPrimsPickedRect) { std::vector pathStrs; pathStrs.reserve(m_selectedSdfPaths.size()); for (const auto& p : m_selectedSdfPaths) pathStrs.push_back(p.GetString()); OnPrimsPickedRect(pathStrs); } } else { m_selectedPrimPath = hitPath.GetString(); m_selectedSdfPaths = { hitPath }; m_renderer.ClearSelected(); m_renderer.AddSelected(hitPath); if (OnPrimPicked) OnPrimPicked(m_selectedPrimPath); } } else { if (!io.KeyShift) { m_renderer.ClearSelected(); m_selectedPrimPath.clear(); m_selectedSdfPaths.clear(); if (OnPrimPicked) OnPrimPicked(""); } } } } } } m_rectDragStarted = false; m_isRectSelecting = false; } if (ImGui::IsMouseReleased(ImGuiMouseButton_Middle)) m_isPanning = false; if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) m_isDollying = false; // --- Mouse wheel zoom --- if (hovered && io.MouseWheel != 0.0f) { InitCameraNavigation(); double delta = std::max(-0.5, std::min(0.5, static_cast(io.MouseWheel) * 0.12)); m_camera.AdjustDistance(1.0 - delta); } // --- Keyboard shortcuts --- // All shortcuts are gated on `hovered` (mouse is over this tile) so that // exactly one viewport responds per key press. This matches standard DCC // behaviour: the viewport under the cursor is the "active" one for both // mouse and keyboard interaction. if (!io.WantTextInput && hovered) { if (ImGui::IsKeyPressed(ImGuiKey_F)) { if (!m_selectedSdfPaths.empty() && m_stage) { pxr::TfTokenVector purposes = { pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy }; pxr::UsdGeomBBoxCache bboxCache( m_displayTime, purposes, true); pxr::GfRange3d combined; for (const auto& path : m_selectedSdfPaths) { pxr::UsdPrim prim = m_stage->GetPrimAtPath(path); if (!prim) continue; pxr::GfBBox3d bbox = bboxCache.ComputeWorldBound(prim); combined.UnionWith(bbox.ComputeAlignedRange()); } if (combined.IsEmpty()) { pxr::UsdGeomXformCache xformCache(m_displayTime); for (const auto& path : m_selectedSdfPaths) { pxr::UsdPrim prim = m_stage->GetPrimAtPath(path); if (!prim) continue; pxr::GfMatrix4d worldXform = xformCache.GetLocalToWorldTransform(prim); pxr::GfVec3d pos = worldXform.ExtractTranslation(); combined.UnionWith(pos - pxr::GfVec3d(1.0)); combined.UnionWith(pos + pxr::GfVec3d(1.0)); } } if (!combined.IsEmpty()) { InitCameraNavigation(); m_camera.FrameSelection(pxr::GfBBox3d(combined), 1.1); m_renderer.SetForceRefresh(true); } } } if (ImGui::IsKeyPressed(ImGuiKey_A)) { FrameScene(); m_renderer.SetForceRefresh(true); } // Q/W/E/R tool mode and World/Object space are handled globally by // ViewportPanel so they always work regardless of which tile is hovered. } } // --------------------------------------------------------------------------- // DrawSelectionRect // --------------------------------------------------------------------------- void ViewportTile::DrawSelectionRect() { if (!m_isRectSelecting || !m_rectDragStarted) return; float vpMinX = m_imageScreenPos.x; float vpMinY = m_imageScreenPos.y; float vpMaxX = vpMinX + static_cast(m_viewWidth); float vpMaxY = vpMinY + static_cast(m_viewHeight); ImVec2 drawMin( std::max(vpMinX, std::min(m_rectAnchor.x, m_rectCurrent.x)), std::max(vpMinY, std::min(m_rectAnchor.y, m_rectCurrent.y))); ImVec2 drawMax( std::min(vpMaxX, std::max(m_rectAnchor.x, m_rectCurrent.x)), std::min(vpMaxY, std::max(m_rectAnchor.y, m_rectCurrent.y))); if (drawMin.x >= drawMax.x || drawMin.y >= drawMax.y) return; ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(drawMin, drawMax, IM_COL32(100, 160, 255, 40)); dl->AddRect (drawMin, drawMax, IM_COL32(100, 160, 255, 220), 0.0f, 0, 1.5f); } // --------------------------------------------------------------------------- // RenderContextMenu // --------------------------------------------------------------------------- void ViewportTile::RenderContextMenu(int tileIndex) { if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) && ImGui::IsMouseReleased(ImGuiMouseButton_Right) && !ImGui::GetIO().KeyAlt) { std::string popupId = "VPCtxMenu_" + std::to_string(tileIndex); ImGui::OpenPopup(popupId.c_str()); } std::string popupId = "VPCtxMenu_" + std::to_string(tileIndex); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8)); if (ImGui::BeginPopup(popupId.c_str())) { bool showGrid = m_renderer.ShowGrid(); if (ImGui::MenuItem("Show Grid", nullptr, &showGrid)) m_renderer.SetShowGrid(showGrid); ImGui::Separator(); bool ambientOnly = m_renderer.GetAmbientLightOnly(); if (ImGui::MenuItem("Camera Light", nullptr, &ambientOnly)) m_renderer.SetAmbientLightOnly(ambientOnly); bool domeLight = m_renderer.GetDomeLightEnabled(); if (ImGui::MenuItem("Dome Light", nullptr, &domeLight)) m_renderer.SetDomeLightEnabled(domeLight); ImGui::Separator(); if (ImGui::BeginMenu("Bounding Box")) { BBoxMode cur = m_renderer.GetBBoxMode(); if (ImGui::MenuItem("None", nullptr, cur == BBoxMode::None)) m_renderer.SetBBoxMode(BBoxMode::None); if (ImGui::MenuItem("Per Object", nullptr, cur == BBoxMode::PerObject)) m_renderer.SetBBoxMode(BBoxMode::PerObject); if (ImGui::MenuItem("All Selection",nullptr, cur == BBoxMode::AllSelection)) m_renderer.SetBBoxMode(BBoxMode::AllSelection); ImGui::EndMenu(); } ImGui::Separator(); if (ImGui::BeginMenu("Background")) { auto& bg = m_renderer.GetBackgroundColor(); if (ImGui::MenuItem("Dark Gray", nullptr, bg == pxr::GfVec3f(0.15f, 0.15f, 0.15f))) m_renderer.SetBackgroundColor(pxr::GfVec3f(0.15f, 0.15f, 0.15f)); if (ImGui::MenuItem("Black", nullptr, bg == pxr::GfVec3f(0.0f, 0.0f, 0.0f))) m_renderer.SetBackgroundColor(pxr::GfVec3f(0.0f, 0.0f, 0.0f)); if (ImGui::MenuItem("Light Gray", nullptr, bg == pxr::GfVec3f(0.45f, 0.45f, 0.45f))) m_renderer.SetBackgroundColor(pxr::GfVec3f(0.45f, 0.45f, 0.45f)); if (ImGui::MenuItem("Midnight Blue", nullptr, bg == pxr::GfVec3f(0.1f, 0.1f, 0.2f))) m_renderer.SetBackgroundColor(pxr::GfVec3f(0.1f, 0.1f, 0.2f)); ImGui::EndMenu(); } ImGui::EndPopup(); } ImGui::PopStyleVar(); } // --------------------------------------------------------------------------- // RenderCompactToolbar -- all per-tile controls in one compact row // --------------------------------------------------------------------------- void ViewportTile::RenderCompactToolbar(int tileIndex) { if (m_cameraListDirty) RefreshCameraList(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(3, 3)); // -- Camera selector ----------------------------------------------------- ImGui::PushItemWidth(140.0f); // Helper: ortho view display name auto OrthoName = [](OrthoView v) -> const char* { switch (v) { case OrthoView::Top: return "Top"; case OrthoView::Bottom: return "Bottom"; case OrthoView::Front: return "Front"; case OrthoView::Back: return "Back"; case OrthoView::Left: return "Left"; case OrthoView::Right: return "Right"; default: return nullptr; } }; // Determine display label: ortho view name > USD cam prim path > "Free Camera" const char* currentLabel = "Free Camera"; if (m_orthoView != OrthoView::None) { currentLabel = OrthoName(m_orthoView); } else if (m_selectedCameraIndex > 0 && static_cast(m_selectedCameraIndex - 1) < m_cameraPaths.size()) { currentLabel = m_cameraPaths[m_selectedCameraIndex - 1].GetText(); } std::string camComboId = "##Cam_" + std::to_string(tileIndex); if (ImGui::BeginCombo(camComboId.c_str(), currentLabel)) { RefreshCameraList(); // -- Perspective free camera ----------------------------------------- bool freeSel = (m_orthoView == OrthoView::None && m_selectedCameraIndex == 0); if (ImGui::Selectable("Free Camera", freeSel)) { m_orthoView = OrthoView::None; m_selectedCameraIndex = 0; m_isDrivingUsdCamPrim = false; if (m_hasSavedFreeCameraState) m_camera.SwitchToFreeCamera(&m_savedFreeCameraState); else m_camera.SwitchToFreeCamera( m_hasLastGfCamera ? &m_lastComputedGfCamera : nullptr); } // -- Orthographic presets ------------------------------------------- ImGui::Separator(); static const struct { OrthoView view; const char* label; } kOrtho[] = { { OrthoView::Top, "Top" }, { OrthoView::Bottom, "Bottom" }, { OrthoView::Front, "Front" }, { OrthoView::Back, "Back" }, { OrthoView::Left, "Left" }, { OrthoView::Right, "Right" }, }; for (const auto& ov : kOrtho) { bool isSel = (m_orthoView == ov.view); if (ImGui::Selectable(ov.label, isSel)) { m_orthoView = ov.view; m_selectedCameraIndex = 0; m_isDrivingUsdCamPrim = false; // Ensure free-camera mode so center/dist are maintained if (m_camera.GetMode() == ViewportCamera::CameraMode::UsdCamera) { m_camera.SwitchToFreeCamera( m_hasLastGfCamera ? &m_lastComputedGfCamera : nullptr); } } if (isSel) ImGui::SetItemDefaultFocus(); } // -- USD camera prims ----------------------------------------------- if (!m_cameraPaths.empty()) ImGui::Separator(); for (size_t i = 0; i < m_cameraPaths.size(); ++i) { bool isSelected = (m_orthoView == OrthoView::None && m_selectedCameraIndex == static_cast(i + 1)); if (ImGui::Selectable(m_cameraPaths[i].GetText(), isSelected)) { if (m_camera.GetMode() == ViewportCamera::CameraMode::Free && m_hasLastGfCamera) { m_savedFreeCameraState = m_lastComputedGfCamera; m_hasSavedFreeCameraState = true; } m_orthoView = OrthoView::None; m_selectedCameraIndex = static_cast(i + 1); m_camera.SetUsdCamera(m_cameraPaths[i]); m_renderer.SetCameraPath(m_cameraPaths[i]); } if (isSelected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) { if (m_orthoView != OrthoView::None) ImGui::SetTooltip("Orthographic: %s\n" "Alt+LMB / Alt+MMB: Pan\n" "Alt+RMB / Scroll: Zoom\n" "F: Frame selection A: Frame all", OrthoName(m_orthoView)); else ImGui::SetTooltip("Camera\n" "Alt+LMB: Orbit Alt+MMB: Pan Alt+RMB: Dolly\n" "Scroll: Zoom F: Frame sel A: Frame all"); } ImGui::PopItemWidth(); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); // -- Render delegate ----------------------------------------------------- pxr::TfToken currentId = m_renderer.GetCurrentRendererId(); std::string displayName = currentId.IsEmpty() ? "Rdr" : UsdSceneRenderer::GetRendererDisplayName(currentId); if (displayName.size() > 8) displayName = displayName.substr(0, 8); std::string rdrBtnId = "##Rdr_" + std::to_string(tileIndex); ImGui::Button(displayName.c_str(), ImVec2(72.0f, 0.0f)); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Render delegate -- click to change"); ImGuiPopupFlags popupFlags = ImGuiPopupFlags_MouseButtonLeft; std::string rdrPopupId = "RdrPopup_" + std::to_string(tileIndex); if (ImGui::BeginPopupContextItem(rdrPopupId.c_str(), popupFlags)) { for (const auto& pluginId : UsdSceneRenderer::GetRendererPlugins()) { std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId); if (name.empty()) name = pluginId.GetString(); bool selected = (pluginId == currentId); if (ImGui::MenuItem(name.c_str(), nullptr, selected)) if (!selected) m_renderer.SetRendererPlugin(pluginId); } ImGui::EndPopup(); } ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); // -- View option toggles (icon buttons) ---------------------------------- auto iconToggle = [this](const char* id, const char* fallback, Icon iconEnum, bool active, const char* tooltip) -> bool { const ImVec4 kActive (0.26f, 0.59f, 0.98f, 1.00f); const ImVec4 kActiveHv(0.36f, 0.69f, 1.00f, 1.00f); if (active) { ImGui::PushStyleColor(ImGuiCol_Button, kActive); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kActiveHv); } bool clicked = false; if (m_iconManager) { ImTextureID tex = m_iconManager->Get(iconEnum); clicked = ImGui::ImageButton(id, ImTextureRef(tex), ImVec2(16.f, 16.f)); } else { clicked = ImGui::Button(fallback); } if (active) ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tooltip); return clicked; }; std::string gridId = "##grd_" + std::to_string(tileIndex); std::string aaId = "##aa_" + std::to_string(tileIndex); bool showGrid = m_renderer.ShowGrid(); if (iconToggle(gridId.c_str(), "Grd", Icon::Grid, showGrid, "Toggle grid")) m_renderer.SetShowGrid(!showGrid); ImGui::SameLine(); bool aa = m_renderer.GetAAEnabled(); if (iconToggle(aaId.c_str(), "AA", Icon::Antialias, aa, "Toggle anti-aliasing")) m_renderer.SetAAEnabled(!aa); ImGui::PopStyleVar(); // ItemSpacing } // --------------------------------------------------------------------------- // RenderManipulatorOverlay -- vertical Q/W/E/R + space buttons on the image // --------------------------------------------------------------------------- void ViewportTile::RenderManipulatorOverlay(TransformManipulator& manipulator) { const float kButtonSize = 32.0f; const float kIconPad = 4.0f; const float kRounding = 4.0f; const float kPadX = 10.0f; const float kPadY = 10.0f; const float kSpacing = 4.0f; const ImVec2 kBtnSz(kButtonSize, kButtonSize); ImVec2 origin(m_imageScreenPos.x + kPadX, m_imageScreenPos.y + kPadY); struct ToolInfo { ManipulatorMode mode; Icon icon; const char* fallbackLabel; const char* tooltip; const char* id; }; static const ToolInfo tools[] = { { ManipulatorMode::Select, Icon::ToolSelect, "Q", "Select (Q)", "##ovl_q" }, { ManipulatorMode::Move, Icon::ToolMove, "W", "Move (W)", "##ovl_w" }, { ManipulatorMode::Rotate, Icon::ToolRotate, "E", "Rotate (E)", "##ovl_e" }, { ManipulatorMode::Scale, Icon::ToolScale, "R", "Scale (R)", "##ovl_r" }, }; ManipulatorMode current = manipulator.GetMode(); ImDrawList* dl = ImGui::GetWindowDrawList(); for (int i = 0; i < 4; ++i) { const auto& t = tools[i]; bool active = (current == t.mode); ImVec2 btnMin(origin.x, origin.y + i * (kButtonSize + kSpacing)); ImVec2 btnMax(btnMin.x + kButtonSize, btnMin.y + kButtonSize); ImGui::SetCursorScreenPos(btnMin); bool clicked = ImGui::InvisibleButton(t.id, kBtnSz); bool hovBtn = ImGui::IsItemHovered(); ImU32 bgCol = active ? IM_COL32( 66, 150, 250, 230) : hovBtn ? IM_COL32( 64, 64, 64, 220) : IM_COL32( 26, 26, 26, 178); dl->AddRectFilled(btnMin, btnMax, bgCol, kRounding); if (m_iconManager) { ImTextureID texId = m_iconManager->Get(t.icon); dl->AddImage(ImTextureRef(texId), ImVec2(btnMin.x + kIconPad, btnMin.y + kIconPad), ImVec2(btnMax.x - kIconPad, btnMax.y - kIconPad)); } else { float tx = btnMin.x + (kButtonSize - ImGui::CalcTextSize(t.fallbackLabel).x) * 0.5f; float ty = btnMin.y + (kButtonSize - ImGui::GetTextLineHeight()) * 0.5f; dl->AddText(ImVec2(tx, ty), IM_COL32(255,255,255,255), t.fallbackLabel); } if (active) dl->AddRect(btnMin, btnMax, IM_COL32(100, 180, 255, 200), kRounding, 0, 1.5f); if (clicked) manipulator.SetMode(t.mode); if (hovBtn) ImGui::SetTooltip("%s", t.tooltip); } // World / Object space toggle float spaceY = origin.y + 4 * (kButtonSize + kSpacing) + 4.0f; ImVec2 spBtnMin(origin.x, spaceY); ImVec2 spBtnMax(spBtnMin.x + kButtonSize, spBtnMin.y + kButtonSize); bool isWorld = (manipulator.GetTransformSpace() == TransformSpace::World); ImGui::SetCursorScreenPos(spBtnMin); bool spClicked = ImGui::InvisibleButton("##ovl_space", kBtnSz); bool spHovered = ImGui::IsItemHovered(); ImU32 spBgCol = isWorld ? IM_COL32( 66, 150, 250, 230) : spHovered ? IM_COL32( 64, 64, 64, 220) : IM_COL32( 26, 26, 26, 178); dl->AddRectFilled(spBtnMin, spBtnMax, spBgCol, kRounding); if (m_iconManager) { ImTextureID spTex = m_iconManager->Get(isWorld ? Icon::WorldSpace : Icon::LocalSpace); dl->AddImage(ImTextureRef(spTex), ImVec2(spBtnMin.x + kIconPad, spBtnMin.y + kIconPad), ImVec2(spBtnMax.x - kIconPad, spBtnMax.y - kIconPad)); } else { const char* spLabel = isWorld ? "W" : "O"; float spTx = spBtnMin.x + (kButtonSize - ImGui::CalcTextSize(spLabel).x) * 0.5f; float spTy = spBtnMin.y + (kButtonSize - ImGui::GetTextLineHeight()) * 0.5f; dl->AddText(ImVec2(spTx, spTy), IM_COL32(255,255,255,255), spLabel); } if (isWorld) dl->AddRect(spBtnMin, spBtnMax, IM_COL32(100,180,255,200), kRounding, 0, 1.5f); if (spClicked) manipulator.SetTransformSpace(isWorld ? TransformSpace::Object : TransformSpace::World); if (spHovered) ImGui::SetTooltip(isWorld ? "World space\nClick to switch to Object space" : "Object space\nClick to switch to World space"); } // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- void ViewportTile::Render(int tileIndex, ImVec2 pos, ImVec2 size, bool isFocused, TransformManipulator& manipulator, bool dividerActive) { m_wasClickedThisFrame = false; m_wasHoveredThisFrame = false; ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::SetCursorScreenPos(pos); std::string childId = "##vptile_" + std::to_string(tileIndex); ImGuiWindowFlags flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoBackground; bool childOpen = ImGui::BeginChild(childId.c_str(), size, false, flags); ImGui::PopStyleVar(); // WindowPadding if (!childOpen) { ImGui::EndChild(); return; } // Hover detection for the whole child (used by Space maximize in container) m_wasHoveredThisFrame = ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows); // NOTE: Border is drawn AFTER EndChild (see below) so it is not clipped // by this child window's scissor rect. // -- Compact toolbar ------------------------------------------------------ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4, 2)); ImGui::SetCursorPos(ImVec2(4, 2)); RenderCompactToolbar(tileIndex); ImGui::PopStyleVar(); ImGui::Separator(); // -- Scene image area ----------------------------------------------------- ImVec2 avail = ImGui::GetContentRegionAvail(); m_viewWidth = static_cast(avail.x); m_viewHeight = static_cast(avail.y); if (m_viewWidth > 0 && m_viewHeight > 0 && m_stage) { m_imageScreenPos = ImGui::GetCursorScreenPos(); HandleInput(isFocused, manipulator, dividerActive); pxr::GfCamera gfCamera = ResolveCamera(); // Render scene into FBO m_renderer.Render(m_viewWidth, m_viewHeight); // Overlays into FBO if (m_hasLastGfCamera) { pxr::GfFrustum frustum = gfCamera.GetFrustum(); pxr::GfMatrix4d viewProj = frustum.ComputeViewMatrix() * frustum.ComputeProjectionMatrix(); m_renderer.DrawAxis(viewProj, m_camera.GetDist()); m_renderer.DrawBoundingBoxes(m_selectedSdfPaths, viewProj); m_renderer.DrawCameraWireframes( m_stage, m_selectedSdfPaths, m_camera.GetUsdCameraPath(), viewProj, m_camera.GetDist()); } // Display FBO texture (flip UV Y: OpenGL is bottom-up) uint32_t texID = m_renderer.GetColorTextureID(); if (texID != 0) { ImGui::Image( ImTextureID(static_cast(texID)), ImVec2(static_cast(m_viewWidth), static_cast(m_viewHeight)), ImVec2(0, 1), ImVec2(1, 0)); } else { ImGui::TextColored(ImVec4(1,0,0,1), "Viewport: No texture (renderer not initialised)"); } // -- 2-D ImGui overlays on top of the image -------------------------- DrawSelectionRect(); // Gizmo -- only in focused tile if (isFocused && m_hasLastGfCamera && manipulator.GetMode() != ManipulatorMode::Select && !m_selectedSdfPaths.empty()) { pxr::GfFrustum frustum = gfCamera.GetFrustum(); pxr::GfMatrix4d viewProj = frustum.ComputeViewMatrix() * frustum.ComputeProjectionMatrix(); pxr::GfVec3d pivot = ComputeGizmoPivot(); pxr::GfVec3d camEye = frustum.GetPosition(); manipulator.Render( ImGui::GetWindowDrawList(), viewProj, pivot, camEye, m_imageScreenPos, m_viewWidth, m_viewHeight); } } else { if (!m_stage) ImGui::TextDisabled("No stage loaded"); else ImGui::TextDisabled("Viewport too small (%dx%d)", m_viewWidth, m_viewHeight); } // -- Focus / hover border ------------------------------------------------- // Drawn as the LAST item inside the child window so it sits above the scene // texture in the draw list sequence. Because it is inside the child window // it is automatically clipped to this tile's bounds and cannot bleed over // floating panels or menus positioned outside this tile. // A 1 px inset ensures the full 2 px focused line stays within the clip rect. { ImVec2 bMin(pos.x + 1.f, pos.y + 1.f); ImVec2 bMax(pos.x + size.x - 1.f, pos.y + size.y - 1.f); ImDrawList* dl = ImGui::GetWindowDrawList(); if (isFocused) { dl->AddRect(bMin, bMax, IM_COL32(66, 150, 250, 230), 0.f, 0, 2.0f); } else if (m_wasHoveredThisFrame) { dl->AddRect(bMin, bMax, IM_COL32(220, 220, 220, 110), 0.f, 0, 1.0f); } } RenderContextMenu(tileIndex); ImGui::EndChild(); } } // namespace UsdLayerManager