Files
XCEngine/tests/editor/test_scene_viewport_scale_gizmo.cpp

251 lines
10 KiB
C++
Raw Normal View History

#include <gtest/gtest.h>
#include <XCEngine/Core/Math/Quaternion.h>
#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<SceneManager&>(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