#include "SceneViewportScaleGizmo.h" #include "Core/IUndoManager.h" #include "SceneViewportMath.h" #include #include #include #include namespace XCEngine { namespace Editor { namespace { constexpr float kScaleGizmoAxisLengthPixels = 110.0f; constexpr float kScaleGizmoCapHalfSizePixels = 6.5f; constexpr float kScaleGizmoCenterHalfSizePixels = 7.5f; constexpr float kScaleGizmoHoverThresholdPixels = 10.0f; constexpr float kScaleGizmoAxisScalePerPixel = 0.015f; constexpr float kScaleGizmoUniformScalePerPixel = 0.0125f; constexpr float kScaleGizmoMinScale = 0.001f; constexpr float kScaleGizmoVisualScaleMin = 0.4f; constexpr float kScaleGizmoVisualScaleMax = 2.25f; Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) { return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized(); } bool IsMouseInsideViewport(const SceneViewportScaleGizmoContext& 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::Color WithAlpha(const Math::Color& color, float alpha) { return Math::Color(color.r, color.g, color.b, alpha); } Math::Color GetScaleHandleBaseColor(SceneViewportScaleGizmoHandle handle) { switch (handle) { case SceneViewportScaleGizmoHandle::X: return Math::Color(0.91f, 0.09f, 0.05f, 1.0f); case SceneViewportScaleGizmoHandle::Y: return Math::Color(0.45f, 1.0f, 0.12f, 1.0f); case SceneViewportScaleGizmoHandle::Z: return Math::Color(0.11f, 0.29f, 1.0f, 1.0f); case SceneViewportScaleGizmoHandle::Uniform: return Math::Color(0.78f, 0.78f, 0.78f, 1.0f); case SceneViewportScaleGizmoHandle::None: default: return Math::Color::White(); } } SceneViewportScaleGizmoHandle GetHandleForIndex(size_t index) { switch (index) { case 0: return SceneViewportScaleGizmoHandle::X; case 1: return SceneViewportScaleGizmoHandle::Y; case 2: return SceneViewportScaleGizmoHandle::Z; default: return SceneViewportScaleGizmoHandle::None; } } Math::Vector3 GetHandleWorldAxis( SceneViewportScaleGizmoHandle handle, const Components::TransformComponent& transform) { switch (handle) { case SceneViewportScaleGizmoHandle::X: return NormalizeVector3(transform.GetRight(), Math::Vector3::Right()); case SceneViewportScaleGizmoHandle::Y: return NormalizeVector3(transform.GetUp(), Math::Vector3::Up()); case SceneViewportScaleGizmoHandle::Z: return NormalizeVector3(transform.GetForward(), Math::Vector3::Forward()); case SceneViewportScaleGizmoHandle::Uniform: case SceneViewportScaleGizmoHandle::None: default: return Math::Vector3::Zero(); } } 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; } bool IsPointInsideSquare( const Math::Vector2& point, const Math::Vector2& center, float halfSize) { return std::abs(point.x - center.x) <= halfSize && std::abs(point.y - center.y) <= halfSize; } float ClampPositiveScale(float value) { return std::max(value, kScaleGizmoMinScale); } float ComputeVisualScaleFactor(float current, float start) { if (std::abs(start) <= Math::EPSILON) { return 1.0f; } return std::clamp( current / start, kScaleGizmoVisualScaleMin, kScaleGizmoVisualScaleMax); } } // namespace void SceneViewportScaleGizmo::Update(const SceneViewportScaleGizmoContext& context) { BuildDrawData(context); if (m_activeHandle == SceneViewportScaleGizmoHandle::None && IsMouseInsideViewport(context)) { m_hoveredHandle = HitTestHandle(context.mousePosition); } else if (m_activeHandle == SceneViewportScaleGizmoHandle::None) { m_hoveredHandle = SceneViewportScaleGizmoHandle::None; } else { m_hoveredHandle = m_activeHandle; } RefreshHandleState(); } bool SceneViewportScaleGizmo::TryBeginDrag(const SceneViewportScaleGizmoContext& context, IUndoManager& undoManager) { if (m_activeHandle != SceneViewportScaleGizmoHandle::None || m_hoveredHandle == SceneViewportScaleGizmoHandle::None || context.selectedObject == nullptr || !m_drawData.visible || undoManager.HasPendingInteractiveChange()) { return false; } Math::Vector2 activeScreenDirection = Math::Vector2::Zero(); if (m_hoveredHandle != SceneViewportScaleGizmoHandle::Uniform) { const SceneViewportScaleGizmoAxisHandleDrawData* handle = FindAxisHandleDrawData(m_hoveredHandle); if (handle == nullptr || !handle->visible) { return false; } activeScreenDirection = handle->end - handle->start; if (activeScreenDirection.SqrMagnitude() <= Math::EPSILON) { const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition(); if (!ProjectSceneViewportAxisDirectionAtPoint( context.overlay, context.viewportSize.x, context.viewportSize.y, pivotWorldPosition, GetHandleWorldAxis(m_hoveredHandle, *context.selectedObject->GetTransform()), activeScreenDirection)) { return false; } } else { activeScreenDirection = activeScreenDirection.Normalized(); } } undoManager.BeginInteractiveChange("Scale Gizmo"); if (!undoManager.HasPendingInteractiveChange()) { return false; } m_activeHandle = m_hoveredHandle; m_activeEntityId = context.selectedObject->GetID(); m_dragStartLocalScale = context.selectedObject->GetTransform()->GetLocalScale(); m_dragCurrentVisualScale = Math::Vector3::One(); m_dragStartMousePosition = context.mousePosition; m_activeScreenDirection = activeScreenDirection; RefreshHandleState(); return true; } void SceneViewportScaleGizmo::UpdateDrag(const SceneViewportScaleGizmoContext& context) { if (m_activeHandle == SceneViewportScaleGizmoHandle::None || context.selectedObject == nullptr || context.selectedObject->GetID() != m_activeEntityId) { return; } const Math::Vector2 mouseDelta = context.mousePosition - m_dragStartMousePosition; Math::Vector3 localScale = m_dragStartLocalScale; if (m_activeHandle == SceneViewportScaleGizmoHandle::Uniform) { const float signedPixels = mouseDelta.x - mouseDelta.y; const float factor = std::max(1.0f + signedPixels * kScaleGizmoUniformScalePerPixel, kScaleGizmoMinScale); localScale.x = ClampPositiveScale(m_dragStartLocalScale.x * factor); localScale.y = ClampPositiveScale(m_dragStartLocalScale.y * factor); localScale.z = ClampPositiveScale(m_dragStartLocalScale.z * factor); } else { if (m_activeScreenDirection.SqrMagnitude() <= Math::EPSILON) { return; } const float signedPixels = Math::Vector2::Dot(mouseDelta, m_activeScreenDirection); const float factor = std::max(1.0f + signedPixels * kScaleGizmoAxisScalePerPixel, kScaleGizmoMinScale); switch (m_activeHandle) { case SceneViewportScaleGizmoHandle::X: localScale.x = ClampPositiveScale(m_dragStartLocalScale.x * factor); break; case SceneViewportScaleGizmoHandle::Y: localScale.y = ClampPositiveScale(m_dragStartLocalScale.y * factor); break; case SceneViewportScaleGizmoHandle::Z: localScale.z = ClampPositiveScale(m_dragStartLocalScale.z * factor); break; case SceneViewportScaleGizmoHandle::Uniform: case SceneViewportScaleGizmoHandle::None: default: break; } } context.selectedObject->GetTransform()->SetLocalScale(localScale); switch (m_activeHandle) { case SceneViewportScaleGizmoHandle::X: m_dragCurrentVisualScale = Math::Vector3( ComputeVisualScaleFactor(localScale.x, m_dragStartLocalScale.x), 1.0f, 1.0f); break; case SceneViewportScaleGizmoHandle::Y: m_dragCurrentVisualScale = Math::Vector3( 1.0f, ComputeVisualScaleFactor(localScale.y, m_dragStartLocalScale.y), 1.0f); break; case SceneViewportScaleGizmoHandle::Z: m_dragCurrentVisualScale = Math::Vector3( 1.0f, 1.0f, ComputeVisualScaleFactor(localScale.z, m_dragStartLocalScale.z)); break; case SceneViewportScaleGizmoHandle::Uniform: m_dragCurrentVisualScale = Math::Vector3( ComputeVisualScaleFactor(localScale.x, m_dragStartLocalScale.x), ComputeVisualScaleFactor(localScale.y, m_dragStartLocalScale.y), ComputeVisualScaleFactor(localScale.z, m_dragStartLocalScale.z)); break; case SceneViewportScaleGizmoHandle::None: default: m_dragCurrentVisualScale = Math::Vector3::One(); break; } } void SceneViewportScaleGizmo::EndDrag(IUndoManager& undoManager) { if (m_activeHandle == SceneViewportScaleGizmoHandle::None) { return; } if (undoManager.HasPendingInteractiveChange()) { undoManager.FinalizeInteractiveChange(); } m_activeHandle = SceneViewportScaleGizmoHandle::None; m_activeEntityId = 0; m_dragStartLocalScale = Math::Vector3::Zero(); m_dragCurrentVisualScale = Math::Vector3::One(); m_dragStartMousePosition = Math::Vector2::Zero(); m_activeScreenDirection = Math::Vector2::Zero(); RefreshHandleState(); } void SceneViewportScaleGizmo::CancelDrag(IUndoManager* undoManager) { if (undoManager != nullptr && undoManager->HasPendingInteractiveChange()) { undoManager->CancelInteractiveChange(); } m_hoveredHandle = SceneViewportScaleGizmoHandle::None; m_activeHandle = SceneViewportScaleGizmoHandle::None; m_activeEntityId = 0; m_dragStartLocalScale = Math::Vector3::Zero(); m_dragCurrentVisualScale = Math::Vector3::One(); m_dragStartMousePosition = Math::Vector2::Zero(); m_activeScreenDirection = Math::Vector2::Zero(); RefreshHandleState(); } bool SceneViewportScaleGizmo::IsHoveringHandle() const { return m_hoveredHandle != SceneViewportScaleGizmoHandle::None; } bool SceneViewportScaleGizmo::IsActive() const { return m_activeHandle != SceneViewportScaleGizmoHandle::None; } uint64_t SceneViewportScaleGizmo::GetActiveEntityId() const { return m_activeEntityId; } const SceneViewportScaleGizmoDrawData& SceneViewportScaleGizmo::GetDrawData() const { return m_drawData; } void SceneViewportScaleGizmo::BuildDrawData(const SceneViewportScaleGizmoContext& context) { m_drawData = {}; const Components::GameObject* selectedObject = context.selectedObject; if (selectedObject == nullptr || !context.overlay.valid || context.viewportSize.x <= 1.0f || context.viewportSize.y <= 1.0f) { return; } const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition(); 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.centerHandle.visible = true; m_drawData.centerHandle.center = projectedPivot.screenPosition; m_drawData.centerHandle.halfSize = kScaleGizmoCenterHalfSizePixels; if (context.uniformOnly) { return; } const bool hasVisualDragFeedback = m_activeHandle != SceneViewportScaleGizmoHandle::None && selectedObject->GetID() == m_activeEntityId; for (size_t index = 0; index < m_drawData.axisHandles.size(); ++index) { SceneViewportScaleGizmoAxisHandleDrawData& handle = m_drawData.axisHandles[index]; handle.handle = GetHandleForIndex(index); handle.start = projectedPivot.screenPosition; handle.capHalfSize = kScaleGizmoCapHalfSizePixels; handle.color = GetScaleHandleBaseColor(handle.handle); const Math::Vector3 axisWorld = GetHandleWorldAxis(handle.handle, *selectedObject->GetTransform()); if (axisWorld.SqrMagnitude() <= Math::EPSILON) { continue; } float axisVisualScale = 1.0f; if (hasVisualDragFeedback) { switch (handle.handle) { case SceneViewportScaleGizmoHandle::X: axisVisualScale = m_dragCurrentVisualScale.x; break; case SceneViewportScaleGizmoHandle::Y: axisVisualScale = m_dragCurrentVisualScale.y; break; case SceneViewportScaleGizmoHandle::Z: axisVisualScale = m_dragCurrentVisualScale.z; break; case SceneViewportScaleGizmoHandle::Uniform: case SceneViewportScaleGizmoHandle::None: default: break; } } const float axisLengthWorld = worldUnitsPerPixel * kScaleGizmoAxisLengthPixels * axisVisualScale; const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, context.viewportSize.y, pivotWorldPosition + axisWorld * axisLengthWorld); if (projectedEnd.ndcDepth < 0.0f || projectedEnd.ndcDepth > 1.0f) { continue; } if ((projectedEnd.screenPosition - projectedPivot.screenPosition).SqrMagnitude() <= Math::EPSILON) { continue; } handle.visible = true; handle.end = projectedEnd.screenPosition; handle.capCenter = projectedEnd.screenPosition; } } void SceneViewportScaleGizmo::RefreshHandleState() { for (SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) { if (!handle.visible) { continue; } handle.hovered = handle.handle == m_hoveredHandle; handle.active = handle.handle == m_activeHandle; handle.color = (handle.hovered || handle.active) ? Math::Color::Yellow() : GetScaleHandleBaseColor(handle.handle); } if (!m_drawData.centerHandle.visible) { return; } m_drawData.centerHandle.hovered = m_hoveredHandle == SceneViewportScaleGizmoHandle::Uniform; m_drawData.centerHandle.active = m_activeHandle == SceneViewportScaleGizmoHandle::Uniform; const Math::Color baseColor = (m_drawData.centerHandle.hovered || m_drawData.centerHandle.active) ? Math::Color::Yellow() : GetScaleHandleBaseColor(SceneViewportScaleGizmoHandle::Uniform); m_drawData.centerHandle.fillColor = WithAlpha( baseColor, m_drawData.centerHandle.active ? 0.96f : (m_drawData.centerHandle.hovered ? 0.9f : 0.82f)); m_drawData.centerHandle.outlineColor = WithAlpha( baseColor, m_drawData.centerHandle.active ? 1.0f : (m_drawData.centerHandle.hovered ? 0.96f : 0.88f)); } SceneViewportScaleGizmoHandle SceneViewportScaleGizmo::HitTestHandle(const Math::Vector2& mousePosition) const { if (!m_drawData.visible) { return SceneViewportScaleGizmoHandle::None; } if (m_drawData.centerHandle.visible && IsPointInsideSquare( mousePosition, m_drawData.centerHandle.center, m_drawData.centerHandle.halfSize + 2.0f)) { return SceneViewportScaleGizmoHandle::Uniform; } SceneViewportScaleGizmoHandle bestHandle = SceneViewportScaleGizmoHandle::None; float bestDistanceSq = Math::FLOAT_MAX; for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) { if (!handle.visible || !IsPointInsideSquare(mousePosition, handle.capCenter, handle.capHalfSize + 2.0f)) { continue; } const float distanceSq = (handle.capCenter - mousePosition).SqrMagnitude(); if (distanceSq >= bestDistanceSq) { continue; } bestDistanceSq = distanceSq; bestHandle = handle.handle; } if (bestHandle != SceneViewportScaleGizmoHandle::None) { return bestHandle; } bestDistanceSq = kScaleGizmoHoverThresholdPixels * kScaleGizmoHoverThresholdPixels; for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) { if (!handle.visible) { continue; } const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end); if (distanceSq > bestDistanceSq) { continue; } bestDistanceSq = distanceSq; bestHandle = handle.handle; } return bestHandle; } const SceneViewportScaleGizmoAxisHandleDrawData* SceneViewportScaleGizmo::FindAxisHandleDrawData( SceneViewportScaleGizmoHandle handle) const { for (const SceneViewportScaleGizmoAxisHandleDrawData& drawHandle : m_drawData.axisHandles) { if (drawHandle.handle == handle) { return &drawHandle; } } return nullptr; } } // namespace Editor } // namespace XCEngine