#include "Features/Scene/SceneViewportTransformGizmoSupport.h" #include #include #include #include namespace XCEngine::UI::Editor::App::SceneViewportGizmoSupport { namespace { constexpr float kMoveGizmoHandleLengthPixels = 100.0f; constexpr float kMoveGizmoHoverThresholdPixels = 10.0f; constexpr float kMoveGizmoPlaneInsetPixels = 2.0f; constexpr float kMoveGizmoPlaneSizePixels = 24.0f; constexpr float kRotateGizmoAxisRadiusPixels = 96.0f; constexpr float kRotateGizmoViewRadiusPixels = 106.0f; constexpr float kRotateGizmoHoverThresholdPixels = 9.0f; constexpr float kRotateGizmoAngleFillMinRadians = 0.01f; 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; constexpr float kSceneViewportMoveArrowLengthPixels = 14.0f; constexpr float kSceneViewportMoveArrowHalfWidthPixels = 7.0f; using ::XCEngine::Components::GameObject; using ::XCEngine::Components::TransformComponent; using ::XCEngine::Math::Color; using ::XCEngine::Math::Matrix4x4; using ::XCEngine::Math::Plane; using ::XCEngine::Math::Quaternion; using ::XCEngine::Math::Ray; using ::XCEngine::Math::Vector2; using ::XCEngine::Math::Vector3; using ::XCEngine::Math::Vector4; Vector3 NormalizeVector3(const Vector3& value, const Vector3& fallback) { return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized(); } Vector2 NormalizeVector2( const Vector2& value, const Vector2& fallback = Vector2(1.0f, 0.0f)) { const float lengthSq = value.SqrMagnitude(); if (lengthSq <= Math::EPSILON) { return fallback; } return value / std::sqrt(lengthSq); } Color WithAlpha(const Color& color, float alpha) { return Color(color.r, color.g, color.b, alpha); } Color LerpColor(const Color& a, const Color& b, float t) { return Color( a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t, a.a + (b.a - a.a) * t); } bool PointInTriangle( const Vector2& point, const Vector2& a, const Vector2& b, const Vector2& c) { const float ab = (b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x); const float bc = (c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x); const float ca = (a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x); 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 Vector2& point, const std::array& corners) { return PointInTriangle(point, corners[0], corners[1], corners[2]) || PointInTriangle(point, corners[0], corners[2], corners[3]); } Vector2 QuadCenter(const std::array& corners) { Vector2 center = Vector2::Zero(); for (const Vector2& corner : corners) { center += corner; } return center / 4.0f; } float Cross2D(const Vector2& a, const 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 Vector2& current = corners[index]; const Vector2& next = corners[(index + 1u) % corners.size()]; areaTwice += Cross2D(current, next); } return areaTwice; } float ComputeWorldUnitsPerPixel( const SceneViewportOverlayData& overlay, const Vector3& worldPoint, float viewportHeight) { if (!overlay.valid || viewportHeight <= 1.0f) { return 0.0f; } const Vector3 cameraForward = NormalizeVector3(overlay.cameraForward, Vector3::Forward()); const float depth = 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 IsMouseInsideViewport(const Vector2& mousePosition, const Vector2& viewportSize) { return mousePosition.x >= 0.0f && mousePosition.y >= 0.0f && mousePosition.x <= viewportSize.x && mousePosition.y <= viewportSize.y; } Vector3 GetBaseAxisVector(SceneViewportGizmoAxis axis) { switch (axis) { case SceneViewportGizmoAxis::X: return Vector3::Right(); case SceneViewportGizmoAxis::Y: return Vector3::Up(); case SceneViewportGizmoAxis::Z: return Vector3::Forward(); case SceneViewportGizmoAxis::None: default: return Vector3::Zero(); } } Vector3 GetAxisVector( SceneViewportGizmoAxis axis, const Quaternion& orientation) { const Vector3 baseAxis = GetBaseAxisVector(axis); const Vector3 orientedAxis = orientation * baseAxis; return orientedAxis.SqrMagnitude() <= Math::EPSILON ? baseAxis : orientedAxis.Normalized(); } Color GetAxisBaseColor(SceneViewportGizmoAxis axis) { switch (axis) { case SceneViewportGizmoAxis::X: return Color(0.91f, 0.09f, 0.05f, 1.0f); case SceneViewportGizmoAxis::Y: return Color(0.45f, 1.0f, 0.12f, 1.0f); case SceneViewportGizmoAxis::Z: return Color(0.11f, 0.29f, 1.0f, 1.0f); case SceneViewportGizmoAxis::None: default: return Color::White(); } } 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 Quaternion& orientation, Vector3& outAxisA, 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 = Vector3::Zero(); outAxisB = Vector3::Zero(); return; } } Vector3 GetPlaneNormal( SceneViewportGizmoPlane plane, const 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 Vector3::Zero(); } } 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 Color::White(); } } Vector3 GetBaseRotateAxisVector(SceneViewportRotateGizmoAxis axis) { switch (axis) { case SceneViewportRotateGizmoAxis::X: return Vector3::Right(); case SceneViewportRotateGizmoAxis::Y: return Vector3::Up(); case SceneViewportRotateGizmoAxis::Z: return Vector3::Forward(); case SceneViewportRotateGizmoAxis::View: case SceneViewportRotateGizmoAxis::None: default: return Vector3::Zero(); } } Quaternion ComputeStableWorldRotation(const GameObject* gameObject) { if (gameObject == nullptr || gameObject->GetTransform() == nullptr) { return Quaternion::Identity(); } const TransformComponent* transform = gameObject->GetTransform(); Quaternion worldRotation = transform->GetLocalRotation(); for (const TransformComponent* parent = transform->GetParent(); parent != nullptr; parent = parent->GetParent()) { worldRotation = parent->GetLocalRotation() * worldRotation; } return worldRotation.Normalized(); } Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) { switch (axis) { case SceneViewportRotateGizmoAxis::X: return Color(0.91f, 0.09f, 0.05f, 1.0f); case SceneViewportRotateGizmoAxis::Y: return Color(0.45f, 1.0f, 0.12f, 1.0f); case SceneViewportRotateGizmoAxis::Z: return Color(0.11f, 0.29f, 1.0f, 1.0f); case SceneViewportRotateGizmoAxis::View: return Color(0.78f, 0.78f, 0.78f, 0.9f); case SceneViewportRotateGizmoAxis::None: default: return Color::White(); } } Vector3 GetRotateAxisVector( SceneViewportRotateGizmoAxis axis, const SceneViewportOverlayData& overlay, const 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, Vector3::Forward()); case SceneViewportRotateGizmoAxis::None: default: return Vector3::Zero(); } } bool GetRotateRingBasis( SceneViewportRotateGizmoAxis axis, const SceneViewportOverlayData& overlay, const Quaternion& axisOrientation, Vector3& outBasisA, Vector3& outBasisB) { switch (axis) { case SceneViewportRotateGizmoAxis::X: outBasisA = NormalizeVector3(axisOrientation * Vector3::Up(), Vector3::Up()); outBasisB = NormalizeVector3(axisOrientation * Vector3::Forward(), Vector3::Forward()); return true; case SceneViewportRotateGizmoAxis::Y: outBasisA = NormalizeVector3(axisOrientation * Vector3::Forward(), Vector3::Forward()); outBasisB = NormalizeVector3(axisOrientation * Vector3::Right(), Vector3::Right()); return true; case SceneViewportRotateGizmoAxis::Z: outBasisA = NormalizeVector3(axisOrientation * Vector3::Right(), Vector3::Right()); outBasisB = NormalizeVector3(axisOrientation * Vector3::Up(), Vector3::Up()); return true; case SceneViewportRotateGizmoAxis::View: outBasisA = NormalizeVector3(overlay.cameraRight, Vector3::Right()); outBasisB = NormalizeVector3(overlay.cameraUp, Vector3::Up()); return outBasisA.SqrMagnitude() > Math::EPSILON && outBasisB.SqrMagnitude() > Math::EPSILON; case SceneViewportRotateGizmoAxis::None: default: outBasisA = Vector3::Zero(); outBasisB = Vector3::Zero(); return false; } } float GetRotateRingRadiusPixels(SceneViewportRotateGizmoAxis axis) { return axis == SceneViewportRotateGizmoAxis::View ? kRotateGizmoViewRadiusPixels : kRotateGizmoAxisRadiusPixels; } 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 Quaternion& axisOrientation, const Vector3& directionWorld, float& outAngle) { Vector3 basisA = Vector3::Zero(); Vector3 basisB = Vector3::Zero(); if (!GetRotateRingBasis(axis, overlay, axisOrientation, basisA, basisB)) { return false; } const Vector3 direction = directionWorld.Normalized(); const float projectedX = Vector3::Dot(direction, basisA); const float projectedY = Vector3::Dot(direction, basisB); if (projectedX * projectedX + projectedY * projectedY <= Math::EPSILON) { return false; } outAngle = std::atan2(projectedY, projectedX); return true; } Color GetScaleHandleBaseColor(SceneViewportScaleGizmoHandle handle) { switch (handle) { case SceneViewportScaleGizmoHandle::X: return Color(0.91f, 0.09f, 0.05f, 1.0f); case SceneViewportScaleGizmoHandle::Y: return Color(0.45f, 1.0f, 0.12f, 1.0f); case SceneViewportScaleGizmoHandle::Z: return Color(0.11f, 0.29f, 1.0f, 1.0f); case SceneViewportScaleGizmoHandle::Uniform: return Color(0.78f, 0.78f, 0.78f, 1.0f); case SceneViewportScaleGizmoHandle::None: default: return 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; } } Vector3 GetHandleWorldAxis( SceneViewportScaleGizmoHandle handle, const TransformComponent& transform) { switch (handle) { case SceneViewportScaleGizmoHandle::X: return NormalizeVector3(transform.GetRight(), Vector3::Right()); case SceneViewportScaleGizmoHandle::Y: return NormalizeVector3(transform.GetUp(), Vector3::Up()); case SceneViewportScaleGizmoHandle::Z: return NormalizeVector3(transform.GetForward(), Vector3::Forward()); case SceneViewportScaleGizmoHandle::Uniform: case SceneViewportScaleGizmoHandle::None: default: return Vector3::Zero(); } } bool IsPointInsideSquare( const Vector2& point, const 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); } void AppendScreenTriangle( SceneViewportOverlayFrameData& frameData, const Vector2& a, const Vector2& b, const Vector2& c, const Color& color) { SceneViewportOverlayScreenTrianglePrimitive& triangle = frameData.screenTriangles.emplace_back(); triangle.vertices[0].screenPosition = a; triangle.vertices[0].color = color; triangle.vertices[1].screenPosition = b; triangle.vertices[1].color = color; triangle.vertices[2].screenPosition = c; triangle.vertices[2].color = color; } void AppendScreenQuad( SceneViewportOverlayFrameData& frameData, const Vector2& a, const Vector2& b, const Vector2& c, const Vector2& d, const Color& color) { AppendScreenTriangle(frameData, a, b, c, color); AppendScreenTriangle(frameData, a, c, d, color); } void AppendScreenRect( SceneViewportOverlayFrameData& frameData, const Vector2& center, const Vector2& halfSize, const Color& color) { AppendScreenQuad( frameData, Vector2(center.x - halfSize.x, center.y - halfSize.y), Vector2(center.x + halfSize.x, center.y - halfSize.y), Vector2(center.x + halfSize.x, center.y + halfSize.y), Vector2(center.x - halfSize.x, center.y + halfSize.y), color); } void AppendScreenSegmentQuad( SceneViewportOverlayFrameData& frameData, const Vector2& start, const Vector2& end, float thicknessPixels, const Color& color) { const Vector2 delta = end - start; if (delta.SqrMagnitude() <= Math::EPSILON || thicknessPixels <= Math::EPSILON) { return; } const Vector2 direction = NormalizeVector2(delta); const Vector2 normal(-direction.y, direction.x); const Vector2 offset = normal * (thicknessPixels * 0.5f); AppendScreenQuad( frameData, start + offset, start - offset, end - offset, end + offset, color); } void AppendScreenQuadOutline( SceneViewportOverlayFrameData& frameData, const std::array& corners, float thicknessPixels, const Color& color) { for (size_t index = 0; index < corners.size(); ++index) { AppendScreenSegmentQuad( frameData, corners[index], corners[(index + 1u) % corners.size()], thicknessPixels, color); } } void AppendScreenRectOutline( SceneViewportOverlayFrameData& frameData, const Vector2& center, const Vector2& halfSize, float thicknessPixels, const Color& color) { const std::array corners = {{ Vector2(center.x - halfSize.x, center.y - halfSize.y), Vector2(center.x + halfSize.x, center.y - halfSize.y), Vector2(center.x + halfSize.x, center.y + halfSize.y), Vector2(center.x - halfSize.x, center.y + halfSize.y) }}; AppendScreenQuadOutline(frameData, corners, thicknessPixels, color); } void AppendMoveGizmoScreenTriangles( SceneViewportOverlayFrameData& frameData, const SceneViewportMoveGizmoDrawData& drawData) { if (!drawData.visible) { return; } for (const SceneViewportMoveGizmoPlaneDrawData& plane : drawData.planes) { if (!plane.visible) { continue; } AppendScreenQuad( frameData, plane.corners[0], plane.corners[1], plane.corners[2], plane.corners[3], plane.fillColor); AppendScreenQuadOutline( frameData, plane.corners, plane.active ? 2.6f : (plane.hovered ? 2.0f : 1.4f), plane.outlineColor); } for (const SceneViewportMoveGizmoHandleDrawData& handle : drawData.handles) { if (!handle.visible) { continue; } const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f); const Vector2 direction = NormalizeVector2(handle.end - handle.start); const float arrowLength = (std::min)( kSceneViewportMoveArrowLengthPixels, (handle.end - handle.start).Magnitude()); const Vector2 normal(-direction.y, direction.x); const Vector2 arrowBase = handle.end - direction * arrowLength; const Vector2 arrowLeft = arrowBase + normal * kSceneViewportMoveArrowHalfWidthPixels; const Vector2 arrowRight = arrowBase - normal * kSceneViewportMoveArrowHalfWidthPixels; AppendScreenSegmentQuad( frameData, handle.start, arrowBase, thickness, handle.color); AppendScreenTriangle(frameData, handle.end, arrowLeft, arrowRight, handle.color); } } void AppendRotateGizmoHandleScreenTriangles( SceneViewportOverlayFrameData& frameData, const SceneViewportRotateGizmoHandleDrawData& handle, bool frontPass) { if (!handle.visible) { return; } const bool isViewHandle = handle.axis == SceneViewportRotateGizmoAxis::View; if (isViewHandle && !frontPass) { return; } const float thickness = handle.active ? 3.6f : (handle.hovered ? 3.0f : 2.1f); for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) { if (!segment.visible || (!isViewHandle && segment.frontFacing != frontPass)) { continue; } Color drawColor = handle.color; if (!isViewHandle && !frontPass) { drawColor = LerpColor( handle.color, Color(0.72f, 0.72f, 0.72f, 1.0f), 0.78f); drawColor = WithAlpha( drawColor, handle.active ? 0.55f : 0.38f); } else if (isViewHandle) { drawColor = WithAlpha( drawColor, handle.active ? 0.95f : (handle.hovered ? 0.88f : 0.78f)); } AppendScreenSegmentQuad( frameData, segment.start, segment.end, thickness, drawColor); } } void AppendRotateGizmoAngleFillScreenTriangles( SceneViewportOverlayFrameData& frameData, const SceneViewportRotateGizmoAngleFillDrawData& angleFill) { if (!angleFill.visible || angleFill.arcPointCount < 2u) { return; } for (size_t index = 0; index + 1u < angleFill.arcPointCount; ++index) { AppendScreenTriangle( frameData, angleFill.pivot, angleFill.arcPoints[index], angleFill.arcPoints[index + 1u], angleFill.fillColor); } for (size_t index = 0; index + 1u < angleFill.arcPointCount; ++index) { AppendScreenSegmentQuad( frameData, angleFill.arcPoints[index], angleFill.arcPoints[index + 1u], 2.0f, angleFill.outlineColor); } AppendScreenSegmentQuad( frameData, angleFill.pivot, angleFill.arcPoints[0], 1.6f, angleFill.outlineColor); AppendScreenSegmentQuad( frameData, angleFill.pivot, angleFill.arcPoints[angleFill.arcPointCount - 1u], 1.6f, angleFill.outlineColor); } void AppendRotateGizmoScreenTriangles( SceneViewportOverlayFrameData& frameData, const SceneViewportRotateGizmoDrawData& drawData) { if (!drawData.visible) { return; } for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) { if (handle.axis == SceneViewportRotateGizmoAxis::View) { AppendRotateGizmoHandleScreenTriangles(frameData, handle, true); } } for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) { if (handle.axis != SceneViewportRotateGizmoAxis::View) { AppendRotateGizmoHandleScreenTriangles(frameData, handle, false); } } AppendRotateGizmoAngleFillScreenTriangles(frameData, drawData.angleFill); for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) { if (handle.axis != SceneViewportRotateGizmoAxis::View) { AppendRotateGizmoHandleScreenTriangles(frameData, handle, true); } } } void AppendScaleGizmoScreenTriangles( SceneViewportOverlayFrameData& frameData, const SceneViewportScaleGizmoDrawData& drawData) { if (!drawData.visible) { return; } constexpr Color kScaleCapOutlineColor( 24.0f / 255.0f, 24.0f / 255.0f, 24.0f / 255.0f, 220.0f / 255.0f); for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : drawData.axisHandles) { if (!handle.visible) { continue; } const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.2f); const Vector2 direction = NormalizeVector2(handle.capCenter - handle.start); const Vector2 lineEnd = handle.capCenter - direction * handle.capHalfSize; const Vector2 capHalfSize(handle.capHalfSize, handle.capHalfSize); AppendScreenSegmentQuad( frameData, handle.start, lineEnd, thickness, handle.color); AppendScreenRect(frameData, handle.capCenter, capHalfSize, handle.color); AppendScreenRectOutline( frameData, handle.capCenter, capHalfSize, handle.active ? 2.0f : 1.0f, kScaleCapOutlineColor); } if (!drawData.centerHandle.visible) { return; } const Vector2 halfSize( drawData.centerHandle.halfSize, drawData.centerHandle.halfSize); AppendScreenRect( frameData, drawData.centerHandle.center, halfSize, drawData.centerHandle.fillColor); AppendScreenRectOutline( frameData, drawData.centerHandle.center, halfSize, drawData.centerHandle.active ? 2.0f : 1.0f, drawData.centerHandle.outlineColor); } } // namespace Matrix4x4 BuildSceneViewportViewMatrix(const SceneViewportOverlayData& overlay) { const Vector3 right = overlay.cameraRight.Normalized(); const Vector3 up = overlay.cameraUp.Normalized(); const Vector3 forward = overlay.cameraForward.Normalized(); Matrix4x4 view = Matrix4x4::Identity(); view.m[0][0] = right.x; view.m[0][1] = right.y; view.m[0][2] = right.z; view.m[0][3] = -Vector3::Dot(right, overlay.cameraPosition); view.m[1][0] = up.x; view.m[1][1] = up.y; view.m[1][2] = up.z; view.m[1][3] = -Vector3::Dot(up, overlay.cameraPosition); view.m[2][0] = forward.x; view.m[2][1] = forward.y; view.m[2][2] = forward.z; view.m[2][3] = -Vector3::Dot(forward, overlay.cameraPosition); return view; } Matrix4x4 BuildSceneViewportProjectionMatrix( const SceneViewportOverlayData& overlay, float viewportWidth, float viewportHeight) { const float aspect = viewportHeight > 0.0f ? viewportWidth / viewportHeight : 1.0f; return Matrix4x4::Perspective( overlay.verticalFovDegrees * Math::DEG_TO_RAD, aspect, overlay.nearClipPlane, overlay.farClipPlane); } Matrix4x4 BuildSceneViewportViewProjectionMatrix( const SceneViewportOverlayData& overlay, float viewportWidth, float viewportHeight) { return BuildSceneViewportProjectionMatrix(overlay, viewportWidth, viewportHeight) * BuildSceneViewportViewMatrix(overlay); } SceneViewportProjectedPoint ProjectSceneViewportWorldPoint( const SceneViewportOverlayData& overlay, float viewportWidth, float viewportHeight, const Vector3& worldPoint) { SceneViewportProjectedPoint result = {}; if (!overlay.valid || viewportWidth <= 1.0f || viewportHeight <= 1.0f) { return result; } const Vector4 clipPoint = BuildSceneViewportViewProjectionMatrix(overlay, viewportWidth, viewportHeight) * Vector4(worldPoint, 1.0f); if (clipPoint.w <= Math::EPSILON) { return result; } const 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; } bool ProjectSceneViewportAxisDirection( const SceneViewportOverlayData& overlay, const Vector3& worldAxis, Vector2& outScreenDirection) { if (!overlay.valid) { return false; } const Vector3 viewAxis = BuildSceneViewportViewMatrix(overlay).MultiplyVector(worldAxis.Normalized()); const Vector2 screenDirection(viewAxis.x, -viewAxis.y); if (screenDirection.SqrMagnitude() <= Math::EPSILON) { return false; } outScreenDirection = screenDirection.Normalized(); return true; } bool ProjectSceneViewportAxisDirectionAtPoint( const SceneViewportOverlayData& overlay, float viewportWidth, float viewportHeight, const Vector3& worldPoint, const Vector3& worldAxis, Vector2& outScreenDirection, float sampleDistance) { const Vector3 axis = worldAxis.Normalized(); if (!overlay.valid || viewportWidth <= 1.0f || viewportHeight <= 1.0f || axis.SqrMagnitude() <= Math::EPSILON || sampleDistance <= Math::EPSILON) { return false; } const Matrix4x4 viewProjection = BuildSceneViewportViewProjectionMatrix(overlay, viewportWidth, viewportHeight); const Vector4 startClip = viewProjection * Vector4(worldPoint, 1.0f); const Vector4 endClip = viewProjection * Vector4(worldPoint + axis * sampleDistance, 1.0f); if (startClip.w <= Math::EPSILON || endClip.w <= Math::EPSILON) { return ProjectSceneViewportAxisDirection(overlay, axis, outScreenDirection); } const Vector3 startNdc = startClip.ToVector3() / startClip.w; const Vector3 endNdc = endClip.ToVector3() / endClip.w; const Vector2 startScreen( (startNdc.x * 0.5f + 0.5f) * viewportWidth, (1.0f - (startNdc.y * 0.5f + 0.5f)) * viewportHeight); const Vector2 endScreen( (endNdc.x * 0.5f + 0.5f) * viewportWidth, (1.0f - (endNdc.y * 0.5f + 0.5f)) * viewportHeight); const Vector2 screenDirection = endScreen - startScreen; if (screenDirection.SqrMagnitude() <= Math::EPSILON) { return ProjectSceneViewportAxisDirection(overlay, axis, outScreenDirection); } outScreenDirection = screenDirection.Normalized(); return true; } float DistanceToSegmentSquared( const Vector2& point, const Vector2& segmentStart, const Vector2& segmentEnd, float* outSegmentT) { const 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( Vector2::Dot(point - segmentStart, segment) / segmentLengthSq, 0.0f, 1.0f); if (outSegmentT != nullptr) { *outSegmentT = segmentT; } const Vector2 closestPoint = segmentStart + segment * segmentT; return (point - closestPoint).SqrMagnitude(); } Plane BuildSceneViewportPlaneFromPointNormal( const Vector3& point, const Vector3& normal) { const Vector3 planeNormal = normal.Normalized(); return Plane(planeNormal, -Vector3::Dot(planeNormal, point)); } bool BuildSceneViewportAxisDragPlaneNormal( const SceneViewportOverlayData& overlay, const Vector3& worldAxis, Vector3& outPlaneNormal) { if (!overlay.valid || worldAxis.SqrMagnitude() <= Math::EPSILON) { return false; } const Vector3 axis = worldAxis.Normalized(); const Vector3 candidates[] = { Vector3::ProjectOnPlane(overlay.cameraForward.Normalized(), axis), Vector3::ProjectOnPlane(overlay.cameraUp.Normalized(), axis), Vector3::ProjectOnPlane(overlay.cameraRight.Normalized(), axis), Vector3::ProjectOnPlane(Vector3::Up(), axis), Vector3::ProjectOnPlane(Vector3::Right(), axis), Vector3::ProjectOnPlane(Vector3::Forward(), axis) }; for (const Vector3& candidate : candidates) { if (candidate.SqrMagnitude() <= Math::EPSILON) { continue; } outPlaneNormal = candidate.Normalized(); return true; } return false; } bool BuildSceneViewportRay( const SceneViewportOverlayData& overlay, const Vector2& viewportSize, const Vector2& viewportPosition, Ray& outRay) { const bool validViewportPosition = viewportSize.x > 1.0f && viewportSize.y > 1.0f && viewportPosition.x >= 0.0f && viewportPosition.y >= 0.0f && viewportPosition.x <= viewportSize.x && viewportPosition.y <= viewportSize.y; if (!overlay.valid || !validViewportPosition) { return false; } const float aspect = viewportSize.y > 0.0f ? viewportSize.x / viewportSize.y : 1.0f; const float tanHalfFov = std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f); const float ndcX = (viewportPosition.x / viewportSize.x) * 2.0f - 1.0f; const float ndcY = 1.0f - (viewportPosition.y / viewportSize.y) * 2.0f; const Vector3 direction = Vector3::Normalize( overlay.cameraForward.Normalized() + overlay.cameraRight.Normalized() * (ndcX * aspect * tanHalfFov) + overlay.cameraUp.Normalized() * (ndcY * tanHalfFov)); if (direction.SqrMagnitude() <= Math::EPSILON) { return false; } outRay = Ray(overlay.cameraPosition, direction); return true; } void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) { BuildDrawData(context); if (m_dragMode == DragMode::None && IsMouseInsideViewport(context.mousePosition, context.viewportSize)) { 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; } Ray worldRay; if (!BuildSceneViewportRay( context.overlay, context.viewportSize, context.mousePosition, worldRay)) { return false; } const Vector3 pivotWorldPosition = context.pivotWorldPosition; Vector3 dragPlaneNormal = Vector3::Zero(); Vector3 worldAxis = 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 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 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 = 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 (GameObject* gameObject : m_dragObjects) { m_dragStartObjectWorldPositions.push_back( gameObject != nullptr && gameObject->GetTransform() != nullptr ? gameObject->GetTransform()->GetPosition() : 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; } Ray worldRay; if (!BuildSceneViewportRay( context.overlay, context.viewportSize, context.mousePosition, worldRay)) { return; } float hitDistance = 0.0f; if (!worldRay.Intersects(m_dragPlane, hitDistance)) { return; } const Vector3 hitPoint = worldRay.GetPoint(hitDistance); if (m_dragMode == DragMode::Axis) { const float currentAxisScalar = Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection); const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar; const 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) { return; } const Vector3 worldDelta = 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 = Vector3::Zero(); m_activePlaneNormal = Vector3::Zero(); m_dragStartPivotWorldPosition = Vector3::Zero(); m_dragStartHitWorldPosition = 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 = Vector3::Zero(); m_activePlaneNormal = Vector3::Zero(); m_dragStartPivotWorldPosition = Vector3::Zero(); m_dragStartHitWorldPosition = 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; } std::uint64_t SceneViewportMoveGizmo::GetActiveEntityId() const { return m_activeEntityId; } const SceneViewportMoveGizmoDrawData& SceneViewportMoveGizmo::GetDrawData() const { return m_drawData; } SceneViewportMoveGizmoHitResult SceneViewportMoveGizmo::EvaluateHit( const 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 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 = worldUnitsPerPixel * kMoveGizmoPlaneInsetPixels; const float planeExtentWorld = worldUnitsPerPixel * (kMoveGizmoPlaneInsetPixels + kMoveGizmoPlaneSizePixels); 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 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); Vector3 axisA = Vector3::Zero(); Vector3 axisB = Vector3::Zero(); GetPlaneAxes(plane.plane, context.axisOrientation, axisA, axisB); if (axisA.SqrMagnitude() <= Math::EPSILON || axisB.SqrMagnitude() <= Math::EPSILON) { continue; } const 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 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) ? 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 Color baseColor = plane.hovered || plane.active ? 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)); } } void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) { BuildDrawData(context); if (m_activeAxis == SceneViewportRotateGizmoAxis::None && IsMouseInsideViewport(context.mousePosition, context.viewportSize)) { 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 Vector3 pivotWorldPosition = context.pivotWorldPosition; const Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay, context.axisOrientation); if (worldAxis.SqrMagnitude() <= Math::EPSILON) { return false; } const Plane dragPlane = BuildSceneViewportPlaneFromPointNormal(pivotWorldPosition, worldAxis); Vector3 startDirection = Vector3::Zero(); bool useScreenSpaceDrag = true; Ray worldRay; if (BuildSceneViewportRay( context.overlay, context.viewportSize, context.mousePosition, worldRay)) { float hitDistance = 0.0f; if (worldRay.Intersects(dragPlane, hitDistance)) { const Vector3 hitPoint = worldRay.GetPoint(hitDistance); startDirection = 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 (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(Vector3::Zero()); m_dragStartWorldRotations.push_back(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 { Ray worldRay; if (!BuildSceneViewportRay( context.overlay, context.viewportSize, context.mousePosition, worldRay)) { return; } float hitDistance = 0.0f; if (!worldRay.Intersects(m_dragPlane, hitDistance)) { return; } const Vector3 hitPoint = worldRay.GetPoint(hitDistance); const Vector3 currentDirection = Vector3::ProjectOnPlane( hitPoint - m_dragStartPivotWorldPosition, 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 Quaternion worldDeltaRotation = Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians); const Vector3 localAxis = GetBaseRotateAxisVector(m_activeAxis); const Quaternion localDeltaRotation = localAxis.SqrMagnitude() > Math::EPSILON ? Quaternion::FromAxisAngle(localAxis, deltaRadians) : Quaternion::Identity(); for (size_t index = 0; index < m_dragObjects.size(); ++index) { 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 = Vector3::Zero(); m_dragStartRingAngle = 0.0f; m_dragCurrentDeltaRadians = 0.0f; m_dragStartPivotWorldPosition = 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 = Vector3::Zero(); m_dragStartRingAngle = 0.0f; m_dragCurrentDeltaRadians = 0.0f; m_dragStartPivotWorldPosition = 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; } std::uint64_t SceneViewportRotateGizmo::GetActiveEntityId() const { return m_activeEntityId; } const SceneViewportRotateGizmoDrawData& SceneViewportRotateGizmo::GetDrawData() const { return m_drawData; } SceneViewportRotateGizmoHitResult SceneViewportRotateGizmo::EvaluateHit( const 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 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 Quaternion dragFeedbackRotation = hasActiveDragFeedback ? Quaternion::FromAxisAngle(m_activeWorldAxis, m_dragCurrentDeltaRadians) : 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); Vector3 basisA = Vector3::Zero(); Vector3 basisB = 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 + 1u) / static_cast(handle.segments.size()) * Math::PI * 2.0f; const float midAngle = (angle0 + angle1) * 0.5f; const Vector3 startWorld = pivotWorldPosition + (basisA * std::cos(angle0) + basisB * std::sin(angle0)) * ringRadiusWorld; const Vector3 endWorld = pivotWorldPosition + (basisA * std::cos(angle1) + basisB * std::sin(angle1)) * ringRadiusWorld; const 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 Vector3 radial = (midWorld - pivotWorldPosition).Normalized(); segment.frontFacing = Vector3::Dot( radial, NormalizeVector3(context.overlay.cameraForward, Vector3::Forward())) < 0.0f; } } handle.visible = anyVisibleSegment; } if (m_activeAxis == SceneViewportRotateGizmoAxis::None || std::abs(m_dragCurrentDeltaRadians) < kRotateGizmoAngleFillMinRadians) { return; } SceneViewportRotateGizmoAngleFillDrawData& angleFill = m_drawData.angleFill; angleFill.axis = m_activeAxis; angleFill.pivot = projectedPivot.screenPosition; angleFill.fillColor = Color(1.0f, 0.92f, 0.12f, 0.22f); angleFill.outlineColor = Color(1.0f, 0.92f, 0.12f, 0.95f); Vector3 basisA = Vector3::Zero(); Vector3 basisB = Vector3::Zero(); if (!GetRotateRingBasis( m_activeAxis, context.overlay, context.axisOrientation, basisA, basisB)) { return; } 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(1u), kSceneViewportRotateGizmoAngleFillPointCount - 1u); 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 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) { return; } angleFill.arcPointCount = stepCount + 1u; 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) ? Color::Yellow() : GetRotateAxisBaseColor(handle.axis); } } bool SceneViewportRotateGizmo::TryGetClosestRingAngle( SceneViewportRotateGizmoAxis axis, const 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; } void SceneViewportScaleGizmo::Update(const SceneViewportScaleGizmoContext& context) { BuildDrawData(context); if (m_activeHandle == SceneViewportScaleGizmoHandle::None && IsMouseInsideViewport(context.mousePosition, context.viewportSize)) { m_hoveredHandle = EvaluateHit(context.mousePosition).handle; } 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 || context.selectedObject->GetTransform() == nullptr || !m_drawData.visible || undoManager.HasPendingInteractiveChange()) { return false; } Vector2 activeScreenDirection = 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) { if (!ProjectSceneViewportAxisDirectionAtPoint( context.overlay, context.viewportSize.x, context.viewportSize.y, context.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 = 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->GetTransform() == nullptr || context.selectedObject->GetID() != m_activeEntityId) { return; } const Vector2 mouseDelta = context.mousePosition - m_dragStartMousePosition; 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 = 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 = Vector3( ComputeVisualScaleFactor(localScale.x, m_dragStartLocalScale.x), 1.0f, 1.0f); break; case SceneViewportScaleGizmoHandle::Y: m_dragCurrentVisualScale = Vector3( 1.0f, ComputeVisualScaleFactor(localScale.y, m_dragStartLocalScale.y), 1.0f); break; case SceneViewportScaleGizmoHandle::Z: m_dragCurrentVisualScale = Vector3( 1.0f, 1.0f, ComputeVisualScaleFactor(localScale.z, m_dragStartLocalScale.z)); break; case SceneViewportScaleGizmoHandle::Uniform: m_dragCurrentVisualScale = 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 = 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 = Vector3::Zero(); m_dragCurrentVisualScale = Vector3::One(); m_dragStartMousePosition = Vector2::Zero(); m_activeScreenDirection = 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 = Vector3::Zero(); m_dragCurrentVisualScale = Vector3::One(); m_dragStartMousePosition = Vector2::Zero(); m_activeScreenDirection = Vector2::Zero(); RefreshHandleState(); } bool SceneViewportScaleGizmo::IsHoveringHandle() const { return m_hoveredHandle != SceneViewportScaleGizmoHandle::None; } bool SceneViewportScaleGizmo::IsActive() const { return m_activeHandle != SceneViewportScaleGizmoHandle::None; } std::uint64_t SceneViewportScaleGizmo::GetActiveEntityId() const { return m_activeEntityId; } const SceneViewportScaleGizmoDrawData& SceneViewportScaleGizmo::GetDrawData() const { return m_drawData; } SceneViewportScaleGizmoHitResult SceneViewportScaleGizmo::EvaluateHit( const Vector2& mousePosition) const { SceneViewportScaleGizmoHitResult result = {}; if (!m_drawData.visible) { return result; } if (m_drawData.centerHandle.visible && IsPointInsideSquare( mousePosition, m_drawData.centerHandle.center, m_drawData.centerHandle.halfSize + 2.0f)) { result.handle = SceneViewportScaleGizmoHandle::Uniform; result.distanceSq = (m_drawData.centerHandle.center - mousePosition).SqrMagnitude(); return result; } 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 >= result.distanceSq) { continue; } result.handle = handle.handle; result.distanceSq = distanceSq; } if (result.handle != SceneViewportScaleGizmoHandle::None) { return result; } const float hoverThresholdSq = kScaleGizmoHoverThresholdPixels * kScaleGizmoHoverThresholdPixels; for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) { if (!handle.visible) { continue; } const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end); if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) { continue; } result.handle = handle.handle; result.distanceSq = distanceSq; } return result; } void SceneViewportScaleGizmo::SetHoveredHandle( SceneViewportScaleGizmoHandle handle) { if (m_activeHandle != SceneViewportScaleGizmoHandle::None) { return; } m_hoveredHandle = handle; RefreshHandleState(); } void SceneViewportScaleGizmo::BuildDrawData( const SceneViewportScaleGizmoContext& context) { m_drawData = {}; const GameObject* selectedObject = context.selectedObject; if (selectedObject == nullptr || selectedObject->GetTransform() == nullptr || !context.overlay.valid || context.viewportSize.x <= 1.0f || context.viewportSize.y <= 1.0f) { return; } const 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.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 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) ? 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 Color baseColor = (m_drawData.centerHandle.hovered || m_drawData.centerHandle.active) ? 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)); } const SceneViewportScaleGizmoAxisHandleDrawData* SceneViewportScaleGizmo::FindAxisHandleDrawData( SceneViewportScaleGizmoHandle handle) const { for (const SceneViewportScaleGizmoAxisHandleDrawData& drawHandle : m_drawData.axisHandles) { if (drawHandle.handle == handle) { return &drawHandle; } } return nullptr; } SceneViewportOverlayFrameData BuildSceneViewportTransformGizmoOverlayFrameData( const SceneViewportOverlayData& overlay, const SceneViewportTransformGizmoHandleBuildInputs& inputs) { SceneViewportOverlayFrameData frameData = {}; frameData.overlay = overlay; if (inputs.moveGizmo != nullptr) { AppendMoveGizmoScreenTriangles(frameData, *inputs.moveGizmo); } if (inputs.rotateGizmo != nullptr) { AppendRotateGizmoScreenTriangles(frameData, *inputs.rotateGizmo); } if (inputs.scaleGizmo != nullptr) { AppendScaleGizmoScreenTriangles(frameData, *inputs.scaleGizmo); } return frameData; } } // namespace XCEngine::UI::Editor::App::SceneViewportGizmoSupport