From 3f185303962d44fea091d105df1962cc5ea26b65 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Wed, 1 Apr 2026 16:42:57 +0800 Subject: [PATCH] Add scene transform toolbar and scale gizmo --- editor/CMakeLists.txt | 1 + editor/resources/Icons/move_tool.png | Bin 0 -> 445 bytes editor/resources/Icons/move_tool_on.png | Bin 0 -> 337 bytes editor/resources/Icons/rotate_tool.png | Bin 0 -> 625 bytes editor/resources/Icons/rotate_tool_on.png | Bin 0 -> 565 bytes editor/resources/Icons/scale_tool.png | Bin 0 -> 298 bytes editor/resources/Icons/scale_tool_on.png | Bin 0 -> 295 bytes editor/resources/Icons/transform_tool.png | Bin 0 -> 847 bytes editor/resources/Icons/transform_tool_on.png | Bin 0 -> 559 bytes editor/resources/Icons/view_move_tool.png | Bin 0 -> 639 bytes editor/resources/Icons/view_move_tool_on.png | Bin 0 -> 657 bytes .../src/Viewport/SceneViewportMoveGizmo.cpp | 73 +-- .../Viewport/SceneViewportOverlayRenderer.cpp | 120 +++- .../Viewport/SceneViewportOverlayRenderer.h | 4 +- .../src/Viewport/SceneViewportRotateGizmo.cpp | 154 +++-- .../src/Viewport/SceneViewportRotateGizmo.h | 14 +- .../src/Viewport/SceneViewportScaleGizmo.cpp | 502 +++++++++++++++ editor/src/Viewport/SceneViewportScaleGizmo.h | 96 +++ editor/src/panels/SceneViewPanel.cpp | 576 +++++++++++++++--- editor/src/panels/SceneViewPanel.h | 18 +- tests/editor/CMakeLists.txt | 2 + .../test_scene_viewport_rotate_gizmo.cpp | 56 ++ .../test_scene_viewport_scale_gizmo.cpp | 250 ++++++++ 23 files changed, 1668 insertions(+), 198 deletions(-) create mode 100644 editor/resources/Icons/move_tool.png create mode 100644 editor/resources/Icons/move_tool_on.png create mode 100644 editor/resources/Icons/rotate_tool.png create mode 100644 editor/resources/Icons/rotate_tool_on.png create mode 100644 editor/resources/Icons/scale_tool.png create mode 100644 editor/resources/Icons/scale_tool_on.png create mode 100644 editor/resources/Icons/transform_tool.png create mode 100644 editor/resources/Icons/transform_tool_on.png create mode 100644 editor/resources/Icons/view_move_tool.png create mode 100644 editor/resources/Icons/view_move_tool_on.png create mode 100644 editor/src/Viewport/SceneViewportScaleGizmo.cpp create mode 100644 editor/src/Viewport/SceneViewportScaleGizmo.h create mode 100644 tests/editor/test_scene_viewport_scale_gizmo.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index c3864aeb..75cdf591 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -76,6 +76,7 @@ add_executable(${PROJECT_NAME} WIN32 src/Viewport/SceneViewportPicker.cpp src/Viewport/SceneViewportMoveGizmo.cpp src/Viewport/SceneViewportRotateGizmo.cpp + src/Viewport/SceneViewportScaleGizmo.cpp src/Viewport/SceneViewportGrid.cpp src/Viewport/SceneViewportInfiniteGridPass.cpp src/Viewport/SceneViewportSelectionMaskPass.cpp diff --git a/editor/resources/Icons/move_tool.png b/editor/resources/Icons/move_tool.png new file mode 100644 index 0000000000000000000000000000000000000000..31720d7337cfd13d6abc6c13e30f51f2b91d422c GIT binary patch literal 445 zcmV;u0Yd(XP)kdg0004mNklb;@5S+D17pi;%$uyKmvrs#G&l`;Kpv3@3q%o;lt^@>q2U>6ARU-Z;WJ6`gJK+QSm1oC$ZR@aI`JnGn2diXiq>9qv{1xjJb=2kcX#`jR nQF^=fZ44pw?WbdN@_GLTt6q|L)?WU?00000NkvXXu0mjfUIV>! literal 0 HcmV?d00001 diff --git a/editor/resources/Icons/move_tool_on.png b/editor/resources/Icons/move_tool_on.png new file mode 100644 index 0000000000000000000000000000000000000000..9d166ff92eec33c6d5ea216e4e47b0ba928a3b74 GIT binary patch literal 337 zcmV-X0j~auP)kdg0003PNkl zOKQY043%dSDAO{~gJji1lH z;8cj1U0K2ERQugHscgW}xVbQYd_-q&K?S!Pl9nF}l=V=QbX zaW;)-TmsTKEhG>eey1-axA0hs93G#3{zo+qiRu$N^aGn}n}ukusm``_Bp}!n&dRpF jd$8PLwB2F2hK=_t)FByHdgX&K00000NkvXXu0mjft-q7J literal 0 HcmV?d00001 diff --git a/editor/resources/Icons/rotate_tool.png b/editor/resources/Icons/rotate_tool.png new file mode 100644 index 0000000000000000000000000000000000000000..8174c1189aeb4efcfb1d939a9bb40d13cb01f8a5 GIT binary patch literal 625 zcmV-%0*?KOP)kdg0006wNklchk-LaMaNDx2;6i9;WDA+O8xeCf zn$cDYAr;grp#+6&2$%L}oYO*kZ=B~e^Hz0M_nv!y-#zDC&b{!jO^$XCo-ZV&lV&|@ z4|bR}+RLO%=@>+UVv}IJ&qCdud}ATL&CSTAmtow|B_U@BiR1=B%} z2HBC()RRD53T$&)erS(<<@=pdxvbE@Lf^@teBW#n^IFfcd+i8-X(Fe{gpJ=RR<612 zlSR;F(;5KCJPl;h52{^T`W9L0uw?*~(95zD@2}iaD}>#;FwF^xWW84^VJHY?a)*YJ}j!1RiI+hIEJM-nv)TbZz|=V{v}r;OQINM9 z_fPl!1vjUi`_tWE+&{f0*kdg0005}NklMq)FpHg%p(rgKbiz zF@`9GAqXShoW#YP62BC(PAA>oC90C?c6R5tGjsFyfPZbC+Z{d~HdON#Ref}K!cO&k zCbV680Tj}~{rc_er~&{=cxS@_+&XTK7VMm&*tI>|3aToOcPNUoQ_<+?2NL5?DEC{z?zhu zq&iPo&z<}%K)&^;U}yUN4&`c{3jkPy;ze?7mMM1gC(c^dOyI7mU=;v-02poq-iLfZ zX`{56SiS@FG6tvy;Nx5hXaU-c0p23uUCslz{~5s2q@swL4DH{T?os?1X8=D#8rkLp z_%#7^fnj-``2J(SyP5zxz>jBhmGA`61mHCRZOm0d9q`)mDp;j>JvB6~{CIY$a5?F? zpse8q%QP zQMR8AkZ*^SX|R&j+jZXDJ)Ls3&U%!Enc)Bslkdg0002-Nkl20s@NvIabi#}Q%_FCZQwg(#v&u!|sS#v(=~fyj=9pQ-l4 zdo#?wc?*0x1ORy5*hfVZxi>t24j2HyfCH7-2Y^zt0Ro|iSCk|*-tEEX2}(tm{ROo$ zM~6VZS3y~;f^j=t^-L=yo81FfT5Gb!l1ZSX2&pEvCx=C#BtlfNi=Ek-L6E}HoJ0&A zJm5jZ5Rz-Yh8I4gS&uVtu^`eQ@Rx(eaWlO<>7t`4<(hChurx6zu$XGcoi w<~I!GpFB}f&i7ybP4^1Kkdboh3Z2$lO07*qoM6N<$f;hlkdg0002)NklxHR-@lMuFr! z-sk29`1B$Gh!?y<&yb8L@YY!Xc{auYAoBa!PO@ZEI%irdW~c5oYNg!W8@z=|4w>7$ZD~&mDBR{{34l+f41B5__I}4u t{>2js{4mNleu}8bU`ZDL5(W5*bpt{tXq1d&weJ7`002ovPDHLkV1lgIe?|ZR literal 0 HcmV?d00001 diff --git a/editor/resources/Icons/transform_tool.png b/editor/resources/Icons/transform_tool.png new file mode 100644 index 0000000000000000000000000000000000000000..b11e562a2c573910eb686061484cd898f625aff8 GIT binary patch literal 847 zcmV-V1F-ywP)kdg0009PNkl$m~ z*=O}cI3Cyy!0KXJcFxkMkTS83*u!U84Z3 zamKq8d&p@$bq@pcSh~al57Uqfa%S}>e>J@F=+0N7?IXoa0LRhyG)0S7yem;SCGIZYDDugu{q7ckNpYQhK#h#I39v9&_jFd! zLlJ~Ty=lK!qd)*ss)->a@0CS$>w0Oe%*`He-q#_Bq9J+&bUH3{EfSZh2 zUx(}7n{t_i6g%ev+2>=I%Vd&qkqe&7SNdkCgLCsWQgRcIM-)C$oX`t;6XAQktSo-_ie z?54B_qv3-u-uZ!jQuecdzbCYhL?KbZRRF6;opn!BQuf_rI?y~Vea$`>&fABnF#*5?wf5l-vvWF2OY?n{cuz}9Qf2JF zTAF2_?ev-}dWe=Dt*`sx^freMN<}I`;&7b$y3W#yyMq{&x82iGXOyB&0S&oG$^9&# zJxxyEU>KyV`LG*y03Z&r^N%G4&|c6JJllH+f)xM&002ovPDHLkV1iDcq9*_V literal 0 HcmV?d00001 diff --git a/editor/resources/Icons/transform_tool_on.png b/editor/resources/Icons/transform_tool_on.png new file mode 100644 index 0000000000000000000000000000000000000000..37d3dbe9926f3879ec78a07777ba5016d702901c GIT binary patch literal 559 zcmV+~0?_@5P)kdg0005@Nkl(0J8(0;@V}n=IQL99HOlG zn4_?mWgN|pS0M_`_xtQa5GFWdYq7M2GfW_e{rmk8B4~dn(3?JR%XWx_!k=gZri zLYO}s`g8{E6vQuzvixa}cM!x|2cZaEi1H!4&0`Mn+~vPsaqO60LYVP0gt>nZSLj3N zdif71&4RI96 z2EwFFP*qyP1j?jUH#5@*GrKS|OJgSNRSLMrB&YKt@5%v=%xd4v()jsCO>0z+Kts*v zn>5Z=>!?soW8Oj>`c#HTP{d()sb)2Ag9E+YY!+)tNyi@Mp8(-%KqHfv*hU2R0Q@6nq$&F}5&zY6L7|C4g*F!h)y zS`1TfWtcvej599_gbgCQ7#<6^;gI8wb_zjLAKjYf>ybb^W002ovPDHLkV1iM`1o{8~ literal 0 HcmV?d00001 diff --git a/editor/resources/Icons/view_move_tool.png b/editor/resources/Icons/view_move_tool.png new file mode 100644 index 0000000000000000000000000000000000000000..8b74e0960954f56629e92b9dca0c359bf81b0409 GIT binary patch literal 639 zcmV-_0)YLAP)kdg0006;Nkl+-N1CeqLBnBW;0CopEL5gj29suV@sUllxTF^VA z`Z?nOTmVovdqo56lL@ik*m3Qx>%MS}er?%0w@+&y!0vEASq4fuP^Sc1d7a!``46;7 zoz}vPRQ!Q^|63O#7t*Iw@#v1E)4P}Qh(t;}Z^@~k$A36XJPi%R+SjJm5WO~&I` zgC4ygXQ^32&piOndzG&3xXKPsdUm56C07eTUiq}MGh=nt6u~p!d60g{V>4j&t$=|d zhtzMw{gkdg00075Nkl7A^K;|4#;s>7aEaxMAO2|50#?I{?0*^*7R%$fXspndY1q; z5@u5Y$W;*3TnK<#1z;K=7B`?V0Cf<6l`sZ5044z{iH22qOp*Y62?5|6fKZx%$WRkN z#^_-HD(|nG(@-@4DgtVvPXG)6hguoPegn1)P_*oOg&BZY8-twG0jjk(h=R%ppcu{p zc&}xOIT=#rHUAI>s>x&gpL zB?iF0qQ(B;OdIR(CZP{x-e~*E$IaOi=&VcvaHmnee#J|1H-LWQcR=%Jo3(Hm?Dtx< z=*8wNy!A%yF7L?aoNVkl^MNrZ3nK4j*KtE?VsjB10sg~F!VGxVyOrVslB99D*@lbcv z{U*JpxO*y}q|c!j-D&09nko62C;hoA$I5r?#@Ger_J#1SOFJiva<(skS1o*6&-8O< zLINO?vfQo%W#Eh|h1`GJa(gfUeS)Vw-2cQzQ*O_ZmVU;I9syiSxxtXy#I-6vCDA#7 r!=tqx=sr|QI&!WHf%%~O{~&(>0F>oy9W&ft00000NkvXXu0mjfZ%8a| literal 0 HcmV?d00001 diff --git a/editor/src/Viewport/SceneViewportMoveGizmo.cpp b/editor/src/Viewport/SceneViewportMoveGizmo.cpp index 9d279673..90f560ec 100644 --- a/editor/src/Viewport/SceneViewportMoveGizmo.cpp +++ b/editor/src/Viewport/SceneViewportMoveGizmo.cpp @@ -5,16 +5,13 @@ #include "SceneViewportPicker.h" #include -#include -#include -#include namespace XCEngine { namespace Editor { namespace { -constexpr float kMoveGizmoHandleLengthPixels = 88.0f; +constexpr float kMoveGizmoHandleLengthPixels = 100.0f; constexpr float kMoveGizmoHoverThresholdPixels = 10.0f; Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) { @@ -166,63 +163,6 @@ bool IsMouseInsideViewport(const SceneViewportMoveGizmoContext& context) { context.mousePosition.y <= context.viewportSize.y; } -void EncapsulateTransformedBounds( - const Math::Bounds& localBounds, - const Components::TransformComponent& transform, - Math::Bounds& inOutBounds, - bool& inOutHasBounds) { - const Math::Vector3 localMin = localBounds.GetMin(); - const Math::Vector3 localMax = localBounds.GetMax(); - const Math::Vector3 corners[] = { - Math::Vector3(localMin.x, localMin.y, localMin.z), - Math::Vector3(localMax.x, localMin.y, localMin.z), - Math::Vector3(localMin.x, localMax.y, localMin.z), - Math::Vector3(localMin.x, localMin.y, localMax.z), - Math::Vector3(localMax.x, localMax.y, localMin.z), - Math::Vector3(localMin.x, localMax.y, localMax.z), - Math::Vector3(localMax.x, localMin.y, localMax.z), - Math::Vector3(localMax.x, localMax.y, localMax.z) - }; - - for (const Math::Vector3& localCorner : corners) { - const Math::Vector3 worldCorner = transform.TransformPoint(localCorner); - if (!inOutHasBounds) { - inOutBounds = Math::Bounds(worldCorner, Math::Vector3::Zero()); - inOutHasBounds = true; - } else { - inOutBounds.Encapsulate(worldCorner); - } - } -} - -void CollectRenderableWorldBoundsRecursive( - const Components::GameObject& gameObject, - Math::Bounds& inOutBounds, - bool& inOutHasBounds) { - if (!gameObject.IsActiveInHierarchy()) { - return; - } - - const auto* meshFilter = gameObject.GetComponent(); - const auto* meshRenderer = gameObject.GetComponent(); - if (meshFilter != nullptr && - meshRenderer != nullptr && - meshFilter->IsEnabled() && - meshRenderer->IsEnabled()) { - const auto* mesh = meshFilter->GetMesh(); - if (mesh != nullptr) { - EncapsulateTransformedBounds(mesh->GetBounds(), *gameObject.GetTransform(), inOutBounds, inOutHasBounds); - } - } - - for (size_t childIndex = 0; childIndex < gameObject.GetChildCount(); ++childIndex) { - const Components::GameObject* child = gameObject.GetChild(childIndex); - if (child != nullptr) { - CollectRenderableWorldBoundsRecursive(*child, inOutBounds, inOutHasBounds); - } - } -} - float ComputeWorldUnitsPerPixel( const SceneViewportOverlayData& overlay, const Math::Vector3& worldPoint, @@ -240,13 +180,6 @@ float ComputeWorldUnitsPerPixel( return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / viewportHeight; } -Math::Vector3 GetGizmoWorldOrigin(const Components::GameObject& gameObject) { - Math::Bounds worldBounds = {}; - bool hasBounds = false; - CollectRenderableWorldBoundsRecursive(gameObject, worldBounds, hasBounds); - return hasBounds ? worldBounds.center : gameObject.GetTransform()->GetPosition(); -} - } // namespace void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) { @@ -289,7 +222,7 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c } const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition(); - const Math::Vector3 pivotWorldPosition = GetGizmoWorldOrigin(*context.selectedObject); + const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition(); Math::Vector3 dragPlaneNormal = Math::Vector3::Zero(); Math::Vector3 worldAxis = Math::Vector3::Zero(); @@ -442,7 +375,7 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& return; } - const Math::Vector3 gizmoWorldOrigin = GetGizmoWorldOrigin(*selectedObject); + const Math::Vector3 gizmoWorldOrigin = selectedObject->GetTransform()->GetPosition(); const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, diff --git a/editor/src/Viewport/SceneViewportOverlayRenderer.cpp b/editor/src/Viewport/SceneViewportOverlayRenderer.cpp index 5fe0857a..dfa0df7f 100644 --- a/editor/src/Viewport/SceneViewportOverlayRenderer.cpp +++ b/editor/src/Viewport/SceneViewportOverlayRenderer.cpp @@ -135,6 +135,99 @@ void DrawSceneRotateGizmoHandle( } } +void DrawSceneRotateGizmoAngleFill( + ImDrawList* drawList, + const ImVec2& viewportMin, + const SceneViewportRotateGizmoAngleFillDrawData& angleFill) { + if (drawList == nullptr || !angleFill.visible || angleFill.arcPointCount < 2) { + return; + } + + const ImVec2 pivot(viewportMin.x + angleFill.pivot.x, viewportMin.y + angleFill.pivot.y); + const ImU32 fillColor = ToImGuiColor(angleFill.fillColor); + const ImU32 outlineColor = ToImGuiColor(angleFill.outlineColor); + + ImVec2 fillPoints[kSceneViewportRotateGizmoAngleFillPointCount + 1] = {}; + fillPoints[0] = pivot; + for (size_t index = 0; index < angleFill.arcPointCount; ++index) { + fillPoints[index + 1] = ImVec2( + viewportMin.x + angleFill.arcPoints[index].x, + viewportMin.y + angleFill.arcPoints[index].y); + } + drawList->AddConvexPolyFilled( + fillPoints, + static_cast(angleFill.arcPointCount + 1), + fillColor); + + for (size_t index = 0; index + 1 < angleFill.arcPointCount; ++index) { + drawList->AddLine( + ImVec2(viewportMin.x + angleFill.arcPoints[index].x, viewportMin.y + angleFill.arcPoints[index].y), + ImVec2( + viewportMin.x + angleFill.arcPoints[index + 1].x, + viewportMin.y + angleFill.arcPoints[index + 1].y), + outlineColor, + 2.0f); + } + + drawList->AddLine( + pivot, + ImVec2(viewportMin.x + angleFill.arcPoints.front().x, viewportMin.y + angleFill.arcPoints.front().y), + outlineColor, + 1.6f); + drawList->AddLine( + pivot, + ImVec2( + viewportMin.x + angleFill.arcPoints[angleFill.arcPointCount - 1].x, + viewportMin.y + angleFill.arcPoints[angleFill.arcPointCount - 1].y), + outlineColor, + 1.6f); +} + +void DrawSceneScaleGizmoAxis( + ImDrawList* drawList, + const ImVec2& viewportMin, + const SceneViewportScaleGizmoAxisHandleDrawData& handle) { + if (drawList == nullptr || !handle.visible) { + return; + } + + const ImU32 color = ToImGuiColor(handle.color); + const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.2f); + const ImVec2 start(viewportMin.x + handle.start.x, viewportMin.y + handle.start.y); + const ImVec2 capCenter(viewportMin.x + handle.capCenter.x, viewportMin.y + handle.capCenter.y); + const ImVec2 direction = NormalizeImVec2(ImVec2(capCenter.x - start.x, capCenter.y - start.y)); + const ImVec2 lineEnd( + capCenter.x - direction.x * handle.capHalfSize, + capCenter.y - direction.y * handle.capHalfSize); + const ImVec2 capMin(capCenter.x - handle.capHalfSize, capCenter.y - handle.capHalfSize); + const ImVec2 capMax(capCenter.x + handle.capHalfSize, capCenter.y + handle.capHalfSize); + + drawList->AddLine(start, lineEnd, color, thickness); + drawList->AddRectFilled(capMin, capMax, color, 1.2f); + drawList->AddRect(capMin, capMax, IM_COL32(24, 24, 24, 220), 1.2f, 0, handle.active ? 2.0f : 1.0f); +} + +void DrawSceneScaleGizmoCenterHandle( + ImDrawList* drawList, + const ImVec2& viewportMin, + const SceneViewportScaleGizmoCenterHandleDrawData& handle) { + if (drawList == nullptr || !handle.visible) { + return; + } + + const ImVec2 center(viewportMin.x + handle.center.x, viewportMin.y + handle.center.y); + const ImVec2 handleMin(center.x - handle.halfSize, center.y - handle.halfSize); + const ImVec2 handleMax(center.x + handle.halfSize, center.y + handle.halfSize); + drawList->AddRectFilled(handleMin, handleMax, ToImGuiColor(handle.fillColor), 1.2f); + drawList->AddRect( + handleMin, + handleMax, + ToImGuiColor(handle.outlineColor), + 1.2f, + 0, + handle.active ? 2.0f : 1.0f); +} + void DrawSceneMoveGizmo( ImDrawList* drawList, const ImVec2& viewportMin, @@ -143,7 +236,6 @@ void DrawSceneMoveGizmo( return; } - const ImVec2 pivot(viewportMin.x + moveGizmo.pivot.x, viewportMin.y + moveGizmo.pivot.y); for (const SceneViewportMoveGizmoPlaneDrawData& plane : moveGizmo.planes) { DrawSceneMoveGizmoPlane(drawList, viewportMin, plane); } @@ -151,9 +243,6 @@ void DrawSceneMoveGizmo( for (const SceneViewportMoveGizmoHandleDrawData& handle : moveGizmo.handles) { DrawSceneMoveGizmoAxis(drawList, viewportMin, handle); } - - drawList->AddCircleFilled(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(20, 22, 24, 220), 20); - drawList->AddCircle(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(255, 255, 255, 48), 20, 1.0f); } void DrawSceneRotateGizmo( @@ -176,6 +265,8 @@ void DrawSceneRotateGizmo( } } + DrawSceneRotateGizmoAngleFill(drawList, viewportMin, rotateGizmo.angleFill); + for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) { if (handle.axis != SceneViewportRotateGizmoAxis::View) { DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, true); @@ -183,6 +274,21 @@ void DrawSceneRotateGizmo( } } +void DrawSceneScaleGizmo( + ImDrawList* drawList, + const ImVec2& viewportMin, + const SceneViewportScaleGizmoDrawData& scaleGizmo) { + if (drawList == nullptr || !scaleGizmo.visible) { + return; + } + + for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : scaleGizmo.axisHandles) { + DrawSceneScaleGizmoAxis(drawList, viewportMin, handle); + } + + DrawSceneScaleGizmoCenterHandle(drawList, viewportMin, scaleGizmo.centerHandle); +} + } // namespace void DrawSceneViewportOverlay( @@ -192,7 +298,8 @@ void DrawSceneViewportOverlay( const ImVec2& viewportMax, const ImVec2& viewportSize, const SceneViewportMoveGizmoDrawData* moveGizmo, - const SceneViewportRotateGizmoDrawData* rotateGizmo) { + const SceneViewportRotateGizmoDrawData* rotateGizmo, + const SceneViewportScaleGizmoDrawData* scaleGizmo) { if (drawList == nullptr || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) { return; } @@ -207,6 +314,9 @@ void DrawSceneViewportOverlay( if (rotateGizmo != nullptr) { DrawSceneRotateGizmo(drawList, viewportMin, *rotateGizmo); } + if (scaleGizmo != nullptr) { + DrawSceneScaleGizmo(drawList, viewportMin, *scaleGizmo); + } drawList->PopClipRect(); } diff --git a/editor/src/Viewport/SceneViewportOverlayRenderer.h b/editor/src/Viewport/SceneViewportOverlayRenderer.h index b2ecb513..efd74f2e 100644 --- a/editor/src/Viewport/SceneViewportOverlayRenderer.h +++ b/editor/src/Viewport/SceneViewportOverlayRenderer.h @@ -3,6 +3,7 @@ #include "IViewportHostService.h" #include "SceneViewportMoveGizmo.h" #include "SceneViewportRotateGizmo.h" +#include "SceneViewportScaleGizmo.h" #include @@ -16,7 +17,8 @@ void DrawSceneViewportOverlay( const ImVec2& viewportMax, const ImVec2& viewportSize, const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr, - const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr); + const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr, + const SceneViewportScaleGizmoDrawData* scaleGizmo = nullptr); } // namespace Editor } // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportRotateGizmo.cpp b/editor/src/Viewport/SceneViewportRotateGizmo.cpp index a4b7e0be..34daa0bc 100644 --- a/editor/src/Viewport/SceneViewportRotateGizmo.cpp +++ b/editor/src/Viewport/SceneViewportRotateGizmo.cpp @@ -6,6 +6,7 @@ #include +#include #include namespace XCEngine { @@ -13,9 +14,10 @@ namespace Editor { namespace { -constexpr float kRotateGizmoAxisRadiusPixels = 84.0f; -constexpr float kRotateGizmoViewRadiusPixels = 92.0f; +constexpr float kRotateGizmoAxisRadiusPixels = 96.0f; +constexpr float kRotateGizmoViewRadiusPixels = 106.0f; constexpr float kRotateGizmoHoverThresholdPixels = 9.0f; +constexpr float kRotateGizmoAngleFillMinRadians = 0.01f; Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) { return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized(); @@ -92,6 +94,12 @@ bool GetRotateRingBasis( } } +float GetRotateRingRadiusPixels(SceneViewportRotateGizmoAxis axis) { + return axis == SceneViewportRotateGizmoAxis::View + ? kRotateGizmoViewRadiusPixels + : kRotateGizmoAxisRadiusPixels; +} + float ComputeWorldUnitsPerPixel( const SceneViewportOverlayData& overlay, const Math::Vector3& worldPoint, @@ -109,20 +117,6 @@ float ComputeWorldUnitsPerPixel( return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / viewportHeight; } -float SignedAngleRadiansAroundAxis( - const Math::Vector3& from, - const Math::Vector3& to, - const Math::Vector3& axis) { - const Math::Vector3 normalizedFrom = from.Normalized(); - const Math::Vector3 normalizedTo = to.Normalized(); - const float dot = std::clamp( - Math::Vector3::Dot(normalizedFrom, normalizedTo), - -1.0f, - 1.0f); - const float sine = Math::Vector3::Dot(axis.Normalized(), Math::Vector3::Cross(normalizedFrom, normalizedTo)); - return std::atan2(sine, dot); -} - float NormalizeSignedAngleRadians(float radians) { while (radians > Math::PI) { radians -= Math::PI * 2.0f; @@ -150,6 +144,28 @@ SceneViewportRotateGizmoAxis GetRotateAxisForIndex(size_t index) { } } +bool TryComputeRingAngleFromWorldDirection( + SceneViewportRotateGizmoAxis axis, + const SceneViewportOverlayData& overlay, + const Math::Vector3& directionWorld, + float& outAngle) { + Math::Vector3 basisA = Math::Vector3::Zero(); + Math::Vector3 basisB = Math::Vector3::Zero(); + if (!GetRotateRingBasis(axis, overlay, basisA, basisB)) { + return false; + } + + const Math::Vector3 direction = directionWorld.Normalized(); + const float projectedX = Math::Vector3::Dot(direction, basisA); + const float projectedY = Math::Vector3::Dot(direction, basisB); + if (projectedX * projectedX + projectedY * projectedY <= Math::EPSILON) { + return false; + } + + outAngle = std::atan2(projectedY, projectedX); + return true; +} + } // namespace void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) { @@ -201,9 +217,18 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex } float startRingAngle = 0.0f; - if (useScreenSpaceDrag && - !TryGetClosestRingAngle(m_hoveredAxis, context.mousePosition, false, startRingAngle)) { - return false; + if (useScreenSpaceDrag) { + if (!TryGetClosestRingAngle(m_hoveredAxis, context.mousePosition, false, startRingAngle)) { + return false; + } + } else { + if (!TryComputeRingAngleFromWorldDirection( + m_hoveredAxis, + context.overlay, + startDirection, + startRingAngle)) { + return false; + } } undoManager.BeginInteractiveChange("Rotate Gizmo"); @@ -217,8 +242,8 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex m_screenSpaceDrag = useScreenSpaceDrag; m_dragPlane = dragPlane; m_dragStartWorldRotation = context.selectedObject->GetTransform()->GetRotation(); - m_dragStartDirectionWorld = useScreenSpaceDrag ? Math::Vector3::Zero() : startDirection.Normalized(); - m_dragStartRingAngle = useScreenSpaceDrag ? startRingAngle : 0.0f; + m_dragStartRingAngle = startRingAngle; + m_dragCurrentDeltaRadians = 0.0f; RefreshHandleState(); return true; } @@ -230,14 +255,11 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& return; } - float deltaRadians = 0.0f; + float currentRingAngle = 0.0f; if (m_screenSpaceDrag) { - float currentRingAngle = 0.0f; if (!TryGetClosestRingAngle(m_activeAxis, context.mousePosition, false, currentRingAngle)) { return; } - - deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle); } else { Math::Ray worldRay; if (!BuildSceneViewportRay( @@ -260,14 +282,22 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& return; } - deltaRadians = SignedAngleRadiansAroundAxis( - m_dragStartDirectionWorld, - currentDirection, - m_activeWorldAxis); + if (!TryComputeRingAngleFromWorldDirection( + m_activeAxis, + context.overlay, + currentDirection, + currentRingAngle)) { + return; + } } + const float deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle); + m_dragCurrentDeltaRadians = deltaRadians; const Math::Quaternion deltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians); context.selectedObject->GetTransform()->SetRotation(deltaRotation * m_dragStartWorldRotation); + BuildDrawData(context); + m_hoveredAxis = m_activeAxis; + RefreshHandleState(); } void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) { @@ -284,8 +314,8 @@ void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) { m_screenSpaceDrag = false; m_activeWorldAxis = Math::Vector3::Zero(); m_dragStartWorldRotation = Math::Quaternion::Identity(); - m_dragStartDirectionWorld = Math::Vector3::Zero(); m_dragStartRingAngle = 0.0f; + m_dragCurrentDeltaRadians = 0.0f; RefreshHandleState(); } @@ -299,8 +329,8 @@ void SceneViewportRotateGizmo::CancelDrag(IUndoManager* undoManager) { m_screenSpaceDrag = false; m_activeWorldAxis = Math::Vector3::Zero(); m_dragStartWorldRotation = Math::Quaternion::Identity(); - m_dragStartDirectionWorld = Math::Vector3::Zero(); m_dragStartRingAngle = 0.0f; + m_dragCurrentDeltaRadians = 0.0f; m_hoveredAxis = SceneViewportRotateGizmoAxis::None; RefreshHandleState(); } @@ -352,21 +382,29 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte m_drawData.visible = true; m_drawData.pivot = projectedPivot.screenPosition; + const bool hasActiveDragFeedback = + m_activeAxis != SceneViewportRotateGizmoAxis::None && + m_activeAxis != SceneViewportRotateGizmoAxis::View && + std::abs(m_dragCurrentDeltaRadians) > Math::EPSILON; + const Math::Quaternion dragFeedbackRotation = hasActiveDragFeedback + ? Math::Quaternion::FromAxisAngle(m_activeWorldAxis, m_dragCurrentDeltaRadians) + : Math::Quaternion::Identity(); for (size_t handleIndex = 0; handleIndex < m_drawData.handles.size(); ++handleIndex) { SceneViewportRotateGizmoHandleDrawData& handle = m_drawData.handles[handleIndex]; handle.axis = GetRotateAxisForIndex(handleIndex); handle.color = GetRotateAxisBaseColor(handle.axis); - const float ringRadiusWorld = worldUnitsPerPixel * - (handle.axis == SceneViewportRotateGizmoAxis::View - ? kRotateGizmoViewRadiusPixels - : kRotateGizmoAxisRadiusPixels); + const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(handle.axis); Math::Vector3 basisA = Math::Vector3::Zero(); Math::Vector3 basisB = Math::Vector3::Zero(); if (!GetRotateRingBasis(handle.axis, context.overlay, basisA, basisB)) { continue; } + if (hasActiveDragFeedback && handle.axis != SceneViewportRotateGizmoAxis::View) { + basisA = dragFeedbackRotation * basisA; + basisB = dragFeedbackRotation * basisB; + } bool anyVisibleSegment = false; for (size_t segmentIndex = 0; segmentIndex < handle.segments.size(); ++segmentIndex) { @@ -419,6 +457,52 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte handle.visible = anyVisibleSegment; } + + if (m_activeAxis != SceneViewportRotateGizmoAxis::None && + std::abs(m_dragCurrentDeltaRadians) >= kRotateGizmoAngleFillMinRadians) { + SceneViewportRotateGizmoAngleFillDrawData& angleFill = m_drawData.angleFill; + angleFill.axis = m_activeAxis; + angleFill.pivot = projectedPivot.screenPosition; + angleFill.fillColor = Math::Color(1.0f, 0.92f, 0.12f, 0.22f); + angleFill.outlineColor = Math::Color(1.0f, 0.92f, 0.12f, 0.95f); + + Math::Vector3 basisA = Math::Vector3::Zero(); + Math::Vector3 basisB = Math::Vector3::Zero(); + if (GetRotateRingBasis(m_activeAxis, context.overlay, basisA, basisB)) { + const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(m_activeAxis); + const float sweepRadians = NormalizeSignedAngleRadians(m_dragCurrentDeltaRadians); + const float sweepAbs = std::abs(sweepRadians); + const size_t stepCount = std::clamp( + static_cast(std::ceil( + sweepAbs / (Math::PI * 2.0f) * static_cast(kSceneViewportRotateGizmoSegmentCount))), + static_cast(1), + kSceneViewportRotateGizmoAngleFillPointCount - 1); + + bool valid = true; + for (size_t pointIndex = 0; pointIndex <= stepCount; ++pointIndex) { + const float t = static_cast(pointIndex) / static_cast(stepCount); + const float angle = m_dragStartRingAngle + sweepRadians * t; + const Math::Vector3 worldPoint = + pivotWorldPosition + (basisA * std::cos(angle) + basisB * std::sin(angle)) * ringRadiusWorld; + const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint( + context.overlay, + context.viewportSize.x, + context.viewportSize.y, + worldPoint); + if (projectedPoint.ndcDepth < 0.0f || projectedPoint.ndcDepth > 1.0f) { + valid = false; + break; + } + + angleFill.arcPoints[pointIndex] = projectedPoint.screenPosition; + } + + if (valid) { + angleFill.arcPointCount = stepCount + 1; + angleFill.visible = true; + } + } + } } void SceneViewportRotateGizmo::RefreshHandleState() { diff --git a/editor/src/Viewport/SceneViewportRotateGizmo.h b/editor/src/Viewport/SceneViewportRotateGizmo.h index 832518ce..1bf602ac 100644 --- a/editor/src/Viewport/SceneViewportRotateGizmo.h +++ b/editor/src/Viewport/SceneViewportRotateGizmo.h @@ -29,6 +29,7 @@ enum class SceneViewportRotateGizmoAxis : uint8_t { }; constexpr size_t kSceneViewportRotateGizmoSegmentCount = 96; +constexpr size_t kSceneViewportRotateGizmoAngleFillPointCount = kSceneViewportRotateGizmoSegmentCount + 1; struct SceneViewportRotateGizmoSegmentDrawData { Math::Vector2 start = Math::Vector2::Zero(); @@ -48,10 +49,21 @@ struct SceneViewportRotateGizmoHandleDrawData { bool active = false; }; +struct SceneViewportRotateGizmoAngleFillDrawData { + SceneViewportRotateGizmoAxis axis = SceneViewportRotateGizmoAxis::None; + Math::Vector2 pivot = Math::Vector2::Zero(); + std::array arcPoints = {}; + size_t arcPointCount = 0; + Math::Color fillColor = Math::Color::White(); + Math::Color outlineColor = Math::Color::White(); + bool visible = false; +}; + struct SceneViewportRotateGizmoDrawData { bool visible = false; Math::Vector2 pivot = Math::Vector2::Zero(); std::array handles = {}; + SceneViewportRotateGizmoAngleFillDrawData angleFill = {}; }; struct SceneViewportRotateGizmoContext { @@ -92,8 +104,8 @@ private: Math::Vector3 m_activeWorldAxis = Math::Vector3::Zero(); Math::Plane m_dragPlane = {}; Math::Quaternion m_dragStartWorldRotation = Math::Quaternion::Identity(); - Math::Vector3 m_dragStartDirectionWorld = Math::Vector3::Zero(); float m_dragStartRingAngle = 0.0f; + float m_dragCurrentDeltaRadians = 0.0f; }; } // namespace Editor diff --git a/editor/src/Viewport/SceneViewportScaleGizmo.cpp b/editor/src/Viewport/SceneViewportScaleGizmo.cpp new file mode 100644 index 00000000..f4aa8a6b --- /dev/null +++ b/editor/src/Viewport/SceneViewportScaleGizmo.cpp @@ -0,0 +1,502 @@ +#include "SceneViewportScaleGizmo.h" + +#include "Core/IUndoManager.h" +#include "SceneViewportMath.h" + +#include +#include + +#include +#include + +namespace XCEngine { +namespace Editor { + +namespace { + +constexpr float kScaleGizmoAxisLengthPixels = 110.0f; +constexpr float kScaleGizmoCapHalfSizePixels = 6.5f; +constexpr float kScaleGizmoCenterHalfSizePixels = 7.5f; +constexpr float kScaleGizmoHoverThresholdPixels = 10.0f; +constexpr float kScaleGizmoAxisScalePerPixel = 0.015f; +constexpr float kScaleGizmoUniformScalePerPixel = 0.0125f; +constexpr float kScaleGizmoMinScale = 0.001f; +constexpr float kScaleGizmoVisualScaleMin = 0.4f; +constexpr float kScaleGizmoVisualScaleMax = 2.25f; + +Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) { + return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized(); +} + +bool IsMouseInsideViewport(const SceneViewportScaleGizmoContext& context) { + return context.mousePosition.x >= 0.0f && + context.mousePosition.y >= 0.0f && + context.mousePosition.x <= context.viewportSize.x && + context.mousePosition.y <= context.viewportSize.y; +} + +Math::Color WithAlpha(const Math::Color& color, float alpha) { + return Math::Color(color.r, color.g, color.b, alpha); +} + +Math::Color GetScaleHandleBaseColor(SceneViewportScaleGizmoHandle handle) { + switch (handle) { + case SceneViewportScaleGizmoHandle::X: + return Math::Color(0.91f, 0.09f, 0.05f, 1.0f); + case SceneViewportScaleGizmoHandle::Y: + return Math::Color(0.45f, 1.0f, 0.12f, 1.0f); + case SceneViewportScaleGizmoHandle::Z: + return Math::Color(0.11f, 0.29f, 1.0f, 1.0f); + case SceneViewportScaleGizmoHandle::Uniform: + return Math::Color(0.78f, 0.78f, 0.78f, 1.0f); + case SceneViewportScaleGizmoHandle::None: + default: + return Math::Color::White(); + } +} + +SceneViewportScaleGizmoHandle GetHandleForIndex(size_t index) { + switch (index) { + case 0: + return SceneViewportScaleGizmoHandle::X; + case 1: + return SceneViewportScaleGizmoHandle::Y; + case 2: + return SceneViewportScaleGizmoHandle::Z; + default: + return SceneViewportScaleGizmoHandle::None; + } +} + +Math::Vector3 GetHandleWorldAxis( + SceneViewportScaleGizmoHandle handle, + const Components::TransformComponent& transform) { + switch (handle) { + case SceneViewportScaleGizmoHandle::X: + return NormalizeVector3(transform.GetRight(), Math::Vector3::Right()); + case SceneViewportScaleGizmoHandle::Y: + return NormalizeVector3(transform.GetUp(), Math::Vector3::Up()); + case SceneViewportScaleGizmoHandle::Z: + return NormalizeVector3(transform.GetForward(), Math::Vector3::Forward()); + case SceneViewportScaleGizmoHandle::Uniform: + case SceneViewportScaleGizmoHandle::None: + default: + return Math::Vector3::Zero(); + } +} + +float ComputeWorldUnitsPerPixel( + const SceneViewportOverlayData& overlay, + const Math::Vector3& worldPoint, + float viewportHeight) { + if (!overlay.valid || viewportHeight <= 1.0f) { + return 0.0f; + } + + const Math::Vector3 cameraForward = NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward()); + const float depth = Math::Vector3::Dot(worldPoint - overlay.cameraPosition, cameraForward); + if (depth <= Math::EPSILON) { + return 0.0f; + } + + return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / viewportHeight; +} + +bool IsPointInsideSquare( + const Math::Vector2& point, + const Math::Vector2& center, + float halfSize) { + return std::abs(point.x - center.x) <= halfSize && + std::abs(point.y - center.y) <= halfSize; +} + +float ClampPositiveScale(float value) { + return std::max(value, kScaleGizmoMinScale); +} + +float ComputeVisualScaleFactor(float current, float start) { + if (std::abs(start) <= Math::EPSILON) { + return 1.0f; + } + + return std::clamp( + current / start, + kScaleGizmoVisualScaleMin, + kScaleGizmoVisualScaleMax); +} + +} // namespace + +void SceneViewportScaleGizmo::Update(const SceneViewportScaleGizmoContext& context) { + BuildDrawData(context); + if (m_activeHandle == SceneViewportScaleGizmoHandle::None && IsMouseInsideViewport(context)) { + m_hoveredHandle = HitTestHandle(context.mousePosition); + } else if (m_activeHandle == SceneViewportScaleGizmoHandle::None) { + m_hoveredHandle = SceneViewportScaleGizmoHandle::None; + } else { + m_hoveredHandle = m_activeHandle; + } + + RefreshHandleState(); +} + +bool SceneViewportScaleGizmo::TryBeginDrag(const SceneViewportScaleGizmoContext& context, IUndoManager& undoManager) { + if (m_activeHandle != SceneViewportScaleGizmoHandle::None || + m_hoveredHandle == SceneViewportScaleGizmoHandle::None || + context.selectedObject == nullptr || + !m_drawData.visible || + undoManager.HasPendingInteractiveChange()) { + return false; + } + + Math::Vector2 activeScreenDirection = Math::Vector2::Zero(); + if (m_hoveredHandle != SceneViewportScaleGizmoHandle::Uniform) { + const SceneViewportScaleGizmoAxisHandleDrawData* handle = FindAxisHandleDrawData(m_hoveredHandle); + if (handle == nullptr || !handle->visible) { + return false; + } + + activeScreenDirection = handle->end - handle->start; + if (activeScreenDirection.SqrMagnitude() <= Math::EPSILON) { + const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition(); + if (!ProjectSceneViewportAxisDirectionAtPoint( + context.overlay, + context.viewportSize.x, + context.viewportSize.y, + pivotWorldPosition, + GetHandleWorldAxis(m_hoveredHandle, *context.selectedObject->GetTransform()), + activeScreenDirection)) { + return false; + } + } else { + activeScreenDirection = activeScreenDirection.Normalized(); + } + } + + undoManager.BeginInteractiveChange("Scale Gizmo"); + if (!undoManager.HasPendingInteractiveChange()) { + return false; + } + + m_activeHandle = m_hoveredHandle; + m_activeEntityId = context.selectedObject->GetID(); + m_dragStartLocalScale = context.selectedObject->GetTransform()->GetLocalScale(); + m_dragCurrentVisualScale = Math::Vector3::One(); + m_dragStartMousePosition = context.mousePosition; + m_activeScreenDirection = activeScreenDirection; + RefreshHandleState(); + return true; +} + +void SceneViewportScaleGizmo::UpdateDrag(const SceneViewportScaleGizmoContext& context) { + if (m_activeHandle == SceneViewportScaleGizmoHandle::None || + context.selectedObject == nullptr || + context.selectedObject->GetID() != m_activeEntityId) { + return; + } + + const Math::Vector2 mouseDelta = context.mousePosition - m_dragStartMousePosition; + Math::Vector3 localScale = m_dragStartLocalScale; + + if (m_activeHandle == SceneViewportScaleGizmoHandle::Uniform) { + const float signedPixels = mouseDelta.x - mouseDelta.y; + const float factor = std::max(1.0f + signedPixels * kScaleGizmoUniformScalePerPixel, kScaleGizmoMinScale); + localScale.x = ClampPositiveScale(m_dragStartLocalScale.x * factor); + localScale.y = ClampPositiveScale(m_dragStartLocalScale.y * factor); + localScale.z = ClampPositiveScale(m_dragStartLocalScale.z * factor); + } else { + if (m_activeScreenDirection.SqrMagnitude() <= Math::EPSILON) { + return; + } + + const float signedPixels = Math::Vector2::Dot(mouseDelta, m_activeScreenDirection); + const float factor = std::max(1.0f + signedPixels * kScaleGizmoAxisScalePerPixel, kScaleGizmoMinScale); + switch (m_activeHandle) { + case SceneViewportScaleGizmoHandle::X: + localScale.x = ClampPositiveScale(m_dragStartLocalScale.x * factor); + break; + case SceneViewportScaleGizmoHandle::Y: + localScale.y = ClampPositiveScale(m_dragStartLocalScale.y * factor); + break; + case SceneViewportScaleGizmoHandle::Z: + localScale.z = ClampPositiveScale(m_dragStartLocalScale.z * factor); + break; + case SceneViewportScaleGizmoHandle::Uniform: + case SceneViewportScaleGizmoHandle::None: + default: + break; + } + } + + context.selectedObject->GetTransform()->SetLocalScale(localScale); + switch (m_activeHandle) { + case SceneViewportScaleGizmoHandle::X: + m_dragCurrentVisualScale = Math::Vector3( + ComputeVisualScaleFactor(localScale.x, m_dragStartLocalScale.x), + 1.0f, + 1.0f); + break; + case SceneViewportScaleGizmoHandle::Y: + m_dragCurrentVisualScale = Math::Vector3( + 1.0f, + ComputeVisualScaleFactor(localScale.y, m_dragStartLocalScale.y), + 1.0f); + break; + case SceneViewportScaleGizmoHandle::Z: + m_dragCurrentVisualScale = Math::Vector3( + 1.0f, + 1.0f, + ComputeVisualScaleFactor(localScale.z, m_dragStartLocalScale.z)); + break; + case SceneViewportScaleGizmoHandle::Uniform: + m_dragCurrentVisualScale = Math::Vector3( + ComputeVisualScaleFactor(localScale.x, m_dragStartLocalScale.x), + ComputeVisualScaleFactor(localScale.y, m_dragStartLocalScale.y), + ComputeVisualScaleFactor(localScale.z, m_dragStartLocalScale.z)); + break; + case SceneViewportScaleGizmoHandle::None: + default: + m_dragCurrentVisualScale = Math::Vector3::One(); + break; + } +} + +void SceneViewportScaleGizmo::EndDrag(IUndoManager& undoManager) { + if (m_activeHandle == SceneViewportScaleGizmoHandle::None) { + return; + } + + if (undoManager.HasPendingInteractiveChange()) { + undoManager.FinalizeInteractiveChange(); + } + + m_activeHandle = SceneViewportScaleGizmoHandle::None; + m_activeEntityId = 0; + m_dragStartLocalScale = Math::Vector3::Zero(); + m_dragCurrentVisualScale = Math::Vector3::One(); + m_dragStartMousePosition = Math::Vector2::Zero(); + m_activeScreenDirection = Math::Vector2::Zero(); + RefreshHandleState(); +} + +void SceneViewportScaleGizmo::CancelDrag(IUndoManager* undoManager) { + if (undoManager != nullptr && undoManager->HasPendingInteractiveChange()) { + undoManager->CancelInteractiveChange(); + } + + m_hoveredHandle = SceneViewportScaleGizmoHandle::None; + m_activeHandle = SceneViewportScaleGizmoHandle::None; + m_activeEntityId = 0; + m_dragStartLocalScale = Math::Vector3::Zero(); + m_dragCurrentVisualScale = Math::Vector3::One(); + m_dragStartMousePosition = Math::Vector2::Zero(); + m_activeScreenDirection = Math::Vector2::Zero(); + RefreshHandleState(); +} + +bool SceneViewportScaleGizmo::IsHoveringHandle() const { + return m_hoveredHandle != SceneViewportScaleGizmoHandle::None; +} + +bool SceneViewportScaleGizmo::IsActive() const { + return m_activeHandle != SceneViewportScaleGizmoHandle::None; +} + +uint64_t SceneViewportScaleGizmo::GetActiveEntityId() const { + return m_activeEntityId; +} + +const SceneViewportScaleGizmoDrawData& SceneViewportScaleGizmo::GetDrawData() const { + return m_drawData; +} + +void SceneViewportScaleGizmo::BuildDrawData(const SceneViewportScaleGizmoContext& context) { + m_drawData = {}; + + const Components::GameObject* selectedObject = context.selectedObject; + if (selectedObject == nullptr || + !context.overlay.valid || + context.viewportSize.x <= 1.0f || + context.viewportSize.y <= 1.0f) { + return; + } + + const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition(); + const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint( + context.overlay, + context.viewportSize.x, + context.viewportSize.y, + pivotWorldPosition); + if (!projectedPivot.visible) { + return; + } + + const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel( + context.overlay, + pivotWorldPosition, + context.viewportSize.y); + if (worldUnitsPerPixel <= Math::EPSILON) { + return; + } + + m_drawData.visible = true; + m_drawData.centerHandle.visible = true; + m_drawData.centerHandle.center = projectedPivot.screenPosition; + m_drawData.centerHandle.halfSize = kScaleGizmoCenterHalfSizePixels; + + if (context.uniformOnly) { + return; + } + + const bool hasVisualDragFeedback = + m_activeHandle != SceneViewportScaleGizmoHandle::None && + selectedObject->GetID() == m_activeEntityId; + for (size_t index = 0; index < m_drawData.axisHandles.size(); ++index) { + SceneViewportScaleGizmoAxisHandleDrawData& handle = m_drawData.axisHandles[index]; + handle.handle = GetHandleForIndex(index); + handle.start = projectedPivot.screenPosition; + handle.capHalfSize = kScaleGizmoCapHalfSizePixels; + handle.color = GetScaleHandleBaseColor(handle.handle); + + const Math::Vector3 axisWorld = + GetHandleWorldAxis(handle.handle, *selectedObject->GetTransform()); + if (axisWorld.SqrMagnitude() <= Math::EPSILON) { + continue; + } + + float axisVisualScale = 1.0f; + if (hasVisualDragFeedback) { + switch (handle.handle) { + case SceneViewportScaleGizmoHandle::X: + axisVisualScale = m_dragCurrentVisualScale.x; + break; + case SceneViewportScaleGizmoHandle::Y: + axisVisualScale = m_dragCurrentVisualScale.y; + break; + case SceneViewportScaleGizmoHandle::Z: + axisVisualScale = m_dragCurrentVisualScale.z; + break; + case SceneViewportScaleGizmoHandle::Uniform: + case SceneViewportScaleGizmoHandle::None: + default: + break; + } + } + const float axisLengthWorld = + worldUnitsPerPixel * kScaleGizmoAxisLengthPixels * axisVisualScale; + + const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint( + context.overlay, + context.viewportSize.x, + context.viewportSize.y, + pivotWorldPosition + axisWorld * axisLengthWorld); + if (projectedEnd.ndcDepth < 0.0f || projectedEnd.ndcDepth > 1.0f) { + continue; + } + + if ((projectedEnd.screenPosition - projectedPivot.screenPosition).SqrMagnitude() <= Math::EPSILON) { + continue; + } + + handle.visible = true; + handle.end = projectedEnd.screenPosition; + handle.capCenter = projectedEnd.screenPosition; + } +} + +void SceneViewportScaleGizmo::RefreshHandleState() { + for (SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) { + if (!handle.visible) { + continue; + } + + handle.hovered = handle.handle == m_hoveredHandle; + handle.active = handle.handle == m_activeHandle; + handle.color = (handle.hovered || handle.active) + ? Math::Color::Yellow() + : GetScaleHandleBaseColor(handle.handle); + } + + if (!m_drawData.centerHandle.visible) { + return; + } + + m_drawData.centerHandle.hovered = m_hoveredHandle == SceneViewportScaleGizmoHandle::Uniform; + m_drawData.centerHandle.active = m_activeHandle == SceneViewportScaleGizmoHandle::Uniform; + const Math::Color baseColor = + (m_drawData.centerHandle.hovered || m_drawData.centerHandle.active) + ? Math::Color::Yellow() + : GetScaleHandleBaseColor(SceneViewportScaleGizmoHandle::Uniform); + m_drawData.centerHandle.fillColor = WithAlpha( + baseColor, + m_drawData.centerHandle.active ? 0.96f : (m_drawData.centerHandle.hovered ? 0.9f : 0.82f)); + m_drawData.centerHandle.outlineColor = WithAlpha( + baseColor, + m_drawData.centerHandle.active ? 1.0f : (m_drawData.centerHandle.hovered ? 0.96f : 0.88f)); +} + +SceneViewportScaleGizmoHandle SceneViewportScaleGizmo::HitTestHandle(const Math::Vector2& mousePosition) const { + if (!m_drawData.visible) { + return SceneViewportScaleGizmoHandle::None; + } + + if (m_drawData.centerHandle.visible && + IsPointInsideSquare( + mousePosition, + m_drawData.centerHandle.center, + m_drawData.centerHandle.halfSize + 2.0f)) { + return SceneViewportScaleGizmoHandle::Uniform; + } + + SceneViewportScaleGizmoHandle bestHandle = SceneViewportScaleGizmoHandle::None; + float bestDistanceSq = Math::FLOAT_MAX; + for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) { + if (!handle.visible || + !IsPointInsideSquare(mousePosition, handle.capCenter, handle.capHalfSize + 2.0f)) { + continue; + } + + const float distanceSq = (handle.capCenter - mousePosition).SqrMagnitude(); + if (distanceSq >= bestDistanceSq) { + continue; + } + + bestDistanceSq = distanceSq; + bestHandle = handle.handle; + } + + if (bestHandle != SceneViewportScaleGizmoHandle::None) { + return bestHandle; + } + + bestDistanceSq = kScaleGizmoHoverThresholdPixels * kScaleGizmoHoverThresholdPixels; + for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) { + if (!handle.visible) { + continue; + } + + const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end); + if (distanceSq > bestDistanceSq) { + continue; + } + + bestDistanceSq = distanceSq; + bestHandle = handle.handle; + } + + return bestHandle; +} + +const SceneViewportScaleGizmoAxisHandleDrawData* SceneViewportScaleGizmo::FindAxisHandleDrawData( + SceneViewportScaleGizmoHandle handle) const { + for (const SceneViewportScaleGizmoAxisHandleDrawData& drawHandle : m_drawData.axisHandles) { + if (drawHandle.handle == handle) { + return &drawHandle; + } + } + + return nullptr; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportScaleGizmo.h b/editor/src/Viewport/SceneViewportScaleGizmo.h new file mode 100644 index 00000000..213a96d9 --- /dev/null +++ b/editor/src/Viewport/SceneViewportScaleGizmo.h @@ -0,0 +1,96 @@ +#pragma once + +#include "IViewportHostService.h" + +#include +#include +#include + +#include +#include + +namespace XCEngine { +namespace Components { +class GameObject; +} // namespace Components + +namespace Editor { + +class IUndoManager; + +enum class SceneViewportScaleGizmoHandle : uint8_t { + None = 0, + X, + Y, + Z, + Uniform +}; + +struct SceneViewportScaleGizmoAxisHandleDrawData { + SceneViewportScaleGizmoHandle handle = SceneViewportScaleGizmoHandle::None; + Math::Vector2 start = Math::Vector2::Zero(); + Math::Vector2 end = Math::Vector2::Zero(); + Math::Vector2 capCenter = Math::Vector2::Zero(); + float capHalfSize = 0.0f; + Math::Color color = Math::Color::White(); + bool visible = false; + bool hovered = false; + bool active = false; +}; + +struct SceneViewportScaleGizmoCenterHandleDrawData { + Math::Vector2 center = Math::Vector2::Zero(); + float halfSize = 0.0f; + Math::Color fillColor = Math::Color::White(); + Math::Color outlineColor = Math::Color::White(); + bool visible = false; + bool hovered = false; + bool active = false; +}; + +struct SceneViewportScaleGizmoDrawData { + bool visible = false; + std::array axisHandles = {}; + SceneViewportScaleGizmoCenterHandleDrawData centerHandle = {}; +}; + +struct SceneViewportScaleGizmoContext { + SceneViewportOverlayData overlay = {}; + Math::Vector2 viewportSize = Math::Vector2::Zero(); + Math::Vector2 mousePosition = Math::Vector2::Zero(); + Components::GameObject* selectedObject = nullptr; + bool uniformOnly = false; +}; + +class SceneViewportScaleGizmo { +public: + void Update(const SceneViewportScaleGizmoContext& context); + bool TryBeginDrag(const SceneViewportScaleGizmoContext& context, IUndoManager& undoManager); + void UpdateDrag(const SceneViewportScaleGizmoContext& context); + void EndDrag(IUndoManager& undoManager); + void CancelDrag(IUndoManager* undoManager = nullptr); + + bool IsHoveringHandle() const; + bool IsActive() const; + uint64_t GetActiveEntityId() const; + const SceneViewportScaleGizmoDrawData& GetDrawData() const; + +private: + void BuildDrawData(const SceneViewportScaleGizmoContext& context); + void RefreshHandleState(); + SceneViewportScaleGizmoHandle HitTestHandle(const Math::Vector2& mousePosition) const; + const SceneViewportScaleGizmoAxisHandleDrawData* FindAxisHandleDrawData( + SceneViewportScaleGizmoHandle handle) const; + + SceneViewportScaleGizmoDrawData m_drawData = {}; + SceneViewportScaleGizmoHandle m_hoveredHandle = SceneViewportScaleGizmoHandle::None; + SceneViewportScaleGizmoHandle m_activeHandle = SceneViewportScaleGizmoHandle::None; + uint64_t m_activeEntityId = 0; + Math::Vector3 m_dragStartLocalScale = Math::Vector3::Zero(); + Math::Vector3 m_dragCurrentVisualScale = Math::Vector3::One(); + Math::Vector2 m_dragStartMousePosition = Math::Vector2::Zero(); + Math::Vector2 m_activeScreenDirection = Math::Vector2::Zero(); +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index 16aca034..dae0b540 100644 --- a/editor/src/panels/SceneViewPanel.cpp +++ b/editor/src/panels/SceneViewPanel.cpp @@ -6,15 +6,205 @@ #include "Viewport/SceneViewportOrientationGizmo.h" #include "Viewport/SceneViewportOverlayRenderer.h" #include "ViewportPanelContent.h" +#include "Platform/Win32Utf8.h" #include "UI/UI.h" +#include + #include +#include +#include +#include + namespace XCEngine { namespace Editor { namespace { +struct SceneViewportToolOverlayResult { + bool hovered = false; + bool clicked = false; + SceneViewportToolMode clickedTool = SceneViewportToolMode::Move; +}; + +enum class SceneViewportActiveGizmoKind : uint8_t { + None = 0, + Move, + Rotate, + Scale +}; + +const char* GetSceneViewportToolTooltip(SceneViewportToolMode toolMode) { + switch (toolMode) { + case SceneViewportToolMode::ViewMove: + return "View Move"; + case SceneViewportToolMode::Move: + return "Move"; + case SceneViewportToolMode::Rotate: + return "Rotate"; + case SceneViewportToolMode::Scale: + return "Scale"; + case SceneViewportToolMode::Transform: + return "Transform"; + default: + return ""; + } +} + +const char* GetSceneViewportToolIconBaseName(SceneViewportToolMode toolMode) { + switch (toolMode) { + case SceneViewportToolMode::ViewMove: + return "view_move_tool"; + case SceneViewportToolMode::Move: + return "move_tool"; + case SceneViewportToolMode::Rotate: + return "rotate_tool"; + case SceneViewportToolMode::Scale: + return "scale_tool"; + case SceneViewportToolMode::Transform: + return "transform_tool"; + default: + return ""; + } +} + +const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode, bool active) { + static std::string cachedPaths[5][2] = {}; + const size_t toolIndex = static_cast(toolMode); + const size_t stateIndex = active ? 1u : 0u; + std::string& cachedPath = cachedPaths[toolIndex][stateIndex]; + if (!cachedPath.empty()) { + return cachedPath; + } + + const std::filesystem::path exeDir( + XCEngine::Editor::Platform::Utf8ToWide(XCEngine::Editor::Platform::GetExecutableDirectoryUtf8())); + std::filesystem::path iconPath = + (exeDir / L".." / L".." / L"resources" / L"Icons" / + std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(GetSceneViewportToolIconBaseName(toolMode)))); + if (active) { + iconPath += L"_on"; + } + iconPath += L".png"; + cachedPath = XCEngine::Editor::Platform::WideToUtf8(iconPath.lexically_normal().wstring()); + return cachedPath; +} + +SceneViewportToolOverlayResult RenderSceneViewportToolOverlay( + const ViewportPanelContentResult& content, + SceneViewportToolMode activeTool) { + SceneViewportToolOverlayResult result = {}; + if (!content.hasViewportArea) { + return result; + } + + constexpr float kButtonExtent = 30.0f; + constexpr float kButtonSpacing = 6.0f; + constexpr float kPanelPadding = 6.0f; + constexpr float kViewportInset = 10.0f; + constexpr float kIconInset = 4.0f; + constexpr size_t kToolCount = 5; + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + if (drawList == nullptr) { + return result; + } + + const ImVec2 panelMin( + content.itemMin.x + kViewportInset, + content.itemMin.y + kViewportInset); + const ImVec2 panelMax( + panelMin.x + kPanelPadding * 2.0f + kButtonExtent, + panelMin.y + kPanelPadding * 2.0f + kToolCount * kButtonExtent + (kToolCount - 1) * kButtonSpacing); + drawList->AddRectFilled(panelMin, panelMax, IM_COL32(24, 26, 29, 220), 7.0f); + drawList->AddRect(panelMin, panelMax, IM_COL32(255, 255, 255, 28), 7.0f, 0, 1.0f); + + const SceneViewportToolMode toolModes[kToolCount] = { + SceneViewportToolMode::ViewMove, + SceneViewportToolMode::Move, + SceneViewportToolMode::Rotate, + SceneViewportToolMode::Scale, + SceneViewportToolMode::Transform + }; + + for (size_t index = 0; index < kToolCount; ++index) { + const SceneViewportToolMode toolMode = toolModes[index]; + const bool active = toolMode == activeTool; + const ImVec2 buttonMin( + panelMin.x + kPanelPadding, + panelMin.y + kPanelPadding + index * (kButtonExtent + kButtonSpacing)); + const ImVec2 buttonMax(buttonMin.x + kButtonExtent, buttonMin.y + kButtonExtent); + + ImGui::SetCursorScreenPos(buttonMin); + const std::string buttonId = std::string("##SceneToolButton") + std::to_string(static_cast(toolMode)); + const bool clicked = ImGui::InvisibleButton( + buttonId.c_str(), + ImVec2(kButtonExtent, kButtonExtent), + ImGuiButtonFlags_MouseButtonLeft | + ImGuiButtonFlags_PressedOnClick | + ImGuiButtonFlags_AllowOverlap); + + const bool hovered = ImGui::IsItemHovered(); + const bool held = ImGui::IsItemActive(); + result.hovered = result.hovered || hovered; + result.clicked = result.clicked || clicked; + if (clicked) { + result.clickedTool = toolMode; + } + + const ImU32 backgroundColor = ImGui::GetColorU32( + held ? UI::ToolbarButtonActiveColor() + : hovered ? UI::ToolbarButtonHoveredColor(active) + : UI::ToolbarButtonColor(active)); + drawList->AddRectFilled(buttonMin, buttonMax, backgroundColor, 5.0f); + drawList->AddRect(buttonMin, buttonMax, IM_COL32(255, 255, 255, active ? 48 : 24), 5.0f, 0, 1.0f); + + const ImVec2 iconMin(buttonMin.x + kIconInset, buttonMin.y + kIconInset); + const ImVec2 iconMax(buttonMax.x - kIconInset, buttonMax.y - kIconInset); + if (!UI::DrawTextureAssetPreview( + drawList, + iconMin, + iconMax, + GetSceneViewportToolIconPath(toolMode, active))) { + drawList->AddText( + ImVec2(buttonMin.x + 8.0f, buttonMin.y + 7.0f), + IM_COL32(230, 230, 230, 255), + GetSceneViewportToolTooltip(toolMode)); + } + + if (hovered) { + ImGui::SetTooltip("%s", GetSceneViewportToolTooltip(toolMode)); + } + } + + return result; +} + +void LogSceneViewNavigation(Debug::Logger& logger, const char* format, ...) { + char buffer[512] = {}; + va_list args; + va_start(args, format); + std::vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + logger.Info(Debug::LogCategory::General, buffer); +} + +bool ShouldBeginSceneViewportNavigationDrag( + bool hasInteractiveViewport, + bool hovered, + bool activeDrag, + bool otherDrag, + bool gizmoActive, + ImGuiMouseButton button) { + return hasInteractiveViewport && + hovered && + !activeDrag && + !otherDrag && + !gizmoActive && + ImGui::IsMouseClicked(button); +} + SceneViewportMoveGizmoContext BuildMoveGizmoContext( IEditorContext& context, const SceneViewportOverlayData& overlay, @@ -59,6 +249,28 @@ SceneViewportRotateGizmoContext BuildRotateGizmoContext( return gizmoContext; } +SceneViewportScaleGizmoContext BuildScaleGizmoContext( + IEditorContext& context, + const SceneViewportOverlayData& overlay, + const ViewportPanelContentResult& content, + const ImVec2& mousePosition) { + SceneViewportScaleGizmoContext gizmoContext = {}; + gizmoContext.overlay = overlay; + gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y); + gizmoContext.mousePosition = Math::Vector2( + mousePosition.x - content.itemMin.x, + mousePosition.y - content.itemMin.y); + + if (context.GetSelectionManager().GetSelectionCount() == 1) { + const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity(); + if (selectedEntity != 0) { + gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity); + } + } + + return gizmoContext; +} + } // namespace SceneViewPanel::SceneViewPanel() : Panel("Scene") {} @@ -74,53 +286,130 @@ void SceneViewPanel::Render() { const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene); if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) { const ImGuiIO& io = ImGui::GetIO(); + auto& logger = Debug::Logger::Get(); const bool hasInteractiveViewport = content.hasViewportArea && content.frame.hasTexture; + const SceneViewportToolOverlayResult toolOverlay = RenderSceneViewportToolOverlay(content, m_toolMode); + const bool viewportContentHovered = content.hovered && !toolOverlay.hovered; + + if (toolOverlay.clicked) { + if (m_moveGizmo.IsActive()) { + m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); + } + if (m_rotateGizmo.IsActive()) { + m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); + } + if (m_scaleGizmo.IsActive()) { + m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); + } + m_toolMode = toolOverlay.clickedTool; + } if (content.focused && !io.WantTextInput && !m_lookDragging && !m_panDragging) { if (ImGui::IsKeyPressed(ImGuiKey_W, false)) { if (m_rotateGizmo.IsActive()) { m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); } - m_transformTool = SceneViewportTransformTool::Move; + if (m_scaleGizmo.IsActive()) { + m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); + } + m_toolMode = SceneViewportToolMode::Move; } else if (ImGui::IsKeyPressed(ImGuiKey_E, false)) { if (m_moveGizmo.IsActive()) { m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); } - m_transformTool = SceneViewportTransformTool::Rotate; - } - } - - const bool usingMoveGizmo = m_transformTool == SceneViewportTransformTool::Move; - SceneViewportOverlayData overlay = {}; - SceneViewportMoveGizmoContext moveGizmoContext = {}; - SceneViewportRotateGizmoContext rotateGizmoContext = {}; - - if (hasInteractiveViewport) { - overlay = viewportHostService->GetSceneViewOverlayData(); - if (usingMoveGizmo) { + if (m_scaleGizmo.IsActive()) { + m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); + } + m_toolMode = SceneViewportToolMode::Rotate; + } else if (ImGui::IsKeyPressed(ImGuiKey_R, false)) { + if (m_moveGizmo.IsActive()) { + m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); + } if (m_rotateGizmo.IsActive()) { m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); } + m_toolMode = SceneViewportToolMode::Scale; + } + } + const bool usingViewMoveTool = m_toolMode == SceneViewportToolMode::ViewMove; + const bool usingTransformTool = m_toolMode == SceneViewportToolMode::Transform; + const bool showingMoveGizmo = m_toolMode == SceneViewportToolMode::Move || usingTransformTool; + const bool showingRotateGizmo = m_toolMode == SceneViewportToolMode::Rotate || usingTransformTool; + const bool showingScaleGizmo = m_toolMode == SceneViewportToolMode::Scale || usingTransformTool; + SceneViewportOverlayData overlay = {}; + SceneViewportMoveGizmoContext moveGizmoContext = {}; + SceneViewportRotateGizmoContext rotateGizmoContext = {}; + SceneViewportScaleGizmoContext scaleGizmoContext = {}; + SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None; + + if (hasInteractiveViewport) { + overlay = viewportHostService->GetSceneViewOverlayData(); + if (showingMoveGizmo) { moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos); if (m_moveGizmo.IsActive() && (moveGizmoContext.selectedObject == nullptr || m_context->GetSelectionManager().GetSelectedEntity() != m_moveGizmo.GetActiveEntityId())) { m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); } - m_moveGizmo.Update(moveGizmoContext); - } else { - if (m_moveGizmo.IsActive()) { - m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); - } + } else if (m_moveGizmo.IsActive()) { + m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); + } + if (showingRotateGizmo) { rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos); if (m_rotateGizmo.IsActive() && (rotateGizmoContext.selectedObject == nullptr || m_context->GetSelectionManager().GetSelectedEntity() != m_rotateGizmo.GetActiveEntityId())) { m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); } - m_rotateGizmo.Update(rotateGizmoContext); + } else if (m_rotateGizmo.IsActive()) { + m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); + } + + if (showingScaleGizmo) { + scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos); + scaleGizmoContext.uniformOnly = usingTransformTool; + if (m_scaleGizmo.IsActive() && + (scaleGizmoContext.selectedObject == nullptr || + m_context->GetSelectionManager().GetSelectedEntity() != m_scaleGizmo.GetActiveEntityId())) { + m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); + } + } else if (m_scaleGizmo.IsActive()) { + m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); + } + + if (m_moveGizmo.IsActive()) { + activeGizmoKind = SceneViewportActiveGizmoKind::Move; + } else if (m_rotateGizmo.IsActive()) { + activeGizmoKind = SceneViewportActiveGizmoKind::Rotate; + } else if (m_scaleGizmo.IsActive()) { + activeGizmoKind = SceneViewportActiveGizmoKind::Scale; + } + + if (showingMoveGizmo) { + SceneViewportMoveGizmoContext updateContext = moveGizmoContext; + if (activeGizmoKind != SceneViewportActiveGizmoKind::None && + activeGizmoKind != SceneViewportActiveGizmoKind::Move) { + updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); + } + m_moveGizmo.Update(updateContext); + } + if (showingRotateGizmo) { + SceneViewportRotateGizmoContext updateContext = rotateGizmoContext; + if (activeGizmoKind != SceneViewportActiveGizmoKind::None && + activeGizmoKind != SceneViewportActiveGizmoKind::Rotate) { + updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); + } + m_rotateGizmo.Update(updateContext); + } + if (showingScaleGizmo) { + SceneViewportScaleGizmoContext updateContext = scaleGizmoContext; + if (activeGizmoKind != SceneViewportActiveGizmoKind::None && + activeGizmoKind != SceneViewportActiveGizmoKind::Scale) { + updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); + } + m_scaleGizmo.Update(updateContext); } } else { if (m_moveGizmo.IsActive()) { @@ -129,20 +418,45 @@ void SceneViewPanel::Render() { if (m_rotateGizmo.IsActive()) { m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); } + if (m_scaleGizmo.IsActive()) { + m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); + } } - const bool gizmoHovering = usingMoveGizmo ? m_moveGizmo.IsHoveringHandle() : m_rotateGizmo.IsHoveringHandle(); - const bool gizmoActive = usingMoveGizmo ? m_moveGizmo.IsActive() : m_rotateGizmo.IsActive(); + const bool moveGizmoHovering = showingMoveGizmo && m_moveGizmo.IsHoveringHandle(); + const bool rotateGizmoHovering = showingRotateGizmo && m_rotateGizmo.IsHoveringHandle(); + const bool scaleGizmoHovering = showingScaleGizmo && m_scaleGizmo.IsHoveringHandle(); + const bool moveGizmoActive = showingMoveGizmo && m_moveGizmo.IsActive(); + const bool rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive(); + const bool scaleGizmoActive = showingScaleGizmo && m_scaleGizmo.IsActive(); + const SceneViewportActiveGizmoKind hoveredGizmoKind = scaleGizmoHovering + ? SceneViewportActiveGizmoKind::Scale + : (moveGizmoHovering ? SceneViewportActiveGizmoKind::Move + : (rotateGizmoHovering ? SceneViewportActiveGizmoKind::Rotate + : SceneViewportActiveGizmoKind::None)); + if (moveGizmoActive) { + activeGizmoKind = SceneViewportActiveGizmoKind::Move; + } else if (rotateGizmoActive) { + activeGizmoKind = SceneViewportActiveGizmoKind::Rotate; + } else if (scaleGizmoActive) { + activeGizmoKind = SceneViewportActiveGizmoKind::Scale; + } else { + activeGizmoKind = SceneViewportActiveGizmoKind::None; + } + const bool gizmoHovering = hoveredGizmoKind != SceneViewportActiveGizmoKind::None; + const bool gizmoActive = activeGizmoKind != SceneViewportActiveGizmoKind::None; const bool beginTransformGizmo = hasInteractiveViewport && content.clickedLeft && !m_lookDragging && !m_panDragging && + !toolOverlay.hovered && gizmoHovering; const SceneViewportOrientationAxis orientationAxisHit = hasInteractiveViewport && - content.hovered && + viewportContentHovered && + !usingViewMoveTool && !m_lookDragging && !m_panDragging && !gizmoHovering && @@ -160,33 +474,68 @@ void SceneViewPanel::Render() { const bool selectClick = hasInteractiveViewport && content.clickedLeft && + viewportContentHovered && + !usingViewMoveTool && !m_lookDragging && !m_panDragging && !orientationGizmoClick && !gizmoHovering && !gizmoActive; - const bool beginLookDrag = - hasInteractiveViewport && - content.hovered && - !m_lookDragging && - !gizmoActive && - ImGui::IsMouseClicked(ImGuiMouseButton_Right); - const bool beginPanDrag = - hasInteractiveViewport && - content.hovered && - !m_panDragging && - !gizmoActive && - !m_lookDragging && - ImGui::IsMouseClicked(ImGuiMouseButton_Middle); + const bool beginLeftPanDrag = usingViewMoveTool + ? ShouldBeginSceneViewportNavigationDrag( + hasInteractiveViewport, + viewportContentHovered, + m_panDragging, + m_lookDragging, + gizmoActive, + ImGuiMouseButton_Left) + : false; + const bool beginLookDrag = ShouldBeginSceneViewportNavigationDrag( + hasInteractiveViewport, + viewportContentHovered, + m_lookDragging, + m_panDragging, + gizmoActive, + ImGuiMouseButton_Right); + const bool beginMiddlePanDrag = ShouldBeginSceneViewportNavigationDrag( + hasInteractiveViewport, + viewportContentHovered, + m_panDragging, + m_lookDragging, + gizmoActive, + ImGuiMouseButton_Middle); + const bool beginPanDrag = beginLeftPanDrag || beginMiddlePanDrag; - if (beginTransformGizmo || orientationGizmoClick || selectClick || beginLookDrag || beginPanDrag) { + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) || ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) { + LogSceneViewNavigation( + logger, + "SceneView nav click hovered=%d focused=%d hasViewport=%d clickedR=%d clickedM=%d downR=%d downM=%d " + "look=%d pan=%d gizmoActive=%d ioDelta=(%.2f, %.2f)", + content.hovered ? 1 : 0, + content.focused ? 1 : 0, + hasInteractiveViewport ? 1 : 0, + ImGui::IsMouseClicked(ImGuiMouseButton_Right) ? 1 : 0, + ImGui::IsMouseClicked(ImGuiMouseButton_Middle) ? 1 : 0, + ImGui::IsMouseDown(ImGuiMouseButton_Right) ? 1 : 0, + ImGui::IsMouseDown(ImGuiMouseButton_Middle) ? 1 : 0, + m_lookDragging ? 1 : 0, + m_panDragging ? 1 : 0, + gizmoActive ? 1 : 0, + io.MouseDelta.x, + io.MouseDelta.y); + } + + if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || selectClick || beginLookDrag || + beginPanDrag) { ImGui::SetWindowFocus(); } if (beginTransformGizmo) { - if (usingMoveGizmo) { + if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) { + m_scaleGizmo.TryBeginDrag(scaleGizmoContext, m_context->GetUndoManager()); + } else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) { m_moveGizmo.TryBeginDrag(moveGizmoContext, m_context->GetUndoManager()); - } else { + } else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) { m_rotateGizmo.TryBeginDrag(rotateGizmoContext, m_context->GetUndoManager()); } } @@ -194,13 +543,19 @@ void SceneViewPanel::Render() { if (orientationGizmoClick) { viewportHostService->AlignSceneViewToOrientationAxis(orientationAxisHit); overlay = viewportHostService->GetSceneViewOverlayData(); - if (usingMoveGizmo) { + if (showingMoveGizmo) { moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos); m_moveGizmo.Update(moveGizmoContext); - } else { + } + if (showingRotateGizmo) { rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos); m_rotateGizmo.Update(rotateGizmoContext); } + if (showingScaleGizmo) { + scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos); + scaleGizmoContext.uniformOnly = usingTransformTool; + m_scaleGizmo.Update(scaleGizmoContext); + } } if (selectClick) { @@ -220,16 +575,20 @@ void SceneViewPanel::Render() { if (gizmoActive) { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { - if (usingMoveGizmo) { + if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) { m_moveGizmo.UpdateDrag(moveGizmoContext); - } else { + } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) { m_rotateGizmo.UpdateDrag(rotateGizmoContext); + } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) { + m_scaleGizmo.UpdateDrag(scaleGizmoContext); } } else { - if (usingMoveGizmo) { + if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) { m_moveGizmo.EndDrag(m_context->GetUndoManager()); - } else { + } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) { m_rotateGizmo.EndDrag(m_context->GetUndoManager()); + } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) { + m_scaleGizmo.EndDrag(m_context->GetUndoManager()); } } } @@ -237,26 +596,49 @@ void SceneViewPanel::Render() { if (beginLookDrag) { m_lookDragging = true; m_panDragging = false; - m_lastLookDragDelta = ImVec2(0.0f, 0.0f); - m_lastPanDragDelta = ImVec2(0.0f, 0.0f); + m_loggedLookDelta = false; + m_loggedPanDelta = false; + LogSceneViewNavigation( + logger, + "SceneView begin look drag hovered=%d focused=%d downR=%d downM=%d gizmoActive=%d", + content.hovered ? 1 : 0, + content.focused ? 1 : 0, + ImGui::IsMouseDown(ImGuiMouseButton_Right) ? 1 : 0, + ImGui::IsMouseDown(ImGuiMouseButton_Middle) ? 1 : 0, + gizmoActive ? 1 : 0); } if (beginPanDrag) { m_panDragging = true; m_lookDragging = false; - m_lastPanDragDelta = ImVec2(0.0f, 0.0f); - m_lastLookDragDelta = ImVec2(0.0f, 0.0f); + m_panDragButton = beginLeftPanDrag ? ImGuiMouseButton_Left : ImGuiMouseButton_Middle; + m_loggedPanDelta = false; + m_loggedLookDelta = false; + LogSceneViewNavigation( + logger, + "SceneView begin pan drag hovered=%d focused=%d button=%d downR=%d downM=%d downL=%d gizmoActive=%d", + content.hovered ? 1 : 0, + content.focused ? 1 : 0, + m_panDragButton, + ImGui::IsMouseDown(ImGuiMouseButton_Right) ? 1 : 0, + ImGui::IsMouseDown(ImGuiMouseButton_Middle) ? 1 : 0, + ImGui::IsMouseDown(ImGuiMouseButton_Left) ? 1 : 0, + gizmoActive ? 1 : 0); } if (m_lookDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) { + LogSceneViewNavigation(logger, "SceneView end look drag"); m_lookDragging = false; - m_lastLookDragDelta = ImVec2(0.0f, 0.0f); + m_loggedLookDelta = false; } - if (m_panDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Middle)) { + if (m_panDragging && !ImGui::IsMouseDown(m_panDragButton)) { + LogSceneViewNavigation(logger, "SceneView end pan drag"); m_panDragging = false; - m_lastPanDragDelta = ImVec2(0.0f, 0.0f); + m_panDragButton = ImGuiMouseButton_Middle; + m_loggedPanDelta = false; } - if (m_lookDragging || m_panDragging || m_moveGizmo.IsActive() || m_rotateGizmo.IsActive()) { + if (m_lookDragging || m_panDragging || m_moveGizmo.IsActive() || m_rotateGizmo.IsActive() || + m_scaleGizmo.IsActive()) { ImGui::SetNextFrameWantCaptureMouse(true); } if (m_lookDragging) { @@ -266,10 +648,10 @@ void SceneViewPanel::Render() { SceneViewportInput input = {}; input.viewportSize = content.availableSize; input.deltaTime = io.DeltaTime; - input.hovered = content.hovered; + input.hovered = viewportContentHovered; input.focused = content.focused || m_lookDragging || m_panDragging; - input.mouseWheel = (content.hovered && !m_lookDragging) ? io.MouseWheel : 0.0f; - input.flySpeedDelta = (content.hovered && m_lookDragging) ? io.MouseWheel : 0.0f; + input.mouseWheel = (viewportContentHovered && !m_lookDragging) ? io.MouseWheel : 0.0f; + input.flySpeedDelta = (viewportContentHovered && m_lookDragging) ? io.MouseWheel : 0.0f; input.looking = m_lookDragging; input.orbiting = false; input.panning = m_panDragging; @@ -291,29 +673,33 @@ void SceneViewPanel::Render() { if (m_lookDragging || m_panDragging) { if (m_lookDragging) { - const ImVec2 lookDragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Right, 0.0f); - input.mouseDelta.x += lookDragDelta.x - m_lastLookDragDelta.x; - input.mouseDelta.y += lookDragDelta.y - m_lastLookDragDelta.y; - m_lastLookDragDelta = lookDragDelta; - } else { - m_lastLookDragDelta = ImVec2(0.0f, 0.0f); + input.mouseDelta = io.MouseDelta; + if (!m_loggedLookDelta && + (input.mouseDelta.x != 0.0f || input.mouseDelta.y != 0.0f)) { + LogSceneViewNavigation( + logger, + "SceneView look delta=(%.2f, %.2f) hovered=%d downR=%d", + input.mouseDelta.x, + input.mouseDelta.y, + content.hovered ? 1 : 0, + ImGui::IsMouseDown(ImGuiMouseButton_Right) ? 1 : 0); + m_loggedLookDelta = true; + } } if (m_panDragging) { - const ImVec2 panDragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Middle, 0.0f); - ImVec2 framePanDelta( - panDragDelta.x - m_lastPanDragDelta.x, - panDragDelta.y - m_lastPanDragDelta.y); - // Some middle-button drags report a zero drag delta on the interaction surface. - if ((framePanDelta.x == 0.0f && framePanDelta.y == 0.0f) && - (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)) { - framePanDelta = io.MouseDelta; + input.mouseDelta = io.MouseDelta; + if (!m_loggedPanDelta && + (input.mouseDelta.x != 0.0f || input.mouseDelta.y != 0.0f)) { + LogSceneViewNavigation( + logger, + "SceneView pan delta=(%.2f, %.2f) hovered=%d downM=%d", + input.mouseDelta.x, + input.mouseDelta.y, + content.hovered ? 1 : 0, + ImGui::IsMouseDown(ImGuiMouseButton_Middle) ? 1 : 0); + m_loggedPanDelta = true; } - input.mouseDelta.x += framePanDelta.x; - input.mouseDelta.y += framePanDelta.y; - m_lastPanDragDelta = panDragDelta; - } else { - m_lastPanDragDelta = ImVec2(0.0f, 0.0f); } } @@ -321,12 +707,41 @@ void SceneViewPanel::Render() { if (content.hasViewportArea && content.frame.hasTexture) { overlay = viewportHostService->GetSceneViewOverlayData(); - if (usingMoveGizmo) { + SceneViewportActiveGizmoKind drawActiveGizmoKind = SceneViewportActiveGizmoKind::None; + if (m_moveGizmo.IsActive()) { + drawActiveGizmoKind = SceneViewportActiveGizmoKind::Move; + } else if (m_rotateGizmo.IsActive()) { + drawActiveGizmoKind = SceneViewportActiveGizmoKind::Rotate; + } else if (m_scaleGizmo.IsActive()) { + drawActiveGizmoKind = SceneViewportActiveGizmoKind::Scale; + } + if (showingMoveGizmo) { moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos); - m_moveGizmo.Update(moveGizmoContext); - } else { + SceneViewportMoveGizmoContext updateContext = moveGizmoContext; + if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None && + drawActiveGizmoKind != SceneViewportActiveGizmoKind::Move) { + updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); + } + m_moveGizmo.Update(updateContext); + } + if (showingRotateGizmo) { rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos); - m_rotateGizmo.Update(rotateGizmoContext); + SceneViewportRotateGizmoContext updateContext = rotateGizmoContext; + if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None && + drawActiveGizmoKind != SceneViewportActiveGizmoKind::Rotate) { + updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); + } + m_rotateGizmo.Update(updateContext); + } + if (showingScaleGizmo) { + scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos); + scaleGizmoContext.uniformOnly = usingTransformTool; + SceneViewportScaleGizmoContext updateContext = scaleGizmoContext; + if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None && + drawActiveGizmoKind != SceneViewportActiveGizmoKind::Scale) { + updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); + } + m_scaleGizmo.Update(updateContext); } DrawSceneViewportOverlay( @@ -335,8 +750,9 @@ void SceneViewPanel::Render() { content.itemMin, content.itemMax, content.availableSize, - usingMoveGizmo ? &m_moveGizmo.GetDrawData() : nullptr, - usingMoveGizmo ? nullptr : &m_rotateGizmo.GetDrawData()); + showingMoveGizmo ? &m_moveGizmo.GetDrawData() : nullptr, + showingRotateGizmo ? &m_rotateGizmo.GetDrawData() : nullptr, + showingScaleGizmo ? &m_scaleGizmo.GetDrawData() : nullptr); } } diff --git a/editor/src/panels/SceneViewPanel.h b/editor/src/panels/SceneViewPanel.h index 1f964a63..51c0eab3 100644 --- a/editor/src/panels/SceneViewPanel.h +++ b/editor/src/panels/SceneViewPanel.h @@ -3,15 +3,19 @@ #include "Panel.h" #include "Viewport/SceneViewportMoveGizmo.h" #include "Viewport/SceneViewportRotateGizmo.h" +#include "Viewport/SceneViewportScaleGizmo.h" #include namespace XCEngine { namespace Editor { -enum class SceneViewportTransformTool : uint8_t { - Move = 0, - Rotate +enum class SceneViewportToolMode : uint8_t { + ViewMove = 0, + Move, + Rotate, + Scale, + Transform }; class SceneViewPanel : public Panel { @@ -20,13 +24,15 @@ public: void Render() override; private: - SceneViewportTransformTool m_transformTool = SceneViewportTransformTool::Move; + SceneViewportToolMode m_toolMode = SceneViewportToolMode::Move; bool m_lookDragging = false; bool m_panDragging = false; - ImVec2 m_lastLookDragDelta = ImVec2(0.0f, 0.0f); - ImVec2 m_lastPanDragDelta = ImVec2(0.0f, 0.0f); + int m_panDragButton = ImGuiMouseButton_Middle; + bool m_loggedLookDelta = false; + bool m_loggedPanDelta = false; SceneViewportMoveGizmo m_moveGizmo; SceneViewportRotateGizmo m_rotateGizmo; + SceneViewportScaleGizmo m_scaleGizmo; }; } diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index 88a2747e..69d570b5 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -7,6 +7,7 @@ set(EDITOR_TEST_SOURCES test_scene_viewport_camera_controller.cpp test_scene_viewport_move_gizmo.cpp test_scene_viewport_rotate_gizmo.cpp + test_scene_viewport_scale_gizmo.cpp test_scene_viewport_post_pass_plan.cpp test_scene_viewport_picker.cpp test_scene_viewport_overlay_renderer.cpp @@ -17,6 +18,7 @@ set(EDITOR_TEST_SOURCES ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportPicker.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportMoveGizmo.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportRotateGizmo.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportScaleGizmo.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportGrid.cpp ) diff --git a/tests/editor/test_scene_viewport_rotate_gizmo.cpp b/tests/editor/test_scene_viewport_rotate_gizmo.cpp index 0481f7fb..3b8db01b 100644 --- a/tests/editor/test_scene_viewport_rotate_gizmo.cpp +++ b/tests/editor/test_scene_viewport_rotate_gizmo.cpp @@ -221,6 +221,62 @@ TEST_F(SceneViewportRotateGizmoTest, DraggingXAxisRotatesAroundWorldXAndCreatesU EXPECT_NEAR(restoredForward.z, 1.0f, 1e-4f); } +TEST_F(SceneViewportRotateGizmoTest, DraggingXAxisShowsAngleFillAndTemporarilyRotatesOtherRings) { + Components::GameObject* target = GetSceneManager().CreateEntity("Target"); + ASSERT_NE(target, nullptr); + + SceneViewportRotateGizmo gizmo; + const SceneViewportOverlayData overlay = MakeIsometricOverlay(); + gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), overlay)); + + const SceneViewportRotateGizmoHandleDrawData* xHandle = + FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::X); + const SceneViewportRotateGizmoHandleDrawData* yHandle = + FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::Y); + ASSERT_NE(xHandle, nullptr); + ASSERT_NE(yHandle, nullptr); + + const SceneViewportRotateGizmoSegmentDrawData* xStartSegment = FindLongestVisibleSegment(*xHandle, true); + const SceneViewportRotateGizmoSegmentDrawData* xEndSegment = + FindFarthestVisibleSegment(*xHandle, SegmentMidpoint(*xStartSegment), true); + const SceneViewportRotateGizmoSegmentDrawData* yInitialSegment = FindLongestVisibleSegment(*yHandle, false); + ASSERT_NE(xStartSegment, nullptr); + ASSERT_NE(xEndSegment, nullptr); + ASSERT_NE(yInitialSegment, nullptr); + + const Math::Vector2 initialYMidpoint = SegmentMidpoint(*yInitialSegment); + const auto startContext = MakeContext(target, SegmentMidpoint(*xStartSegment), overlay); + gizmo.Update(startContext); + ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager())); + + const auto dragContext = MakeContext(target, SegmentMidpoint(*xEndSegment), overlay); + gizmo.Update(dragContext); + gizmo.UpdateDrag(dragContext); + + ASSERT_TRUE(gizmo.GetDrawData().angleFill.visible); + EXPECT_GT(gizmo.GetDrawData().angleFill.arcPointCount, 2u); + + const SceneViewportRotateGizmoHandleDrawData* yHandleDuring = + FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::Y); + ASSERT_NE(yHandleDuring, nullptr); + const SceneViewportRotateGizmoSegmentDrawData* yDuringSegment = FindLongestVisibleSegment(*yHandleDuring, false); + ASSERT_NE(yDuringSegment, nullptr); + const Math::Vector2 duringYMidpoint = SegmentMidpoint(*yDuringSegment); + EXPECT_GT((duringYMidpoint - initialYMidpoint).Magnitude(), 2.0f); + + gizmo.EndDrag(m_context.GetUndoManager()); + gizmo.Update(dragContext); + + EXPECT_FALSE(gizmo.GetDrawData().angleFill.visible); + const SceneViewportRotateGizmoHandleDrawData* yHandleAfter = + FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::Y); + ASSERT_NE(yHandleAfter, nullptr); + const SceneViewportRotateGizmoSegmentDrawData* yAfterSegment = FindLongestVisibleSegment(*yHandleAfter, false); + ASSERT_NE(yAfterSegment, nullptr); + const Math::Vector2 afterYMidpoint = SegmentMidpoint(*yAfterSegment); + EXPECT_LT((afterYMidpoint - initialYMidpoint).Magnitude(), 1.0f); +} + TEST_F(SceneViewportRotateGizmoTest, DraggingEdgeOnXAxisFallsBackToScreenSpaceRotation) { Components::GameObject* target = GetSceneManager().CreateEntity("Target"); ASSERT_NE(target, nullptr); diff --git a/tests/editor/test_scene_viewport_scale_gizmo.cpp b/tests/editor/test_scene_viewport_scale_gizmo.cpp new file mode 100644 index 00000000..2c79d6c5 --- /dev/null +++ b/tests/editor/test_scene_viewport_scale_gizmo.cpp @@ -0,0 +1,250 @@ +#include + +#include + +#include "Core/EditorContext.h" +#include "Managers/SceneManager.h" +#include "Viewport/SceneViewportMath.h" +#include "Viewport/SceneViewportScaleGizmo.h" + +namespace XCEngine::Editor { +namespace { + +float HandleLength(const SceneViewportScaleGizmoAxisHandleDrawData& handle) { + return (handle.end - handle.start).Magnitude(); +} + +Math::Vector2 HandleDirection(const SceneViewportScaleGizmoAxisHandleDrawData& handle) { + return (handle.end - handle.start).Normalized(); +} + +const SceneViewportScaleGizmoAxisHandleDrawData* FindAxisHandle( + const SceneViewportScaleGizmoDrawData& drawData, + SceneViewportScaleGizmoHandle handleKind) { + for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : drawData.axisHandles) { + if (handle.handle == handleKind) { + return &handle; + } + } + + return nullptr; +} + +class SceneViewportScaleGizmoTest : public ::testing::Test { +protected: + void SetUp() override { + m_context.GetSceneManager().NewScene("Scale Gizmo Test Scene"); + } + + static SceneViewportOverlayData MakeOverlay() { + SceneViewportOverlayData overlay = {}; + overlay.valid = true; + overlay.cameraPosition = Math::Vector3(0.0f, 0.0f, -5.0f); + overlay.cameraForward = Math::Vector3::Forward(); + overlay.cameraRight = Math::Vector3::Right(); + overlay.cameraUp = Math::Vector3::Up(); + overlay.verticalFovDegrees = 60.0f; + overlay.nearClipPlane = 0.03f; + overlay.farClipPlane = 2000.0f; + return overlay; + } + + static SceneViewportOverlayData MakeIsometricOverlay() { + SceneViewportOverlayData overlay = {}; + overlay.valid = true; + overlay.cameraPosition = Math::Vector3(-5.0f, 5.0f, -5.0f); + overlay.cameraForward = (Math::Vector3::Zero() - overlay.cameraPosition).Normalized(); + overlay.cameraRight = Math::Vector3::Cross(Math::Vector3::Up(), overlay.cameraForward).Normalized(); + overlay.cameraUp = Math::Vector3::Cross(overlay.cameraForward, overlay.cameraRight).Normalized(); + overlay.verticalFovDegrees = 60.0f; + overlay.nearClipPlane = 0.03f; + overlay.farClipPlane = 2000.0f; + return overlay; + } + + static SceneViewportScaleGizmoContext MakeContext( + Components::GameObject* selectedObject, + const Math::Vector2& mousePosition) { + SceneViewportScaleGizmoContext context = {}; + context.overlay = MakeOverlay(); + context.viewportSize = Math::Vector2(800.0f, 600.0f); + context.mousePosition = mousePosition; + context.selectedObject = selectedObject; + return context; + } + + static SceneViewportScaleGizmoContext MakeContext( + Components::GameObject* selectedObject, + const Math::Vector2& mousePosition, + const SceneViewportOverlayData& overlay) { + SceneViewportScaleGizmoContext context = {}; + context.overlay = overlay; + context.viewportSize = Math::Vector2(800.0f, 600.0f); + context.mousePosition = mousePosition; + context.selectedObject = selectedObject; + return context; + } + + SceneManager& GetSceneManager() { + return dynamic_cast(m_context.GetSceneManager()); + } + + EditorContext m_context; +}; + +TEST_F(SceneViewportScaleGizmoTest, UpdateHighlightsXAxisWhenMouseIsNearXAxisHandle) { + Components::GameObject* target = GetSceneManager().CreateEntity("Target"); + ASSERT_NE(target, nullptr); + + SceneViewportScaleGizmo gizmo; + gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f))); + + ASSERT_TRUE(gizmo.GetDrawData().visible); + const SceneViewportScaleGizmoAxisHandleDrawData* xHandle = + FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X); + ASSERT_NE(xHandle, nullptr); + ASSERT_TRUE(xHandle->visible); + + gizmo.Update(MakeContext(target, xHandle->capCenter)); + + EXPECT_TRUE(gizmo.IsHoveringHandle()); + EXPECT_TRUE(FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X)->hovered); +} + +TEST_F(SceneViewportScaleGizmoTest, DraggingXAxisOnChildWithScaledParentChangesOnlyLocalXScale) { + Components::GameObject* parent = GetSceneManager().CreateEntity("Parent"); + ASSERT_NE(parent, nullptr); + parent->GetTransform()->SetLocalScale(Math::Vector3(0.5f, 2.0f, 1.5f)); + parent->GetTransform()->SetLocalRotation(Math::Quaternion::FromAxisAngle(Math::Vector3::Up(), Math::PI * 0.25f)); + + Components::GameObject* target = GetSceneManager().CreateEntity("Target", parent); + ASSERT_NE(target, nullptr); + target->GetTransform()->SetLocalScale(Math::Vector3(1.0f, 2.0f, 3.0f)); + + const SceneViewportOverlayData overlay = MakeIsometricOverlay(); + SceneViewportScaleGizmo gizmo; + gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), overlay)); + + const SceneViewportScaleGizmoAxisHandleDrawData* xHandle = + FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X); + ASSERT_NE(xHandle, nullptr); + ASSERT_TRUE(xHandle->visible); + + const Math::Vector2 startMouse = xHandle->capCenter; + const Math::Vector2 dragMouse = startMouse + HandleDirection(*xHandle) * 48.0f; + const auto startContext = MakeContext(target, startMouse, overlay); + gizmo.Update(startContext); + ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager())); + + const auto dragContext = MakeContext(target, dragMouse, overlay); + gizmo.Update(dragContext); + gizmo.UpdateDrag(dragContext); + gizmo.EndDrag(m_context.GetUndoManager()); + + const Math::Vector3 localScale = target->GetTransform()->GetLocalScale(); + EXPECT_GT(localScale.x, 1.1f); + EXPECT_NEAR(localScale.y, 2.0f, 1e-4f); + EXPECT_NEAR(localScale.z, 3.0f, 1e-4f); +} + +TEST_F(SceneViewportScaleGizmoTest, DraggingXAxisTemporarilyChangesHandleLengthAndResetsAfterRelease) { + Components::GameObject* target = GetSceneManager().CreateEntity("Target"); + ASSERT_NE(target, nullptr); + + const SceneViewportOverlayData overlay = MakeIsometricOverlay(); + SceneViewportScaleGizmo gizmo; + gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), overlay)); + + const SceneViewportScaleGizmoAxisHandleDrawData* xInitial = + FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X); + ASSERT_NE(xInitial, nullptr); + ASSERT_TRUE(xInitial->visible); + const float initialLength = HandleLength(*xInitial); + + const Math::Vector2 startMouse = xInitial->capCenter; + const Math::Vector2 dragMouse = startMouse + HandleDirection(*xInitial) * 48.0f; + const auto startContext = MakeContext(target, startMouse, overlay); + gizmo.Update(startContext); + ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager())); + + const auto dragContext = MakeContext(target, dragMouse, overlay); + gizmo.Update(dragContext); + gizmo.UpdateDrag(dragContext); + gizmo.Update(dragContext); + + const SceneViewportScaleGizmoAxisHandleDrawData* xDuring = + FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X); + ASSERT_NE(xDuring, nullptr); + EXPECT_GT(HandleLength(*xDuring), initialLength + 6.0f); + + gizmo.EndDrag(m_context.GetUndoManager()); + gizmo.Update(dragContext); + + const SceneViewportScaleGizmoAxisHandleDrawData* xAfter = + FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X); + ASSERT_NE(xAfter, nullptr); + EXPECT_NEAR(HandleLength(*xAfter), initialLength, 1.0f); +} + +TEST_F(SceneViewportScaleGizmoTest, DraggingCenterHandleScalesUniformlyAndCreatesUndoStep) { + Components::GameObject* target = GetSceneManager().CreateEntity("Target"); + ASSERT_NE(target, nullptr); + const uint64_t targetId = target->GetID(); + + SceneViewportScaleGizmo gizmo; + const SceneViewportOverlayData overlay = MakeIsometricOverlay(); + gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), overlay)); + + ASSERT_TRUE(gizmo.GetDrawData().centerHandle.visible); + const Math::Vector2 startMouse = gizmo.GetDrawData().centerHandle.center; + const auto startContext = MakeContext(target, startMouse, overlay); + gizmo.Update(startContext); + ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager())); + + const auto dragContext = MakeContext(target, startMouse + Math::Vector2(28.0f, -28.0f), overlay); + gizmo.Update(dragContext); + gizmo.UpdateDrag(dragContext); + gizmo.EndDrag(m_context.GetUndoManager()); + + const Math::Vector3 localScale = target->GetTransform()->GetLocalScale(); + EXPECT_GT(localScale.x, 1.1f); + EXPECT_NEAR(localScale.x, localScale.y, 1e-4f); + EXPECT_NEAR(localScale.y, localScale.z, 1e-4f); + EXPECT_TRUE(m_context.GetUndoManager().CanUndo()); + + m_context.GetUndoManager().Undo(); + Components::GameObject* restoredTarget = GetSceneManager().GetEntity(targetId); + ASSERT_NE(restoredTarget, nullptr); + EXPECT_NEAR(restoredTarget->GetTransform()->GetLocalScale().x, 1.0f, 1e-4f); + EXPECT_NEAR(restoredTarget->GetTransform()->GetLocalScale().y, 1.0f, 1e-4f); + EXPECT_NEAR(restoredTarget->GetTransform()->GetLocalScale().z, 1.0f, 1e-4f); +} + +TEST_F(SceneViewportScaleGizmoTest, RotatedObjectPlacesXAxisHandleAlongProjectedLocalRight) { + Components::GameObject* target = GetSceneManager().CreateEntity("Target"); + ASSERT_NE(target, nullptr); + target->GetTransform()->SetLocalRotation(Math::Quaternion::FromAxisAngle(Math::Vector3::Up(), Math::PI * 0.33f)); + + const SceneViewportOverlayData overlay = MakeIsometricOverlay(); + SceneViewportScaleGizmo gizmo; + gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), overlay)); + + const SceneViewportScaleGizmoAxisHandleDrawData* xHandle = + FindAxisHandle(gizmo.GetDrawData(), SceneViewportScaleGizmoHandle::X); + ASSERT_NE(xHandle, nullptr); + ASSERT_TRUE(xHandle->visible); + + Math::Vector2 expectedDirection = Math::Vector2::Zero(); + ASSERT_TRUE(ProjectSceneViewportAxisDirectionAtPoint( + overlay, + 800.0f, + 600.0f, + target->GetTransform()->GetPosition(), + target->GetTransform()->GetRight(), + expectedDirection)); + + EXPECT_GT(Math::Vector2::Dot(HandleDirection(*xHandle), expectedDirection), 0.99f); +} + +} // namespace +} // namespace XCEngine::Editor