From 8b2609669fc58f9a4c6389018cde1cfbe4d5b461 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Fri, 10 Apr 2026 15:06:11 +0200 Subject: [PATCH] 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) --- Unreal/Config/DefaultEngine.ini | 2 + Unreal/Content/test.umap | Bin 45718 -> 48048 bytes .../Content/M_PS_Editor_Gizmo.uasset | Bin 0 -> 11245 bytes .../PS_Editor/GameMode/PS_Editor_Pawn.cpp | 85 ++++- .../PS_Editor/GameMode/PS_Editor_Pawn.h | 43 +-- .../GameMode/PS_Editor_PlayerController.cpp | 26 +- .../GameMode/PS_Editor_PlayerController.h | 14 +- .../PS_Editor/Gizmo/PS_Editor_Gizmo.cpp | 297 ++++++++++++++++++ .../Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h | 102 ++++++ .../Source/PS_Editor/PS_Editor.Build.cs | 2 + .../Selection/PS_Editor_SelectionManager.cpp | 244 ++++++++++++++ .../Selection/PS_Editor_SelectionManager.h | 81 +++++ .../Selection/PS_Editor_SelectionTypes.h | 35 +++ 13 files changed, 896 insertions(+), 35 deletions(-) create mode 100644 Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionTypes.h diff --git a/Unreal/Config/DefaultEngine.ini b/Unreal/Config/DefaultEngine.ini index f56be48..24b19a6 100644 --- a/Unreal/Config/DefaultEngine.ini +++ b/Unreal/Config/DefaultEngine.ini @@ -16,6 +16,8 @@ r.ReflectionMethod=1 r.SkinCache.CompileShaders=True +r.CustomDepth=3 + r.RayTracing=True r.Shadow.Virtual.Enable=1 diff --git a/Unreal/Content/test.umap b/Unreal/Content/test.umap index 7fe963f635c1f4619cd9f11fb1401a7eab8afc58..e87f29c383b0624bf8d84c97aaf6891dcaf6176c 100644 GIT binary patch delta 4121 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 delta 3098 zcmZuzc~n$Y7{BicHsqk7#4?D3K#wtSSR6J*U_e>MB~c*7ScX;H29OxUTu>~}$p!tg z?3_|7Pe(0{>WD_vX#>4RFu7ckcatzwh^bcYV`- zpMUE%-)`c9lvXNItB!~kJ|RTx4)fr2qmdLfue`pdwjpZHyprh^H4V0U*3s4< zv_qH%C4#TGQ}~q+9D_8)Ttf4jWvi%Uy>5Yr#GMmf=eTSLiU@W862oLky)Upg3i_9f zf#?X0XAWhEjHDcV4lTAs|!v|)Vt{9 zNwas=@iyvJs{JNORfSkx?B=+^Q5R5DmpCsE$b*Y=&Ew7xdBnuHa@+=~)(k@Y1|f?F zM9v)$xo(h$et^df$GWOkpe26bnwX>sEv`@1yb80wc|~M=SXdx)OJq__pFfh=|E>nIaI5%nkykSDr6oZ;1#EFr@P}P z*%zzqd6K=;q~T6MOQas!<229~=LY+td|+Li7xxYHNBK#!uTy#DgJ*mwcLHw2={*}% zcmWh4ehhHWgu56|zIJRBcCuxjhPknF*^pEOC*v(HRDU#Osi;Po`fd>z6D-_aSeX#Y zpQAy6&KqsEA-E45W|A7pEEG%LWUd{;JCDqTzC(GgcX$R35T-n+v=_t0Bn#|{*XlxX zIF!PvV%UwD3uv}rYznWzx(6$jJH=ql(ScX81};xFz^2JsaGT=KZ!LjwQ!GNSYDwvH zGDDSpo8-^fy;9hU)E*U;COS(+Ju6dNt?+nCq3>)o4U-{%cSmPdnUY6avO)M$rA7}w z-4PO`)C*mB{DmrYyX+FO6pSer(4=T}&m&Z&P#9Y+C-BZvMYS`h4p$mI|xPz0rQC4+>SF<9yAK*Y%C?CIB3DuWnk`Aq1m64~zmE|8% zXrfdBl0ir_*{hFi7|V_*pisuO{2{=t;-%6Ax3Fc+T3+q44!GA8}jxVV=7NZR?}NWmWlF zXfF4f@GO$)0izkw-44V`yH7Wf7KPrMV61xaNqs!;Q`Ivj;lYClw&&=>2IKer(vpGI z9wsh(QIPvYRr+L4k1J&kDyIRuB+J>GH zKR0&zw%$#-SigG5w%CBZANQo4IdAF|x=Mfi@bcnL^pr{!a32MTSGJ!Y#$6TP-#JS7 EA55|m%>V!Z diff --git a/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset b/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset new file mode 100644 index 0000000000000000000000000000000000000000..c3c290019e3a88784e8c79889c5eef26db8add72 GIT binary patch literal 11245 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 zQ7@`?G?B?FV9(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(PC)) + { + if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager()) + { + for (const TWeakObjectPtr& 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(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(this, TEXT("IA_MiddleClick")); IA_MiddleClick->ValueType = EInputActionValueType::Boolean; - // --- Create Mapping Context --- EditorMappingContext = NewObject(this, TEXT("EditorMappingContext")); // Movement helpers @@ -191,22 +208,42 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value) const FVector2D LookInput = Value.Get(); + 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(GetController()); + if (!PC) + { + return FVector2D::ZeroVector; + } + + float MouseX = 0.0f; + float MouseY = 0.0f; + PC->GetMousePosition(MouseX, MouseY); + return FVector2D(MouseX, MouseY); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h index 86bdb31..8666577 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h @@ -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 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; }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp index 4063023..1c51bfa 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp @@ -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(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(InPawn)) + { + EditorPawn->OnEditorClick.BindUObject(this, &APS_Editor_PlayerController::HandleEditorClick); + } +} + +void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive) +{ + if (SelectionManager) + { + SelectionManager->HandleClickAtScreenPosition(ScreenPos, bAdditive); + } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h index 9181cc9..a8a7e36 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h @@ -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 SelectionManager; + + void HandleEditorClick(FVector2D ScreenPos, bool bAdditive); }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp new file mode 100644 index 0000000..34407e1 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp @@ -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(TEXT("GizmoRoot")); + SetRootComponent(GizmoRoot); + + TranslateRoot = CreateDefaultSubobject(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( + 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(Name); + Comp->SetupAttachment(Parent); + + static ConstructorHelpers::FObjectFinder 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(Name); + Comp->SetupAttachment(Parent); + + static ConstructorHelpers::FObjectFinder 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::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); + } +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h new file mode 100644 index 0000000..4b2dacc --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h @@ -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 GizmoRoot; + + // ---- Translate handles ---- + UPROPERTY(VisibleAnywhere) + TObjectPtr TranslateRoot; + + UPROPERTY(VisibleAnywhere) + TObjectPtr Arrow_X; + + UPROPERTY(VisibleAnywhere) + TObjectPtr Arrow_Y; + + UPROPERTY(VisibleAnywhere) + TObjectPtr Arrow_Z; + + UPROPERTY(VisibleAnywhere) + TObjectPtr Cone_X; + + UPROPERTY(VisibleAnywhere) + TObjectPtr Cone_Y; + + UPROPERTY(VisibleAnywhere) + TObjectPtr Cone_Z; + + // ---- Materials ---- + UPROPERTY() + TObjectPtr Mat_X; + + UPROPERTY() + TObjectPtr Mat_Y; + + UPROPERTY() + TObjectPtr Mat_Z; + + UPROPERTY() + TObjectPtr 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); +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs index f0a5bb3..9af7921 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs @@ -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[] { diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp new file mode 100644 index 0000000..67f0a4f --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp @@ -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::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& 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& 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& 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& 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 Primitives; + Actor->GetComponents(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& Weak) + { + return !Weak.IsValid(); + }); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h new file mode 100644 index 0000000..6adf866 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h @@ -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>& 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 OwnerPC; + + UPROPERTY() + TArray> SelectedActors; + + UPROPERTY() + TObjectPtr 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(); +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionTypes.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionTypes.h new file mode 100644 index 0000000..6a767c4 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionTypes.h @@ -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 +};