Init Repo
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user