Add scene viewport move gizmo workflow

This commit is contained in:
2026-03-29 16:18:13 +08:00
parent 2651bad080
commit 0ea1cb29e6
11 changed files with 749 additions and 13 deletions

View File

@@ -79,6 +79,7 @@ add_executable(${PROJECT_NAME} WIN32
src/panels/HierarchyPanel.cpp
src/panels/SceneViewPanel.cpp
src/Viewport/SceneViewportPicker.cpp
src/Viewport/SceneViewportMoveGizmo.cpp
src/Viewport/SceneViewportGrid.cpp
src/Viewport/SceneViewportInfiniteGridPass.cpp
src/Viewport/SceneViewportSelectionMaskPass.cpp

View File

@@ -3,10 +3,19 @@
#include "IViewportHostService.h"
#include <XCEngine/Core/Math/Matrix4.h>
#include <XCEngine/Core/Math/Plane.h>
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Math/Vector4.h>
namespace XCEngine {
namespace Editor {
struct SceneViewportProjectedPoint {
Math::Vector2 screenPosition = Math::Vector2::Zero();
float ndcDepth = 0.0f;
bool visible = false;
};
inline Math::Matrix4x4 BuildSceneViewportViewMatrix(const SceneViewportOverlayData& overlay) {
const Math::Vector3 right = overlay.cameraRight.Normalized();
const Math::Vector3 up = overlay.cameraUp.Normalized();
@@ -44,5 +53,122 @@ inline Math::Matrix4x4 BuildSceneViewportProjectionMatrix(
overlay.farClipPlane);
}
inline Math::Matrix4x4 BuildSceneViewportViewProjectionMatrix(
const SceneViewportOverlayData& overlay,
float viewportWidth,
float viewportHeight) {
return BuildSceneViewportProjectionMatrix(overlay, viewportWidth, viewportHeight) *
BuildSceneViewportViewMatrix(overlay);
}
inline SceneViewportProjectedPoint ProjectSceneViewportWorldPoint(
const SceneViewportOverlayData& overlay,
float viewportWidth,
float viewportHeight,
const Math::Vector3& worldPoint) {
SceneViewportProjectedPoint result = {};
if (!overlay.valid || viewportWidth <= 1.0f || viewportHeight <= 1.0f) {
return result;
}
const Math::Vector4 clipPoint =
BuildSceneViewportViewProjectionMatrix(overlay, viewportWidth, viewportHeight) *
Math::Vector4(worldPoint, 1.0f);
if (clipPoint.w <= Math::EPSILON) {
return result;
}
const Math::Vector3 ndcPoint = clipPoint.ToVector3() / clipPoint.w;
result.screenPosition.x = (ndcPoint.x * 0.5f + 0.5f) * viewportWidth;
result.screenPosition.y = (1.0f - (ndcPoint.y * 0.5f + 0.5f)) * viewportHeight;
result.ndcDepth = ndcPoint.z;
result.visible =
ndcPoint.x >= -1.0f && ndcPoint.x <= 1.0f &&
ndcPoint.y >= -1.0f && ndcPoint.y <= 1.0f &&
ndcPoint.z >= 0.0f && ndcPoint.z <= 1.0f;
return result;
}
inline bool ProjectSceneViewportAxisDirection(
const SceneViewportOverlayData& overlay,
const Math::Vector3& worldAxis,
Math::Vector2& outScreenDirection) {
if (!overlay.valid) {
return false;
}
const Math::Vector3 viewAxis = BuildSceneViewportViewMatrix(overlay).MultiplyVector(worldAxis.Normalized());
const Math::Vector2 screenDirection(viewAxis.x, -viewAxis.y);
if (screenDirection.SqrMagnitude() <= Math::EPSILON) {
return false;
}
outScreenDirection = screenDirection.Normalized();
return true;
}
inline float DistanceToSegmentSquared(
const Math::Vector2& point,
const Math::Vector2& segmentStart,
const Math::Vector2& segmentEnd,
float* outSegmentT = nullptr) {
const Math::Vector2 segment = segmentEnd - segmentStart;
const float segmentLengthSq = segment.SqrMagnitude();
if (segmentLengthSq <= Math::EPSILON) {
if (outSegmentT != nullptr) {
*outSegmentT = 0.0f;
}
return (point - segmentStart).SqrMagnitude();
}
const float segmentT = std::clamp(
Math::Vector2::Dot(point - segmentStart, segment) / segmentLengthSq,
0.0f,
1.0f);
if (outSegmentT != nullptr) {
*outSegmentT = segmentT;
}
const Math::Vector2 closestPoint = segmentStart + segment * segmentT;
return (point - closestPoint).SqrMagnitude();
}
inline Math::Plane BuildSceneViewportPlaneFromPointNormal(
const Math::Vector3& point,
const Math::Vector3& normal) {
const Math::Vector3 planeNormal = normal.Normalized();
return Math::Plane(planeNormal, -Math::Vector3::Dot(planeNormal, point));
}
inline bool BuildSceneViewportAxisDragPlaneNormal(
const SceneViewportOverlayData& overlay,
const Math::Vector3& worldAxis,
Math::Vector3& outPlaneNormal) {
if (!overlay.valid || worldAxis.SqrMagnitude() <= Math::EPSILON) {
return false;
}
const Math::Vector3 axis = worldAxis.Normalized();
const Math::Vector3 candidates[] = {
Math::Vector3::ProjectOnPlane(overlay.cameraForward.Normalized(), axis),
Math::Vector3::ProjectOnPlane(overlay.cameraUp.Normalized(), axis),
Math::Vector3::ProjectOnPlane(overlay.cameraRight.Normalized(), axis),
Math::Vector3::ProjectOnPlane(Math::Vector3::Up(), axis),
Math::Vector3::ProjectOnPlane(Math::Vector3::Right(), axis),
Math::Vector3::ProjectOnPlane(Math::Vector3::Forward(), axis)
};
for (const Math::Vector3& candidate : candidates) {
if (candidate.SqrMagnitude() <= Math::EPSILON) {
continue;
}
outPlaneNormal = candidate.Normalized();
return true;
}
return false;
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,275 @@
#include "SceneViewportMoveGizmo.h"
#include "Core/IUndoManager.h"
#include "SceneViewportMath.h"
#include "SceneViewportPicker.h"
#include <XCEngine/Components/GameObject.h>
namespace XCEngine {
namespace Editor {
namespace {
constexpr float kMoveGizmoHandleLengthPixels = 72.0f;
constexpr float kMoveGizmoHoverThresholdPixels = 10.0f;
Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) {
switch (axis) {
case SceneViewportGizmoAxis::X:
return Math::Vector3::Right();
case SceneViewportGizmoAxis::Y:
return Math::Vector3::Up();
case SceneViewportGizmoAxis::Z:
return Math::Vector3::Forward();
case SceneViewportGizmoAxis::None:
default:
return Math::Vector3::Zero();
}
}
Math::Color GetAxisBaseColor(SceneViewportGizmoAxis axis) {
switch (axis) {
case SceneViewportGizmoAxis::X:
return Math::Color(0.937f, 0.325f, 0.314f, 1.0f);
case SceneViewportGizmoAxis::Y:
return Math::Color(0.400f, 0.733f, 0.416f, 1.0f);
case SceneViewportGizmoAxis::Z:
return Math::Color(0.259f, 0.647f, 0.961f, 1.0f);
case SceneViewportGizmoAxis::None:
default:
return Math::Color::White();
}
}
bool IsMouseInsideViewport(const SceneViewportMoveGizmoContext& context) {
return context.mousePosition.x >= 0.0f &&
context.mousePosition.y >= 0.0f &&
context.mousePosition.x <= context.viewportSize.x &&
context.mousePosition.y <= context.viewportSize.y;
}
} // namespace
void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) {
BuildDrawData(context);
if (m_activeAxis == SceneViewportGizmoAxis::None && IsMouseInsideViewport(context)) {
m_hoveredAxis = HitTestAxis(context.mousePosition);
} else if (m_activeAxis == SceneViewportGizmoAxis::None) {
m_hoveredAxis = SceneViewportGizmoAxis::None;
} else {
m_hoveredAxis = m_activeAxis;
}
RefreshHandleState();
}
bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& context, IUndoManager& undoManager) {
if (m_activeAxis != SceneViewportGizmoAxis::None ||
m_hoveredAxis == SceneViewportGizmoAxis::None ||
context.selectedObject == nullptr ||
!m_drawData.visible ||
undoManager.HasPendingInteractiveChange()) {
return false;
}
Math::Ray worldRay;
if (!BuildSceneViewportRay(
context.overlay,
context.viewportSize,
context.mousePosition,
worldRay)) {
return false;
}
const Math::Vector3 worldAxis = GetAxisVector(m_hoveredAxis);
Math::Vector3 dragPlaneNormal = Math::Vector3::Zero();
if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) {
return false;
}
const Math::Vector3 worldPosition = context.selectedObject->GetTransform()->GetPosition();
const Math::Plane dragPlane = BuildSceneViewportPlaneFromPointNormal(worldPosition, dragPlaneNormal);
float hitDistance = 0.0f;
if (!worldRay.Intersects(dragPlane, hitDistance)) {
return false;
}
undoManager.BeginInteractiveChange("Move Gizmo");
if (!undoManager.HasPendingInteractiveChange()) {
return false;
}
const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance);
m_activeAxis = m_hoveredAxis;
m_activeEntityId = context.selectedObject->GetID();
m_activeAxisDirection = worldAxis;
m_dragPlane = dragPlane;
m_dragStartWorldPosition = worldPosition;
m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - worldPosition, worldAxis);
RefreshHandleState();
return true;
}
void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& context) {
if (m_activeAxis == SceneViewportGizmoAxis::None ||
context.selectedObject == nullptr ||
context.selectedObject->GetID() != m_activeEntityId) {
return;
}
Math::Ray worldRay;
if (!BuildSceneViewportRay(
context.overlay,
context.viewportSize,
context.mousePosition,
worldRay)) {
return;
}
float hitDistance = 0.0f;
if (!worldRay.Intersects(m_dragPlane, hitDistance)) {
return;
}
const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance);
const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartWorldPosition, m_activeAxisDirection);
const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar;
context.selectedObject->GetTransform()->SetPosition(
m_dragStartWorldPosition + m_activeAxisDirection * deltaScalar);
}
void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) {
if (m_activeAxis == SceneViewportGizmoAxis::None) {
return;
}
if (undoManager.HasPendingInteractiveChange()) {
undoManager.FinalizeInteractiveChange();
}
m_activeAxis = SceneViewportGizmoAxis::None;
m_activeEntityId = 0;
m_activeAxisDirection = Math::Vector3::Zero();
m_dragStartWorldPosition = Math::Vector3::Zero();
m_dragStartAxisScalar = 0.0f;
RefreshHandleState();
}
void SceneViewportMoveGizmo::CancelDrag(IUndoManager* undoManager) {
if (undoManager != nullptr && undoManager->HasPendingInteractiveChange()) {
undoManager->CancelInteractiveChange();
}
m_activeAxis = SceneViewportGizmoAxis::None;
m_activeEntityId = 0;
m_activeAxisDirection = Math::Vector3::Zero();
m_dragStartWorldPosition = Math::Vector3::Zero();
m_dragStartAxisScalar = 0.0f;
m_hoveredAxis = SceneViewportGizmoAxis::None;
RefreshHandleState();
}
bool SceneViewportMoveGizmo::IsHoveringHandle() const {
return m_hoveredAxis != SceneViewportGizmoAxis::None;
}
bool SceneViewportMoveGizmo::IsActive() const {
return m_activeAxis != SceneViewportGizmoAxis::None;
}
uint64_t SceneViewportMoveGizmo::GetActiveEntityId() const {
return m_activeEntityId;
}
const SceneViewportMoveGizmoDrawData& SceneViewportMoveGizmo::GetDrawData() const {
return m_drawData;
}
void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& context) {
m_drawData = {};
m_drawData.pivotRadius = 5.0f;
const Components::GameObject* selectedObject = context.selectedObject;
if (selectedObject == nullptr ||
!context.overlay.valid ||
context.viewportSize.x <= 1.0f ||
context.viewportSize.y <= 1.0f) {
return;
}
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
context.overlay,
context.viewportSize.x,
context.viewportSize.y,
selectedObject->GetTransform()->GetPosition());
if (!projectedPivot.visible) {
return;
}
m_drawData.visible = true;
m_drawData.pivot = projectedPivot.screenPosition;
const SceneViewportGizmoAxis axes[] = {
SceneViewportGizmoAxis::X,
SceneViewportGizmoAxis::Y,
SceneViewportGizmoAxis::Z
};
for (size_t index = 0; index < m_drawData.handles.size(); ++index) {
SceneViewportMoveGizmoHandleDrawData& handle = m_drawData.handles[index];
handle.axis = axes[index];
handle.start = projectedPivot.screenPosition;
Math::Vector2 screenDirection = Math::Vector2::Zero();
if (!ProjectSceneViewportAxisDirection(context.overlay, GetAxisVector(handle.axis), screenDirection)) {
continue;
}
handle.end = handle.start + screenDirection * kMoveGizmoHandleLengthPixels;
handle.visible = true;
handle.color = GetAxisBaseColor(handle.axis);
}
}
void SceneViewportMoveGizmo::RefreshHandleState() {
for (SceneViewportMoveGizmoHandleDrawData& handle : m_drawData.handles) {
if (!handle.visible) {
continue;
}
handle.hovered = handle.axis == m_hoveredAxis;
handle.active = handle.axis == m_activeAxis;
handle.color = (handle.hovered || handle.active)
? Math::Color::Yellow()
: GetAxisBaseColor(handle.axis);
}
}
SceneViewportGizmoAxis SceneViewportMoveGizmo::HitTestAxis(const Math::Vector2& mousePosition) const {
if (!m_drawData.visible) {
return SceneViewportGizmoAxis::None;
}
const float hoverThresholdSq = kMoveGizmoHoverThresholdPixels * kMoveGizmoHoverThresholdPixels;
SceneViewportGizmoAxis bestAxis = SceneViewportGizmoAxis::None;
float bestDistanceSq = hoverThresholdSq;
for (const SceneViewportMoveGizmoHandleDrawData& handle : m_drawData.handles) {
if (!handle.visible) {
continue;
}
const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end);
if (distanceSq > bestDistanceSq) {
continue;
}
bestDistanceSq = distanceSq;
bestAxis = handle.axis;
}
return bestAxis;
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,82 @@
#pragma once
#include "IViewportHostService.h"
#include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Core/Math/Plane.h>
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <array>
#include <cstdint>
namespace XCEngine {
namespace Components {
class GameObject;
} // namespace Components
namespace Editor {
class IUndoManager;
enum class SceneViewportGizmoAxis : uint8_t {
None = 0,
X,
Y,
Z
};
struct SceneViewportMoveGizmoHandleDrawData {
SceneViewportGizmoAxis axis = SceneViewportGizmoAxis::None;
Math::Vector2 start = Math::Vector2::Zero();
Math::Vector2 end = Math::Vector2::Zero();
Math::Color color = Math::Color::White();
bool visible = false;
bool hovered = false;
bool active = false;
};
struct SceneViewportMoveGizmoDrawData {
bool visible = false;
Math::Vector2 pivot = Math::Vector2::Zero();
float pivotRadius = 5.0f;
std::array<SceneViewportMoveGizmoHandleDrawData, 3> handles = {};
};
struct SceneViewportMoveGizmoContext {
SceneViewportOverlayData overlay = {};
Math::Vector2 viewportSize = Math::Vector2::Zero();
Math::Vector2 mousePosition = Math::Vector2::Zero();
Components::GameObject* selectedObject = nullptr;
};
class SceneViewportMoveGizmo {
public:
void Update(const SceneViewportMoveGizmoContext& context);
bool TryBeginDrag(const SceneViewportMoveGizmoContext& context, IUndoManager& undoManager);
void UpdateDrag(const SceneViewportMoveGizmoContext& context);
void EndDrag(IUndoManager& undoManager);
void CancelDrag(IUndoManager* undoManager = nullptr);
bool IsHoveringHandle() const;
bool IsActive() const;
uint64_t GetActiveEntityId() const;
const SceneViewportMoveGizmoDrawData& GetDrawData() const;
private:
void BuildDrawData(const SceneViewportMoveGizmoContext& context);
void RefreshHandleState();
SceneViewportGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const;
SceneViewportMoveGizmoDrawData m_drawData = {};
SceneViewportGizmoAxis m_hoveredAxis = SceneViewportGizmoAxis::None;
SceneViewportGizmoAxis m_activeAxis = SceneViewportGizmoAxis::None;
uint64_t m_activeEntityId = 0;
Math::Vector3 m_activeAxisDirection = Math::Vector3::Zero();
Math::Plane m_dragPlane = {};
Math::Vector3 m_dragStartWorldPosition = Math::Vector3::Zero();
float m_dragStartAxisScalar = 0.0f;
};
} // namespace Editor
} // namespace XCEngine

