2463 lines
82 KiB
C++
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
|