diff --git a/.claude/memory/project_ps_editor.md b/.claude/memory/project_ps_editor.md index 3560e87..61efaab 100644 --- a/.claude/memory/project_ps_editor.md +++ b/.claude/memory/project_ps_editor.md @@ -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) diff --git a/Unreal/Plugins/PS_Editor/PS_Editor.uplugin b/Unreal/Plugins/PS_Editor/PS_Editor.uplugin index 8a28fe4..46af049 100644 --- a/Unreal/Plugins/PS_Editor/PS_Editor.uplugin +++ b/Unreal/Plugins/PS_Editor/PS_Editor.uplugin @@ -20,6 +20,10 @@ { "Name": "EnhancedInput", "Enabled": true + }, + { + "Name": "ProceduralMeshComponent", + "Enabled": true } ] } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp index 1a2b16e..5742b85 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp @@ -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(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( + Cast(GetController()), UPS_Editor_ConfirmDialog::StaticClass()); - // Soft-delete: hide actors instead of destroying (enables undo) - TSharedPtr Action = MakeShared(); - for (const TWeakObjectPtr& Weak : SM->GetSelectedActors()) + if (!Dialog) return; + + // Weak refs to avoid dangling pointers in the callback + TWeakObjectPtr 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 Action = MakeShared(); + for (const TWeakObjectPtr& 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) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp index 072d98c..bfdbb0b 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp @@ -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(TEXT("GizmoRoot")); SetRootComponent(GizmoRoot); - TranslateRoot = CreateDefaultSubobject(TEXT("TranslateRoot")); - TranslateRoot->SetupAttachment(GizmoRoot); - const FRotator RotToX = FRotationMatrix::MakeFromZ(FVector::ForwardVector).Rotator(); const FRotator RotToY = FRotationMatrix::MakeFromZ(FVector::RightVector).Rotator(); const FRotator RotToZ = FRotator::ZeroRotator; + // ---- Translate ---- + TranslateRoot = CreateDefaultSubobject(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(TEXT("RotateRoot")); + RotateRoot->SetupAttachment(GizmoRoot); + + Arc_X = CreateDefaultSubobject(TEXT("Arc_X")); + Arc_X->SetupAttachment(RotateRoot); + ConfigureGizmoComponent(Arc_X); + + Arc_Y = CreateDefaultSubobject(TEXT("Arc_Y")); + Arc_Y->SetupAttachment(RotateRoot); + ConfigureGizmoComponent(Arc_Y); + + Arc_Z = CreateDefaultSubobject(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(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(TEXT("ScaleCube_Center")); + ScaleCube_Center->SetupAttachment(ScaleRoot); + static ConstructorHelpers::FObjectFinder 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( 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 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 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(Name); + Comp->SetupAttachment(Parent); + + static ConstructorHelpers::FObjectFinder 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(Name); + Comp->SetupAttachment(Parent); + + static ConstructorHelpers::FObjectFinder 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 Vertices; + TArray Triangles; + TArray Normals; + TArray UVs; + TArray 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(), 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::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 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& 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 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); - } -} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h index 4b2dacc..0fdb6a5 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h @@ -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 GizmoRoot; - // ---- Translate handles ---- + // ---- Translate components ---- UPROPERTY(VisibleAnywhere) TObjectPtr TranslateRoot; UPROPERTY(VisibleAnywhere) TObjectPtr Arrow_X; - UPROPERTY(VisibleAnywhere) TObjectPtr Arrow_Y; - UPROPERTY(VisibleAnywhere) TObjectPtr Arrow_Z; UPROPERTY(VisibleAnywhere) TObjectPtr Cone_X; - UPROPERTY(VisibleAnywhere) TObjectPtr Cone_Y; - UPROPERTY(VisibleAnywhere) TObjectPtr Cone_Z; + // ---- Rotate components (quarter-arc rings) ---- + UPROPERTY(VisibleAnywhere) + TObjectPtr RotateRoot; + + UPROPERTY(VisibleAnywhere) + TObjectPtr Arc_X; + UPROPERTY(VisibleAnywhere) + TObjectPtr Arc_Y; + UPROPERTY(VisibleAnywhere) + TObjectPtr Arc_Z; + + // ---- Rotate guide lines (grey lines from center to arc endpoints) ---- + UPROPERTY(VisibleAnywhere) + TObjectPtr GuideLine_A; + UPROPERTY(VisibleAnywhere) + TObjectPtr GuideLine_B; + UPROPERTY(VisibleAnywhere) + TObjectPtr GuideLine_C; + + // ---- Scale components ---- + UPROPERTY(VisibleAnywhere) + TObjectPtr ScaleRoot; + + UPROPERTY(VisibleAnywhere) + TObjectPtr ScaleArrow_X; + UPROPERTY(VisibleAnywhere) + TObjectPtr ScaleArrow_Y; + UPROPERTY(VisibleAnywhere) + TObjectPtr ScaleArrow_Z; + + UPROPERTY(VisibleAnywhere) + TObjectPtr ScaleCube_X; + UPROPERTY(VisibleAnywhere) + TObjectPtr ScaleCube_Y; + UPROPERTY(VisibleAnywhere) + TObjectPtr ScaleCube_Z; + + /** Center cube for uniform scaling. */ + UPROPERTY(VisibleAnywhere) + TObjectPtr ScaleCube_Center; + // ---- Materials ---- UPROPERTY() TObjectPtr Mat_X; - UPROPERTY() TObjectPtr Mat_Y; - UPROPERTY() TObjectPtr Mat_Z; - UPROPERTY() TObjectPtr Mat_Highlight; + UPROPERTY() + TObjectPtr 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& OutComponents) const; + + void SetAxisMaterial(EPS_Editor_GizmoAxis Axis, UMaterialInstanceDynamic* Mat); }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs index cb2a12f..4bbc3e7 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs @@ -27,7 +27,8 @@ public class PS_Editor : ModuleRules "EnhancedInput", "UMG", "Json", - "JsonUtilities" + "JsonUtilities", + "ProceduralMeshComponent" }); PrivateDependencyModuleNames.AddRange(new string[] diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp index ea9c473..d2bf088 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp @@ -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 { diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_ConfirmDialog.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_ConfirmDialog.cpp new file mode 100644 index 0000000..ba1c862 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_ConfirmDialog.cpp @@ -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 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); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_ConfirmDialog.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_ConfirmDialog.h new file mode 100644 index 0000000..ee16c37 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_ConfirmDialog.h @@ -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 RebuildWidget() override; + +private: + FText DialogMessage; + TSharedPtr MessageText; + + void OnYesClicked(); + void OnNoClicked(); + void CloseDialog(bool bConfirmed); +};