Add scene transform toolbar and scale gizmo
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
250
tests/editor/test_scene_viewport_scale_gizmo.cpp
Normal file
250
tests/editor/test_scene_viewport_scale_gizmo.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user