Files
XCEngine/tests/editor/test_scene_viewport_rotate_gizmo.cpp

371 lines
15 KiB
C++

#include <gtest/gtest.h>
#include <cmath>
#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<SceneManager&>(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