Add gizmo drag interaction and hover highlighting
- Click+drag on gizmo arrows to translate selected actors along axis - Drag plane computation based on axis direction and camera orientation - Axis-constrained movement with initial transform preservation - Hover highlighting: gizmo arrows turn yellow on mouse-over - Sphere sweep for easier gizmo clicking (generous hit radius) - UE editor-style gizmo colors (dark red/green/blue) - Fixed gizmo visibility toggle (component-level instead of actor-level) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
|
||||
APS_Editor_Pawn::APS_Editor_Pawn()
|
||||
@@ -44,6 +45,12 @@ void APS_Editor_Pawn::Tick(float DeltaTime)
|
||||
bAltHeld = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt);
|
||||
bCtrlHeld = PC->IsInputKeyDown(EKeys::LeftControl) || PC->IsInputKeyDown(EKeys::RightControl);
|
||||
|
||||
// Gizmo hover highlighting in Idle mode
|
||||
if (CameraMode == EPS_Editor_CameraMode::Idle)
|
||||
{
|
||||
UpdateGizmoHover();
|
||||
}
|
||||
|
||||
// Draw debug bounding box around selected actors
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||
{
|
||||
@@ -264,6 +271,45 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
SetActorLocation(OrbitFocalPoint + Offset);
|
||||
SetActorRotation((OrbitFocalPoint - GetActorLocation()).Rotation());
|
||||
}
|
||||
else if (CameraMode == EPS_Editor_CameraMode::GizmoDrag)
|
||||
{
|
||||
// Intersect mouse ray with drag plane, constrain delta to axis, apply to actors
|
||||
FVector RayOrigin, RayDir;
|
||||
if (!GetMouseWorldRay(RayOrigin, RayDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FVector CurrentIntersection;
|
||||
if (!RayPlaneIntersection(RayOrigin, RayDir, GizmoDragPlaneOrigin, GizmoDragPlaneNormal, CurrentIntersection))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||
if (!EditorPC) return;
|
||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||
if (!SM) return;
|
||||
APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
|
||||
if (!Gizmo) return;
|
||||
|
||||
const FVector AxisDir = Gizmo->GetAxisDirection(ActiveGizmoAxis);
|
||||
const FVector TotalDelta = CurrentIntersection - GizmoDragInitialIntersection;
|
||||
const FVector ConstrainedDelta = FVector::DotProduct(TotalDelta, AxisDir) * AxisDir;
|
||||
|
||||
// Apply delta to all selected actors from their initial transforms
|
||||
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||
{
|
||||
if (AActor* Actor = Selected[i].Get())
|
||||
{
|
||||
Actor->SetActorLocation(GizmoDragInitialTransforms[i].GetLocation() + ConstrainedDelta);
|
||||
}
|
||||
}
|
||||
|
||||
// Update gizmo position
|
||||
Gizmo->SetActorLocation(SM->GetSelectionPivot());
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value)
|
||||
@@ -293,16 +339,35 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
||||
{
|
||||
if (bAltHeld)
|
||||
{
|
||||
// Alt + left-click = orbit immediately (no pending)
|
||||
ComputeOrbitFocalPoint();
|
||||
SetCameraMode(EPS_Editor_CameraMode::Orbit);
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
// Check if clicking on a gizmo handle
|
||||
FVector RayOrigin, RayDir;
|
||||
if (GetMouseWorldRay(RayOrigin, RayDir))
|
||||
{
|
||||
// Enter PendingClick: wait to see if this is a click or a drag
|
||||
PendingClickScreenPos = GetMouseScreenPosition();
|
||||
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||
{
|
||||
if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo())
|
||||
{
|
||||
EPS_Editor_GizmoAxis Axis = Gizmo->GetAxisUnderCursor(RayOrigin, RayDir);
|
||||
if (Axis != EPS_Editor_GizmoAxis::None)
|
||||
{
|
||||
BeginGizmoDrag(Axis);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No gizmo hit: enter PendingClick
|
||||
PendingClickScreenPos = GetMouseScreenPosition();
|
||||
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
||||
@@ -419,3 +484,106 @@ FVector2D APS_Editor_Pawn::GetMouseScreenPosition() const
|
||||
PC->GetMousePosition(MouseX, MouseY);
|
||||
return FVector2D(MouseX, MouseY);
|
||||
}
|
||||
|
||||
bool APS_Editor_Pawn::GetMouseWorldRay(FVector& OutOrigin, FVector& OutDirection) const
|
||||
{
|
||||
APlayerController* PC = Cast<APlayerController>(GetController());
|
||||
if (!PC)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FVector2D ScreenPos = GetMouseScreenPosition();
|
||||
return PC->DeprojectScreenPositionToWorld(ScreenPos.X, ScreenPos.Y, OutOrigin, OutDirection);
|
||||
}
|
||||
|
||||
bool APS_Editor_Pawn::RayPlaneIntersection(const FVector& RayOrigin, const FVector& RayDir,
|
||||
const FVector& PlaneOrigin, const FVector& PlaneNormal, FVector& OutIntersection) const
|
||||
{
|
||||
const float Denom = FVector::DotProduct(RayDir, PlaneNormal);
|
||||
if (FMath::Abs(Denom) < KINDA_SMALL_NUMBER)
|
||||
{
|
||||
return false; // Ray is parallel to plane
|
||||
}
|
||||
|
||||
const float T = FVector::DotProduct(PlaneOrigin - RayOrigin, PlaneNormal) / Denom;
|
||||
if (T < 0.0f)
|
||||
{
|
||||
return false; // Intersection is behind the ray
|
||||
}
|
||||
|
||||
OutIntersection = RayOrigin + RayDir * T;
|
||||
return true;
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::UpdateGizmoHover()
|
||||
{
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||
if (!EditorPC) return;
|
||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||
if (!SM) return;
|
||||
APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
|
||||
if (!Gizmo) return;
|
||||
|
||||
FVector RayOrigin, RayDir;
|
||||
if (GetMouseWorldRay(RayOrigin, RayDir))
|
||||
{
|
||||
EPS_Editor_GizmoAxis Axis = Gizmo->GetAxisUnderCursor(RayOrigin, RayDir);
|
||||
Gizmo->SetHighlightedAxis(Axis);
|
||||
}
|
||||
else
|
||||
{
|
||||
Gizmo->SetHighlightedAxis(EPS_Editor_GizmoAxis::None);
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
|
||||
{
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||
if (!EditorPC) return;
|
||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||
if (!SM) return;
|
||||
APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
|
||||
if (!Gizmo) return;
|
||||
|
||||
ActiveGizmoAxis = Axis;
|
||||
|
||||
// Get the axis direction in world space
|
||||
const FVector AxisDir = Gizmo->GetAxisDirection(Axis);
|
||||
|
||||
// Compute the drag plane: contains the axis and faces the camera as much as possible
|
||||
const FVector CameraForward = GetActorForwardVector();
|
||||
FVector PlaneNormal = FVector::CrossProduct(AxisDir, CameraForward).GetSafeNormal();
|
||||
if (PlaneNormal.IsNearlyZero())
|
||||
{
|
||||
// Camera looking along the axis - use camera up instead
|
||||
PlaneNormal = FVector::CrossProduct(AxisDir, FVector::UpVector).GetSafeNormal();
|
||||
}
|
||||
PlaneNormal = FVector::CrossProduct(AxisDir, PlaneNormal).GetSafeNormal();
|
||||
|
||||
GizmoDragPlaneNormal = PlaneNormal;
|
||||
GizmoDragPlaneOrigin = Gizmo->GetActorLocation();
|
||||
|
||||
// Get initial mouse-plane intersection
|
||||
FVector RayOrigin, RayDir;
|
||||
if (GetMouseWorldRay(RayOrigin, RayDir))
|
||||
{
|
||||
RayPlaneIntersection(RayOrigin, RayDir, GizmoDragPlaneOrigin, GizmoDragPlaneNormal, GizmoDragInitialIntersection);
|
||||
}
|
||||
|
||||
// Store initial transforms of all selected actors
|
||||
GizmoDragInitialTransforms.Empty();
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
GizmoDragInitialTransforms.Add(Actor->GetActorTransform());
|
||||
}
|
||||
else
|
||||
{
|
||||
GizmoDragInitialTransforms.Add(FTransform::Identity);
|
||||
}
|
||||
}
|
||||
|
||||
SetCameraMode(EPS_Editor_CameraMode::GizmoDrag);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "InputActionValue.h"
|
||||
#include "PS_Editor_SelectionTypes.h"
|
||||
#include "PS_Editor_Pawn.generated.h"
|
||||
|
||||
class UCameraComponent;
|
||||
@@ -152,6 +153,13 @@ private:
|
||||
// ---- Click vs drag state ----
|
||||
FVector2D PendingClickScreenPos = FVector2D::ZeroVector;
|
||||
|
||||
// ---- Gizmo drag state ----
|
||||
EPS_Editor_GizmoAxis ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
|
||||
FVector GizmoDragPlaneNormal = FVector::ZeroVector;
|
||||
FVector GizmoDragPlaneOrigin = FVector::ZeroVector;
|
||||
FVector GizmoDragInitialIntersection = FVector::ZeroVector;
|
||||
TArray<FTransform> GizmoDragInitialTransforms;
|
||||
|
||||
// ---- Input Handlers ----
|
||||
void HandleMove(const FInputActionValue& Value);
|
||||
void HandleLook(const FInputActionValue& Value);
|
||||
@@ -169,6 +177,10 @@ private:
|
||||
void UpdateCursorVisibility();
|
||||
void ComputeOrbitFocalPoint();
|
||||
FVector2D GetMouseScreenPosition() const;
|
||||
bool GetMouseWorldRay(FVector& OutOrigin, FVector& OutDirection) const;
|
||||
bool RayPlaneIntersection(const FVector& RayOrigin, const FVector& RayDir, const FVector& PlaneOrigin, const FVector& PlaneNormal, FVector& OutIntersection) const;
|
||||
void UpdateGizmoHover();
|
||||
void BeginGizmoDrag(EPS_Editor_GizmoAxis Axis);
|
||||
|
||||
float OrbitPitch = 0.0f;
|
||||
float OrbitYaw = 0.0f;
|
||||
|
||||
@@ -53,9 +53,10 @@ void APS_Editor_Gizmo::BeginPlay()
|
||||
return MID;
|
||||
};
|
||||
|
||||
Mat_X = MakeColorMat(FLinearColor::Red);
|
||||
Mat_Y = MakeColorMat(FLinearColor::Green);
|
||||
Mat_Z = MakeColorMat(FLinearColor(0.0f, 0.0f, 1.0f));
|
||||
// UE editor gizmo colors
|
||||
Mat_X = MakeColorMat(FLinearColor(0.594f, 0.0197f, 0.0f)); // Dark red
|
||||
Mat_Y = MakeColorMat(FLinearColor(0.1255f, 0.372f, 0.0f)); // Dark green
|
||||
Mat_Z = MakeColorMat(FLinearColor(0.0392f, 0.0784f, 0.594f)); // Dark blue
|
||||
Mat_Highlight = MakeColorMat(FLinearColor::Yellow);
|
||||
|
||||
// Apply materials to shafts and cones
|
||||
@@ -182,13 +183,18 @@ EPS_Editor_GizmoAxis APS_Editor_Gizmo::GetAxisUnderCursor(const FVector& RayOrig
|
||||
float BestDistance = TNumericLimits<float>::Max();
|
||||
EPS_Editor_GizmoAxis BestAxis = EPS_Editor_GizmoAxis::None;
|
||||
|
||||
// Use a sphere sweep with a generous radius for easier clicking
|
||||
const float SweepRadius = 8.0f * GetActorScale3D().X;
|
||||
|
||||
auto TestComponent = [&](UStaticMeshComponent* Comp, EPS_Editor_GizmoAxis Axis)
|
||||
{
|
||||
if (!Comp || !Comp->IsVisible()) return;
|
||||
if (!Comp) return;
|
||||
|
||||
FHitResult Hit;
|
||||
const FVector RayEnd = RayOrigin + RayDirection * 100000.0f;
|
||||
if (Comp->LineTraceComponent(Hit, RayOrigin, RayEnd, FCollisionQueryParams::DefaultQueryParam))
|
||||
const FCollisionShape Sphere = FCollisionShape::MakeSphere(SweepRadius);
|
||||
|
||||
if (Comp->SweepComponent(Hit, RayOrigin, RayEnd, FQuat::Identity, Sphere, false))
|
||||
{
|
||||
if (Hit.Distance < BestDistance)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user