feat: refine scene viewport gizmos and controls
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -59,6 +60,16 @@ struct SceneViewportOverlayData {
|
||||
float orbitDistance = 6.0f;
|
||||
};
|
||||
|
||||
enum class SceneViewportOrientationAxis : uint8_t {
|
||||
None = 0,
|
||||
PositiveX,
|
||||
NegativeX,
|
||||
PositiveY,
|
||||
NegativeY,
|
||||
PositiveZ,
|
||||
NegativeZ
|
||||
};
|
||||
|
||||
class IViewportHostService {
|
||||
public:
|
||||
virtual ~IViewportHostService() = default;
|
||||
@@ -70,6 +81,7 @@ public:
|
||||
IEditorContext& context,
|
||||
const ImVec2& viewportSize,
|
||||
const ImVec2& viewportMousePosition) = 0;
|
||||
virtual void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis axis) = 0;
|
||||
virtual SceneViewportOverlayData GetSceneViewOverlayData() const = 0;
|
||||
virtual void RenderRequestedViewports(
|
||||
IEditorContext& context,
|
||||
|
||||
@@ -36,6 +36,7 @@ public:
|
||||
m_flySpeed = 5.0f;
|
||||
m_yawDegrees = -35.0f;
|
||||
m_pitchDegrees = -20.0f;
|
||||
m_snapAnimating = false;
|
||||
UpdatePositionFromFocalPoint();
|
||||
}
|
||||
|
||||
@@ -78,11 +79,53 @@ public:
|
||||
UpdatePositionFromFocalPoint();
|
||||
}
|
||||
|
||||
void SnapToForward(const Math::Vector3& forward) {
|
||||
if (forward.SqrMagnitude() <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
const OrientationAngles target = ComputeOrientationAngles(forward);
|
||||
m_yawDegrees = target.yawDegrees;
|
||||
m_pitchDegrees = target.pitchDegrees;
|
||||
m_snapAnimating = false;
|
||||
UpdatePositionFromFocalPoint();
|
||||
}
|
||||
|
||||
void AnimateToForward(const Math::Vector3& forward) {
|
||||
if (forward.SqrMagnitude() <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
const OrientationAngles target = ComputeOrientationAngles(forward);
|
||||
m_snapStartYawDegrees = m_yawDegrees;
|
||||
m_snapStartPitchDegrees = m_pitchDegrees;
|
||||
m_snapTargetYawDegrees = target.yawDegrees;
|
||||
m_snapTargetPitchDegrees = target.pitchDegrees;
|
||||
m_snapElapsed = 0.0f;
|
||||
m_snapAnimating = true;
|
||||
}
|
||||
|
||||
void ApplyInput(const SceneViewportCameraInputState& input) {
|
||||
if (input.viewportHeight <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool hasManualInput =
|
||||
std::abs(input.lookDeltaX) > Math::EPSILON ||
|
||||
std::abs(input.lookDeltaY) > Math::EPSILON ||
|
||||
std::abs(input.orbitDeltaX) > Math::EPSILON ||
|
||||
std::abs(input.orbitDeltaY) > Math::EPSILON ||
|
||||
std::abs(input.panDeltaX) > Math::EPSILON ||
|
||||
std::abs(input.panDeltaY) > Math::EPSILON ||
|
||||
std::abs(input.zoomDelta) > Math::EPSILON ||
|
||||
std::abs(input.flySpeedDelta) > Math::EPSILON ||
|
||||
std::abs(input.moveForward) > Math::EPSILON ||
|
||||
std::abs(input.moveRight) > Math::EPSILON ||
|
||||
std::abs(input.moveUp) > Math::EPSILON;
|
||||
if (hasManualInput) {
|
||||
m_snapAnimating = false;
|
||||
}
|
||||
|
||||
if (std::abs(input.lookDeltaX) > Math::EPSILON ||
|
||||
std::abs(input.lookDeltaY) > Math::EPSILON) {
|
||||
ApplyRotationDelta(input.lookDeltaX, input.lookDeltaY);
|
||||
@@ -108,7 +151,7 @@ public:
|
||||
|
||||
if (std::abs(input.flySpeedDelta) > Math::EPSILON) {
|
||||
const float speedFactor = std::pow(1.20f, input.flySpeedDelta);
|
||||
m_flySpeed = std::clamp(m_flySpeed * speedFactor, 0.5f, 500.0f);
|
||||
m_flySpeed = std::clamp(m_flySpeed * speedFactor, 0.1f, 500.0f);
|
||||
}
|
||||
|
||||
if (input.deltaTime > 0.0f &&
|
||||
@@ -133,6 +176,18 @@ public:
|
||||
m_distance = std::clamp(m_distance * zoomFactor, 0.5f, 500.0f);
|
||||
UpdatePositionFromFocalPoint();
|
||||
}
|
||||
|
||||
if (m_snapAnimating && input.deltaTime > 0.0f) {
|
||||
m_snapElapsed += input.deltaTime;
|
||||
const float t = std::clamp(m_snapElapsed / kSnapDurationSeconds, 0.0f, 1.0f);
|
||||
const float easedT = 1.0f - std::pow(1.0f - t, 3.0f);
|
||||
m_yawDegrees = LerpAngleDegrees(m_snapStartYawDegrees, m_snapTargetYawDegrees, easedT);
|
||||
m_pitchDegrees = m_snapStartPitchDegrees + (m_snapTargetPitchDegrees - m_snapStartPitchDegrees) * easedT;
|
||||
UpdatePositionFromFocalPoint();
|
||||
if (t >= 1.0f) {
|
||||
m_snapAnimating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyTo(Components::TransformComponent& transform) const {
|
||||
@@ -144,6 +199,46 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr float kSnapDurationSeconds = 0.22f;
|
||||
|
||||
struct OrientationAngles {
|
||||
float yawDegrees = 0.0f;
|
||||
float pitchDegrees = 0.0f;
|
||||
};
|
||||
|
||||
static float NormalizeDegrees(float value) {
|
||||
while (value > 180.0f) {
|
||||
value -= 360.0f;
|
||||
}
|
||||
while (value < -180.0f) {
|
||||
value += 360.0f;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static float LerpAngleDegrees(float fromDegrees, float toDegrees, float t) {
|
||||
const float delta = NormalizeDegrees(toDegrees - fromDegrees);
|
||||
return NormalizeDegrees(fromDegrees + delta * t);
|
||||
}
|
||||
|
||||
static OrientationAngles ComputeOrientationAngles(const Math::Vector3& forward) {
|
||||
OrientationAngles result = {};
|
||||
const Math::Vector3 normalizedForward = forward.Normalized();
|
||||
const float horizontalLengthSq =
|
||||
normalizedForward.x * normalizedForward.x +
|
||||
normalizedForward.z * normalizedForward.z;
|
||||
if (horizontalLengthSq <= Math::EPSILON) {
|
||||
result.yawDegrees = 0.0f;
|
||||
} else {
|
||||
result.yawDegrees = std::atan2(normalizedForward.x, normalizedForward.z) * Math::RAD_TO_DEG;
|
||||
}
|
||||
result.pitchDegrees = std::clamp(
|
||||
std::asin(std::clamp(normalizedForward.y, -1.0f, 1.0f)) * Math::RAD_TO_DEG,
|
||||
-89.0f,
|
||||
89.0f);
|
||||
return result;
|
||||
}
|
||||
|
||||
void ApplyRotationDelta(float deltaX, float deltaY) {
|
||||
m_yawDegrees += deltaX * 0.30f;
|
||||
m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * 0.20f, -89.0f, 89.0f);
|
||||
@@ -180,6 +275,12 @@ private:
|
||||
float m_flySpeed = 5.0f;
|
||||
float m_yawDegrees = -35.0f;
|
||||
float m_pitchDegrees = -20.0f;
|
||||
bool m_snapAnimating = false;
|
||||
float m_snapElapsed = 0.0f;
|
||||
float m_snapStartYawDegrees = 0.0f;
|
||||
float m_snapStartPitchDegrees = 0.0f;
|
||||
float m_snapTargetYawDegrees = 0.0f;
|
||||
float m_snapTargetPitchDegrees = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
|
||||
@@ -88,6 +88,8 @@ PSOutput MainPS(VSOutput input) {
|
||||
const float tanHalfFov = max(gCameraUpAndTanHalfFov.w, 1e-4);
|
||||
const float aspect = max(gCameraForwardAndAspect.w, 1e-4);
|
||||
const float transitionBlend = saturate(gGridTransition.x);
|
||||
const float nearClip = gViewportNearFar.z;
|
||||
const float sceneFarClip = gViewportNearFar.w;
|
||||
|
||||
const float2 ndc = float2(
|
||||
(input.position.x / viewportSize.x) * 2.0 - 1.0,
|
||||
@@ -104,19 +106,22 @@ PSOutput MainPS(VSOutput input) {
|
||||
}
|
||||
|
||||
const float t = -cameraPosition.y / rayDirection.y;
|
||||
if (t <= gViewportNearFar.z || t >= gViewportNearFar.w) {
|
||||
if (t <= nearClip) {
|
||||
discard;
|
||||
}
|
||||
|
||||
const float3 worldPosition = cameraPosition + rayDirection * t;
|
||||
const float4 clipPosition = mul(gViewProjectionMatrix, float4(worldPosition, 1.0));
|
||||
if (clipPosition.w <= 1e-6) {
|
||||
discard;
|
||||
}
|
||||
float depth = 0.999999;
|
||||
if (t < sceneFarClip) {
|
||||
const float4 clipPosition = mul(gViewProjectionMatrix, float4(worldPosition, 1.0));
|
||||
if (clipPosition.w <= 1e-6) {
|
||||
discard;
|
||||
}
|
||||
|
||||
const float depth = clipPosition.z / clipPosition.w;
|
||||
if (depth <= 0.0 || depth >= 1.0) {
|
||||
discard;
|
||||
depth = clipPosition.z / clipPosition.w;
|
||||
if (depth <= 0.0 || depth >= 1.0) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
|
||||
const float radialFade =
|
||||
|
||||
@@ -107,6 +107,70 @@ inline bool ProjectSceneViewportAxisDirection(
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ProjectSceneViewportAxisDirectionAtPoint(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
float viewportWidth,
|
||||
float viewportHeight,
|
||||
const Math::Vector3& worldPoint,
|
||||
const Math::Vector3& worldAxis,
|
||||
Math::Vector2& outScreenDirection,
|
||||
float sampleDistance = 1.0f) {
|
||||
const Math::Vector3 axis = worldAxis.Normalized();
|
||||
if (!overlay.valid ||
|
||||
viewportWidth <= 1.0f ||
|
||||
viewportHeight <= 1.0f ||
|
||||
axis.SqrMagnitude() <= Math::EPSILON ||
|
||||
sampleDistance <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Matrix4x4 viewProjection =
|
||||
BuildSceneViewportViewProjectionMatrix(overlay, viewportWidth, viewportHeight);
|
||||
const Math::Vector4 startClip = viewProjection * Math::Vector4(worldPoint, 1.0f);
|
||||
const Math::Vector4 endClip = viewProjection * Math::Vector4(worldPoint + axis * sampleDistance, 1.0f);
|
||||
if (startClip.w <= Math::EPSILON || endClip.w <= Math::EPSILON) {
|
||||
return ProjectSceneViewportAxisDirection(overlay, axis, outScreenDirection);
|
||||
}
|
||||
|
||||
const Math::Vector3 startNdc = startClip.ToVector3() / startClip.w;
|
||||
const Math::Vector3 endNdc = endClip.ToVector3() / endClip.w;
|
||||
const Math::Vector2 startScreen(
|
||||
(startNdc.x * 0.5f + 0.5f) * viewportWidth,
|
||||
(1.0f - (startNdc.y * 0.5f + 0.5f)) * viewportHeight);
|
||||
const Math::Vector2 endScreen(
|
||||
(endNdc.x * 0.5f + 0.5f) * viewportWidth,
|
||||
(1.0f - (endNdc.y * 0.5f + 0.5f)) * viewportHeight);
|
||||
|
||||
const Math::Vector2 screenDirection = endScreen - startScreen;
|
||||
if (screenDirection.SqrMagnitude() <= Math::EPSILON) {
|
||||
return ProjectSceneViewportAxisDirection(overlay, axis, outScreenDirection);
|
||||
}
|
||||
|
||||
outScreenDirection = screenDirection.Normalized();
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ProjectSceneViewportWorldPointClamped(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
float viewportWidth,
|
||||
float viewportHeight,
|
||||
const Math::Vector3& worldPoint,
|
||||
float edgePadding,
|
||||
SceneViewportProjectedPoint& outProjectedPoint) {
|
||||
outProjectedPoint = ProjectSceneViewportWorldPoint(overlay, viewportWidth, viewportHeight, worldPoint);
|
||||
if (!overlay.valid || viewportWidth <= 1.0f || viewportHeight <= 1.0f || outProjectedPoint.ndcDepth < 0.0f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float minX = edgePadding;
|
||||
const float minY = edgePadding;
|
||||
const float maxX = viewportWidth - edgePadding;
|
||||
const float maxY = viewportHeight - edgePadding;
|
||||
outProjectedPoint.screenPosition.x = std::clamp(outProjectedPoint.screenPosition.x, minX, maxX);
|
||||
outProjectedPoint.screenPosition.y = std::clamp(outProjectedPoint.screenPosition.y, minY, maxY);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline float DistanceToSegmentSquared(
|
||||
const Math::Vector2& point,
|
||||
const Math::Vector2& segmentStart,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,13 @@ enum class SceneViewportGizmoAxis : uint8_t {
|
||||
Z
|
||||
};
|
||||
|
||||
enum class SceneViewportGizmoPlane : uint8_t {
|
||||
None = 0,
|
||||
XY,
|
||||
XZ,
|
||||
YZ
|
||||
};
|
||||
|
||||
struct SceneViewportMoveGizmoHandleDrawData {
|
||||
SceneViewportGizmoAxis axis = SceneViewportGizmoAxis::None;
|
||||
Math::Vector2 start = Math::Vector2::Zero();
|
||||
@@ -36,11 +43,22 @@ struct SceneViewportMoveGizmoHandleDrawData {
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
struct SceneViewportMoveGizmoPlaneDrawData {
|
||||
SceneViewportGizmoPlane plane = SceneViewportGizmoPlane::None;
|
||||
std::array<Math::Vector2, 4> corners = {};
|
||||
Math::Color fillColor = Math::Color::White();
|
||||
Math::Color outlineColor = Math::Color::White();
|
||||
bool visible = false;
|
||||
bool hovered = false;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
struct SceneViewportMoveGizmoDrawData {
|
||||
bool visible = false;
|
||||
Math::Vector2 pivot = Math::Vector2::Zero();
|
||||
float pivotRadius = 5.0f;
|
||||
std::array<SceneViewportMoveGizmoHandleDrawData, 3> handles = {};
|
||||
std::array<SceneViewportMoveGizmoPlaneDrawData, 3> planes = {};
|
||||
};
|
||||
|
||||
struct SceneViewportMoveGizmoContext {
|
||||
@@ -64,17 +82,30 @@ public:
|
||||
const SceneViewportMoveGizmoDrawData& GetDrawData() const;
|
||||
|
||||
private:
|
||||
enum class DragMode : uint8_t {
|
||||
None = 0,
|
||||
Axis,
|
||||
Plane
|
||||
};
|
||||
|
||||
void BuildDrawData(const SceneViewportMoveGizmoContext& context);
|
||||
void RefreshHandleState();
|
||||
SceneViewportGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const;
|
||||
SceneViewportGizmoPlane HitTestPlane(const Math::Vector2& mousePosition) const;
|
||||
|
||||
SceneViewportMoveGizmoDrawData m_drawData = {};
|
||||
SceneViewportGizmoAxis m_hoveredAxis = SceneViewportGizmoAxis::None;
|
||||
SceneViewportGizmoPlane m_hoveredPlane = SceneViewportGizmoPlane::None;
|
||||
SceneViewportGizmoAxis m_activeAxis = SceneViewportGizmoAxis::None;
|
||||
SceneViewportGizmoPlane m_activePlane = SceneViewportGizmoPlane::None;
|
||||
DragMode m_dragMode = DragMode::None;
|
||||
uint64_t m_activeEntityId = 0;
|
||||
Math::Vector3 m_activeAxisDirection = Math::Vector3::Zero();
|
||||
Math::Vector3 m_activePlaneNormal = Math::Vector3::Zero();
|
||||
Math::Plane m_dragPlane = {};
|
||||
Math::Vector3 m_dragStartWorldPosition = Math::Vector3::Zero();
|
||||
Math::Vector3 m_dragStartObjectWorldPosition = Math::Vector3::Zero();
|
||||
Math::Vector3 m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
Math::Vector3 m_dragStartHitWorldPosition = Math::Vector3::Zero();
|
||||
float m_dragStartAxisScalar = 0.0f;
|
||||
};
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ ProjectedPoint ProjectPoint(const ImVec2& center, const Math::Vector3& point) {
|
||||
}
|
||||
|
||||
struct AxisHandleVisual {
|
||||
SceneViewportOrientationAxis axis = SceneViewportOrientationAxis::None;
|
||||
Math::Vector3 cameraDirection = Math::Vector3::Zero();
|
||||
Math::Color baseColor = Math::Color::White();
|
||||
const char* label = nullptr;
|
||||
@@ -137,6 +138,17 @@ struct AxisHandleVisual {
|
||||
float sortDepth = 0.0f;
|
||||
};
|
||||
|
||||
struct AxisHandleProjection {
|
||||
bool valid = false;
|
||||
ProjectedPoint tip = {};
|
||||
ProjectedPoint capCenter = {};
|
||||
ImVec2 axisDirection = ImVec2(0.0f, 0.0f);
|
||||
ImVec2 leftPoint = ImVec2(0.0f, 0.0f);
|
||||
ImVec2 rightPoint = ImVec2(0.0f, 0.0f);
|
||||
std::array<ImVec2, kConeCapSegments> capPoints = {};
|
||||
float capMinorSpan = 0.0f;
|
||||
};
|
||||
|
||||
struct CubeFaceVisual {
|
||||
std::array<ImVec2, 4> points = {};
|
||||
Math::Vector3 cameraNormal = Math::Vector3::Zero();
|
||||
@@ -157,6 +169,178 @@ float ComputePolygonSignedArea(const ImVec2* points, int pointCount) {
|
||||
return area * 0.5f;
|
||||
}
|
||||
|
||||
float ComputeSegmentDistanceSquared(const ImVec2& point, const ImVec2& a, const ImVec2& b) {
|
||||
const float dx = b.x - a.x;
|
||||
const float dy = b.y - a.y;
|
||||
const float lengthSq = dx * dx + dy * dy;
|
||||
if (lengthSq <= 1e-6f) {
|
||||
const float px = point.x - a.x;
|
||||
const float py = point.y - a.y;
|
||||
return px * px + py * py;
|
||||
}
|
||||
|
||||
const float t = std::clamp(
|
||||
((point.x - a.x) * dx + (point.y - a.y) * dy) / lengthSq,
|
||||
0.0f,
|
||||
1.0f);
|
||||
const float closestX = a.x + dx * t;
|
||||
const float closestY = a.y + dy * t;
|
||||
const float px = point.x - closestX;
|
||||
const float py = point.y - closestY;
|
||||
return px * px + py * py;
|
||||
}
|
||||
|
||||
bool PointInTriangle(const ImVec2& point, const ImVec2& a, const ImVec2& b, const ImVec2& c) {
|
||||
const float ab = (point.x - b.x) * (a.y - b.y) - (a.x - b.x) * (point.y - b.y);
|
||||
const float bc = (point.x - c.x) * (b.y - c.y) - (b.x - c.x) * (point.y - c.y);
|
||||
const float ca = (point.x - a.x) * (c.y - a.y) - (c.x - a.x) * (point.y - a.y);
|
||||
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 PointInConvexPolygon(const ImVec2& point, const ImVec2* polygon, int pointCount) {
|
||||
if (polygon == nullptr || pointCount < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float sign = 0.0f;
|
||||
for (int i = 0; i < pointCount; ++i) {
|
||||
const ImVec2& a = polygon[i];
|
||||
const ImVec2& b = polygon[(i + 1) % pointCount];
|
||||
const float cross = (point.x - a.x) * (b.y - a.y) - (point.y - a.y) * (b.x - a.x);
|
||||
if (std::abs(cross) <= 1e-4f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sign == 0.0f) {
|
||||
sign = cross;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((sign > 0.0f) != (cross > 0.0f)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ImVec2 BuildWidgetCenter(const ImVec2& viewportMin, const ImVec2& viewportMax) {
|
||||
return ImVec2(viewportMax.x - kWidgetInset, viewportMin.y + kWidgetInset);
|
||||
}
|
||||
|
||||
std::array<AxisHandleVisual, 6> BuildAxisHandles(const SceneViewportOverlayData& overlay) {
|
||||
return {{
|
||||
{ SceneViewportOrientationAxis::PositiveX, TransformToCameraSpace(overlay, Math::Vector3::Right()), Math::Color(0.91f, 0.09f, 0.05f, 1.0f), "x", true, 0.0f },
|
||||
{ SceneViewportOrientationAxis::NegativeX, TransformToCameraSpace(overlay, Math::Vector3::Left()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f },
|
||||
{ SceneViewportOrientationAxis::PositiveY, TransformToCameraSpace(overlay, Math::Vector3::Up()), Math::Color(0.45f, 1.0f, 0.12f, 1.0f), "y", true, 0.0f },
|
||||
{ SceneViewportOrientationAxis::NegativeY, TransformToCameraSpace(overlay, Math::Vector3::Down()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f },
|
||||
{ SceneViewportOrientationAxis::PositiveZ, TransformToCameraSpace(overlay, Math::Vector3::Forward()), Math::Color(0.11f, 0.29f, 1.0f, 1.0f), "z", true, 0.0f },
|
||||
{ SceneViewportOrientationAxis::NegativeZ, TransformToCameraSpace(overlay, Math::Vector3::Back()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f }
|
||||
}};
|
||||
}
|
||||
|
||||
std::vector<AxisHandleVisual> BuildSortedAxisHandles(const SceneViewportOverlayData& overlay) {
|
||||
const std::array<AxisHandleVisual, 6> handles = BuildAxisHandles(overlay);
|
||||
std::vector<AxisHandleVisual> sortedHandles(handles.begin(), handles.end());
|
||||
for (AxisHandleVisual& handle : sortedHandles) {
|
||||
handle.sortDepth = handle.cameraDirection.z;
|
||||
}
|
||||
|
||||
std::sort(
|
||||
sortedHandles.begin(),
|
||||
sortedHandles.end(),
|
||||
[](const AxisHandleVisual& lhs, const AxisHandleVisual& rhs) {
|
||||
return lhs.sortDepth > rhs.sortDepth;
|
||||
});
|
||||
return sortedHandles;
|
||||
}
|
||||
|
||||
AxisHandleProjection BuildAxisHandleProjection(const ImVec2& center, const AxisHandleVisual& handle) {
|
||||
AxisHandleProjection projection = {};
|
||||
if (handle.cameraDirection.SqrMagnitude() <= Math::EPSILON) {
|
||||
return projection;
|
||||
}
|
||||
|
||||
const float tipDistance = kCubeHalfExtent + 0.65f;
|
||||
const float capDistance = handle.positive ? kPositiveAxisLength : kNegativeAxisLength;
|
||||
const float capRadius = 7.1f;
|
||||
|
||||
const Math::Vector3 tipPoint3 = handle.cameraDirection * tipDistance;
|
||||
const Math::Vector3 capCenter3 = handle.cameraDirection * capDistance;
|
||||
|
||||
projection.tip = ProjectPoint(center, tipPoint3);
|
||||
projection.capCenter = ProjectPoint(center, capCenter3);
|
||||
const ImVec2 axisVector(
|
||||
projection.capCenter.position.x - projection.tip.position.x,
|
||||
projection.capCenter.position.y - projection.tip.position.y);
|
||||
const float axisLengthSq = axisVector.x * axisVector.x + axisVector.y * axisVector.y;
|
||||
const float axisLength = std::sqrt(axisLengthSq);
|
||||
projection.axisDirection = axisLength > 1e-4f
|
||||
? ImVec2(axisVector.x / axisLength, axisVector.y / axisLength)
|
||||
: ImVec2(0.0f, 0.0f);
|
||||
const ImVec2 sideDirection(-projection.axisDirection.y, projection.axisDirection.x);
|
||||
|
||||
Math::Vector3 capTangent = Math::Vector3::Right();
|
||||
Math::Vector3 capBitangent = Math::Vector3::Up();
|
||||
BuildPerpendicularBasis(handle.cameraDirection, capTangent, capBitangent);
|
||||
|
||||
float maxSide = std::numeric_limits<float>::lowest();
|
||||
float minSide = std::numeric_limits<float>::max();
|
||||
float maxAxis = std::numeric_limits<float>::lowest();
|
||||
float minAxis = std::numeric_limits<float>::max();
|
||||
for (int i = 0; i < kConeCapSegments; ++i) {
|
||||
const float angle = (static_cast<float>(i) / static_cast<float>(kConeCapSegments)) * kTau;
|
||||
const Math::Vector3 ringPoint =
|
||||
capCenter3 +
|
||||
capTangent * (std::cos(angle) * capRadius) +
|
||||
capBitangent * (std::sin(angle) * capRadius);
|
||||
projection.capPoints[i] = ProjectPoint(center, ringPoint).position;
|
||||
|
||||
const ImVec2 offset(
|
||||
projection.capPoints[i].x - projection.capCenter.position.x,
|
||||
projection.capPoints[i].y - projection.capCenter.position.y);
|
||||
const float sideValue = offset.x * sideDirection.x + offset.y * sideDirection.y;
|
||||
const float axisValue = offset.x * projection.axisDirection.x + offset.y * projection.axisDirection.y;
|
||||
if (sideValue > maxSide) {
|
||||
maxSide = sideValue;
|
||||
projection.leftPoint = projection.capPoints[i];
|
||||
}
|
||||
if (sideValue < minSide) {
|
||||
minSide = sideValue;
|
||||
projection.rightPoint = projection.capPoints[i];
|
||||
}
|
||||
maxAxis = std::max(maxAxis, axisValue);
|
||||
minAxis = std::min(minAxis, axisValue);
|
||||
}
|
||||
|
||||
projection.capMinorSpan = maxAxis - minAxis;
|
||||
projection.valid = true;
|
||||
return projection;
|
||||
}
|
||||
|
||||
bool HitTestAxisHandle(const ImVec2& mousePosition, const ImVec2& center, const AxisHandleVisual& handle) {
|
||||
const AxisHandleProjection projection = BuildAxisHandleProjection(center, handle);
|
||||
if (!projection.valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (projection.capMinorSpan <= 1.35f) {
|
||||
if (ComputeSegmentDistanceSquared(mousePosition, projection.rightPoint, projection.leftPoint) <= 16.0f) {
|
||||
return true;
|
||||
}
|
||||
} else if (PointInConvexPolygon(mousePosition, projection.capPoints.data(), static_cast<int>(projection.capPoints.size()))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return PointInTriangle(
|
||||
mousePosition,
|
||||
projection.tip.position,
|
||||
projection.leftPoint,
|
||||
projection.rightPoint);
|
||||
}
|
||||
|
||||
void DrawAxisLabel(ImDrawList* drawList, const ImVec2& position, const char* label) {
|
||||
if (drawList == nullptr || label == nullptr) {
|
||||
return;
|
||||
@@ -283,59 +467,9 @@ void DrawAxisHandle(
|
||||
}
|
||||
|
||||
const float frontFactor = Saturate((-handle.cameraDirection.z + 1.0f) * 0.5f);
|
||||
const float tipDistance = kCubeHalfExtent + 0.65f;
|
||||
const float capDistance = handle.positive ? kPositiveAxisLength : kNegativeAxisLength;
|
||||
const float capRadius = 7.1f;
|
||||
|
||||
const Math::Vector3 tipPoint3 = handle.cameraDirection * tipDistance;
|
||||
const Math::Vector3 capCenter3 = handle.cameraDirection * capDistance;
|
||||
|
||||
const ProjectedPoint tip = ProjectPoint(center, tipPoint3);
|
||||
const ProjectedPoint capCenter = ProjectPoint(center, capCenter3);
|
||||
const ImVec2 axisVector(
|
||||
capCenter.position.x - tip.position.x,
|
||||
capCenter.position.y - tip.position.y);
|
||||
const float axisLengthSq = axisVector.x * axisVector.x + axisVector.y * axisVector.y;
|
||||
const float axisLength = std::sqrt(axisLengthSq);
|
||||
const ImVec2 axisDirection = axisLength > 1e-4f
|
||||
? ImVec2(axisVector.x / axisLength, axisVector.y / axisLength)
|
||||
: ImVec2(0.0f, 0.0f);
|
||||
const ImVec2 sideDirection(-axisDirection.y, axisDirection.x);
|
||||
|
||||
Math::Vector3 capTangent = Math::Vector3::Right();
|
||||
Math::Vector3 capBitangent = Math::Vector3::Up();
|
||||
BuildPerpendicularBasis(handle.cameraDirection, capTangent, capBitangent);
|
||||
|
||||
std::array<ImVec2, kConeCapSegments> capPoints = {};
|
||||
ImVec2 leftPoint = capCenter.position;
|
||||
ImVec2 rightPoint = capCenter.position;
|
||||
float maxSide = std::numeric_limits<float>::lowest();
|
||||
float minSide = std::numeric_limits<float>::max();
|
||||
float maxAxis = std::numeric_limits<float>::lowest();
|
||||
float minAxis = std::numeric_limits<float>::max();
|
||||
for (int i = 0; i < kConeCapSegments; ++i) {
|
||||
const float angle = (static_cast<float>(i) / static_cast<float>(kConeCapSegments)) * kTau;
|
||||
const Math::Vector3 ringPoint =
|
||||
capCenter3 +
|
||||
capTangent * (std::cos(angle) * capRadius) +
|
||||
capBitangent * (std::sin(angle) * capRadius);
|
||||
capPoints[i] = ProjectPoint(center, ringPoint).position;
|
||||
|
||||
const ImVec2 offset(
|
||||
capPoints[i].x - capCenter.position.x,
|
||||
capPoints[i].y - capCenter.position.y);
|
||||
const float sideValue = offset.x * sideDirection.x + offset.y * sideDirection.y;
|
||||
const float axisValue = offset.x * axisDirection.x + offset.y * axisDirection.y;
|
||||
if (sideValue > maxSide) {
|
||||
maxSide = sideValue;
|
||||
leftPoint = capPoints[i];
|
||||
}
|
||||
if (sideValue < minSide) {
|
||||
minSide = sideValue;
|
||||
rightPoint = capPoints[i];
|
||||
}
|
||||
maxAxis = std::max(maxAxis, axisValue);
|
||||
minAxis = std::min(minAxis, axisValue);
|
||||
const AxisHandleProjection projection = BuildAxisHandleProjection(center, handle);
|
||||
if (!projection.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Color bodyColor = handle.positive
|
||||
@@ -348,58 +482,60 @@ void DrawAxisHandle(
|
||||
: LerpColor(bodyColor, Math::Color::White(), 0.16f + frontFactor * 0.14f);
|
||||
|
||||
const ImVec2 shadowTriangle[] = {
|
||||
ImVec2(tip.position.x + 1.2f, tip.position.y + 1.4f),
|
||||
ImVec2(leftPoint.x + 1.2f, leftPoint.y + 1.4f),
|
||||
ImVec2(rightPoint.x + 1.2f, rightPoint.y + 1.4f)
|
||||
ImVec2(projection.tip.position.x + 1.2f, projection.tip.position.y + 1.4f),
|
||||
ImVec2(projection.leftPoint.x + 1.2f, projection.leftPoint.y + 1.4f),
|
||||
ImVec2(projection.rightPoint.x + 1.2f, projection.rightPoint.y + 1.4f)
|
||||
};
|
||||
const ImVec2 leftFacet[] = {
|
||||
tip.position,
|
||||
leftPoint,
|
||||
capCenter.position
|
||||
projection.tip.position,
|
||||
projection.leftPoint,
|
||||
projection.capCenter.position
|
||||
};
|
||||
const ImVec2 rightFacet[] = {
|
||||
tip.position,
|
||||
capCenter.position,
|
||||
rightPoint
|
||||
projection.tip.position,
|
||||
projection.capCenter.position,
|
||||
projection.rightPoint
|
||||
};
|
||||
|
||||
drawList->AddConvexPolyFilled(shadowTriangle, 3, IM_COL32(0, 0, 0, 58));
|
||||
drawList->AddConvexPolyFilled(leftFacet, 3, ToImGuiColor(lightColor));
|
||||
drawList->AddConvexPolyFilled(rightFacet, 3, ToImGuiColor(darkColor));
|
||||
drawList->AddLine(tip.position, leftPoint, IM_COL32(255, 255, 255, handle.positive ? 34 : 44), 1.0f);
|
||||
drawList->AddLine(tip.position, rightPoint, IM_COL32(0, 0, 0, 38), 1.0f);
|
||||
const float capMinorSpan = maxAxis - minAxis;
|
||||
if (capMinorSpan <= 1.35f) {
|
||||
drawList->AddLine(projection.tip.position, projection.leftPoint, IM_COL32(255, 255, 255, handle.positive ? 34 : 44), 1.0f);
|
||||
drawList->AddLine(projection.tip.position, projection.rightPoint, IM_COL32(0, 0, 0, 38), 1.0f);
|
||||
if (projection.capMinorSpan <= 1.35f) {
|
||||
drawList->AddLine(
|
||||
ImVec2(rightPoint.x + 1.0f, rightPoint.y + 1.2f),
|
||||
ImVec2(leftPoint.x + 1.0f, leftPoint.y + 1.2f),
|
||||
ImVec2(projection.rightPoint.x + 1.0f, projection.rightPoint.y + 1.2f),
|
||||
ImVec2(projection.leftPoint.x + 1.0f, projection.leftPoint.y + 1.2f),
|
||||
IM_COL32(0, 0, 0, 52),
|
||||
2.4f);
|
||||
drawList->AddLine(
|
||||
rightPoint,
|
||||
leftPoint,
|
||||
projection.rightPoint,
|
||||
projection.leftPoint,
|
||||
ToImGuiColor(capColor),
|
||||
2.0f);
|
||||
drawList->AddLine(
|
||||
rightPoint,
|
||||
leftPoint,
|
||||
projection.rightPoint,
|
||||
projection.leftPoint,
|
||||
IM_COL32(255, 255, 255, handle.positive ? 56 : 68),
|
||||
1.0f);
|
||||
} else {
|
||||
drawList->AddConvexPolyFilled(capPoints.data(), static_cast<int>(capPoints.size()), ToImGuiColor(capColor));
|
||||
drawList->AddConvexPolyFilled(projection.capPoints.data(), static_cast<int>(projection.capPoints.size()), ToImGuiColor(capColor));
|
||||
drawList->AddPolyline(
|
||||
capPoints.data(),
|
||||
static_cast<int>(capPoints.size()),
|
||||
projection.capPoints.data(),
|
||||
static_cast<int>(projection.capPoints.size()),
|
||||
IM_COL32(255, 255, 255, handle.positive ? 60 : 72),
|
||||
true,
|
||||
1.0f);
|
||||
}
|
||||
|
||||
if (handle.positive && handle.label != nullptr) {
|
||||
const float axisLength = std::sqrt(
|
||||
(projection.capCenter.position.x - projection.tip.position.x) * (projection.capCenter.position.x - projection.tip.position.x) +
|
||||
(projection.capCenter.position.y - projection.tip.position.y) * (projection.capCenter.position.y - projection.tip.position.y));
|
||||
const float labelOffset = 9.0f * Saturate((axisLength - 2.0f) / 12.0f);
|
||||
const ImVec2 labelPosition(
|
||||
capCenter.position.x + axisDirection.x * labelOffset,
|
||||
capCenter.position.y + axisDirection.y * labelOffset);
|
||||
projection.capCenter.position.x + projection.axisDirection.x * labelOffset,
|
||||
projection.capCenter.position.y + projection.axisDirection.y * labelOffset);
|
||||
DrawAxisLabel(drawList, labelPosition, handle.label);
|
||||
}
|
||||
}
|
||||
@@ -415,27 +551,8 @@ void DrawSceneViewportOrientationGizmo(
|
||||
return;
|
||||
}
|
||||
|
||||
const ImVec2 center(viewportMax.x - kWidgetInset, viewportMin.y + kWidgetInset);
|
||||
const std::array<AxisHandleVisual, 6> handles = {{
|
||||
{ TransformToCameraSpace(overlay, Math::Vector3::Right()), Math::Color(0.91f, 0.09f, 0.05f, 1.0f), "x", true, 0.0f },
|
||||
{ TransformToCameraSpace(overlay, Math::Vector3::Left()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f },
|
||||
{ TransformToCameraSpace(overlay, Math::Vector3::Up()), Math::Color(0.45f, 1.0f, 0.12f, 1.0f), "y", true, 0.0f },
|
||||
{ TransformToCameraSpace(overlay, Math::Vector3::Down()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f },
|
||||
{ TransformToCameraSpace(overlay, Math::Vector3::Forward()), Math::Color(0.11f, 0.29f, 1.0f, 1.0f), "z", true, 0.0f },
|
||||
{ TransformToCameraSpace(overlay, Math::Vector3::Back()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f }
|
||||
}};
|
||||
|
||||
std::vector<AxisHandleVisual> sortedHandles(handles.begin(), handles.end());
|
||||
for (AxisHandleVisual& handle : sortedHandles) {
|
||||
handle.sortDepth = handle.cameraDirection.z;
|
||||
}
|
||||
|
||||
std::sort(
|
||||
sortedHandles.begin(),
|
||||
sortedHandles.end(),
|
||||
[](const AxisHandleVisual& lhs, const AxisHandleVisual& rhs) {
|
||||
return lhs.sortDepth > rhs.sortDepth;
|
||||
});
|
||||
const ImVec2 center = BuildWidgetCenter(viewportMin, viewportMax);
|
||||
const std::vector<AxisHandleVisual> sortedHandles = BuildSortedAxisHandles(overlay);
|
||||
|
||||
for (const AxisHandleVisual& handle : sortedHandles) {
|
||||
if (handle.sortDepth > 0.0f) {
|
||||
@@ -452,5 +569,32 @@ void DrawSceneViewportOrientationGizmo(
|
||||
}
|
||||
}
|
||||
|
||||
SceneViewportOrientationAxis HitTestSceneViewportOrientationGizmo(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportMax,
|
||||
const ImVec2& mousePosition) {
|
||||
if (!overlay.valid) {
|
||||
return SceneViewportOrientationAxis::None;
|
||||
}
|
||||
|
||||
const ImVec2 center = BuildWidgetCenter(viewportMin, viewportMax);
|
||||
std::vector<AxisHandleVisual> handles = BuildSortedAxisHandles(overlay);
|
||||
std::sort(
|
||||
handles.begin(),
|
||||
handles.end(),
|
||||
[](const AxisHandleVisual& lhs, const AxisHandleVisual& rhs) {
|
||||
return lhs.sortDepth < rhs.sortDepth;
|
||||
});
|
||||
|
||||
for (const AxisHandleVisual& handle : handles) {
|
||||
if (HitTestAxisHandle(mousePosition, center, handle)) {
|
||||
return handle.axis;
|
||||
}
|
||||
}
|
||||
|
||||
return SceneViewportOrientationAxis::None;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -13,5 +13,11 @@ void DrawSceneViewportOrientationGizmo(
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportMax);
|
||||
|
||||
SceneViewportOrientationAxis HitTestSceneViewportOrientationGizmo(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportMax,
|
||||
const ImVec2& mousePosition);
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
#include "SceneViewportOrientationGizmo.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kMoveGizmoArrowLength = 14.0f;
|
||||
constexpr float kMoveGizmoArrowHalfWidth = 7.0f;
|
||||
|
||||
ImU32 ToImGuiColor(const Math::Color& color) {
|
||||
const auto toChannel = [](float value) -> int {
|
||||
return static_cast<int>(std::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
@@ -20,6 +24,69 @@ ImU32 ToImGuiColor(const Math::Color& color) {
|
||||
toChannel(color.a));
|
||||
}
|
||||
|
||||
ImVec2 NormalizeImVec2(const ImVec2& value, const ImVec2& fallback = ImVec2(1.0f, 0.0f)) {
|
||||
const float lengthSq = value.x * value.x + value.y * value.y;
|
||||
if (lengthSq <= 1e-6f) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const float inverseLength = 1.0f / std::sqrt(lengthSq);
|
||||
return ImVec2(value.x * inverseLength, value.y * inverseLength);
|
||||
}
|
||||
|
||||
void DrawSceneMoveGizmoPlane(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportMoveGizmoPlaneDrawData& plane) {
|
||||
if (drawList == nullptr || !plane.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImVec2 points[4] = {};
|
||||
for (size_t index = 0; index < plane.corners.size(); ++index) {
|
||||
points[index] = ImVec2(
|
||||
viewportMin.x + plane.corners[index].x,
|
||||
viewportMin.y + plane.corners[index].y);
|
||||
}
|
||||
|
||||
drawList->AddConvexPolyFilled(points, 4, ToImGuiColor(plane.fillColor));
|
||||
drawList->AddPolyline(
|
||||
points,
|
||||
4,
|
||||
ToImGuiColor(plane.outlineColor),
|
||||
true,
|
||||
plane.active ? 2.6f : (plane.hovered ? 2.0f : 1.4f));
|
||||
}
|
||||
|
||||
void DrawSceneMoveGizmoAxis(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportMoveGizmoHandleDrawData& handle) {
|
||||
if (drawList == nullptr || !handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImU32 color = ToImGuiColor(handle.color);
|
||||
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f);
|
||||
const ImVec2 start(viewportMin.x + handle.start.x, viewportMin.y + handle.start.y);
|
||||
const ImVec2 end(viewportMin.x + handle.end.x, viewportMin.y + handle.end.y);
|
||||
const ImVec2 direction = NormalizeImVec2(ImVec2(end.x - start.x, end.y - start.y));
|
||||
const ImVec2 normal(-direction.y, direction.x);
|
||||
const ImVec2 arrowBase(
|
||||
end.x - direction.x * kMoveGizmoArrowLength,
|
||||
end.y - direction.y * kMoveGizmoArrowLength);
|
||||
const ImVec2 arrowLeft(
|
||||
arrowBase.x + normal.x * kMoveGizmoArrowHalfWidth,
|
||||
arrowBase.y + normal.y * kMoveGizmoArrowHalfWidth);
|
||||
const ImVec2 arrowRight(
|
||||
arrowBase.x - normal.x * kMoveGizmoArrowHalfWidth,
|
||||
arrowBase.y - normal.y * kMoveGizmoArrowHalfWidth);
|
||||
|
||||
drawList->AddLine(start, arrowBase, color, thickness);
|
||||
const ImVec2 triangle[3] = { end, arrowLeft, arrowRight };
|
||||
drawList->AddConvexPolyFilled(triangle, 3, color);
|
||||
}
|
||||
|
||||
void DrawSceneMoveGizmo(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
@@ -29,21 +96,16 @@ void DrawSceneMoveGizmo(
|
||||
}
|
||||
|
||||
const ImVec2 pivot(viewportMin.x + moveGizmo.pivot.x, viewportMin.y + moveGizmo.pivot.y);
|
||||
drawList->AddCircleFilled(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(20, 22, 24, 220), 20);
|
||||
drawList->AddCircle(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(255, 255, 255, 48), 20, 1.0f);
|
||||
for (const SceneViewportMoveGizmoPlaneDrawData& plane : moveGizmo.planes) {
|
||||
DrawSceneMoveGizmoPlane(drawList, viewportMin, plane);
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoHandleDrawData& handle : moveGizmo.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ImU32 color = ToImGuiColor(handle.color);
|
||||
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f);
|
||||
const ImVec2 start(viewportMin.x + handle.start.x, viewportMin.y + handle.start.y);
|
||||
const ImVec2 end(viewportMin.x + handle.end.x, viewportMin.y + handle.end.y);
|
||||
drawList->AddLine(start, end, color, thickness);
|
||||
drawList->AddCircleFilled(end, handle.active ? 6.5f : 5.5f, color, 20);
|
||||
DrawSceneMoveGizmoAxis(drawList, viewportMin, handle);
|
||||
}
|
||||
|
||||
drawList->AddCircleFilled(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(20, 22, 24, 220), 20);
|
||||
drawList->AddCircle(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(255, 255, 255, 48), 20, 1.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -102,7 +102,9 @@ bool SceneViewportSelectionMaskPass::Render(
|
||||
const Rendering::RenderContext& renderContext,
|
||||
const Rendering::RenderSurface& surface,
|
||||
const Rendering::RenderCameraData& cameraData,
|
||||
const std::vector<Rendering::VisibleRenderItem>& renderables) {
|
||||
const std::vector<Rendering::VisibleRenderItem>& renderables,
|
||||
bool debugColor) {
|
||||
(void)debugColor;
|
||||
if (!renderContext.IsValid() ||
|
||||
renderContext.backendType != RHI::RHIType::D3D12 ||
|
||||
renderables.empty()) {
|
||||
|
||||
@@ -29,7 +29,8 @@ public:
|
||||
const Rendering::RenderContext& renderContext,
|
||||
const Rendering::RenderSurface& surface,
|
||||
const Rendering::RenderCameraData& cameraData,
|
||||
const std::vector<Rendering::VisibleRenderItem>& renderables);
|
||||
const std::vector<Rendering::VisibleRenderItem>& renderables,
|
||||
bool debugColor = false);
|
||||
|
||||
private:
|
||||
struct OwnedDescriptorSet {
|
||||
|
||||
@@ -10,7 +10,9 @@ namespace Editor {
|
||||
GameViewPanel::GameViewPanel() : Panel("Game") {}
|
||||
|
||||
void GameViewPanel::Render() {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
UI::PanelWindowScope panel(m_name.c_str());
|
||||
ImGui::PopStyleVar();
|
||||
if (!panel.IsOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ XCEngine::Editor::UI::TreeNodeDefinition BuildHierarchyNodeDefinition(
|
||||
XCEngine::Editor::UI::TreeNodeDefinition nodeDefinition;
|
||||
nodeDefinition.options.selected = context.GetSelectionManager().IsSelected(gameObject->GetID());
|
||||
nodeDefinition.options.leaf = gameObject->GetChildCount() == 0;
|
||||
nodeDefinition.options.openOnDoubleClick = false;
|
||||
nodeDefinition.style = XCEngine::Editor::UI::HierarchyTreeStyle();
|
||||
nodeDefinition.prefix.width = XCEngine::Editor::UI::NavigationTreePrefixWidth();
|
||||
nodeDefinition.prefix.draw = DrawHierarchyTreePrefix;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "Core/ISelectionManager.h"
|
||||
#include "SceneViewPanel.h"
|
||||
#include "Viewport/SceneViewportOrientationGizmo.h"
|
||||
#include "Viewport/SceneViewportOverlayRenderer.h"
|
||||
#include "ViewportPanelContent.h"
|
||||
#include "UI/UI.h"
|
||||
@@ -75,24 +76,48 @@ void SceneViewPanel::Render() {
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
m_moveGizmo.IsHoveringHandle();
|
||||
const SceneViewportOrientationAxis orientationAxisHit =
|
||||
hasInteractiveViewport &&
|
||||
content.hovered &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
!m_moveGizmo.IsHoveringHandle() &&
|
||||
!m_moveGizmo.IsActive()
|
||||
? HitTestSceneViewportOrientationGizmo(
|
||||
overlay,
|
||||
content.itemMin,
|
||||
content.itemMax,
|
||||
io.MousePos)
|
||||
: SceneViewportOrientationAxis::None;
|
||||
const bool orientationGizmoClick =
|
||||
hasInteractiveViewport &&
|
||||
content.hovered &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
orientationAxisHit != SceneViewportOrientationAxis::None;
|
||||
const bool selectClick =
|
||||
hasInteractiveViewport &&
|
||||
content.hovered &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
!orientationGizmoClick &&
|
||||
!m_moveGizmo.IsHoveringHandle() &&
|
||||
!m_moveGizmo.IsActive();
|
||||
const bool beginLookDrag =
|
||||
hasInteractiveViewport &&
|
||||
content.hovered &&
|
||||
!m_lookDragging &&
|
||||
!m_moveGizmo.IsActive() &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Right);
|
||||
const bool beginPanDrag =
|
||||
hasInteractiveViewport &&
|
||||
content.hovered &&
|
||||
!m_panDragging &&
|
||||
!m_moveGizmo.IsActive() &&
|
||||
!m_lookDragging &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Middle);
|
||||
|
||||
if (beginMoveGizmo || selectClick || beginLookDrag || beginPanDrag) {
|
||||
if (beginMoveGizmo || orientationGizmoClick || selectClick || beginLookDrag || beginPanDrag) {
|
||||
ImGui::SetWindowFocus();
|
||||
}
|
||||
|
||||
@@ -100,6 +125,13 @@ void SceneViewPanel::Render() {
|
||||
m_moveGizmo.TryBeginDrag(moveGizmoContext, m_context->GetUndoManager());
|
||||
}
|
||||
|
||||
if (orientationGizmoClick) {
|
||||
viewportHostService->AlignSceneViewToOrientationAxis(orientationAxisHit);
|
||||
overlay = viewportHostService->GetSceneViewOverlayData();
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
m_moveGizmo.Update(moveGizmoContext);
|
||||
}
|
||||
|
||||
if (selectClick) {
|
||||
const ImVec2 localMousePosition(
|
||||
io.MousePos.x - content.itemMin.x,
|
||||
@@ -125,11 +157,15 @@ void SceneViewPanel::Render() {
|
||||
|
||||
if (beginLookDrag) {
|
||||
m_lookDragging = true;
|
||||
m_panDragging = false;
|
||||
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
|
||||
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
if (beginPanDrag) {
|
||||
m_panDragging = true;
|
||||
m_lookDragging = false;
|
||||
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
|
||||
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
|
||||
if (m_lookDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Viewport/IViewportHostService.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
@@ -18,6 +19,9 @@ struct ViewportPanelContentResult {
|
||||
bool hasViewportArea = false;
|
||||
bool hovered = false;
|
||||
bool focused = false;
|
||||
bool clickedLeft = false;
|
||||
bool clickedRight = false;
|
||||
bool clickedMiddle = false;
|
||||
};
|
||||
|
||||
inline void DrawViewportStatusMessage(const std::string& message) {
|
||||
@@ -40,8 +44,39 @@ inline void DrawViewportStatusMessage(const std::string& message) {
|
||||
drawList->AddText(textPos, ImGui::GetColorU32(ImGuiCol_TextDisabled), message.c_str());
|
||||
}
|
||||
|
||||
inline const char* GetViewportInteractionSurfaceId(EditorViewportKind kind) {
|
||||
switch (kind) {
|
||||
case EditorViewportKind::Scene:
|
||||
return "##SceneViewportInteractionSurface";
|
||||
case EditorViewportKind::Game:
|
||||
return "##GameViewportInteractionSurface";
|
||||
default:
|
||||
return "##ViewportInteractionSurface";
|
||||
}
|
||||
}
|
||||
|
||||
inline void RenderViewportInteractionSurface(
|
||||
ViewportPanelContentResult& result,
|
||||
EditorViewportKind kind,
|
||||
const ImVec2& interactionSize) {
|
||||
ImGui::InvisibleButton(
|
||||
GetViewportInteractionSurfaceId(kind),
|
||||
interactionSize,
|
||||
ImGuiButtonFlags_MouseButtonLeft |
|
||||
ImGuiButtonFlags_MouseButtonRight |
|
||||
ImGuiButtonFlags_MouseButtonMiddle);
|
||||
|
||||
result.itemMin = ImGui::GetItemRectMin();
|
||||
result.itemMax = ImGui::GetItemRectMax();
|
||||
result.hovered = ImGui::IsItemHovered();
|
||||
result.clickedLeft = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||
result.clickedRight = ImGui::IsItemClicked(ImGuiMouseButton_Right);
|
||||
result.clickedMiddle = ImGui::IsItemClicked(ImGuiMouseButton_Middle);
|
||||
}
|
||||
|
||||
inline ViewportPanelContentResult RenderViewportPanelContent(IEditorContext& context, EditorViewportKind kind) {
|
||||
ViewportPanelContentResult result = {};
|
||||
UI::CollapsePanelSectionSpacing();
|
||||
result.availableSize = ImGui::GetContentRegionAvail();
|
||||
result.focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
|
||||
|
||||
@@ -54,24 +89,19 @@ inline ViewportPanelContentResult RenderViewportPanelContent(IEditorContext& con
|
||||
result.hasViewportArea = true;
|
||||
|
||||
if (viewportHostService == nullptr) {
|
||||
ImGui::Dummy(result.availableSize);
|
||||
result.itemMin = ImGui::GetItemRectMin();
|
||||
result.itemMax = ImGui::GetItemRectMax();
|
||||
result.hovered = ImGui::IsMouseHoveringRect(result.itemMin, result.itemMax, true);
|
||||
RenderViewportInteractionSurface(result, kind, result.availableSize);
|
||||
DrawViewportStatusMessage("Viewport host is unavailable");
|
||||
return result;
|
||||
}
|
||||
|
||||
result.frame = viewportHostService->RequestViewport(kind, result.availableSize);
|
||||
if (result.frame.hasTexture) {
|
||||
ImGui::Image(result.frame.textureId, result.availableSize);
|
||||
} else {
|
||||
ImGui::Dummy(result.availableSize);
|
||||
}
|
||||
RenderViewportInteractionSurface(result, kind, result.availableSize);
|
||||
|
||||
result.itemMin = ImGui::GetItemRectMin();
|
||||
result.itemMax = ImGui::GetItemRectMax();
|
||||
result.hovered = ImGui::IsMouseHoveringRect(result.itemMin, result.itemMax, true);
|
||||
if (result.frame.hasTexture) {
|
||||
if (ImDrawList* drawList = ImGui::GetWindowDrawList()) {
|
||||
drawList->AddImage(result.frame.textureId, result.itemMin, result.itemMax);
|
||||
}
|
||||
}
|
||||
|
||||
DrawViewportStatusMessage(
|
||||
result.frame.statusText.empty() && !result.frame.hasTexture
|
||||
|
||||
Reference in New Issue
Block a user