2026-03-29 16:18:13 +08:00
|
|
|
#include "SceneViewportMoveGizmo.h"
|
|
|
|
|
|
|
|
|
|
#include "Core/IUndoManager.h"
|
|
|
|
|
#include "SceneViewportMath.h"
|
|
|
|
|
#include "SceneViewportPicker.h"
|
|
|
|
|
|
|
|
|
|
#include <XCEngine/Components/GameObject.h>
|
2026-03-30 00:47:31 +08:00
|
|
|
#include <XCEngine/Components/MeshFilterComponent.h>
|
|
|
|
|
#include <XCEngine/Components/MeshRendererComponent.h>
|
|
|
|
|
#include <XCEngine/Core/Math/Bounds.h>
|
2026-03-29 16:18:13 +08:00
|
|
|
|
|
|
|
|
namespace XCEngine {
|
|
|
|
|
namespace Editor {
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
constexpr float kMoveGizmoHandleLengthPixels = 72.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:
|
2026-03-30 00:47:31 +08:00
|
|
|
return Math::Color(0.91f, 0.09f, 0.05f, 1.0f);
|
2026-03-29 16:18:13 +08:00
|
|
|
case SceneViewportGizmoAxis::Y:
|
2026-03-30 00:47:31 +08:00
|
|
|
return Math::Color(0.45f, 1.0f, 0.12f, 1.0f);
|
2026-03-29 16:18:13 +08:00
|
|
|
case SceneViewportGizmoAxis::Z:
|
2026-03-30 00:47:31 +08:00
|
|
|
return Math::Color(0.11f, 0.29f, 1.0f, 1.0f);
|
2026-03-29 16:18:13 +08:00
|
|
|
case SceneViewportGizmoAxis::None:
|
|
|
|
|
default:
|
|
|
|
|
return Math::Color::White();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 00:47:31 +08:00
|
|
|
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
|
|
|
|
|
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 16:18:13 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 00:47:31 +08:00
|
|
|
void EncapsulateTransformedBounds(
|
|
|
|
|
const Math::Bounds& localBounds,
|
|
|
|
|
const Components::TransformComponent& transform,
|
|
|
|
|
Math::Bounds& inOutBounds,
|
|
|
|
|
bool& inOutHasBounds) {
|
|
|
|
|
const Math::Vector3 localMin = localBounds.GetMin();
|
|
|
|
|
const Math::Vector3 localMax = localBounds.GetMax();
|
|
|
|
|
const Math::Vector3 corners[] = {
|
|
|
|
|
Math::Vector3(localMin.x, localMin.y, localMin.z),
|
|
|
|
|
Math::Vector3(localMax.x, localMin.y, localMin.z),
|
|
|
|
|
Math::Vector3(localMin.x, localMax.y, localMin.z),
|
|
|
|
|
Math::Vector3(localMin.x, localMin.y, localMax.z),
|
|
|
|
|
Math::Vector3(localMax.x, localMax.y, localMin.z),
|
|
|
|
|
Math::Vector3(localMin.x, localMax.y, localMax.z),
|
|
|
|
|
Math::Vector3(localMax.x, localMin.y, localMax.z),
|
|
|
|
|
Math::Vector3(localMax.x, localMax.y, localMax.z)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const Math::Vector3& localCorner : corners) {
|
|
|
|
|
const Math::Vector3 worldCorner = transform.TransformPoint(localCorner);
|
|
|
|
|
if (!inOutHasBounds) {
|
|
|
|
|
inOutBounds = Math::Bounds(worldCorner, Math::Vector3::Zero());
|
|
|
|
|
inOutHasBounds = true;
|
|
|
|
|
} else {
|
|
|
|
|
inOutBounds.Encapsulate(worldCorner);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CollectRenderableWorldBoundsRecursive(
|
|
|
|
|
const Components::GameObject& gameObject,
|
|
|
|
|
Math::Bounds& inOutBounds,
|
|
|
|
|
bool& inOutHasBounds) {
|
|
|
|
|
if (!gameObject.IsActiveInHierarchy()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto* meshFilter = gameObject.GetComponent<Components::MeshFilterComponent>();
|
|
|
|
|
const auto* meshRenderer = gameObject.GetComponent<Components::MeshRendererComponent>();
|
|
|
|
|
if (meshFilter != nullptr &&
|
|
|
|
|
meshRenderer != nullptr &&
|
|
|
|
|
meshFilter->IsEnabled() &&
|
|
|
|
|
meshRenderer->IsEnabled()) {
|
|
|
|
|
const auto* mesh = meshFilter->GetMesh();
|
|
|
|
|
if (mesh != nullptr) {
|
|
|
|
|
EncapsulateTransformedBounds(mesh->GetBounds(), *gameObject.GetTransform(), inOutBounds, inOutHasBounds);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (size_t childIndex = 0; childIndex < gameObject.GetChildCount(); ++childIndex) {
|
|
|
|
|
const Components::GameObject* child = gameObject.GetChild(childIndex);
|
|
|
|
|
if (child != nullptr) {
|
|
|
|
|
CollectRenderableWorldBoundsRecursive(*child, inOutBounds, inOutHasBounds);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Math::Vector3 GetGizmoWorldOrigin(const Components::GameObject& gameObject) {
|
|
|
|
|
Math::Bounds worldBounds = {};
|
|
|
|
|
bool hasBounds = false;
|
|
|
|
|
CollectRenderableWorldBoundsRecursive(gameObject, worldBounds, hasBounds);
|
|
|
|
|
return hasBounds ? worldBounds.center : gameObject.GetTransform()->GetPosition();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 16:18:13 +08:00
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) {
|
|
|
|
|
BuildDrawData(context);
|
|
|
|
|
if (m_activeAxis == SceneViewportGizmoAxis::None && IsMouseInsideViewport(context)) {
|
|
|
|
|
m_hoveredAxis = HitTestAxis(context.mousePosition);
|
|
|
|
|
} else if (m_activeAxis == SceneViewportGizmoAxis::None) {
|
|
|
|
|
m_hoveredAxis = SceneViewportGizmoAxis::None;
|
|
|
|
|
} else {
|
|
|
|
|
m_hoveredAxis = m_activeAxis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RefreshHandleState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& context, IUndoManager& undoManager) {
|
|
|
|
|
if (m_activeAxis != SceneViewportGizmoAxis::None ||
|
|
|
|
|
m_hoveredAxis == SceneViewportGizmoAxis::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 worldAxis = GetAxisVector(m_hoveredAxis);
|
|
|
|
|
Math::Vector3 dragPlaneNormal = Math::Vector3::Zero();
|
|
|
|
|
if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 00:47:31 +08:00
|
|
|
const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
|
|
|
|
const Math::Vector3 pivotWorldPosition = GetGizmoWorldOrigin(*context.selectedObject);
|
|
|
|
|
const Math::Plane dragPlane = BuildSceneViewportPlaneFromPointNormal(pivotWorldPosition, dragPlaneNormal);
|
2026-03-29 16:18:13 +08:00
|
|
|
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_activeAxis = m_hoveredAxis;
|
|
|
|
|
m_activeEntityId = context.selectedObject->GetID();
|
|
|
|
|
m_activeAxisDirection = worldAxis;
|
|
|
|
|
m_dragPlane = dragPlane;
|
2026-03-30 00:47:31 +08:00
|
|
|
m_dragStartObjectWorldPosition = objectWorldPosition;
|
|
|
|
|
m_dragStartPivotWorldPosition = pivotWorldPosition;
|
|
|
|
|
m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - pivotWorldPosition, worldAxis);
|
2026-03-29 16:18:13 +08:00
|
|
|
RefreshHandleState();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& context) {
|
|
|
|
|
if (m_activeAxis == SceneViewportGizmoAxis::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);
|
2026-03-30 00:47:31 +08:00
|
|
|
const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection);
|
2026-03-29 16:18:13 +08:00
|
|
|
const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar;
|
|
|
|
|
context.selectedObject->GetTransform()->SetPosition(
|
2026-03-30 00:47:31 +08:00
|
|
|
m_dragStartObjectWorldPosition + m_activeAxisDirection * deltaScalar);
|
2026-03-29 16:18:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) {
|
|
|
|
|
if (m_activeAxis == SceneViewportGizmoAxis::None) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (undoManager.HasPendingInteractiveChange()) {
|
|
|
|
|
undoManager.FinalizeInteractiveChange();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_activeAxis = SceneViewportGizmoAxis::None;
|
|
|
|
|
m_activeEntityId = 0;
|
|
|
|
|
m_activeAxisDirection = Math::Vector3::Zero();
|
2026-03-30 00:47:31 +08:00
|
|
|
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
|
|
|
|
|
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
2026-03-29 16:18:13 +08:00
|
|
|
m_dragStartAxisScalar = 0.0f;
|
|
|
|
|
RefreshHandleState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SceneViewportMoveGizmo::CancelDrag(IUndoManager* undoManager) {
|
|
|
|
|
if (undoManager != nullptr && undoManager->HasPendingInteractiveChange()) {
|
|
|
|
|
undoManager->CancelInteractiveChange();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_activeAxis = SceneViewportGizmoAxis::None;
|
|
|
|
|
m_activeEntityId = 0;
|
|
|
|
|
m_activeAxisDirection = Math::Vector3::Zero();
|
2026-03-30 00:47:31 +08:00
|
|
|
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
|
|
|
|
|
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
2026-03-29 16:18:13 +08:00
|
|
|
m_dragStartAxisScalar = 0.0f;
|
|
|
|
|
m_hoveredAxis = SceneViewportGizmoAxis::None;
|
|
|
|
|
RefreshHandleState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool SceneViewportMoveGizmo::IsHoveringHandle() const {
|
|
|
|
|
return m_hoveredAxis != SceneViewportGizmoAxis::None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool SceneViewportMoveGizmo::IsActive() const {
|
|
|
|
|
return m_activeAxis != SceneViewportGizmoAxis::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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 00:47:31 +08:00
|
|
|
const Math::Vector3 gizmoWorldOrigin = GetGizmoWorldOrigin(*selectedObject);
|
2026-03-29 16:18:13 +08:00
|
|
|
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
|
|
|
|
|
context.overlay,
|
|
|
|
|
context.viewportSize.x,
|
|
|
|
|
context.viewportSize.y,
|
2026-03-30 00:47:31 +08:00
|
|
|
gizmoWorldOrigin);
|
2026-03-29 16:18:13 +08:00
|
|
|
if (!projectedPivot.visible) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_drawData.visible = true;
|
|
|
|
|
m_drawData.pivot = projectedPivot.screenPosition;
|
2026-03-30 00:47:31 +08:00
|
|
|
const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(
|
|
|
|
|
context.overlay,
|
|
|
|
|
gizmoWorldOrigin,
|
|
|
|
|
context.viewportSize.y);
|
|
|
|
|
if (worldUnitsPerPixel <= Math::EPSILON) {
|
|
|
|
|
m_drawData = {};
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const float axisLengthWorld = worldUnitsPerPixel * kMoveGizmoHandleLengthPixels;
|
2026-03-29 16:18:13 +08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-03-30 00:47:31 +08:00
|
|
|
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) {
|
2026-03-29 16:18:13 +08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 00:47:31 +08:00
|
|
|
handle.end = projectedEnd.screenPosition;
|
2026-03-29 16:18:13 +08:00
|
|
|
handle.visible = true;
|
|
|
|
|
handle.color = GetAxisBaseColor(handle.axis);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace Editor
|
|
|
|
|
} // namespace XCEngine
|