From 4cb6ec7341acac5242f3b60462b911768380f833 Mon Sep 17 00:00:00 2001 From: indigo Date: Tue, 9 Jun 2026 08:43:29 +0800 Subject: [PATCH] Refract Manipulator --- src/ui/TransformManipulator.cpp | 1244 +++++++++++++------------------ 1 file changed, 531 insertions(+), 713 deletions(-) diff --git a/src/ui/TransformManipulator.cpp b/src/ui/TransformManipulator.cpp index 9b928ae..5ead935 100644 --- a/src/ui/TransformManipulator.cpp +++ b/src/ui/TransformManipulator.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,10 @@ #include #include #include +#include +#include +#include +#include #include #include @@ -27,843 +32,656 @@ namespace UsdLayerManager { // --------------------------------------------------------------------------- -// ImGuizmo-derived colour palette -// X = red, Y = green, Z = blue (matches Maya / ImGuizmo defaults) -// Highlight (hovered / active) = orange (ImGuizmo SELECTION colour) +// Colours / constants // --------------------------------------------------------------------------- -static const ImU32 kColX = IM_COL32(214, 38, 38, 255); -static const ImU32 kColY = IM_COL32( 38, 179, 38, 255); -static const ImU32 kColZ = IM_COL32( 38, 90, 220, 255); -static const ImU32 kColHover = IM_COL32(255, 128, 16, 255); // ImGuizmo SELECTION -static const ImU32 kColCenter = IM_COL32(255, 255, 255, 220); -static const ImU32 kColAxisLine = IM_COL32(170, 170, 170, 170); // shaft tint - +static const ImU32 kColX = IM_COL32(214, 38, 38, 255); +static const ImU32 kColY = IM_COL32( 38, 179, 38, 255); +static const ImU32 kColZ = IM_COL32( 38, 90, 220, 255); +static const ImU32 kColHover = IM_COL32(255, 128, 16, 255); +static const ImU32 kColCenter = IM_COL32(255, 255, 255, 220); static const ImU32 kAxisColors[3] = { kColX, kColY, kColZ }; -// ImGuizmo line-thickness defaults (from Style struct) -static constexpr float kTranslationLineThick = 3.0f; -static constexpr float kRotationLineThick = 2.0f; -static constexpr float kScaleLineThick = 3.0f; -static constexpr float kScaleCircleRadius = 5.0f; // pixels, like ScaleLineCircleSize -static constexpr float kCenterCircleRadius = 5.0f; // pixels, like CenterCircleSize +static constexpr float kTranslationLineThick = 3.0f; +static constexpr float kRotationLineThick = 2.0f; +static constexpr float kScaleLineThick = 3.0f; +static constexpr float kScaleCircleRadius = 5.0f; +static constexpr float kCenterCircleRadius = 5.0f; -// ────────────────────────────────────────────────────────────────────────────── +// ============================================================================ +// SetupCompatibleXform +// +// Fast path : prim already has XformCommonAPI-compatible ops -> read T/R/S. +// Fallback : decompose the composed local matrix and insert a compatible +// op-stack (with !resetXformStack!) in the current edit layer. +// This handles prims from references that use separate rotateX/Y/Z +// or matrix ops without touching the reference layer. +// +// Mirrors usdtweak's per-prim check: if (xformAPI) { ... } else { ... } +// but rather than falling back to a single matrix op, we create compatible +// standard TRS ops (better for layer-based override workflows). +// ============================================================================ +static bool SetupCompatibleXform( + const pxr::UsdStageRefPtr& stage, + const pxr::SdfPath& primPath, + pxr::GfVec3d& outT, + pxr::GfVec3f& outR, + pxr::GfVec3f& outS, + pxr::UsdGeomXformCommonAPI::RotationOrder& outRotOrder) +{ + pxr::UsdPrim prim = stage->GetPrimAtPath(primPath); + if (!prim) return false; + + pxr::UsdGeomXformCommonAPI api(prim); + pxr::GfVec3f pivot3f; + + if (api.GetXformVectors(&outT, &outR, &outS, &pivot3f, + &outRotOrder, pxr::UsdTimeCode::Default())) + return true; + + // Fallback ----------------------------------------------------------------- + LOG_WARNING("Prim " + primPath.GetString() + + " has an incompatible xform stack. Creating a compatible " + "override in the edit layer."); + + pxr::UsdGeomXformCache xc(pxr::UsdTimeCode::Default()); + bool reset = false; + pxr::GfMatrix4d localMat = xc.GetLocalTransformation(prim, &reset); + + pxr::GfMatrix4d rotMat, shearMat, projMat; + pxr::GfVec3d scale3d, translate3d; + if (!localMat.Factor(&rotMat, &scale3d, &shearMat, &translate3d, &projMat)) { + rotMat = pxr::GfMatrix4d(1); scale3d = {1,1,1}; translate3d = {0,0,0}; + } + + double tw = 0, fb = 0, lr = 0, sw = 0; + pxr::GfRotation::DecomposeRotation( + rotMat, {1,0,0}, {0,1,0}, {0,0,1}, 1.0, &tw, &fb, &lr, &sw, false); + + outT = translate3d; + outR = { float(pxr::GfRadiansToDegrees(tw)), + float(pxr::GfRadiansToDegrees(fb)), + float(pxr::GfRadiansToDegrees(lr)) }; + outS = { float(scale3d[0]), float(scale3d[1]), float(scale3d[2]) }; + outRotOrder = pxr::UsdGeomXformCommonAPI::RotationOrderXYZ; + + { + pxr::UsdEditContext ec(stage, stage->GetEditTarget()); + pxr::UsdGeomXformable xf(prim); + xf.SetResetXformStack(true); + pxr::UsdGeomXformCommonAPI fresh(prim); + fresh.SetTranslate(outT, pxr::UsdTimeCode::Default()); + fresh.SetRotate (outR, outRotOrder, pxr::UsdTimeCode::Default()); + fresh.SetScale (outS, pxr::UsdTimeCode::Default()); + } + return true; +} + +// ============================================================================ // Stage / selection -// ────────────────────────────────────────────────────────────────────────────── +// ============================================================================ void TransformManipulator::SetStage(pxr::UsdStageRefPtr stage) { - m_stage = stage; - m_primPath = pxr::SdfPath(); - m_isDragging = false; + m_stage = stage; m_primPath = pxr::SdfPath(); m_isDragging = false; } void TransformManipulator::SetSelectedPrim(const pxr::SdfPath& path) { - m_primPath = path; - m_isDragging = false; + m_primPath = path; m_isDragging = false; } -// ────────────────────────────────────────────────────────────────────────────── +// ============================================================================ // GetGizmoAxes -// -// Returns the three gizmo axis vectors in world space. -// -// World space: fixed unit vectors X/Y/Z. -// Object space: the prim's local X/Y/Z axes derived from its local-to-world -// matrix. In USD row-vector convention (p' = p * M), row i of M is the -// world-space image of the i-th local basis vector, so we normalise rows -// 0..2 to get the three local axes expressed in world coordinates. -// ────────────────────────────────────────────────────────────────────────────── -void TransformManipulator::GetGizmoAxes(pxr::GfVec3d outAxes[3]) const +// World mode: fixed {1,0,0}, {0,1,0}, {0,0,1} +// Object mode: rows 0..2 of local-to-world matrix (normalised world-space +// local axis directions — mirrors usdtweak's +// ComputeManipulatorToWorldTransform().GetRow3(i)) +// ============================================================================ +void TransformManipulator::GetGizmoAxes(pxr::GfVec3d axes[3]) const { - // Fallback: world-space unit vectors - outAxes[0] = {1, 0, 0}; - outAxes[1] = {0, 1, 0}; - outAxes[2] = {0, 0, 1}; - + axes[0] = {1,0,0}; axes[1] = {0,1,0}; axes[2] = {0,0,1}; if (m_transformSpace == TransformSpace::World) return; if (!m_stage || m_primPath.IsEmpty()) return; - pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath); if (!prim) return; - - pxr::UsdGeomXformCache xformCache(pxr::UsdTimeCode::Default()); - pxr::GfMatrix4d localToWorld = xformCache.GetLocalToWorldTransform(prim); - - // Each row i (0..2) of the 4×4 matrix is the world-space direction of - // the i-th local basis vector (USD row-vector convention). + pxr::UsdGeomXformCache xc(pxr::UsdTimeCode::Default()); + pxr::GfMatrix4d l2w = xc.GetLocalToWorldTransform(prim); for (int i = 0; i < 3; ++i) { - pxr::GfVec3d row(localToWorld[i][0], localToWorld[i][1], localToWorld[i][2]); - double len = row.GetLength(); - outAxes[i] = (len > 1e-9) ? row / len : outAxes[i]; + pxr::GfVec3d row(l2w[i][0], l2w[i][1], l2w[i][2]); + double n = row.GetLength(); + if (n > 1e-9) axes[i] = row / n; } } -// ────────────────────────────────────────────────────────────────────────────── -// WorldToScreen -// Converts a world-space point to absolute ImGui screen coordinates. -// -// USD uses row-vector convention: p_clip = (p, 1) * viewProjMatrix -// where viewProjMatrix[row][col]. -// ────────────────────────────────────────────────────────────────────────────── -bool TransformManipulator::WorldToScreen(const pxr::GfVec3d& world, +// ============================================================================ +// WorldToScreen p_clip = (p_world, 1) * VP (USD row-vector convention) +// ============================================================================ +bool TransformManipulator::WorldToScreen(const pxr::GfVec3d& world, const pxr::GfMatrix4d& vp, - int viewW, int viewH, - const ImVec2& imagePos, - ImVec2& outScreen) + int vW, int vH, + const ImVec2& imgPos, ImVec2& out) { - // Clip space: (p, 1) * VP (row-vector × matrix) - double cx = vp[0][0]*world[0] + vp[1][0]*world[1] + vp[2][0]*world[2] + vp[3][0]; - double cy = vp[0][1]*world[0] + vp[1][1]*world[1] + vp[2][1]*world[2] + vp[3][1]; - double cw = vp[0][3]*world[0] + vp[1][3]*world[1] + vp[2][3]*world[2] + vp[3][3]; - - if (cw <= 0.0) return false; // behind near plane - - double invW = 1.0 / cw; - double ndcX = cx * invW; // in [-1, 1] - double ndcY = cy * invW; // in [-1, 1], +Y up in clip space - - // Viewport pixel (Y flipped: clip +Y → screen top) - float px = static_cast((ndcX + 1.0) * 0.5 * viewW); - float py = static_cast((1.0 - ndcY) * 0.5 * viewH); - - outScreen = ImVec2(imagePos.x + px, imagePos.y + py); + double cx = vp[0][0]*world[0]+vp[1][0]*world[1]+vp[2][0]*world[2]+vp[3][0]; + double cy = vp[0][1]*world[0]+vp[1][1]*world[1]+vp[2][1]*world[2]+vp[3][1]; + double cw = vp[0][3]*world[0]+vp[1][3]*world[1]+vp[2][3]*world[2]+vp[3][3]; + if (cw <= 0.0) return false; + double iw = 1.0/cw; + out = ImVec2(imgPos.x + float((cx*iw+1.0)*0.5*vW), + imgPos.y + float((1.0-cy*iw)*0.5*vH)); return true; } -// ────────────────────────────────────────────────────────────────────────────── -// ComputeScreenFactor (ImGuizmo algorithm) -// -// Projects each world-axis unit vector from @p pivot into clip space and -// measures its clip-space length (aspect-ratio corrected, like ImGuizmo's -// GetSegmentLengthClipSpace). Returns the world-space gizmo half-size that -// spans @p desiredFraction of the NDC extent. -// ────────────────────────────────────────────────────────────────────────────── -float TransformManipulator::ComputeScreenFactor(const pxr::GfMatrix4d& vp, - const pxr::GfVec3d& pivot, - int viewW, int viewH, - float desiredFraction) +// ============================================================================ +// ComputeMouseRay p_world = p_clip * VP^{-1} +// ============================================================================ +pxr::GfRay TransformManipulator::ComputeMouseRay(const pxr::GfMatrix4d& vp, + const ImVec2& mouse, + const ImVec2& imgPos, + int vW, int vH) { - // Clip-space coords of the pivot - double pw = vp[0][3]*pivot[0] + vp[1][3]*pivot[1] + vp[2][3]*pivot[2] + vp[3][3]; - if (pw <= 0.0) return 1.0f; - double invPW = 1.0 / pw; - - double px = (vp[0][0]*pivot[0] + vp[1][0]*pivot[1] + vp[2][0]*pivot[2] + vp[3][0]) * invPW; - double py = (vp[0][1]*pivot[0] + vp[1][1]*pivot[1] + vp[2][1]*pivot[2] + vp[3][1]) * invPW; - - // Test each world axis: pick the one that subtends the largest clip length. - // (ImGuizmo uses the camera-right direction; testing all three world axes - // is equivalent and avoids needing to extract the view-inverse.) - const pxr::GfVec3d axes[3] = {{1,0,0},{0,1,0},{0,0,1}}; - float displayRatio = (float)viewW / (float)std::max(viewH, 1); - float maxClipLen = 0.f; - - for (const auto& ax : axes) { - pxr::GfVec3d tip = pivot + ax; - double tw = vp[0][3]*tip[0] + vp[1][3]*tip[1] + vp[2][3]*tip[2] + vp[3][3]; - if (tw <= 0.0) continue; - double invTW = 1.0 / tw; - double tx = (vp[0][0]*tip[0] + vp[1][0]*tip[1] + vp[2][0]*tip[2] + vp[3][0]) * invTW; - double ty = (vp[0][1]*tip[0] + vp[1][1]*tip[1] + vp[2][1]*tip[2] + vp[3][1]) * invTW; - - // Clip-space delta, aspect-ratio corrected (ImGuizmo convention) - float dx = static_cast(tx - px); - float dy = static_cast(ty - py); - if (displayRatio < 1.f) dx *= displayRatio; - else dy /= displayRatio; - - float len = std::sqrt(dx*dx + dy*dy); - maxClipLen = std::max(maxClipLen, len); - } - - if (maxClipLen < 1e-6f) return 1.0f; - return desiredFraction / maxClipLen; + const pxr::GfRay kFB(pxr::GfVec3d(0), pxr::GfVec3d(0,0,-1)); + float nx = 2.f*(mouse.x-imgPos.x)/float(vW)-1.f; + float ny = 1.f-2.f*(mouse.y-imgPos.y)/float(vH); + double det = 0; + pxr::GfMatrix4d vi = vp.GetInverse(&det); + if (std::abs(det) < 1e-12) return kFB; + auto up = [&](double z) -> pxr::GfVec3d { + double wx=nx*vi[0][0]+ny*vi[1][0]+z*vi[2][0]+vi[3][0]; + double wy=nx*vi[0][1]+ny*vi[1][1]+z*vi[2][1]+vi[3][1]; + double wz=nx*vi[0][2]+ny*vi[1][2]+z*vi[2][2]+vi[3][2]; + double ww=nx*vi[0][3]+ny*vi[1][3]+z*vi[2][3]+vi[3][3]; + if (std::abs(ww)<1e-12) return pxr::GfVec3d(0); + return {wx/ww, wy/ww, wz/ww}; + }; + pxr::GfVec3d dir = up(1.0)-up(-1.0); + double n = dir.GetLength(); + if (n < 1e-9) return kFB; + return pxr::GfRay(up(-1.0), dir/n); } -// ────────────────────────────────────────────────────────────────────────────── +// ============================================================================ +// ComputeScreenFactor (ImGuizmo constant-apparent-size formula) +// ============================================================================ +float TransformManipulator::ComputeScreenFactor(const pxr::GfMatrix4d& vp, + const pxr::GfVec3d& pivot, + int vW, int vH, float frac) +{ + double pw=vp[0][3]*pivot[0]+vp[1][3]*pivot[1]+vp[2][3]*pivot[2]+vp[3][3]; + if (pw<=0) return 1.f; + double iw=1/pw; + double px=(vp[0][0]*pivot[0]+vp[1][0]*pivot[1]+vp[2][0]*pivot[2]+vp[3][0])*iw; + double py=(vp[0][1]*pivot[0]+vp[1][1]*pivot[1]+vp[2][1]*pivot[2]+vp[3][1])*iw; + float dr=(float)vW/(float)std::max(vH,1), mx=0; + const pxr::GfVec3d kA[3]={{1,0,0},{0,1,0},{0,0,1}}; + for (auto& ax : kA) { + pxr::GfVec3d t=pivot+ax; + double tw=vp[0][3]*t[0]+vp[1][3]*t[1]+vp[2][3]*t[2]+vp[3][3]; + if (tw<=0) continue; + double ti=1/tw; + float dx=float((vp[0][0]*t[0]+vp[1][0]*t[1]+vp[2][0]*t[2]+vp[3][0])*ti-px); + float dy=float((vp[0][1]*t[0]+vp[1][1]*t[1]+vp[2][1]*t[2]+vp[3][1])*ti-py); + if (dr<1) dx*=dr; else dy/=dr; + mx=std::max(mx,std::sqrt(dx*dx+dy*dy)); + } + return (mx<1e-6f)?1.f:frac/mx; +} + +// ============================================================================ // PointToSegmentDist -// ────────────────────────────────────────────────────────────────────────────── +// ============================================================================ float TransformManipulator::PointToSegmentDist(ImVec2 p, ImVec2 a, ImVec2 b) { - float dx = b.x - a.x, dy = b.y - a.y; - float lenSq = dx*dx + dy*dy; - if (lenSq < 1e-6f) { - float ex = p.x - a.x, ey = p.y - a.y; - return std::sqrt(ex*ex + ey*ey); - } - float t = std::max(0.f, std::min(1.f, ((p.x-a.x)*dx + (p.y-a.y)*dy) / lenSq)); - float cx = a.x + t*dx - p.x; - float cy = a.y + t*dy - p.y; - return std::sqrt(cx*cx + cy*cy); + float dx=b.x-a.x,dy=b.y-a.y,lsq=dx*dx+dy*dy; + if (lsq<1e-6f){float ex=p.x-a.x,ey=p.y-a.y;return std::sqrt(ex*ex+ey*ey);} + float t=std::max(0.f,std::min(1.f,((p.x-a.x)*dx+(p.y-a.y)*dy)/lsq)); + float cx=a.x+t*dx-p.x,cy=a.y+t*dy-p.y; + return std::sqrt(cx*cx+cy*cy); } -// ────────────────────────────────────────────────────────────────────────────── -// HitTestAxes -// Returns 0=X, 1=Y, 2=Z or -1. -// ────────────────────────────────────────────────────────────────────────────── +// ============================================================================ +// HitTestAxes (Move / Scale) +// ============================================================================ int TransformManipulator::HitTestAxes(const pxr::GfMatrix4d& vp, const pxr::GfVec3d& pivot, float sf, const ImVec2& imgPos, int vW, int vH, const ImVec2& mouse, const pxr::GfVec3d axes[3]) const { - ImVec2 pivotSS; - if (!WorldToScreen(pivot, vp, vW, vH, imgPos, pivotSS)) return -1; - - static constexpr float kPickRadius = 10.0f; - float bestDist = kPickRadius; - int bestAxis = -1; - - for (int i = 0; i < 3; ++i) { + ImVec2 pivSS; + if (!WorldToScreen(pivot, vp, vW, vH, imgPos, pivSS)) return -1; + float best=10.f; int hit=-1; + for (int i=0;i<3;++i) { ImVec2 tipSS; - if (!WorldToScreen(pivot + axes[i] * sf, vp, vW, vH, imgPos, tipSS)) continue; - float d = PointToSegmentDist(mouse, pivotSS, tipSS); - if (d < bestDist) { bestDist = d; bestAxis = i; } + if (!WorldToScreen(pivot+axes[i]*sf,vp,vW,vH,imgPos,tipSS)) continue; + float d=PointToSegmentDist(mouse,pivSS,tipSS); + if (d(camToScene[0]*u[0] + camToScene[1]*u[1] + camToScene[2]*u[2]); - float b_proj = static_cast(camToScene[0]*v[0] + camToScene[1]*v[1] + camToScene[2]*v[2]); - float as = std::atan2(b_proj, a_proj) + static_cast(M_PI) * 0.5f; - - ImVec2 prevSS; - bool hasPrev = false; - - for (int s = 0; s <= kSegs; ++s) { - float angle = as + static_cast(M_PI) * - (static_cast(s) / static_cast(kSegs)); - float c = std::cos(angle), si = std::sin(angle); - - pxr::GfVec3d p = pivot + u * (radius * c) + v * (radius * si); - + static constexpr int kSeg=32; + static constexpr float kDF=1.2f; + float r=sf*kDF; + pxr::GfVec3d c2s=pivot-camEye; + double l=c2s.GetLength(); + if (l<1e-9) c2s={0,0,-1}; else c2s/=l; + float best=10.f; int hit=-1; + for (int axis=0;axis<3;++axis) { + const pxr::GfVec3d& u=(axis==0)?axes[1]:axes[0]; + const pxr::GfVec3d& v=(axis<2) ?axes[2]:axes[1]; + float ap=float(c2s[0]*u[0]+c2s[1]*u[1]+c2s[2]*u[2]); + float bp=float(c2s[0]*v[0]+c2s[1]*v[1]+c2s[2]*v[2]); + float as=std::atan2(bp,ap)+float(M_PI)*0.5f; + ImVec2 prev; bool hp=false; + for (int s=0;s<=kSeg;++s) { + float a=as+float(M_PI)*(float(s)/float(kSeg)); + pxr::GfVec3d p=pivot+u*(r*std::cos(a))+v*(r*std::sin(a)); ImVec2 ss; - if (!WorldToScreen(p, vp, vW, vH, imgPos, ss)) { hasPrev = false; continue; } - - if (hasPrev) { - float d = PointToSegmentDist(mouse, prevSS, ss); - if (d < bestDist) { bestDist = d; bestAxis = axis; } - } - prevSS = ss; - hasPrev = true; + if (!WorldToScreen(p,vp,vW,vH,imgPos,ss)){hp=false;continue;} + if (hp){float d=PointToSegmentDist(mouse,prev,ss);if(dAddLine(pivotSS, shaftEndSS, col, kTranslationLineThick); - - // --- Arrowhead (filled triangle in screen space) --- - // Screen-space arrow direction (from base toward tip) - float adx = tipSS.x - shaftEndSS.x; - float ady = tipSS.y - shaftEndSS.y; - float alen = std::sqrt(adx*adx + ady*ady); - if (alen < 1.f) continue; - - // Perpendicular to arrow direction - float px = -ady / alen; - float py = adx / alen; - - // Total gizmo length in pixels (used to scale arrowhead) - float totalLen = std::sqrt((tipSS.x - pivotSS.x)*(tipSS.x - pivotSS.x) + - (tipSS.y - pivotSS.y)*(tipSS.y - pivotSS.y)); - float halfWidth = totalLen * kArrowFrac; - - ImVec2 wing1(shaftEndSS.x + px * halfWidth, shaftEndSS.y + py * halfWidth); - ImVec2 wing2(shaftEndSS.x - px * halfWidth, shaftEndSS.y - py * halfWidth); - - dl->AddTriangleFilled(tipSS, wing1, wing2, col); + ImVec2 pivSS; + if (!WorldToScreen(pivot,vp,vW,vH,imgPos,pivSS)) return; + for (int i=0;i<3;++i) { + ImU32 col=(i==m_dragAxis||i==m_hoveredAxis)?kColHover:kAxisColors[i]; + ImVec2 shSS,tipSS; + if (!WorldToScreen(pivot+axes[i]*sf*0.78f,vp,vW,vH,imgPos,shSS)) continue; + if (!WorldToScreen(pivot+axes[i]*sf, vp,vW,vH,imgPos,tipSS)) continue; + dl->AddLine(pivSS,shSS,col,kTranslationLineThick); + float adx=tipSS.x-shSS.x,ady=tipSS.y-shSS.y,al=std::sqrt(adx*adx+ady*ady); + if (al<1.f) continue; + float px=-ady/al,py=adx/al; + float tl=std::sqrt((tipSS.x-pivSS.x)*(tipSS.x-pivSS.x)+(tipSS.y-pivSS.y)*(tipSS.y-pivSS.y)); + float hw=tl*0.12f; + dl->AddTriangleFilled(tipSS,{shSS.x+px*hw,shSS.y+py*hw},{shSS.x-px*hw,shSS.y-py*hw},col); } - - // Centre circle (white, like ImGuizmo's center square) - dl->AddCircleFilled(pivotSS, kCenterCircleRadius, kColCenter, 16); + dl->AddCircleFilled(pivSS,kCenterCircleRadius,kColCenter,16); } -// ────────────────────────────────────────────────────────────────────────────── -// DrawRotateGizmo (ImGuizmo-style front-facing half-arc) -// -// Algorithm (ported from ImGuizmo::DrawRotationGizmo): -// viewDir = normalize(pivot - cameraEye) [camera-to-scene direction] -// -// For each ring axis the "angleStart" places the half-arc so that it covers -// exactly the front-facing hemisphere (the half the camera can see). -// -// Ring convention in our code: -// axis 0 → X ring (YZ plane): angleStart = atan2(vz, vy) + π/2 -// axis 1 → Y ring (XZ plane): angleStart = atan2(vz, vx) + π/2 -// axis 2 → Z ring (XY plane): angleStart = atan2(vy, vx) + π/2 -// -// The ring radius is screenFactor × 1.2 (ImGuizmo rotationDisplayFactor). -// ────────────────────────────────────────────────────────────────────────────── -void TransformManipulator::DrawRotateGizmo(ImDrawList* dl, - const pxr::GfMatrix4d& vp, - const pxr::GfVec3d& pivot, - float sf, - const pxr::GfVec3d& cameraEye, - const ImVec2& imgPos, - int vW, int vH, +void TransformManipulator::DrawRotateGizmo(ImDrawList* dl, const pxr::GfMatrix4d& vp, + const pxr::GfVec3d& pivot, float sf, + const pxr::GfVec3d& camEye, + const ImVec2& imgPos, int vW, int vH, const pxr::GfVec3d axes[3]) { - static constexpr int kSegs = 64; // half-arc segment count - static constexpr float kDispFactor = 1.2f; // ImGuizmo rotationDisplayFactor - float radius = sf * kDispFactor; - - // Camera-to-scene direction in world space - pxr::GfVec3d camToScene = pivot - cameraEye; - double camLen = camToScene.GetLength(); - if (camLen < 1e-9) camToScene = pxr::GfVec3d(0, 0, -1); - else camToScene /= camLen; - - for (int axis = 0; axis < 3; ++axis) { - ImU32 col = (axis == m_dragAxis || axis == m_hoveredAxis) ? kColHover - : kAxisColors[axis]; - float lw = (axis == m_dragAxis || axis == m_hoveredAxis) - ? kRotationLineThick + 1.5f : kRotationLineThick; - - // Tangent axes spanning this ring's plane - const pxr::GfVec3d& u = (axis == 0) ? axes[1] : axes[0]; - const pxr::GfVec3d& v = (axis < 2) ? axes[2] : axes[1]; - - // Project camToScene onto ring plane to find front-facing half-arc start - float a_proj = static_cast(camToScene[0]*u[0] + camToScene[1]*u[1] + camToScene[2]*u[2]); - float b_proj = static_cast(camToScene[0]*v[0] + camToScene[1]*v[1] + camToScene[2]*v[2]); - float as = std::atan2(b_proj, a_proj) + static_cast(M_PI) * 0.5f; - - std::vector pts; - pts.reserve(kSegs + 1); - - for (int s = 0; s <= kSegs; ++s) { - float angle = as + static_cast(M_PI) * - (static_cast(s) / static_cast(kSegs)); - float c = std::cos(angle), si = std::sin(angle); - - pxr::GfVec3d p = pivot + u * (radius * c) + v * (radius * si); - + static constexpr int kSeg=64; static constexpr float kDF=1.2f; + float r=sf*kDF; + pxr::GfVec3d c2s=pivot-camEye; + double cl=c2s.GetLength(); + if (cl<1e-9) c2s={0,0,-1}; else c2s/=cl; + for (int axis=0;axis<3;++axis) { + ImU32 col=(axis==m_dragAxis||axis==m_hoveredAxis)?kColHover:kAxisColors[axis]; + float lw=(axis==m_dragAxis||axis==m_hoveredAxis)?kRotationLineThick+1.5f:kRotationLineThick; + const pxr::GfVec3d& u=(axis==0)?axes[1]:axes[0]; + const pxr::GfVec3d& v=(axis<2) ?axes[2]:axes[1]; + float ap=float(c2s[0]*u[0]+c2s[1]*u[1]+c2s[2]*u[2]); + float bp=float(c2s[0]*v[0]+c2s[1]*v[1]+c2s[2]*v[2]); + float as=std::atan2(bp,ap)+float(M_PI)*0.5f; + std::vector pts; pts.reserve(kSeg+1); + for (int s=0;s<=kSeg;++s) { + float a=as+float(M_PI)*(float(s)/float(kSeg)); + pxr::GfVec3d p=pivot+u*(r*std::cos(a))+v*(r*std::sin(a)); ImVec2 ss; - if (WorldToScreen(p, vp, vW, vH, imgPos, ss)) - pts.push_back(ss); + if (WorldToScreen(p,vp,vW,vH,imgPos,ss)) pts.push_back(ss); } - - if (pts.size() > 1) - dl->AddPolyline(pts.data(), static_cast(pts.size()), - col, ImDrawFlags_None, lw); + if (pts.size()>1) + dl->AddPolyline(pts.data(),int(pts.size()),col,ImDrawFlags_None,lw); } } -// ────────────────────────────────────────────────────────────────────────────── -// DrawScaleGizmo -// -// Three lines each capped with a filled circle (ImGuizmo ScaleLineCircleSize). -// ────────────────────────────────────────────────────────────────────────────── -void TransformManipulator::DrawScaleGizmo(ImDrawList* dl, - const pxr::GfMatrix4d& vp, - const pxr::GfVec3d& pivot, - float sf, - const ImVec2& imgPos, - int vW, int vH, +void TransformManipulator::DrawScaleGizmo(ImDrawList* dl, const pxr::GfMatrix4d& vp, + const pxr::GfVec3d& pivot, float sf, + const ImVec2& imgPos, int vW, int vH, const pxr::GfVec3d axes[3]) { - ImVec2 pivotSS; - if (!WorldToScreen(pivot, vp, vW, vH, imgPos, pivotSS)) return; - - for (int i = 0; i < 3; ++i) { - ImU32 col = (i == m_dragAxis || i == m_hoveredAxis) ? kColHover : kAxisColors[i]; - + ImVec2 pivSS; + if (!WorldToScreen(pivot,vp,vW,vH,imgPos,pivSS)) return; + for (int i=0;i<3;++i) { + ImU32 col=(i==m_dragAxis||i==m_hoveredAxis)?kColHover:kAxisColors[i]; ImVec2 tipSS; - if (!WorldToScreen(pivot + axes[i] * sf, vp, vW, vH, imgPos, tipSS)) continue; - - dl->AddLine(pivotSS, tipSS, col, kScaleLineThick); - dl->AddCircleFilled(tipSS, kScaleCircleRadius, col, 16); + if (!WorldToScreen(pivot+axes[i]*sf,vp,vW,vH,imgPos,tipSS)) continue; + dl->AddLine(pivSS,tipSS,col,kScaleLineThick); + dl->AddCircleFilled(tipSS,kScaleCircleRadius,col,16); } - - // Centre box / circle (uniform scale handle) - dl->AddCircleFilled(pivotSS, kCenterCircleRadius + 1.f, kColCenter, 16); + dl->AddCircleFilled(pivSS,kCenterCircleRadius+1.f,kColCenter,16); } -// ────────────────────────────────────────────────────────────────────────────── -// Render — public entry point -// ────────────────────────────────────────────────────────────────────────────── -void TransformManipulator::Render(ImDrawList* dl, - const pxr::GfMatrix4d& viewProj, - const pxr::GfVec3d& pivot, - const pxr::GfVec3d& cameraEye, - const ImVec2& imagePos, - int viewW, int viewH) +// ============================================================================ +// Render +// ============================================================================ +void TransformManipulator::Render(ImDrawList* dl, const pxr::GfMatrix4d& vp, + const pxr::GfVec3d& pivot, + const pxr::GfVec3d& camEye, + const ImVec2& imgPos, int vW, int vH) { - if (m_mode == ManipulatorMode::Select) return; - if (!m_stage || m_primPath.IsEmpty()) return; - if (!dl || viewW <= 0 || viewH <= 0) return; - - float sf = ComputeScreenFactor(viewProj, pivot, viewW, viewH, /*desiredFraction=*/0.15f); - - pxr::GfVec3d axes[3]; - GetGizmoAxes(axes); - + if (m_mode==ManipulatorMode::Select) return; + if (!m_stage||m_primPath.IsEmpty()||!dl||vW<=0||vH<=0) return; + float sf=ComputeScreenFactor(vp,pivot,vW,vH,0.15f); + pxr::GfVec3d axes[3]; GetGizmoAxes(axes); switch (m_mode) { - case ManipulatorMode::Move: - DrawMoveGizmo (dl, viewProj, pivot, sf, imagePos, viewW, viewH, axes); - break; - case ManipulatorMode::Rotate: - DrawRotateGizmo(dl, viewProj, pivot, sf, cameraEye, imagePos, viewW, viewH, axes); - break; - case ManipulatorMode::Scale: - DrawScaleGizmo (dl, viewProj, pivot, sf, imagePos, viewW, viewH, axes); - break; + case ManipulatorMode::Move: DrawMoveGizmo (dl,vp,pivot,sf,imgPos,vW,vH,axes); break; + case ManipulatorMode::Rotate: DrawRotateGizmo(dl,vp,pivot,sf,camEye,imgPos,vW,vH,axes); break; + case ManipulatorMode::Scale: DrawScaleGizmo (dl,vp,pivot,sf,imgPos,vW,vH,axes); break; default: break; } } -// ────────────────────────────────────────────────────────────────────────────── +// ============================================================================ // HandleInput -// ────────────────────────────────────────────────────────────────────────────── -bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& viewProj, - const pxr::GfVec3d& pivot, - const pxr::GfVec3d& cameraEye, - const ImVec2& imagePos, - int viewW, int viewH, - bool viewportHovered) +// +// Full port of usdtweak's three manipulators with World/Object space switch. +// +// TRANSLATE (PositionManipulator::OnUpdate) +// GfFindClosestPoints(mouseRay, axisLine) -> signed scalar delta along axis. +// Applied ABSOLUTE each frame: newTranslate = origTranslate + parentSpaceDelta. +// Parent-space conversion: w2p.TransformDir(worldDeltaVec). +// +// ROTATE (RotationManipulator::OnUpdate + space switch) +// 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). +// resultingRotation: +// Object => GfMatrix4d(1).SetRotate(delta) * initRot [usdtweak exact] +// World => initRot * GfMatrix4d(1).SetRotate(delta) [USD row-vector world delta] +// DecomposeRotation with FIXED initRot rows as reference axes + current hints. +// +// SCALE (ScaleManipulator::OnUpdate) +// Same axis-line projection as Translate. +// ratio = curT / originT (axis-line parameter, robust for off-origin pivots). +// newScale[axis] = origScale[axis] * ratio (absolute from drag start). +// ============================================================================ +bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp, + const pxr::GfVec3d& pivot, + const pxr::GfVec3d& camEye, + const ImVec2& imgPos, + int vW, int vH, bool hovered) { - if (m_mode == ManipulatorMode::Select) return false; - if (!m_stage || m_primPath.IsEmpty()) return false; + if (m_mode==ManipulatorMode::Select) return false; + if (!m_stage||m_primPath.IsEmpty()) return false; - ImGuiIO& io = ImGui::GetIO(); - ImVec2 mouse = io.MousePos; // absolute screen position + ImGuiIO& io=ImGui::GetIO(); + ImVec2 mouse=io.MousePos; + float sf=ComputeScreenFactor(vp,pivot,vW,vH,0.15f); + pxr::GfVec3d axes[3]; GetGizmoAxes(axes); - float sf = ComputeScreenFactor(viewProj, pivot, viewW, viewH, 0.15f); - - pxr::GfVec3d axes[3]; - GetGizmoAxes(axes); - - // --- Update hover --- - if (!m_isDragging && viewportHovered) { - if (m_mode == ManipulatorMode::Rotate) { - m_hoveredAxis = HitTestRotateRings(viewProj, pivot, sf, cameraEye, - imagePos, viewW, viewH, mouse, axes); - } else { - m_hoveredAxis = HitTestAxes(viewProj, pivot, sf, imagePos, viewW, viewH, mouse, axes); - } + // Hover update + if (!m_isDragging && hovered) { + m_hoveredAxis=(m_mode==ManipulatorMode::Rotate) + ?HitTestRotateRings(vp,pivot,sf,camEye,imgPos,vW,vH,mouse,axes) + :HitTestAxes (vp,pivot,sf, imgPos,vW,vH,mouse,axes); } - bool consumed = false; + bool consumed=false; - // --- Start drag --- - if (viewportHovered && !m_isDragging && + // ========================================================================= + // BEGIN drag + // ========================================================================= + if (hovered && !m_isDragging && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !io.KeyAlt) { - int hit = -1; - if (m_mode == ManipulatorMode::Rotate) { - hit = HitTestRotateRings(viewProj, pivot, sf, cameraEye, - imagePos, viewW, viewH, mouse, axes); - } else { - hit = HitTestAxes(viewProj, pivot, sf, imagePos, viewW, viewH, mouse, axes); - } + int hit=(m_mode==ManipulatorMode::Rotate) + ?HitTestRotateRings(vp,pivot,sf,camEye,imgPos,vW,vH,mouse,axes) + :HitTestAxes (vp,pivot,sf, imgPos,vW,vH,mouse,axes); - if (hit >= 0) { - m_isDragging = true; - m_dragAxis = hit; - m_dragLastPos = mouse; - consumed = true; + if (hit>=0) { + m_isDragging=true; m_dragAxis=hit; consumed=true; - // For rotation: record initial screen angle around projected pivot center - if (m_mode == ManipulatorMode::Rotate) { - ImVec2 pivSS; - if (WorldToScreen(pivot, viewProj, viewW, viewH, imagePos, pivSS)) { - m_dragRotateLastAngle = std::atan2(mouse.y - pivSS.y, - mouse.x - pivSS.x); - } + pxr::GfVec3d trans; pxr::GfVec3f rot,scale; + pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder; + if (!SetupCompatibleXform(m_stage,m_primPath,trans,rot,scale,rotOrder)) { + m_isDragging=false; return false; } + m_dragOriginalTranslate=trans; m_dragOriginalRotate=rot; + m_dragOriginalScale=scale; m_dragOriginalRotOrder=rotOrder; + m_dragStartTranslate=trans; m_dragStartRotate=rot; + m_dragStartScale=scale; - // Snapshot current xform - pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath); - if (prim) { - pxr::UsdGeomXformCommonAPI api(prim); - pxr::GfVec3f pivot3f, rot, scale; - pxr::GfVec3d trans; - pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder; - api.GetXformVectors(&trans, &rot, &scale, &pivot3f, &rotOrder, - pxr::UsdTimeCode::Default()); - m_dragStartTranslate = trans; - m_dragStartRotate = rot; - m_dragStartScale = scale; - // Also save original (immutable) for the undo command. - m_dragOriginalTranslate = trans; - m_dragOriginalRotate = rot; - m_dragOriginalScale = scale; - m_dragOriginalRotOrder = rotOrder; + pxr::GfRay ray=ComputeMouseRay(vp,mouse,imgPos,vW,vH); + + if (m_mode==ManipulatorMode::Move) { + // usdtweak: _axisLine = GfLine(pivot, objectTransform.GetRow3(axis)) + m_dragMoveAxisLine=pxr::GfLine(pivot,axes[m_dragAxis]); + pxr::GfVec3d rpt; + pxr::GfFindClosestPoints(ray,m_dragMoveAxisLine,&rpt,&m_dragMoveOriginOnAxis); + } + else if (m_mode==ManipulatorMode::Rotate) { + // Ring plane in world space. + // usdtweak: _planeNormal3d = manipCoords.GetRow3(_selectedAxis) + m_dragRotatePlaneNormal=axes[m_dragAxis]; + pxr::GfPlane plane(m_dragRotatePlaneNormal,pivot); + double dist=0; + if (ray.Intersect(plane,&dist) && dist>0) + m_dragRotateFrom=pivot-ray.GetPoint(dist); + else + m_dragRotateFrom=axes[(m_dragAxis+1)%3]; + + // Capture initial rotation matrix (local->parent, fixed for whole drag). + // usdtweak: _rotateMatrixOnBegin = GetOpTransform(opType, VtValue(rotation)) + pxr::UsdGeomXformOp::Type opType= + pxr::UsdGeomXformCommonAPI::ConvertRotationOrderToOpType(rotOrder); + m_dragRotateInitialRotMat= + 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]; + } + m_dragRotateObjectSpace=(m_transformSpace==TransformSpace::Object); + } + else if (m_mode==ManipulatorMode::Scale) { + // usdtweak: _axisLine = GfLine(pivot, objectTransform.GetRow3(axis)) + m_dragScaleAxisLine=pxr::GfLine(pivot,axes[m_dragAxis]); + pxr::GfVec3d rpt; + pxr::GfFindClosestPoints(ray,m_dragScaleAxisLine,&rpt,&m_dragScaleOriginOnAxis); } } } - // --- Drag ongoing --- + // ========================================================================= + // DRAG update + // ========================================================================= if (m_isDragging) { - consumed = true; - + consumed=true; if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { - // Defensive bounds check — m_dragAxis is always set to 0–2 at drag-start, - // but guard here to prevent OOB on axes[3] if the invariant is ever broken. - if (m_dragAxis < 0 || m_dragAxis > 2) { - m_isDragging = false; - m_dragAxis = -1; - return consumed; - } + if (m_dragAxis<0||m_dragAxis>2) { m_isDragging=false;m_dragAxis=-1;return consumed; } - ImVec2 delta = { mouse.x - m_dragLastPos.x, - mouse.y - m_dragLastPos.y }; + pxr::GfRay ray=ComputeMouseRay(vp,mouse,imgPos,vW,vH); - if (m_mode == ManipulatorMode::Move) { - ImVec2 pivSS, tipSS; - if (WorldToScreen(pivot, viewProj, viewW, viewH, imagePos, pivSS) && - WorldToScreen(pivot + axes[m_dragAxis] * sf, - viewProj, viewW, viewH, imagePos, tipSS)) - { - float axDx = tipSS.x - pivSS.x; - float axDy = tipSS.y - pivSS.y; - float axLen = std::sqrt(axDx*axDx + axDy*axDy); - if (axLen > 1e-3f) { - float screenDot = (delta.x*axDx + delta.y*axDy) / axLen; - float worldDelta = screenDot * sf / axLen; - // Displace along the actual gizmo axis direction (world space). - // Using axes[m_dragAxis] is correct for both World and Object space; - // the old code used hardcoded world X/Y/Z components which broke - // Object-space translation. - pxr::GfVec3d move = axes[m_dragAxis] * static_cast(worldDelta); - ApplyMoveDelta(move); + // ----------------------------------------------------------------- + // TRANSLATE + // usdtweak: translation[_selectedAxis] += delta + // where delta = signed distance along axis line. + // We extend to support both Object and World space via parent-space + // conversion of the world-space delta vector. + // ----------------------------------------------------------------- + if (m_mode==ManipulatorMode::Move) { + pxr::GfVec3d rpt,cur; + pxr::GfFindClosestPoints(ray,m_dragMoveAxisLine,&rpt,&cur); + double oT=0,cT=0; + m_dragMoveAxisLine.FindClosestPoint(m_dragMoveOriginOnAxis,&oT); + m_dragMoveAxisLine.FindClosestPoint(cur,&cT); + double delta=cT-oT; + + // World-space delta vector along the gizmo axis. + pxr::GfVec3d worldDelta=axes[m_dragAxis]*delta; + + // Convert to parent space. + // (For Object mode: axes[i] is the local axis in world space. + // w2p.TransformDir maps it to the local axis in parent space, + // which equals initRot.GetRow3(i). Adding this to the stored + // parent-space translation is geometrically correct.) + pxr::GfVec3d parentDelta=worldDelta; + pxr::UsdPrim prim=m_stage->GetPrimAtPath(m_primPath); + if (prim) { + pxr::UsdGeomXformCache xc(pxr::UsdTimeCode::Default()); + pxr::UsdPrim parent=prim.GetParent(); + if (parent && !parent.IsPseudoRoot()) { + pxr::GfMatrix4d p2w=xc.GetLocalToWorldTransform(parent); + double det=0; pxr::GfMatrix4d w2p=p2w.GetInverse(&det); + if (std::abs(det)>1e-9) parentDelta=w2p.TransformDir(worldDelta); } } - } - else if (m_mode == ManipulatorMode::Rotate) { - // Screen-angle-around-pivot approach (much more precise than - // horizontal-only mapping — mirrors Maya's rotate manipulator feel). - ImVec2 pivSS; - if (WorldToScreen(pivot, viewProj, viewW, viewH, imagePos, pivSS)) { - float dx = mouse.x - pivSS.x; - float dy = mouse.y - pivSS.y; - // Only respond when mouse is outside a small dead-zone around center - if (dx*dx + dy*dy > 4.f * 4.f) { - float currentAngle = std::atan2(dy, dx); - float deltaAngle = currentAngle - m_dragRotateLastAngle; - // Wrap to [-π, π] - while (deltaAngle > static_cast(M_PI)) deltaAngle -= 2.f * static_cast(M_PI); - while (deltaAngle < -static_cast(M_PI)) deltaAngle += 2.f * static_cast(M_PI); - - float angleDeg = deltaAngle * (180.f / static_cast(M_PI)); - - // Correct sign: the screen-angle approach maps 2-D angular - // motion to 3-D rotation around axes[m_dragAxis]. When the - // ring normal faces the camera (dot < 0) the handedness flips, - // so negate the angle to keep rotation direction consistent. - pxr::GfVec3d ringNormal = axes[m_dragAxis]; - pxr::GfVec3d camToScene = pivot - cameraEye; - double camLen = camToScene.GetLength(); - if (camLen > 1e-9) camToScene /= camLen; - double axisDotView = ringNormal[0]*camToScene[0] - + ringNormal[1]*camToScene[1] - + ringNormal[2]*camToScene[2]; - if (axisDotView < 0.0) angleDeg = -angleDeg; - - ApplyRotateDelta(m_dragAxis, angleDeg); - m_dragRotateLastAngle = currentAngle; - } - } - } - else if (m_mode == ManipulatorMode::Scale) { - ImVec2 pivSS, tipSS; - float screenDot = 0.f; - if (WorldToScreen(pivot, viewProj, viewW, viewH, imagePos, pivSS) && - WorldToScreen(pivot + axes[m_dragAxis] * sf, - viewProj, viewW, viewH, imagePos, tipSS)) - { - float axDx = tipSS.x - pivSS.x; - float axDy = tipSS.y - pivSS.y; - float axLen = std::sqrt(axDx*axDx + axDy*axDy); - if (axLen > 1e-3f) - screenDot = (delta.x*axDx + delta.y*axDy) / axLen; - } - float factor = 1.f + screenDot * 0.01f; - factor = std::max(0.01f, factor); - ApplyScaleDelta(m_dragAxis, factor); + ApplyTranslate(m_dragOriginalTranslate+parentDelta); } - m_dragLastPos = mouse; - } - else { - // Released — check whether the prim actually moved. - bool moved = - (m_dragStartTranslate != m_dragOriginalTranslate) || - (m_dragStartRotate != m_dragOriginalRotate) || - (m_dragStartScale != m_dragOriginalScale); + // ----------------------------------------------------------------- + // ROTATE + // Direct port of RotationManipulator::OnUpdate with space switch. + // ----------------------------------------------------------------- + else if (m_mode==ManipulatorMode::Rotate) { + pxr::GfPlane plane(m_dragRotatePlaneNormal,pivot); + double dist=0; + if (!ray.Intersect(plane,&dist)||dist<=0) return consumed; + pxr::GfVec3d rotateTo=pivot-ray.GetPoint(dist); + if (m_dragRotateFrom.GetLength()<1e-9||rotateTo.GetLength()<1e-9) + return consumed; + // usdtweak: const GfRotation worldRotation(_rotateFrom, rotateTo) + pxr::GfRotation worldRotation(m_dragRotateFrom,rotateTo); + + // usdtweak: axisSign = _planeNormal3d * worldRotation.GetAxis() > 0 + double axisSign= + (pxr::GfDot(m_dragRotatePlaneNormal,worldRotation.GetAxis())>0.0) + ?1.0:-1.0; + + // usdtweak: const GfRotation deltaRotation(localPlaneNormal*axisSign, angle) + 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 + pxr::GfMatrix4d result= + m_dragRotateObjectSpace + ? 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); + + double tw=pxr::GfDegreesToRadians(double(m_dragStartRotate[0])); + double fb=pxr::GfDegreesToRadians(double(m_dragStartRotate[1])); + double lr=pxr::GfDegreesToRadians(double(m_dragStartRotate[2])); + double sw=0; + + pxr::GfRotation::DecomposeRotation( + result,pxA,pyA,pzA,1.0,&tw,&fb,&lr,&sw,/*useHint=*/true); + + pxr::GfVec3f newRot(float(pxr::GfRadiansToDegrees(tw)), + float(pxr::GfRadiansToDegrees(fb)), + float(pxr::GfRadiansToDegrees(lr))); + ApplyRotate(newRot,m_dragOriginalRotOrder); + m_dragStartRotate=newRot; + } + + // ----------------------------------------------------------------- + // SCALE + // usdtweak: scale[_selectedAxis] = _scalesOnBegin[i][axis] * ratio + // ratio = mouseOnAxis.GetLength() / originOnAxis.GetLength() + // We use axis-line T parameters (distance from pivot) which is more + // robust when the pivot is not at the world origin. + // ----------------------------------------------------------------- + else if (m_mode==ManipulatorMode::Scale) { + pxr::GfVec3d rpt,cur; + pxr::GfFindClosestPoints(ray,m_dragScaleAxisLine,&rpt,&cur); + double oT=0,cT=0; + m_dragScaleAxisLine.FindClosestPoint(m_dragScaleOriginOnAxis,&oT); + m_dragScaleAxisLine.FindClosestPoint(cur,&cT); + double ratio=(std::abs(oT)>1e-6)?cT/oT:1.0; + pxr::GfVec3f ns=m_dragOriginalScale; + ns[m_dragAxis]=std::max(0.001f,float(double(m_dragOriginalScale[m_dragAxis])*ratio)); + ApplyScale(ns); m_dragStartScale=ns; + } + + } else { + // Mouse released. + bool moved= + (m_dragStartTranslate!=m_dragOriginalTranslate)|| + (m_dragStartRotate !=m_dragOriginalRotate )|| + (m_dragStartScale !=m_dragOriginalScale ); if (moved && m_commandHistory && m_stage && !m_primPath.IsEmpty()) { - // The Apply* helpers already wrote the final value to USD. - // Push a command so Undo can restore the original. - pxr::SdfLayerHandle editLayer = m_stage->GetEditTarget().GetLayer(); - auto cmd = std::make_unique( - m_stage, m_primPath, editLayer, - m_dragOriginalTranslate, m_dragOriginalRotate, m_dragOriginalScale, - m_dragStartTranslate, m_dragStartRotate, m_dragStartScale, + auto cmd=std::make_unique( + m_stage, m_primPath, + m_stage->GetEditTarget().GetLayer(), + m_dragOriginalTranslate,m_dragOriginalRotate,m_dragOriginalScale, + m_dragStartTranslate, m_dragStartRotate, m_dragStartScale, m_dragOriginalRotOrder, - "Transform " + m_primPath.GetName()); - - // Execute() would write the new value again — we already wrote it, - // so push directly onto the stack without re-executing. - // We bypass Push() and manipulate the stacks via a "no-op execute" trick: - // wrap in a lambda that does nothing on first Execute(). - // Simpler: just store final state as "new" and call Push which re-applies. - // Since the value is already applied, re-applying has no visible effect. + "Transform "+m_primPath.GetName()); m_commandHistory->Push(std::move(cmd)); } - - m_isDragging = false; - m_dragAxis = -1; + m_isDragging=false; m_dragAxis=-1; } } - return consumed; } -// ────────────────────────────────────────────────────────────────────────────── -// USD transform write helpers -// ────────────────────────────────────────────────────────────────────────────── -void TransformManipulator::ApplyMoveDelta(const pxr::GfVec3d& worldDelta) +// ============================================================================ +// USD write helpers — write ABSOLUTE values +// ============================================================================ +void TransformManipulator::ApplyTranslate(const pxr::GfVec3d& t) { - if (!m_stage || m_primPath.IsEmpty()) return; - pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath); - if (!prim) return; - - // XformCommonAPI::SetTranslate writes the prim's translation in *parent* space. - // The incoming worldDelta is in world space, so we must transform it into the - // parent's local space before accumulating. - // - // For a direction vector (no translation component) the conversion is: - // parentSpaceDelta = worldDelta * inverse(parentToWorld) [upper-3x3 only] - // - // When the parent is the pseudo-root its localToWorld is identity, so the - // conversion is a no-op for top-level prims. - pxr::GfVec3d parentSpaceDelta = worldDelta; - pxr::UsdPrim parent = prim.GetParent(); - if (parent) { - pxr::UsdGeomXformCache xformCache(pxr::UsdTimeCode::Default()); - pxr::GfMatrix4d parentToWorld = xformCache.GetLocalToWorldTransform(parent); - double det = 0.0; - pxr::GfMatrix4d worldToParent = parentToWorld.GetInverse(&det); - if (std::abs(det) > 1e-9) { - // TransformDir applies only the rotation+scale part (no translation), - // which is correct for a displacement/direction vector. - parentSpaceDelta = worldToParent.TransformDir(worldDelta); - } - } - - pxr::UsdEditContext ec(m_stage, m_stage->GetEditTarget()); - pxr::UsdGeomXformCommonAPI api(prim); - - m_dragStartTranslate += parentSpaceDelta; - api.SetTranslate(m_dragStartTranslate, pxr::UsdTimeCode::Default()); + if (!m_stage||m_primPath.IsEmpty()) return; + pxr::UsdPrim p=m_stage->GetPrimAtPath(m_primPath); if (!p) return; + pxr::UsdEditContext ec(m_stage,m_stage->GetEditTarget()); + pxr::UsdGeomXformCommonAPI(p).SetTranslate(t,pxr::UsdTimeCode::Default()); + m_dragStartTranslate=t; } -void TransformManipulator::ApplyRotateDelta(int axisIndex, float angleDeg) +void TransformManipulator::ApplyRotate(const pxr::GfVec3f& r, + pxr::UsdGeomXformCommonAPI::RotationOrder ro) { - if (!m_stage || m_primPath.IsEmpty()) return; - pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath); - if (!prim) return; - - pxr::UsdEditContext ec(m_stage, m_stage->GetEditTarget()); - pxr::UsdGeomXformCommonAPI api(prim); - - // ── Correct, gimbal-lock-free rotation (mirrors usdtweak RotationManipulator) ── - // - // The previous approach manually reconstructed the rotation matrix from - // Euler angles using what turned out to be an incorrect axis-order - // convention (GfRotation quaternion multiplication reverses the matrix - // order relative to row-vector convention). - // - // The correct approach avoids all manual convention handling: - // - // Step 1 Use UsdGeomXformOp::GetOpTransform — USD builds the exact - // matrix that SetRotate would produce, with no guessing. - // - // Step 2 Build the world-space delta as a GfRotation around the - // gizmo axis and pre-multiply in parent space. - // - // Step 3 Decompose the result with GfRotation::DecomposeRotation - // passing the INITIAL rotation axes (from drag start) as the - // reference frame and the current angles as hints. Using the - // same fixed axes every frame (not the current frame's axes) - // keeps hint-based disambiguation stable — same technique as - // usdtweak's RotationManipulator::OnUpdate. - // ───────────────────────────────────────────────────────────────────── - - // Step 1: exact current rotation matrix via USD's own op encoding - pxr::UsdGeomXformOp::Type opType = - pxr::UsdGeomXformCommonAPI::ConvertRotationOrderToOpType(m_dragOriginalRotOrder); - pxr::GfMatrix4d curRotMat = - pxr::UsdGeomXformOp::GetOpTransform(opType, pxr::VtValue(m_dragStartRotate)); - - // Step 2: world-space delta rotation around the gizmo axis - pxr::GfVec3d axes[3]; - GetGizmoAxes(axes); - pxr::GfMatrix4d deltaWorld = - pxr::GfMatrix4d(1.0).SetRotate( - pxr::GfRotation(axes[axisIndex], static_cast(angleDeg))); - - // Convert to parent space: new_local = (P^-1 * deltaWorld * P) * curLocal - pxr::UsdGeomXformCache xformCache(pxr::UsdTimeCode::Default()); - pxr::UsdPrim parent = prim.GetParent(); - pxr::GfMatrix4d parentRotOnly(1.0); - if (parent && !parent.IsPseudoRoot()) { - pxr::GfMatrix4d p2w = xformCache.GetLocalToWorldTransform(parent); - parentRotOnly = pxr::GfMatrix4d(1.0).SetRotate(p2w.ExtractRotation()); - } - double det = 0.0; - pxr::GfMatrix4d parentRotInv = parentRotOnly.GetInverse(&det); - if (std::abs(det) < 1e-9) return; - - pxr::GfMatrix4d newRotMat = - curRotMat * parentRotOnly * deltaWorld * parentRotInv; - newRotMat.Orthonormalize(); - - // Step 3: decompose newRotMat (which is in local-to-parent space) back into - // Euler angles matching m_dragOriginalRotOrder. - // - // IMPORTANT: The reference axes for GfRotation::DecomposeRotation must be the - // canonical parent-space unit vectors in the order dictated by the rotation - // order — NOT rows of initRotMat (which are the rotated local axes in world - // space). Using rotated rows causes the decomposition to solve a completely - // different problem once any rotation has been applied, producing the random - // value jumps observed on the second axis drag. - // - // For each RotationOrder the Euler product is (row-vector convention): - // XYZ: R = Rx * Ry * Rz → axes in order (X,Y,Z) - // XZY: R = Rx * Rz * Ry → axes in order (X,Z,Y) - // YXZ: R = Ry * Rx * Rz → axes in order (Y,X,Z) - // YZX: R = Ry * Rz * Rx → axes in order (Y,Z,X) - // ZXY: R = Rz * Rx * Ry → axes in order (Z,X,Y) - // ZYX: R = Rz * Ry * Rx → axes in order (Z,Y,X) - // - // The hint angles must be ordered the same way (tw, fb, lr). - - // Map rotation order → (twIdx, fbIdx, lrIdx) where 0=X,1=Y,2=Z - int twIdx = 0, fbIdx = 1, lrIdx = 2; - switch (m_dragOriginalRotOrder) { - case pxr::UsdGeomXformCommonAPI::RotationOrderXYZ: twIdx=0; fbIdx=1; lrIdx=2; break; - case pxr::UsdGeomXformCommonAPI::RotationOrderXZY: twIdx=0; fbIdx=2; lrIdx=1; break; - case pxr::UsdGeomXformCommonAPI::RotationOrderYXZ: twIdx=1; fbIdx=0; lrIdx=2; break; - case pxr::UsdGeomXformCommonAPI::RotationOrderYZX: twIdx=1; fbIdx=2; lrIdx=0; break; - case pxr::UsdGeomXformCommonAPI::RotationOrderZXY: twIdx=2; fbIdx=0; lrIdx=1; break; - case pxr::UsdGeomXformCommonAPI::RotationOrderZYX: twIdx=2; fbIdx=1; lrIdx=0; break; - default: break; - } - - static const pxr::GfVec3d kUnitAxes[3] = { - {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0} - }; - const pxr::GfVec3d& twAxis = kUnitAxes[twIdx]; - const pxr::GfVec3d& fbAxis = kUnitAxes[fbIdx]; - const pxr::GfVec3d& lrAxis = kUnitAxes[lrIdx]; - - // Hint angles in tw/fb/lr order from current accumulated state - double thetaTw = pxr::GfDegreesToRadians(double(m_dragStartRotate[twIdx])); - double thetaFB = pxr::GfDegreesToRadians(double(m_dragStartRotate[fbIdx])); - double thetaLR = pxr::GfDegreesToRadians(double(m_dragStartRotate[lrIdx])); - double thetaSw = 0.0; - - pxr::GfRotation::DecomposeRotation( - newRotMat, twAxis, fbAxis, lrAxis, /*handedness=*/1.0, - &thetaTw, &thetaFB, &thetaLR, &thetaSw, /*useHint=*/true); - - // Write results back into the correct component slots - pxr::GfVec3f newAngles = m_dragStartRotate; - newAngles[twIdx] = float(pxr::GfRadiansToDegrees(thetaTw)); - newAngles[fbIdx] = float(pxr::GfRadiansToDegrees(thetaFB)); - newAngles[lrIdx] = float(pxr::GfRadiansToDegrees(thetaLR)); - m_dragStartRotate = newAngles; - api.SetRotate(m_dragStartRotate, m_dragOriginalRotOrder, - pxr::UsdTimeCode::Default()); + if (!m_stage||m_primPath.IsEmpty()) return; + pxr::UsdPrim p=m_stage->GetPrimAtPath(m_primPath); if (!p) return; + pxr::UsdEditContext ec(m_stage,m_stage->GetEditTarget()); + pxr::UsdGeomXformCommonAPI(p).SetRotate(r,ro,pxr::UsdTimeCode::Default()); + m_dragStartRotate=r; } -void TransformManipulator::ApplyScaleDelta(int axisIndex, float factor) +void TransformManipulator::ApplyScale(const pxr::GfVec3f& s) { - if (!m_stage || m_primPath.IsEmpty()) return; - pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath); - if (!prim) return; - - pxr::UsdEditContext ec(m_stage, m_stage->GetEditTarget()); - pxr::UsdGeomXformCommonAPI api(prim); - - m_dragStartScale[axisIndex] *= factor; - m_dragStartScale[axisIndex] = std::max(0.001f, m_dragStartScale[axisIndex]); - api.SetScale(m_dragStartScale, pxr::UsdTimeCode::Default()); + if (!m_stage||m_primPath.IsEmpty()) return; + pxr::UsdPrim p=m_stage->GetPrimAtPath(m_primPath); if (!p) return; + pxr::UsdEditContext ec(m_stage,m_stage->GetEditTarget()); + pxr::UsdGeomXformCommonAPI(p).SetScale(s,pxr::UsdTimeCode::Default()); + m_dragStartScale=s; } } // namespace UsdLayerManager