Files
UsdLayerManager/src/ui/TransformManipulator.cpp
T
indigo dc0c15de70 Scale manipulator: center handle for uniform scale + hover fix
- Add center dot hit test (axis 3) for the scale gizmo so hovering/
  clicking the center circle performs uniform scale on all three axes.
  Center is checked before HitTestAxes to prevent the pivot (which is
  an endpoint of all three axis segments) from always resolving to X.

- Uniform scale uses screen-space pixel delta (right = grow, left =
  shrink, 200 px ≈ ×2) instead of the world-space axis-line ratio.
  The axis-line approach broke because the mouse starts at the pivot
  (oT ≈ 0), making cT/oT explode on any small movement.

- Center dot highlights in orange and grows when hovered or active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 06:47:42 +08:00

753 lines
35 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "TransformManipulator.h"
#include "../utils/Logger.h"
#include "../core/CommandHistory.h"
#include "../core/commands/TransformCommand.h"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usdGeom/xformable.h>
#include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/usd/usdGeom/xformOp.h>
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/matrix4f.h>
#include <pxr/base/gf/vec3d.h>
#include <pxr/base/gf/rotation.h>
#include <pxr/base/gf/math.h>
#include <pxr/base/gf/line.h>
#include <pxr/base/gf/lineSeg.h>
#include <pxr/base/gf/plane.h>
#include <pxr/base/gf/ray.h>
#include <pxr/base/vt/value.h>
#include <cmath>
#include <algorithm>
#include <vector>
#include <memory>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace UsdLayerManager {
// ---------------------------------------------------------------------------
// 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);
static const ImU32 kColCenter = IM_COL32(255, 255, 255, 220);
static const ImU32 kAxisColors[3] = { kColX, kColY, kColZ };
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::UsdTimeCode time,
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, time))
return true;
// Fallback -----------------------------------------------------------------
LOG_WARNING("Prim " + primPath.GetString() +
" has an incompatible xform stack. Creating a compatible "
"override in the edit layer.");
pxr::UsdGeomXformCache xc(time);
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;
}
void TransformManipulator::SetSelectedPrim(const pxr::SdfPath& path)
{
m_primPath = path; m_isDragging = false;
}
// ============================================================================
// GetGizmoAxes
// 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
{
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 xc(m_displayTime);
pxr::GfMatrix4d l2w = xc.GetLocalToWorldTransform(prim);
for (int i = 0; i < 3; ++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 p_clip = (p_world, 1) * VP (USD row-vector convention)
// ============================================================================
bool TransformManipulator::WorldToScreen(const pxr::GfVec3d& world,
const pxr::GfMatrix4d& vp,
int vW, int vH,
const ImVec2& imgPos, ImVec2& out)
{
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;
}
// ============================================================================
// 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)
{
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,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 (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 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,pivSS,tipSS);
if (d<best){best=d;hit=i;}
}
return hit;
}
// ============================================================================
// HitTestRotateRings (front-facing half-arc proximity)
// ============================================================================
int TransformManipulator::HitTestRotateRings(const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot, float sf,
const pxr::GfVec3d& camEye,
const ImVec2& imgPos, int vW, int vH,
const ImVec2& mouse,
const pxr::GfVec3d axes[3]) const
{
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)){hp=false;continue;}
if (hp){float d=PointToSegmentDist(mouse,prev,ss);if(d<best){best=d;hit=axis;}}
prev=ss; hp=true;
}
}
return hit;
}
// ============================================================================
// Draw helpers
// ============================================================================
void TransformManipulator::DrawMoveGizmo(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 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);
}
dl->AddCircleFilled(pivSS,kCenterCircleRadius,kColCenter,16);
}
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 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<ImVec2> 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 (pts.size()>1)
dl->AddPolyline(pts.data(),int(pts.size()),col,ImDrawFlags_None,lw);
}
}
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 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(pivSS,tipSS,col,kScaleLineThick);
dl->AddCircleFilled(tipSS,kScaleCircleRadius,col,16);
}
// Center dot: axis 3 = uniform scale handle
bool centerActive = (m_dragAxis==3||m_hoveredAxis==3);
ImU32 centerCol = centerActive ? kColHover : kColCenter;
float centerR = centerActive ? kCenterCircleRadius+3.f : kCenterCircleRadius+1.f;
dl->AddCircleFilled(pivSS,centerR,centerCol,16);
}
// ============================================================================
// 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()||!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,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
//
// 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.
// deltaAxis = canonical axis e_a for the dragged ring (both spaces).
// deltaRotation = GfRotation(deltaAxis * axisSign, angle).
// resultingRotation:
// 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)
// 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;
ImGuiIO& io=ImGui::GetIO();
ImVec2 mouse=io.MousePos;
float sf=ComputeScreenFactor(vp,pivot,vW,vH,0.15f);
pxr::GfVec3d axes[3]; GetGizmoAxes(axes);
// Hover update
if (!m_isDragging && hovered) {
if (m_mode==ManipulatorMode::Rotate) {
m_hoveredAxis=HitTestRotateRings(vp,pivot,sf,camEye,imgPos,vW,vH,mouse,axes);
} else {
// Scale center check FIRST: the pivot is an endpoint of all three
// axis segments, so HitTestAxes always hits an axis (typically X)
// when the mouse is at the center. Check the center dot before
// falling through to the per-axis test.
m_hoveredAxis=-1;
if (m_mode==ManipulatorMode::Scale) {
ImVec2 pivSS;
if (WorldToScreen(pivot,vp,vW,vH,imgPos,pivSS)) {
float dx=mouse.x-pivSS.x, dy=mouse.y-pivSS.y;
if (std::sqrt(dx*dx+dy*dy)<kCenterCircleRadius+6.f)
m_hoveredAxis=3;
}
}
if (m_hoveredAxis<0)
m_hoveredAxis=HitTestAxes(vp,pivot,sf,imgPos,vW,vH,mouse,axes);
}
}
bool consumed=false;
// =========================================================================
// BEGIN drag
// =========================================================================
if (hovered && !m_isDragging &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !io.KeyAlt)
{
int hit=-1;
if (m_mode==ManipulatorMode::Rotate) {
hit=HitTestRotateRings(vp,pivot,sf,camEye,imgPos,vW,vH,mouse,axes);
} else {
// Scale center check before per-axis test (same reason as hover).
if (m_mode==ManipulatorMode::Scale) {
ImVec2 pivSS;
if (WorldToScreen(pivot,vp,vW,vH,imgPos,pivSS)) {
float dx=mouse.x-pivSS.x, dy=mouse.y-pivSS.y;
if (std::sqrt(dx*dx+dy*dy)<kCenterCircleRadius+6.f)
hit=3;
}
}
if (hit<0)
hit=HitTestAxes(vp,pivot,sf,imgPos,vW,vH,mouse,axes);
}
if (hit>=0) {
m_isDragging=true; m_dragAxis=hit; consumed=true;
pxr::GfVec3d trans; pxr::GfVec3f rot,scale;
pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder;
if (!SetupCompatibleXform(m_stage,m_primPath,m_displayTime,
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 keyframe presence before live writes author samples.
m_dragPreEditSamples=TransformCommand::QuerySamplePresence(
m_stage,m_primPath,rotOrder,m_editTime);
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.
//
// 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) {
if (m_dragAxis<3) {
// Single-axis scale: project onto world-space axis line
m_dragScaleAxisLine=pxr::GfLine(pivot,axes[m_dragAxis]);
pxr::GfVec3d rpt;
pxr::GfFindClosestPoints(ray,m_dragScaleAxisLine,&rpt,&m_dragScaleOriginOnAxis);
} else {
// Uniform scale (axis 3): mouse starts at the center circle,
// so the axis-line origin parameter oT ≈ 0 and cT/oT blows up.
// Use screen-space pixel delta instead: store drag-start mouse
// position in m_dragScaleOriginOnAxis.x/y, compute ratio as
// 1 + deltaX * sensitivity each frame.
m_dragScaleOriginOnAxis={double(mouse.x),double(mouse.y),0.0};
}
}
}
}
// =========================================================================
// DRAG update
// =========================================================================
if (m_isDragging) {
consumed=true;
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
if (m_dragAxis<0||m_dragAxis>3) { m_isDragging=false;m_dragAxis=-1;return consumed; }
pxr::GfRay ray=ComputeMouseRay(vp,mouse,imgPos,vW,vH);
// -----------------------------------------------------------------
// 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(m_displayTime);
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);
}
}
ApplyTranslate(m_dragOriginalTranslate+parentDelta);
}
// -----------------------------------------------------------------
// 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)
// 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);
// 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
: 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::GfVec3f ns=m_dragOriginalScale;
if (m_dragAxis<3) {
// Single-axis: project onto world-space axis line.
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;
ns[m_dragAxis]=std::max(0.001f,float(double(m_dragOriginalScale[m_dragAxis])*ratio));
} else {
// Uniform scale: screen-space pixel delta → ratio.
// Right drag = grow, left drag = shrink.
// 200px right ≈ ×2 (sensitivity 0.005 per pixel).
double deltaX=double(mouse.x)-m_dragScaleOriginOnAxis[0];
double ratio=std::max(0.001,1.0+deltaX*0.005);
for (int i=0;i<3;++i)
ns[i]=std::max(0.001f,float(double(m_dragOriginalScale[i])*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()) {
auto cmd=std::make_unique<TransformCommand>(
m_stage, m_primPath,
m_stage->GetEditTarget().GetLayer(),
m_dragOriginalTranslate,m_dragOriginalRotate,m_dragOriginalScale,
m_dragStartTranslate, m_dragStartRotate, m_dragStartScale,
m_dragOriginalRotOrder, m_editTime, m_dragPreEditSamples,
"Transform "+m_primPath.GetName());
m_commandHistory->Push(std::move(cmd));
}
m_isDragging=false; m_dragAxis=-1;
}
}
return consumed;
}
// ============================================================================
// USD write helpers — write ABSOLUTE values
// ============================================================================
void TransformManipulator::ApplyTranslate(const pxr::GfVec3d& t)
{
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,m_editTime);
m_dragStartTranslate=t;
}
void TransformManipulator::ApplyRotate(const pxr::GfVec3f& r,
pxr::UsdGeomXformCommonAPI::RotationOrder ro)
{
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,m_editTime);
m_dragStartRotate=r;
}
void TransformManipulator::ApplyScale(const pxr::GfVec3f& s)
{
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,m_editTime);
m_dragStartScale=s;
}
} // namespace UsdLayerManager