From dc0c15de700f20dc7ae551708554eba38f6aaf6e Mon Sep 17 00:00:00 2001 From: indigo Date: Wed, 17 Jun 2026 06:47:42 +0800 Subject: [PATCH] Scale manipulator: center handle for uniform scale + hover fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/ui/TransformManipulator.cpp | 91 ++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 19 deletions(-) diff --git a/src/ui/TransformManipulator.cpp b/src/ui/TransformManipulator.cpp index d84ed1f..464f6e2 100644 --- a/src/ui/TransformManipulator.cpp +++ b/src/ui/TransformManipulator.cpp @@ -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)=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; }