feat: refine scene viewport gizmos and controls
This commit is contained in:
@@ -14,7 +14,7 @@ namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kMoveGizmoHandleLengthPixels = 72.0f;
|
||||
constexpr float kMoveGizmoHandleLengthPixels = 88.0f;
|
||||
constexpr float kMoveGizmoHoverThresholdPixels = 10.0f;
|
||||
|
||||
Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) {
|
||||
@@ -45,10 +45,120 @@ Math::Color GetAxisBaseColor(SceneViewportGizmoAxis axis) {
|
||||
}
|
||||
}
|
||||
|
||||
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 &&
|
||||
@@ -141,20 +251,28 @@ Math::Vector3 GetGizmoWorldOrigin(const Components::GameObject& gameObject) {
|
||||
|
||||
void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) {
|
||||
BuildDrawData(context);
|
||||
if (m_activeAxis == SceneViewportGizmoAxis::None && IsMouseInsideViewport(context)) {
|
||||
if (m_dragMode == DragMode::None && IsMouseInsideViewport(context)) {
|
||||
m_hoveredAxis = HitTestAxis(context.mousePosition);
|
||||
} else if (m_activeAxis == SceneViewportGizmoAxis::None) {
|
||||
m_hoveredPlane = m_hoveredAxis == SceneViewportGizmoAxis::None
|
||||
? HitTestPlane(context.mousePosition)
|
||||
: SceneViewportGizmoPlane::None;
|
||||
} else if (m_dragMode == DragMode::None) {
|
||||
m_hoveredAxis = SceneViewportGizmoAxis::None;
|
||||
} else {
|
||||
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_activeAxis != SceneViewportGizmoAxis::None ||
|
||||
m_hoveredAxis == SceneViewportGizmoAxis::None ||
|
||||
if (m_dragMode != DragMode::None ||
|
||||
(m_hoveredAxis == SceneViewportGizmoAxis::None && m_hoveredPlane == SceneViewportGizmoPlane::None) ||
|
||||
context.selectedObject == nullptr ||
|
||||
!m_drawData.visible ||
|
||||
undoManager.HasPendingInteractiveChange()) {
|
||||
@@ -170,14 +288,23 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector3 worldAxis = GetAxisVector(m_hoveredAxis);
|
||||
Math::Vector3 dragPlaneNormal = Math::Vector3::Zero();
|
||||
if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = GetGizmoWorldOrigin(*context.selectedObject);
|
||||
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)) {
|
||||
@@ -190,19 +317,23 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
|
||||
}
|
||||
|
||||
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_activeAxis == SceneViewportGizmoAxis::None ||
|
||||
if (m_dragMode == DragMode::None ||
|
||||
context.selectedObject == nullptr ||
|
||||
context.selectedObject->GetID() != m_activeEntityId) {
|
||||
return;
|
||||
@@ -223,14 +354,25 @@ void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& con
|
||||
}
|
||||
|
||||
const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance);
|
||||
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);
|
||||
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_activeAxis == SceneViewportGizmoAxis::None) {
|
||||
if (m_dragMode == DragMode::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -238,11 +380,15 @@ void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) {
|
||||
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();
|
||||
}
|
||||
@@ -252,22 +398,28 @@ void SceneViewportMoveGizmo::CancelDrag(IUndoManager* undoManager) {
|
||||
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;
|
||||
return m_hoveredAxis != SceneViewportGizmoAxis::None ||
|
||||
m_hoveredPlane != SceneViewportGizmoPlane::None;
|
||||
}
|
||||
|
||||
bool SceneViewportMoveGizmo::IsActive() const {
|
||||
return m_activeAxis != SceneViewportGizmoAxis::None;
|
||||
return m_dragMode != DragMode::None;
|
||||
}
|
||||
|
||||
uint64_t SceneViewportMoveGizmo::GetActiveEntityId() const {
|
||||
@@ -312,6 +464,8 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
|
||||
}
|
||||
|
||||
const float axisLengthWorld = worldUnitsPerPixel * kMoveGizmoHandleLengthPixels;
|
||||
const float planeInsetWorld = axisLengthWorld * 0.02f;
|
||||
const float planeExtentWorld = axisLengthWorld * 0.36f;
|
||||
|
||||
const SceneViewportGizmoAxis axes[] = {
|
||||
SceneViewportGizmoAxis::X,
|
||||
@@ -342,6 +496,49 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
|
||||
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() {
|
||||
@@ -356,6 +553,20 @@ void SceneViewportMoveGizmo::RefreshHandleState() {
|
||||
? 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 {
|
||||
@@ -384,5 +595,29 @@ SceneViewportGizmoAxis SceneViewportMoveGizmo::HitTestAxis(const Math::Vector2&
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user