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

557 lines
19 KiB
C++

#include "SceneViewportMoveGizmo.h"
#include "Core/IUndoManager.h"
#include "SceneViewportMath.h"
#include "SceneViewportPicker.h"
#include <XCEngine/Components/GameObject.h>
namespace XCEngine {
namespace Editor {
namespace {
constexpr float kMoveGizmoHandleLengthPixels = 100.0f;
constexpr float kMoveGizmoHoverThresholdPixels = 10.0f;
Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) {
switch (axis) {
case SceneViewportGizmoAxis::X:
return Math::Vector3::Right();
case SceneViewportGizmoAxis::Y:
return Math::Vector3::Up();
case SceneViewportGizmoAxis::Z:
return Math::Vector3::Forward();
case SceneViewportGizmoAxis::None:
default:
return Math::Vector3::Zero();
}
}
Math::Color GetAxisBaseColor(SceneViewportGizmoAxis axis) {
switch (axis) {
case SceneViewportGizmoAxis::X:
return Math::Color(0.91f, 0.09f, 0.05f, 1.0f);
case SceneViewportGizmoAxis::Y:
return Math::Color(0.45f, 1.0f, 0.12f, 1.0f);
case SceneViewportGizmoAxis::Z:
return Math::Color(0.11f, 0.29f, 1.0f, 1.0f);
case SceneViewportGizmoAxis::None:
default:
return Math::Color::White();
}
}
Math::Color WithAlpha(const Math::Color& color, float alpha) {
return Math::Color(color.r, color.g, color.b, alpha);
}
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,
Math::Vector3& outAxisA,
Math::Vector3& outAxisB) {
switch (plane) {
case SceneViewportGizmoPlane::XY:
outAxisA = Math::Vector3::Right();
outAxisB = Math::Vector3::Up();
return;
case SceneViewportGizmoPlane::XZ:
outAxisA = Math::Vector3::Right();
outAxisB = Math::Vector3::Forward();
return;
case SceneViewportGizmoPlane::YZ:
outAxisA = Math::Vector3::Up();
outAxisB = Math::Vector3::Forward();
return;
case SceneViewportGizmoPlane::None:
default:
outAxisA = Math::Vector3::Zero();
outAxisB = Math::Vector3::Zero();
return;
}
}
Math::Vector3 GetPlaneNormal(SceneViewportGizmoPlane plane) {
switch (plane) {
case SceneViewportGizmoPlane::XY:
return Math::Vector3::Forward();
case SceneViewportGizmoPlane::XZ:
return Math::Vector3::Up();
case SceneViewportGizmoPlane::YZ:
return Math::Vector3::Right();
case SceneViewportGizmoPlane::None:
default:
return Math::Vector3::Zero();
}
}
Math::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 Math::Color::White();
}
}
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
}
float Cross2D(const Math::Vector2& a, const Math::Vector2& b) {
return a.x * b.y - a.y * b.x;
}
float PolygonAreaTwice(const std::array<Math::Vector2, 4>& corners) {
float areaTwice = 0.0f;
for (size_t index = 0; index < corners.size(); ++index) {
const Math::Vector2& current = corners[index];
const Math::Vector2& next = corners[(index + 1) % corners.size()];
areaTwice += current.x * next.y - next.x * current.y;
}
return areaTwice;
}
bool PointInTriangle(
const Math::Vector2& point,
const Math::Vector2& a,
const Math::Vector2& b,
const Math::Vector2& c) {
const float ab = Cross2D(b - a, point - a);
const float bc = Cross2D(c - b, point - b);
const float ca = Cross2D(a - c, point - c);
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 Math::Vector2& point, const std::array<Math::Vector2, 4>& corners) {
return PointInTriangle(point, corners[0], corners[1], corners[2]) ||
PointInTriangle(point, corners[0], corners[2], corners[3]);
}
Math::Vector2 QuadCenter(const std::array<Math::Vector2, 4>& corners) {
Math::Vector2 center = Math::Vector2::Zero();
for (const Math::Vector2& corner : corners) {
center += corner;
}
return center / 4.0f;
}
bool IsMouseInsideViewport(const SceneViewportMoveGizmoContext& context) {
return context.mousePosition.x >= 0.0f &&
context.mousePosition.y >= 0.0f &&
context.mousePosition.x <= context.viewportSize.x &&
context.mousePosition.y <= context.viewportSize.y;
}
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;
}
} // namespace
void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) {
BuildDrawData(context);
if (m_dragMode == DragMode::None && IsMouseInsideViewport(context)) {
m_hoveredAxis = HitTestAxis(context.mousePosition);
m_hoveredPlane = m_hoveredAxis == SceneViewportGizmoAxis::None
? HitTestPlane(context.mousePosition)
: SceneViewportGizmoPlane::None;
} 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;
}
Math::Ray worldRay;
if (!BuildSceneViewportRay(
context.overlay,
context.viewportSize,
context.mousePosition,
worldRay)) {
return false;
}
const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition();
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
Math::Vector3 dragPlaneNormal = Math::Vector3::Zero();
Math::Vector3 worldAxis = Math::Vector3::Zero();
if (m_hoveredAxis != SceneViewportGizmoAxis::None) {
worldAxis = GetAxisVector(m_hoveredAxis);
if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) {
return false;
}
} else {
dragPlaneNormal = GetPlaneNormal(m_hoveredPlane);
if (dragPlaneNormal.SqrMagnitude() <= Math::EPSILON) {
return false;
}
}
const Math::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 Math::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_dragStartObjectWorldPosition = objectWorldPosition;
m_dragStartPivotWorldPosition = pivotWorldPosition;
m_dragStartHitWorldPosition = hitPoint;
m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - pivotWorldPosition, worldAxis);
RefreshHandleState();
return true;
}
void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& context) {
if (m_dragMode == DragMode::None ||
context.selectedObject == nullptr ||
context.selectedObject->GetID() != m_activeEntityId) {
return;
}
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 hitPoint = worldRay.GetPoint(hitDistance);
if (m_dragMode == DragMode::Axis) {
const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection);
const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar;
context.selectedObject->GetTransform()->SetPosition(
m_dragStartObjectWorldPosition + m_activeAxisDirection * deltaScalar);
return;
}
if (m_dragMode == DragMode::Plane) {
const Math::Vector3 planeDelta = Math::Vector3::ProjectOnPlane(
hitPoint - m_dragStartHitWorldPosition,
m_activePlaneNormal);
context.selectedObject->GetTransform()->SetPosition(
m_dragStartObjectWorldPosition + planeDelta);
}
}
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 = Math::Vector3::Zero();
m_activePlaneNormal = Math::Vector3::Zero();
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
m_dragStartHitWorldPosition = Math::Vector3::Zero();
m_dragStartAxisScalar = 0.0f;
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 = Math::Vector3::Zero();
m_activePlaneNormal = Math::Vector3::Zero();
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
m_dragStartHitWorldPosition = Math::Vector3::Zero();
m_dragStartAxisScalar = 0.0f;
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;
}
uint64_t SceneViewportMoveGizmo::GetActiveEntityId() const {
return m_activeEntityId;
}
const SceneViewportMoveGizmoDrawData& SceneViewportMoveGizmo::GetDrawData() const {
return m_drawData;
}
void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& context) {
m_drawData = {};
m_drawData.pivotRadius = 5.0f;
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 gizmoWorldOrigin = selectedObject->GetTransform()->GetPosition();
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 = axisLengthWorld * 0.02f;
const float planeExtentWorld = axisLengthWorld * 0.36f;
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 Math::Vector3 axisEndWorld =
gizmoWorldOrigin + GetAxisVector(handle.axis) * 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);
Math::Vector3 axisA = Math::Vector3::Zero();
Math::Vector3 axisB = Math::Vector3::Zero();
GetPlaneAxes(plane.plane, axisA, axisB);
if (axisA.SqrMagnitude() <= Math::EPSILON || axisB.SqrMagnitude() <= Math::EPSILON) {
continue;
}
const Math::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 Math::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)
? Math::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 Math::Color baseColor = plane.hovered || plane.active
? Math::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));
}
}
SceneViewportGizmoAxis SceneViewportMoveGizmo::HitTestAxis(const Math::Vector2& mousePosition) const {
if (!m_drawData.visible) {
return SceneViewportGizmoAxis::None;
}
const float hoverThresholdSq = kMoveGizmoHoverThresholdPixels * kMoveGizmoHoverThresholdPixels;
SceneViewportGizmoAxis bestAxis = SceneViewportGizmoAxis::None;
float bestDistanceSq = hoverThresholdSq;
for (const SceneViewportMoveGizmoHandleDrawData& handle : m_drawData.handles) {
if (!handle.visible) {
continue;
}
const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end);
if (distanceSq > bestDistanceSq) {
continue;
}
bestDistanceSq = distanceSq;
bestAxis = handle.axis;
}
return bestAxis;
}
SceneViewportGizmoPlane SceneViewportMoveGizmo::HitTestPlane(const Math::Vector2& mousePosition) const {
if (!m_drawData.visible) {
return SceneViewportGizmoPlane::None;
}
SceneViewportGizmoPlane bestPlane = SceneViewportGizmoPlane::None;
float bestDistanceSq = Math::FLOAT_MAX;
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 >= bestDistanceSq) {
continue;
}
bestDistanceSq = distanceSq;
bestPlane = plane.plane;
}
return bestPlane;
}
} // namespace Editor
} // namespace XCEngine