#include "SceneViewportMoveGizmo.h" #include "Core/IUndoManager.h" #include "SceneViewportMath.h" #include "SceneViewportPicker.h" #include 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