Init Repo

This commit is contained in:
2026-06-03 09:00:11 +08:00
commit 9be48d8b9e
155 changed files with 14827 additions and 0 deletions
+737
View File
@@ -0,0 +1,737 @@
#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/xformCache.h>
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/matrix4f.h>
#include <pxr/base/gf/vec3d.h>
#include <cmath>
#include <algorithm>
#include <vector>
#include <memory>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace UsdLayerManager {
// ---------------------------------------------------------------------------
// ImGuizmo-derived colour palette
// X = red, Y = green, Z = blue (matches Maya / ImGuizmo defaults)
// Highlight (hovered / active) = orange (ImGuizmo SELECTION colour)
// ---------------------------------------------------------------------------
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 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
// ──────────────────────────────────────────────────────────────────────────────
// 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
//
// 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
{
// Fallback: world-space unit vectors
outAxes[0] = {1, 0, 0};
outAxes[1] = {0, 1, 0};
outAxes[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).
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];
}
}
// ──────────────────────────────────────────────────────────────────────────────
// 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,
const pxr::GfMatrix4d& vp,
int viewW, int viewH,
const ImVec2& imagePos,
ImVec2& outScreen)
{
// 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<float>((ndcX + 1.0) * 0.5 * viewW);
float py = static_cast<float>((1.0 - ndcY) * 0.5 * viewH);
outScreen = ImVec2(imagePos.x + px, imagePos.y + py);
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)
{
// 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<float>(tx - px);
float dy = static_cast<float>(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;
}
// ──────────────────────────────────────────────────────────────────────────────
// 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);
}
// ──────────────────────────────────────────────────────────────────────────────
// HitTestAxes
// Returns 0=X, 1=Y, 2=Z or -1.
// ──────────────────────────────────────────────────────────────────────────────
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 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; }
}
return bestAxis;
}
// ──────────────────────────────────────────────────────────────────────────────
// HitTestRotateRings
//
// Tests proximity to the VISIBLE (front-facing) half-arc of each ring.
// Uses the same angleStart formula as DrawRotateGizmo so hit area exactly
// matches the drawn arcs. Returns 0=X, 1=Y, 2=Z or -1 for no hit.
// ──────────────────────────────────────────────────────────────────────────────
int TransformManipulator::HitTestRotateRings(const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot, float sf,
const pxr::GfVec3d& cameraEye,
const ImVec2& imgPos, int vW, int vH,
const ImVec2& mouse,
const pxr::GfVec3d axes[3]) const
{
static constexpr int kSegs = 32; // fewer segs needed for hit testing
static constexpr float kDispFactor = 1.2f;
static constexpr float kPickRadius = 10.0f; // pixels, matches ImGuizmo's 8 px + margin
float radius = sf * kDispFactor;
pxr::GfVec3d camToScene = pivot - cameraEye;
double len = camToScene.GetLength();
if (len < 1e-9) camToScene = pxr::GfVec3d(0,0,-1);
else camToScene /= len;
float bestDist = kPickRadius;
int bestAxis = -1;
for (int axis = 0; axis < 3; ++axis) {
// Tangent axes spanning this ring's plane
// axis 0: ring normal = axes[0], plane spanned by axes[1], axes[2]
// axis 1: ring normal = axes[1], plane spanned by axes[0], axes[2]
// axis 2: ring normal = axes[2], plane spanned by axes[0], axes[1]
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 compute front-facing half-arc start
float a_proj = static_cast<float>(camToScene[0]*u[0] + camToScene[1]*u[1] + camToScene[2]*u[2]);
float b_proj = static_cast<float>(camToScene[0]*v[0] + camToScene[1]*v[1] + camToScene[2]*v[2]);
float as = std::atan2(b_proj, a_proj) + static_cast<float>(M_PI) * 0.5f;
ImVec2 prevSS;
bool hasPrev = false;
for (int s = 0; s <= kSegs; ++s) {
float angle = as + static_cast<float>(M_PI) *
(static_cast<float>(s) / static_cast<float>(kSegs));
float c = std::cos(angle), si = std::sin(angle);
pxr::GfVec3d p = pivot + u * (radius * c) + v * (radius * si);
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;
}
}
return bestAxis;
}
// ──────────────────────────────────────────────────────────────────────────────
// DrawMoveGizmo
//
// For each axis:
// • Shaft — thick line from pivot to cone-base (~78 % of arrow length)
// • Head — screen-space filled isoceles triangle (ImGuizmo arrowhead style)
// Centre — small filled circle
// ──────────────────────────────────────────────────────────────────────────────
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])
{
// Arrow geometry ratios (tuned to match ImGuizmo proportions)
static constexpr float kShaftFrac = 0.78f; // shaft ends at 78 % of arrow
static constexpr float kArrowFrac = 0.12f; // arrowhead half-width / total pixel length
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 shaftEndSS, tipSS;
bool okShaft = WorldToScreen(pivot + axes[i] * sf * kShaftFrac,
vp, vW, vH, imgPos, shaftEndSS);
bool okTip = WorldToScreen(pivot + axes[i] * sf,
vp, vW, vH, imgPos, tipSS);
if (!okShaft || !okTip) continue;
// --- Shaft ---
dl->AddLine(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);
}
// Centre circle (white, like ImGuizmo's center square)
dl->AddCircleFilled(pivotSS, 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,
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<float>(camToScene[0]*u[0] + camToScene[1]*u[1] + camToScene[2]*u[2]);
float b_proj = static_cast<float>(camToScene[0]*v[0] + camToScene[1]*v[1] + camToScene[2]*v[2]);
float as = std::atan2(b_proj, a_proj) + static_cast<float>(M_PI) * 0.5f;
std::vector<ImVec2> pts;
pts.reserve(kSegs + 1);
for (int s = 0; s <= kSegs; ++s) {
float angle = as + static_cast<float>(M_PI) *
(static_cast<float>(s) / static_cast<float>(kSegs));
float c = std::cos(angle), si = std::sin(angle);
pxr::GfVec3d p = pivot + u * (radius * c) + v * (radius * si);
ImVec2 ss;
if (WorldToScreen(p, vp, vW, vH, imgPos, ss))
pts.push_back(ss);
}
if (pts.size() > 1)
dl->AddPolyline(pts.data(), static_cast<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,
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 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);
}
// Centre box / circle (uniform scale handle)
dl->AddCircleFilled(pivotSS, 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)
{
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);
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;
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)
{
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
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);
}
}
bool consumed = false;
// --- Start drag ---
if (viewportHovered && !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);
}
if (hit >= 0) {
m_isDragging = true;
m_dragAxis = hit;
m_dragLastPos = mouse;
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);
}
}
// 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;
}
}
}
// --- Drag ongoing ---
if (m_isDragging) {
consumed = true;
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
ImVec2 delta = { mouse.x - m_dragLastPos.x,
mouse.y - m_dragLastPos.y };
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;
pxr::GfVec3d move(
m_dragAxis == 0 ? worldDelta : 0.f,
m_dragAxis == 1 ? worldDelta : 0.f,
m_dragAxis == 2 ? worldDelta : 0.f);
ApplyMoveDelta(move);
}
}
}
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<float>(M_PI)) deltaAngle -= 2.f * static_cast<float>(M_PI);
while (deltaAngle < -static_cast<float>(M_PI)) deltaAngle += 2.f * static_cast<float>(M_PI);
float angleDeg = deltaAngle * (180.f / static_cast<float>(M_PI));
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);
}
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);
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<TransformCommand>(
m_stage, m_primPath, editLayer,
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.
m_commandHistory->Push(std::move(cmd));
}
m_isDragging = false;
m_dragAxis = -1;
}
}
return consumed;
}
// ──────────────────────────────────────────────────────────────────────────────
// USD transform write helpers
// ──────────────────────────────────────────────────────────────────────────────
void TransformManipulator::ApplyMoveDelta(const pxr::GfVec3d& worldDelta)
{
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());
}
void TransformManipulator::ApplyRotateDelta(int axisIndex, float angleDeg)
{
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_dragStartRotate[axisIndex] += angleDeg;
api.SetRotate(m_dragStartRotate,
pxr::UsdGeomXformCommonAPI::RotationOrderXYZ,
pxr::UsdTimeCode::Default());
}
void TransformManipulator::ApplyScaleDelta(int axisIndex, float factor)
{
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());
}
} // namespace UsdLayerManager