513 lines
18 KiB
C++
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
|