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.CustomDepth=3
|
||||
|
||||
r.RayTracing=True
|
||||
|
||||
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 "InputTriggers.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
|
||||
APS_Editor_Pawn::APS_Editor_Pawn()
|
||||
{
|
||||
@@ -36,17 +39,32 @@ void APS_Editor_Pawn::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
// Track Alt key state every frame
|
||||
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
||||
{
|
||||
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()
|
||||
{
|
||||
// --- Create Input Actions ---
|
||||
|
||||
IA_Move = NewObject<UInputAction>(this, TEXT("IA_Move"));
|
||||
IA_Move->ValueType = EInputActionValueType::Axis3D;
|
||||
IA_Move->bConsumeInput = false;
|
||||
@@ -66,7 +84,6 @@ void APS_Editor_Pawn::CreateInputActions()
|
||||
IA_MiddleClick = NewObject<UInputAction>(this, TEXT("IA_MiddleClick"));
|
||||
IA_MiddleClick->ValueType = EInputActionValueType::Boolean;
|
||||
|
||||
// --- Create Mapping Context ---
|
||||
EditorMappingContext = NewObject<UInputMappingContext>(this, TEXT("EditorMappingContext"));
|
||||
|
||||
// Movement helpers
|
||||
@@ -191,22 +208,42 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
|
||||
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)
|
||||
{
|
||||
// Left-click drag: mouse X = yaw rotation (Z axis only), mouse Y = move forward/back on horizontal plane
|
||||
const float YawDelta = LookInput.X * PanYawSensitivity;
|
||||
FRotator CurrentRotation = GetActorRotation();
|
||||
CurrentRotation.Yaw += YawDelta;
|
||||
SetActorRotation(CurrentRotation);
|
||||
|
||||
// Forward/back movement on horizontal plane
|
||||
const FVector HorizontalForward = FVector(GetActorForwardVector().X, GetActorForwardVector().Y, 0.0f).GetSafeNormal();
|
||||
const FVector PanMovement = HorizontalForward * LookInput.Y * PanSpeed;
|
||||
AddActorWorldOffset(PanMovement, true);
|
||||
}
|
||||
else if (CameraMode == EPS_Editor_CameraMode::Fly)
|
||||
{
|
||||
// Right-click drag: full free look
|
||||
const float YawDelta = LookInput.X * LookSensitivity;
|
||||
const float PitchDelta = LookInput.Y * LookSensitivity;
|
||||
|
||||
@@ -247,7 +284,6 @@ void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Idle or Pan: dolly forward/backward
|
||||
const FVector Forward = GetActorForwardVector();
|
||||
AddActorWorldOffset(Forward * ScrollValue * OrbitZoomStep * 5.0f, true);
|
||||
}
|
||||
@@ -257,20 +293,28 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
||||
{
|
||||
if (bAltHeld)
|
||||
{
|
||||
// Alt + left-click = orbit
|
||||
// Alt + left-click = orbit immediately (no pending)
|
||||
ComputeOrbitFocalPoint();
|
||||
SetCameraMode(EPS_Editor_CameraMode::Orbit);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Left-click = pan (yaw + forward/back on horizontal plane)
|
||||
SetCameraMode(EPS_Editor_CameraMode::Pan);
|
||||
// Enter PendingClick: wait to see if this is a click or a drag
|
||||
PendingClickScreenPos = GetMouseScreenPosition();
|
||||
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -327,7 +371,8 @@ void APS_Editor_Pawn::UpdateCursorVisibility()
|
||||
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;
|
||||
|
||||
if (bShowCursor)
|
||||
@@ -360,3 +405,17 @@ void APS_Editor_Pawn::ComputeOrbitFocalPoint()
|
||||
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 UInputMappingContext;
|
||||
|
||||
DECLARE_DELEGATE_TwoParams(FPS_Editor_OnClick, FVector2D /*ScreenPos*/, bool /*bAdditive*/);
|
||||
|
||||
/**
|
||||
* Camera modes for the editor pawn.
|
||||
*/
|
||||
@@ -18,19 +20,23 @@ enum class EPS_Editor_CameraMode : uint8
|
||||
{
|
||||
/** Default mode - cursor visible, ZQSD/arrows still move. */
|
||||
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,
|
||||
/** Right-click held: full free look + ZQSD movement. */
|
||||
Fly,
|
||||
/** Alt + left-click held: orbit around focal point. */
|
||||
Orbit
|
||||
/** Alt + left-click or middle-click: orbit around focal point. */
|
||||
Orbit,
|
||||
/** Dragging a gizmo handle. */
|
||||
GizmoDrag
|
||||
};
|
||||
|
||||
/**
|
||||
* Editor camera pawn mimicking UE viewport navigation.
|
||||
*
|
||||
* 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)
|
||||
* - Alt + left-click held: Orbit around focal point
|
||||
* - Middle-click held: Orbit around focal point (alternative)
|
||||
@@ -49,6 +55,9 @@ public:
|
||||
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) 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:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
@@ -65,60 +74,51 @@ protected:
|
||||
|
||||
// ---- Camera Settings ----
|
||||
|
||||
/** Base movement speed in fly mode (cm/s). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||
float FlySpeed = 1000.0f;
|
||||
|
||||
/** Minimum fly speed. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||
float FlySpeedMin = 100.0f;
|
||||
|
||||
/** Maximum fly speed. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||
float FlySpeedMax = 10000.0f;
|
||||
|
||||
/** Mouse look sensitivity. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||
float LookSensitivity = 2.5f;
|
||||
|
||||
/** Speed change per scroll tick. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||
float FlySpeedScrollStep = 200.0f;
|
||||
|
||||
/** Pan speed multiplier (left-click drag forward/back). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Pan")
|
||||
float PanSpeed = 5.0f;
|
||||
|
||||
/** Pan yaw sensitivity (left-click drag left/right). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Pan")
|
||||
float PanYawSensitivity = 1.0f;
|
||||
|
||||
/** Orbit distance from focal point. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||
float OrbitDistance = 500.0f;
|
||||
|
||||
/** Orbit zoom step per scroll tick. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||
float OrbitZoomStep = 50.0f;
|
||||
|
||||
/** Minimum orbit distance. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||
float OrbitDistanceMin = 50.0f;
|
||||
|
||||
/** Maximum orbit distance. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||
float OrbitDistanceMax = 5000.0f;
|
||||
|
||||
/** Orbit rotation sensitivity. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||
float OrbitSensitivity = 1.0f;
|
||||
|
||||
/** Pixel distance threshold to distinguish click from drag. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Selection")
|
||||
float ClickDragThreshold = 8.0f;
|
||||
|
||||
// ---- State ----
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
EPS_Editor_CameraMode CameraMode = EPS_Editor_CameraMode::Idle;
|
||||
|
||||
/** The point in world space the orbit camera rotates around. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera|Orbit")
|
||||
FVector OrbitFocalPoint = FVector::ZeroVector;
|
||||
|
||||
@@ -145,8 +145,12 @@ private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_MiddleClick;
|
||||
|
||||
/** Is the Alt key currently held down. */
|
||||
// ---- Modifier key state ----
|
||||
bool bAltHeld = false;
|
||||
bool bCtrlHeld = false;
|
||||
|
||||
// ---- Click vs drag state ----
|
||||
FVector2D PendingClickScreenPos = FVector2D::ZeroVector;
|
||||
|
||||
// ---- Input Handlers ----
|
||||
void HandleMove(const FInputActionValue& Value);
|
||||
@@ -164,9 +168,8 @@ private:
|
||||
void SetCameraMode(EPS_Editor_CameraMode NewMode);
|
||||
void UpdateCursorVisibility();
|
||||
void ComputeOrbitFocalPoint();
|
||||
FVector2D GetMouseScreenPosition() const;
|
||||
|
||||
/** Current pitch accumulated during orbit (clamped). */
|
||||
float OrbitPitch = 0.0f;
|
||||
/** Current yaw accumulated during orbit. */
|
||||
float OrbitYaw = 0.0f;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_Pawn.h"
|
||||
|
||||
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
||||
{
|
||||
@@ -10,6 +12,28 @@ void APS_Editor_PlayerController::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Start in Game+UI mode so cursor is visible and clicks go to both UI and game
|
||||
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 "PS_Editor_PlayerController.generated.h"
|
||||
|
||||
class UPS_Editor_SelectionManager;
|
||||
|
||||
/**
|
||||
* 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()
|
||||
class PS_EDITOR_API APS_Editor_PlayerController : public APlayerController
|
||||
@@ -16,6 +18,16 @@ class PS_EDITOR_API APS_Editor_PlayerController : public APlayerController
|
||||
public:
|
||||
APS_Editor_PlayerController();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
UPS_Editor_SelectionManager* GetSelectionManager() const { return SelectionManager; }
|
||||
|
||||
protected:
|
||||
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, "UI"));
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "UI", "Widgets"));
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Selection"));
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo"));
|
||||
|
||||
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