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:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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,19 +599,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())
|
||||
{
|
||||
@@ -604,12 +632,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)
|
||||
|
||||
@@ -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,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,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -27,7 +27,8 @@ public class PS_Editor : ModuleRules
|
||||
"EnhancedInput",
|
||||
"UMG",
|
||||
"Json",
|
||||
"JsonUtilities"
|
||||
"JsonUtilities",
|
||||
"ProceduralMeshComponent"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user