View File

@@ -10,6 +10,18 @@ Math::Matrix4x4 BuildOverlayViewMatrix(const SceneViewportOverlayData& overlay)
return BuildSceneViewportViewMatrix(overlay);
}
ImU32 ToImGuiColor(const Math::Color& color) {
const auto toChannel = [](float value) -> int {
return static_cast<int>(std::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f);
};
return IM_COL32(
toChannel(color.r),
toChannel(color.g),
toChannel(color.b),
toChannel(color.a));
}
void DrawAxisLabel(ImDrawList* drawList, const ImVec2& position, const char* label, ImU32 color) {
if (drawList == nullptr || label == nullptr) {
return;
@@ -61,6 +73,31 @@ void DrawSceneAxisWidget(
}
}
void DrawSceneMoveGizmo(
ImDrawList* drawList,
const SceneViewportMoveGizmoDrawData& moveGizmo) {
if (drawList == nullptr || !moveGizmo.visible) {
return;
}
const ImVec2 pivot(moveGizmo.pivot.x, moveGizmo.pivot.y);
drawList->AddCircleFilled(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(20, 22, 24, 220), 20);
drawList->AddCircle(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(255, 255, 255, 48), 20, 1.0f);
for (const SceneViewportMoveGizmoHandleDrawData& handle : moveGizmo.handles) {
if (!handle.visible) {
continue;
}
const ImU32 color = ToImGuiColor(handle.color);
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f);
const ImVec2 start(handle.start.x, handle.start.y);
const ImVec2 end(handle.end.x, handle.end.y);
drawList->AddLine(start, end, color, thickness);
drawList->AddCircleFilled(end, handle.active ? 6.5f : 5.5f, color, 20);
}
}
} // namespace
void DrawSceneViewportOverlay(
@@ -68,13 +105,19 @@ void DrawSceneViewportOverlay(
const SceneViewportOverlayData& overlay,
const ImVec2& viewportMin,
const ImVec2& viewportMax,
const ImVec2& viewportSize) {
if (drawList == nullptr || !overlay.valid || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) {
const ImVec2& viewportSize,
const SceneViewportMoveGizmoDrawData* moveGizmo) {
if (drawList == nullptr || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) {
return;
}
drawList->PushClipRect(viewportMin, viewportMax, true);
DrawSceneAxisWidget(drawList, overlay, viewportMin, viewportMax);
if (overlay.valid) {
DrawSceneAxisWidget(drawList, overlay, viewportMin, viewportMax);
}
if (moveGizmo != nullptr) {
DrawSceneMoveGizmo(drawList, *moveGizmo);
}
drawList->PopClipRect();
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include "IViewportHostService.h"
#include "SceneViewportMoveGizmo.h"
#include <imgui.h>
@@ -12,7 +13,8 @@ void DrawSceneViewportOverlay(
const SceneViewportOverlayData& overlay,
const ImVec2& viewportMin,
const ImVec2& viewportMax,
const ImVec2& viewportSize);
const ImVec2& viewportSize,
const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr);
} // namespace Editor
} // namespace XCEngine

