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 7fe963f..e87f29c 100644 Binary files a/Unreal/Content/test.umap and b/Unreal/Content/test.umap differ 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 0000000..c3c2900 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset differ diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp index f70c8b0..5d720db 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp @@ -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(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 +};