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
+25 -9
View File
@@ -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();
}