diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index 1ec0395..bc00ed1 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -1,4 +1,4 @@ -#include "Application.h" +#include "Application.h" #include "../utils/Logger.h" #include "../utils/FileDialog.h" #include "../utils/PathUtils.h" diff --git a/src/ui/PropertyPanel.cpp b/src/ui/PropertyPanel.cpp index 830a6e6..b348cdc 100644 --- a/src/ui/PropertyPanel.cpp +++ b/src/ui/PropertyPanel.cpp @@ -116,6 +116,7 @@ void PropertyPanel::ReadTransform() { static_cast(translation[2])); m_rotate = rotation; m_scale = scale; + m_pivot = pivot; m_rotOrder = rotOrder; } else @@ -163,6 +164,7 @@ void PropertyPanel::ReadTransform() { static_cast(eulerDeg[1]), static_cast(eulerDeg[2])); m_rotOrder = UsdGeomXformCommonAPI::RotationOrderXYZ; + m_pivot = GfVec3f(0.f, 0.f, 0.f); m_xformFallback = true; } @@ -500,6 +502,23 @@ void PropertyPanel::RenderTransformSection() { } } + // ---- Pivot row (read-only) ---- + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(kColorLabel, "Pivot"); + for (int i = 0; i < 3; ++i) { + ImGui::TableSetColumnIndex(i + 1); + ImGui::SetNextItemWidth(-FLT_MIN); + // Use InputFloat with ReadOnly so the value is visible but not editable. + // Pivot changes require a dedicated pivot-editing mode (not yet supported). + char id[8]; snprintf(id, sizeof(id), "##pv%d", i); + float v = m_pivot[i]; + ImGui::PushStyleColor(ImGuiCol_FrameBg, + ImVec4(0.15f, 0.15f, 0.15f, 1.f)); // dimmer background signals read-only + ImGui::InputFloat(id, &v, 0.f, 0.f, "%.3f", ImGuiInputTextFlags_ReadOnly); + ImGui::PopStyleColor(); + } + ImGui::EndTable(); // Record whether any field was active this frame so Render() can decide diff --git a/src/ui/PropertyPanel.h b/src/ui/PropertyPanel.h index 0b1afcd..003d788 100644 --- a/src/ui/PropertyPanel.h +++ b/src/ui/PropertyPanel.h @@ -59,10 +59,11 @@ private: pxr::UsdStageRefPtr m_stage; std::string m_selectedPrimPath; - // Cached TRS values (float matches DragFloat precision) + // Cached TRS + pivot values (float matches DragFloat precision) pxr::GfVec3f m_translate{ 0.f, 0.f, 0.f }; pxr::GfVec3f m_rotate { 0.f, 0.f, 0.f }; pxr::GfVec3f m_scale { 1.f, 1.f, 1.f }; + pxr::GfVec3f m_pivot { 0.f, 0.f, 0.f }; pxr::UsdGeomXformCommonAPI::RotationOrder m_rotOrder{ pxr::UsdGeomXformCommonAPI::RotationOrderXYZ }; diff --git a/src/ui/TransformManipulator.cpp b/src/ui/TransformManipulator.cpp index 9bab30f..d84ed1f 100644 --- a/src/ui/TransformManipulator.cpp +++ b/src/ui/TransformManipulator.cpp @@ -403,13 +403,11 @@ void TransformManipulator::Render(ImDrawList* dl, const pxr::GfMatrix4d& vp, // Clock-hand: intersect mouse ray with ring plane (GfPlane) -> rotateTo vector. // worldRotation = GfRotation(dragStartClockHand, currentClockHand). // axisSign = dot(planeNormal, worldRotation.GetAxis()) > 0 ? 1 : -1. -// localDeltaAxis: -// Object => initRot.GetRow3(dragAxis) [usdtweak exact: local axis in parent space] -// World => world unit vector -// deltaRotation = GfRotation(localDeltaAxis * axisSign, angle). +// deltaAxis = canonical axis e_a for the dragged ring (both spaces). +// deltaRotation = GfRotation(deltaAxis * axisSign, angle). // resultingRotation: -// Object => GfMatrix4d(1).SetRotate(delta) * initRot [usdtweak exact] -// World => initRot * GfMatrix4d(1).SetRotate(delta) [USD row-vector world delta] +// Object => GfMatrix4d(1).SetRotate(delta) * initRot [delta in prim local frame] +// World => initRot * GfMatrix4d(1).SetRotate(delta) [delta about world axis] // DecomposeRotation with FIXED initRot rows as reference axes + current hints. // // SCALE (ScaleManipulator::OnUpdate) @@ -494,16 +492,24 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp, pxr::UsdGeomXformOp::GetOpTransform(opType,pxr::VtValue(rot)); // Capture delta axis and space mode at drag start. - // Object: initRot.GetRow3(axis) -- local axis in parent space. - // World : world unit vector. - if (m_transformSpace==TransformSpace::Object) { - m_dragRotateDeltaAxis=m_dragRotateInitialRotMat.GetRow3(m_dragAxis); - double n=m_dragRotateDeltaAxis.GetLength(); - if (n>1e-9) m_dragRotateDeltaAxis/=n; - } else { - static const pxr::GfVec3d kW[3]={{1,0,0},{0,1,0},{0,0,1}}; - m_dragRotateDeltaAxis=kW[m_dragAxis]; - } + // + // The delta axis is the CANONICAL axis e_a for the dragged ring + // in BOTH spaces; only the composition order (below) differs. + // + // Object: e_a applied in the prim's own local frame via + // deltaM * initRot. Because Rot(e_a) fixes e_a, the + // prim's current local axis (e_a * initRot) stays put, + // so the object rotates purely about its own axis + // (Maya local mode). + // World : e_a applied after initRot via initRot * deltaM, i.e. + // rotation about the fixed world axis. + // + // NOTE: using initRot.GetRow3(axis) (a parent-space vector) with + // deltaM * initRot is WRONG -- the axis is then applied in the + // local frame it does not belong to, which makes the object + // tumble once it already carries a rotation. + static const pxr::GfVec3d kAxis[3]={{1,0,0},{0,1,0},{0,0,1}}; + m_dragRotateDeltaAxis=kAxis[m_dragAxis]; m_dragRotateObjectSpace=(m_transformSpace==TransformSpace::Object); } else if (m_mode==ManipulatorMode::Scale) { @@ -584,13 +590,14 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp, ?1.0:-1.0; // usdtweak: const GfRotation deltaRotation(localPlaneNormal*axisSign, angle) + // deltaAxis is the canonical axis e_a for the dragged ring. pxr::GfRotation deltaRot(m_dragRotateDeltaAxis*axisSign, worldRotation.GetAngle()); pxr::GfMatrix4d deltaM=pxr::GfMatrix4d(1.0).SetRotate(deltaRot); - // usdtweak: resultingRotation = GfMatrix4d(1).SetRotate(delta)*initRot - // Object (usdtweak exact): deltaM * initRot - // World (USD row-vector) : initRot * deltaM + // resultingRotation: + // Object (rotate about prim's own axis): deltaM * initRot + // World (rotate about fixed world axis): initRot * deltaM pxr::GfMatrix4d result= m_dragRotateObjectSpace ? deltaM * m_dragRotateInitialRotMat diff --git a/src/ui/TransformManipulator.h b/src/ui/TransformManipulator.h index 8ccdcd6..d11bac0 100644 --- a/src/ui/TransformManipulator.h +++ b/src/ui/TransformManipulator.h @@ -108,19 +108,15 @@ private: // // Ring plane: world-space ring normal (for mouse-ray intersection + sign). // - // Delta-rotation axis differs by TransformSpace: - // Object => initRot.GetRow3(axis) -- local axis in PARENT space - // (exact usdtweak localPlaneNormal convention) - // World => world unit vector - // (correct for root-level / simple hierarchies) - // - // resultingRotation formula: - // Object => GfMatrix4d(1).SetRotate(delta) * initRot - // (usdtweak: delta applied in local frame before initRot) - // World => initRot * GfMatrix4d(1).SetRotate(delta) - // (USD row-vector: initRot maps local->parent, then world delta) + // Delta-rotation axis is the canonical axis e_a for the dragged ring in + // BOTH spaces; only the composition order differs: + // Object => SetRotate(delta) * initRot + // delta applied in the prim's own local frame, so the dragged + // local axis stays fixed -> rotates purely about its own axis. + // World => initRot * SetRotate(delta) + // initRot maps local->parent, then delta about the world axis. pxr::GfVec3d m_dragRotatePlaneNormal; // world-space - pxr::GfVec3d m_dragRotateDeltaAxis; // space-dependent (see above) + pxr::GfVec3d m_dragRotateDeltaAxis; // canonical axis e_a for ring pxr::GfVec3d m_dragRotateFrom; // clock-hand at drag start pxr::GfMatrix4d m_dragRotateInitialRotMat; // local->parent rot at drag start bool m_dragRotateObjectSpace = true; diff --git a/src/ui/ViewportPanel.h b/src/ui/ViewportPanel.h index 69b33b3..ab284e2 100644 --- a/src/ui/ViewportPanel.h +++ b/src/ui/ViewportPanel.h @@ -65,6 +65,7 @@ public: LayoutMode GetLayout() const { return m_layout; } int GetFocusedTileIndex() const { return m_focusedTileIndex; } + private: // ── Tile rect helper ───────────────────────────────────────────────────── struct TileRect { ImVec2 pos; ImVec2 size; }; diff --git a/src/ui/ViewportTile.cpp b/src/ui/ViewportTile.cpp index f240ba1..83e715f 100644 --- a/src/ui/ViewportTile.cpp +++ b/src/ui/ViewportTile.cpp @@ -130,16 +130,32 @@ pxr::GfVec3d ViewportTile::ComputeGizmoPivot() const pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_selectedSdfPaths.front()); if (!prim) return pxr::GfVec3d(0.0); - pxr::TfTokenVector purposes = { - pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy }; - pxr::UsdGeomBBoxCache bboxCache( - m_displayTime, purposes, true); - pxr::GfBBox3d bbox = bboxCache.ComputeWorldBound(prim); - pxr::GfRange3d range = bbox.ComputeAlignedRange(); - if (!range.IsEmpty()) - return (range.GetMin() + range.GetMax()) * 0.5; - 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(); }