Add rotation arc rings, scale cubes, and gizmo improvements

Rotation gizmo:
- Quarter-arc rings via UProceduralMeshComponent (3D solid with depth)
- Dynamic arc facing: arcs flip to face camera based on octant
- Grey guide lines from center to arc endpoints
- Screen-space rotation direction (correct from any camera angle)

Scale gizmo:
- Arrow shafts + cube tips for per-axis scale
- Center cube (grey) for uniform XYZ scale
- Local space orientation (gizmo aligns with actor rotation)

Other:
- Runtime confirmation dialog (replaces FMessageDialog)
- ProceduralMeshComponent plugin dependency added

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 11:47:03 +02:00
parent 03c79a2610
commit 48505bfb82
9 changed files with 690 additions and 172 deletions

View File

@@ -13,6 +13,7 @@ Key decisions:
- 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
- Selection outline: auto-generated post-process material (C++), 8-neighbor edge detection on CustomStencil
## Completed Phases
@@ -27,13 +28,15 @@ Key decisions:
- Snap to ground (End), soft-delete (Delete + 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)
- Materials: M_PS_Editor_Gizmo (Unlit, "Color" param, Translucent, Disable Depth Test)
- Selection outline: auto-generated PP material, BL_SceneColorAfterDOF, Lerp blend, color AF1500, 1px
- Outline material auto-created via OnPostEngineInit, uses InvSize for resolution-independent offsets
### 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
- Catalogue UI: thumbnail cards (80x80), click to spawn, grouped by category
- CaptureAllThumbnails on DataAsset: auto-generates from UE thumbnails
- Spawn undo/redo, auto-deselect hidden actors
@@ -44,4 +47,4 @@ Key decisions:
- UI: Save field + button, New Scene, saved scenes list with click-to-load
## Next: Phase 5 - Advanced Editing
- Splines, lighting, AI characters, property editing
- Splines, lighting, AI characters, property editing, duplication (Ctrl+D)

View File

@@ -20,6 +20,10 @@
{
"Name": "EnhancedInput",
"Enabled": true
},
{
"Name": "ProceduralMeshComponent",
"Enabled": true
}
]
}

View File

@@ -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()
{
@@ -344,7 +344,23 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
else if (Mode == EPS_Editor_TransformMode::Rotate)
{
const float RotationSpeed = 1.0f;
const float AngleDelta = (LookInput.X + LookInput.Y) * RotationSpeed;
// 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();
// Mouse movement perpendicular to the projected axis drives rotation
FVector2D ScreenPerp(-ScreenAxisDir.Y, ScreenAxisDir.X);
AngleDelta = -(LookInput.X * ScreenPerp.X - LookInput.Y * ScreenPerp.Y) * RotationSpeed;
}
GizmoDragAccumulator += AngleDelta;
@@ -376,7 +392,11 @@ 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);
@@ -579,37 +599,46 @@ 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)
{
return;
}
UPS_Editor_ConfirmDialog* Dialog = CreateWidget<UPS_Editor_ConfirmDialog>(
Cast<APlayerController>(GetController()), UPS_Editor_ConfirmDialog::StaticClass());
// 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())
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)
{
if (AActor* Actor = Weak.Get())
if (!bConfirmed || !WeakPC.IsValid()) return;
UPS_Editor_SelectionManager* SM = WeakPC->GetSelectionManager();
if (!SM || !SM->IsAnythingSelected()) return;
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
{
Action->Actors.Add(Actor);
if (AActor* Actor = Weak.Get())
{
Action->Actors.Add(Actor);
}
}
}
SM->ClearSelection();
Action->Execute();
SM->ClearSelection();
Action->Execute();
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
{
UM->RecordAction(Action);
}
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)

View File

