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>
This commit is contained in:
2026-06-17 06:47:42 +08:00
parent 7d53eb18d4
commit dc0c15de70
+72 -19
View File
@@ -366,7 +366,11 @@ void TransformManipulator::DrawScaleGizmo(ImDrawList* dl, const pxr::GfMatrix4d&
dl->AddLine(pivSS,tipSS,col,kScaleLineThick);
dl->AddCircleFilled(tipSS,kScaleCircleRadius,col,16);
}
dl->AddCircleFilled(pivSS,kCenterCircleRadius+1.f,kColCenter,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);
}
// ============================================================================
@@ -431,9 +435,25 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
// 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);
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;
@@ -444,9 +464,22 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
if (hovered && !m_isDragging &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !io.KeyAlt)
{
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);
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;
@@ -513,10 +546,19 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
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);
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};
}
}
}
}
@@ -527,7 +569,7 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
if (m_isDragging) {
consumed=true;
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
if (m_dragAxis<0||m_dragAxis>2) { m_isDragging=false;m_dragAxis=-1;return consumed; }
if (m_dragAxis<0||m_dragAxis>3) { m_isDragging=false;m_dragAxis=-1;return consumed; }
pxr::GfRay ray=ComputeMouseRay(vp,mouse,imgPos,vW,vH);
@@ -632,14 +674,25 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
// 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));
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;
}