Compare commits
7 Commits
88544c4c85
...
11945e90d2
| Author | SHA1 | Date | |
|---|---|---|---|
| 11945e90d2 | |||
| 7ef566f3f3 | |||
| b33b7cf9d3 | |||
| 76d2ccd249 | |||
| 8dba2ab0fc | |||
| 48505bfb82 | |||
| 03c79a2610 |
@@ -8,11 +8,12 @@ PS_Editor is a runtime editor plugin for Unreal Engine 5.5 for the PROSERVE proj
|
||||
|
||||
Key decisions:
|
||||
- UI: UMG driven by C++ (Slate)
|
||||
- Gizmos: Custom actor with BasicShapes + unlit material (ITF is editor-only)
|
||||
- Gizmos: Custom actor (BasicShapes + ProceduralMeshComponent for arcs)
|
||||
- Serialization: JSON via Json/JsonUtilities (FJsonObjectConverter)
|
||||
- Naming: PS_Editor_ prefix on all files/classes
|
||||
- Plugin path: Unreal/Plugins/PS_Editor/
|
||||
- AZERTY keyboard: arrows + A/Q for movement, E/R/T for modes
|
||||
- AZERTY keyboard: arrows + A/Q movement, Z/E/R for translate/rotate/scale
|
||||
- Selection outline: auto-generated post-process material (C++), 8-neighbor edge detection on CustomStencil
|
||||
|
||||
## Completed Phases
|
||||
|
||||
@@ -22,26 +23,32 @@ Key decisions:
|
||||
|
||||
### Phase 2 - Selection + Transform + Undo
|
||||
- Click-to-select (Movable actors only), Ctrl+click multi-select
|
||||
- Custom gizmo (R/G/B arrows + cones, SDPG_Foreground, constant screen size)
|
||||
- Translate/Rotate/Scale via gizmo drag, hotkeys E/R/T
|
||||
- Snap to ground (End), soft-delete (Delete + confirmation dialog)
|
||||
- Gizmo modes:
|
||||
- Translate (Z): arrows + cones, world or local space
|
||||
- Rotate (E): quarter-arc rings (ProceduralMesh), dynamic octant facing, local orientation during drag
|
||||
- Scale (R): arrows + cubes + center cube (uniform), always local space
|
||||
- World/Local space toggle (UI button), Scale always local
|
||||
- Snap system: configurable translation/rotation/scale snapping (UI toggle + value field)
|
||||
- Snap to ground (End), soft-delete (Delete + in-game confirmation dialog)
|
||||
- Undo/Redo (Ctrl+Z/Y, 50 action stack)
|
||||
- Properties panel + toolbar UI
|
||||
- Material: M_PS_Editor_Gizmo (Unlit, "Color" param, Translucent, Disable Depth Test)
|
||||
- Selection outline: auto-generated PP material, BL_SceneColorAfterDOF, color AF1500, 1px
|
||||
- Rotation: screen-space direction with magnitude speed, fixed axis during drag
|
||||
|
||||
### Phase 3 - Object Spawning
|
||||
- SpawnableComponent: marker on BPs (name, category, thumbnail)
|
||||
- SpawnCatalog DataAsset + Factory for easy creation
|
||||
- SpawnManager: spawns in front of camera, tracks all spawned actors
|
||||
- Catalogue UI: thumbnail cards, click to spawn, grouped by category
|
||||
- CaptureAllThumbnails on DataAsset: auto-generates from UE thumbnails
|
||||
- Spawn undo/redo, auto-deselect hidden actors
|
||||
- SpawnCatalog DataAsset + Factory
|
||||
- CaptureAllThumbnails on DataAsset
|
||||
- Catalogue UI: thumbnail cards (80x80), grouped by category
|
||||
|
||||
### Phase 4 - JSON Serialization
|
||||
- SceneData USTRUCT (Version, SceneName, Timestamp, Actors[ClassPath, Location, Rotation, Scale])
|
||||
- SceneSerializer: Save/Load/Clear/GetSavedSceneNames
|
||||
- Files in Saved/PS_Editor/Scenes/{name}.json
|
||||
- UI: Save field + button, New Scene, saved scenes list with click-to-load
|
||||
- UI: Save field + button, New Scene, saved scenes list
|
||||
|
||||
## Materials (plugin Content/)
|
||||
- M_PS_Editor_Gizmo: Unlit, "Color" Vector Parameter, Translucent, Disable Depth Test
|
||||
- M_PS_Editor_SelectionOutline: auto-generated via C++ OnPostEngineInit
|
||||
|
||||
## Next: Phase 5 - Advanced Editing
|
||||
- Splines, lighting, AI characters, property editing
|
||||
- Splines, lighting, AI characters, property editing, duplication (Ctrl+D)
|
||||
|
||||
@@ -20,7 +20,7 @@ r.CustomDepth=3
|
||||
|
||||
r.RayTracing=True
|
||||
|
||||
r.Shadow.Virtual.Enable=1
|
||||
r.Shadow.Virtual.Enable=0
|
||||
|
||||
r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=True
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -20,6 +20,10 @@
|
||||
{
|
||||
"Name": "EnhancedInput",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "ProceduralMeshComponent",
|
||||
"Enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "Misc/MessageDialog.h"
|
||||
#include "PS_Editor_ConfirmDialog.h"
|
||||
|
||||
APS_Editor_Pawn::APS_Editor_Pawn()
|
||||
{
|
||||
@@ -160,15 +160,15 @@ void APS_Editor_Pawn::CreateInputActions()
|
||||
// Transform mode hotkeys (E/R/T)
|
||||
IA_TranslateMode = NewObject<UInputAction>(this, TEXT("IA_TranslateMode"));
|
||||
IA_TranslateMode->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::E);
|
||||
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::Z);
|
||||
|
||||
IA_RotateMode = NewObject<UInputAction>(this, TEXT("IA_RotateMode"));
|
||||
IA_RotateMode->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_RotateMode, EKeys::R);
|
||||
EditorMappingContext->MapKey(IA_RotateMode, EKeys::E);
|
||||
|
||||
IA_ScaleMode = NewObject<UInputAction>(this, TEXT("IA_ScaleMode"));
|
||||
IA_ScaleMode->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_ScaleMode, EKeys::T);
|
||||
EditorMappingContext->MapKey(IA_ScaleMode, EKeys::R);
|
||||
|
||||
IA_Delete = NewObject<UInputAction>(this, TEXT("IA_Delete"));
|
||||
IA_Delete->ValueType = EInputActionValueType::Boolean;
|
||||
@@ -186,6 +186,19 @@ void APS_Editor_Pawn::CreateInputActions()
|
||||
IA_Redo = NewObject<UInputAction>(this, TEXT("IA_Redo"));
|
||||
IA_Redo->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_Redo, EKeys::Y);
|
||||
|
||||
// Duplicate / Copy / Paste (Ctrl+D / Ctrl+C / Ctrl+V)
|
||||
IA_Duplicate = NewObject<UInputAction>(this, TEXT("IA_Duplicate"));
|
||||
IA_Duplicate->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_Duplicate, EKeys::D);
|
||||
|
||||
IA_Copy = NewObject<UInputAction>(this, TEXT("IA_Copy"));
|
||||
IA_Copy->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_Copy, EKeys::C);
|
||||
|
||||
IA_Paste = NewObject<UInputAction>(this, TEXT("IA_Paste"));
|
||||
IA_Paste->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_Paste, EKeys::V);
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||
@@ -226,6 +239,9 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp
|
||||
EIC->BindAction(IA_SnapToGround, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSnapToGround);
|
||||
EIC->BindAction(IA_Undo, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleUndo);
|
||||
EIC->BindAction(IA_Redo, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleRedo);
|
||||
EIC->BindAction(IA_Duplicate, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDuplicate);
|
||||
EIC->BindAction(IA_Copy, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCopy);
|
||||
EIC->BindAction(IA_Paste, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandlePaste);
|
||||
}
|
||||
|
||||
// ---- Input Handlers ----
|
||||
@@ -318,7 +334,8 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
|
||||
if (!Gizmo) return;
|
||||
|
||||
const FVector AxisDir = Gizmo->GetAxisDirection(ActiveGizmoAxis);
|
||||
// Use fixed axis direction from drag start (avoid feedback loop with local orientation updates)
|
||||
const FVector AxisDir = GizmoDragAxisDir;
|
||||
const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
|
||||
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
||||
|
||||
@@ -331,7 +348,14 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
if (!RayPlaneIntersection(RayOrigin, RayDir, GizmoDragPlaneOrigin, GizmoDragPlaneNormal, CurrentIntersection)) return;
|
||||
|
||||
const FVector TotalDelta = CurrentIntersection - GizmoDragInitialIntersection;
|
||||
const FVector ConstrainedDelta = FVector::DotProduct(TotalDelta, AxisDir) * AxisDir;
|
||||
float DisplacementAlongAxis = FVector::DotProduct(TotalDelta, AxisDir);
|
||||
|
||||
// Snap displacement along axis, not world position
|
||||
if (SM->bSnapTranslation && SM->TranslationSnapSize > 0.0f)
|
||||
{
|
||||
DisplacementAlongAxis = FMath::RoundToFloat(DisplacementAlongAxis / SM->TranslationSnapSize) * SM->TranslationSnapSize;
|
||||
}
|
||||
const FVector ConstrainedDelta = DisplacementAlongAxis * AxisDir;
|
||||
|
||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||
{
|
||||
@@ -343,14 +367,32 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
}
|
||||
else if (Mode == EPS_Editor_TransformMode::Rotate)
|
||||
{
|
||||
// Use combined mouse X+Y screen movement to drive rotation angle
|
||||
const float RotationSpeed = 1.0f;
|
||||
const float AngleDelta = (LookInput.X + LookInput.Y) * RotationSpeed;
|
||||
|
||||
// Accumulate total angle since drag start
|
||||
// Project the rotation axis onto screen space to determine correct mouse mapping.
|
||||
// This ensures consistent rotation direction from any camera angle.
|
||||
APlayerController* DragPC = Cast<APlayerController>(GetController());
|
||||
float AngleDelta = 0.0f;
|
||||
if (DragPC)
|
||||
{
|
||||
FVector2D ScreenStart, ScreenEnd;
|
||||
DragPC->ProjectWorldLocationToScreen(GizmoDragPlaneOrigin, ScreenStart);
|
||||
DragPC->ProjectWorldLocationToScreen(GizmoDragPlaneOrigin + AxisDir * 100.0f, ScreenEnd);
|
||||
|
||||
FVector2D ScreenAxisDir = (ScreenEnd - ScreenStart).GetSafeNormal();
|
||||
FVector2D ScreenPerp(-ScreenAxisDir.Y, ScreenAxisDir.X);
|
||||
AngleDelta = -(LookInput.X * ScreenPerp.X - LookInput.Y * ScreenPerp.Y) * RotationSpeed;
|
||||
}
|
||||
|
||||
GizmoDragAccumulator += AngleDelta;
|
||||
|
||||
const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(GizmoDragAccumulator));
|
||||
float SnapAngle = GizmoDragAccumulator;
|
||||
if (SM->bSnapRotation && SM->RotationSnapSize > 0.0f)
|
||||
{
|
||||
SnapAngle = FMath::RoundToFloat(SnapAngle / SM->RotationSnapSize) * SM->RotationSnapSize;
|
||||
}
|
||||
|
||||
const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(SnapAngle));
|
||||
const FVector GizmoLoc = GizmoDragPlaneOrigin;
|
||||
|
||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||
@@ -378,16 +420,33 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
if (AActor* Actor = Selected[i].Get())
|
||||
{
|
||||
FVector NewScale = GizmoDragInitialTransforms[i].GetScale3D();
|
||||
if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::X) NewScale.X *= ScaleFactor;
|
||||
if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::XYZ)
|
||||
{
|
||||
NewScale *= ScaleFactor; // Uniform scale
|
||||
}
|
||||
else if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::X) NewScale.X *= ScaleFactor;
|
||||
else if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::Y) NewScale.Y *= ScaleFactor;
|
||||
else if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::Z) NewScale.Z *= ScaleFactor;
|
||||
Actor->SetActorScale3D(NewScale);
|
||||
Actor->SetActorScale3D(SM->SnapScale(NewScale));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In translate mode, gizmo follows the selection. In rotate/scale, stay at initial position.
|
||||
if (Mode == EPS_Editor_TransformMode::Translate)
|
||||
{
|
||||
Gizmo->SetActorLocation(SM->GetSelectionPivot());
|
||||
}
|
||||
|
||||
// Update local orientation in real-time during rotation drag
|
||||
if (Mode == EPS_Editor_TransformMode::Rotate && SM->GetTransformSpace() == EPS_Editor_Space::Local)
|
||||
{
|
||||
if (AActor* FirstActor = Selected.Num() > 0 ? Selected[0].Get() : nullptr)
|
||||
{
|
||||
Gizmo->SetLocalOrientation(FirstActor->GetActorRotation());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value)
|
||||
@@ -495,6 +554,16 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
||||
}
|
||||
}
|
||||
ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
|
||||
|
||||
// Refresh gizmo orientation (local mode needs to track actor's new rotation)
|
||||
if (APS_Editor_PlayerController* PC2 = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM2 = PC2->GetSelectionManager())
|
||||
{
|
||||
SM2->RefreshGizmoOrientation();
|
||||
}
|
||||
}
|
||||
|
||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||
}
|
||||
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit)
|
||||
@@ -577,19 +646,27 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||
if (!SM || !SM->IsAnythingSelected()) return;
|
||||
|
||||
// Confirmation dialog
|
||||
// Show in-game confirmation dialog (runtime compatible)
|
||||
const int32 Count = SM->GetSelectedActors().Num();
|
||||
const FText Message = FText::Format(
|
||||
NSLOCTEXT("PS_Editor", "DeleteConfirm", "Delete {0} selected object(s)?"),
|
||||
FText::AsNumber(Count));
|
||||
|
||||
EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, Message);
|
||||
if (Result != EAppReturnType::Yes)
|
||||
UPS_Editor_ConfirmDialog* Dialog = CreateWidget<UPS_Editor_ConfirmDialog>(
|
||||
Cast<APlayerController>(GetController()), UPS_Editor_ConfirmDialog::StaticClass());
|
||||
|
||||
if (!Dialog) return;
|
||||
|
||||
// Weak refs to avoid dangling pointers in the callback
|
||||
TWeakObjectPtr<APS_Editor_PlayerController> WeakPC = EditorPC;
|
||||
|
||||
Dialog->Show(Message, FPS_Editor_OnConfirmResult::CreateLambda([WeakPC](bool bConfirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!bConfirmed || !WeakPC.IsValid()) return;
|
||||
|
||||
UPS_Editor_SelectionManager* SM = WeakPC->GetSelectionManager();
|
||||
if (!SM || !SM->IsAnythingSelected()) return;
|
||||
|
||||
// Soft-delete: hide actors instead of destroying (enables undo)
|
||||
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||
{
|
||||
@@ -602,12 +679,13 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
||||
SM->ClearSelection();
|
||||
Action->Execute();
|
||||
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
if (UPS_Editor_UndoManager* UM = WeakPC->GetUndoManager())
|
||||
{
|
||||
UM->RecordAction(Action);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Soft-deleted %d actor(s)"), Count);
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Soft-deleted %d actor(s)"), Action->Actors.Num());
|
||||
}));
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleUndo(const FInputActionValue& Value)
|
||||
@@ -646,6 +724,42 @@ void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleDuplicate(const FInputActionValue& Value)
|
||||
{
|
||||
if (!bCtrlHeld || CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||
{
|
||||
SM->DuplicateSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleCopy(const FInputActionValue& Value)
|
||||
{
|
||||
if (!bCtrlHeld || CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||
{
|
||||
SM->CopySelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandlePaste(const FInputActionValue& Value)
|
||||
{
|
||||
if (!bCtrlHeld || CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||
{
|
||||
SM->PasteClipboard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
||||
@@ -782,7 +896,9 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
|
||||
|
||||
ActiveGizmoAxis = Axis;
|
||||
|
||||
const FVector AxisDir = Gizmo->GetAxisDirection(Axis);
|
||||
// Store axis direction at drag start - used for entire drag duration
|
||||
GizmoDragAxisDir = Gizmo->GetAxisDirection(Axis);
|
||||
const FVector AxisDir = GizmoDragAxisDir;
|
||||
const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
|
||||
|
||||
FVector PlaneNormal;
|
||||
|
||||
@@ -171,6 +171,15 @@ private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Redo;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Duplicate;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Copy;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Paste;
|
||||
|
||||
// ---- Modifier key state ----
|
||||
bool bAltHeld = false;
|
||||
bool bCtrlHeld = false;
|
||||
@@ -185,6 +194,7 @@ private:
|
||||
FVector GizmoDragInitialIntersection = FVector::ZeroVector;
|
||||
TArray<FTransform> GizmoDragInitialTransforms;
|
||||
float GizmoDragAccumulator = 0.0f;
|
||||
FVector GizmoDragAxisDir = FVector::ZeroVector; // Fixed axis direction for the entire drag
|
||||
|
||||
// ---- Input Handlers ----
|
||||
void HandleMove(const FInputActionValue& Value);
|
||||
@@ -203,6 +213,9 @@ private:
|
||||
void HandleSnapToGround(const FInputActionValue& Value);
|
||||
void HandleUndo(const FInputActionValue& Value);
|
||||
void HandleRedo(const FInputActionValue& Value);
|
||||
void HandleDuplicate(const FInputActionValue& Value);
|
||||
void HandleCopy(const FInputActionValue& Value);
|
||||
void HandlePaste(const FInputActionValue& Value);
|
||||
|
||||
// ---- Helpers ----
|
||||
void CreateInputActions();
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "ProceduralMeshComponent.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Materials/MaterialInstanceDynamic.h"
|
||||
#include "UObject/ConstructorHelpers.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
|
||||
// ---- Shared component setup ----
|
||||
|
||||
static void ConfigureGizmoComponent(UPrimitiveComponent* 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);
|
||||
Comp->SetDepthPriorityGroup(SDPG_Foreground);
|
||||
}
|
||||
|
||||
// ---- Constructor ----
|
||||
|
||||
APS_Editor_Gizmo::APS_Editor_Gizmo()
|
||||
{
|
||||
@@ -12,39 +29,73 @@ APS_Editor_Gizmo::APS_Editor_Gizmo()
|
||||
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;
|
||||
|
||||
// ---- Translate ----
|
||||
TranslateRoot = CreateDefaultSubobject<USceneComponent>(TEXT("TranslateRoot"));
|
||||
TranslateRoot->SetupAttachment(GizmoRoot);
|
||||
|
||||
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);
|
||||
|
||||
// ---- Rotate (arcs created in BeginPlay via procedural mesh) ----
|
||||
RotateRoot = CreateDefaultSubobject<USceneComponent>(TEXT("RotateRoot"));
|
||||
RotateRoot->SetupAttachment(GizmoRoot);
|
||||
|
||||
Arc_X = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("Arc_X"));
|
||||
Arc_X->SetupAttachment(RotateRoot);
|
||||
ConfigureGizmoComponent(Arc_X);
|
||||
|
||||
Arc_Y = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("Arc_Y"));
|
||||
Arc_Y->SetupAttachment(RotateRoot);
|
||||
ConfigureGizmoComponent(Arc_Y);
|
||||
|
||||
Arc_Z = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("Arc_Z"));
|
||||
Arc_Z->SetupAttachment(RotateRoot);
|
||||
ConfigureGizmoComponent(Arc_Z);
|
||||
|
||||
// Guide lines (grey, from center to arc endpoints)
|
||||
GuideLine_A = CreateGuideLine(TEXT("GuideLine_A"), RotateRoot);
|
||||
GuideLine_B = CreateGuideLine(TEXT("GuideLine_B"), RotateRoot);
|
||||
GuideLine_C = CreateGuideLine(TEXT("GuideLine_C"), RotateRoot);
|
||||
|
||||
// ---- Scale ----
|
||||
ScaleRoot = CreateDefaultSubobject<USceneComponent>(TEXT("ScaleRoot"));
|
||||
ScaleRoot->SetupAttachment(GizmoRoot);
|
||||
|
||||
ScaleArrow_X = CreateArrow(TEXT("ScaleArrow_X"), ScaleRoot, RotToX);
|
||||
ScaleArrow_Y = CreateArrow(TEXT("ScaleArrow_Y"), ScaleRoot, RotToY);
|
||||
ScaleArrow_Z = CreateArrow(TEXT("ScaleArrow_Z"), ScaleRoot, RotToZ);
|
||||
ScaleCube_X = CreateCube(TEXT("ScaleCube_X"), ScaleRoot, RotToX);
|
||||
ScaleCube_Y = CreateCube(TEXT("ScaleCube_Y"), ScaleRoot, RotToY);
|
||||
ScaleCube_Z = CreateCube(TEXT("ScaleCube_Z"), ScaleRoot, RotToZ);
|
||||
|
||||
// Center cube for uniform scale
|
||||
ScaleCube_Center = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ScaleCube_Center"));
|
||||
ScaleCube_Center->SetupAttachment(ScaleRoot);
|
||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> CenterCubeMesh(TEXT("/Engine/BasicShapes/Cube"));
|
||||
if (CenterCubeMesh.Succeeded()) ScaleCube_Center->SetStaticMesh(CenterCubeMesh.Object);
|
||||
ScaleCube_Center->SetRelativeScale3D(FVector(0.08f, 0.08f, 0.08f));
|
||||
ScaleCube_Center->SetRelativeLocation(FVector::ZeroVector);
|
||||
ConfigureGizmoComponent(ScaleCube_Center);
|
||||
}
|
||||
|
||||
// ---- BeginPlay ----
|
||||
|
||||
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)
|
||||
// Load gizmo material
|
||||
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;
|
||||
@@ -54,54 +105,64 @@ void APS_Editor_Gizmo::BeginPlay()
|
||||
};
|
||||
|
||||
// UE editor gizmo colors
|
||||
Mat_X = MakeColorMat(FLinearColor(0.594f, 0.0197f, 0.0f)); // Dark red
|
||||
Mat_Y = MakeColorMat(FLinearColor(0.1255f, 0.372f, 0.0f)); // Dark green
|
||||
Mat_Z = MakeColorMat(FLinearColor(0.0392f, 0.0784f, 0.594f)); // Dark blue
|
||||
Mat_X = MakeColorMat(FLinearColor(0.594f, 0.0197f, 0.0f));
|
||||
Mat_Y = MakeColorMat(FLinearColor(0.1255f, 0.372f, 0.0f));
|
||||
Mat_Z = MakeColorMat(FLinearColor(0.0392f, 0.0784f, 0.594f));
|
||||
Mat_Highlight = MakeColorMat(FLinearColor::Yellow);
|
||||
|
||||
// Apply materials to shafts and cones
|
||||
auto ApplyToAxis = [](UStaticMeshComponent* Shaft, UStaticMeshComponent* Tip, UMaterialInstanceDynamic* Mat)
|
||||
// Apply materials to translate components
|
||||
auto ApplyMat = [](UPrimitiveComponent* Comp, UMaterialInstanceDynamic* Mat)
|
||||
{
|
||||
if (Mat)
|
||||
{
|
||||
if (Shaft) Shaft->SetMaterial(0, Mat);
|
||||
if (Tip) Tip->SetMaterial(0, Mat);
|
||||
}
|
||||
if (Comp && Mat) Comp->SetMaterial(0, Mat);
|
||||
};
|
||||
|
||||
ApplyToAxis(Arrow_X, Cone_X, Mat_X);
|
||||
ApplyToAxis(Arrow_Y, Cone_Y, Mat_Y);
|
||||
ApplyToAxis(Arrow_Z, Cone_Z, Mat_Z);
|
||||
ApplyMat(Arrow_X, Mat_X); ApplyMat(Cone_X, Mat_X);
|
||||
ApplyMat(Arrow_Y, Mat_Y); ApplyMat(Cone_Y, Mat_Y);
|
||||
ApplyMat(Arrow_Z, Mat_Z); ApplyMat(Cone_Z, Mat_Z);
|
||||
|
||||
// Generate arc geometry and apply materials
|
||||
// Grey material for guide lines
|
||||
UMaterialInstanceDynamic* Mat_Grey = MakeColorMat(FLinearColor(0.3f, 0.3f, 0.3f));
|
||||
ApplyMat(GuideLine_A, Mat_Grey);
|
||||
ApplyMat(GuideLine_B, Mat_Grey);
|
||||
ApplyMat(GuideLine_C, Mat_Grey);
|
||||
|
||||
// Arc geometry will be generated dynamically in UpdateArcFacing() based on camera direction
|
||||
LastCameraOctant = FVector(1, 1, 1);
|
||||
UpdateArcFacing();
|
||||
|
||||
ApplyMat(Arc_X, Mat_X);
|
||||
ApplyMat(Arc_Y, Mat_Y);
|
||||
ApplyMat(Arc_Z, Mat_Z);
|
||||
|
||||
// Apply materials to scale components
|
||||
ApplyMat(ScaleArrow_X, Mat_X); ApplyMat(ScaleCube_X, Mat_X);
|
||||
ApplyMat(ScaleArrow_Y, Mat_Y); ApplyMat(ScaleCube_Y, Mat_Y);
|
||||
ApplyMat(ScaleArrow_Z, Mat_Z); ApplyMat(ScaleCube_Z, Mat_Z);
|
||||
|
||||
// Grey center cube for uniform scale
|
||||
Mat_Center = MakeColorMat(FLinearColor(0.5f, 0.5f, 0.5f));
|
||||
ApplyMat(ScaleCube_Center, Mat_Center);
|
||||
|
||||
// Start hidden
|
||||
SetGizmoVisible(false);
|
||||
}
|
||||
|
||||
// ---- Tick ----
|
||||
|
||||
void APS_Editor_Gizmo::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
if (bGizmoVisible)
|
||||
{
|
||||
UpdateConstantScreenSize();
|
||||
if (CurrentMode == EPS_Editor_TransformMode::Rotate)
|
||||
{
|
||||
UpdateArcFacing();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// ---- Component creation helpers ----
|
||||
|
||||
UStaticMeshComponent* APS_Editor_Gizmo::CreateArrow(const FName& Name, USceneComponent* Parent, const FRotator& Rotation)
|
||||
{
|
||||
@@ -109,15 +170,10 @@ UStaticMeshComponent* APS_Editor_Gizmo::CreateArrow(const FName& Name, USceneCom
|
||||
Comp->SetupAttachment(Parent);
|
||||
|
||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> CylinderMesh(TEXT("/Engine/BasicShapes/Cylinder"));
|
||||
if (CylinderMesh.Succeeded())
|
||||
{
|
||||
Comp->SetStaticMesh(CylinderMesh.Object);
|
||||
}
|
||||
if (CylinderMesh.Succeeded()) Comp->SetStaticMesh(CylinderMesh.Object);
|
||||
|
||||
// Shaft: radius ~3 units, length ~60 units
|
||||
Comp->SetRelativeScale3D(FVector(0.06f, 0.06f, 0.6f));
|
||||
Comp->SetRelativeScale3D(FVector(0.03f, 0.03f, 0.6f));
|
||||
Comp->SetRelativeRotation(Rotation);
|
||||
|
||||
const FVector AxisDir = Rotation.RotateVector(FVector::UpVector);
|
||||
Comp->SetRelativeLocation(AxisDir * 30.0f);
|
||||
|
||||
@@ -131,14 +187,10 @@ UStaticMeshComponent* APS_Editor_Gizmo::CreateCone(const FName& Name, USceneComp
|
||||
Comp->SetupAttachment(Parent);
|
||||
|
||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> ConeMesh(TEXT("/Engine/BasicShapes/Cone"));
|
||||
if (ConeMesh.Succeeded())
|
||||
{
|
||||
Comp->SetStaticMesh(ConeMesh.Object);
|
||||
}
|
||||
if (ConeMesh.Succeeded()) Comp->SetStaticMesh(ConeMesh.Object);
|
||||
|
||||
Comp->SetRelativeScale3D(FVector(0.15f, 0.15f, 0.2f));
|
||||
Comp->SetRelativeScale3D(FVector(0.075f, 0.075f, 0.1f));
|
||||
Comp->SetRelativeRotation(Rotation);
|
||||
|
||||
const FVector AxisDir = Rotation.RotateVector(FVector::UpVector);
|
||||
Comp->SetRelativeLocation(AxisDir * 65.0f);
|
||||
|
||||
@@ -146,6 +198,120 @@ UStaticMeshComponent* APS_Editor_Gizmo::CreateCone(const FName& Name, USceneComp
|
||||
return Comp;
|
||||
}
|
||||
|
||||
UStaticMeshComponent* APS_Editor_Gizmo::CreateCube(const FName& Name, USceneComponent* Parent, const FRotator& Rotation)
|
||||
{
|
||||
UStaticMeshComponent* Comp = CreateDefaultSubobject<UStaticMeshComponent>(Name);
|
||||
Comp->SetupAttachment(Parent);
|
||||
|
||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeMesh(TEXT("/Engine/BasicShapes/Cube"));
|
||||
if (CubeMesh.Succeeded()) Comp->SetStaticMesh(CubeMesh.Object);
|
||||
|
||||
Comp->SetRelativeScale3D(FVector(0.072f, 0.072f, 0.072f));
|
||||
Comp->SetRelativeRotation(Rotation);
|
||||
const FVector AxisDir = Rotation.RotateVector(FVector::UpVector);
|
||||
Comp->SetRelativeLocation(AxisDir * 63.0f);
|
||||
|
||||
ConfigureGizmoComponent(Comp);
|
||||
return Comp;
|
||||
}
|
||||
|
||||
UStaticMeshComponent* APS_Editor_Gizmo::CreateGuideLine(const FName& Name, USceneComponent* Parent)
|
||||
{
|
||||
UStaticMeshComponent* Comp = CreateDefaultSubobject<UStaticMeshComponent>(Name);
|
||||
Comp->SetupAttachment(Parent);
|
||||
|
||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> CylinderMesh(TEXT("/Engine/BasicShapes/Cylinder"));
|
||||
if (CylinderMesh.Succeeded()) Comp->SetStaticMesh(CylinderMesh.Object);
|
||||
|
||||
// Very thin, length matches arc radius. Will be repositioned in UpdateArcFacing.
|
||||
Comp->SetRelativeScale3D(FVector(0.01f, 0.01f, 0.55f));
|
||||
ConfigureGizmoComponent(Comp);
|
||||
return Comp;
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::CreateArcRing(UProceduralMeshComponent* Mesh, const FVector& AxisNormal, const FVector& ArcStart, const FVector& ArcEnd, float Radius, float Width, int32 Segments)
|
||||
{
|
||||
// Generate a 3D solid quarter-arc (90 degrees) with rectangular cross-section.
|
||||
// ArcStart/ArcEnd define the directions the arc sweeps between (must be perpendicular to AxisNormal).
|
||||
|
||||
const float InnerRadius = Radius - Width * 0.5f;
|
||||
const float OuterRadius = Radius + Width * 0.5f;
|
||||
const float HalfDepth = Width * 0.3f;
|
||||
// Extend slightly past 90° to close gaps between connecting arcs
|
||||
const float StartAngle = FMath::DegreesToRadians(-2.0f);
|
||||
const float EndAngle = FMath::DegreesToRadians(92.0f);
|
||||
|
||||
const FVector AxisA = ArcStart.GetSafeNormal();
|
||||
const FVector AxisB = ArcEnd.GetSafeNormal();
|
||||
|
||||
TArray<FVector> Vertices;
|
||||
TArray<int32> Triangles;
|
||||
TArray<FVector> Normals;
|
||||
TArray<FVector2D> UVs;
|
||||
TArray<FColor> Colors;
|
||||
|
||||
// For each angle step, create 4 vertices (rectangular cross-section):
|
||||
// 0: inner-bottom, 1: outer-bottom, 2: outer-top, 3: inner-top
|
||||
for (int32 i = 0; i <= Segments; ++i)
|
||||
{
|
||||
const float Alpha = (float)i / (float)Segments;
|
||||
const float Angle = FMath::Lerp(StartAngle, EndAngle, Alpha);
|
||||
const FVector Dir = AxisA * FMath::Cos(Angle) + AxisB * FMath::Sin(Angle);
|
||||
const FVector Up = AxisNormal;
|
||||
|
||||
Vertices.Add(Dir * InnerRadius - Up * HalfDepth); // 0: inner-bottom
|
||||
Vertices.Add(Dir * OuterRadius - Up * HalfDepth); // 1: outer-bottom
|
||||
Vertices.Add(Dir * OuterRadius + Up * HalfDepth); // 2: outer-top
|
||||
Vertices.Add(Dir * InnerRadius + Up * HalfDepth); // 3: inner-top
|
||||
|
||||
// Normals (approximate - will be per-face below)
|
||||
Normals.Add(-Up); // bottom faces down
|
||||
Normals.Add(-Up);
|
||||
Normals.Add(Up); // top faces up
|
||||
Normals.Add(Up);
|
||||
|
||||
UVs.Add(FVector2D(Alpha, 0.0f));
|
||||
UVs.Add(FVector2D(Alpha, 0.33f));
|
||||
UVs.Add(FVector2D(Alpha, 0.66f));
|
||||
UVs.Add(FVector2D(Alpha, 1.0f));
|
||||
|
||||
Colors.Add(FColor::White);
|
||||
Colors.Add(FColor::White);
|
||||
Colors.Add(FColor::White);
|
||||
Colors.Add(FColor::White);
|
||||
}
|
||||
|
||||
// Generate quads between adjacent angle steps
|
||||
for (int32 i = 0; i < Segments; ++i)
|
||||
{
|
||||
const int32 A = i * 4; // Current step
|
||||
const int32 B = (i + 1) * 4; // Next step
|
||||
|
||||
// Top face (vertices 3,2 of current and next)
|
||||
Triangles.Append({ A+3, B+3, A+2, A+2, B+3, B+2 });
|
||||
|
||||
// Bottom face (vertices 0,1 of current and next)
|
||||
Triangles.Append({ A+0, A+1, B+0, B+0, A+1, B+1 });
|
||||
|
||||
// Outer face (vertices 1,2 of current and next)
|
||||
Triangles.Append({ A+1, A+2, B+1, B+1, A+2, B+2 });
|
||||
|
||||
// Inner face (vertices 0,3 of current and next)
|
||||
Triangles.Append({ A+0, B+0, A+3, A+3, B+0, B+3 });
|
||||
}
|
||||
|
||||
// Start cap (first angle step, quad 0-1-2-3)
|
||||
Triangles.Append({ 0, 1, 2, 0, 2, 3 });
|
||||
|
||||
// End cap (last angle step)
|
||||
const int32 Last = Segments * 4;
|
||||
Triangles.Append({ Last+0, Last+3, Last+2, Last+0, Last+2, Last+1 });
|
||||
|
||||
Mesh->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, Colors, TArray<FProcMeshTangent>(), true);
|
||||
}
|
||||
|
||||
// ---- Gizmo control ----
|
||||
|
||||
void APS_Editor_Gizmo::SetGizmoActive(bool bActive, FVector WorldLocation)
|
||||
{
|
||||
if (bActive)
|
||||
@@ -153,7 +319,6 @@ void APS_Editor_Gizmo::SetGizmoActive(bool bActive, FVector WorldLocation)
|
||||
SetActorLocation(WorldLocation);
|
||||
SetTransformMode(CurrentMode);
|
||||
}
|
||||
|
||||
SetGizmoVisible(bActive);
|
||||
SetActorEnableCollision(bActive);
|
||||
}
|
||||
@@ -161,34 +326,37 @@ void APS_Editor_Gizmo::SetGizmoActive(bool bActive, FVector WorldLocation)
|
||||
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);
|
||||
RotateRoot->SetVisibility(bVisible && CurrentMode == EPS_Editor_TransformMode::Rotate, true);
|
||||
ScaleRoot->SetVisibility(bVisible && CurrentMode == EPS_Editor_TransformMode::Scale, true);
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::SetTransformMode(EPS_Editor_TransformMode NewMode)
|
||||
{
|
||||
CurrentMode = NewMode;
|
||||
TranslateRoot->SetVisibility(bGizmoVisible && NewMode == EPS_Editor_TransformMode::Translate, true);
|
||||
RotateRoot->SetVisibility(bGizmoVisible && NewMode == EPS_Editor_TransformMode::Rotate, true);
|
||||
ScaleRoot->SetVisibility(bGizmoVisible && NewMode == EPS_Editor_TransformMode::Scale, true);
|
||||
}
|
||||
|
||||
// ---- Hit detection ----
|
||||
|
||||
EPS_Editor_GizmoAxis APS_Editor_Gizmo::GetAxisUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const
|
||||
{
|
||||
if (!bGizmoVisible)
|
||||
{
|
||||
return EPS_Editor_GizmoAxis::None;
|
||||
}
|
||||
if (!bGizmoVisible) return EPS_Editor_GizmoAxis::None;
|
||||
|
||||
const float SweepRadius = 8.0f * GetActorScale3D().X;
|
||||
float BestDistance = TNumericLimits<float>::Max();
|
||||
EPS_Editor_GizmoAxis BestAxis = EPS_Editor_GizmoAxis::None;
|
||||
|
||||
// Use a sphere sweep with a generous radius for easier clicking
|
||||
const float SweepRadius = 8.0f * GetActorScale3D().X;
|
||||
|
||||
auto TestComponent = [&](UStaticMeshComponent* Comp, EPS_Editor_GizmoAxis Axis)
|
||||
auto TestAxis = [&](EPS_Editor_GizmoAxis Axis)
|
||||
{
|
||||
if (!Comp) return;
|
||||
TArray<UPrimitiveComponent*> Components;
|
||||
GetComponentsForAxis(Axis, Components);
|
||||
|
||||
for (UPrimitiveComponent* Comp : Components)
|
||||
{
|
||||
if (!Comp || !Comp->IsVisible()) continue;
|
||||
|
||||
FHitResult Hit;
|
||||
const FVector RayEnd = RayOrigin + RayDirection * 100000.0f;
|
||||
@@ -202,37 +370,44 @@ EPS_Editor_GizmoAxis APS_Editor_Gizmo::GetAxisUnderCursor(const FVector& RayOrig
|
||||
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);
|
||||
TestAxis(EPS_Editor_GizmoAxis::X);
|
||||
TestAxis(EPS_Editor_GizmoAxis::Y);
|
||||
TestAxis(EPS_Editor_GizmoAxis::Z);
|
||||
TestAxis(EPS_Editor_GizmoAxis::XYZ); // Center cube for uniform scale
|
||||
|
||||
return BestAxis;
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::GetComponentsForAxis(EPS_Editor_GizmoAxis Axis, TArray<UPrimitiveComponent*>& OutComponents) const
|
||||
{
|
||||
switch (Axis)
|
||||
{
|
||||
case EPS_Editor_GizmoAxis::X:
|
||||
OutComponents.Append({ Arrow_X, Cone_X, Arc_X, ScaleArrow_X, ScaleCube_X });
|
||||
break;
|
||||
case EPS_Editor_GizmoAxis::Y:
|
||||
OutComponents.Append({ Arrow_Y, Cone_Y, Arc_Y, ScaleArrow_Y, ScaleCube_Y });
|
||||
break;
|
||||
case EPS_Editor_GizmoAxis::Z:
|
||||
OutComponents.Append({ Arrow_Z, Cone_Z, Arc_Z, ScaleArrow_Z, ScaleCube_Z });
|
||||
break;
|
||||
case EPS_Editor_GizmoAxis::XYZ:
|
||||
OutComponents.Add(ScaleCube_Center);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Highlighting ----
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -242,29 +417,144 @@ void APS_Editor_Gizmo::SetHighlightedAxis(EPS_Editor_GizmoAxis Axis)
|
||||
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;
|
||||
case EPS_Editor_GizmoAxis::XYZ: OrigMat = Mat_Center; break;
|
||||
default: break;
|
||||
}
|
||||
SetAxisMat(HighlightedAxis, OrigMat);
|
||||
SetAxisMaterial(HighlightedAxis, OrigMat);
|
||||
}
|
||||
|
||||
HighlightedAxis = Axis;
|
||||
|
||||
// Highlight new
|
||||
if (Axis != EPS_Editor_GizmoAxis::None)
|
||||
{
|
||||
SetAxisMat(Axis, Mat_Highlight);
|
||||
SetAxisMaterial(Axis, Mat_Highlight);
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::SetAxisMaterial(EPS_Editor_GizmoAxis Axis, UMaterialInstanceDynamic* Mat)
|
||||
{
|
||||
if (!Mat) return;
|
||||
|
||||
TArray<UPrimitiveComponent*> Components;
|
||||
GetComponentsForAxis(Axis, Components);
|
||||
|
||||
for (UPrimitiveComponent* Comp : Components)
|
||||
{
|
||||
if (Comp) Comp->SetMaterial(0, Mat);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
void APS_Editor_Gizmo::UpdateArcFacing()
|
||||
{
|
||||
APlayerController* PC = GetWorld()->GetFirstPlayerController();
|
||||
if (!PC) return;
|
||||
|
||||
FVector CameraLocation;
|
||||
FRotator CameraRotation;
|
||||
PC->GetPlayerViewPoint(CameraLocation, CameraRotation);
|
||||
|
||||
// Compute local axes (world axes rotated by LocalOrientation)
|
||||
const FVector LocalX = LocalOrientation.RotateVector(FVector::ForwardVector);
|
||||
const FVector LocalY = LocalOrientation.RotateVector(FVector::RightVector);
|
||||
const FVector LocalZ = LocalOrientation.RotateVector(FVector::UpVector);
|
||||
|
||||
// Direction from gizmo to camera in world space
|
||||
const FVector CamDir = (CameraLocation - GetActorLocation()).GetSafeNormal();
|
||||
|
||||
// Project camera onto local axes to determine octant
|
||||
const FVector NewOctant(
|
||||
FVector::DotProduct(CamDir, LocalX) >= 0 ? 1.0f : -1.0f,
|
||||
FVector::DotProduct(CamDir, LocalY) >= 0 ? 1.0f : -1.0f,
|
||||
FVector::DotProduct(CamDir, LocalZ) >= 0 ? 1.0f : -1.0f
|
||||
);
|
||||
|
||||
if (NewOctant.Equals(LastCameraOctant))
|
||||
{
|
||||
return;
|
||||
}
|
||||
LastCameraOctant = NewOctant;
|
||||
|
||||
// Arc directions in world space using local axes
|
||||
const FVector ArcX_Start = LocalY * NewOctant.Y;
|
||||
const FVector ArcX_End = LocalZ * NewOctant.Z;
|
||||
|
||||
const FVector ArcY_Start = LocalX * NewOctant.X;
|
||||
const FVector ArcY_End = LocalZ * NewOctant.Z;
|
||||
|
||||
const FVector ArcZ_Start = LocalX * NewOctant.X;
|
||||
const FVector ArcZ_End = LocalY * NewOctant.Y;
|
||||
|
||||
CreateArcRing(Arc_X, LocalX, ArcX_Start, ArcX_End, ArcRadius, ArcWidth, ArcSegments);
|
||||
CreateArcRing(Arc_Y, LocalY, ArcY_Start, ArcY_End, ArcRadius, ArcWidth, ArcSegments);
|
||||
CreateArcRing(Arc_Z, LocalZ, ArcZ_Start, ArcZ_End, ArcRadius, ArcWidth, ArcSegments);
|
||||
|
||||
// Re-apply materials
|
||||
if (Mat_X) Arc_X->SetMaterial(0, Mat_X);
|
||||
if (Mat_Y) Arc_Y->SetMaterial(0, Mat_Y);
|
||||
if (Mat_Z) Arc_Z->SetMaterial(0, Mat_Z);
|
||||
|
||||
if (HighlightedAxis != EPS_Editor_GizmoAxis::None)
|
||||
{
|
||||
SetAxisMaterial(HighlightedAxis, Mat_Highlight);
|
||||
}
|
||||
|
||||
// Guide lines toward arc connection points (world space)
|
||||
auto PositionGuideLine = [](UStaticMeshComponent* Line, const FVector& Dir)
|
||||
{
|
||||
if (!Line) return;
|
||||
Line->SetRelativeRotation(FRotationMatrix::MakeFromZ(Dir).Rotator());
|
||||
Line->SetRelativeLocation(Dir * 27.5f);
|
||||
};
|
||||
|
||||
PositionGuideLine(GuideLine_A, LocalX * NewOctant.X);
|
||||
PositionGuideLine(GuideLine_B, LocalY * NewOctant.Y);
|
||||
PositionGuideLine(GuideLine_C, LocalZ * NewOctant.Z);
|
||||
|
||||
// Re-apply materials (mesh sections are recreated)
|
||||
if (Mat_X) Arc_X->SetMaterial(0, Mat_X);
|
||||
if (Mat_Y) Arc_Y->SetMaterial(0, Mat_Y);
|
||||
if (Mat_Z) Arc_Z->SetMaterial(0, Mat_Z);
|
||||
|
||||
// Restore highlight if needed
|
||||
if (HighlightedAxis != EPS_Editor_GizmoAxis::None)
|
||||
{
|
||||
SetAxisMaterial(HighlightedAxis, Mat_Highlight);
|
||||
}
|
||||
}
|
||||
|
||||
FVector APS_Editor_Gizmo::GetAxisDirection(EPS_Editor_GizmoAxis Axis) const
|
||||
{
|
||||
FVector LocalDir;
|
||||
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;
|
||||
case EPS_Editor_GizmoAxis::X: LocalDir = FVector::ForwardVector; break;
|
||||
case EPS_Editor_GizmoAxis::Y: LocalDir = FVector::RightVector; break;
|
||||
case EPS_Editor_GizmoAxis::Z: LocalDir = FVector::UpVector; break;
|
||||
default: return FVector::ZeroVector;
|
||||
}
|
||||
|
||||
// Return axis in local space if orientation is set
|
||||
if (!LocalOrientation.IsNearlyZero())
|
||||
{
|
||||
return LocalOrientation.RotateVector(LocalDir);
|
||||
}
|
||||
|
||||
return LocalDir;
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::SetLocalOrientation(FRotator ActorRotation)
|
||||
{
|
||||
LocalOrientation = ActorRotation;
|
||||
|
||||
if (TranslateRoot) TranslateRoot->SetRelativeRotation(ActorRotation);
|
||||
// RotateRoot stays at identity - arcs are generated in world space using LocalOrientation
|
||||
if (RotateRoot) RotateRoot->SetRelativeRotation(FRotator::ZeroRotator);
|
||||
if (ScaleRoot) ScaleRoot->SetRelativeRotation(ActorRotation);
|
||||
|
||||
// Force arc regeneration
|
||||
LastCameraOctant = FVector::ZeroVector;
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::UpdateConstantScreenSize()
|
||||
@@ -282,22 +572,3 @@ void APS_Editor_Gizmo::UpdateConstantScreenSize()
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
#include "PS_Editor_Gizmo.generated.h"
|
||||
|
||||
class UStaticMeshComponent;
|
||||
class UProceduralMeshComponent;
|
||||
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).
|
||||
* - Translate: arrows (cylinders + cones)
|
||||
* - Rotate: quarter-arc rings (procedural mesh)
|
||||
* - Scale: arrows (cylinders + cubes)
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API APS_Editor_Gizmo : public AActor
|
||||
@@ -25,21 +25,17 @@ public:
|
||||
|
||||
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;
|
||||
|
||||
/** Set the orientation for local space mode (matches selected actor's rotation). */
|
||||
void SetLocalOrientation(FRotator ActorRotation);
|
||||
|
||||
/** Stored local orientation for axis direction computation. */
|
||||
FRotator LocalOrientation = FRotator::ZeroRotator;
|
||||
EPS_Editor_TransformMode GetTransformMode() const { return CurrentMode; }
|
||||
|
||||
protected:
|
||||
@@ -50,53 +46,101 @@ private:
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<USceneComponent> GizmoRoot;
|
||||
|
||||
// ---- Translate handles ----
|
||||
// ---- Translate components ----
|
||||
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;
|
||||
|
||||
// ---- Rotate components (quarter-arc rings) ----
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<USceneComponent> RotateRoot;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UProceduralMeshComponent> Arc_X;
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UProceduralMeshComponent> Arc_Y;
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UProceduralMeshComponent> Arc_Z;
|
||||
|
||||
// ---- Rotate guide lines (grey lines from center to arc endpoints) ----
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> GuideLine_A;
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> GuideLine_B;
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> GuideLine_C;
|
||||
|
||||
// ---- Scale components ----
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<USceneComponent> ScaleRoot;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> ScaleArrow_X;
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> ScaleArrow_Y;
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> ScaleArrow_Z;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> ScaleCube_X;
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> ScaleCube_Y;
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> ScaleCube_Z;
|
||||
|
||||
/** Center cube for uniform scaling. */
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> ScaleCube_Center;
|
||||
|
||||
// ---- Materials ----
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_X;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Y;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Z;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Highlight;
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Center;
|
||||
|
||||
// ---- 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);
|
||||
UStaticMeshComponent* CreateCube(const FName& Name, USceneComponent* Parent, const FRotator& Rotation);
|
||||
void CreateArcRing(UProceduralMeshComponent* Mesh, const FVector& AxisNormal, const FVector& ArcStart, const FVector& ArcEnd, float Radius, float Width, int32 Segments);
|
||||
UStaticMeshComponent* CreateGuideLine(const FName& Name, USceneComponent* Parent);
|
||||
void UpdateConstantScreenSize();
|
||||
void SetGizmoVisible(bool bVisible);
|
||||
UStaticMeshComponent* GetComponentForAxis(EPS_Editor_GizmoAxis Axis) const;
|
||||
void SetComponentMaterial(UStaticMeshComponent* Comp, UMaterialInstanceDynamic* Mat);
|
||||
void UpdateArcFacing();
|
||||
|
||||
// Arc parameters (stored for regeneration)
|
||||
float ArcRadius = 55.0f;
|
||||
float ArcWidth = 5.0f;
|
||||
int32 ArcSegments = 24;
|
||||
FVector LastCameraOctant = FVector::ZeroVector;
|
||||
|
||||
/** Get all components for a given axis (across all modes) for hit testing. */
|
||||
void GetComponentsForAxis(EPS_Editor_GizmoAxis Axis, TArray<UPrimitiveComponent*>& OutComponents) const;
|
||||
|
||||
void SetAxisMaterial(EPS_Editor_GizmoAxis Axis, UMaterialInstanceDynamic* Mat);
|
||||
};
|
||||
|
||||
@@ -27,7 +27,8 @@ public class PS_Editor : ModuleRules
|
||||
"EnhancedInput",
|
||||
"UMG",
|
||||
"Json",
|
||||
"JsonUtilities"
|
||||
"JsonUtilities",
|
||||
"ProceduralMeshComponent"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
|
||||
@@ -1,10 +1,232 @@
|
||||
#include "PS_Editor.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "Materials/Material.h"
|
||||
#include "MaterialDomain.h"
|
||||
#include "Materials/MaterialExpressionSceneTexture.h"
|
||||
#include "Materials/MaterialExpressionAdd.h"
|
||||
#include "Materials/MaterialExpressionSubtract.h"
|
||||
#include "Materials/MaterialExpressionMultiply.h"
|
||||
#include "Materials/MaterialExpressionAbs.h"
|
||||
#include "Materials/MaterialExpressionSaturate.h"
|
||||
#include "Materials/MaterialExpressionConstant.h"
|
||||
#include "Materials/MaterialExpressionConstant4Vector.h"
|
||||
#include "Materials/MaterialExpressionTextureCoordinate.h"
|
||||
#include "Materials/MaterialExpressionLinearInterpolate.h"
|
||||
#include "Materials/MaterialExpressionComponentMask.h"
|
||||
#include "Materials/MaterialExpressionAppendVector.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "UObject/SavePackage.h"
|
||||
#include "Misc/PackageName.h"
|
||||
#endif
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FPS_EditorModule"
|
||||
|
||||
#if WITH_EDITOR
|
||||
static void CreateOutlineMaterialAsset()
|
||||
{
|
||||
const FString MaterialPath = TEXT("/PS_Editor/M_PS_Editor_SelectionOutline");
|
||||
const FString AssetName = TEXT("M_PS_Editor_SelectionOutline");
|
||||
|
||||
if (LoadObject<UMaterial>(nullptr, *(MaterialPath + TEXT(".") + AssetName)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UPackage* Package = CreatePackage(*MaterialPath);
|
||||
UMaterial* Mat = NewObject<UMaterial>(Package, *AssetName, RF_Public | RF_Standalone);
|
||||
Mat->MaterialDomain = EMaterialDomain::MD_PostProcess;
|
||||
Mat->BlendableLocation = BL_SceneColorAfterDOF; // Before TAA, matches stencil buffer resolution
|
||||
|
||||
auto AddExpr = [&](UMaterialExpression* Expr) { Mat->GetExpressionCollection().AddExpression(Expr); };
|
||||
|
||||
const float ThicknessPixels = 1.0f;
|
||||
|
||||
// ---- TexCoord ----
|
||||
UMaterialExpressionTextureCoordinate* TexCoord = NewObject<UMaterialExpressionTextureCoordinate>(Mat);
|
||||
AddExpr(TexCoord);
|
||||
TexCoord->MaterialExpressionEditorX = -1600;
|
||||
TexCoord->MaterialExpressionEditorY = 400;
|
||||
|
||||
// ---- Center stencil sample ----
|
||||
UMaterialExpressionSceneTexture* StencilCenter = NewObject<UMaterialExpressionSceneTexture>(Mat);
|
||||
StencilCenter->SceneTextureId = PPI_CustomStencil;
|
||||
AddExpr(StencilCenter);
|
||||
StencilCenter->Coordinates.Connect(0, TexCoord);
|
||||
StencilCenter->MaterialExpressionEditorX = -1200;
|
||||
StencilCenter->MaterialExpressionEditorY = 0;
|
||||
|
||||
// ---- Get InvSize from SceneTexture (output index 2) = 1/resolution ----
|
||||
// This gives dynamic pixel size regardless of resolution/aspect ratio
|
||||
UMaterialExpressionMultiply* PixelOffset = NewObject<UMaterialExpressionMultiply>(Mat);
|
||||
AddExpr(PixelOffset);
|
||||
PixelOffset->A.Connect(2, StencilCenter); // Output 2 = InvSize (float2)
|
||||
PixelOffset->ConstB = ThicknessPixels;
|
||||
PixelOffset->MaterialExpressionEditorX = -1000;
|
||||
PixelOffset->MaterialExpressionEditorY = 200;
|
||||
|
||||
// Split into X and Y components
|
||||
UMaterialExpressionComponentMask* OffsetX = NewObject<UMaterialExpressionComponentMask>(Mat);
|
||||
AddExpr(OffsetX);
|
||||
OffsetX->Input.Connect(0, PixelOffset);
|
||||
OffsetX->R = true; OffsetX->G = false; OffsetX->B = false; OffsetX->A = false;
|
||||
OffsetX->MaterialExpressionEditorX = -800;
|
||||
OffsetX->MaterialExpressionEditorY = 100;
|
||||
|
||||
UMaterialExpressionComponentMask* OffsetY = NewObject<UMaterialExpressionComponentMask>(Mat);
|
||||
AddExpr(OffsetY);
|
||||
OffsetY->Input.Connect(0, PixelOffset);
|
||||
OffsetY->R = false; OffsetY->G = true; OffsetY->B = false; OffsetY->A = false;
|
||||
OffsetY->MaterialExpressionEditorX = -800;
|
||||
OffsetY->MaterialExpressionEditorY = 300;
|
||||
|
||||
UMaterialExpressionConstant* ZeroConst = NewObject<UMaterialExpressionConstant>(Mat);
|
||||
ZeroConst->R = 0.0f;
|
||||
AddExpr(ZeroConst);
|
||||
ZeroConst->MaterialExpressionEditorX = -800;
|
||||
ZeroConst->MaterialExpressionEditorY = 500;
|
||||
|
||||
// Negative offsets
|
||||
UMaterialExpressionMultiply* NegX = NewObject<UMaterialExpressionMultiply>(Mat);
|
||||
AddExpr(NegX);
|
||||
NegX->A.Connect(0, OffsetX);
|
||||
NegX->ConstB = -1.0f;
|
||||
NegX->MaterialExpressionEditorX = -800;
|
||||
NegX->MaterialExpressionEditorY = 0;
|
||||
|
||||
UMaterialExpressionMultiply* NegY = NewObject<UMaterialExpressionMultiply>(Mat);
|
||||
AddExpr(NegY);
|
||||
NegY->A.Connect(0, OffsetY);
|
||||
NegY->ConstB = -1.0f;
|
||||
NegY->MaterialExpressionEditorX = -800;
|
||||
NegY->MaterialExpressionEditorY = 400;
|
||||
|
||||
// ---- 8 direction samples ----
|
||||
struct FDirDef { UMaterialExpression* XE; UMaterialExpression* YE; };
|
||||
FDirDef Dirs[8] = {
|
||||
{ OffsetX, ZeroConst }, // right
|
||||
{ NegX, ZeroConst }, // left
|
||||
{ ZeroConst, OffsetY }, // down
|
||||
{ ZeroConst, NegY }, // up
|
||||
{ OffsetX, OffsetY }, // bottom-right
|
||||
{ NegX, OffsetY }, // bottom-left
|
||||
{ OffsetX, NegY }, // top-right
|
||||
{ NegX, NegY }, // top-left
|
||||
};
|
||||
|
||||
UMaterialExpression* EdgeAccum = nullptr;
|
||||
|
||||
for (int32 i = 0; i < 8; ++i)
|
||||
{
|
||||
int32 PosY = 100 + i * 160;
|
||||
|
||||
// Append(X, Y) -> float2 offset
|
||||
UMaterialExpressionAppendVector* OffsetVec = NewObject<UMaterialExpressionAppendVector>(Mat);
|
||||
AddExpr(OffsetVec);
|
||||
OffsetVec->A.Connect(0, Dirs[i].XE);
|
||||
OffsetVec->B.Connect(0, Dirs[i].YE);
|
||||
OffsetVec->MaterialExpressionEditorX = -600;
|
||||
OffsetVec->MaterialExpressionEditorY = PosY;
|
||||
|
||||
// TexCoord + offset
|
||||
UMaterialExpressionAdd* UVAdd = NewObject<UMaterialExpressionAdd>(Mat);
|
||||
AddExpr(UVAdd);
|
||||
UVAdd->A.Connect(0, TexCoord);
|
||||
UVAdd->B.Connect(0, OffsetVec);
|
||||
UVAdd->MaterialExpressionEditorX = -400;
|
||||
UVAdd->MaterialExpressionEditorY = PosY;
|
||||
|
||||
// Sample stencil at offset
|
||||
UMaterialExpressionSceneTexture* StencilN = NewObject<UMaterialExpressionSceneTexture>(Mat);
|
||||
StencilN->SceneTextureId = PPI_CustomStencil;
|
||||
AddExpr(StencilN);
|
||||
StencilN->Coordinates.Connect(0, UVAdd);
|
||||
StencilN->MaterialExpressionEditorX = -200;
|
||||
StencilN->MaterialExpressionEditorY = PosY;
|
||||
|
||||
// abs(center - neighbor)
|
||||
UMaterialExpressionSubtract* Sub = NewObject<UMaterialExpressionSubtract>(Mat);
|
||||
AddExpr(Sub);
|
||||
Sub->A.Connect(0, StencilCenter);
|
||||
Sub->B.Connect(0, StencilN);
|
||||
Sub->MaterialExpressionEditorX = 0;
|
||||
Sub->MaterialExpressionEditorY = PosY;
|
||||
|
||||
UMaterialExpressionAbs* AbsN = NewObject<UMaterialExpressionAbs>(Mat);
|
||||
AddExpr(AbsN);
|
||||
AbsN->Input.Connect(0, Sub);
|
||||
AbsN->MaterialExpressionEditorX = 200;
|
||||
AbsN->MaterialExpressionEditorY = PosY;
|
||||
|
||||
if (!EdgeAccum)
|
||||
{
|
||||
EdgeAccum = AbsN;
|
||||
}
|
||||
else
|
||||
{
|
||||
UMaterialExpressionAdd* AccumAdd = NewObject<UMaterialExpressionAdd>(Mat);
|
||||
AddExpr(AccumAdd);
|
||||
AccumAdd->A.Connect(0, EdgeAccum);
|
||||
AccumAdd->B.Connect(0, AbsN);
|
||||
AccumAdd->MaterialExpressionEditorX = 400;
|
||||
AccumAdd->MaterialExpressionEditorY = PosY;
|
||||
EdgeAccum = AccumAdd;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Saturate edge mask ----
|
||||
UMaterialExpressionSaturate* Saturate = NewObject<UMaterialExpressionSaturate>(Mat);
|
||||
AddExpr(Saturate);
|
||||
Saturate->Input.Connect(0, EdgeAccum);
|
||||
Saturate->MaterialExpressionEditorX = 600;
|
||||
Saturate->MaterialExpressionEditorY = 500;
|
||||
|
||||
// ---- Outline color (orange, float4) ----
|
||||
UMaterialExpressionConstant4Vector* OutlineColor = NewObject<UMaterialExpressionConstant4Vector>(Mat);
|
||||
OutlineColor->Constant = FColor(0xAF, 0x15, 0x00, 0xFF).ReinterpretAsLinear();
|
||||
AddExpr(OutlineColor);
|
||||
OutlineColor->MaterialExpressionEditorX = 600;
|
||||
OutlineColor->MaterialExpressionEditorY = 700;
|
||||
|
||||
// ---- Scene input ----
|
||||
UMaterialExpressionSceneTexture* SceneInput = NewObject<UMaterialExpressionSceneTexture>(Mat);
|
||||
SceneInput->SceneTextureId = PPI_PostProcessInput0;
|
||||
AddExpr(SceneInput);
|
||||
SceneInput->MaterialExpressionEditorX = 600;
|
||||
SceneInput->MaterialExpressionEditorY = 300;
|
||||
|
||||
// ---- Lerp(Scene, OutlineColor, EdgeMask) ----
|
||||
UMaterialExpressionLinearInterpolate* Lerp = NewObject<UMaterialExpressionLinearInterpolate>(Mat);
|
||||
AddExpr(Lerp);
|
||||
Lerp->A.Connect(0, SceneInput);
|
||||
Lerp->B.Connect(0, OutlineColor);
|
||||
Lerp->Alpha.Connect(0, Saturate);
|
||||
Lerp->MaterialExpressionEditorX = 800;
|
||||
Lerp->MaterialExpressionEditorY = 500;
|
||||
|
||||
// ---- Output ----
|
||||
Mat->GetEditorOnlyData()->EmissiveColor.Connect(0, Lerp);
|
||||
|
||||
Mat->PreEditChange(nullptr);
|
||||
Mat->PostEditChange();
|
||||
|
||||
FString PackageFilename = FPackageName::LongPackageNameToFilename(MaterialPath, FPackageName::GetAssetPackageExtension());
|
||||
FSavePackageArgs SaveArgs;
|
||||
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
||||
UPackage::SavePackage(Package, Mat, *PackageFilename, SaveArgs);
|
||||
FAssetRegistryModule::AssetCreated(Mat);
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material auto-generated (8-sample, dynamic resolution)"));
|
||||
}
|
||||
#endif
|
||||
|
||||
void FPS_EditorModule::StartupModule()
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor module started."));
|
||||
|
||||
#if WITH_EDITOR
|
||||
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateOutlineMaterialAsset);
|
||||
#endif
|
||||
}
|
||||
|
||||
void FPS_EditorModule::ShutdownModule()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
@@ -194,15 +195,93 @@ FVector UPS_Editor_SelectionManager::GetSelectionPivot() const
|
||||
void UPS_Editor_SelectionManager::SetTransformMode(EPS_Editor_TransformMode NewMode)
|
||||
{
|
||||
CurrentTransformMode = NewMode;
|
||||
if (Gizmo)
|
||||
{
|
||||
Gizmo->SetTransformMode(NewMode);
|
||||
|
||||
// Update gizmo orientation: Scale always local, others depend on space setting
|
||||
if (IsAnythingSelected())
|
||||
{
|
||||
if (AActor* Actor = SelectedActors[0].Get())
|
||||
{
|
||||
bool bUseLocal = (NewMode == EPS_Editor_TransformMode::Scale) || (CurrentSpace == EPS_Editor_Space::Local);
|
||||
Gizmo->SetLocalOrientation(bUseLocal ? Actor->GetActorRotation() : FRotator::ZeroRotator);
|
||||
}
|
||||
}
|
||||
}
|
||||
OnSelectionChanged.Broadcast();
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::SetTransformSpace(EPS_Editor_Space NewSpace)
|
||||
{
|
||||
CurrentSpace = NewSpace;
|
||||
|
||||
// Update gizmo orientation based on space
|
||||
if (Gizmo && IsAnythingSelected())
|
||||
{
|
||||
if (NewSpace == EPS_Editor_Space::Local)
|
||||
{
|
||||
if (AActor* Actor = SelectedActors[0].Get())
|
||||
{
|
||||
Gizmo->SetLocalOrientation(Actor->GetActorRotation());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Gizmo->SetLocalOrientation(FRotator::ZeroRotator);
|
||||
}
|
||||
}
|
||||
|
||||
OnSelectionChanged.Broadcast();
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::RefreshGizmoOrientation()
|
||||
{
|
||||
if (!Gizmo || !IsAnythingSelected()) return;
|
||||
|
||||
if (AActor* Actor = SelectedActors[0].Get())
|
||||
{
|
||||
bool bUseLocal = (CurrentTransformMode == EPS_Editor_TransformMode::Scale) || (CurrentSpace == EPS_Editor_Space::Local);
|
||||
Gizmo->SetLocalOrientation(bUseLocal ? Actor->GetActorRotation() : FRotator::ZeroRotator);
|
||||
Gizmo->SetActorLocation(GetSelectionPivot());
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::ToggleTransformSpace()
|
||||
{
|
||||
SetTransformSpace(CurrentSpace == EPS_Editor_Space::World ? EPS_Editor_Space::Local : EPS_Editor_Space::World);
|
||||
}
|
||||
|
||||
FVector UPS_Editor_SelectionManager::SnapPosition(const FVector& Position) const
|
||||
{
|
||||
if (!bSnapTranslation || TranslationSnapSize <= 0.0f) return Position;
|
||||
return FVector(
|
||||
FMath::RoundToFloat(Position.X / TranslationSnapSize) * TranslationSnapSize,
|
||||
FMath::RoundToFloat(Position.Y / TranslationSnapSize) * TranslationSnapSize,
|
||||
FMath::RoundToFloat(Position.Z / TranslationSnapSize) * TranslationSnapSize
|
||||
);
|
||||
}
|
||||
|
||||
FRotator UPS_Editor_SelectionManager::SnapRotation(const FRotator& Rotation) const
|
||||
{
|
||||
if (!bSnapRotation || RotationSnapSize <= 0.0f) return Rotation;
|
||||
return FRotator(
|
||||
FMath::RoundToFloat(Rotation.Pitch / RotationSnapSize) * RotationSnapSize,
|
||||
FMath::RoundToFloat(Rotation.Yaw / RotationSnapSize) * RotationSnapSize,
|
||||
FMath::RoundToFloat(Rotation.Roll / RotationSnapSize) * RotationSnapSize
|
||||
);
|
||||
}
|
||||
|
||||
FVector UPS_Editor_SelectionManager::SnapScale(const FVector& Scale) const
|
||||
{
|
||||
if (!bSnapScale || ScaleSnapSize <= 0.0f) return Scale;
|
||||
return FVector(
|
||||
FMath::RoundToFloat(Scale.X / ScaleSnapSize) * ScaleSnapSize,
|
||||
FMath::RoundToFloat(Scale.Y / ScaleSnapSize) * ScaleSnapSize,
|
||||
FMath::RoundToFloat(Scale.Z / ScaleSnapSize) * ScaleSnapSize
|
||||
);
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::SetActorHighlight(AActor* Actor, bool bHighlight)
|
||||
{
|
||||
if (!Actor)
|
||||
@@ -285,6 +364,13 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
|
||||
if (IsAnythingSelected())
|
||||
{
|
||||
Gizmo->SetGizmoActive(true, GetSelectionPivot());
|
||||
|
||||
// Update gizmo orientation
|
||||
if (AActor* Actor = SelectedActors[0].Get())
|
||||
{
|
||||
bool bUseLocal = (CurrentTransformMode == EPS_Editor_TransformMode::Scale) || (CurrentSpace == EPS_Editor_Space::Local);
|
||||
Gizmo->SetLocalOrientation(bUseLocal ? Actor->GetActorRotation() : FRotator::ZeroRotator);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -292,6 +378,168 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::DuplicateSelection()
|
||||
{
|
||||
APlayerController* PC = OwnerPC.Get();
|
||||
if (!PC || !IsAnythingSelected()) return;
|
||||
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC);
|
||||
if (!EditorPC) return;
|
||||
|
||||
static const FVector DuplicateOffset(50.0f, 50.0f, 0.0f);
|
||||
TArray<AActor*> NewActors;
|
||||
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||
{
|
||||
AActor* Original = Weak.Get();
|
||||
if (!Original) continue;
|
||||
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||
|
||||
AActor* NewActor = PC->GetWorld()->SpawnActor<AActor>(
|
||||
Original->GetClass(),
|
||||
Original->GetActorLocation() + DuplicateOffset,
|
||||
Original->GetActorRotation(),
|
||||
SpawnParams);
|
||||
|
||||
if (NewActor)
|
||||
{
|
||||
NewActor->SetActorScale3D(Original->GetActorScale3D());
|
||||
if (USceneComponent* Root = NewActor->GetRootComponent())
|
||||
{
|
||||
Root->SetMobility(EComponentMobility::Movable);
|
||||
}
|
||||
NewActors.Add(NewActor);
|
||||
|
||||
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
||||
{
|
||||
SpawnMgr->TrackSpawnedActor(NewActor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (NewActors.Num() == 0) return;
|
||||
|
||||
// Record undo action
|
||||
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
|
||||
for (AActor* Actor : NewActors)
|
||||
{
|
||||
Action->Actors.Add(Actor);
|
||||
}
|
||||
Action->Description = FString::Printf(TEXT("Duplicate %d actor(s)"), NewActors.Num());
|
||||
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
{
|
||||
UM->RecordAction(Action);
|
||||
}
|
||||
|
||||
// Select the duplicates (deselect originals)
|
||||
ClearSelection();
|
||||
for (AActor* Actor : NewActors)
|
||||
{
|
||||
SelectActor(Actor);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Duplicated %d actor(s)"), NewActors.Num());
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::CopySelection()
|
||||
{
|
||||
Clipboard.Empty();
|
||||
|
||||
if (!IsAnythingSelected()) return;
|
||||
|
||||
const FVector Pivot = GetSelectionPivot();
|
||||
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||
{
|
||||
AActor* Actor = Weak.Get();
|
||||
if (!Actor) continue;
|
||||
|
||||
FPS_Editor_ClipboardEntry Entry;
|
||||
Entry.ActorClass = Actor->GetClass();
|
||||
Entry.RelativeLocation = Actor->GetActorLocation() - Pivot;
|
||||
Entry.Rotation = Actor->GetActorRotation();
|
||||
Entry.Scale = Actor->GetActorScale3D();
|
||||
Clipboard.Add(Entry);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Copied %d actor(s) to clipboard"), Clipboard.Num());
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::PasteClipboard()
|
||||
{
|
||||
if (Clipboard.Num() == 0) return;
|
||||
|
||||
APlayerController* PC = OwnerPC.Get();
|
||||
if (!PC) return;
|
||||
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC);
|
||||
if (!EditorPC) return;
|
||||
|
||||
// Paste center: in front of the camera
|
||||
FVector CamLoc;
|
||||
FRotator CamRot;
|
||||
PC->GetPlayerViewPoint(CamLoc, CamRot);
|
||||
const FVector PasteCenter = CamLoc + CamRot.Vector() * 500.0f;
|
||||
|
||||
TArray<AActor*> NewActors;
|
||||
|
||||
for (const FPS_Editor_ClipboardEntry& Entry : Clipboard)
|
||||
{
|
||||
if (!Entry.ActorClass) continue;
|
||||
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||
|
||||
AActor* NewActor = PC->GetWorld()->SpawnActor<AActor>(
|
||||
Entry.ActorClass,
|
||||
PasteCenter + Entry.RelativeLocation,
|
||||
Entry.Rotation,
|
||||
SpawnParams);
|
||||
|
||||
if (NewActor)
|
||||
{
|
||||
NewActor->SetActorScale3D(Entry.Scale);
|
||||
if (USceneComponent* Root = NewActor->GetRootComponent())
|
||||
{
|
||||
Root->SetMobility(EComponentMobility::Movable);
|
||||
}
|
||||
NewActors.Add(NewActor);
|
||||
|
||||
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
||||
{
|
||||
SpawnMgr->TrackSpawnedActor(NewActor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (NewActors.Num() == 0) return;
|
||||
|
||||
// Record undo action
|
||||
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
|
||||
for (AActor* Actor : NewActors)
|
||||
{
|
||||
Action->Actors.Add(Actor);
|
||||
}
|
||||
Action->Description = FString::Printf(TEXT("Paste %d actor(s)"), NewActors.Num());
|
||||
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
{
|
||||
UM->RecordAction(Action);
|
||||
}
|
||||
|
||||
// Select the pasted actors
|
||||
ClearSelection();
|
||||
for (AActor* Actor : NewActors)
|
||||
{
|
||||
SelectActor(Actor);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Pasted %d actor(s) from clipboard"), NewActors.Num());
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::CleanupStaleReferences()
|
||||
{
|
||||
SelectedActors.RemoveAll([](const TWeakObjectPtr<AActor>& Weak)
|
||||
|
||||
@@ -8,6 +8,17 @@
|
||||
class APlayerController;
|
||||
class APS_Editor_Gizmo;
|
||||
|
||||
/**
|
||||
* Clipboard entry for copy/paste operations.
|
||||
*/
|
||||
struct FPS_Editor_ClipboardEntry
|
||||
{
|
||||
UClass* ActorClass = nullptr;
|
||||
FVector RelativeLocation = FVector::ZeroVector; // Relative to clipboard pivot
|
||||
FRotator Rotation = FRotator::ZeroRotator;
|
||||
FVector Scale = FVector::OneVector;
|
||||
};
|
||||
|
||||
DECLARE_MULTICAST_DELEGATE(FPS_Editor_OnSelectionChanged);
|
||||
|
||||
/**
|
||||
@@ -39,6 +50,18 @@ public:
|
||||
/** Snap all selected actors to the ground below them. */
|
||||
void SnapSelectionToGround();
|
||||
|
||||
/** Duplicate selected actors with a small offset. Selects the duplicates. */
|
||||
void DuplicateSelection();
|
||||
|
||||
/** Copy selected actors to the internal clipboard. */
|
||||
void CopySelection();
|
||||
|
||||
/** Paste clipboard contents in front of the camera. */
|
||||
void PasteClipboard();
|
||||
|
||||
/** Returns true if the clipboard has entries. */
|
||||
bool HasClipboardData() const { return Clipboard.Num() > 0; }
|
||||
|
||||
// ---- Queries ----
|
||||
|
||||
const TArray<TWeakObjectPtr<AActor>>& GetSelectedActors() const { return SelectedActors; }
|
||||
@@ -55,6 +78,28 @@ public:
|
||||
|
||||
EPS_Editor_Space GetTransformSpace() const { return CurrentSpace; }
|
||||
void SetTransformSpace(EPS_Editor_Space NewSpace);
|
||||
void ToggleTransformSpace();
|
||||
|
||||
/** Refresh gizmo orientation to match current selection (call after transforms change). */
|
||||
void RefreshGizmoOrientation();
|
||||
|
||||
// ---- Snap Settings ----
|
||||
|
||||
bool bSnapTranslation = false;
|
||||
float TranslationSnapSize = 10.0f; // cm
|
||||
|
||||
bool bSnapRotation = false;
|
||||
float RotationSnapSize = 10.0f; // degrees
|
||||
|
||||
bool bSnapScale = false;
|
||||
float ScaleSnapSize = 0.1f;
|
||||
|
||||
/** Apply snap to a position value. */
|
||||
FVector SnapPosition(const FVector& Position) const;
|
||||
/** Apply snap to a rotation value. */
|
||||
FRotator SnapRotation(const FRotator& Rotation) const;
|
||||
/** Apply snap to a scale value. */
|
||||
FVector SnapScale(const FVector& Scale) const;
|
||||
|
||||
// ---- Delegates ----
|
||||
|
||||
@@ -73,6 +118,9 @@ private:
|
||||
EPS_Editor_TransformMode CurrentTransformMode = EPS_Editor_TransformMode::Translate;
|
||||
EPS_Editor_Space CurrentSpace = EPS_Editor_Space::World;
|
||||
|
||||
/** Clipboard for copy/paste. */
|
||||
TArray<FPS_Editor_ClipboardEntry> Clipboard;
|
||||
|
||||
/** Toggle custom depth stencil rendering on an actor's primitive components. */
|
||||
void SetActorHighlight(AActor* Actor, bool bHighlight);
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ struct FPS_Editor_SpawnAction : public FPS_Editor_Action
|
||||
FPS_Editor_SpawnAction() : FPS_Editor_Action(EPS_Editor_ActionType::Spawn) {}
|
||||
|
||||
TArray<TWeakObjectPtr<AActor>> Actors;
|
||||
FString Description; // Optional override (e.g. "Duplicate", "Paste")
|
||||
|
||||
virtual void Execute() override
|
||||
{
|
||||
@@ -150,6 +151,7 @@ struct FPS_Editor_SpawnAction : public FPS_Editor_Action
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
if (!Description.IsEmpty()) return Description;
|
||||
return FString::Printf(TEXT("Spawn %d actor(s)"), Actors.Num());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -203,6 +203,14 @@ void UPS_Editor_SpawnManager::DestroyAllSpawnedActors()
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: All spawned actors destroyed"));
|
||||
}
|
||||
|
||||
void UPS_Editor_SpawnManager::TrackSpawnedActor(AActor* Actor)
|
||||
{
|
||||
if (Actor)
|
||||
{
|
||||
SpawnedActors.Add(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
TArray<FString> UPS_Editor_SpawnManager::GetCategories() const
|
||||
{
|
||||
TArray<FString> Categories;
|
||||
|
||||
@@ -60,6 +60,9 @@ public:
|
||||
/** Get unique categories for filtering. */
|
||||
TArray<FString> GetCategories() const;
|
||||
|
||||
/** Register an externally-spawned actor for tracking (used by duplicate/paste). */
|
||||
void TrackSpawnedActor(AActor* Actor);
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#include "PS_Editor_ConfirmDialog.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Layout/SBorder.h"
|
||||
#include "Widgets/SOverlay.h"
|
||||
#include "Widgets/Layout/SBox.h"
|
||||
|
||||
void UPS_Editor_ConfirmDialog::Show(const FText& Message, FPS_Editor_OnConfirmResult InCallback)
|
||||
{
|
||||
DialogMessage = Message;
|
||||
OnResult = InCallback;
|
||||
|
||||
if (MessageText.IsValid())
|
||||
{
|
||||
MessageText->SetText(DialogMessage);
|
||||
}
|
||||
|
||||
AddToViewport(100); // High Z-order to be on top of everything
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UPS_Editor_ConfirmDialog::RebuildWidget()
|
||||
{
|
||||
return SNew(SOverlay)
|
||||
// Dark overlay background
|
||||
+ SOverlay::Slot()
|
||||
[
|
||||
SNew(SBorder)
|
||||
.BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.5f))
|
||||
]
|
||||
// Centered dialog box
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(HAlign_Center)
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(SBorder)
|
||||
.BorderBackgroundColor(FLinearColor(0.1f, 0.1f, 0.1f, 0.95f))
|
||||
.Padding(FMargin(24.0f, 16.0f))
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
// Message
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.HAlign(HAlign_Center)
|
||||
.Padding(0.0f, 0.0f, 0.0f, 16.0f)
|
||||
[
|
||||
SAssignNew(MessageText, STextBlock)
|
||||
.Text(DialogMessage)
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 14))
|
||||
.ColorAndOpacity(FLinearColor::White)
|
||||
.Justification(ETextJustify::Center)
|
||||
]
|
||||
// Buttons
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.HAlign(HAlign_Center)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(8.0f, 0.0f)
|
||||
[
|
||||
SNew(SBox)
|
||||
.MinDesiredWidth(100.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.HAlign(HAlign_Center)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
OnYesClicked();
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Yes")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 12))
|
||||
.Justification(ETextJustify::Center)
|
||||
]
|
||||
]
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(8.0f, 0.0f)
|
||||
[
|
||||
SNew(SBox)
|
||||
.MinDesiredWidth(100.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.HAlign(HAlign_Center)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
OnNoClicked();
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("No")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 12))
|
||||
.Justification(ETextJustify::Center)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
void UPS_Editor_ConfirmDialog::OnYesClicked()
|
||||
{
|
||||
CloseDialog(true);
|
||||
}
|
||||
|
||||
void UPS_Editor_ConfirmDialog::OnNoClicked()
|
||||
{
|
||||
CloseDialog(false);
|
||||
}
|
||||
|
||||
void UPS_Editor_ConfirmDialog::CloseDialog(bool bConfirmed)
|
||||
{
|
||||
RemoveFromParent();
|
||||
OnResult.ExecuteIfBound(bConfirmed);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "PS_Editor_ConfirmDialog.generated.h"
|
||||
|
||||
DECLARE_DELEGATE_OneParam(FPS_Editor_OnConfirmResult, bool /*bConfirmed*/);
|
||||
|
||||
/**
|
||||
* Simple in-game confirmation dialog (Yes/No).
|
||||
* Works in packaged builds unlike FMessageDialog.
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API UPS_Editor_ConfirmDialog : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Show the dialog with a message. Callback fired when user clicks Yes or No. */
|
||||
void Show(const FText& Message, FPS_Editor_OnConfirmResult InCallback);
|
||||
|
||||
FPS_Editor_OnConfirmResult OnResult;
|
||||
|
||||
protected:
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
|
||||
private:
|
||||
FText DialogMessage;
|
||||
TSharedPtr<STextBlock> MessageText;
|
||||
|
||||
void OnYesClicked();
|
||||
void OnNoClicked();
|
||||
void CloseDialog(bool bConfirmed);
|
||||
};
|
||||
@@ -63,7 +63,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||
.Padding(12.0f, 0.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SAssignNew(ModeText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("Translate (E)")))
|
||||
.Text(FText::FromString(TEXT("Translate (Z)")))
|
||||
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 11))
|
||||
]
|
||||
@@ -111,7 +111,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||
.Padding(FMargin(16.0f, 6.0f))
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | E/R/T: Translate/Rotate/Scale | End: Snap | Del: Delete")))
|
||||
.Text(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | Z/E/R: Translate/Rotate/Scale | End: Snap | Del: Delete")))
|
||||
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||
]
|
||||
@@ -142,11 +142,11 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||
[ MakeButton(FText::FromString(TEXT("Translate (E)")), EPS_Editor_TransformMode::Translate) ]
|
||||
[ MakeButton(FText::FromString(TEXT("Translate (Z)")), EPS_Editor_TransformMode::Translate) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||
[ MakeButton(FText::FromString(TEXT("Rotate (R)")), EPS_Editor_TransformMode::Rotate) ]
|
||||
[ MakeButton(FText::FromString(TEXT("Rotate (E)")), EPS_Editor_TransformMode::Rotate) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||
[ MakeButton(FText::FromString(TEXT("Scale (T)")), EPS_Editor_TransformMode::Scale) ]
|
||||
[ MakeButton(FText::FromString(TEXT("Scale (R)")), EPS_Editor_TransformMode::Scale) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
@@ -164,6 +164,66 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
]
|
||||
// World/Local toggle
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
|
||||
{
|
||||
SM->ToggleTransformSpace();
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SAssignNew(SpaceText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("World")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
]
|
||||
// Snap toggle + value
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
|
||||
{
|
||||
SM->bSnapTranslation = !SM->bSnapTranslation;
|
||||
SM->bSnapRotation = SM->bSnapTranslation;
|
||||
SM->bSnapScale = SM->bSnapTranslation;
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SAssignNew(SnapText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("Snap: Off")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f).VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(SBox).MinDesiredWidth(40.0f)
|
||||
[
|
||||
SAssignNew(SnapSizeField, SEditableTextBox)
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||
.HintText(FText::FromString(TEXT("10")))
|
||||
.OnTextCommitted_Lambda([this](const FText& Text, ETextCommit::Type)
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
|
||||
{
|
||||
float Val = FCString::Atof(*Text.ToString());
|
||||
if (Val > 0.0f)
|
||||
{
|
||||
SM->TranslationSnapSize = Val;
|
||||
SM->RotationSnapSize = Val;
|
||||
SM->ScaleSnapSize = Val * 0.01f;
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -608,13 +668,27 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
||||
FString ModeStr;
|
||||
switch (SM->GetTransformMode())
|
||||
{
|
||||
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (E)"); break;
|
||||
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (R)"); break;
|
||||
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (T)"); break;
|
||||
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (Z)"); break;
|
||||
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (E)"); break;
|
||||
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (R)"); break;
|
||||
}
|
||||
ModeText->SetText(FText::FromString(ModeStr));
|
||||
}
|
||||
|
||||
// Update space button text
|
||||
if (SpaceText.IsValid())
|
||||
{
|
||||
SpaceText->SetText(FText::FromString(
|
||||
SM->GetTransformSpace() == EPS_Editor_Space::World ? TEXT("World") : TEXT("Local")));
|
||||
}
|
||||
|
||||
// Update snap button text
|
||||
if (SnapText.IsValid())
|
||||
{
|
||||
SnapText->SetText(FText::FromString(
|
||||
SM->bSnapTranslation ? TEXT("Snap: On") : TEXT("Snap: Off")));
|
||||
}
|
||||
|
||||
if (!SM->IsAnythingSelected())
|
||||
{
|
||||
if (SelectionInfoText.IsValid())
|
||||
|
||||
@@ -44,6 +44,11 @@ private:
|
||||
TSharedPtr<STextBlock> ModeText;
|
||||
TSharedPtr<STextBlock> SelectionInfoText;
|
||||
|
||||
// Toolbar refs
|
||||
TSharedPtr<STextBlock> SpaceText;
|
||||
TSharedPtr<STextBlock> SnapText;
|
||||
TSharedPtr<SEditableTextBox> SnapSizeField;
|
||||
|
||||
// Spawn catalog
|
||||
TSharedPtr<SVerticalBox> SpawnListContainer;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user