#include "SceneViewportMoveGizmo.h" #include "Core/IUndoManager.h" #include "SceneViewportMath.h" #include "SceneViewportPicker.h" #include namespace XCEngine { namespace Editor { namespace { constexpr float kMoveGizmoHandleLengthPixels = 100.0f; constexpr float kMoveGizmoHoverThresholdPixels = 10.0f; Math::Vector3 GetBaseAxisVector(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::Vector3 GetAxisVector( SceneViewportGizmoAxis axis, const Math::Quaternion& orientation) { const Math::Vector3 baseAxis = GetBaseAxisVector(axis); const Math::Vector3 orientedAxis = orientation * baseAxis; return orientedAxis.SqrMagnitude() <= Math::EPSILON ? baseAxis : orientedAxis.Normalized(); } Math::Color GetAxisBaseColor(SceneViewportGizmoAxis axis) { switch (axis) { case SceneViewportGizmoAxis::X: return Math::Color(0.91f, 0.09f, 0.05f, 1.0f); case SceneViewportGizmoAxis::Y: return Math::Color(0.45f, 1.0f, 0.12f, 1.0f); case SceneViewportGizmoAxis::Z: return Math::Color(0.11f, 0.29f, 1.0f, 1.0f); case SceneViewportGizmoAxis::None: default: return Math::Color::White(); } } Math::Color WithAlpha(const Math::Color& color, float alpha) { return Math::Color(color.r, color.g, color.b, alpha); } SceneViewportGizmoPlane GetPlaneForIndex(size_t index) { switch (index) { case 0: return SceneViewportGizmoPlane::XY; case 1: return SceneViewportGizmoPlane::XZ; case 2: return SceneViewportGizmoPlane::YZ; default: return SceneViewportGizmoPlane::None; } } void GetPlaneAxes( SceneViewportGizmoPlane plane, const Math::Quaternion& orientation, Math::Vector3& outAxisA, Math::Vector3& outAxisB) { switch (plane) { case SceneViewportGizmoPlane::XY: outAxisA = GetAxisVector(SceneViewportGizmoAxis::X, orientation); outAxisB = GetAxisVector(SceneViewportGizmoAxis::Y, orientation); return; case SceneViewportGizmoPlane::XZ: outAxisA = GetAxisVector(SceneViewportGizmoAxis::X, orientation); outAxisB = GetAxisVector(SceneViewportGizmoAxis::Z, orientation); return; case SceneViewportGizmoPlane::YZ: outAxisA = GetAxisVector(SceneViewportGizmoAxis::Y, orientation); outAxisB = GetAxisVector(SceneViewportGizmoAxis::Z, orientation); return; case SceneViewportGizmoPlane::None: default: outAxisA = Math::Vector3::Zero(); outAxisB = Math::Vector3::Zero(); return; } } Math::Vector3 GetPlaneNormal( SceneViewportGizmoPlane plane, const Math::Quaternion& orientation) { switch (plane) { case SceneViewportGizmoPlane::XY: return GetAxisVector(SceneViewportGizmoAxis::Z, orientation); case SceneViewportGizmoPlane::XZ: return GetAxisVector(SceneViewportGizmoAxis::Y, orientation); case SceneViewportGizmoPlane::YZ: return GetAxisVector(SceneViewportGizmoAxis::X, orientation); case SceneViewportGizmoPlane::None: default: return Math::Vector3::Zero(); } } Math::Color GetPlaneBaseColor(SceneViewportGizmoPlane plane) { switch (plane) { case SceneViewportGizmoPlane::XY: return GetAxisBaseColor(SceneViewportGizmoAxis::Z); case SceneViewportGizmoPlane::XZ: return GetAxisBaseColor(SceneViewportGizmoAxis::Y); case SceneViewportGizmoPlane::YZ: return GetAxisBaseColor(SceneViewportGizmoAxis::X); case SceneViewportGizmoPlane::None: default: return Math::Color::White(); } } Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) { return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized(); } float Cross2D(const Math::Vector2& a, const Math::Vector2& b) { return a.x * b.y - a.y * b.x; } float PolygonAreaTwice(const std::array& corners) { float areaTwice = 0.0f; for (size_t index = 0; index < corners.size(); ++index) { const Math::Vector2& current = corners[index]; const Math::Vector2& next = corners[(index + 1) % corners.size()]; areaTwice += current.x * next.y - next.x * current.y; } return areaTwice; } bool PointInTriangle( const Math::Vector2& point, const Math::Vector2& a, const Math::Vector2& b, const Math::Vector2& c) { const float ab = Cross2D(b - a, point - a); const float bc = Cross2D(c - b, point - b); const float ca = Cross2D(a - c, point - c); const bool hasNegative = ab < 0.0f || bc < 0.0f || ca < 0.0f; const bool hasPositive = ab > 0.0f || bc > 0.0f || ca > 0.0f; return !(hasNegative && hasPositive); } bool PointInQuad(const Math::Vector2& point, const std::array& corners) { return PointInTriangle(point, corners[0], corners[1], corners[2]) || PointInTriangle(point, corners[0], corners[2], corners[3]); } Math::Vector2 QuadCenter(const std::array& corners) { Math::Vector2 center = Math::Vector2::Zero(); for (const Math::Vector2& corner : corners) { center += corner; } return center / 4.0f; } 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; } float ComputeWorldUnitsPerPixel( const SceneViewportOverlayData& overlay, const Math::Vector3& worldPoint, float viewportHeight) { if (!overlay.valid || viewportHeight <= 1.0f) { return 0.0f; } const Math::Vector3 cameraForward = NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward()); const float depth = Math::Vector3::Dot(worldPoint - overlay.cameraPosition, cameraForward); if (depth <= Math::EPSILON) { return 0.0f; } return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / viewportHeight; } } // namespace void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) { BuildDrawData(context); if (m_dragMode == DragMode::None && IsMouseInsideViewport(context)) { const SceneViewportMoveGizmoHitResult hitResult = EvaluateHit(context.mousePosition); m_hoveredAxis = hitResult.axis; m_hoveredPlane = hitResult.plane; } else if (m_dragMode == DragMode::None) { m_hoveredAxis = SceneViewportGizmoAxis::None; m_hoveredPlane = SceneViewportGizmoPlane::None; } else if (m_dragMode == DragMode::Axis) { m_hoveredAxis = m_activeAxis; m_hoveredPlane = SceneViewportGizmoPlane::None; } else { m_hoveredAxis = SceneViewportGizmoAxis::None; m_hoveredPlane = m_activePlane; } RefreshHandleState(); } bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& context, IUndoManager& undoManager) { if (m_dragMode != DragMode::None || (m_hoveredAxis == SceneViewportGizmoAxis::None && m_hoveredPlane == SceneViewportGizmoPlane::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 pivotWorldPosition = context.pivotWorldPosition; Math::Vector3 dragPlaneNormal = Math::Vector3::Zero(); Math::Vector3 worldAxis = Math::Vector3::Zero(); if (m_hoveredAxis != SceneViewportGizmoAxis::None) { worldAxis = GetAxisVector(m_hoveredAxis, context.axisOrientation); if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) { return false; } } else { dragPlaneNormal = GetPlaneNormal(m_hoveredPlane, context.axisOrientation); if (dragPlaneNormal.SqrMagnitude() <= Math::EPSILON) { return false; } } const Math::Plane dragPlane = BuildSceneViewportPlaneFromPointNormal(pivotWorldPosition, 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_dragMode = m_hoveredAxis != SceneViewportGizmoAxis::None ? DragMode::Axis : DragMode::Plane; m_activeAxis = m_hoveredAxis; m_activePlane = m_hoveredPlane; m_activeEntityId = context.selectedObject->GetID(); m_activeAxisDirection = worldAxis; m_activePlaneNormal = dragPlaneNormal; m_dragPlane = dragPlane; m_dragStartPivotWorldPosition = pivotWorldPosition; m_dragStartHitWorldPosition = hitPoint; m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - pivotWorldPosition, worldAxis); m_dragObjects = context.selectedObjects; if (m_dragObjects.empty()) { m_dragObjects.push_back(context.selectedObject); } m_dragStartObjectWorldPositions.clear(); m_dragStartObjectWorldPositions.reserve(m_dragObjects.size()); for (Components::GameObject* gameObject : m_dragObjects) { m_dragStartObjectWorldPositions.push_back( gameObject != nullptr && gameObject->GetTransform() != nullptr ? gameObject->GetTransform()->GetPosition() : Math::Vector3::Zero()); } RefreshHandleState(); return true; } void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& context) { if (m_dragMode == DragMode::None || context.selectedObject == nullptr || context.selectedObject->GetID() != m_activeEntityId || m_dragObjects.empty() || m_dragObjects.size() != m_dragStartObjectWorldPositions.size()) { 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); if (m_dragMode == DragMode::Axis) { const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection); const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar; const Math::Vector3 worldDelta = m_activeAxisDirection * deltaScalar; for (size_t index = 0; index < m_dragObjects.size(); ++index) { if (m_dragObjects[index] == nullptr || m_dragObjects[index]->GetTransform() == nullptr) { continue; } m_dragObjects[index]->GetTransform()->SetPosition( m_dragStartObjectWorldPositions[index] + worldDelta); } return; } if (m_dragMode == DragMode::Plane) { const Math::Vector3 worldDelta = Math::Vector3::ProjectOnPlane( hitPoint - m_dragStartHitWorldPosition, m_activePlaneNormal); for (size_t index = 0; index < m_dragObjects.size(); ++index) { if (m_dragObjects[index] == nullptr || m_dragObjects[index]->GetTransform() == nullptr) { continue; } m_dragObjects[index]->GetTransform()->SetPosition( m_dragStartObjectWorldPositions[index] + worldDelta); } } } void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) { if (m_dragMode == DragMode::None) { return; } if (undoManager.HasPendingInteractiveChange()) { undoManager.FinalizeInteractiveChange(); } m_dragMode = DragMode::None; m_activeAxis = SceneViewportGizmoAxis::None; m_activePlane = SceneViewportGizmoPlane::None; m_activeEntityId = 0; m_activeAxisDirection = Math::Vector3::Zero(); m_activePlaneNormal = Math::Vector3::Zero(); m_dragStartPivotWorldPosition = Math::Vector3::Zero(); m_dragStartHitWorldPosition = Math::Vector3::Zero(); m_dragStartAxisScalar = 0.0f; m_dragObjects.clear(); m_dragStartObjectWorldPositions.clear(); RefreshHandleState(); } void SceneViewportMoveGizmo::CancelDrag(IUndoManager* undoManager) { if (undoManager != nullptr && undoManager->HasPendingInteractiveChange()) { undoManager->CancelInteractiveChange(); } m_dragMode = DragMode::None; m_activeAxis = SceneViewportGizmoAxis::None; m_activePlane = SceneViewportGizmoPlane::None; m_activeEntityId = 0; m_activeAxisDirection = Math::Vector3::Zero(); m_activePlaneNormal = Math::Vector3::Zero(); m_dragStartPivotWorldPosition = Math::Vector3::Zero(); m_dragStartHitWorldPosition = Math::Vector3::Zero(); m_dragStartAxisScalar = 0.0f; m_dragObjects.clear(); m_dragStartObjectWorldPositions.clear(); m_hoveredAxis = SceneViewportGizmoAxis::None; m_hoveredPlane = SceneViewportGizmoPlane::None; RefreshHandleState(); } bool SceneViewportMoveGizmo::IsHoveringHandle() const { return m_hoveredAxis != SceneViewportGizmoAxis::None || m_hoveredPlane != SceneViewportGizmoPlane::None; } bool SceneViewportMoveGizmo::IsActive() const { return m_dragMode != DragMode::None; } uint64_t SceneViewportMoveGizmo::GetActiveEntityId() const { return m_activeEntityId; } const SceneViewportMoveGizmoDrawData& SceneViewportMoveGizmo::GetDrawData() const { return m_drawData; } SceneViewportMoveGizmoHitResult SceneViewportMoveGizmo::EvaluateHit(const Math::Vector2& mousePosition) const { SceneViewportMoveGizmoHitResult result = {}; if (!m_drawData.visible) { return result; } const float hoverThresholdSq = kMoveGizmoHoverThresholdPixels * kMoveGizmoHoverThresholdPixels; for (const SceneViewportMoveGizmoHandleDrawData& handle : m_drawData.handles) { if (!handle.visible) { continue; } const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end); if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) { continue; } result.axis = handle.axis; result.plane = SceneViewportGizmoPlane::None; result.distanceSq = distanceSq; } if (result.axis != SceneViewportGizmoAxis::None) { return result; } for (const SceneViewportMoveGizmoPlaneDrawData& plane : m_drawData.planes) { if (!plane.visible || !PointInQuad(mousePosition, plane.corners)) { continue; } const float distanceSq = (QuadCenter(plane.corners) - mousePosition).SqrMagnitude(); if (distanceSq >= result.distanceSq) { continue; } result.axis = SceneViewportGizmoAxis::None; result.plane = plane.plane; result.distanceSq = distanceSq; } return result; } void SceneViewportMoveGizmo::SetHoveredHandle( SceneViewportGizmoAxis axis, SceneViewportGizmoPlane plane) { if (m_dragMode != DragMode::None) { return; } m_hoveredAxis = axis; m_hoveredPlane = axis == SceneViewportGizmoAxis::None ? plane : SceneViewportGizmoPlane::None; RefreshHandleState(); } void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& context) { m_drawData = {}; m_drawData.pivotRadius = 5.0f; if ((context.selectedObject == nullptr && context.selectedObjects.empty()) || !context.overlay.valid || context.viewportSize.x <= 1.0f || context.viewportSize.y <= 1.0f) { return; } const Math::Vector3 gizmoWorldOrigin = context.pivotWorldPosition; const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, context.viewportSize.y, gizmoWorldOrigin); if (!projectedPivot.visible) { return; } m_drawData.visible = true; m_drawData.pivot = projectedPivot.screenPosition; const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel( context.overlay, gizmoWorldOrigin, context.viewportSize.y); if (worldUnitsPerPixel <= Math::EPSILON) { m_drawData = {}; return; } const float axisLengthWorld = worldUnitsPerPixel * kMoveGizmoHandleLengthPixels; const float planeInsetWorld = axisLengthWorld * 0.02f; const float planeExtentWorld = axisLengthWorld * 0.36f; 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; const Math::Vector3 axisEndWorld = gizmoWorldOrigin + GetAxisVector(handle.axis, context.axisOrientation) * axisLengthWorld; const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, context.viewportSize.y, axisEndWorld); if (projectedEnd.ndcDepth < 0.0f || projectedEnd.ndcDepth > 1.0f) { continue; } if ((projectedEnd.screenPosition - projectedPivot.screenPosition).SqrMagnitude() <= Math::EPSILON) { continue; } handle.end = projectedEnd.screenPosition; handle.visible = true; handle.color = GetAxisBaseColor(handle.axis); } for (size_t index = 0; index < m_drawData.planes.size(); ++index) { SceneViewportMoveGizmoPlaneDrawData& plane = m_drawData.planes[index]; plane.plane = GetPlaneForIndex(index); Math::Vector3 axisA = Math::Vector3::Zero(); Math::Vector3 axisB = Math::Vector3::Zero(); GetPlaneAxes(plane.plane, context.axisOrientation, axisA, axisB); if (axisA.SqrMagnitude() <= Math::EPSILON || axisB.SqrMagnitude() <= Math::EPSILON) { continue; } const Math::Vector3 worldCorners[] = { gizmoWorldOrigin + axisA * planeInsetWorld + axisB * planeInsetWorld, gizmoWorldOrigin + axisA * planeExtentWorld + axisB * planeInsetWorld, gizmoWorldOrigin + axisA * planeExtentWorld + axisB * planeExtentWorld, gizmoWorldOrigin + axisA * planeInsetWorld + axisB * planeExtentWorld }; bool valid = true; for (size_t cornerIndex = 0; cornerIndex < plane.corners.size(); ++cornerIndex) { const SceneViewportProjectedPoint projectedCorner = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, context.viewportSize.y, worldCorners[cornerIndex]); if (projectedCorner.ndcDepth < 0.0f || projectedCorner.ndcDepth > 1.0f) { valid = false; break; } plane.corners[cornerIndex] = projectedCorner.screenPosition; } if (!valid || std::abs(PolygonAreaTwice(plane.corners)) <= 4.0f) { continue; } plane.visible = true; const Math::Color baseColor = GetPlaneBaseColor(plane.plane); plane.fillColor = WithAlpha(baseColor, 0.16f); plane.outlineColor = WithAlpha(baseColor, 0.88f); } } 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); } for (SceneViewportMoveGizmoPlaneDrawData& plane : m_drawData.planes) { if (!plane.visible) { continue; } plane.hovered = plane.plane == m_hoveredPlane; plane.active = plane.plane == m_activePlane; const Math::Color baseColor = plane.hovered || plane.active ? Math::Color::Yellow() : GetPlaneBaseColor(plane.plane); plane.fillColor = WithAlpha(baseColor, plane.active ? 0.34f : (plane.hovered ? 0.26f : 0.16f)); plane.outlineColor = WithAlpha(baseColor, plane.active ? 1.0f : (plane.hovered ? 0.95f : 0.82f)); } } } // namespace Editor } // namespace XCEngine