Compare commits
3 Commits
50087e5dfb
...
861c15c193
| Author | SHA1 | Date | |
|---|---|---|---|
| 861c15c193 | |||
| ca4f7ee355 | |||
| 1f4b3994ff |
Binary file not shown.
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/M_PS_Editor_SplineLine.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/M_PS_Editor_SplineLine.uasset
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/PS_MasterCatalog.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/PS_MasterCatalog.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/PS_PropsCatalog.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/PS_PropsCatalog.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/PS_SplineEnnemy.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/PS_SplineEnnemy.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/PS_ToolsCatalog.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/PS_ToolsCatalog.uasset
Normal file
Binary file not shown.
@@ -13,6 +13,8 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_Gizmo.h"
|
#include "PS_Editor_Gizmo.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
|
#include "Components/SplineComponent.h"
|
||||||
#include "DrawDebugHelpers.h"
|
#include "DrawDebugHelpers.h"
|
||||||
#include "PS_Editor_ConfirmDialog.h"
|
#include "PS_Editor_ConfirmDialog.h"
|
||||||
|
|
||||||
@@ -199,6 +201,19 @@ void APS_Editor_Pawn::CreateInputActions()
|
|||||||
IA_Paste = NewObject<UInputAction>(this, TEXT("IA_Paste"));
|
IA_Paste = NewObject<UInputAction>(this, TEXT("IA_Paste"));
|
||||||
IA_Paste->ValueType = EInputActionValueType::Boolean;
|
IA_Paste->ValueType = EInputActionValueType::Boolean;
|
||||||
EditorMappingContext->MapKey(IA_Paste, EKeys::V);
|
EditorMappingContext->MapKey(IA_Paste, EKeys::V);
|
||||||
|
|
||||||
|
// Spline mode / Confirm / Cancel
|
||||||
|
IA_SplineMode = NewObject<UInputAction>(this, TEXT("IA_SplineMode"));
|
||||||
|
IA_SplineMode->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_SplineMode, EKeys::S);
|
||||||
|
|
||||||
|
IA_Confirm = NewObject<UInputAction>(this, TEXT("IA_Confirm"));
|
||||||
|
IA_Confirm->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_Confirm, EKeys::Enter);
|
||||||
|
|
||||||
|
IA_Cancel = NewObject<UInputAction>(this, TEXT("IA_Cancel"));
|
||||||
|
IA_Cancel->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_Cancel, EKeys::Escape);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||||
@@ -242,6 +257,9 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp
|
|||||||
EIC->BindAction(IA_Duplicate, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDuplicate);
|
EIC->BindAction(IA_Duplicate, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDuplicate);
|
||||||
EIC->BindAction(IA_Copy, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCopy);
|
EIC->BindAction(IA_Copy, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCopy);
|
||||||
EIC->BindAction(IA_Paste, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandlePaste);
|
EIC->BindAction(IA_Paste, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandlePaste);
|
||||||
|
EIC->BindAction(IA_SplineMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSplineMode);
|
||||||
|
EIC->BindAction(IA_Confirm, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleConfirm);
|
||||||
|
EIC->BindAction(IA_Cancel, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Input Handlers ----
|
// ---- Input Handlers ----
|
||||||
@@ -357,11 +375,26 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
const FVector ConstrainedDelta = DisplacementAlongAxis * AxisDir;
|
const FVector ConstrainedDelta = DisplacementAlongAxis * AxisDir;
|
||||||
|
|
||||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
// Spline point mode: move the single point instead of actors
|
||||||
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
{
|
{
|
||||||
if (AActor* Actor = Selected[i].Get())
|
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
{
|
{
|
||||||
Actor->SetActorLocation(GizmoDragInitialTransforms[i].GetLocation() + ConstrainedDelta);
|
if (SplinePointDragInitialState.IsValidIndex(ActiveSplinePointIndex))
|
||||||
|
{
|
||||||
|
const FVector NewPos = SplinePointDragInitialState[ActiveSplinePointIndex] + ConstrainedDelta;
|
||||||
|
SplineActor->MovePoint(ActiveSplinePointIndex, NewPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Selected[i].Get())
|
||||||
|
{
|
||||||
|
Actor->SetActorLocation(GizmoDragInitialTransforms[i].GetLocation() + ConstrainedDelta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,10 +465,33 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In translate mode, gizmo follows the selection. In rotate/scale, stay at initial position.
|
// In translate mode, gizmo follows the target
|
||||||
if (Mode == EPS_Editor_TransformMode::Translate)
|
if (Mode == EPS_Editor_TransformMode::Translate)
|
||||||
{
|
{
|
||||||
Gizmo->SetActorLocation(SM->GetSelectionPivot());
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
|
{
|
||||||
|
// Following a single spline point
|
||||||
|
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
|
{
|
||||||
|
Gizmo->SetActorLocation(SplineActor->GetPointLocation(ActiveSplinePointIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Selected.Num() == 1)
|
||||||
|
{
|
||||||
|
// Single actor: use centroid for splines, location for others
|
||||||
|
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(Selected[0].Get()))
|
||||||
|
{
|
||||||
|
Gizmo->SetActorLocation(SplineActor->GetCentroid());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Gizmo->SetActorLocation(SM->GetSelectionPivot());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Gizmo->SetActorLocation(SM->GetSelectionPivot());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update local orientation in real-time during rotation drag
|
// Update local orientation in real-time during rotation drag
|
||||||
@@ -447,6 +503,25 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (CameraMode == EPS_Editor_CameraMode::SplinePointDrag)
|
||||||
|
{
|
||||||
|
// Drag spline point along horizontal plane
|
||||||
|
APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get());
|
||||||
|
if (!Spline || ActiveSplinePointIndex < 0) return;
|
||||||
|
|
||||||
|
FVector RayOrigin, RayDir;
|
||||||
|
if (!GetMouseWorldRay(RayOrigin, RayDir)) return;
|
||||||
|
|
||||||
|
// Ray-plane intersection
|
||||||
|
const float Denom = FVector::DotProduct(RayDir, SplinePointDragPlaneNormal);
|
||||||
|
if (FMath::Abs(Denom) < KINDA_SMALL_NUMBER) return;
|
||||||
|
|
||||||
|
const float T = FVector::DotProduct(SplinePointDragPlaneOrigin - RayOrigin, SplinePointDragPlaneNormal) / Denom;
|
||||||
|
if (T < 0.0f) return;
|
||||||
|
|
||||||
|
const FVector NewPos = RayOrigin + RayDir * T;
|
||||||
|
Spline->MovePoint(ActiveSplinePointIndex, NewPos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value)
|
void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value)
|
||||||
@@ -502,6 +577,205 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for spline point handle hit → select point, move gizmo to it (SplineEditing or SplinePlacement)
|
||||||
|
if (APS_Editor_PlayerController* EdPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
const EPS_Editor_EditorMode Mode = EdPC->GetEditorMode();
|
||||||
|
if (Mode == EPS_Editor_EditorMode::SplinePlacement || Mode == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
// Collect candidate splines: selected actors + active placement actor
|
||||||
|
TArray<APS_Editor_SplineActor*> CandidateSplines;
|
||||||
|
if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* S = Cast<APS_Editor_SplineActor>(Weak.Get()))
|
||||||
|
{
|
||||||
|
CandidateSplines.AddUnique(S);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (APS_Editor_SplineActor* ActiveSpline = Cast<APS_Editor_SplineActor>(EdPC->GetActivePlacementActor()))
|
||||||
|
{
|
||||||
|
CandidateSplines.AddUnique(ActiveSpline);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (APS_Editor_SplineActor* Spline : CandidateSplines)
|
||||||
|
{
|
||||||
|
// Center cube check (skip in SplinePlacement — no center cube interaction during creation)
|
||||||
|
if (Mode == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
FHitResult SplineHit;
|
||||||
|
FCollisionQueryParams SplineParams;
|
||||||
|
SplineParams.AddIgnoredActor(this);
|
||||||
|
const FVector TraceEnd = RayOrigin + RayDir * 100000.0f;
|
||||||
|
if (GetWorld()->LineTraceSingleByChannel(SplineHit, RayOrigin, TraceEnd, ECC_Visibility, SplineParams))
|
||||||
|
{
|
||||||
|
if (Spline->IsCenterCube(SplineHit.GetComponent()))
|
||||||
|
{
|
||||||
|
ActiveSplinePointIndex = -1;
|
||||||
|
DraggedSplineActor = nullptr;
|
||||||
|
Spline->ClearHandleHighlight();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 HandleIdx = Spline->GetHandleIndexUnderCursor(RayOrigin, RayDir);
|
||||||
|
if (HandleIdx >= 0)
|
||||||
|
{
|
||||||
|
ActiveSplinePointIndex = HandleIdx;
|
||||||
|
DraggedSplineActor = Spline;
|
||||||
|
Spline->HighlightHandle(HandleIdx);
|
||||||
|
|
||||||
|
if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo())
|
||||||
|
{
|
||||||
|
// Force gizmo active at this point (even if spline not in SelectionManager yet)
|
||||||
|
Gizmo->SetGizmoActive(true, Spline->GetPointLocation(HandleIdx));
|
||||||
|
}
|
||||||
|
SM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In SplinePlacement mode: click on spline tube mesh → insert a CP
|
||||||
|
if (APS_Editor_PlayerController* EdPC2 = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (EdPC2->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
// Collect candidate splines: selected + active placement actor
|
||||||
|
TArray<APS_Editor_SplineActor*> TubeCandidates;
|
||||||
|
if (UPS_Editor_SelectionManager* SM2 = EdPC2->GetSelectionManager())
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM2->GetSelectedActors())
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* S = Cast<APS_Editor_SplineActor>(Weak.Get()))
|
||||||
|
TubeCandidates.AddUnique(S);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (APS_Editor_SplineActor* ActiveSpline = Cast<APS_Editor_SplineActor>(EdPC2->GetActivePlacementActor()))
|
||||||
|
{
|
||||||
|
TubeCandidates.AddUnique(ActiveSpline);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TubeCandidates.Num() > 0)
|
||||||
|
{
|
||||||
|
FHitResult LineHit;
|
||||||
|
FCollisionQueryParams LineParams;
|
||||||
|
LineParams.AddIgnoredActor(this);
|
||||||
|
const FVector TraceEnd = RayOrigin + RayDir * 100000.0f;
|
||||||
|
|
||||||
|
if (GetWorld()->LineTraceSingleByChannel(LineHit, RayOrigin, TraceEnd, ECC_Visibility, LineParams))
|
||||||
|
{
|
||||||
|
for (APS_Editor_SplineActor* Spline : TubeCandidates)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (Spline->IsSplineMesh(LineHit.GetComponent()))
|
||||||
|
{
|
||||||
|
const FVector HitPos = LineHit.ImpactPoint;
|
||||||
|
|
||||||
|
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||||
|
const int32 InsertIdx = Spline->FindInsertIndex(HitPos);
|
||||||
|
Spline->InsertPoint(InsertIdx, HitPos);
|
||||||
|
|
||||||
|
if (UPS_Editor_UndoManager* UM = EdPC2->GetUndoManager())
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
||||||
|
Action->SplineActor = Spline;
|
||||||
|
Action->OldPoints = OldPoints;
|
||||||
|
Action->NewPoints = Spline->GetAllPointLocations();
|
||||||
|
Action->Description = TEXT("Insert spline point");
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveSplinePointIndex = InsertIdx;
|
||||||
|
DraggedSplineActor = Spline;
|
||||||
|
Spline->HighlightHandle(InsertIdx);
|
||||||
|
|
||||||
|
if (UPS_Editor_SelectionManager* InsertSM = EdPC2->GetSelectionManager())
|
||||||
|
{
|
||||||
|
if (APS_Editor_Gizmo* G = InsertSM->GetGizmo())
|
||||||
|
{
|
||||||
|
G->SetGizmoActive(true, Spline->GetPointLocation(InsertIdx));
|
||||||
|
}
|
||||||
|
InsertSM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Inserted spline point at index %d"), InsertIdx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In SplineEditing mode: click outside tube/handles → add point at end
|
||||||
|
if (APS_Editor_PlayerController* EdPC3 = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (EdPC3->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(EdPC3->GetActivePlacementActor()))
|
||||||
|
{
|
||||||
|
// Raycast to get world click position
|
||||||
|
FHitResult AddHit;
|
||||||
|
FCollisionQueryParams AddParams;
|
||||||
|
AddParams.AddIgnoredActor(this);
|
||||||
|
AddParams.AddIgnoredActor(Spline);
|
||||||
|
const FVector AddEnd = RayOrigin + RayDir * 100000.0f;
|
||||||
|
|
||||||
|
FVector ClickPos;
|
||||||
|
if (GetWorld()->LineTraceSingleByChannel(AddHit, RayOrigin, AddEnd, ECC_Visibility, AddParams))
|
||||||
|
{
|
||||||
|
ClickPos = AddHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClickPos = RayOrigin + RayDir * 1000.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||||
|
Spline->AddPoint(ClickPos);
|
||||||
|
|
||||||
|
if (UPS_Editor_UndoManager* UM = EdPC3->GetUndoManager())
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
||||||
|
Action->SplineActor = Spline;
|
||||||
|
Action->OldPoints = OldPoints;
|
||||||
|
Action->NewPoints = Spline->GetAllPointLocations();
|
||||||
|
Action->Description = TEXT("Add spline point");
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the new point
|
||||||
|
const int32 NewIdx = Spline->GetNumPoints() - 1;
|
||||||
|
ActiveSplinePointIndex = NewIdx;
|
||||||
|
DraggedSplineActor = Spline;
|
||||||
|
Spline->HighlightHandle(NewIdx);
|
||||||
|
|
||||||
|
if (UPS_Editor_SelectionManager* SM3 = EdPC3->GetSelectionManager())
|
||||||
|
{
|
||||||
|
if (APS_Editor_Gizmo* G = SM3->GetGizmo())
|
||||||
|
{
|
||||||
|
G->SetActorLocation(Spline->GetPointLocation(NewIdx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Added spline point at end (index %d)"), NewIdx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking elsewhere resets spline point selection
|
||||||
|
ActiveSplinePointIndex = -1;
|
||||||
|
DraggedSplineActor = nullptr;
|
||||||
|
|
||||||
// No gizmo hit: enter PendingClick
|
// No gizmo hit: enter PendingClick
|
||||||
PendingClickScreenPos = GetMouseScreenPosition();
|
PendingClickScreenPos = GetMouseScreenPosition();
|
||||||
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
|
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
|
||||||
@@ -522,8 +796,22 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
|||||||
{
|
{
|
||||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
{
|
{
|
||||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
// Spline point drag: record SplinePointAction
|
||||||
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
{
|
{
|
||||||
|
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
||||||
|
Action->SplineActor = SplineActor;
|
||||||
|
Action->OldPoints = SplinePointDragInitialState;
|
||||||
|
Action->NewPoints = SplineActor->GetAllPointLocations();
|
||||||
|
Action->Description = TEXT("Move spline point");
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
// Normal: record TransformAction
|
||||||
TSharedPtr<FPS_Editor_TransformAction> Action = MakeShared<FPS_Editor_TransformAction>();
|
TSharedPtr<FPS_Editor_TransformAction> Action = MakeShared<FPS_Editor_TransformAction>();
|
||||||
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
||||||
|
|
||||||
@@ -555,17 +843,53 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
|
ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
|
||||||
|
|
||||||
// Refresh gizmo orientation (local mode needs to track actor's new rotation)
|
// Refresh gizmo: keep at spline point if editing, else refresh orientation
|
||||||
if (APS_Editor_PlayerController* PC2 = Cast<APS_Editor_PlayerController>(GetController()))
|
if (APS_Editor_PlayerController* PC2 = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
{
|
{
|
||||||
if (UPS_Editor_SelectionManager* SM2 = PC2->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM2 = PC2->GetSelectionManager())
|
||||||
{
|
{
|
||||||
SM2->RefreshGizmoOrientation();
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
|
{
|
||||||
|
// Keep gizmo at the active spline point
|
||||||
|
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
|
{
|
||||||
|
if (APS_Editor_Gizmo* G = SM2->GetGizmo())
|
||||||
|
{
|
||||||
|
G->SetActorLocation(SplineActor->GetPointLocation(ActiveSplinePointIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SM2->RefreshGizmoOrientation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||||
}
|
}
|
||||||
|
else if (CameraMode == EPS_Editor_CameraMode::SplinePointDrag)
|
||||||
|
{
|
||||||
|
// Record undo for spline point move
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
||||||
|
Action->SplineActor = Spline;
|
||||||
|
Action->OldPoints = SplinePointDragInitialState;
|
||||||
|
Action->NewPoints = Spline->GetAllPointLocations();
|
||||||
|
Action->Description = TEXT("Move spline point");
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActiveSplinePointIndex = -1;
|
||||||
|
DraggedSplineActor = nullptr;
|
||||||
|
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||||
|
}
|
||||||
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit)
|
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit)
|
||||||
{
|
{
|
||||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||||
@@ -643,6 +967,50 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
|||||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||||
if (!EditorPC) return;
|
if (!EditorPC) return;
|
||||||
|
|
||||||
|
// In SplineEditing mode with a point selected: delete that point
|
||||||
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement
|
||||||
|
&& ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
|
{
|
||||||
|
APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get());
|
||||||
|
if (Spline && Spline->GetNumPoints() > 2)
|
||||||
|
{
|
||||||
|
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||||
|
Spline->RemovePoint(ActiveSplinePointIndex);
|
||||||
|
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
||||||
|
Action->SplineActor = Spline;
|
||||||
|
Action->OldPoints = OldPoints;
|
||||||
|
Action->NewPoints = Spline->GetAllPointLocations();
|
||||||
|
Action->Description = TEXT("Delete spline point");
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deselect the point
|
||||||
|
ActiveSplinePointIndex = -1;
|
||||||
|
DraggedSplineActor = nullptr;
|
||||||
|
Spline->ClearHandleHighlight();
|
||||||
|
|
||||||
|
// Move gizmo back to centroid
|
||||||
|
if (UPS_Editor_SelectionManager* SM2 = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
if (APS_Editor_Gizmo* G = SM2->GetGizmo())
|
||||||
|
{
|
||||||
|
G->SetActorLocation(Spline->GetCentroid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deleted spline point, %d remaining"), Spline->GetNumPoints());
|
||||||
|
}
|
||||||
|
else if (Spline)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Cannot delete point, minimum 2 points required"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||||
if (!SM || !SM->IsAnythingSelected()) return;
|
if (!SM || !SM->IsAnythingSelected()) return;
|
||||||
|
|
||||||
@@ -717,6 +1085,63 @@ void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
|
|||||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
{
|
{
|
||||||
|
// If a spline point is selected (in edit or placement mode), snap that point to ground
|
||||||
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
|
{
|
||||||
|
const FVector PointLoc = Spline->GetPointLocation(ActiveSplinePointIndex);
|
||||||
|
const FVector TraceStart = PointLoc + FVector(0.0f, 0.0f, 1.0f); // Just above current position
|
||||||
|
const FVector TraceEnd = PointLoc - FVector(0.0f, 0.0f, 10000.0f);
|
||||||
|
|
||||||
|
FHitResult Hit;
|
||||||
|
FCollisionQueryParams Params;
|
||||||
|
Params.AddIgnoredActor(Spline);
|
||||||
|
Params.AddIgnoredActor(this);
|
||||||
|
if (UPS_Editor_SelectionManager* SnapSM = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
if (APS_Editor_Gizmo* SnapGizmo = SnapSM->GetGizmo())
|
||||||
|
{
|
||||||
|
Params.AddIgnoredActor(SnapGizmo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, Params))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SnapToGround - PointLoc=%s, HitZ=%.2f, HitActor=%s, HitComp=%s"),
|
||||||
|
*PointLoc.ToString(), Hit.ImpactPoint.Z,
|
||||||
|
Hit.GetActor() ? *Hit.GetActor()->GetName() : TEXT("null"),
|
||||||
|
Hit.GetComponent() ? *Hit.GetComponent()->GetName() : TEXT("null"));
|
||||||
|
|
||||||
|
// Store state for undo
|
||||||
|
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||||
|
|
||||||
|
Spline->MovePoint(ActiveSplinePointIndex, Hit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f));
|
||||||
|
|
||||||
|
// Update gizmo position
|
||||||
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo())
|
||||||
|
{
|
||||||
|
Gizmo->SetActorLocation(Spline->GetPointLocation(ActiveSplinePointIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record undo
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
||||||
|
Action->SplineActor = Spline;
|
||||||
|
Action->OldPoints = OldPoints;
|
||||||
|
Action->NewPoints = Spline->GetAllPointLocations();
|
||||||
|
Action->Description = TEXT("Snap spline point to ground");
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
{
|
{
|
||||||
SM->SnapSelectionToGround();
|
SM->SnapSelectionToGround();
|
||||||
@@ -760,6 +1185,64 @@ void APS_Editor_Pawn::HandlePaste(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleSplineMode(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
EditorPC->FinishPointPlacement();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// S key only appends to a selected point-placeable actor (no new creation from shortcut)
|
||||||
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
if (Actor->Implements<UPS_Editor_PointPlaceable>())
|
||||||
|
{
|
||||||
|
ActiveSplinePointIndex = -1;
|
||||||
|
DraggedSplineActor = nullptr;
|
||||||
|
EditorPC->BeginPointPlacementAppend(Actor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleConfirm(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
EditorPC->FinishPointPlacement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleCancel(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
EditorPC->CancelPointPlacement();
|
||||||
|
}
|
||||||
|
else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
EditorPC->FinishSplineEditing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|
||||||
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
||||||
@@ -944,5 +1427,14 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store initial spline state if editing a point
|
||||||
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
|
{
|
||||||
|
SplinePointDragInitialState = Spline->GetAllPointLocations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SetCameraMode(EPS_Editor_CameraMode::GizmoDrag);
|
SetCameraMode(EPS_Editor_CameraMode::GizmoDrag);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ enum class EPS_Editor_CameraMode : uint8
|
|||||||
/** Alt + left-click or middle-click: orbit around focal point. */
|
/** Alt + left-click or middle-click: orbit around focal point. */
|
||||||
Orbit,
|
Orbit,
|
||||||
/** Dragging a gizmo handle. */
|
/** Dragging a gizmo handle. */
|
||||||
GizmoDrag
|
GizmoDrag,
|
||||||
|
/** Dragging a spline point handle. */
|
||||||
|
SplinePointDrag
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,6 +62,12 @@ public:
|
|||||||
/** Fired when a left-click is confirmed (mouse up without exceeding drag threshold). */
|
/** Fired when a left-click is confirmed (mouse up without exceeding drag threshold). */
|
||||||
FPS_Editor_OnClick OnEditorClick;
|
FPS_Editor_OnClick OnEditorClick;
|
||||||
|
|
||||||
|
/** Delete handler (public so UI buttons can call it). */
|
||||||
|
void HandleDelete(const FInputActionValue& Value);
|
||||||
|
|
||||||
|
/** Currently selected spline point index (-1 = none). Used by UI for Delete Point button. */
|
||||||
|
int32 ActiveSplinePointIndex = -1;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
@@ -180,6 +188,21 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UInputAction> IA_Paste;
|
TObjectPtr<UInputAction> IA_Paste;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_SplineMode;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_Confirm;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_Cancel;
|
||||||
|
|
||||||
|
// ---- Spline point drag state ----
|
||||||
|
TWeakObjectPtr<AActor> DraggedSplineActor;
|
||||||
|
TArray<FVector> SplinePointDragInitialState;
|
||||||
|
FVector SplinePointDragPlaneOrigin;
|
||||||
|
FVector SplinePointDragPlaneNormal;
|
||||||
|
|
||||||
// ---- Modifier key state ----
|
// ---- Modifier key state ----
|
||||||
bool bAltHeld = false;
|
bool bAltHeld = false;
|
||||||
bool bCtrlHeld = false;
|
bool bCtrlHeld = false;
|
||||||
@@ -209,13 +232,15 @@ private:
|
|||||||
void HandleTranslateMode(const FInputActionValue& Value);
|
void HandleTranslateMode(const FInputActionValue& Value);
|
||||||
void HandleRotateMode(const FInputActionValue& Value);
|
void HandleRotateMode(const FInputActionValue& Value);
|
||||||
void HandleScaleMode(const FInputActionValue& Value);
|
void HandleScaleMode(const FInputActionValue& Value);
|
||||||
void HandleDelete(const FInputActionValue& Value);
|
|
||||||
void HandleSnapToGround(const FInputActionValue& Value);
|
void HandleSnapToGround(const FInputActionValue& Value);
|
||||||
void HandleUndo(const FInputActionValue& Value);
|
void HandleUndo(const FInputActionValue& Value);
|
||||||
void HandleRedo(const FInputActionValue& Value);
|
void HandleRedo(const FInputActionValue& Value);
|
||||||
void HandleDuplicate(const FInputActionValue& Value);
|
void HandleDuplicate(const FInputActionValue& Value);
|
||||||
void HandleCopy(const FInputActionValue& Value);
|
void HandleCopy(const FInputActionValue& Value);
|
||||||
void HandlePaste(const FInputActionValue& Value);
|
void HandlePaste(const FInputActionValue& Value);
|
||||||
|
void HandleSplineMode(const FInputActionValue& Value);
|
||||||
|
void HandleConfirm(const FInputActionValue& Value);
|
||||||
|
void HandleCancel(const FInputActionValue& Value);
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
void CreateInputActions();
|
void CreateInputActions();
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
|
#include "PS_Editor_SpawnCatalog.h"
|
||||||
#include "PS_Editor_SceneSerializer.h"
|
#include "PS_Editor_SceneSerializer.h"
|
||||||
#include "PS_Editor_Gizmo.h"
|
#include "PS_Editor_Gizmo.h"
|
||||||
#include "PS_Editor_Pawn.h"
|
#include "PS_Editor_Pawn.h"
|
||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
|
#include "PS_Editor_PointPlaceable.h"
|
||||||
|
|
||||||
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
||||||
{
|
{
|
||||||
@@ -24,10 +27,26 @@ void APS_Editor_PlayerController::BeginPlay()
|
|||||||
|
|
||||||
UndoManager = NewObject<UPS_Editor_UndoManager>(this);
|
UndoManager = NewObject<UPS_Editor_UndoManager>(this);
|
||||||
|
|
||||||
// Load spawn catalog and create spawn manager
|
// Create spawn manager and load catalogs from MasterCatalog
|
||||||
SpawnManager = NewObject<UPS_Editor_SpawnManager>(this);
|
SpawnManager = NewObject<UPS_Editor_SpawnManager>(this);
|
||||||
UPS_Editor_SpawnCatalog* LoadedCatalog = SpawnCatalogAsset.LoadSynchronous();
|
SpawnManager->Initialize(this);
|
||||||
SpawnManager->Initialize(this, LoadedCatalog);
|
|
||||||
|
if (UPS_Editor_MasterCatalog* Master = MasterCatalogAsset.LoadSynchronous())
|
||||||
|
{
|
||||||
|
for (const TSoftObjectPtr<UPS_Editor_SpawnCatalog>& CatalogRef : Master->Catalogs)
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnCatalog* Catalog = CatalogRef.LoadSynchronous())
|
||||||
|
{
|
||||||
|
SpawnManager->AddCatalog(Catalog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: MasterCatalog loaded with %d sub-catalogs, %d total entries"),
|
||||||
|
Master->Catalogs.Num(), SpawnManager->GetResolvedEntries().Num());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: No MasterCatalog assigned."));
|
||||||
|
}
|
||||||
|
|
||||||
SceneSerializer = NewObject<UPS_Editor_SceneSerializer>(this);
|
SceneSerializer = NewObject<UPS_Editor_SceneSerializer>(this);
|
||||||
SceneSerializer->SetSelectionManager(SelectionManager);
|
SceneSerializer->SetSelectionManager(SelectionManager);
|
||||||
@@ -80,8 +99,175 @@ void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
APS_Editor_SplineActor* APS_Editor_PlayerController::GetActiveSplineActor() const
|
||||||
|
{
|
||||||
|
return Cast<APS_Editor_SplineActor>(ActivePlacementActor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActorClass)
|
||||||
|
{
|
||||||
|
if (SelectionManager)
|
||||||
|
{
|
||||||
|
SelectionManager->ClearSelection();
|
||||||
|
}
|
||||||
|
ActivePlacementActor = nullptr;
|
||||||
|
PlacementActorClass = InActorClass ? InActorClass : TSubclassOf<AActor>(APS_Editor_SplineActor::StaticClass());
|
||||||
|
bAppendingToActor = false;
|
||||||
|
AppendInitialPoints.Empty();
|
||||||
|
EditorMode = EPS_Editor_EditorMode::SplinePlacement;
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Point placement mode started (class: %s)"), *PlacementActorClass->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActor)
|
||||||
|
{
|
||||||
|
if (!ExistingActor || !ExistingActor->Implements<UPS_Editor_PointPlaceable>()) return;
|
||||||
|
|
||||||
|
ActivePlacementActor = ExistingActor;
|
||||||
|
bAppendingToActor = true;
|
||||||
|
EditorMode = EPS_Editor_EditorMode::SplinePlacement;
|
||||||
|
|
||||||
|
// Store initial state for undo (spline-specific)
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ExistingActor))
|
||||||
|
{
|
||||||
|
AppendInitialPoints = Spline->GetAllPointLocations();
|
||||||
|
Spline->SetHandlesVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Appending points to %s"), *ExistingActor->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::FinishPointPlacement()
|
||||||
|
{
|
||||||
|
// Just exit the mode — actor is already tracked, all edits already have undo
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
||||||
|
{
|
||||||
|
Spline->SetHandlesVisible(false);
|
||||||
|
Spline->ClearHandleHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ActivePlacementActor && SelectionManager)
|
||||||
|
{
|
||||||
|
SelectionManager->ClearSelection();
|
||||||
|
SelectionManager->SelectActor(ActivePlacementActor);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exited placement mode"));
|
||||||
|
|
||||||
|
ActivePlacementActor = nullptr;
|
||||||
|
bAppendingToActor = false;
|
||||||
|
AppendInitialPoints.Empty();
|
||||||
|
EditorMode = EPS_Editor_EditorMode::Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::CancelPointPlacement()
|
||||||
|
{
|
||||||
|
// Same as finish — just exit mode. Undo handles reverting if needed.
|
||||||
|
FinishPointPlacement();
|
||||||
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)
|
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)
|
||||||
{
|
{
|
||||||
|
if (EditorMode == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
// Deproject click to world position
|
||||||
|
FVector WorldOrigin, WorldDirection;
|
||||||
|
if (!DeprojectScreenPositionToWorld(ScreenPos.X, ScreenPos.Y, WorldOrigin, WorldDirection))
|
||||||
|
return;
|
||||||
|
|
||||||
|
FHitResult Hit;
|
||||||
|
FCollisionQueryParams Params;
|
||||||
|
Params.AddIgnoredActor(GetPawn());
|
||||||
|
if (ActivePlacementActor) Params.AddIgnoredActor(ActivePlacementActor);
|
||||||
|
|
||||||
|
const FVector End = WorldOrigin + WorldDirection * 100000.0f;
|
||||||
|
FVector ClickWorldPos;
|
||||||
|
if (GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Visibility, Params))
|
||||||
|
{
|
||||||
|
ClickWorldPos = Hit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClickWorldPos = WorldOrigin + WorldDirection * 1000.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ActivePlacementActor)
|
||||||
|
{
|
||||||
|
// First click: spawn the actor, track immediately, record undo
|
||||||
|
FActorSpawnParameters SpawnParams;
|
||||||
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
UClass* ClassToSpawn = PlacementActorClass ? PlacementActorClass.Get() : APS_Editor_SplineActor::StaticClass();
|
||||||
|
ActivePlacementActor = GetWorld()->SpawnActor<AActor>(ClassToSpawn, ClickWorldPos, FRotator::ZeroRotator, SpawnParams);
|
||||||
|
|
||||||
|
if (ActivePlacementActor)
|
||||||
|
{
|
||||||
|
// Track immediately (for outliner, save/load, scene clear)
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
SpawnManager->TrackSpawnedActor(ActivePlacementActor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record spawn undo immediately
|
||||||
|
if (UndoManager)
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
|
||||||
|
Action->Actors.Add(ActivePlacementActor);
|
||||||
|
Action->Description = TEXT("Create placeable actor");
|
||||||
|
UndoManager->RecordAction(Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we're "appending" to the tracked actor
|
||||||
|
bAppendingToActor = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we clicked on the existing spline tube → insert instead of append
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
||||||
|
{
|
||||||
|
if (Spline->GetNumPoints() >= 2)
|
||||||
|
{
|
||||||
|
// Raycast to check if we hit the tube mesh
|
||||||
|
FHitResult TubeHit;
|
||||||
|
FCollisionQueryParams TubeParams;
|
||||||
|
TubeParams.AddIgnoredActor(GetPawn());
|
||||||
|
const FVector TubeEnd = WorldOrigin + WorldDirection * 100000.0f;
|
||||||
|
|
||||||
|
if (GetWorld()->LineTraceSingleByChannel(TubeHit, WorldOrigin, TubeEnd, ECC_Visibility, TubeParams))
|
||||||
|
{
|
||||||
|
if (Spline->IsSplineMesh(TubeHit.GetComponent()))
|
||||||
|
{
|
||||||
|
const int32 InsertIdx = Spline->FindInsertIndex(TubeHit.ImpactPoint);
|
||||||
|
Spline->InsertPoint(InsertIdx, TubeHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f));
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Inserted point at index %d during placement"), InsertIdx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: add point at end via the interface (with undo)
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
||||||
|
{
|
||||||
|
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||||
|
Spline->AddPoint(ClickWorldPos);
|
||||||
|
|
||||||
|
if (UndoManager)
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
||||||
|
Action->SplineActor = Spline;
|
||||||
|
Action->OldPoints = OldPoints;
|
||||||
|
Action->NewPoints = Spline->GetAllPointLocations();
|
||||||
|
Action->Description = TEXT("Add spline point");
|
||||||
|
UndoManager->RecordAction(Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (IPS_Editor_PointPlaceable* Placeable = Cast<IPS_Editor_PointPlaceable>(ActivePlacementActor))
|
||||||
|
{
|
||||||
|
Placeable->AddPlacementPoint(ClickWorldPos);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal mode: selection
|
||||||
if (SelectionManager)
|
if (SelectionManager)
|
||||||
{
|
{
|
||||||
SelectionManager->HandleClickAtScreenPosition(ScreenPos, bAdditive);
|
SelectionManager->HandleClickAtScreenPosition(ScreenPos, bAdditive);
|
||||||
|
|||||||
@@ -2,13 +2,17 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "PS_Editor_SelectionTypes.h"
|
||||||
#include "PS_Editor_PlayerController.generated.h"
|
#include "PS_Editor_PlayerController.generated.h"
|
||||||
|
|
||||||
class UPS_Editor_SelectionManager;
|
class UPS_Editor_SelectionManager;
|
||||||
class UPS_Editor_UndoManager;
|
class UPS_Editor_UndoManager;
|
||||||
class UPS_Editor_SpawnManager;
|
class UPS_Editor_SpawnManager;
|
||||||
class UPS_Editor_SpawnCatalog;
|
class UPS_Editor_SpawnCatalog;
|
||||||
|
class UPS_Editor_MasterCatalog;
|
||||||
class UPS_Editor_SceneSerializer;
|
class UPS_Editor_SceneSerializer;
|
||||||
|
class APS_Editor_SplineActor;
|
||||||
|
class IPS_Editor_PointPlaceable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player controller for PS_Editor runtime editing.
|
* Player controller for PS_Editor runtime editing.
|
||||||
@@ -34,9 +38,41 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
UPS_Editor_SceneSerializer* GetSceneSerializer() const { return SceneSerializer; }
|
UPS_Editor_SceneSerializer* GetSceneSerializer() const { return SceneSerializer; }
|
||||||
|
|
||||||
/** The spawn catalog DataAsset to load. Set this in the Blueprint or World Settings. */
|
/** Master catalog grouping all sub-catalogs (Tools, Characters, Props...). */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||||
TSoftObjectPtr<UPS_Editor_SpawnCatalog> SpawnCatalogAsset;
|
TSoftObjectPtr<UPS_Editor_MasterCatalog> MasterCatalogAsset;
|
||||||
|
|
||||||
|
// ---- Editor Mode (Normal / SplinePlacement) ----
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
|
EPS_Editor_EditorMode GetEditorMode() const { return EditorMode; }
|
||||||
|
|
||||||
|
/** Enter point-placement mode for any actor implementing IPS_Editor_PointPlaceable. */
|
||||||
|
void BeginPointPlacement(TSubclassOf<AActor> InActorClass = nullptr);
|
||||||
|
|
||||||
|
/** Enter point-placement mode appending to an existing placeable actor. */
|
||||||
|
void BeginPointPlacementAppend(AActor* ExistingActor);
|
||||||
|
|
||||||
|
/** Finish current placement (min points required). Returns to Normal mode. */
|
||||||
|
void FinishPointPlacement();
|
||||||
|
|
||||||
|
/** Cancel current placement (destroys in-progress actor or restores state). */
|
||||||
|
void CancelPointPlacement();
|
||||||
|
|
||||||
|
/** Get the actor currently being placed/edited (nullptr if not in placement mode). */
|
||||||
|
AActor* GetActivePlacementActor() const { return ActivePlacementActor; }
|
||||||
|
|
||||||
|
/** True if we're appending to an existing actor (editing), false if creating new. */
|
||||||
|
bool IsAppendingToActor() const { return bAppendingToActor; }
|
||||||
|
|
||||||
|
// Legacy convenience (for spline-specific code like point editing)
|
||||||
|
APS_Editor_SplineActor* GetActiveSplineActor() const;
|
||||||
|
|
||||||
|
/** Convenience: enter point placement for an existing spline. */
|
||||||
|
void BeginSplineEditing(AActor* Spline) { BeginPointPlacementAppend(Spline); }
|
||||||
|
|
||||||
|
/** Convenience: exit spline editing. */
|
||||||
|
void FinishSplineEditing() { FinishPointPlacement(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
@@ -55,5 +91,20 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UPS_Editor_SceneSerializer> SceneSerializer;
|
TObjectPtr<UPS_Editor_SceneSerializer> SceneSerializer;
|
||||||
|
|
||||||
|
EPS_Editor_EditorMode EditorMode = EPS_Editor_EditorMode::Normal;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<AActor> ActivePlacementActor;
|
||||||
|
|
||||||
|
/** Class to use when spawning the placement actor. */
|
||||||
|
UPROPERTY()
|
||||||
|
TSubclassOf<AActor> PlacementActorClass;
|
||||||
|
|
||||||
|
/** True when appending to an existing actor (vs creating new). */
|
||||||
|
bool bAppendingToActor = false;
|
||||||
|
|
||||||
|
/** Snapshot of spline points before append, for undo. */
|
||||||
|
TArray<FVector> AppendInitialPoints;
|
||||||
|
|
||||||
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public class PS_Editor : ModuleRules
|
|||||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo"));
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo"));
|
||||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Spawn"));
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Spawn"));
|
||||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Serialization"));
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Serialization"));
|
||||||
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Spline"));
|
||||||
|
|
||||||
PublicDependencyModuleNames.AddRange(new string[]
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "Materials/MaterialExpressionLinearInterpolate.h"
|
#include "Materials/MaterialExpressionLinearInterpolate.h"
|
||||||
#include "Materials/MaterialExpressionComponentMask.h"
|
#include "Materials/MaterialExpressionComponentMask.h"
|
||||||
#include "Materials/MaterialExpressionAppendVector.h"
|
#include "Materials/MaterialExpressionAppendVector.h"
|
||||||
|
#include "Materials/MaterialExpressionVectorParameter.h"
|
||||||
#include "AssetRegistry/AssetRegistryModule.h"
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
#include "UObject/SavePackage.h"
|
#include "UObject/SavePackage.h"
|
||||||
#include "Misc/PackageName.h"
|
#include "Misc/PackageName.h"
|
||||||
@@ -218,6 +219,48 @@ static void CreateOutlineMaterialAsset()
|
|||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material auto-generated (8-sample, dynamic resolution)"));
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material auto-generated (8-sample, dynamic resolution)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void CreateSplineLineMaterialAsset()
|
||||||
|
{
|
||||||
|
const FString MaterialPath = TEXT("/PS_Editor/M_PS_Editor_SplineLine");
|
||||||
|
const FString AssetName = TEXT("M_PS_Editor_SplineLine");
|
||||||
|
|
||||||
|
if (LoadObject<UMaterial>(nullptr, *(MaterialPath + TEXT(".") + AssetName)))
|
||||||
|
{
|
||||||
|
return; // Already exists
|
||||||
|
}
|
||||||
|
|
||||||
|
UPackage* Package = CreatePackage(*MaterialPath);
|
||||||
|
UMaterial* Mat = NewObject<UMaterial>(Package, *AssetName, RF_Public | RF_Standalone);
|
||||||
|
Mat->MaterialDomain = EMaterialDomain::MD_Surface;
|
||||||
|
Mat->SetShadingModel(EMaterialShadingModel::MSM_Unlit);
|
||||||
|
Mat->BlendMode = EBlendMode::BLEND_Opaque;
|
||||||
|
Mat->TwoSided = true;
|
||||||
|
|
||||||
|
auto AddExpr = [&](UMaterialExpression* Expr) { Mat->GetExpressionCollection().AddExpression(Expr); };
|
||||||
|
|
||||||
|
// Vector parameter "Color" (default orange)
|
||||||
|
UMaterialExpressionVectorParameter* ColorParam = NewObject<UMaterialExpressionVectorParameter>(Mat);
|
||||||
|
ColorParam->ParameterName = TEXT("Color");
|
||||||
|
ColorParam->DefaultValue = FLinearColor(1.0f, 0.4f, 0.0f, 1.0f);
|
||||||
|
AddExpr(ColorParam);
|
||||||
|
ColorParam->MaterialExpressionEditorX = -200;
|
||||||
|
ColorParam->MaterialExpressionEditorY = 0;
|
||||||
|
|
||||||
|
// Connect Color → Emissive
|
||||||
|
Mat->GetEditorOnlyData()->EmissiveColor.Connect(0, ColorParam);
|
||||||
|
|
||||||
|
Mat->PreEditChange(nullptr);
|
||||||
|
Mat->PostEditChange();
|
||||||
|
|
||||||
|
FString PackageFilename = FPackageName::LongPackageNameToFilename(MaterialPath, FPackageName::GetAssetPackageExtension());
|
||||||
|
FSavePackageArgs SaveArgs;
|
||||||
|
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
||||||
|
UPackage::SavePackage(Package, Mat, *PackageFilename, SaveArgs);
|
||||||
|
FAssetRegistryModule::AssetCreated(Mat);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline line material auto-generated (unlit opaque with Color parameter)"));
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void FPS_EditorModule::StartupModule()
|
void FPS_EditorModule::StartupModule()
|
||||||
@@ -226,6 +269,7 @@ void FPS_EditorModule::StartupModule()
|
|||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateOutlineMaterialAsset);
|
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateOutlineMaterialAsset);
|
||||||
|
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateSplineLineMaterialAsset);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/Interface.h"
|
||||||
|
#include "PS_Editor_PointPlaceable.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for actors that use a click-to-place-points placement flow.
|
||||||
|
* Implement this on any actor class to get automatic placement mode
|
||||||
|
* when spawned from the PS_Editor catalogue.
|
||||||
|
*
|
||||||
|
* Examples: splines, patrol routes, area polygons, cable paths...
|
||||||
|
*/
|
||||||
|
UINTERFACE(MinimalAPI, BlueprintType)
|
||||||
|
class UPS_Editor_PointPlaceable : public UInterface
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
};
|
||||||
|
|
||||||
|
class PS_EDITOR_API IPS_Editor_PointPlaceable
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Called for each click during placement mode. */
|
||||||
|
virtual void AddPlacementPoint(const FVector& WorldPosition) = 0;
|
||||||
|
|
||||||
|
/** Minimum number of points required. Below this, finishing = cancelling. */
|
||||||
|
virtual int32 GetMinimumPoints() const { return 2; }
|
||||||
|
|
||||||
|
/** Current number of placed points. */
|
||||||
|
virtual int32 GetCurrentPointCount() const = 0;
|
||||||
|
|
||||||
|
/** Called when placement is finished (enough points placed). */
|
||||||
|
virtual void OnPlacementFinished() {}
|
||||||
|
|
||||||
|
/** Called when placement is cancelled (actor will be destroyed). */
|
||||||
|
virtual void OnPlacementCancelled() {}
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_EditableComponent.h"
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
#include "GameFramework/Pawn.h"
|
#include "GameFramework/Pawn.h"
|
||||||
@@ -185,6 +186,12 @@ void UPS_Editor_SelectionManager::SelectActor(AActor* Actor)
|
|||||||
UpdateGizmo();
|
UpdateGizmo();
|
||||||
OnSelectionChanged.Broadcast();
|
OnSelectionChanged.Broadcast();
|
||||||
|
|
||||||
|
// Mark spline as selected (handles shown only in SplineEditing mode, not here)
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
|
||||||
|
{
|
||||||
|
Spline->SetSelected(true);
|
||||||
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selected %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selected %s"), *Actor->GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +213,25 @@ void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
|||||||
UpdateGizmo();
|
UpdateGizmo();
|
||||||
OnSelectionChanged.Broadcast();
|
OnSelectionChanged.Broadcast();
|
||||||
|
|
||||||
|
// Unmark spline
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
|
||||||
|
{
|
||||||
|
Spline->SetHandlesVisible(false);
|
||||||
|
Spline->SetSelected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit spline editing mode if the edited spline is deselected
|
||||||
|
if (APlayerController* PC = OwnerPC.Get())
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||||
|
{
|
||||||
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement && EditorPC->GetActivePlacementActor() == Actor)
|
||||||
|
{
|
||||||
|
EditorPC->FinishSplineEditing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,6 +255,25 @@ void UPS_Editor_SelectionManager::ClearSelection()
|
|||||||
if (AActor* Actor = Weak.Get())
|
if (AActor* Actor = Weak.Get())
|
||||||
{
|
{
|
||||||
SetActorHighlight(Actor, false);
|
SetActorHighlight(Actor, false);
|
||||||
|
|
||||||
|
// Spline-specific cleanup
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
|
||||||
|
{
|
||||||
|
Spline->SetHandlesVisible(false);
|
||||||
|
Spline->SetSelected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit spline editing if needed
|
||||||
|
if (APlayerController* PC = OwnerPC.Get())
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||||
|
{
|
||||||
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
EditorPC->FinishSplineEditing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +314,15 @@ FVector UPS_Editor_SelectionManager::GetSelectionPivot() const
|
|||||||
{
|
{
|
||||||
if (AActor* Actor = Weak.Get())
|
if (AActor* Actor = Weak.Get())
|
||||||
{
|
{
|
||||||
Sum += Actor->GetActorLocation();
|
// For SplineActors, use the centroid of all points as pivot
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
|
||||||
|
{
|
||||||
|
Sum += Spline->GetCentroid();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Sum += Actor->GetActorLocation();
|
||||||
|
}
|
||||||
Count++;
|
Count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,7 +501,18 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
|
|||||||
|
|
||||||
if (IsAnythingSelected())
|
if (IsAnythingSelected())
|
||||||
{
|
{
|
||||||
Gizmo->SetGizmoActive(true, GetSelectionPivot());
|
FVector Pivot = GetSelectionPivot();
|
||||||
|
|
||||||
|
// Override pivot for SplineActors: use centroid so gizmo aligns with center cube
|
||||||
|
if (SelectedActors.Num() == 1)
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(SelectedActors[0].Get()))
|
||||||
|
{
|
||||||
|
Pivot = Spline->GetCentroid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Gizmo->SetGizmoActive(true, Pivot);
|
||||||
|
|
||||||
// Update gizmo orientation
|
// Update gizmo orientation
|
||||||
if (AActor* Actor = SelectedActors[0].Get())
|
if (AActor* Actor = SelectedActors[0].Get())
|
||||||
|
|||||||
@@ -33,3 +33,11 @@ enum class EPS_Editor_Space : uint8
|
|||||||
World,
|
World,
|
||||||
Local
|
Local
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Top-level editor mode (separate from transform mode). */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EPS_Editor_EditorMode : uint8
|
||||||
|
{
|
||||||
|
Normal, // Standard select/transform behavior
|
||||||
|
SplinePlacement // Creating or editing a point-placeable actor (spline, etc.)
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "UObject/UnrealType.h"
|
#include "UObject/UnrealType.h"
|
||||||
#include "Components/ActorComponent.h"
|
#include "Components/ActorComponent.h"
|
||||||
|
|
||||||
|
void FPS_Editor_SplinePointAction::Execute()
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = SplineActor.Get())
|
||||||
|
{
|
||||||
|
Spline->SetAllPointLocations(NewPoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FPS_Editor_SplinePointAction::Undo()
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = SplineActor.Get())
|
||||||
|
{
|
||||||
|
Spline->SetAllPointLocations(OldPoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void FPS_Editor_PropertyAction::ApplyValue(const FString& ValueStr)
|
void FPS_Editor_PropertyAction::ApplyValue(const FString& ValueStr)
|
||||||
{
|
{
|
||||||
AActor* Act = Actor.Get();
|
AActor* Act = Actor.Get();
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ enum class EPS_Editor_ActionType : uint8
|
|||||||
Transform,
|
Transform,
|
||||||
Delete,
|
Delete,
|
||||||
Spawn,
|
Spawn,
|
||||||
PropertyChange
|
PropertyChange,
|
||||||
|
SplinePointChange
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,6 +158,26 @@ struct FPS_Editor_SpawnAction : public FPS_Editor_Action
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class APS_Editor_SplineActor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a spline point state change (move, add, remove).
|
||||||
|
* Stores full state snapshots (all points before/after) for simplicity.
|
||||||
|
*/
|
||||||
|
struct FPS_Editor_SplinePointAction : public FPS_Editor_Action
|
||||||
|
{
|
||||||
|
FPS_Editor_SplinePointAction() : FPS_Editor_Action(EPS_Editor_ActionType::SplinePointChange) {}
|
||||||
|
|
||||||
|
TWeakObjectPtr<APS_Editor_SplineActor> SplineActor;
|
||||||
|
TArray<FVector> OldPoints;
|
||||||
|
TArray<FVector> NewPoints;
|
||||||
|
FString Description;
|
||||||
|
|
||||||
|
virtual void Execute() override;
|
||||||
|
virtual void Undo() override;
|
||||||
|
virtual FString GetDescription() const override { return Description; }
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records a single property value change on a component or actor.
|
* Records a single property value change on a component or actor.
|
||||||
* Stores text-serialized old/new values for undo/redo via ImportText.
|
* Stores text-serialized old/new values for undo/redo via ImportText.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_EditableComponent.h"
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "JsonObjectConverter.h"
|
#include "JsonObjectConverter.h"
|
||||||
#include "Misc/FileHelper.h"
|
#include "Misc/FileHelper.h"
|
||||||
#include "Misc/Paths.h"
|
#include "Misc/Paths.h"
|
||||||
@@ -36,6 +37,14 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
|||||||
ActorData.Rotation = Actor->GetActorRotation();
|
ActorData.Rotation = Actor->GetActorRotation();
|
||||||
ActorData.Scale = Actor->GetActorScale3D();
|
ActorData.Scale = Actor->GetActorScale3D();
|
||||||
|
|
||||||
|
// Export spline data
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
|
||||||
|
{
|
||||||
|
Spline->ExportToCustomProperties(ActorData.CustomProperties);
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exported spline with %d points, class=%s"),
|
||||||
|
Spline->GetNumPoints(), *ActorData.ClassPath);
|
||||||
|
}
|
||||||
|
|
||||||
// Export custom editable properties
|
// Export custom editable properties
|
||||||
if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
||||||
{
|
{
|
||||||
@@ -137,6 +146,11 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
{
|
{
|
||||||
UClass* LoadedClass = LoadObject<UClass>(nullptr, *ActorData.ClassPath);
|
UClass* LoadedClass = LoadObject<UClass>(nullptr, *ActorData.ClassPath);
|
||||||
if (!LoadedClass)
|
if (!LoadedClass)
|
||||||
|
{
|
||||||
|
// Fallback for C++ native classes (e.g. /Script/PS_Editor.PS_Editor_SplineActor)
|
||||||
|
LoadedClass = FindObject<UClass>(nullptr, *ActorData.ClassPath);
|
||||||
|
}
|
||||||
|
if (!LoadedClass)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load class: %s"), *ActorData.ClassPath);
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load class: %s"), *ActorData.ClassPath);
|
||||||
continue;
|
continue;
|
||||||
@@ -190,6 +204,12 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Import spline data
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(SpawnedActor))
|
||||||
|
{
|
||||||
|
Spline->ImportFromCustomProperties(ActorData.CustomProperties);
|
||||||
|
}
|
||||||
|
|
||||||
SpawnedCount++;
|
SpawnedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,3 +43,19 @@ public:
|
|||||||
void CaptureAllThumbnails();
|
void CaptureAllThumbnails();
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master catalog that groups multiple SpawnCatalogs.
|
||||||
|
* Assign this to the PlayerController. Each sub-catalog can represent a domain
|
||||||
|
* (Tools, Characters, Props, Vehicles...).
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class PS_EDITOR_API UPS_Editor_MasterCatalog : public UPrimaryDataAsset
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Sub-catalogs to load. Each one contributes its entries to the runtime spawn UI. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||||
|
TArray<TSoftObjectPtr<UPS_Editor_SpawnCatalog>> Catalogs;
|
||||||
|
};
|
||||||
|
|||||||
@@ -22,6 +22,25 @@ FText UPS_Editor_SpawnCatalogFactory::GetDisplayName() const
|
|||||||
return LOCTEXT("SpawnCatalogDisplayName", "PS Editor Spawn Catalog");
|
return LOCTEXT("SpawnCatalogDisplayName", "PS Editor Spawn Catalog");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Master Catalog Factory ----
|
||||||
|
|
||||||
|
UPS_Editor_MasterCatalogFactory::UPS_Editor_MasterCatalogFactory()
|
||||||
|
{
|
||||||
|
SupportedClass = UPS_Editor_MasterCatalog::StaticClass();
|
||||||
|
bCreateNew = true;
|
||||||
|
bEditAfterNew = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
UObject* UPS_Editor_MasterCatalogFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||||
|
{
|
||||||
|
return NewObject<UPS_Editor_MasterCatalog>(InParent, InClass, InName, Flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UPS_Editor_MasterCatalogFactory::GetDisplayName() const
|
||||||
|
{
|
||||||
|
return LOCTEXT("MasterCatalogDisplayName", "PS Editor Master Catalog");
|
||||||
|
}
|
||||||
|
|
||||||
#undef LOCTEXT_NAMESPACE
|
#undef LOCTEXT_NAMESPACE
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -19,4 +19,17 @@ public:
|
|||||||
virtual FText GetDisplayName() const override;
|
virtual FText GetDisplayName() const override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
UCLASS(HideCategories = Object)
|
||||||
|
class UPS_Editor_MasterCatalogFactory : public UFactory
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPS_Editor_MasterCatalogFactory();
|
||||||
|
|
||||||
|
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
|
||||||
|
virtual bool ShouldShowInNewMenu() const override { return true; }
|
||||||
|
virtual FText GetDisplayName() const override;
|
||||||
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -5,32 +5,18 @@
|
|||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
|
|
||||||
void UPS_Editor_SpawnManager::Initialize(APlayerController* InOwnerPC, UPS_Editor_SpawnCatalog* InCatalog)
|
void UPS_Editor_SpawnManager::Initialize(APlayerController* InOwnerPC)
|
||||||
{
|
{
|
||||||
OwnerPC = InOwnerPC;
|
OwnerPC = InOwnerPC;
|
||||||
Catalog = InCatalog;
|
|
||||||
|
|
||||||
if (Catalog)
|
|
||||||
{
|
|
||||||
ResolveCatalog();
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SpawnCatalog loaded with %d entries"), ResolvedEntries.Num());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: No SpawnCatalog assigned. Spawn catalogue will be empty."));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_SpawnManager::ResolveCatalog()
|
void UPS_Editor_SpawnManager::AddCatalog(UPS_Editor_SpawnCatalog* InCatalog)
|
||||||
{
|
{
|
||||||
ResolvedEntries.Empty();
|
if (!InCatalog) return;
|
||||||
|
|
||||||
if (!Catalog)
|
const int32 BeforeCount = ResolvedEntries.Num();
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const FPS_Editor_SpawnEntry& Entry : Catalog->Entries)
|
for (const FPS_Editor_SpawnEntry& Entry : InCatalog->Entries)
|
||||||
{
|
{
|
||||||
// Synchronously load the class
|
// Synchronously load the class
|
||||||
UClass* LoadedClass = Entry.ActorClass.LoadSynchronous();
|
UClass* LoadedClass = Entry.ActorClass.LoadSynchronous();
|
||||||
@@ -84,6 +70,9 @@ void UPS_Editor_SpawnManager::ResolveCatalog()
|
|||||||
|
|
||||||
ResolvedEntries.Add(Resolved);
|
ResolvedEntries.Add(Resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Added catalog with %d entries (total: %d)"),
|
||||||
|
ResolvedEntries.Num() - BeforeCount, ResolvedEntries.Num());
|
||||||
}
|
}
|
||||||
|
|
||||||
AActor* UPS_Editor_SpawnManager::SpawnFromCatalog(int32 Index)
|
AActor* UPS_Editor_SpawnManager::SpawnFromCatalog(int32 Index)
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ class PS_EDITOR_API UPS_Editor_SpawnManager : public UObject
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/** Initialize with owning controller and load catalog. */
|
/** Initialize with owning controller. */
|
||||||
void Initialize(APlayerController* InOwnerPC, UPS_Editor_SpawnCatalog* InCatalog);
|
void Initialize(APlayerController* InOwnerPC);
|
||||||
|
|
||||||
|
/** Load and add a catalog. Can be called multiple times for multiple catalogs. */
|
||||||
|
void AddCatalog(UPS_Editor_SpawnCatalog* InCatalog);
|
||||||
|
|
||||||
/** Spawn the actor at the given catalog index, positioned in front of the camera. Returns the spawned actor. */
|
/** Spawn the actor at the given catalog index, positioned in front of the camera. Returns the spawned actor. */
|
||||||
AActor* SpawnFromCatalog(int32 Index);
|
AActor* SpawnFromCatalog(int32 Index);
|
||||||
@@ -70,9 +73,6 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TWeakObjectPtr<APlayerController> OwnerPC;
|
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
TObjectPtr<UPS_Editor_SpawnCatalog> Catalog;
|
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TArray<FPS_Editor_ResolvedEntry> ResolvedEntries;
|
TArray<FPS_Editor_ResolvedEntry> ResolvedEntries;
|
||||||
|
|
||||||
@@ -80,8 +80,6 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TArray<TWeakObjectPtr<AActor>> SpawnedActors;
|
TArray<TWeakObjectPtr<AActor>> SpawnedActors;
|
||||||
|
|
||||||
/** Resolve all catalog entries (load classes, read SpawnableComponent metadata). */
|
|
||||||
void ResolveCatalog();
|
|
||||||
|
|
||||||
/** Compute spawn location in front of the camera. */
|
/** Compute spawn location in front of the camera. */
|
||||||
FVector ComputeSpawnLocation() const;
|
FVector ComputeSpawnLocation() const;
|
||||||
|
|||||||
@@ -0,0 +1,567 @@
|
|||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
|
#include "PS_Editor_SpawnableComponent.h"
|
||||||
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "Components/SplineComponent.h"
|
||||||
|
#include "ProceduralMeshComponent.h"
|
||||||
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
#include "Engine/StaticMesh.h"
|
||||||
|
#include "Materials/MaterialInstanceDynamic.h"
|
||||||
|
|
||||||
|
APS_Editor_SplineActor::APS_Editor_SplineActor()
|
||||||
|
{
|
||||||
|
PrimaryActorTick.bCanEverTick = false;
|
||||||
|
|
||||||
|
// Root
|
||||||
|
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||||
|
SetRootComponent(Root);
|
||||||
|
Root->SetMobility(EComponentMobility::Movable);
|
||||||
|
|
||||||
|
// Spline data
|
||||||
|
SplineComp = CreateDefaultSubobject<USplineComponent>(TEXT("SplineComp"));
|
||||||
|
SplineComp->SetupAttachment(Root);
|
||||||
|
SplineComp->ClearSplinePoints(false);
|
||||||
|
SplineComp->SetClosedLoop(false);
|
||||||
|
|
||||||
|
// Line mesh
|
||||||
|
SplineMeshComp = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("SplineMesh"));
|
||||||
|
SplineMeshComp->SetupAttachment(Root);
|
||||||
|
SplineMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
|
SplineMeshComp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||||
|
SplineMeshComp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||||
|
SplineMeshComp->bUseComplexAsSimpleCollision = true;
|
||||||
|
SplineMeshComp->SetCastShadow(false);
|
||||||
|
|
||||||
|
// Handle parent
|
||||||
|
HandleRoot = CreateDefaultSubobject<USceneComponent>(TEXT("HandleRoot"));
|
||||||
|
HandleRoot->SetupAttachment(Root);
|
||||||
|
|
||||||
|
// Spawnable: makes this actor appear in the catalogue
|
||||||
|
SpawnableComp = CreateDefaultSubobject<UPS_Editor_SpawnableComponent>(TEXT("PS_Editor_Spawnable"));
|
||||||
|
SpawnableComp->DisplayName = TEXT("Spline");
|
||||||
|
SpawnableComp->Category = TEXT("Tools");
|
||||||
|
|
||||||
|
// Editable: exposes SplineColor and SplineTag in the runtime property panel
|
||||||
|
EditableComp = CreateDefaultSubobject<UPS_Editor_EditableComponent>(TEXT("PS_Editor_Editable"));
|
||||||
|
EditableComp->EditableProperties.Add(FName(TEXT("SplineColor")));
|
||||||
|
EditableComp->EditableProperties.Add(FName(TEXT("SplineSelectedColor")));
|
||||||
|
EditableComp->EditableProperties.Add(FName(TEXT("SplineTag")));
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
// Gizmo material (no depth test, renders on top) for handles and selected state
|
||||||
|
UMaterialInterface* GizmoMat = LoadObject<UMaterialInterface>(
|
||||||
|
nullptr, TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
||||||
|
|
||||||
|
// Auto-generated opaque unlit material (depth tested) for spline line
|
||||||
|
UMaterialInterface* SplineLineMat = LoadObject<UMaterialInterface>(
|
||||||
|
nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine.M_PS_Editor_SplineLine"));
|
||||||
|
|
||||||
|
if (!SplineLineMat)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: M_PS_Editor_SplineLine not found, trying without double name..."));
|
||||||
|
SplineLineMat = LoadObject<UMaterialInterface>(nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spline line: normal depth-tested material (uses SplineColor property)
|
||||||
|
if (SplineLineMat)
|
||||||
|
{
|
||||||
|
SplineMat = UMaterialInstanceDynamic::Create(SplineLineMat, this);
|
||||||
|
SplineMat->SetVectorParameterValue(TEXT("Color"), SplineColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spline line selected: no depth test (uses SplineSelectedColor property)
|
||||||
|
if (GizmoMat)
|
||||||
|
{
|
||||||
|
SplineMatSelected = UMaterialInstanceDynamic::Create(GizmoMat, this);
|
||||||
|
SplineMatSelected->SetVectorParameterValue(TEXT("Color"), SplineSelectedColor);
|
||||||
|
|
||||||
|
// Handles always use gizmo material (on top, clickable)
|
||||||
|
HandleMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
|
||||||
|
HandleMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 1.0f, 1.0f)); // White
|
||||||
|
|
||||||
|
HandleHighlightMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
|
||||||
|
HandleHighlightMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 1.0f, 0.0f)); // Yellow
|
||||||
|
|
||||||
|
CenterCubeMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
|
||||||
|
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), SplineColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create center cube (selection handle for whole spline)
|
||||||
|
CenterCube = NewObject<UStaticMeshComponent>(this);
|
||||||
|
CenterCube->SetupAttachment(GetRootComponent());
|
||||||
|
CenterCube->RegisterComponent();
|
||||||
|
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||||
|
if (CubeMesh)
|
||||||
|
{
|
||||||
|
CenterCube->SetStaticMesh(CubeMesh);
|
||||||
|
}
|
||||||
|
CenterCube->SetWorldScale3D(FVector(CenterCubeScale));
|
||||||
|
CenterCube->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
|
CenterCube->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||||
|
CenterCube->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||||
|
CenterCube->SetCastShadow(false);
|
||||||
|
if (CenterCubeMat)
|
||||||
|
{
|
||||||
|
CenterCube->SetMaterial(0, CenterCubeMat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Point manipulation ----
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::AddPoint(const FVector& WorldPosition)
|
||||||
|
{
|
||||||
|
SplineComp->AddSplinePoint(WorldPosition, ESplineCoordinateSpace::World, true);
|
||||||
|
|
||||||
|
// Create handle
|
||||||
|
UStaticMeshComponent* Handle = CreatePointHandle(WorldPosition);
|
||||||
|
PointHandles.Add(Handle);
|
||||||
|
|
||||||
|
UpdateVisualization();
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::MovePoint(int32 Index, const FVector& NewWorldPosition)
|
||||||
|
{
|
||||||
|
if (Index < 0 || Index >= GetNumPoints()) return;
|
||||||
|
|
||||||
|
SplineComp->SetLocationAtSplinePoint(Index, NewWorldPosition, ESplineCoordinateSpace::World, true);
|
||||||
|
|
||||||
|
if (PointHandles.IsValidIndex(Index) && PointHandles[Index])
|
||||||
|
{
|
||||||
|
PointHandles[Index]->SetWorldLocation(NewWorldPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateVisualization();
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::RemovePoint(int32 Index)
|
||||||
|
{
|
||||||
|
if (Index < 0 || Index >= GetNumPoints()) return;
|
||||||
|
|
||||||
|
SplineComp->RemoveSplinePoint(Index, true);
|
||||||
|
|
||||||
|
if (PointHandles.IsValidIndex(Index))
|
||||||
|
{
|
||||||
|
if (PointHandles[Index])
|
||||||
|
{
|
||||||
|
PointHandles[Index]->DestroyComponent();
|
||||||
|
}
|
||||||
|
PointHandles.RemoveAt(Index);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateVisualization();
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 APS_Editor_SplineActor::GetNumPoints() const
|
||||||
|
{
|
||||||
|
return SplineComp ? SplineComp->GetNumberOfSplinePoints() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector APS_Editor_SplineActor::GetPointLocation(int32 Index) const
|
||||||
|
{
|
||||||
|
if (!SplineComp || Index < 0 || Index >= GetNumPoints()) return FVector::ZeroVector;
|
||||||
|
return SplineComp->GetLocationAtSplinePoint(Index, ESplineCoordinateSpace::World);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FVector> APS_Editor_SplineActor::GetAllPointLocations() const
|
||||||
|
{
|
||||||
|
TArray<FVector> Result;
|
||||||
|
const int32 Num = GetNumPoints();
|
||||||
|
Result.Reserve(Num);
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
Result.Add(GetPointLocation(i));
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::SetAllPointLocations(const TArray<FVector>& Points)
|
||||||
|
{
|
||||||
|
if (!SplineComp) return;
|
||||||
|
|
||||||
|
SplineComp->ClearSplinePoints(false);
|
||||||
|
for (const FVector& WorldPos : Points)
|
||||||
|
{
|
||||||
|
SplineComp->AddSplinePoint(WorldPos, ESplineCoordinateSpace::World, false);
|
||||||
|
}
|
||||||
|
SplineComp->UpdateSpline();
|
||||||
|
|
||||||
|
RebuildHandles();
|
||||||
|
UpdateVisualization();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Visualization ----
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::UpdateVisualization()
|
||||||
|
{
|
||||||
|
if (!SplineMeshComp || !SplineComp || GetNumPoints() < 2)
|
||||||
|
{
|
||||||
|
if (SplineMeshComp) SplineMeshComp->ClearAllMeshSections();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float SplineLength = SplineComp->GetSplineLength();
|
||||||
|
const int32 NumSamples = FMath::Max(2, FMath::CeilToInt(SplineLength / SampleInterval));
|
||||||
|
const int32 Segs = TubeSegments;
|
||||||
|
|
||||||
|
TArray<FVector> Vertices;
|
||||||
|
TArray<int32> Triangles;
|
||||||
|
TArray<FVector> Normals;
|
||||||
|
TArray<FVector2D> UVs;
|
||||||
|
|
||||||
|
// TubeSegments vertices per ring, NumSamples rings
|
||||||
|
Vertices.Reserve(NumSamples * Segs);
|
||||||
|
Normals.Reserve(NumSamples * Segs);
|
||||||
|
UVs.Reserve(NumSamples * Segs);
|
||||||
|
|
||||||
|
for (int32 i = 0; i < NumSamples; i++)
|
||||||
|
{
|
||||||
|
const float Distance = (static_cast<float>(i) / (NumSamples - 1)) * SplineLength;
|
||||||
|
const FVector Location = SplineComp->GetLocationAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::Local);
|
||||||
|
const FVector SplineRight = SplineComp->GetRightVectorAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::Local);
|
||||||
|
const FVector SplineUp = SplineComp->GetUpVectorAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::Local);
|
||||||
|
|
||||||
|
const float U = static_cast<float>(i) / (NumSamples - 1);
|
||||||
|
|
||||||
|
for (int32 j = 0; j < Segs; j++)
|
||||||
|
{
|
||||||
|
const float Angle = (static_cast<float>(j) / Segs) * 2.0f * PI;
|
||||||
|
const FVector Normal = FMath::Cos(Angle) * SplineRight + FMath::Sin(Angle) * SplineUp;
|
||||||
|
const FVector VertPos = Location + Normal * TubeRadius;
|
||||||
|
|
||||||
|
Vertices.Add(VertPos);
|
||||||
|
Normals.Add(Normal);
|
||||||
|
UVs.Add(FVector2D(U, static_cast<float>(j) / Segs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate triangles connecting consecutive rings
|
||||||
|
Triangles.Reserve((NumSamples - 1) * Segs * 6);
|
||||||
|
for (int32 i = 0; i < NumSamples - 1; i++)
|
||||||
|
{
|
||||||
|
for (int32 j = 0; j < Segs; j++)
|
||||||
|
{
|
||||||
|
const int32 NextJ = (j + 1) % Segs;
|
||||||
|
const int32 A = i * Segs + j;
|
||||||
|
const int32 B = i * Segs + NextJ;
|
||||||
|
const int32 C = (i + 1) * Segs + j;
|
||||||
|
const int32 D = (i + 1) * Segs + NextJ;
|
||||||
|
|
||||||
|
Triangles.Add(A); Triangles.Add(C); Triangles.Add(B);
|
||||||
|
Triangles.Add(B); Triangles.Add(C); Triangles.Add(D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SplineMeshComp->ClearAllMeshSections();
|
||||||
|
SplineMeshComp->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, TArray<FColor>(), TArray<FProcMeshTangent>(), true);
|
||||||
|
|
||||||
|
// Force collision and bounds update immediately
|
||||||
|
SplineMeshComp->UpdateBounds();
|
||||||
|
SplineMeshComp->MarkRenderStateDirty();
|
||||||
|
|
||||||
|
// Apply the current material (normal or selected)
|
||||||
|
// Check if handles are visible as a proxy for "selected" state
|
||||||
|
const bool bCurrentlySelected = (PointHandles.Num() > 0 && PointHandles[0] && PointHandles[0]->IsVisible());
|
||||||
|
UMaterialInstanceDynamic* LineMat = (bCurrentlySelected && SplineMatSelected) ? SplineMatSelected : SplineMat;
|
||||||
|
if (LineMat)
|
||||||
|
{
|
||||||
|
SplineMeshComp->SetMaterial(0, LineMat);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCenterCube();
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::UpdateCenterCube()
|
||||||
|
{
|
||||||
|
if (!CenterCube) return;
|
||||||
|
|
||||||
|
const int32 Num = GetNumPoints();
|
||||||
|
if (Num == 0)
|
||||||
|
{
|
||||||
|
CenterCube->SetVisibility(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position at centroid of all points
|
||||||
|
FVector Centroid = FVector::ZeroVector;
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
Centroid += GetPointLocation(i);
|
||||||
|
}
|
||||||
|
Centroid /= Num;
|
||||||
|
CenterCube->SetWorldLocation(Centroid);
|
||||||
|
CenterCube->SetVisibility(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Handle interaction ----
|
||||||
|
|
||||||
|
UStaticMeshComponent* APS_Editor_SplineActor::CreatePointHandle(const FVector& WorldPosition)
|
||||||
|
{
|
||||||
|
UStaticMeshComponent* Handle = NewObject<UStaticMeshComponent>(this);
|
||||||
|
Handle->SetupAttachment(HandleRoot);
|
||||||
|
Handle->RegisterComponent();
|
||||||
|
|
||||||
|
// Load sphere mesh
|
||||||
|
UStaticMesh* SphereMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
||||||
|
if (SphereMesh)
|
||||||
|
{
|
||||||
|
Handle->SetStaticMesh(SphereMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle->SetWorldLocation(WorldPosition);
|
||||||
|
Handle->SetWorldScale3D(FVector(HandleScale));
|
||||||
|
|
||||||
|
// Collision for raycast
|
||||||
|
Handle->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
|
Handle->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||||
|
Handle->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||||
|
|
||||||
|
Handle->SetCastShadow(false);
|
||||||
|
Handle->SetRenderInDepthPass(false);
|
||||||
|
|
||||||
|
if (HandleMat)
|
||||||
|
{
|
||||||
|
Handle->SetMaterial(0, HandleMat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::RebuildHandles()
|
||||||
|
{
|
||||||
|
// Destroy existing handles
|
||||||
|
for (UStaticMeshComponent* Handle : PointHandles)
|
||||||
|
{
|
||||||
|
if (Handle)
|
||||||
|
{
|
||||||
|
Handle->DestroyComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PointHandles.Empty();
|
||||||
|
|
||||||
|
// Create new handles at each point
|
||||||
|
const int32 Num = GetNumPoints();
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
UStaticMeshComponent* Handle = CreatePointHandle(GetPointLocation(i));
|
||||||
|
PointHandles.Add(Handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 APS_Editor_SplineActor::GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius) const
|
||||||
|
{
|
||||||
|
int32 BestIndex = -1;
|
||||||
|
float BestDist = MAX_FLT;
|
||||||
|
|
||||||
|
for (int32 i = 0; i < PointHandles.Num(); i++)
|
||||||
|
{
|
||||||
|
if (!PointHandles[i] || !PointHandles[i]->IsVisible()) continue;
|
||||||
|
|
||||||
|
const FVector HandlePos = PointHandles[i]->GetComponentLocation();
|
||||||
|
|
||||||
|
// Sphere-ray intersection
|
||||||
|
const FVector ToHandle = HandlePos - RayOrigin;
|
||||||
|
const float Proj = FVector::DotProduct(ToHandle, RayDirection);
|
||||||
|
if (Proj < 0.0f) continue; // Behind camera
|
||||||
|
|
||||||
|
const FVector ClosestPoint = RayOrigin + RayDirection * Proj;
|
||||||
|
const float Dist = FVector::Dist(ClosestPoint, HandlePos);
|
||||||
|
|
||||||
|
if (Dist < SphereRadius && Proj < BestDist)
|
||||||
|
{
|
||||||
|
BestDist = Proj;
|
||||||
|
BestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BestIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::SetHandlesVisible(bool bVisible)
|
||||||
|
{
|
||||||
|
for (UStaticMeshComponent* Handle : PointHandles)
|
||||||
|
{
|
||||||
|
if (Handle)
|
||||||
|
{
|
||||||
|
Handle->SetVisibility(bVisible);
|
||||||
|
Handle->SetCollisionEnabled(bVisible ? ECollisionEnabled::QueryOnly : ECollisionEnabled::NoCollision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::HighlightHandle(int32 Index)
|
||||||
|
{
|
||||||
|
for (int32 i = 0; i < PointHandles.Num(); i++)
|
||||||
|
{
|
||||||
|
if (PointHandles[i])
|
||||||
|
{
|
||||||
|
PointHandles[i]->SetMaterial(0, (i == Index) ? HandleHighlightMat : HandleMat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::ClearHandleHighlight()
|
||||||
|
{
|
||||||
|
for (UStaticMeshComponent* Handle : PointHandles)
|
||||||
|
{
|
||||||
|
if (Handle && HandleMat)
|
||||||
|
{
|
||||||
|
Handle->SetMaterial(0, HandleMat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::SetSelected(bool bSelected)
|
||||||
|
{
|
||||||
|
// Swap line material: depth-tested (normal) vs no-depth-test (on top when selected)
|
||||||
|
if (SplineMeshComp)
|
||||||
|
{
|
||||||
|
UMaterialInstanceDynamic* Mat = bSelected ? SplineMatSelected : SplineMat;
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SetSelected(%s) - SplineMat=%s, SplineMatSelected=%s, Using=%s"),
|
||||||
|
bSelected ? TEXT("true") : TEXT("false"),
|
||||||
|
SplineMat ? TEXT("valid") : TEXT("NULL"),
|
||||||
|
SplineMatSelected ? TEXT("valid") : TEXT("NULL"),
|
||||||
|
Mat ? TEXT("valid") : TEXT("NULL"));
|
||||||
|
if (Mat)
|
||||||
|
{
|
||||||
|
SplineMeshComp->SetMaterial(0, Mat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center cube: selected color when selected, normal color otherwise
|
||||||
|
if (CenterCubeMat)
|
||||||
|
{
|
||||||
|
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), bSelected ? SplineSelectedColor : SplineColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool APS_Editor_SplineActor::IsCenterCube(const UPrimitiveComponent* Comp) const
|
||||||
|
{
|
||||||
|
return Comp && Comp == CenterCube;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::UpdateSplineMaterials()
|
||||||
|
{
|
||||||
|
if (SplineMat)
|
||||||
|
{
|
||||||
|
SplineMat->SetVectorParameterValue(TEXT("Color"), SplineColor);
|
||||||
|
}
|
||||||
|
if (SplineMatSelected)
|
||||||
|
{
|
||||||
|
SplineMatSelected->SetVectorParameterValue(TEXT("Color"), SplineSelectedColor);
|
||||||
|
}
|
||||||
|
if (CenterCubeMat)
|
||||||
|
{
|
||||||
|
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), SplineColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool APS_Editor_SplineActor::IsSplineMesh(const UPrimitiveComponent* Comp) const
|
||||||
|
{
|
||||||
|
return Comp && Comp == SplineMeshComp;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 APS_Editor_SplineActor::FindInsertIndex(const FVector& WorldLocation) const
|
||||||
|
{
|
||||||
|
if (!SplineComp || GetNumPoints() < 2) return GetNumPoints();
|
||||||
|
|
||||||
|
const float InputKey = SplineComp->FindInputKeyClosestToWorldLocation(WorldLocation);
|
||||||
|
// Insert after the floor of the input key (between that point and the next)
|
||||||
|
return FMath::Clamp(FMath::FloorToInt(InputKey) + 1, 1, GetNumPoints());
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::InsertPoint(int32 Index, const FVector& WorldPosition)
|
||||||
|
{
|
||||||
|
if (!SplineComp) return;
|
||||||
|
|
||||||
|
Index = FMath::Clamp(Index, 0, GetNumPoints());
|
||||||
|
|
||||||
|
const FVector LocalPos = GetActorTransform().InverseTransformPosition(WorldPosition);
|
||||||
|
|
||||||
|
// Use AddSplinePointAtIndex via setting all points (USplineComponent doesn't have a direct insert-at-index in all versions)
|
||||||
|
TArray<FVector> Points = GetAllPointLocations();
|
||||||
|
Points.Insert(WorldPosition, Index);
|
||||||
|
SetAllPointLocations(Points);
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector APS_Editor_SplineActor::GetCentroid() const
|
||||||
|
{
|
||||||
|
const int32 Num = GetNumPoints();
|
||||||
|
if (Num == 0) return GetActorLocation();
|
||||||
|
|
||||||
|
FVector Centroid = FVector::ZeroVector;
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
Centroid += GetPointLocation(i);
|
||||||
|
}
|
||||||
|
return Centroid / Num;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Serialization ----
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::ExportToCustomProperties(TMap<FString, FString>& OutProperties) const
|
||||||
|
{
|
||||||
|
const int32 Num = GetNumPoints();
|
||||||
|
OutProperties.Add(TEXT("SplinePointCount"), FString::FromInt(Num));
|
||||||
|
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
const FVector Loc = GetPointLocation(i);
|
||||||
|
OutProperties.Add(
|
||||||
|
FString::Printf(TEXT("SplinePoint_%d"), i),
|
||||||
|
FString::Printf(TEXT("X=%.4f Y=%.4f Z=%.4f"), Loc.X, Loc.Y, Loc.Z));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save color and tag
|
||||||
|
OutProperties.Add(TEXT("SplineColor"), SplineColor.ToString());
|
||||||
|
OutProperties.Add(TEXT("SplineSelectedColor"), SplineSelectedColor.ToString());
|
||||||
|
if (!SplineTag.IsEmpty())
|
||||||
|
{
|
||||||
|
OutProperties.Add(TEXT("SplineTag"), SplineTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::ImportFromCustomProperties(const TMap<FString, FString>& Properties)
|
||||||
|
{
|
||||||
|
const FString* CountStr = Properties.Find(TEXT("SplinePointCount"));
|
||||||
|
if (!CountStr) return;
|
||||||
|
|
||||||
|
const int32 Num = FCString::Atoi(**CountStr);
|
||||||
|
if (Num < 2) return;
|
||||||
|
|
||||||
|
TArray<FVector> Points;
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
const FString Key = FString::Printf(TEXT("SplinePoint_%d"), i);
|
||||||
|
const FString* ValStr = Properties.Find(Key);
|
||||||
|
if (!ValStr) continue;
|
||||||
|
|
||||||
|
FVector Loc;
|
||||||
|
if (Loc.InitFromString(*ValStr))
|
||||||
|
{
|
||||||
|
Points.Add(Loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Points.Num() >= 2)
|
||||||
|
{
|
||||||
|
SetAllPointLocations(Points);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore color and tag
|
||||||
|
if (const FString* ColorStr = Properties.Find(TEXT("SplineColor")))
|
||||||
|
{
|
||||||
|
SplineColor.InitFromString(*ColorStr);
|
||||||
|
}
|
||||||
|
if (const FString* SelColorStr = Properties.Find(TEXT("SplineSelectedColor")))
|
||||||
|
{
|
||||||
|
SplineSelectedColor.InitFromString(*SelColorStr);
|
||||||
|
}
|
||||||
|
if (const FString* TagStr = Properties.Find(TEXT("SplineTag")))
|
||||||
|
{
|
||||||
|
SplineTag = *TagStr;
|
||||||
|
}
|
||||||
|
UpdateSplineMaterials();
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "PS_Editor_PointPlaceable.h"
|
||||||
|
#include "PS_Editor_SplineActor.generated.h"
|
||||||
|
|
||||||
|
class USplineComponent;
|
||||||
|
class UProceduralMeshComponent;
|
||||||
|
class UPS_Editor_SpawnableComponent;
|
||||||
|
class UPS_Editor_EditableComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime spline actor for path/route editing in PS_Editor.
|
||||||
|
* Renders as a visible quad strip with clickable point handles.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class PS_EDITOR_API APS_Editor_SplineActor : public AActor, public IPS_Editor_PointPlaceable
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
APS_Editor_SplineActor();
|
||||||
|
|
||||||
|
// ---- IPS_Editor_PointPlaceable interface ----
|
||||||
|
virtual void AddPlacementPoint(const FVector& WorldPosition) override { AddPoint(WorldPosition); }
|
||||||
|
virtual int32 GetMinimumPoints() const override { return 2; }
|
||||||
|
virtual int32 GetCurrentPointCount() const override { return GetNumPoints(); }
|
||||||
|
|
||||||
|
/** Get the underlying USplineComponent (for AI path following, etc.). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS Editor|Spline")
|
||||||
|
USplineComponent* GetSplineComponent() const { return SplineComp; }
|
||||||
|
|
||||||
|
/** Spline display color (override in Blueprint subclasses for variants). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline")
|
||||||
|
FLinearColor SplineColor = FLinearColor(1.0f, 0.4f, 0.0f); // Orange default
|
||||||
|
|
||||||
|
/** Spline selected color (brighter version). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline")
|
||||||
|
FLinearColor SplineSelectedColor = FLinearColor(1.0f, 0.6f, 0.0f);
|
||||||
|
|
||||||
|
/** Custom tag for gameplay identification (e.g. "Enemy", "Civilian", "Patrol"). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline")
|
||||||
|
FString SplineTag;
|
||||||
|
|
||||||
|
// ---- Point manipulation ----
|
||||||
|
|
||||||
|
/** Add a point at the end of the spline (world space). */
|
||||||
|
void AddPoint(const FVector& WorldPosition);
|
||||||
|
|
||||||
|
/** Move an existing point to a new world position. */
|
||||||
|
void MovePoint(int32 Index, const FVector& NewWorldPosition);
|
||||||
|
|
||||||
|
/** Remove a point by index. */
|
||||||
|
void RemovePoint(int32 Index);
|
||||||
|
|
||||||
|
/** Get the number of spline points. */
|
||||||
|
int32 GetNumPoints() const;
|
||||||
|
|
||||||
|
/** Get the world position of a spline point. */
|
||||||
|
FVector GetPointLocation(int32 Index) const;
|
||||||
|
|
||||||
|
/** Get all point world positions (for undo snapshots). */
|
||||||
|
TArray<FVector> GetAllPointLocations() const;
|
||||||
|
|
||||||
|
/** Set all point positions at once (for undo restore). Rebuilds handles and visualization. */
|
||||||
|
void SetAllPointLocations(const TArray<FVector>& Points);
|
||||||
|
|
||||||
|
// ---- Visualization ----
|
||||||
|
|
||||||
|
/** Regenerate the line mesh and reposition all handles. */
|
||||||
|
void UpdateVisualization();
|
||||||
|
|
||||||
|
/** Re-apply SplineColor to materials (call after changing SplineColor at runtime). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS Editor|Spline")
|
||||||
|
void UpdateSplineMaterials();
|
||||||
|
|
||||||
|
// ---- Handle interaction ----
|
||||||
|
|
||||||
|
/** Find which point handle is under the cursor ray. Returns -1 if none. */
|
||||||
|
int32 GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius = 15.0f) const;
|
||||||
|
|
||||||
|
/** Show or hide all point handles and center cube. */
|
||||||
|
void SetHandlesVisible(bool bVisible);
|
||||||
|
|
||||||
|
/** Set the spline's selected state (changes line + cube color). */
|
||||||
|
void SetSelected(bool bSelected);
|
||||||
|
|
||||||
|
/** Returns true if the given component is the center cube. */
|
||||||
|
bool IsCenterCube(const UPrimitiveComponent* Comp) const;
|
||||||
|
|
||||||
|
/** Highlight a specific handle (yellow), others go back to normal (white). */
|
||||||
|
void HighlightHandle(int32 Index);
|
||||||
|
|
||||||
|
/** Clear all handle highlights. */
|
||||||
|
void ClearHandleHighlight();
|
||||||
|
|
||||||
|
// ---- Serialization ----
|
||||||
|
|
||||||
|
/** Returns true if the given component is the spline line mesh. */
|
||||||
|
bool IsSplineMesh(const UPrimitiveComponent* Comp) const;
|
||||||
|
|
||||||
|
/** Find the best insertion index for a new point at the given world location. */
|
||||||
|
int32 FindInsertIndex(const FVector& WorldLocation) const;
|
||||||
|
|
||||||
|
/** Insert a point at a specific index. */
|
||||||
|
void InsertPoint(int32 Index, const FVector& WorldPosition);
|
||||||
|
|
||||||
|
/** Get the centroid of all spline points (world space). Used as pivot for gizmo. */
|
||||||
|
FVector GetCentroid() const;
|
||||||
|
|
||||||
|
/** Export spline data to a key-value map (for JSON serialization). */
|
||||||
|
void ExportToCustomProperties(TMap<FString, FString>& OutProperties) const;
|
||||||
|
|
||||||
|
/** Import spline data from a key-value map (for scene loading). */
|
||||||
|
void ImportFromCustomProperties(const TMap<FString, FString>& Properties);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<USplineComponent> SplineComp;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UProceduralMeshComponent> SplineMeshComp;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<USceneComponent> HandleRoot;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<TObjectPtr<UStaticMeshComponent>> PointHandles;
|
||||||
|
|
||||||
|
// Materials
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> SplineMat; // Normal (depth tested)
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> SplineMatSelected; // Selected (no depth test, on top)
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> HandleMat;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> HandleHighlightMat;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UStaticMeshComponent> CenterCube;
|
||||||
|
|
||||||
|
/** Create a handle sphere for a point at the given world position. */
|
||||||
|
UStaticMeshComponent* CreatePointHandle(const FVector& WorldPosition);
|
||||||
|
|
||||||
|
/** Rebuild all handles from current spline points. */
|
||||||
|
void RebuildHandles();
|
||||||
|
|
||||||
|
/** Update center cube position to the centroid of all points. */
|
||||||
|
void UpdateCenterCube();
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> CenterCubeMat;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UPS_Editor_SpawnableComponent> SpawnableComp;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UPS_Editor_EditableComponent> EditableComp;
|
||||||
|
|
||||||
|
/** Tube rendering settings. */
|
||||||
|
float TubeRadius = 2.0f;
|
||||||
|
int32 TubeSegments = 8;
|
||||||
|
float SampleInterval = 10.0f;
|
||||||
|
float HandleScale = 0.12f;
|
||||||
|
float CenterCubeScale = 0.10f;
|
||||||
|
};
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_EditableComponent.h"
|
#include "PS_Editor_EditableComponent.h"
|
||||||
#include "PS_Editor_SpawnableComponent.h"
|
#include "PS_Editor_SpawnableComponent.h"
|
||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
|
#include "PS_Editor_Pawn.h"
|
||||||
|
#include "PS_Editor_PointPlaceable.h"
|
||||||
#include "Widgets/SBoxPanel.h"
|
#include "Widgets/SBoxPanel.h"
|
||||||
#include "Widgets/Text/STextBlock.h"
|
#include "Widgets/Text/STextBlock.h"
|
||||||
#include "Widgets/Layout/SBorder.h"
|
#include "Widgets/Layout/SBorder.h"
|
||||||
@@ -115,20 +118,99 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
BuildOutliner()
|
BuildOutliner()
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
// Bottom help bar
|
// Bottom notification / help bar
|
||||||
+ SOverlay::Slot()
|
+ SOverlay::Slot()
|
||||||
.HAlign(HAlign_Center)
|
.HAlign(HAlign_Center)
|
||||||
.VAlign(VAlign_Bottom)
|
.VAlign(VAlign_Bottom)
|
||||||
.Padding(16.0f)
|
.Padding(16.0f)
|
||||||
[
|
[
|
||||||
SNew(SBorder)
|
SNew(SBorder)
|
||||||
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.75f))
|
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||||
.Padding(FMargin(16.0f, 6.0f))
|
.Padding(FMargin(16.0f, 6.0f))
|
||||||
[
|
[
|
||||||
SNew(STextBlock)
|
SNew(SHorizontalBox)
|
||||||
.Text(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | Z/E/R: Translate/Rotate/Scale | End: Snap | Del: Delete")))
|
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
|
||||||
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f))
|
[
|
||||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
SAssignNew(HelpBarText, STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | Z/E/R: Translate/Rotate/Scale | End: Snap | Del: Delete | S: Spline")))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(12.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SAssignNew(SplineEditButton, SButton)
|
||||||
|
.Visibility(EVisibility::Collapsed)
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
EditorPC->FinishSplineEditing();
|
||||||
|
}
|
||||||
|
else if (UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get())
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SelMgr->GetSelectedActors())
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Weak.Get()))
|
||||||
|
{
|
||||||
|
EditorPC->BeginSplineEditing(Spline);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Edit Spline")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
|
||||||
|
.ColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SAssignNew(SplineDeletePointButton, SButton)
|
||||||
|
.Visibility(EVisibility::Collapsed)
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
// Simulate Delete key press for the selected spline point
|
||||||
|
if (APS_Editor_Pawn* Pawn = Cast<APS_Editor_Pawn>(GetOwningPlayerPawn()))
|
||||||
|
{
|
||||||
|
Pawn->HandleDelete(FInputActionValue());
|
||||||
|
}
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Delete Point")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
|
||||||
|
.ColorAndOpacity(FLinearColor(1.0f, 0.3f, 0.2f))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SAssignNew(SplineFinishButton, SButton)
|
||||||
|
.Visibility(EVisibility::Collapsed)
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
EditorPC->FinishPointPlacement();
|
||||||
|
else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
EditorPC->FinishSplineEditing();
|
||||||
|
}
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Done")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.3f, 0.9f, 0.3f))
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -647,10 +729,24 @@ void UPS_Editor_MainWidget::PopulateSpawnCatalog()
|
|||||||
SNew(SButton)
|
SNew(SButton)
|
||||||
.OnClicked_Lambda([this, EntryIndex]() -> FReply
|
.OnClicked_Lambda([this, EntryIndex]() -> FReply
|
||||||
{
|
{
|
||||||
if (UPS_Editor_SpawnManager* Mgr = SpawnManager.Get())
|
UPS_Editor_SpawnManager* Mgr = SpawnManager.Get();
|
||||||
|
if (!Mgr) return FReply::Handled();
|
||||||
|
|
||||||
|
// Check if this actor implements IPS_Editor_PointPlaceable → enter placement mode
|
||||||
|
const TArray<FPS_Editor_ResolvedEntry>& AllEntries = Mgr->GetResolvedEntries();
|
||||||
|
if (AllEntries.IsValidIndex(EntryIndex) && AllEntries[EntryIndex].ActorClass)
|
||||||
{
|
{
|
||||||
Mgr->SpawnFromCatalog(EntryIndex);
|
if (AllEntries[EntryIndex].ActorClass->ImplementsInterface(UPS_Editor_PointPlaceable::StaticClass()))
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
EditorPC->BeginPointPlacement(AllEntries[EntryIndex].ActorClass);
|
||||||
|
}
|
||||||
|
return FReply::Handled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mgr->SpawnFromCatalog(EntryIndex);
|
||||||
return FReply::Handled();
|
return FReply::Handled();
|
||||||
})
|
})
|
||||||
[
|
[
|
||||||
@@ -662,6 +758,7 @@ void UPS_Editor_MainWidget::PopulateSpawnCatalog()
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
||||||
@@ -682,17 +779,92 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update mode text
|
// Update mode text + help bar based on editor mode
|
||||||
|
EPS_Editor_EditorMode CurrentEditorMode = EPS_Editor_EditorMode::Normal;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
CurrentEditorMode = EditorPC->GetEditorMode();
|
||||||
|
}
|
||||||
|
|
||||||
if (ModeText.IsValid())
|
if (ModeText.IsValid())
|
||||||
{
|
{
|
||||||
FString ModeStr;
|
if (CurrentEditorMode == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
switch (SM->GetTransformMode())
|
|
||||||
{
|
{
|
||||||
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (Z)"); break;
|
ModeText->SetText(FText::FromString(TEXT("Spline Placement")));
|
||||||
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (E)"); break;
|
|
||||||
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (R)"); break;
|
|
||||||
}
|
}
|
||||||
ModeText->SetText(FText::FromString(ModeStr));
|
else
|
||||||
|
{
|
||||||
|
FString ModeStr;
|
||||||
|
switch (SM->GetTransformMode())
|
||||||
|
{
|
||||||
|
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (Z)"); break;
|
||||||
|
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (E)"); break;
|
||||||
|
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (R)"); break;
|
||||||
|
}
|
||||||
|
ModeText->SetText(FText::FromString(ModeStr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if a spline is selected (for Edit button)
|
||||||
|
bool bSplineSelected = false;
|
||||||
|
if (CurrentEditorMode == EPS_Editor_EditorMode::Normal && SM->IsAnythingSelected())
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
|
{
|
||||||
|
if (Cast<APS_Editor_SplineActor>(Weak.Get()))
|
||||||
|
{
|
||||||
|
bSplineSelected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update help bar text and button visibility
|
||||||
|
if (CurrentEditorMode == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
bool bPointSelected = false;
|
||||||
|
if (APS_Editor_Pawn* Pawn = Cast<APS_Editor_Pawn>(GetOwningPlayerPawn()))
|
||||||
|
{
|
||||||
|
bPointSelected = (Pawn->ActiveSplinePointIndex >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HelpBarText.IsValid())
|
||||||
|
{
|
||||||
|
if (bPointSelected)
|
||||||
|
{
|
||||||
|
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Drag gizmo to move | Del: delete point | Click line: insert | Click outside: add | Esc/Done: finish")));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Click to add points | Click handle to select | Click line to insert | End: snap | Esc/Done: finish")));
|
||||||
|
}
|
||||||
|
HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f));
|
||||||
|
}
|
||||||
|
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
|
||||||
|
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(bPointSelected ? EVisibility::Visible : EVisibility::Collapsed);
|
||||||
|
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible);
|
||||||
|
}
|
||||||
|
else if (bSplineSelected)
|
||||||
|
{
|
||||||
|
if (HelpBarText.IsValid())
|
||||||
|
{
|
||||||
|
HelpBarText->SetText(FText::FromString(TEXT("Spline selected — click Edit to modify points | S: add points")));
|
||||||
|
HelpBarText->SetColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.5f));
|
||||||
|
}
|
||||||
|
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Visible);
|
||||||
|
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed);
|
||||||
|
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Collapsed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (HelpBarText.IsValid())
|
||||||
|
{
|
||||||
|
HelpBarText->SetText(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | Z/E/R: Translate/Rotate/Scale | End: Snap | Del: Delete | S: Spline")));
|
||||||
|
HelpBarText->SetColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f));
|
||||||
|
}
|
||||||
|
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
|
||||||
|
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed);
|
||||||
|
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Collapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update space button text
|
// Update space button text
|
||||||
@@ -877,16 +1049,17 @@ void UPS_Editor_MainWidget::RefreshOutliner()
|
|||||||
AActor* Actor = SpawnedActors[i].Get();
|
AActor* Actor = SpawnedActors[i].Get();
|
||||||
if (!Actor || Actor->IsHidden()) continue;
|
if (!Actor || Actor->IsHidden()) continue;
|
||||||
|
|
||||||
// Build display name: SpawnableComponent DisplayName > Class name (cleaned)
|
// Build display name
|
||||||
FString DisplayName;
|
FString DisplayName;
|
||||||
if (UActorComponent* SpawnComp = Actor->FindComponentByClass(UPS_Editor_SpawnableComponent::StaticClass()))
|
if (Cast<APS_Editor_SplineActor>(Actor))
|
||||||
{
|
{
|
||||||
if (UPS_Editor_SpawnableComponent* SC = Cast<UPS_Editor_SpawnableComponent>(SpawnComp))
|
DisplayName = TEXT("Spline");
|
||||||
|
}
|
||||||
|
else if (UPS_Editor_SpawnableComponent* SC = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
|
||||||
|
{
|
||||||
|
if (!SC->DisplayName.IsEmpty())
|
||||||
{
|
{
|
||||||
if (!SC->DisplayName.IsEmpty())
|
DisplayName = SC->DisplayName;
|
||||||
{
|
|
||||||
DisplayName = SC->DisplayName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (DisplayName.IsEmpty())
|
if (DisplayName.IsEmpty())
|
||||||
|
|||||||
@@ -87,6 +87,12 @@ private:
|
|||||||
TSharedPtr<SVerticalBox> OutlinerListContainer;
|
TSharedPtr<SVerticalBox> OutlinerListContainer;
|
||||||
int32 LastOutlinerActorCount = -1;
|
int32 LastOutlinerActorCount = -1;
|
||||||
|
|
||||||
|
// Bottom notification bar
|
||||||
|
TSharedPtr<STextBlock> HelpBarText;
|
||||||
|
TSharedPtr<SWidget> SplineFinishButton;
|
||||||
|
TSharedPtr<SWidget> SplineEditButton;
|
||||||
|
TSharedPtr<SWidget> SplineDeletePointButton;
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
TSharedRef<SWidget> BuildToolbar();
|
TSharedRef<SWidget> BuildToolbar();
|
||||||
TSharedRef<SWidget> BuildPropertiesPanel();
|
TSharedRef<SWidget> BuildPropertiesPanel();
|
||||||
|
|||||||
Reference in New Issue
Block a user