@@ -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: thin cylinder, length ~60 units
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.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,78 +326,88 @@ 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);
FHitResult Hit;
const FVector RayEnd = RayOrigin + RayDirection * 100000.0f;
const FCollisionShape Sphere = FCollisionShape::MakeSphere(SweepRadius);
if (Comp->SweepComponent(Hit, RayOrigin, RayEnd, FQuat::Identity, Sphere, false))
for (UPrimitiveComponent* Comp : Components)
{
if (Hit.Distance < BestDistance)
if (!Comp || !Comp->IsVisible()) continue;
FHitResult Hit;
const FVector RayEnd = RayOrigin + RayDirection * 100000.0f;
const FCollisionShape Sphere = FCollisionShape::MakeSphere(SweepRadius);
if (Comp->SweepComponent(Hit, RayOrigin, RayEnd, FQuat::Identity, Sphere, false))
{
BestDistance = Hit.Distance;
BestAxis = Axis;
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);
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,135 @@ 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);
// Direction from gizmo to camera
const FVector CamDir = (CameraLocation - GetActorLocation()).GetSafeNormal();
// Determine which octant the camera is in (sign of each component)
const FVector NewOctant(
CamDir.X >= 0 ? 1.0f : -1.0f,
CamDir.Y >= 0 ? 1.0f : -1.0f,
CamDir.Z >= 0 ? 1.0f : -1.0f
);
// Only regenerate when the camera crosses an octant boundary
if (NewOctant.Equals(LastCameraOctant))
{
return;
}
LastCameraOctant = NewOctant;
// For each axis, show the quarter-arc facing the camera.
// The arc start/end directions are chosen based on camera octant.
// X-axis (red, arc in YZ plane): choose the Y and Z quadrant facing camera
const FVector ArcX_Start = FVector::RightVector * NewOctant.Y; // +Y or -Y
const FVector ArcX_End = FVector::UpVector * NewOctant.Z; // +Z or -Z
// Y-axis (green, arc in XZ plane): choose the X and Z quadrant facing camera
const FVector ArcY_Start = FVector::ForwardVector * NewOctant.X; // +X or -X
const FVector ArcY_End = FVector::UpVector * NewOctant.Z; // +Z or -Z
// Z-axis (blue, arc in XY plane): choose the X and Y quadrant facing camera
const FVector ArcZ_Start = FVector::ForwardVector * NewOctant.X; // +X or -X
const FVector ArcZ_End = FVector::RightVector * NewOctant.Y; // +Y or -Y
CreateArcRing(Arc_X, FVector::ForwardVector, ArcX_Start, ArcX_End, ArcRadius, ArcWidth, ArcSegments);
CreateArcRing(Arc_Y, FVector::RightVector, ArcY_Start, ArcY_End, ArcRadius, ArcWidth, ArcSegments);
CreateArcRing(Arc_Z, FVector::UpVector, ArcZ_Start, ArcZ_End, ArcRadius, ArcWidth, ArcSegments);
// Position guide lines from center toward each arc connection point
const FVector GuideDir_X = FVector::ForwardVector * NewOctant.X;
const FVector GuideDir_Y = FVector::RightVector * NewOctant.Y;
const FVector GuideDir_Z = FVector::UpVector * NewOctant.Z;
auto PositionGuideLine = [](UStaticMeshComponent* Line, const FVector& Dir)
{
if (!Line) return;
FRotator Rot = FRotationMatrix::MakeFromZ(Dir).Rotator();
Line->SetRelativeRotation(Rot);
Line->SetRelativeLocation(Dir * 27.5f);
};
PositionGuideLine(GuideLine_A, GuideDir_X);
PositionGuideLine(GuideLine_B, GuideDir_Y);
PositionGuideLine(GuideLine_C, GuideDir_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;
}
// In scale mode, return the axis in the object's local space
if (CurrentMode == EPS_Editor_TransformMode::Scale && ScaleRoot)
{
return ScaleRoot->GetRelativeRotation().RotateVector(LocalDir);
}
return LocalDir;
}
void APS_Editor_Gizmo::SetScaleOrientation(FRotator ActorRotation)
{
if (ScaleRoot)
{
ScaleRoot->SetRelativeRotation(ActorRotation);
}
}
void APS_Editor_Gizmo::UpdateConstantScreenSize()
@@ -282,22 +563,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);
}
}

View File

@@ -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,14 @@ 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 scale mode (matches selected actor's rotation). */
void SetScaleOrientation(FRotator ActorRotation);
EPS_Editor_TransformMode GetTransformMode() const { return CurrentMode; }
protected:
@@ -50,53 +43,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);
};

View File

@@ -27,7 +27,8 @@ public class PS_Editor : ModuleRules
"EnhancedInput",
"UMG",
"Json",
"JsonUtilities"
"JsonUtilities",
"ProceduralMeshComponent"
});
PrivateDependencyModuleNames.AddRange(new string[]

View File

@@ -194,6 +194,19 @@ FVector UPS_Editor_SelectionManager::GetSelectionPivot() const
void UPS_Editor_SelectionManager::SetTransformMode(EPS_Editor_TransformMode NewMode)
{
CurrentTransformMode = NewMode;
if (Gizmo)
{
Gizmo->SetTransformMode(NewMode);
// In scale mode, orient the gizmo to match the first selected actor's rotation
if (NewMode == EPS_Editor_TransformMode::Scale && IsAnythingSelected())
{
if (AActor* Actor = SelectedActors[0].Get())
{
Gizmo->SetScaleOrientation(Actor->GetActorRotation());
}
}
}
OnSelectionChanged.Broadcast();
}
@@ -285,6 +298,15 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
if (IsAnythingSelected())
{
Gizmo->SetGizmoActive(true, GetSelectionPivot());
// Update scale orientation to match first selected actor
if (CurrentTransformMode == EPS_Editor_TransformMode::Scale)
{
if (AActor* Actor = SelectedActors[0].Get())
{
Gizmo->SetScaleOrientation(Actor->GetActorRotation());
}
}
}
else
{

View File

@@ -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);
}

View File

@@ -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);
};