Add runtime spline creation and editing (WIP)
Spline placement: New Spline button (S key) enters placement mode where clicks add control points. Right-click/Enter finishes, Escape cancels. Min 2 points. Spline visualization as cross-shaped quad strip (vertical + horizontal ribbons). Point editing: select spline to show handles, click a handle to position gizmo on it, then use translate gizmo to move individual points. Undo supported via full-state snapshots (SplinePointAction). Visual feedback: auto-generated M_PS_Editor_SplineLine material (unlit opaque with depth test) for normal state. Gizmo material (no depth test) for selected state. Center cube at centroid for whole-spline selection. Handles highlight on selection. Serialization: spline points exported to CustomProperties (PointCount + Point_N). C++ class fallback via FindObject for LoadScene. Spline appears in scene outliner. Still WIP: depth material swap on selection, serialization edge cases, and point editing polish to be refined. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
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.
@@ -13,6 +13,7 @@
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "PS_Editor_ConfirmDialog.h"
|
||||
|
||||
@@ -199,6 +200,19 @@ void APS_Editor_Pawn::CreateInputActions()
|
||||
IA_Paste = NewObject<UInputAction>(this, TEXT("IA_Paste"));
|
||||
IA_Paste->ValueType = EInputActionValueType::Boolean;
|
||||
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)
|
||||
@@ -242,6 +256,9 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp
|
||||
EIC->BindAction(IA_Duplicate, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDuplicate);
|
||||
EIC->BindAction(IA_Copy, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCopy);
|
||||
EIC->BindAction(IA_Paste, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandlePaste);
|
||||
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 ----
|
||||
@@ -357,11 +374,26 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
}
|
||||
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 +464,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)
|
||||
{
|
||||
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
|
||||
@@ -447,6 +502,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)
|
||||
@@ -502,6 +576,60 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for spline point handle hit → select point, move gizmo to it (no drag yet)
|
||||
if (APS_Editor_PlayerController* EdPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (EdPC->GetEditorMode() == EPS_Editor_EditorMode::Normal)
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager())
|
||||
{
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||
{
|
||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Weak.Get()))
|
||||
{
|
||||
// First check: did we hit the center cube? If so, skip handle detection
|
||||
// (let the click fall through to normal selection → gizmo at actor pivot)
|
||||
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()))
|
||||
{
|
||||
// Center cube hit → reset point selection, let normal flow handle it
|
||||
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 (APS_Editor_Gizmo* Gizmo = SM->GetGizmo())
|
||||
{
|
||||
Gizmo->SetActorLocation(Spline->GetPointLocation(HandleIdx));
|
||||
}
|
||||
|
||||
SM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clicking elsewhere resets spline point selection
|
||||
ActiveSplinePointIndex = -1;
|
||||
DraggedSplineActor = nullptr;
|
||||
|
||||
// No gizmo hit: enter PendingClick
|
||||
PendingClickScreenPos = GetMouseScreenPosition();
|
||||
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
|
||||
@@ -522,8 +650,22 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
||||
{
|
||||
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>();
|
||||
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
||||
|
||||
@@ -555,17 +697,53 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
||||
}
|
||||
ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
|
||||
|
||||
// Refresh gizmo orientation (local mode needs to track actor's new rotation)
|
||||
// Refresh gizmo: keep at spline point if editing, else refresh orientation
|
||||
if (APS_Editor_PlayerController* PC2 = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
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);
|
||||
}
|
||||
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)
|
||||
{
|
||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||
@@ -574,6 +752,15 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
||||
|
||||
void APS_Editor_Pawn::HandleRightClickStarted(const FInputActionValue& Value)
|
||||
{
|
||||
// In spline placement mode, right-click finishes the spline
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||
{
|
||||
EditorPC->FinishSplinePlacement();
|
||||
return;
|
||||
}
|
||||
}
|
||||
SetCameraMode(EPS_Editor_CameraMode::Fly);
|
||||
}
|
||||
|
||||
@@ -760,6 +947,44 @@ 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->FinishSplinePlacement();
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorPC->BeginSplinePlacement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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->FinishSplinePlacement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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->CancelSplinePlacement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
||||
@@ -944,5 +1169,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);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ enum class EPS_Editor_CameraMode : uint8
|
||||
/** Alt + left-click or middle-click: orbit around focal point. */
|
||||
Orbit,
|
||||
/** Dragging a gizmo handle. */
|
||||
GizmoDrag
|
||||
GizmoDrag,
|
||||
/** Dragging a spline point handle. */
|
||||
SplinePointDrag
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -180,6 +182,22 @@ private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Paste;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_SplineMode;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Confirm;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Cancel;
|
||||
|
||||
// ---- Spline point drag state ----
|
||||
int32 ActiveSplinePointIndex = -1;
|
||||
TWeakObjectPtr<AActor> DraggedSplineActor;
|
||||
TArray<FVector> SplinePointDragInitialState;
|
||||
FVector SplinePointDragPlaneOrigin;
|
||||
FVector SplinePointDragPlaneNormal;
|
||||
|
||||
// ---- Modifier key state ----
|
||||
bool bAltHeld = false;
|
||||
bool bCtrlHeld = false;
|
||||
@@ -216,6 +234,9 @@ private:
|
||||
void HandleDuplicate(const FInputActionValue& Value);
|
||||
void HandleCopy(const FInputActionValue& Value);
|
||||
void HandlePaste(const FInputActionValue& Value);
|
||||
void HandleSplineMode(const FInputActionValue& Value);
|
||||
void HandleConfirm(const FInputActionValue& Value);
|
||||
void HandleCancel(const FInputActionValue& Value);
|
||||
|
||||
// ---- Helpers ----
|
||||
void CreateInputActions();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "PS_Editor_SceneSerializer.h"
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_Pawn.h"
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
|
||||
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
||||
{
|
||||
@@ -80,8 +81,116 @@ void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_PlayerController::BeginSplinePlacement()
|
||||
{
|
||||
if (SelectionManager)
|
||||
{
|
||||
SelectionManager->ClearSelection();
|
||||
}
|
||||
ActiveSplineActor = nullptr;
|
||||
EditorMode = EPS_Editor_EditorMode::SplinePlacement;
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline placement mode started"));
|
||||
}
|
||||
|
||||
void APS_Editor_PlayerController::FinishSplinePlacement()
|
||||
{
|
||||
if (!ActiveSplineActor || ActiveSplineActor->GetNumPoints() < 2)
|
||||
{
|
||||
CancelSplinePlacement();
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide handles after placement (shown again on selection)
|
||||
ActiveSplineActor->SetHandlesVisible(false);
|
||||
|
||||
// Track in SpawnManager — CRITICAL for outliner, save/load, and scene clear
|
||||
if (SpawnManager)
|
||||
{
|
||||
SpawnManager->TrackSpawnedActor(ActiveSplineActor);
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline tracked by SpawnManager (total: %d)"), SpawnManager->GetSpawnedActors().Num());
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("PS_Editor: SpawnManager is null! Spline will NOT be tracked."));
|
||||
}
|
||||
|
||||
// Record spawn action for undo
|
||||
if (UndoManager)
|
||||
{
|
||||
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
|
||||
Action->Actors.Add(ActiveSplineActor);
|
||||
Action->Description = FString::Printf(TEXT("Create spline (%d points)"), ActiveSplineActor->GetNumPoints());
|
||||
UndoManager->RecordAction(Action);
|
||||
}
|
||||
|
||||
// Select the new spline
|
||||
if (SelectionManager)
|
||||
{
|
||||
SelectionManager->SelectActor(ActiveSplineActor);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline placed with %d points, class=%s"),
|
||||
ActiveSplineActor->GetNumPoints(), *ActiveSplineActor->GetClass()->GetPathName());
|
||||
|
||||
ActiveSplineActor = nullptr;
|
||||
EditorMode = EPS_Editor_EditorMode::Normal;
|
||||
}
|
||||
|
||||
void APS_Editor_PlayerController::CancelSplinePlacement()
|
||||
{
|
||||
if (ActiveSplineActor)
|
||||
{
|
||||
ActiveSplineActor->Destroy();
|
||||
ActiveSplineActor = nullptr;
|
||||
}
|
||||
EditorMode = EPS_Editor_EditorMode::Normal;
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline placement cancelled"));
|
||||
}
|
||||
|
||||
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 (ActiveSplineActor) Params.AddIgnoredActor(ActiveSplineActor);
|
||||
|
||||
const FVector End = WorldOrigin + WorldDirection * 100000.0f;
|
||||
FVector ClickWorldPos;
|
||||
if (GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Visibility, Params))
|
||||
{
|
||||
ClickWorldPos = Hit.ImpactPoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
ClickWorldPos = WorldOrigin + WorldDirection * 1000.0f;
|
||||
}
|
||||
|
||||
if (!ActiveSplineActor)
|
||||
{
|
||||
// First click: spawn the spline actor
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
ActiveSplineActor = GetWorld()->SpawnActor<APS_Editor_SplineActor>(
|
||||
APS_Editor_SplineActor::StaticClass(), ClickWorldPos, FRotator::ZeroRotator, SpawnParams);
|
||||
}
|
||||
|
||||
if (ActiveSplineActor)
|
||||
{
|
||||
ActiveSplineActor->AddPoint(ClickWorldPos);
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline point %d added at %s"),
|
||||
ActiveSplineActor->GetNumPoints(), *ClickWorldPos.ToString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal mode: selection
|
||||
if (SelectionManager)
|
||||
{
|
||||
SelectionManager->HandleClickAtScreenPosition(ScreenPos, bAdditive);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "PS_Editor_SelectionTypes.h"
|
||||
#include "PS_Editor_PlayerController.generated.h"
|
||||
|
||||
class UPS_Editor_SelectionManager;
|
||||
@@ -9,6 +10,7 @@ class UPS_Editor_UndoManager;
|
||||
class UPS_Editor_SpawnManager;
|
||||
class UPS_Editor_SpawnCatalog;
|
||||
class UPS_Editor_SceneSerializer;
|
||||
class APS_Editor_SplineActor;
|
||||
|
||||
/**
|
||||
* Player controller for PS_Editor runtime editing.
|
||||
@@ -38,6 +40,23 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
TSoftObjectPtr<UPS_Editor_SpawnCatalog> SpawnCatalogAsset;
|
||||
|
||||
// ---- Editor Mode (Normal / SplinePlacement) ----
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
EPS_Editor_EditorMode GetEditorMode() const { return EditorMode; }
|
||||
|
||||
/** Enter spline placement mode. Clicks will add points. */
|
||||
void BeginSplinePlacement();
|
||||
|
||||
/** Finish current spline (min 2 points required). Returns to Normal mode. */
|
||||
void FinishSplinePlacement();
|
||||
|
||||
/** Cancel current spline placement (destroys in-progress spline). */
|
||||
void CancelSplinePlacement();
|
||||
|
||||
/** Get the spline currently being placed (nullptr if not in SplinePlacement mode). */
|
||||
APS_Editor_SplineActor* GetActiveSplineActor() const { return ActiveSplineActor; }
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
@@ -55,5 +74,10 @@ private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UPS_Editor_SceneSerializer> SceneSerializer;
|
||||
|
||||
EPS_Editor_EditorMode EditorMode = EPS_Editor_EditorMode::Normal;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<APS_Editor_SplineActor> ActiveSplineActor;
|
||||
|
||||
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, "Spawn"));
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Serialization"));
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Spline"));
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "Materials/MaterialExpressionLinearInterpolate.h"
|
||||
#include "Materials/MaterialExpressionComponentMask.h"
|
||||
#include "Materials/MaterialExpressionAppendVector.h"
|
||||
#include "Materials/MaterialExpressionVectorParameter.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "UObject/SavePackage.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)"));
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
void FPS_EditorModule::StartupModule()
|
||||
@@ -226,6 +269,7 @@ void FPS_EditorModule::StartupModule()
|
||||
|
||||
#if WITH_EDITOR
|
||||
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateOutlineMaterialAsset);
|
||||
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateSplineLineMaterialAsset);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
@@ -185,6 +186,13 @@ void UPS_Editor_SelectionManager::SelectActor(AActor* Actor)
|
||||
UpdateGizmo();
|
||||
OnSelectionChanged.Broadcast();
|
||||
|
||||
// Show spline handles and mark selected
|
||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
|
||||
{
|
||||
Spline->SetHandlesVisible(true);
|
||||
Spline->SetSelected(true);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selected %s"), *Actor->GetName());
|
||||
}
|
||||
|
||||
@@ -206,6 +214,13 @@ void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
||||
UpdateGizmo();
|
||||
OnSelectionChanged.Broadcast();
|
||||
|
||||
// Hide spline handles and unmark
|
||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
|
||||
{
|
||||
Spline->SetHandlesVisible(false);
|
||||
Spline->SetSelected(false);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
|
||||
}
|
||||
}
|
||||
@@ -269,7 +284,15 @@ FVector UPS_Editor_SelectionManager::GetSelectionPivot() const
|
||||
{
|
||||
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++;
|
||||
}
|
||||
}
|
||||
@@ -448,7 +471,18 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
|
||||
|
||||
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
|
||||
if (AActor* Actor = SelectedActors[0].Get())
|
||||
|
||||
@@ -33,3 +33,11 @@ enum class EPS_Editor_Space : uint8
|
||||
World,
|
||||
Local
|
||||
};
|
||||
|
||||
/** Top-level editor mode (separate from transform mode). */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_Editor_EditorMode : uint8
|
||||
{
|
||||
Normal, // Standard select/transform behavior
|
||||
SplinePlacement // Clicks add points to current spline
|
||||
};
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
#include "UObject/UnrealType.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)
|
||||
{
|
||||
AActor* Act = Actor.Get();
|
||||
|
||||
@@ -9,7 +9,8 @@ enum class EPS_Editor_ActionType : uint8
|
||||
Transform,
|
||||
Delete,
|
||||
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.
|
||||
* Stores text-serialized old/new values for undo/redo via ImportText.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
#include "JsonObjectConverter.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
@@ -36,6 +37,14 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
||||
ActorData.Rotation = Actor->GetActorRotation();
|
||||
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
|
||||
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);
|
||||
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);
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
#include "PS_Editor_SplineActor.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);
|
||||
}
|
||||
|
||||
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"));
|
||||
|
||||
// Spline line: normal depth-tested material (orange)
|
||||
if (SplineLineMat)
|
||||
{
|
||||
SplineMat = UMaterialInstanceDynamic::Create(SplineLineMat, this);
|
||||
SplineMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 0.4f, 0.0f));
|
||||
}
|
||||
|
||||
// Spline line selected: no depth test (on top, bright orange)
|
||||
if (GizmoMat)
|
||||
{
|
||||
SplineMatSelected = UMaterialInstanceDynamic::Create(GizmoMat, this);
|
||||
SplineMatSelected->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 0.6f, 0.0f));
|
||||
|
||||
// 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"), FLinearColor(1.0f, 0.4f, 0.0f)); // Orange
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
// Convert world to local space
|
||||
const FVector LocalPos = GetActorTransform().InverseTransformPosition(WorldPosition);
|
||||
SplineComp->AddSplinePoint(LocalPos, ESplineCoordinateSpace::Local, 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;
|
||||
|
||||
const FVector LocalPos = GetActorTransform().InverseTransformPosition(NewWorldPosition);
|
||||
SplineComp->SetLocationAtSplinePoint(Index, LocalPos, ESplineCoordinateSpace::Local, 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)
|
||||
{
|
||||
const FVector LocalPos = GetActorTransform().InverseTransformPosition(WorldPos);
|
||||
SplineComp->AddSplinePoint(LocalPos, ESplineCoordinateSpace::Local, 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));
|
||||
|
||||
TArray<FVector> Vertices;
|
||||
TArray<int32> Triangles;
|
||||
TArray<FVector> Normals;
|
||||
TArray<FVector2D> UVs;
|
||||
|
||||
// 4 vertices per sample: 2 for vertical ribbon + 2 for horizontal ribbon (cross-section)
|
||||
Vertices.Reserve(NumSamples * 4);
|
||||
Normals.Reserve(NumSamples * 4);
|
||||
UVs.Reserve(NumSamples * 4);
|
||||
|
||||
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 Right = SplineComp->GetRightVectorAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::Local);
|
||||
const FVector Up = FVector(0.0f, 0.0f, LineHalfWidth);
|
||||
|
||||
const float U = static_cast<float>(i) / (NumSamples - 1);
|
||||
|
||||
// Vertical ribbon (visible from sides)
|
||||
Vertices.Add(Location - Up);
|
||||
Vertices.Add(Location + Up);
|
||||
Normals.Add(Right);
|
||||
Normals.Add(Right);
|
||||
UVs.Add(FVector2D(U, 0.0f));
|
||||
UVs.Add(FVector2D(U, 1.0f));
|
||||
|
||||
// Horizontal ribbon (visible from above/below)
|
||||
Vertices.Add(Location - Right * LineHalfWidth);
|
||||
Vertices.Add(Location + Right * LineHalfWidth);
|
||||
Normals.Add(FVector::UpVector);
|
||||
Normals.Add(FVector::UpVector);
|
||||
UVs.Add(FVector2D(U, 0.0f));
|
||||
UVs.Add(FVector2D(U, 1.0f));
|
||||
}
|
||||
|
||||
// Generate triangles for both ribbons (double-sided)
|
||||
Triangles.Reserve((NumSamples - 1) * 24);
|
||||
for (int32 i = 0; i < NumSamples - 1; i++)
|
||||
{
|
||||
// Vertical ribbon indices
|
||||
const int32 V0 = i * 4;
|
||||
const int32 V1 = i * 4 + 1;
|
||||
const int32 V2 = (i + 1) * 4;
|
||||
const int32 V3 = (i + 1) * 4 + 1;
|
||||
|
||||
// Horizontal ribbon indices
|
||||
const int32 H0 = i * 4 + 2;
|
||||
const int32 H1 = i * 4 + 3;
|
||||
const int32 H2 = (i + 1) * 4 + 2;
|
||||
const int32 H3 = (i + 1) * 4 + 3;
|
||||
|
||||
// Vertical: front + back
|
||||
Triangles.Add(V0); Triangles.Add(V2); Triangles.Add(V1);
|
||||
Triangles.Add(V1); Triangles.Add(V2); Triangles.Add(V3);
|
||||
Triangles.Add(V0); Triangles.Add(V1); Triangles.Add(V2);
|
||||
Triangles.Add(V1); Triangles.Add(V3); Triangles.Add(V2);
|
||||
|
||||
// Horizontal: front + back
|
||||
Triangles.Add(H0); Triangles.Add(H2); Triangles.Add(H1);
|
||||
Triangles.Add(H1); Triangles.Add(H2); Triangles.Add(H3);
|
||||
Triangles.Add(H0); Triangles.Add(H1); Triangles.Add(H2);
|
||||
Triangles.Add(H1); Triangles.Add(H3); Triangles.Add(H2);
|
||||
}
|
||||
|
||||
SplineMeshComp->ClearAllMeshSections();
|
||||
SplineMeshComp->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, TArray<FColor>(), TArray<FProcMeshTangent>(), false);
|
||||
|
||||
// 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;
|
||||
if (Mat)
|
||||
{
|
||||
SplineMeshComp->SetMaterial(0, Mat);
|
||||
}
|
||||
}
|
||||
|
||||
// Center cube: bright when selected
|
||||
if (CenterCubeMat)
|
||||
{
|
||||
const FLinearColor Color = bSelected ? FLinearColor(1.0f, 0.8f, 0.0f) : FLinearColor(1.0f, 0.4f, 0.0f);
|
||||
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), Color);
|
||||
}
|
||||
}
|
||||
|
||||
bool APS_Editor_SplineActor::IsCenterCube(const UPrimitiveComponent* Comp) const
|
||||
{
|
||||
return Comp && Comp == CenterCube;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "PS_Editor_SplineActor.generated.h"
|
||||
|
||||
class USplineComponent;
|
||||
class UProceduralMeshComponent;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APS_Editor_SplineActor();
|
||||
|
||||
// ---- 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();
|
||||
|
||||
// ---- 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 ----
|
||||
|
||||
/** 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;
|
||||
|
||||
/** Line rendering settings. */
|
||||
float LineHalfWidth = 2.0f;
|
||||
float SampleInterval = 10.0f;
|
||||
float HandleScale = 0.12f;
|
||||
float CenterCubeScale = 0.10f;
|
||||
};
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "PS_Editor_SpawnableComponent.h"
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#include "Widgets/Layout/SBorder.h"
|
||||
@@ -239,6 +240,31 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
|
||||
})
|
||||
]
|
||||
]
|
||||
// New Spline button
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||
{
|
||||
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||
{
|
||||
EditorPC->FinishSplinePlacement();
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorPC->BeginSplinePlacement();
|
||||
}
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("New Spline (S)")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -685,14 +711,28 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
||||
// Update mode text
|
||||
if (ModeText.IsValid())
|
||||
{
|
||||
FString ModeStr;
|
||||
switch (SM->GetTransformMode())
|
||||
// Check for spline placement mode first
|
||||
bool bSplineMode = false;
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||
{
|
||||
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;
|
||||
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||
{
|
||||
ModeText->SetText(FText::FromString(TEXT("Spline Placement (click to add, RMB/Enter to finish, Esc to cancel)")));
|
||||
bSplineMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bSplineMode)
|
||||
{
|
||||
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));
|
||||
}
|
||||
ModeText->SetText(FText::FromString(ModeStr));
|
||||
}
|
||||
|
||||
// Update space button text
|
||||
@@ -877,16 +917,17 @@ void UPS_Editor_MainWidget::RefreshOutliner()
|
||||
AActor* Actor = SpawnedActors[i].Get();
|
||||
if (!Actor || Actor->IsHidden()) continue;
|
||||
|
||||
// Build display name: SpawnableComponent DisplayName > Class name (cleaned)
|
||||
// Build display name
|
||||
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())
|
||||
|
||||
Reference in New Issue
Block a user