#include #include #include "Core/EditorContext.h" #include "Managers/SceneManager.h" #include "Viewport/SceneViewportRotateGizmo.h" namespace XCEngine::Editor { namespace { Math::Vector2 SegmentMidpoint(const SceneViewportRotateGizmoSegmentDrawData& segment) { return (segment.start + segment.end) * 0.5f; } const SceneViewportRotateGizmoHandleDrawData* FindHandle( const SceneViewportRotateGizmoDrawData& drawData, SceneViewportRotateGizmoAxis axis) { for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) { if (handle.axis == axis) { return &handle; } } return nullptr; } const SceneViewportRotateGizmoSegmentDrawData* FindVisibleSegment( const SceneViewportRotateGizmoHandleDrawData& handle, bool frontOnly) { for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) { if (!segment.visible) { continue; } if (frontOnly && !segment.frontFacing) { continue; } return &segment; } return nullptr; } const SceneViewportRotateGizmoSegmentDrawData* FindLongestVisibleSegment( const SceneViewportRotateGizmoHandleDrawData& handle, bool frontOnly) { const SceneViewportRotateGizmoSegmentDrawData* bestSegment = nullptr; float bestLengthSq = -1.0f; for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) { if (!segment.visible) { continue; } if (frontOnly && !segment.frontFacing) { continue; } const float lengthSq = (segment.end - segment.start).SqrMagnitude(); if (lengthSq <= bestLengthSq) { continue; } bestLengthSq = lengthSq; bestSegment = &segment; } return bestSegment; } const SceneViewportRotateGizmoSegmentDrawData* FindFarthestVisibleSegment( const SceneViewportRotateGizmoHandleDrawData& handle, const Math::Vector2& fromPoint, bool frontOnly) { const SceneViewportRotateGizmoSegmentDrawData* bestSegment = nullptr; float bestDistanceSq = -1.0f; for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) { if (!segment.visible) { continue; } if (frontOnly && !segment.frontFacing) { continue; } const float distanceSq = (SegmentMidpoint(segment) - fromPoint).SqrMagnitude(); if (distanceSq <= bestDistanceSq) { continue; } bestDistanceSq = distanceSq; bestSegment = &segment; } return bestSegment; } class SceneViewportRotateGizmoTest : public ::testing::Test { protected: void SetUp() override { m_context.GetSceneManager().NewScene("Rotate 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 SceneViewportRotateGizmoContext MakeContext( Components::GameObject* selectedObject, const Math::Vector2& mousePosition) { SceneViewportRotateGizmoContext context = {}; context.overlay = MakeOverlay(); context.viewportSize = Math::Vector2(800.0f, 600.0f); context.mousePosition = mousePosition; context.selectedObject = selectedObject; return context; } static SceneViewportRotateGizmoContext MakeContext( Components::GameObject* selectedObject, const Math::Vector2& mousePosition, const SceneViewportOverlayData& overlay) { SceneViewportRotateGizmoContext 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(SceneViewportRotateGizmoTest, UpdateHighlightsXAxisWhenMouseIsNearVisibleXAxisRing) { Components::GameObject* target = GetSceneManager().CreateEntity("Target"); ASSERT_NE(target, nullptr); SceneViewportRotateGizmo gizmo; gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f))); ASSERT_TRUE(gizmo.GetDrawData().visible); const SceneViewportRotateGizmoHandleDrawData* xHandle = FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::X); ASSERT_NE(xHandle, nullptr); const SceneViewportRotateGizmoSegmentDrawData* xSegment = FindLongestVisibleSegment(*xHandle, true); ASSERT_NE(xSegment, nullptr); gizmo.Update(MakeContext(target, SegmentMidpoint(*xSegment))); EXPECT_TRUE(gizmo.IsHoveringHandle()); EXPECT_TRUE(FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::X)->hovered); } TEST_F(SceneViewportRotateGizmoTest, DraggingXAxisRotatesAroundWorldXAndCreatesUndoStep) { Components::GameObject* target = GetSceneManager().CreateEntity("Target"); ASSERT_NE(target, nullptr); const uint64_t targetId = target->GetID(); 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); ASSERT_NE(xHandle, nullptr); const SceneViewportRotateGizmoSegmentDrawData* startSegment = FindLongestVisibleSegment(*xHandle, true); ASSERT_NE(startSegment, nullptr); const Math::Vector2 startMouse = SegmentMidpoint(*startSegment); const SceneViewportRotateGizmoSegmentDrawData* endSegment = FindFarthestVisibleSegment(*xHandle, startMouse, true); ASSERT_NE(endSegment, nullptr); const auto startContext = MakeContext(target, startMouse, overlay); gizmo.Update(startContext); ASSERT_TRUE(gizmo.IsHoveringHandle()); ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager())); ASSERT_TRUE(gizmo.IsActive()); const auto dragContext = MakeContext(target, SegmentMidpoint(*endSegment), overlay); gizmo.Update(dragContext); gizmo.UpdateDrag(dragContext); gizmo.EndDrag(m_context.GetUndoManager()); const Math::Vector3 rotatedRight = target->GetTransform()->GetRight(); const Math::Vector3 rotatedForward = target->GetTransform()->GetForward(); EXPECT_NEAR(rotatedRight.x, 1.0f, 1e-3f); EXPECT_NEAR(rotatedRight.y, 0.0f, 1e-3f); EXPECT_NEAR(rotatedRight.z, 0.0f, 1e-3f); EXPECT_GT(std::abs(rotatedForward.y), 0.05f); EXPECT_TRUE(m_context.GetUndoManager().CanUndo()); m_context.GetUndoManager().Undo(); Components::GameObject* restoredTarget = GetSceneManager().GetEntity(targetId); ASSERT_NE(restoredTarget, nullptr); const Math::Vector3 restoredForward = restoredTarget->GetTransform()->GetForward(); EXPECT_NEAR(restoredForward.x, 0.0f, 1e-4f); EXPECT_NEAR(restoredForward.y, 0.0f, 1e-4f); 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); SceneViewportRotateGizmo gizmo; gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f))); const SceneViewportRotateGizmoHandleDrawData* xHandle = FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::X); ASSERT_NE(xHandle, nullptr); const SceneViewportRotateGizmoSegmentDrawData* startSegment = FindLongestVisibleSegment(*xHandle, true); ASSERT_NE(startSegment, nullptr); const Math::Vector2 startMouse = SegmentMidpoint(*startSegment); const SceneViewportRotateGizmoSegmentDrawData* endSegment = FindFarthestVisibleSegment(*xHandle, startMouse, true); ASSERT_NE(endSegment, nullptr); const auto startContext = MakeContext(target, startMouse); gizmo.Update(startContext); ASSERT_TRUE(gizmo.IsHoveringHandle()); ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager())); ASSERT_TRUE(gizmo.IsActive()); const auto dragContext = MakeContext(target, SegmentMidpoint(*endSegment)); gizmo.Update(dragContext); gizmo.UpdateDrag(dragContext); gizmo.EndDrag(m_context.GetUndoManager()); const Math::Vector3 rotatedForward = target->GetTransform()->GetForward(); EXPECT_GT(std::abs(rotatedForward.y), 0.05f); EXPECT_TRUE(m_context.GetUndoManager().CanUndo()); } TEST_F(SceneViewportRotateGizmoTest, DraggingXAxisOnChildWithScaledParentRotatesObject) { Components::GameObject* parent = GetSceneManager().CreateEntity("Parent"); ASSERT_NE(parent, nullptr); parent->GetTransform()->SetLocalScale(Math::Vector3(0.38912f, 0.38912f, 0.38912f)); 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()->SetLocalPosition(Math::Vector3(1.0f, 0.0f, 0.0f)); 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); ASSERT_NE(xHandle, nullptr); const SceneViewportRotateGizmoSegmentDrawData* startSegment = FindLongestVisibleSegment(*xHandle, true); ASSERT_NE(startSegment, nullptr); const Math::Vector2 startMouse = SegmentMidpoint(*startSegment); const SceneViewportRotateGizmoSegmentDrawData* endSegment = FindFarthestVisibleSegment(*xHandle, startMouse, true); ASSERT_NE(endSegment, nullptr); const auto startContext = MakeContext(target, startMouse, overlay); gizmo.Update(startContext); ASSERT_TRUE(gizmo.IsHoveringHandle()); ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager())); const auto dragContext = MakeContext(target, SegmentMidpoint(*endSegment), overlay); gizmo.Update(dragContext); gizmo.UpdateDrag(dragContext); gizmo.EndDrag(m_context.GetUndoManager()); const Math::Vector3 rotatedForward = target->GetTransform()->GetForward(); EXPECT_GT(std::abs(rotatedForward.y), 0.05f); } TEST_F(SceneViewportRotateGizmoTest, ViewRingIsVisibleAndHoverable) { Components::GameObject* target = GetSceneManager().CreateEntity("Target"); ASSERT_NE(target, nullptr); SceneViewportRotateGizmo gizmo; gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f))); ASSERT_TRUE(gizmo.GetDrawData().visible); const SceneViewportRotateGizmoHandleDrawData* viewHandle = FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::View); ASSERT_NE(viewHandle, nullptr); const SceneViewportRotateGizmoSegmentDrawData* viewSegment = FindLongestVisibleSegment(*viewHandle, false); ASSERT_NE(viewSegment, nullptr); gizmo.Update(MakeContext(target, SegmentMidpoint(*viewSegment))); EXPECT_TRUE(gizmo.IsHoveringHandle()); EXPECT_TRUE(FindHandle(gizmo.GetDrawData(), SceneViewportRotateGizmoAxis::View)->hovered); } } // namespace } // namespace XCEngine::Editor