Fix rotate manipulator desync on already-rotated prims

The matrix->Euler decode at the end of a rotate drag used the rows of the
prim's initial rotation matrix (its already-rotated frame) as the
DecomposeRotation axes. But UsdGeomXformCommonAPI::SetRotate interprets the
written angles about the CANONICAL x/y/z axes, so the two only agreed when
the prim was at zero rotation -- the authored orientation desynced from the
manipulator the moment the prim already carried a rotation.

Decompose the result matrix about the canonical axes instead, matching how
SetRotate reconstructs it. The result-matrix computation (which correctly
tracks the visual ring axis) is unchanged.

Verified numerically via the real XformCommonAPI authoring path: from
rotateXYZ=(30,45,60), a +20deg object-X drag now authors exactly
(50,45,60) (err 0.0) vs the old decode's (41.07,27.07,66.61) (err 0.287).

Assumes RotationOrderXYZ (the XformCommonAPI default); non-XYZ orders are
mapped as before -- noted in a code comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 18:49:22 +08:00
parent 119a55b1aa
commit 4d3cc85ece
+16 -6
View File
@@ -645,11 +645,21 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
? deltaM * m_dragRotateInitialRotMat
: m_dragRotateInitialRotMat * deltaM;
// usdtweak: GfRotation::DecomposeRotation(resultingRotation,
// pxAxis, pyAxis, pzAxis, 1.0, &tw, &fb, &lr, &sw, true)
const pxr::GfVec3d pxA=m_dragRotateInitialRotMat.GetRow3(0);
const pxr::GfVec3d pyA=m_dragRotateInitialRotMat.GetRow3(1);
const pxr::GfVec3d pzA=m_dragRotateInitialRotMat.GetRow3(2);
// Decompose `result` (the new total local rotation matrix) back
// into XformCommonAPI Euler angles.
//
// SetRotate() interprets the written angles as rotations about the
// CANONICAL x/y/z axes, so the decompose axes MUST be canonical.
// The previous port used the rows of initRot here -- i.e. the prim's
// already-rotated frame -- which equals the canonical axes only when
// the prim is at zero rotation. That mismatch is exactly why the
// authored result desynced from the manipulator once the prim already
// carried a rotation: the angles were measured in the rotated frame
// but written as if they were canonical-frame angles.
//
// (Decomposition assumes RotationOrderXYZ -- the XformCommonAPI
// default; non-XYZ orders are mapped tw->X, fb->Y, lr->Z as before.)
static const pxr::GfVec3d kCanonX(1,0,0), kCanonY(0,1,0), kCanonZ(0,0,1);
double tw=pxr::GfDegreesToRadians(double(m_dragStartRotate[0]));
double fb=pxr::GfDegreesToRadians(double(m_dragStartRotate[1]));
@@ -657,7 +667,7 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
double sw=0;
pxr::GfRotation::DecomposeRotation(
result,pxA,pyA,pzA,1.0,&tw,&fb,&lr,&sw,/*useHint=*/true);
result,kCanonX,kCanonY,kCanonZ,1.0,&tw,&fb,&lr,&sw,/*useHint=*/true);
pxr::GfVec3f newRot(float(pxr::GfRadiansToDegrees(tw)),
float(pxr::GfRadiansToDegrees(fb)),