Files
XCEngine/editor/app/Features/Scene/SceneViewportTransformGizmoSupport.cpp
2026-04-25 16:46:01 +08:00

2463 lines
82 KiB
C++

#include "Features/Scene/SceneViewportTransformGizmoSupport.h"
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/TransformComponent.h>
#include <algorithm>
#include <cmath>
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<Vector2, 4>& corners) {
return PointInTriangle(point, corners[0], corners[1], corners[2]) ||
PointInTriangle(point, corners[0], corners[2], corners[3]);
}
Vector2 QuadCenter(const std::array<Vector2, 4>& 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<Vector2, 4>& 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<Vector2, 4>& 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<Vector2, 4> 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<float>(segmentIndex) /
static_cast<float>(handle.segments.size()) * Math::PI * 2.0f;
const float angle1 =
static_cast<float>(segmentIndex + 1u) /
static_cast<float>(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<size_t>(std::ceil(
sweepAbs / (Math::PI * 2.0f) *
static_cast<float>(kSceneViewportRotateGizmoSegmentCount))),
static_cast<size_t>(1u),
kSceneViewportRotateGizmoAngleFillPointCount - 1u);
bool valid = true;
for (size_t pointIndex = 0; pointIndex <= stepCount; ++pointIndex) {
const float t =
static_cast<float>(pointIndex) / static_cast<float>(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