Fix rotate manipulator axis and gizmo pivot placement

Rotate manipulator — correct axis bug:
- Object-space delta axis was set to initRot.GetRow3(axis) (a parent-space
  vector) then applied in the prim's local frame via deltaM * initRot.
  These frames are mismatched: the axis must be the canonical e_a so that
  Rot(e_a) fixes e_a and the prim rotates purely about its own axis.
  Fixed to use the canonical axis for both spaces; only composition order
  differs (deltaM * initRot for object, initRot * deltaM for world).

Gizmo pivot placement:
- ComputeGizmoPivot() used the bounding-box centre (geometry centroid),
  placing the gizmo at the wrong location for any non-centred prim.
  Replaced with parentToWorld.Transform(translate + pivot) from
  XformCommonAPI::GetXformVectors — the authored pivot point in world space.
  Fallback for incompatible op stacks uses worldMatrix.ExtractTranslation().

Property panel:
- Show pivot (X/Y/Z) as read-only row below Scale, populated from
  XformCommonAPI::GetXformVectors each frame.
- Removed active-rotate-axis display (was added and then removed per request).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 09:57:39 +08:00
parent e0b0fd8204
commit 7f0fa6a944
7 changed files with 82 additions and 42 deletions
+26 -19
View File
@@ -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