#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