#include "SceneViewportRotateGizmo.h" #include "Core/IUndoManager.h" #include "SceneViewportMath.h" #include "SceneViewportPicker.h" #include #include #include namespace XCEngine { namespace Editor { namespace { constexpr float kRotateGizmoAxisRadiusPixels = 96.0f; constexpr float kRotateGizmoViewRadiusPixels = 106.0f; constexpr float kRotateGizmoHoverThresholdPixels = 9.0f; constexpr float kRotateGizmoAngleFillMinRadians = 0.01f; Math::Vector3 GetBaseRotateAxisVector(SceneViewportRotateGizmoAxis axis) { switch (axis) { case SceneViewportRotateGizmoAxis::X: return Math::Vector3::Right(); case SceneViewportRotateGizmoAxis::Y: return Math::Vector3::Up(); case SceneViewportRotateGizmoAxis::Z: return Math::Vector3::Forward(); case SceneViewportRotateGizmoAxis::View: case SceneViewportRotateGizmoAxis::None: default: return Math::Vector3::Zero(); } } Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) { return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized(); } bool IsMouseInsideViewport(const SceneViewportRotateGizmoContext& context) { return context.mousePosition.x >= 0.0f && context.mousePosition.y >= 0.0f && context.mousePosition.x <= context.viewportSize.x && context.mousePosition.y <= context.viewportSize.y; } Math::Quaternion ComputeStableWorldRotation(const Components::GameObject* gameObject) { if (gameObject == nullptr || gameObject->GetTransform() == nullptr) { return Math::Quaternion::Identity(); } const Components::TransformComponent* transform = gameObject->GetTransform(); Math::Quaternion worldRotation = transform->GetLocalRotation(); for (const Components::TransformComponent* parent = transform->GetParent(); parent != nullptr; parent = parent->GetParent()) { worldRotation = parent->GetLocalRotation() * worldRotation; } return worldRotation.Normalized(); } Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) { switch (axis) { case SceneViewportRotateGizmoAxis::X: return Math::Color(0.91f, 0.09f, 0.05f, 1.0f); case SceneViewportRotateGizmoAxis::Y: return Math::Color(0.45f, 1.0f, 0.12f, 1.0f); case SceneViewportRotateGizmoAxis::Z: return Math::Color(0.11f, 0.29f, 1.0f, 1.0f); case SceneViewportRotateGizmoAxis::View: return Math::Color(0.78f, 0.78f, 0.78f, 0.9f); case SceneViewportRotateGizmoAxis::None: default: return Math::Color::White(); } } Math::Vector3 GetRotateAxisVector( SceneViewportRotateGizmoAxis axis, const SceneViewportOverlayData& overlay, const Math::Quaternion& axisOrientation) { switch (axis) { case SceneViewportRotateGizmoAxis::X: case SceneViewportRotateGizmoAxis::Y: case SceneViewportRotateGizmoAxis::Z: return NormalizeVector3(axisOrientation * GetBaseRotateAxisVector(axis), GetBaseRotateAxisVector(axis)); case SceneViewportRotateGizmoAxis::View: return NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward()); case SceneViewportRotateGizmoAxis::None: default: return Math::Vector3::Zero(); } } bool GetRotateRingBasis( SceneViewportRotateGizmoAxis axis, const SceneViewportOverlayData& overlay, const Math::Quaternion& axisOrientation, Math::Vector3& outBasisA, Math::Vector3& outBasisB) { switch (axis) { case SceneViewportRotateGizmoAxis::X: outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up()); outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward()); return true; case SceneViewportRotateGizmoAxis::Y: outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward()); outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right()); return true; case SceneViewportRotateGizmoAxis::Z: outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right()); outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up()); return true; case SceneViewportRotateGizmoAxis::View: outBasisA = NormalizeVector3(overlay.cameraRight, Math::Vector3::Right()); outBasisB = NormalizeVector3(overlay.cameraUp, Math::Vector3::Up()); return outBasisA.SqrMagnitude() > Math::EPSILON && outBasisB.SqrMagnitude() > Math::EPSILON; case SceneViewportRotateGizmoAxis::None: default: outBasisA = Math::Vector3::Zero(); outBasisB = Math::Vector3::Zero(); return false; } } float GetRotateRingRadiusPixels(SceneViewportRotateGizmoAxis axis) { return axis == SceneViewportRotateGizmoAxis::View ? kRotateGizmoViewRadiusPixels : kRotateGizmoAxisRadiusPixels; } 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; } float NormalizeSignedAngleRadians(float radians) { while (radians > Math::PI) { radians -= Math::PI * 2.0f; } while (radians < -Math::PI) { radians += Math::PI * 2.0f; } return radians; } SceneViewportRotateGizmoAxis GetRotateAxisForIndex(size_t index) { switch (index) { case 0: return SceneViewportRotateGizmoAxis::X; case 1: return SceneViewportRotateGizmoAxis::Y; case 2: return SceneViewportRotateGizmoAxis::Z; case 3: return SceneViewportRotateGizmoAxis::View; default: return SceneViewportRotateGizmoAxis::None; } } bool TryComputeRingAngleFromWorldDirection( SceneViewportRotateGizmoAxis axis, const SceneViewportOverlayData& overlay, const Math::Quaternion& axisOrientation, const Math::Vector3& directionWorld, float& outAngle) { Math::Vector3 basisA = Math::Vector3::Zero(); Math::Vector3 basisB = Math::Vector3::Zero(); if (!GetRotateRingBasis(axis, overlay, axisOrientation, basisA, basisB)) { return false; } const Math::Vector3 direction = directionWorld.Normalized(); const float projectedX = Math::Vector3::Dot(direction, basisA); const float projectedY = Math::Vector3::Dot(direction, basisB); if (projectedX * projectedX + projectedY * projectedY <= Math::EPSILON) { return false; } outAngle = std::atan2(projectedY, projectedX); return true; } } // namespace void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) { BuildDrawData(context); if (m_activeAxis == SceneViewportRotateGizmoAxis::None && IsMouseInsideViewport(context)) { m_hoveredAxis = EvaluateHit(context.mousePosition).axis; } else if (m_activeAxis == SceneViewportRotateGizmoAxis::None) { m_hoveredAxis = SceneViewportRotateGizmoAxis::None; } else { m_hoveredAxis = m_activeAxis; } RefreshHandleState(); } bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContext& context, IUndoManager& undoManager) { if (m_activeAxis != SceneViewportRotateGizmoAxis::None || m_hoveredAxis == SceneViewportRotateGizmoAxis::None || context.selectedObject == nullptr || !m_drawData.visible || undoManager.HasPendingInteractiveChange()) { return false; } const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition; const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay, context.axisOrientation); if (worldAxis.SqrMagnitude() <= Math::EPSILON) { return false; } const Math::Plane dragPlane = BuildSceneViewportPlaneFromPointNormal(pivotWorldPosition, worldAxis); Math::Vector3 startDirection = Math::Vector3::Zero(); bool useScreenSpaceDrag = true; Math::Ray worldRay; if (BuildSceneViewportRay( context.overlay, context.viewportSize, context.mousePosition, worldRay)) { float hitDistance = 0.0f; if (worldRay.Intersects(dragPlane, hitDistance)) { const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance); startDirection = Math::Vector3::ProjectOnPlane(hitPoint - pivotWorldPosition, worldAxis); if (startDirection.SqrMagnitude() > Math::EPSILON) { useScreenSpaceDrag = false; } } } float startRingAngle = 0.0f; if (useScreenSpaceDrag) { if (!TryGetClosestRingAngle(m_hoveredAxis, context.mousePosition, false, startRingAngle)) { return false; } } else { if (!TryComputeRingAngleFromWorldDirection( m_hoveredAxis, context.overlay, context.axisOrientation, startDirection, startRingAngle)) { return false; } } undoManager.BeginInteractiveChange("Rotate Gizmo"); if (!undoManager.HasPendingInteractiveChange()) { return false; } m_activeAxis = m_hoveredAxis; m_activeEntityId = context.selectedObject->GetID(); m_localSpace = context.localSpace && m_hoveredAxis != SceneViewportRotateGizmoAxis::View; m_rotateAroundSharedPivot = context.rotateAroundSharedPivot; m_activeWorldAxis = worldAxis.Normalized(); m_screenSpaceDrag = useScreenSpaceDrag; m_dragPlane = dragPlane; m_dragStartRingAngle = startRingAngle; m_dragCurrentDeltaRadians = 0.0f; m_dragStartPivotWorldPosition = pivotWorldPosition; m_dragObjects = context.selectedObjects; if (m_dragObjects.empty()) { m_dragObjects.push_back(context.selectedObject); } m_dragStartWorldPositions.clear(); m_dragStartWorldRotations.clear(); m_dragStartWorldPositions.reserve(m_dragObjects.size()); m_dragStartWorldRotations.reserve(m_dragObjects.size()); for (Components::GameObject* gameObject : m_dragObjects) { if (gameObject != nullptr && gameObject->GetTransform() != nullptr) { m_dragStartWorldPositions.push_back(gameObject->GetTransform()->GetPosition()); m_dragStartWorldRotations.push_back(gameObject->GetTransform()->GetRotation()); } else { m_dragStartWorldPositions.push_back(Math::Vector3::Zero()); m_dragStartWorldRotations.push_back(Math::Quaternion::Identity()); } } RefreshHandleState(); return true; } void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& context) { if (m_activeAxis == SceneViewportRotateGizmoAxis::None || context.selectedObject == nullptr || context.selectedObject->GetID() != m_activeEntityId || m_dragObjects.empty() || m_dragObjects.size() != m_dragStartWorldPositions.size() || m_dragObjects.size() != m_dragStartWorldRotations.size()) { return; } float currentRingAngle = 0.0f; if (m_screenSpaceDrag) { if (!TryGetClosestRingAngle(m_activeAxis, context.mousePosition, false, currentRingAngle)) { return; } } else { 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 pivotWorldPosition = m_dragStartPivotWorldPosition; const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance); const Math::Vector3 currentDirection = Math::Vector3::ProjectOnPlane(hitPoint - pivotWorldPosition, m_activeWorldAxis); if (currentDirection.SqrMagnitude() <= Math::EPSILON) { return; } if (!TryComputeRingAngleFromWorldDirection( m_activeAxis, context.overlay, context.axisOrientation, currentDirection, currentRingAngle)) { return; } } const float deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle); m_dragCurrentDeltaRadians = deltaRadians; const Math::Quaternion worldDeltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians); const Math::Vector3 localAxis = GetBaseRotateAxisVector(m_activeAxis); const Math::Quaternion localDeltaRotation = localAxis.SqrMagnitude() > Math::EPSILON ? Math::Quaternion::FromAxisAngle(localAxis, deltaRadians) : Math::Quaternion::Identity(); for (size_t index = 0; index < m_dragObjects.size(); ++index) { Components::GameObject* gameObject = m_dragObjects[index]; if (gameObject == nullptr || gameObject->GetTransform() == nullptr) { continue; } if (m_rotateAroundSharedPivot) { gameObject->GetTransform()->SetPosition( m_dragStartPivotWorldPosition + worldDeltaRotation * (m_dragStartWorldPositions[index] - m_dragStartPivotWorldPosition)); } else { gameObject->GetTransform()->SetPosition(m_dragStartWorldPositions[index]); } if (m_localSpace && m_activeAxis != SceneViewportRotateGizmoAxis::View) { gameObject->GetTransform()->SetRotation(m_dragStartWorldRotations[index] * localDeltaRotation); } else { gameObject->GetTransform()->SetRotation(worldDeltaRotation * m_dragStartWorldRotations[index]); } } SceneViewportRotateGizmoContext drawContext = context; drawContext.pivotWorldPosition = m_dragStartPivotWorldPosition; if (drawContext.localSpace && drawContext.selectedObject != nullptr) { drawContext.axisOrientation = ComputeStableWorldRotation(drawContext.selectedObject); } BuildDrawData(drawContext); m_hoveredAxis = m_activeAxis; RefreshHandleState(); } void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) { if (m_activeAxis == SceneViewportRotateGizmoAxis::None) { return; } if (undoManager.HasPendingInteractiveChange()) { undoManager.FinalizeInteractiveChange(); } m_activeAxis = SceneViewportRotateGizmoAxis::None; m_activeEntityId = 0; m_screenSpaceDrag = false; m_localSpace = false; m_rotateAroundSharedPivot = false; m_activeWorldAxis = Math::Vector3::Zero(); m_dragStartRingAngle = 0.0f; m_dragCurrentDeltaRadians = 0.0f; m_dragStartPivotWorldPosition = Math::Vector3::Zero(); m_dragObjects.clear(); m_dragStartWorldPositions.clear(); m_dragStartWorldRotations.clear(); RefreshHandleState(); } void SceneViewportRotateGizmo::CancelDrag(IUndoManager* undoManager) { if (undoManager != nullptr && undoManager->HasPendingInteractiveChange()) { undoManager->CancelInteractiveChange(); } m_activeAxis = SceneViewportRotateGizmoAxis::None; m_activeEntityId = 0; m_screenSpaceDrag = false; m_localSpace = false; m_rotateAroundSharedPivot = false; m_activeWorldAxis = Math::Vector3::Zero(); m_dragStartRingAngle = 0.0f; m_dragCurrentDeltaRadians = 0.0f; m_dragStartPivotWorldPosition = Math::Vector3::Zero(); m_dragObjects.clear(); m_dragStartWorldPositions.clear(); m_dragStartWorldRotations.clear(); m_hoveredAxis = SceneViewportRotateGizmoAxis::None; RefreshHandleState(); } bool SceneViewportRotateGizmo::IsHoveringHandle() const { return m_hoveredAxis != SceneViewportRotateGizmoAxis::None; } bool SceneViewportRotateGizmo::IsActive() const { return m_activeAxis != SceneViewportRotateGizmoAxis::None; } uint64_t SceneViewportRotateGizmo::GetActiveEntityId() const { return m_activeEntityId; } const SceneViewportRotateGizmoDrawData& SceneViewportRotateGizmo::GetDrawData() const { return m_drawData; } SceneViewportRotateGizmoHitResult SceneViewportRotateGizmo::EvaluateHit(const Math::Vector2& mousePosition) const { SceneViewportRotateGizmoHitResult result = {}; if (!m_drawData.visible) { return result; } const float hoverThresholdSq = kRotateGizmoHoverThresholdPixels * kRotateGizmoHoverThresholdPixels; for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) { if (!handle.visible) { continue; } for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) { if (!segment.visible || (handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) { continue; } const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end); if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) { continue; } result.axis = handle.axis; result.distanceSq = distanceSq; } } return result; } void SceneViewportRotateGizmo::SetHoveredHandle(SceneViewportRotateGizmoAxis axis) { if (m_activeAxis != SceneViewportRotateGizmoAxis::None) { return; } m_hoveredAxis = axis; RefreshHandleState(); } void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoContext& context) { m_drawData = {}; if ((context.selectedObject == nullptr && context.selectedObjects.empty()) || !context.overlay.valid || context.viewportSize.x <= 1.0f || context.viewportSize.y <= 1.0f) { return; } const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition; const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, context.viewportSize.y, pivotWorldPosition); if (!projectedPivot.visible) { return; } const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel( context.overlay, pivotWorldPosition, context.viewportSize.y); if (worldUnitsPerPixel <= Math::EPSILON) { return; } m_drawData.visible = true; m_drawData.pivot = projectedPivot.screenPosition; const bool hasActiveDragFeedback = !context.localSpace && m_activeAxis != SceneViewportRotateGizmoAxis::None && m_activeAxis != SceneViewportRotateGizmoAxis::View && std::abs(m_dragCurrentDeltaRadians) > Math::EPSILON; const Math::Quaternion dragFeedbackRotation = hasActiveDragFeedback ? Math::Quaternion::FromAxisAngle(m_activeWorldAxis, m_dragCurrentDeltaRadians) : Math::Quaternion::Identity(); for (size_t handleIndex = 0; handleIndex < m_drawData.handles.size(); ++handleIndex) { SceneViewportRotateGizmoHandleDrawData& handle = m_drawData.handles[handleIndex]; handle.axis = GetRotateAxisForIndex(handleIndex); handle.color = GetRotateAxisBaseColor(handle.axis); const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(handle.axis); Math::Vector3 basisA = Math::Vector3::Zero(); Math::Vector3 basisB = Math::Vector3::Zero(); if (!GetRotateRingBasis(handle.axis, context.overlay, context.axisOrientation, basisA, basisB)) { continue; } if (hasActiveDragFeedback && handle.axis != SceneViewportRotateGizmoAxis::View) { basisA = dragFeedbackRotation * basisA; basisB = dragFeedbackRotation * basisB; } bool anyVisibleSegment = false; for (size_t segmentIndex = 0; segmentIndex < handle.segments.size(); ++segmentIndex) { const float angle0 = static_cast(segmentIndex) / static_cast(handle.segments.size()) * Math::PI * 2.0f; const float angle1 = static_cast(segmentIndex + 1) / static_cast(handle.segments.size()) * Math::PI * 2.0f; const float midAngle = (angle0 + angle1) * 0.5f; const Math::Vector3 startWorld = pivotWorldPosition + (basisA * std::cos(angle0) + basisB * std::sin(angle0)) * ringRadiusWorld; const Math::Vector3 endWorld = pivotWorldPosition + (basisA * std::cos(angle1) + basisB * std::sin(angle1)) * ringRadiusWorld; const Math::Vector3 midWorld = pivotWorldPosition + (basisA * std::cos(midAngle) + basisB * std::sin(midAngle)) * ringRadiusWorld; const SceneViewportProjectedPoint projectedStart = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, context.viewportSize.y, startWorld); const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, context.viewportSize.y, endWorld); if (projectedStart.ndcDepth < 0.0f || projectedStart.ndcDepth > 1.0f || projectedEnd.ndcDepth < 0.0f || projectedEnd.ndcDepth > 1.0f) { continue; } SceneViewportRotateGizmoSegmentDrawData& segment = handle.segments[segmentIndex]; segment.start = projectedStart.screenPosition; segment.end = projectedEnd.screenPosition; segment.startAngle = angle0; segment.endAngle = angle1; segment.visible = (segment.end - segment.start).SqrMagnitude() > Math::EPSILON; if (!segment.visible) { continue; } anyVisibleSegment = true; if (handle.axis == SceneViewportRotateGizmoAxis::View) { segment.frontFacing = true; } else { const Math::Vector3 radial = (midWorld - pivotWorldPosition).Normalized(); segment.frontFacing = Math::Vector3::Dot( radial, NormalizeVector3(context.overlay.cameraForward, Math::Vector3::Forward())) < 0.0f; } } handle.visible = anyVisibleSegment; } if (m_activeAxis != SceneViewportRotateGizmoAxis::None && std::abs(m_dragCurrentDeltaRadians) >= kRotateGizmoAngleFillMinRadians) { SceneViewportRotateGizmoAngleFillDrawData& angleFill = m_drawData.angleFill; angleFill.axis = m_activeAxis; angleFill.pivot = projectedPivot.screenPosition; angleFill.fillColor = Math::Color(1.0f, 0.92f, 0.12f, 0.22f); angleFill.outlineColor = Math::Color(1.0f, 0.92f, 0.12f, 0.95f); Math::Vector3 basisA = Math::Vector3::Zero(); Math::Vector3 basisB = Math::Vector3::Zero(); if (GetRotateRingBasis(m_activeAxis, context.overlay, context.axisOrientation, basisA, basisB)) { const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(m_activeAxis); const float sweepRadians = NormalizeSignedAngleRadians(m_dragCurrentDeltaRadians); const float sweepAbs = std::abs(sweepRadians); const size_t stepCount = std::clamp( static_cast(std::ceil( sweepAbs / (Math::PI * 2.0f) * static_cast(kSceneViewportRotateGizmoSegmentCount))), static_cast(1), kSceneViewportRotateGizmoAngleFillPointCount - 1); bool valid = true; for (size_t pointIndex = 0; pointIndex <= stepCount; ++pointIndex) { const float t = static_cast(pointIndex) / static_cast(stepCount); const float angle = m_dragStartRingAngle + sweepRadians * t; const Math::Vector3 worldPoint = pivotWorldPosition + (basisA * std::cos(angle) + basisB * std::sin(angle)) * ringRadiusWorld; const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, context.viewportSize.y, worldPoint); if (projectedPoint.ndcDepth < 0.0f || projectedPoint.ndcDepth > 1.0f) { valid = false; break; } angleFill.arcPoints[pointIndex] = projectedPoint.screenPosition; } if (valid) { angleFill.arcPointCount = stepCount + 1; angleFill.visible = true; } } } } void SceneViewportRotateGizmo::RefreshHandleState() { for (SceneViewportRotateGizmoHandleDrawData& 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() : GetRotateAxisBaseColor(handle.axis); } } bool SceneViewportRotateGizmo::TryGetClosestRingAngle( SceneViewportRotateGizmoAxis axis, const Math::Vector2& mousePosition, bool allowBackFacing, float& outAngle) const { if (!m_drawData.visible || axis == SceneViewportRotateGizmoAxis::None) { return false; } const SceneViewportRotateGizmoHandleDrawData* targetHandle = nullptr; for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) { if (handle.axis == axis && handle.visible) { targetHandle = &handle; break; } } if (targetHandle == nullptr) { return false; } const bool isViewHandle = axis == SceneViewportRotateGizmoAxis::View; const SceneViewportRotateGizmoSegmentDrawData* bestSegment = nullptr; float bestSegmentT = 0.0f; float bestDistanceSq = Math::FLOAT_MAX; for (const SceneViewportRotateGizmoSegmentDrawData& segment : targetHandle->segments) { if (!segment.visible || (!isViewHandle && !allowBackFacing && !segment.frontFacing)) { continue; } float segmentT = 0.0f; const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end, &segmentT); if (distanceSq >= bestDistanceSq) { continue; } bestDistanceSq = distanceSq; bestSegment = &segment; bestSegmentT = segmentT; } if (bestSegment == nullptr) { return false; } outAngle = bestSegment->startAngle + (bestSegment->endAngle - bestSegment->startAngle) * bestSegmentT; return true; } } // namespace Editor } // namespace XCEngine