Add Phase 2 selection system and translate gizmo
- Click-to-select with raycast (Movable actors only) - Ctrl+click for multi-select, click empty to deselect - Click vs drag detection (pixel threshold) to avoid accidental selection - SelectionManager owned by PlayerController with OnSelectionChanged delegate - Custom depth stencil toggling on selected actors - Debug bounding box visualization for selection - Custom translate gizmo actor with colored arrows (R/G/B) and cone tips - Gizmo renders on top of scene (SDPG_Foreground + Translucent depth test disabled) - Constant screen-size gizmo scaling based on camera distance - Unlit gizmo material with Color vector parameter (M_PS_Editor_Gizmo) - r.CustomDepth=3 enabled for future outline post-process Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ r.ReflectionMethod=1
|
|||||||
|
|
||||||
r.SkinCache.CompileShaders=True
|
r.SkinCache.CompileShaders=True
|
||||||
|
|
||||||
|
r.CustomDepth=3
|
||||||
|
|
||||||
r.RayTracing=True
|
r.RayTracing=True
|
||||||
|
|
||||||
r.Shadow.Virtual.Enable=1
|
r.Shadow.Virtual.Enable=1
|
||||||
|
|||||||
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset
Normal file
Binary file not shown.
@@ -8,6 +8,9 @@
|
|||||||
#include "InputModifiers.h"
|
#include "InputModifiers.h"
|
||||||
#include "InputTriggers.h"
|
#include "InputTriggers.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "PS_Editor_SelectionManager.h"
|
||||||
|
#include "PS_Editor_PlayerController.h"
|
||||||
|
#include "DrawDebugHelpers.h"
|
||||||
|
|
||||||
APS_Editor_Pawn::APS_Editor_Pawn()
|
APS_Editor_Pawn::APS_Editor_Pawn()
|
||||||
{
|
{
|
||||||
@@ -36,17 +39,32 @@ void APS_Editor_Pawn::Tick(float DeltaTime)
|
|||||||
{
|
{
|
||||||
Super::Tick(DeltaTime);
|
Super::Tick(DeltaTime);
|
||||||
|
|
||||||
// Track Alt key state every frame
|
|
||||||
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
||||||
{
|
{
|
||||||
bAltHeld = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt);
|
bAltHeld = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt);
|
||||||
|
bCtrlHeld = PC->IsInputKeyDown(EKeys::LeftControl) || PC->IsInputKeyDown(EKeys::RightControl);
|
||||||
|
|
||||||
|
// Draw debug bounding box around selected actors
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
FVector Origin, Extent;
|
||||||
|
Actor->GetActorBounds(false, Origin, Extent);
|
||||||
|
DrawDebugBox(GetWorld(), Origin, Extent, FColor::Orange, false, -1.0f, 0, 2.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::CreateInputActions()
|
void APS_Editor_Pawn::CreateInputActions()
|
||||||
{
|
{
|
||||||
// --- Create Input Actions ---
|
|
||||||
|
|
||||||
IA_Move = NewObject<UInputAction>(this, TEXT("IA_Move"));
|
IA_Move = NewObject<UInputAction>(this, TEXT("IA_Move"));
|
||||||
IA_Move->ValueType = EInputActionValueType::Axis3D;
|
IA_Move->ValueType = EInputActionValueType::Axis3D;
|
||||||
IA_Move->bConsumeInput = false;
|
IA_Move->bConsumeInput = false;
|
||||||
@@ -66,7 +84,6 @@ void APS_Editor_Pawn::CreateInputActions()
|
|||||||
IA_MiddleClick = NewObject<UInputAction>(this, TEXT("IA_MiddleClick"));
|
IA_MiddleClick = NewObject<UInputAction>(this, TEXT("IA_MiddleClick"));
|
||||||
IA_MiddleClick->ValueType = EInputActionValueType::Boolean;
|
IA_MiddleClick->ValueType = EInputActionValueType::Boolean;
|
||||||
|
|
||||||
// --- Create Mapping Context ---
|
|
||||||
EditorMappingContext = NewObject<UInputMappingContext>(this, TEXT("EditorMappingContext"));
|
EditorMappingContext = NewObject<UInputMappingContext>(this, TEXT("EditorMappingContext"));
|
||||||
|
|
||||||
// Movement helpers
|
// Movement helpers
|
||||||
@@ -191,22 +208,42 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
|||||||
|
|
||||||
const FVector2D LookInput = Value.Get<FVector2D>();
|
const FVector2D LookInput = Value.Get<FVector2D>();
|
||||||
|
|
||||||
|
if (CameraMode == EPS_Editor_CameraMode::PendingClick)
|
||||||
|
{
|
||||||
|
// Check if mouse moved enough to become a drag
|
||||||
|
const FVector2D CurrentPos = GetMouseScreenPosition();
|
||||||
|
const float PixelDistance = FVector2D::Distance(PendingClickScreenPos, CurrentPos);
|
||||||
|
|
||||||
|
if (PixelDistance > ClickDragThreshold)
|
||||||
|
{
|
||||||
|
// Exceeded threshold: resolve to Pan or Orbit
|
||||||
|
if (bAltHeld)
|
||||||
|
{
|
||||||
|
ComputeOrbitFocalPoint();
|
||||||
|
SetCameraMode(EPS_Editor_CameraMode::Orbit);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetCameraMode(EPS_Editor_CameraMode::Pan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If still under threshold, do nothing (wait for more movement or mouse up)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (CameraMode == EPS_Editor_CameraMode::Pan)
|
if (CameraMode == EPS_Editor_CameraMode::Pan)
|
||||||
{
|
{
|
||||||
// Left-click drag: mouse X = yaw rotation (Z axis only), mouse Y = move forward/back on horizontal plane
|
|
||||||
const float YawDelta = LookInput.X * PanYawSensitivity;
|
const float YawDelta = LookInput.X * PanYawSensitivity;
|
||||||
FRotator CurrentRotation = GetActorRotation();
|
FRotator CurrentRotation = GetActorRotation();
|
||||||
CurrentRotation.Yaw += YawDelta;
|
CurrentRotation.Yaw += YawDelta;
|
||||||
SetActorRotation(CurrentRotation);
|
SetActorRotation(CurrentRotation);
|
||||||
|
|
||||||
// Forward/back movement on horizontal plane
|
|
||||||
const FVector HorizontalForward = FVector(GetActorForwardVector().X, GetActorForwardVector().Y, 0.0f).GetSafeNormal();
|
const FVector HorizontalForward = FVector(GetActorForwardVector().X, GetActorForwardVector().Y, 0.0f).GetSafeNormal();
|
||||||
const FVector PanMovement = HorizontalForward * LookInput.Y * PanSpeed;
|
const FVector PanMovement = HorizontalForward * LookInput.Y * PanSpeed;
|
||||||
AddActorWorldOffset(PanMovement, true);
|
AddActorWorldOffset(PanMovement, true);
|
||||||
}
|
}
|
||||||
else if (CameraMode == EPS_Editor_CameraMode::Fly)
|
else if (CameraMode == EPS_Editor_CameraMode::Fly)
|
||||||
{
|
{
|
||||||
// Right-click drag: full free look
|
|
||||||
const float YawDelta = LookInput.X * LookSensitivity;
|
const float YawDelta = LookInput.X * LookSensitivity;
|
||||||
const float PitchDelta = LookInput.Y * LookSensitivity;
|
const float PitchDelta = LookInput.Y * LookSensitivity;
|
||||||
|
|
||||||
@@ -247,7 +284,6 @@ void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Idle or Pan: dolly forward/backward
|
|
||||||
const FVector Forward = GetActorForwardVector();
|
const FVector Forward = GetActorForwardVector();
|
||||||
AddActorWorldOffset(Forward * ScrollValue * OrbitZoomStep * 5.0f, true);
|
AddActorWorldOffset(Forward * ScrollValue * OrbitZoomStep * 5.0f, true);
|
||||||
}
|
}
|
||||||
@@ -257,20 +293,28 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
{
|
{
|
||||||
if (bAltHeld)
|
if (bAltHeld)
|
||||||
{
|
{
|
||||||
// Alt + left-click = orbit
|
// Alt + left-click = orbit immediately (no pending)
|
||||||
ComputeOrbitFocalPoint();
|
ComputeOrbitFocalPoint();
|
||||||
SetCameraMode(EPS_Editor_CameraMode::Orbit);
|
SetCameraMode(EPS_Editor_CameraMode::Orbit);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Left-click = pan (yaw + forward/back on horizontal plane)
|
// Enter PendingClick: wait to see if this is a click or a drag
|
||||||
SetCameraMode(EPS_Editor_CameraMode::Pan);
|
PendingClickScreenPos = GetMouseScreenPosition();
|
||||||
|
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
||||||
{
|
{
|
||||||
if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit)
|
if (CameraMode == EPS_Editor_CameraMode::PendingClick)
|
||||||
|
{
|
||||||
|
// Mouse released without exceeding drag threshold = this is a click (selection)
|
||||||
|
OnEditorClick.ExecuteIfBound(PendingClickScreenPos, bCtrlHeld);
|
||||||
|
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||||
|
}
|
||||||
|
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit
|
||||||
|
|| CameraMode == EPS_Editor_CameraMode::GizmoDrag)
|
||||||
{
|
{
|
||||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||||
}
|
}
|
||||||
@@ -327,7 +371,8 @@ void APS_Editor_Pawn::UpdateCursorVisibility()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool bShowCursor = (CameraMode == EPS_Editor_CameraMode::Idle);
|
const bool bShowCursor = (CameraMode == EPS_Editor_CameraMode::Idle
|
||||||
|
|| CameraMode == EPS_Editor_CameraMode::PendingClick);
|
||||||
PC->bShowMouseCursor = bShowCursor;
|
PC->bShowMouseCursor = bShowCursor;
|
||||||
|
|
||||||
if (bShowCursor)
|
if (bShowCursor)
|
||||||
@@ -360,3 +405,17 @@ void APS_Editor_Pawn::ComputeOrbitFocalPoint()
|
|||||||
OrbitFocalPoint = End;
|
OrbitFocalPoint = End;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FVector2D APS_Editor_Pawn::GetMouseScreenPosition() const
|
||||||
|
{
|
||||||
|
APlayerController* PC = Cast<APlayerController>(GetController());
|
||||||
|
if (!PC)
|
||||||
|
{
|
||||||
|
return FVector2D::ZeroVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
float MouseX = 0.0f;
|
||||||
|
float MouseY = 0.0f;
|
||||||
|
PC->GetMousePosition(MouseX, MouseY);
|
||||||
|
return FVector2D(MouseX, MouseY);
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class USpringArmComponent;
|
|||||||
class UInputAction;
|
class UInputAction;
|
||||||
class UInputMappingContext;
|
class UInputMappingContext;
|
||||||
|
|
||||||
|
DECLARE_DELEGATE_TwoParams(FPS_Editor_OnClick, FVector2D /*ScreenPos*/, bool /*bAdditive*/);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Camera modes for the editor pawn.
|
* Camera modes for the editor pawn.
|
||||||
*/
|
*/
|
||||||
@@ -18,19 +20,23 @@ enum class EPS_Editor_CameraMode : uint8
|
|||||||
{
|
{
|
||||||
/** Default mode - cursor visible, ZQSD/arrows still move. */
|
/** Default mode - cursor visible, ZQSD/arrows still move. */
|
||||||
Idle,
|
Idle,
|
||||||
/** Left-click held: mouse Y = forward/back on horizontal plane, mouse X = yaw rotation. */
|
/** Left mouse down, waiting to determine click vs drag (pixel threshold). */
|
||||||
|
PendingClick,
|
||||||
|
/** Left-click drag: mouse Y = forward/back on horizontal plane, mouse X = yaw rotation. */
|
||||||
Pan,
|
Pan,
|
||||||
/** Right-click held: full free look + ZQSD movement. */
|
/** Right-click held: full free look + ZQSD movement. */
|
||||||
Fly,
|
Fly,
|
||||||
/** Alt + left-click held: orbit around focal point. */
|
/** Alt + left-click or middle-click: orbit around focal point. */
|
||||||
Orbit
|
Orbit,
|
||||||
|
/** Dragging a gizmo handle. */
|
||||||
|
GizmoDrag
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor camera pawn mimicking UE viewport navigation.
|
* Editor camera pawn mimicking UE viewport navigation.
|
||||||
*
|
*
|
||||||
* Controls:
|
* Controls:
|
||||||
* - Left-click held: Pan (mouse Y = move forward/back, mouse X = yaw)
|
* - Left-click: Select object (or pan if dragged)
|
||||||
* - Right-click held: Fly (ZQSD + free mouse look)
|
* - Right-click held: Fly (ZQSD + free mouse look)
|
||||||
* - Alt + left-click held: Orbit around focal point
|
* - Alt + left-click held: Orbit around focal point
|
||||||
* - Middle-click held: Orbit around focal point (alternative)
|
* - Middle-click held: Orbit around focal point (alternative)
|
||||||
@@ -49,6 +55,9 @@ public:
|
|||||||
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
|
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
|
||||||
virtual void Tick(float DeltaTime) override;
|
virtual void Tick(float DeltaTime) override;
|
||||||
|
|
||||||
|
/** Fired when a left-click is confirmed (mouse up without exceeding drag threshold). */
|
||||||
|
FPS_Editor_OnClick OnEditorClick;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
@@ -65,60 +74,51 @@ protected:
|
|||||||
|
|
||||||
// ---- Camera Settings ----
|
// ---- Camera Settings ----
|
||||||
|
|
||||||
/** Base movement speed in fly mode (cm/s). */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||||
float FlySpeed = 1000.0f;
|
float FlySpeed = 1000.0f;
|
||||||
|
|
||||||
/** Minimum fly speed. */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||||
float FlySpeedMin = 100.0f;
|
float FlySpeedMin = 100.0f;
|
||||||
|
|
||||||
/** Maximum fly speed. */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||||
float FlySpeedMax = 10000.0f;
|
float FlySpeedMax = 10000.0f;
|
||||||
|
|
||||||
/** Mouse look sensitivity. */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||||
float LookSensitivity = 2.5f;
|
float LookSensitivity = 2.5f;
|
||||||
|
|
||||||
/** Speed change per scroll tick. */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||||
float FlySpeedScrollStep = 200.0f;
|
float FlySpeedScrollStep = 200.0f;
|
||||||
|
|
||||||
/** Pan speed multiplier (left-click drag forward/back). */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Pan")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Pan")
|
||||||
float PanSpeed = 5.0f;
|
float PanSpeed = 5.0f;
|
||||||
|
|
||||||
/** Pan yaw sensitivity (left-click drag left/right). */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Pan")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Pan")
|
||||||
float PanYawSensitivity = 1.0f;
|
float PanYawSensitivity = 1.0f;
|
||||||
|
|
||||||
/** Orbit distance from focal point. */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||||
float OrbitDistance = 500.0f;
|
float OrbitDistance = 500.0f;
|
||||||
|
|
||||||
/** Orbit zoom step per scroll tick. */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||||
float OrbitZoomStep = 50.0f;
|
float OrbitZoomStep = 50.0f;
|
||||||
|
|
||||||
/** Minimum orbit distance. */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||||
float OrbitDistanceMin = 50.0f;
|
float OrbitDistanceMin = 50.0f;
|
||||||
|
|
||||||
/** Maximum orbit distance. */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||||
float OrbitDistanceMax = 5000.0f;
|
float OrbitDistanceMax = 5000.0f;
|
||||||
|
|
||||||
/** Orbit rotation sensitivity. */
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||||
float OrbitSensitivity = 1.0f;
|
float OrbitSensitivity = 1.0f;
|
||||||
|
|
||||||
|
/** Pixel distance threshold to distinguish click from drag. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Selection")
|
||||||
|
float ClickDragThreshold = 8.0f;
|
||||||
|
|
||||||
// ---- State ----
|
// ---- State ----
|
||||||
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||||
EPS_Editor_CameraMode CameraMode = EPS_Editor_CameraMode::Idle;
|
EPS_Editor_CameraMode CameraMode = EPS_Editor_CameraMode::Idle;
|
||||||
|
|
||||||
/** The point in world space the orbit camera rotates around. */
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera|Orbit")
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera|Orbit")
|
||||||
FVector OrbitFocalPoint = FVector::ZeroVector;
|
FVector OrbitFocalPoint = FVector::ZeroVector;
|
||||||
|
|
||||||
@@ -145,8 +145,12 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UInputAction> IA_MiddleClick;
|
TObjectPtr<UInputAction> IA_MiddleClick;
|
||||||
|
|
||||||
/** Is the Alt key currently held down. */
|
// ---- Modifier key state ----
|
||||||
bool bAltHeld = false;
|
bool bAltHeld = false;
|
||||||
|
bool bCtrlHeld = false;
|
||||||
|
|
||||||
|
// ---- Click vs drag state ----
|
||||||
|
FVector2D PendingClickScreenPos = FVector2D::ZeroVector;
|
||||||
|
|
||||||
// ---- Input Handlers ----
|
// ---- Input Handlers ----
|
||||||
void HandleMove(const FInputActionValue& Value);
|
void HandleMove(const FInputActionValue& Value);
|
||||||
@@ -164,9 +168,8 @@ private:
|
|||||||
void SetCameraMode(EPS_Editor_CameraMode NewMode);
|
void SetCameraMode(EPS_Editor_CameraMode NewMode);
|
||||||
void UpdateCursorVisibility();
|
void UpdateCursorVisibility();
|
||||||
void ComputeOrbitFocalPoint();
|
void ComputeOrbitFocalPoint();
|
||||||
|
FVector2D GetMouseScreenPosition() const;
|
||||||
|
|
||||||
/** Current pitch accumulated during orbit (clamped). */
|
|
||||||
float OrbitPitch = 0.0f;
|
float OrbitPitch = 0.0f;
|
||||||
/** Current yaw accumulated during orbit. */
|
|
||||||
float OrbitYaw = 0.0f;
|
float OrbitYaw = 0.0f;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
|
#include "PS_Editor_SelectionManager.h"
|
||||||
|
#include "PS_Editor_Pawn.h"
|
||||||
|
|
||||||
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
||||||
{
|
{
|
||||||
@@ -10,6 +12,28 @@ void APS_Editor_PlayerController::BeginPlay()
|
|||||||
{
|
{
|
||||||
Super::BeginPlay();
|
Super::BeginPlay();
|
||||||
|
|
||||||
// Start in Game+UI mode so cursor is visible and clicks go to both UI and game
|
|
||||||
SetInputMode(FInputModeGameAndUI().SetHideCursorDuringCapture(false));
|
SetInputMode(FInputModeGameAndUI().SetHideCursorDuringCapture(false));
|
||||||
|
|
||||||
|
// Create selection manager
|
||||||
|
SelectionManager = NewObject<UPS_Editor_SelectionManager>(this);
|
||||||
|
SelectionManager->Initialize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
|
||||||
|
{
|
||||||
|
Super::OnPossess(InPawn);
|
||||||
|
|
||||||
|
// Bind pawn's click delegate to the selection manager
|
||||||
|
if (APS_Editor_Pawn* EditorPawn = Cast<APS_Editor_Pawn>(InPawn))
|
||||||
|
{
|
||||||
|
EditorPawn->OnEditorClick.BindUObject(this, &APS_Editor_PlayerController::HandleEditorClick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)
|
||||||
|
{
|
||||||
|
if (SelectionManager)
|
||||||
|
{
|
||||||
|
SelectionManager->HandleClickAtScreenPosition(ScreenPos, bAdditive);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "PS_Editor_PlayerController.generated.h"
|
#include "PS_Editor_PlayerController.generated.h"
|
||||||
|
|
||||||
|
class UPS_Editor_SelectionManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player controller for PS_Editor runtime editing.
|
* Player controller for PS_Editor runtime editing.
|
||||||
* Shows mouse cursor by default and handles input mode switching.
|
* Owns the SelectionManager and wires it to the Pawn's click delegate.
|
||||||
*/
|
*/
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class PS_EDITOR_API APS_Editor_PlayerController : public APlayerController
|
class PS_EDITOR_API APS_Editor_PlayerController : public APlayerController
|
||||||
@@ -16,6 +18,16 @@ class PS_EDITOR_API APS_Editor_PlayerController : public APlayerController
|
|||||||
public:
|
public:
|
||||||
APS_Editor_PlayerController();
|
APS_Editor_PlayerController();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
|
UPS_Editor_SelectionManager* GetSelectionManager() const { return SelectionManager; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
virtual void OnPossess(APawn* InPawn) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
|
||||||
|
|
||||||
|
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
#include "PS_Editor_Gizmo.h"
|
||||||
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
#include "Engine/StaticMesh.h"
|
||||||
|
#include "Materials/MaterialInstanceDynamic.h"
|
||||||
|
#include "UObject/ConstructorHelpers.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
|
|
||||||
|
APS_Editor_Gizmo::APS_Editor_Gizmo()
|
||||||
|
{
|
||||||
|
PrimaryActorTick.bCanEverTick = true;
|
||||||
|
|
||||||
|
GizmoRoot = CreateDefaultSubobject<USceneComponent>(TEXT("GizmoRoot"));
|
||||||
|
SetRootComponent(GizmoRoot);
|
||||||
|
|
||||||
|
TranslateRoot = CreateDefaultSubobject<USceneComponent>(TEXT("TranslateRoot"));
|
||||||
|
TranslateRoot->SetupAttachment(GizmoRoot);
|
||||||
|
|
||||||
|
const FRotator RotToX = FRotationMatrix::MakeFromZ(FVector::ForwardVector).Rotator();
|
||||||
|
const FRotator RotToY = FRotationMatrix::MakeFromZ(FVector::RightVector).Rotator();
|
||||||
|
const FRotator RotToZ = FRotator::ZeroRotator;
|
||||||
|
|
||||||
|
Arrow_X = CreateArrow(TEXT("Arrow_X"), TranslateRoot, RotToX);
|
||||||
|
Arrow_Y = CreateArrow(TEXT("Arrow_Y"), TranslateRoot, RotToY);
|
||||||
|
Arrow_Z = CreateArrow(TEXT("Arrow_Z"), TranslateRoot, RotToZ);
|
||||||
|
|
||||||
|
Cone_X = CreateCone(TEXT("Cone_X"), TranslateRoot, RotToX);
|
||||||
|
Cone_Y = CreateCone(TEXT("Cone_Y"), TranslateRoot, RotToY);
|
||||||
|
Cone_Z = CreateCone(TEXT("Cone_Z"), TranslateRoot, RotToZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Gizmo::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
// Load the gizmo base material.
|
||||||
|
// The user must create: /PS_Editor/M_PS_Editor_Gizmo
|
||||||
|
// (Unlit material with a Vector Parameter named "Color" connected to Emissive Color)
|
||||||
|
UMaterialInterface* GizmoBaseMat = LoadObject<UMaterialInterface>(
|
||||||
|
nullptr, TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
||||||
|
|
||||||
|
if (!GizmoBaseMat)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Material /PS_Editor/M_PS_Editor_Gizmo not found! "
|
||||||
|
"Create an Unlit material with a Vector Parameter 'Color' connected to Emissive Color "
|
||||||
|
"in Plugins/PS_Editor/Content/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto MakeColorMat = [&](const FLinearColor& Color) -> UMaterialInstanceDynamic*
|
||||||
|
{
|
||||||
|
if (!GizmoBaseMat) return nullptr;
|
||||||
|
UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(GizmoBaseMat, this);
|
||||||
|
MID->SetVectorParameterValue(TEXT("Color"), Color);
|
||||||
|
return MID;
|
||||||
|
};
|
||||||
|
|
||||||
|
Mat_X = MakeColorMat(FLinearColor::Red);
|
||||||
|
Mat_Y = MakeColorMat(FLinearColor::Green);
|
||||||
|
Mat_Z = MakeColorMat(FLinearColor(0.0f, 0.0f, 1.0f));
|
||||||
|
Mat_Highlight = MakeColorMat(FLinearColor::Yellow);
|
||||||
|
|
||||||
|
// Apply materials to shafts and cones
|
||||||
|
auto ApplyToAxis = [](UStaticMeshComponent* Shaft, UStaticMeshComponent* Tip, UMaterialInstanceDynamic* Mat)
|
||||||
|
{
|
||||||
|
if (Mat)
|
||||||
|
{
|
||||||
|
if (Shaft) Shaft->SetMaterial(0, Mat);
|
||||||
|
if (Tip) Tip->SetMaterial(0, Mat);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ApplyToAxis(Arrow_X, Cone_X, Mat_X);
|
||||||
|
ApplyToAxis(Arrow_Y, Cone_Y, Mat_Y);
|
||||||
|
ApplyToAxis(Arrow_Z, Cone_Z, Mat_Z);
|
||||||
|
|
||||||
|
// Start hidden
|
||||||
|
SetGizmoVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Gizmo::Tick(float DeltaTime)
|
||||||
|
{
|
||||||
|
Super::Tick(DeltaTime);
|
||||||
|
|
||||||
|
if (bGizmoVisible)
|
||||||
|
{
|
||||||
|
UpdateConstantScreenSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ConfigureGizmoComponent(UStaticMeshComponent* Comp)
|
||||||
|
{
|
||||||
|
Comp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
|
Comp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||||
|
Comp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||||
|
Comp->SetCollisionObjectType(ECC_WorldDynamic);
|
||||||
|
|
||||||
|
Comp->SetCastShadow(false);
|
||||||
|
Comp->SetRenderCustomDepth(false);
|
||||||
|
|
||||||
|
// Render on top of everything
|
||||||
|
Comp->SetDepthPriorityGroup(SDPG_Foreground);
|
||||||
|
Comp->bUseViewOwnerDepthPriorityGroup = false;
|
||||||
|
Comp->SetTranslucentSortPriority(10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
UStaticMeshComponent* APS_Editor_Gizmo::CreateArrow(const FName& Name, USceneComponent* Parent, const FRotator& Rotation)
|
||||||
|
{
|
||||||
|
UStaticMeshComponent* Comp = CreateDefaultSubobject<UStaticMeshComponent>(Name);
|
||||||
|
Comp->SetupAttachment(Parent);
|
||||||
|
|
||||||
|
static ConstructorHelpers::FObjectFinder<UStaticMesh> CylinderMesh(TEXT("/Engine/BasicShapes/Cylinder"));
|
||||||
|
if (CylinderMesh.Succeeded())
|
||||||
|
{
|
||||||
|
Comp->SetStaticMesh(CylinderMesh.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shaft: radius ~3 units, length ~60 units
|
||||||
|
Comp->SetRelativeScale3D(FVector(0.06f, 0.06f, 0.6f));
|
||||||
|
Comp->SetRelativeRotation(Rotation);
|
||||||
|
|
||||||
|
const FVector AxisDir = Rotation.RotateVector(FVector::UpVector);
|
||||||
|
Comp->SetRelativeLocation(AxisDir * 30.0f);
|
||||||
|
|
||||||
|
ConfigureGizmoComponent(Comp);
|
||||||
|
return Comp;
|
||||||
|
}
|
||||||
|
|
||||||
|
UStaticMeshComponent* APS_Editor_Gizmo::CreateCone(const FName& Name, USceneComponent* Parent, const FRotator& Rotation)
|
||||||
|
{
|
||||||
|
UStaticMeshComponent* Comp = CreateDefaultSubobject<UStaticMeshComponent>(Name);
|
||||||
|
Comp->SetupAttachment(Parent);
|
||||||
|
|
||||||
|
static ConstructorHelpers::FObjectFinder<UStaticMesh> ConeMesh(TEXT("/Engine/BasicShapes/Cone"));
|
||||||
|
if (ConeMesh.Succeeded())
|
||||||
|
{
|
||||||
|
Comp->SetStaticMesh(ConeMesh.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
Comp->SetRelativeScale3D(FVector(0.15f, 0.15f, 0.2f));
|
||||||
|
Comp->SetRelativeRotation(Rotation);
|
||||||
|
|
||||||
|
const FVector AxisDir = Rotation.RotateVector(FVector::UpVector);
|
||||||
|
Comp->SetRelativeLocation(AxisDir * 65.0f);
|
||||||
|
|
||||||
|
ConfigureGizmoComponent(Comp);
|
||||||
|
return Comp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Gizmo::SetGizmoActive(bool bActive, FVector WorldLocation)
|
||||||
|
{
|
||||||
|
if (bActive)
|
||||||
|
{
|
||||||
|
SetActorLocation(WorldLocation);
|
||||||
|
SetTransformMode(CurrentMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetGizmoVisible(bActive);
|
||||||
|
SetActorEnableCollision(bActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Gizmo::SetGizmoVisible(bool bVisible)
|
||||||
|
{
|
||||||
|
bGizmoVisible = bVisible;
|
||||||
|
|
||||||
|
// Control visibility per-component instead of SetActorHiddenInGame
|
||||||
|
// so we don't override individual component visibility settings.
|
||||||
|
TranslateRoot->SetVisibility(bVisible && CurrentMode == EPS_Editor_TransformMode::Translate, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Gizmo::SetTransformMode(EPS_Editor_TransformMode NewMode)
|
||||||
|
{
|
||||||
|
CurrentMode = NewMode;
|
||||||
|
TranslateRoot->SetVisibility(bGizmoVisible && NewMode == EPS_Editor_TransformMode::Translate, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
EPS_Editor_GizmoAxis APS_Editor_Gizmo::GetAxisUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const
|
||||||
|
{
|
||||||
|
if (!bGizmoVisible)
|
||||||
|
{
|
||||||
|
return EPS_Editor_GizmoAxis::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
float BestDistance = TNumericLimits<float>::Max();
|
||||||
|
EPS_Editor_GizmoAxis BestAxis = EPS_Editor_GizmoAxis::None;
|
||||||
|
|
||||||
|
auto TestComponent = [&](UStaticMeshComponent* Comp, EPS_Editor_GizmoAxis Axis)
|
||||||
|
{
|
||||||
|
if (!Comp || !Comp->IsVisible()) return;
|
||||||
|
|
||||||
|
FHitResult Hit;
|
||||||
|
const FVector RayEnd = RayOrigin + RayDirection * 100000.0f;
|
||||||
|
if (Comp->LineTraceComponent(Hit, RayOrigin, RayEnd, FCollisionQueryParams::DefaultQueryParam))
|
||||||
|
{
|
||||||
|
if (Hit.Distance < BestDistance)
|
||||||
|
{
|
||||||
|
BestDistance = Hit.Distance;
|
||||||
|
BestAxis = Axis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TestComponent(Arrow_X, EPS_Editor_GizmoAxis::X);
|
||||||
|
TestComponent(Cone_X, EPS_Editor_GizmoAxis::X);
|
||||||
|
TestComponent(Arrow_Y, EPS_Editor_GizmoAxis::Y);
|
||||||
|
TestComponent(Cone_Y, EPS_Editor_GizmoAxis::Y);
|
||||||
|
TestComponent(Arrow_Z, EPS_Editor_GizmoAxis::Z);
|
||||||
|
TestComponent(Cone_Z, EPS_Editor_GizmoAxis::Z);
|
||||||
|
|
||||||
|
return BestAxis;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Gizmo::SetHighlightedAxis(EPS_Editor_GizmoAxis Axis)
|
||||||
|
{
|
||||||
|
if (HighlightedAxis == Axis) return;
|
||||||
|
|
||||||
|
auto SetAxisMat = [this](EPS_Editor_GizmoAxis A, UMaterialInstanceDynamic* Mat)
|
||||||
|
{
|
||||||
|
UStaticMeshComponent* Shaft = GetComponentForAxis(A);
|
||||||
|
UStaticMeshComponent* Tip = nullptr;
|
||||||
|
switch (A)
|
||||||
|
{
|
||||||
|
case EPS_Editor_GizmoAxis::X: Tip = Cone_X; break;
|
||||||
|
case EPS_Editor_GizmoAxis::Y: Tip = Cone_Y; break;
|
||||||
|
case EPS_Editor_GizmoAxis::Z: Tip = Cone_Z; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
SetComponentMaterial(Shaft, Mat);
|
||||||
|
SetComponentMaterial(Tip, Mat);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restore previous
|
||||||
|
if (HighlightedAxis != EPS_Editor_GizmoAxis::None)
|
||||||
|
{
|
||||||
|
UMaterialInstanceDynamic* OrigMat = nullptr;
|
||||||
|
switch (HighlightedAxis)
|
||||||
|
{
|
||||||
|
case EPS_Editor_GizmoAxis::X: OrigMat = Mat_X; break;
|
||||||
|
case EPS_Editor_GizmoAxis::Y: OrigMat = Mat_Y; break;
|
||||||
|
case EPS_Editor_GizmoAxis::Z: OrigMat = Mat_Z; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
SetAxisMat(HighlightedAxis, OrigMat);
|
||||||
|
}
|
||||||
|
|
||||||
|
HighlightedAxis = Axis;
|
||||||
|
|
||||||
|
// Highlight new
|
||||||
|
if (Axis != EPS_Editor_GizmoAxis::None)
|
||||||
|
{
|
||||||
|
SetAxisMat(Axis, Mat_Highlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector APS_Editor_Gizmo::GetAxisDirection(EPS_Editor_GizmoAxis Axis) const
|
||||||
|
{
|
||||||
|
switch (Axis)
|
||||||
|
{
|
||||||
|
case EPS_Editor_GizmoAxis::X: return FVector::ForwardVector;
|
||||||
|
case EPS_Editor_GizmoAxis::Y: return FVector::RightVector;
|
||||||
|
case EPS_Editor_GizmoAxis::Z: return FVector::UpVector;
|
||||||
|
default: return FVector::ZeroVector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Gizmo::UpdateConstantScreenSize()
|
||||||
|
{
|
||||||
|
APlayerController* PC = GetWorld()->GetFirstPlayerController();
|
||||||
|
if (!PC) return;
|
||||||
|
|
||||||
|
FVector CameraLocation;
|
||||||
|
FRotator CameraRotation;
|
||||||
|
PC->GetPlayerViewPoint(CameraLocation, CameraRotation);
|
||||||
|
|
||||||
|
const float Distance = FVector::Dist(CameraLocation, GetActorLocation());
|
||||||
|
if (Distance < 1.0f) return;
|
||||||
|
|
||||||
|
const float Scale = Distance * DesiredScreenSize * 0.01f;
|
||||||
|
SetActorScale3D(FVector(Scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
UStaticMeshComponent* APS_Editor_Gizmo::GetComponentForAxis(EPS_Editor_GizmoAxis Axis) const
|
||||||
|
{
|
||||||
|
switch (Axis)
|
||||||
|
{
|
||||||
|
case EPS_Editor_GizmoAxis::X: return Arrow_X;
|
||||||
|
case EPS_Editor_GizmoAxis::Y: return Arrow_Y;
|
||||||
|
case EPS_Editor_GizmoAxis::Z: return Arrow_Z;
|
||||||
|
default: return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Gizmo::SetComponentMaterial(UStaticMeshComponent* Comp, UMaterialInstanceDynamic* Mat)
|
||||||
|
{
|
||||||
|
if (Comp && Mat)
|
||||||
|
{
|
||||||
|
Comp->SetMaterial(0, Mat);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "PS_Editor_SelectionTypes.h"
|
||||||
|
#include "PS_Editor_Gizmo.generated.h"
|
||||||
|
|
||||||
|
class UStaticMeshComponent;
|
||||||
|
class UMaterialInstanceDynamic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom runtime transform gizmo for PS_Editor.
|
||||||
|
* Uses engine BasicShapes (Cone, Cylinder, Cube) to build translate/rotate/scale handles.
|
||||||
|
* Maintains constant screen size by adjusting scale based on camera distance.
|
||||||
|
*
|
||||||
|
* Phase 1: Translate mode only (3 axis arrows + 3 plane handles).
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class PS_EDITOR_API APS_Editor_Gizmo : public AActor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
APS_Editor_Gizmo();
|
||||||
|
|
||||||
|
virtual void Tick(float DeltaTime) override;
|
||||||
|
|
||||||
|
/** Show/hide the gizmo and set its position. */
|
||||||
|
void SetGizmoActive(bool bActive, FVector WorldLocation = FVector::ZeroVector);
|
||||||
|
|
||||||
|
/** Switch between Translate/Rotate/Scale modes. */
|
||||||
|
void SetTransformMode(EPS_Editor_TransformMode NewMode);
|
||||||
|
|
||||||
|
/** Test which gizmo axis is under the given world ray. */
|
||||||
|
EPS_Editor_GizmoAxis GetAxisUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const;
|
||||||
|
|
||||||
|
/** Highlight the hovered axis (change material color). */
|
||||||
|
void SetHighlightedAxis(EPS_Editor_GizmoAxis Axis);
|
||||||
|
|
||||||
|
/** Get the world-space direction for a given axis. */
|
||||||
|
FVector GetAxisDirection(EPS_Editor_GizmoAxis Axis) const;
|
||||||
|
|
||||||
|
EPS_Editor_TransformMode GetTransformMode() const { return CurrentMode; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// ---- Root ----
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<USceneComponent> GizmoRoot;
|
||||||
|
|
||||||
|
// ---- Translate handles ----
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<USceneComponent> TranslateRoot;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UStaticMeshComponent> Arrow_X;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UStaticMeshComponent> Arrow_Y;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UStaticMeshComponent> Arrow_Z;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UStaticMeshComponent> Cone_X;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UStaticMeshComponent> Cone_Y;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UStaticMeshComponent> Cone_Z;
|
||||||
|
|
||||||
|
// ---- Materials ----
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> Mat_X;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> Mat_Y;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> Mat_Z;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> Mat_Highlight;
|
||||||
|
|
||||||
|
// ---- State ----
|
||||||
|
EPS_Editor_TransformMode CurrentMode = EPS_Editor_TransformMode::Translate;
|
||||||
|
EPS_Editor_GizmoAxis HighlightedAxis = EPS_Editor_GizmoAxis::None;
|
||||||
|
bool bGizmoVisible = false;
|
||||||
|
|
||||||
|
float DesiredScreenSize = 0.25f;
|
||||||
|
|
||||||
|
// ---- Helpers ----
|
||||||
|
UStaticMeshComponent* CreateArrow(const FName& Name, USceneComponent* Parent, const FRotator& Rotation);
|
||||||
|
UStaticMeshComponent* CreateCone(const FName& Name, USceneComponent* Parent, const FRotator& Rotation);
|
||||||
|
void UpdateConstantScreenSize();
|
||||||
|
void SetGizmoVisible(bool bVisible);
|
||||||
|
UStaticMeshComponent* GetComponentForAxis(EPS_Editor_GizmoAxis Axis) const;
|
||||||
|
void SetComponentMaterial(UStaticMeshComponent* Comp, UMaterialInstanceDynamic* Mat);
|
||||||
|
};
|
||||||
@@ -13,6 +13,8 @@ public class PS_Editor : ModuleRules
|
|||||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "GameMode"));
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "GameMode"));
|
||||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "UI"));
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "UI"));
|
||||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "UI", "Widgets"));
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "UI", "Widgets"));
|
||||||
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Selection"));
|
||||||
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo"));
|
||||||
|
|
||||||
PublicDependencyModuleNames.AddRange(new string[]
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
#include "PS_Editor_SelectionManager.h"
|
||||||
|
#include "PS_Editor_Gizmo.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "Components/PrimitiveComponent.h"
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::Initialize(APlayerController* InOwnerPC)
|
||||||
|
{
|
||||||
|
OwnerPC = InOwnerPC;
|
||||||
|
|
||||||
|
// Spawn the gizmo actor (kept alive for the whole session, shown/hidden as needed)
|
||||||
|
if (UWorld* World = InOwnerPC->GetWorld())
|
||||||
|
{
|
||||||
|
FActorSpawnParameters SpawnParams;
|
||||||
|
SpawnParams.Owner = InOwnerPC;
|
||||||
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
Gizmo = World->SpawnActor<APS_Editor_Gizmo>(APS_Editor_Gizmo::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::HandleClickAtScreenPosition(FVector2D ScreenPos, bool bAdditive)
|
||||||
|
{
|
||||||
|
APlayerController* PC = OwnerPC.Get();
|
||||||
|
if (!PC)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deproject screen position to world ray
|
||||||
|
FVector WorldOrigin, WorldDirection;
|
||||||
|
if (!PC->DeprojectScreenPositionToWorld(ScreenPos.X, ScreenPos.Y, WorldOrigin, WorldDirection))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raycast
|
||||||
|
FHitResult Hit;
|
||||||
|
FCollisionQueryParams Params;
|
||||||
|
Params.AddIgnoredActor(PC->GetPawn());
|
||||||
|
if (Gizmo)
|
||||||
|
{
|
||||||
|
Params.AddIgnoredActor(Gizmo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector End = WorldOrigin + WorldDirection * 100000.0f;
|
||||||
|
const bool bHit = PC->GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Visibility, Params);
|
||||||
|
|
||||||
|
if (bHit && Hit.GetActor())
|
||||||
|
{
|
||||||
|
AActor* HitActor = Hit.GetActor();
|
||||||
|
|
||||||
|
// Only allow selection of Movable actors (skip static scene geometry like floors/walls)
|
||||||
|
USceneComponent* RootComp = HitActor->GetRootComponent();
|
||||||
|
const bool bIsSelectable = RootComp && RootComp->Mobility == EComponentMobility::Movable;
|
||||||
|
|
||||||
|
if (bIsSelectable)
|
||||||
|
{
|
||||||
|
if (bAdditive)
|
||||||
|
{
|
||||||
|
ToggleActor(HitActor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClearSelection();
|
||||||
|
SelectActor(HitActor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Hit a non-selectable actor (static geometry): treat as clicking empty space
|
||||||
|
if (!bAdditive)
|
||||||
|
{
|
||||||
|
ClearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Clicked on nothing: deselect all
|
||||||
|
if (!bAdditive)
|
||||||
|
{
|
||||||
|
ClearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::SelectActor(AActor* Actor)
|
||||||
|
{
|
||||||
|
if (!Actor || IsActorSelected(Actor))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedActors.Add(Actor);
|
||||||
|
SetActorHighlight(Actor, true);
|
||||||
|
UpdateGizmo();
|
||||||
|
OnSelectionChanged.Broadcast();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selected %s"), *Actor->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
||||||
|
{
|
||||||
|
if (!Actor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 Removed = SelectedActors.RemoveAll([Actor](const TWeakObjectPtr<AActor>& Weak)
|
||||||
|
{
|
||||||
|
return Weak.Get() == Actor;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Removed > 0)
|
||||||
|
{
|
||||||
|
SetActorHighlight(Actor, false);
|
||||||
|
UpdateGizmo();
|
||||||
|
OnSelectionChanged.Broadcast();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::ToggleActor(AActor* Actor)
|
||||||
|
{
|
||||||
|
if (IsActorSelected(Actor))
|
||||||
|
{
|
||||||
|
DeselectActor(Actor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectActor(Actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::ClearSelection()
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
SetActorHighlight(Actor, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 Count = SelectedActors.Num();
|
||||||
|
SelectedActors.Empty();
|
||||||
|
UpdateGizmo();
|
||||||
|
OnSelectionChanged.Broadcast();
|
||||||
|
|
||||||
|
if (Count > 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Cleared selection (%d actors)"), Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_SelectionManager::IsActorSelected(AActor* Actor) const
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||||
|
{
|
||||||
|
if (Weak.Get() == Actor)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector UPS_Editor_SelectionManager::GetSelectionPivot() const
|
||||||
|
{
|
||||||
|
if (SelectedActors.Num() == 0)
|
||||||
|
{
|
||||||
|
return FVector::ZeroVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector Sum = FVector::ZeroVector;
|
||||||
|
int32 Count = 0;
|
||||||
|
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
Sum += Actor->GetActorLocation();
|
||||||
|
Count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Count > 0 ? (Sum / Count) : FVector::ZeroVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::SetTransformMode(EPS_Editor_TransformMode NewMode)
|
||||||
|
{
|
||||||
|
CurrentTransformMode = NewMode;
|
||||||
|
OnSelectionChanged.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::SetTransformSpace(EPS_Editor_Space NewSpace)
|
||||||
|
{
|
||||||
|
CurrentSpace = NewSpace;
|
||||||
|
OnSelectionChanged.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::SetActorHighlight(AActor* Actor, bool bHighlight)
|
||||||
|
{
|
||||||
|
if (!Actor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<UPrimitiveComponent*> Primitives;
|
||||||
|
Actor->GetComponents<UPrimitiveComponent>(Primitives);
|
||||||
|
|
||||||
|
for (UPrimitiveComponent* Prim : Primitives)
|
||||||
|
{
|
||||||
|
Prim->SetRenderCustomDepth(bHighlight);
|
||||||
|
Prim->SetCustomDepthStencilValue(bHighlight ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::UpdateGizmo()
|
||||||
|
{
|
||||||
|
if (!Gizmo)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAnythingSelected())
|
||||||
|
{
|
||||||
|
Gizmo->SetGizmoActive(true, GetSelectionPivot());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Gizmo->SetGizmoActive(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::CleanupStaleReferences()
|
||||||
|
{
|
||||||
|
SelectedActors.RemoveAll([](const TWeakObjectPtr<AActor>& Weak)
|
||||||
|
{
|
||||||
|
return !Weak.IsValid();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/NoExportTypes.h"
|
||||||
|
#include "PS_Editor_SelectionTypes.h"
|
||||||
|
#include "PS_Editor_SelectionManager.generated.h"
|
||||||
|
|
||||||
|
class APlayerController;
|
||||||
|
class APS_Editor_Gizmo;
|
||||||
|
|
||||||
|
DECLARE_MULTICAST_DELEGATE(FPS_Editor_OnSelectionChanged);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the selection state for the PS_Editor runtime editor.
|
||||||
|
* Owned by APS_Editor_PlayerController.
|
||||||
|
*
|
||||||
|
* Handles: raycast selection, multi-select, highlight toggling (custom depth stencil),
|
||||||
|
* transform mode, and selection-changed notifications.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class PS_EDITOR_API UPS_Editor_SelectionManager : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Initialize with owning player controller. Must be called after construction. */
|
||||||
|
void Initialize(APlayerController* InOwnerPC);
|
||||||
|
|
||||||
|
// ---- Selection Operations ----
|
||||||
|
|
||||||
|
/** Perform a raycast at the given screen position and select/deselect accordingly. */
|
||||||
|
void HandleClickAtScreenPosition(FVector2D ScreenPos, bool bAdditive);
|
||||||
|
|
||||||
|
void SelectActor(AActor* Actor);
|
||||||
|
void DeselectActor(AActor* Actor);
|
||||||
|
void ToggleActor(AActor* Actor);
|
||||||
|
void ClearSelection();
|
||||||
|
|
||||||
|
// ---- Queries ----
|
||||||
|
|
||||||
|
const TArray<TWeakObjectPtr<AActor>>& GetSelectedActors() const { return SelectedActors; }
|
||||||
|
bool IsAnythingSelected() const { return SelectedActors.Num() > 0; }
|
||||||
|
bool IsActorSelected(AActor* Actor) const;
|
||||||
|
FVector GetSelectionPivot() const;
|
||||||
|
|
||||||
|
// ---- Transform Mode ----
|
||||||
|
|
||||||
|
EPS_Editor_TransformMode GetTransformMode() const { return CurrentTransformMode; }
|
||||||
|
void SetTransformMode(EPS_Editor_TransformMode NewMode);
|
||||||
|
|
||||||
|
APS_Editor_Gizmo* GetGizmo() const { return Gizmo; }
|
||||||
|
|
||||||
|
EPS_Editor_Space GetTransformSpace() const { return CurrentSpace; }
|
||||||
|
void SetTransformSpace(EPS_Editor_Space NewSpace);
|
||||||
|
|
||||||
|
// ---- Delegates ----
|
||||||
|
|
||||||
|
FPS_Editor_OnSelectionChanged OnSelectionChanged;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY()
|
||||||
|
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<TWeakObjectPtr<AActor>> SelectedActors;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<APS_Editor_Gizmo> Gizmo;
|
||||||
|
|
||||||
|
EPS_Editor_TransformMode CurrentTransformMode = EPS_Editor_TransformMode::Translate;
|
||||||
|
EPS_Editor_Space CurrentSpace = EPS_Editor_Space::World;
|
||||||
|
|
||||||
|
/** Toggle custom depth stencil rendering on an actor's primitive components. */
|
||||||
|
void SetActorHighlight(AActor* Actor, bool bHighlight);
|
||||||
|
|
||||||
|
/** Clean up any stale weak pointers in the selection array. */
|
||||||
|
void CleanupStaleReferences();
|
||||||
|
|
||||||
|
/** Update gizmo visibility and position based on current selection. */
|
||||||
|
void UpdateGizmo();
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "PS_Editor_SelectionTypes.generated.h"
|
||||||
|
|
||||||
|
/** Transform mode for the gizmo. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EPS_Editor_TransformMode : uint8
|
||||||
|
{
|
||||||
|
Translate,
|
||||||
|
Rotate,
|
||||||
|
Scale
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Which gizmo axis/plane is being interacted with. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EPS_Editor_GizmoAxis : uint8
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
Z,
|
||||||
|
XY,
|
||||||
|
XZ,
|
||||||
|
YZ,
|
||||||
|
XYZ
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Transform coordinate space. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EPS_Editor_Space : uint8
|
||||||
|
{
|
||||||
|
World,
|
||||||
|
Local
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user