View File

@@ -1,5 +1,6 @@
#include "Actions/ActionRouting.h"
#include "Core/IEditorContext.h"
#include "Core/ISceneManager.h"
#include "Core/ISelectionManager.h"
#include "SceneViewPanel.h"
#include "Viewport/SceneViewportOverlayRenderer.h"
@@ -11,10 +12,38 @@
namespace XCEngine {
namespace Editor {
namespace {
SceneViewportMoveGizmoContext BuildMoveGizmoContext(
IEditorContext& context,
const SceneViewportOverlayData& overlay,
const ViewportPanelContentResult& content,
const ImVec2& mousePosition) {
SceneViewportMoveGizmoContext gizmoContext = {};
gizmoContext.overlay = overlay;
gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y);
gizmoContext.mousePosition = Math::Vector2(
mousePosition.x - content.itemMin.x,
mousePosition.y - content.itemMin.y);
if (context.GetSelectionManager().GetSelectionCount() == 1) {
const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity();
if (selectedEntity != 0) {
gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity);
}
}
return gizmoContext;
}
} // namespace
SceneViewPanel::SceneViewPanel() : Panel("Scene") {}
void SceneViewPanel::Render() {
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
UI::PanelWindowScope panel(m_name.c_str());
ImGui::PopStyleVar();
if (!panel.IsOpen()) {
return;
}
@@ -22,21 +51,55 @@ void SceneViewPanel::Render() {
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene);
if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) {
const ImGuiIO& io = ImGui::GetIO();
const bool selectClick =
const bool hasInteractiveViewport = content.hasViewportArea && content.frame.hasTexture;
SceneViewportOverlayData overlay = {};
SceneViewportMoveGizmoContext moveGizmoContext = {};
if (hasInteractiveViewport) {
overlay = viewportHostService->GetSceneViewOverlayData();
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
if (m_moveGizmo.IsActive() &&
(moveGizmoContext.selectedObject == nullptr ||
m_context->GetSelectionManager().GetSelectedEntity() != m_moveGizmo.GetActiveEntityId())) {
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
}
m_moveGizmo.Update(moveGizmoContext);
} else if (m_moveGizmo.IsActive()) {
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
}
const bool beginMoveGizmo =
hasInteractiveViewport &&
content.hovered &&
content.frame.hasTexture &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!m_lookDragging &&
!m_panDragging;
!m_panDragging &&
m_moveGizmo.IsHoveringHandle();
const bool selectClick =
hasInteractiveViewport &&
content.hovered &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!m_lookDragging &&
!m_panDragging &&
!m_moveGizmo.IsHoveringHandle() &&
!m_moveGizmo.IsActive();
const bool beginLookDrag =
content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right);
content.hovered &&
!m_moveGizmo.IsActive() &&
ImGui::IsMouseClicked(ImGuiMouseButton_Right);
const bool beginPanDrag =
content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle);
content.hovered &&
!m_moveGizmo.IsActive() &&
ImGui::IsMouseClicked(ImGuiMouseButton_Middle);
if (selectClick || beginLookDrag || beginPanDrag) {
if (beginMoveGizmo || selectClick || beginLookDrag || beginPanDrag) {
ImGui::SetWindowFocus();
}
if (beginMoveGizmo) {
m_moveGizmo.TryBeginDrag(moveGizmoContext, m_context->GetUndoManager());
}
if (selectClick) {
const ImVec2 localMousePosition(
io.MousePos.x - content.itemMin.x,
@@ -52,6 +115,14 @@ void SceneViewPanel::Render() {
}
}
if (m_moveGizmo.IsActive()) {
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
m_moveGizmo.UpdateDrag(moveGizmoContext);
} else {
m_moveGizmo.EndDrag(m_context->GetUndoManager());
}
}
if (beginLookDrag) {
m_lookDragging = true;
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
@@ -70,7 +141,7 @@ void SceneViewPanel::Render() {
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
}
if (m_lookDragging || m_panDragging) {
if (m_lookDragging || m_panDragging || m_moveGizmo.IsActive()) {
ImGui::SetNextFrameWantCaptureMouse(true);
}
if (m_lookDragging) {
@@ -126,13 +197,16 @@ void SceneViewPanel::Render() {
viewportHostService->UpdateSceneViewInput(*m_context, input);
if (content.hasViewportArea && content.frame.hasTexture) {
const SceneViewportOverlayData overlay = viewportHostService->GetSceneViewOverlayData();
overlay = viewportHostService->GetSceneViewOverlayData();
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
m_moveGizmo.Update(moveGizmoContext);
DrawSceneViewportOverlay(
ImGui::GetWindowDrawList(),
overlay,
content.itemMin,
content.itemMax,
content.availableSize);
content.availableSize,
&m_moveGizmo.GetDrawData());
}
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include "Panel.h"
#include "Viewport/SceneViewportMoveGizmo.h"
#include <imgui.h>
@@ -17,6 +18,7 @@ private:
bool m_panDragging = false;
ImVec2 m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
ImVec2 m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
SceneViewportMoveGizmo m_moveGizmo;
};
}

View File

@@ -5,6 +5,7 @@ project(XCEngine_EditorTests)
set(EDITOR_TEST_SOURCES
test_action_routing.cpp
test_scene_viewport_camera_controller.cpp
test_scene_viewport_move_gizmo.cpp
test_scene_viewport_picker.cpp
test_scene_viewport_overlay_renderer.cpp
test_scene_viewport_selection_utils.cpp
@@ -12,6 +13,7 @@ set(EDITOR_TEST_SOURCES
${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp
${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportPicker.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportMoveGizmo.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportGrid.cpp
)

View File

@@ -0,0 +1,96 @@
#include <gtest/gtest.h>
#include "Core/EditorContext.h"
#include "Managers/SceneManager.h"
#include "Viewport/SceneViewportMoveGizmo.h"
namespace XCEngine::Editor {
namespace {
class SceneViewportMoveGizmoTest : public ::testing::Test {
protected:
void SetUp() override {
m_context.GetSceneManager().NewScene("Move 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 SceneViewportMoveGizmoContext MakeContext(
Components::GameObject* selectedObject,
const Math::Vector2& mousePosition) {
SceneViewportMoveGizmoContext context = {};
context.overlay = MakeOverlay();
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(SceneViewportMoveGizmoTest, UpdateHighlightsXAxisWhenMouseIsNearXAxisHandle) {
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
ASSERT_NE(target, nullptr);
m_context.GetSelectionManager().SetSelectedEntity(target->GetID());
SceneViewportMoveGizmo gizmo;
gizmo.Update(MakeContext(target, Math::Vector2(436.0f, 300.0f)));
ASSERT_TRUE(gizmo.GetDrawData().visible);
EXPECT_TRUE(gizmo.IsHoveringHandle());
EXPECT_TRUE(gizmo.GetDrawData().handles[0].hovered);
EXPECT_FALSE(gizmo.GetDrawData().handles[1].hovered);
EXPECT_FALSE(gizmo.GetDrawData().handles[2].hovered);
}
TEST_F(SceneViewportMoveGizmoTest, DraggingXAxisOnlyChangesWorldXAndCreatesUndoStep) {
Components::GameObject* target = GetSceneManager().CreateEntity("Target");
ASSERT_NE(target, nullptr);
const uint64_t targetId = target->GetID();
m_context.GetSelectionManager().SetSelectedEntity(target->GetID());
SceneViewportMoveGizmo gizmo;
const auto startContext = MakeContext(target, Math::Vector2(436.0f, 300.0f));
gizmo.Update(startContext);
ASSERT_TRUE(gizmo.TryBeginDrag(startContext, m_context.GetUndoManager()));
ASSERT_TRUE(gizmo.IsActive());
const auto dragContext = MakeContext(target, Math::Vector2(500.0f, 300.0f));
gizmo.Update(dragContext);
gizmo.UpdateDrag(dragContext);
gizmo.EndDrag(m_context.GetUndoManager());
const Math::Vector3 movedPosition = target->GetTransform()->GetPosition();
EXPECT_GT(movedPosition.x, 0.05f);
EXPECT_NEAR(movedPosition.y, 0.0f, 1e-4f);
EXPECT_NEAR(movedPosition.z, 0.0f, 1e-4f);
EXPECT_TRUE(m_context.GetUndoManager().CanUndo());
m_context.GetUndoManager().Undo();
Components::GameObject* restoredTarget = GetSceneManager().GetEntity(targetId);
ASSERT_NE(restoredTarget, nullptr);
const Math::Vector3 restoredPosition = restoredTarget->GetTransform()->GetPosition();
EXPECT_NEAR(restoredPosition.x, 0.0f, 1e-4f);
EXPECT_NEAR(restoredPosition.y, 0.0f, 1e-4f);
EXPECT_NEAR(restoredPosition.z, 0.0f, 1e-4f);
}
} // namespace
} // namespace XCEngine::Editor

View File

@@ -48,9 +48,11 @@ bool IsPowerOfTenSpacing(float value) {
} // namespace
using XCEngine::Editor::BuildSceneGridParameters;
using XCEngine::Editor::BuildSceneViewportAxisDragPlaneNormal;
using XCEngine::Editor::SceneViewportCameraController;
using XCEngine::Editor::BuildSceneViewportProjectionMatrix;
using XCEngine::Editor::BuildSceneViewportViewMatrix;
using XCEngine::Editor::ProjectSceneViewportWorldPoint;
using XCEngine::Editor::SceneGridParameters;
using XCEngine::Editor::SceneViewportOverlayData;
using XCEngine::Components::GameObject;
@@ -219,3 +221,34 @@ TEST(SceneViewportOverlayRenderer_Test, ViewMatrixMatchesSceneCameraTransformCon
cameraObject.GetTransform()->GetWorldToLocalMatrix(),
1e-3f));
}
TEST(SceneViewportOverlayRenderer_Test, ProjectSceneViewportWorldPointMapsOriginToViewportCenter) {
SceneViewportOverlayData overlay = {};
overlay.valid = true;
overlay.cameraPosition = Vector3(0.0f, 0.0f, -5.0f);
overlay.cameraForward = Vector3::Forward();
overlay.cameraRight = Vector3::Right();
overlay.cameraUp = Vector3::Up();
overlay.verticalFovDegrees = 60.0f;
overlay.nearClipPlane = 0.03f;
overlay.farClipPlane = 2000.0f;
const auto projected = ProjectSceneViewportWorldPoint(overlay, 800.0f, 600.0f, Vector3::Zero());
EXPECT_TRUE(projected.visible);
EXPECT_NEAR(projected.screenPosition.x, 400.0f, 1e-3f);
EXPECT_NEAR(projected.screenPosition.y, 300.0f, 1e-3f);
}
TEST(SceneViewportOverlayRenderer_Test, BuildSceneViewportAxisDragPlaneNormalFallsBackWhenForwardAlignsWithAxis) {
SceneViewportOverlayData overlay = {};
overlay.valid = true;
overlay.cameraForward = Vector3::Right();
overlay.cameraRight = Vector3::Back();
overlay.cameraUp = Vector3::Up();
Vector3 planeNormal = Vector3::Zero();
ASSERT_TRUE(BuildSceneViewportAxisDragPlaneNormal(overlay, Vector3::Right(), planeNormal));
EXPECT_NEAR(Vector3::Dot(planeNormal, Vector3::Right()), 0.0f, 1e-4f);
EXPECT_GT(planeNormal.SqrMagnitude(), 0.5f);
}