From 4d3cc85ecec77f7dc9535e67e90c4fe539e4b66d Mon Sep 17 00:00:00 2001 From: indigo Date: Sun, 21 Jun 2026 18:49:22 +0800 Subject: [PATCH] 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 --- src/ui/TransformManipulator.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/ui/TransformManipulator.cpp b/src/ui/TransformManipulator.cpp index 464f6e2..88276ad 100644 --- a/src/ui/TransformManipulator.cpp +++ b/src/ui/TransformManipulator.cpp @@ -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)),