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