Files
XCEngine/editor/src/Viewport/SceneViewportScaleGizmo.cpp

513 lines
18 KiB
C++

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