Compare commits

...

3 Commits

Author SHA1 Message Date
861c15c193 Unify spline creation/editing into single seamless mode
Major UX simplification:
- Removed SplineEditing mode — single SplinePlacement mode handles both
  creation and editing with identical behavior
- Actor spawned and tracked on first click (no "finish" step needed)
- Each point add/insert/move has individual undo
- Right-click always = camera fly (no special handling during creation)
- Escape/Done just exits edit mode, nothing to "finish"

Catalog system:
- MasterCatalog DataAsset groups multiple SpawnCatalogs (Tools, Characters, Props)
- PlayerController references one MasterCatalog instead of single catalog
- SpawnManager.AddCatalog() loads sub-catalogs incrementally
- Factory for MasterCatalog creation in Content Browser

Spline polish:
- SplineActor has built-in SpawnableComponent + EditableComponent
- SplineColor/SplineSelectedColor/SplineTag as UPROPERTY for BP variants
- All spline point operations use World coordinate space (fixes snap-to-ground)
- Snap-to-ground ignores gizmo actor (was hitting gizmo guide lines)
- Tube mesh collision forced with UpdateBounds after CreateMeshSection
- Tube insert and handle selection include active placement actor (not just selected)
- Notification bar shows contextual Delete Point button when CP selected

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:06:04 +02:00
ca4f7ee355 Refactor spline system with IPS_Editor_PointPlaceable interface
Architecture:
- New IPS_Editor_PointPlaceable interface for any actor using click-to-place-
  points placement. Decouples the placement system from SplineActor so future
  actors (patrol routes, area polygons) can reuse the same flow.
- SplineActor implements the interface. BP subclasses with SpawnableComponent
  appear in the catalog and automatically enter placement mode when spawned.
- PlayerController methods renamed: BeginPointPlacement/FinishPointPlacement/
  CancelPointPlacement (generic, interface-driven).

Features added:
- SplineColor, SplineSelectedColor, SplineTag as UPROPERTY for BP overrides
- UpdateSplineMaterials() for runtime color changes
- Tube visualization (8-segment cylinder) replacing cross-ribbon
- Spline editing mode with Edit/Done buttons in notification bar
- Delete spline point (Del key or Delete Point button, min 2 points)
- Insert point by clicking on tube mesh in edit mode
- Snap spline point to ground (End key)
- Append points to existing spline (S key or catalog when spline selected)
- Notification bar: contextual messages and buttons per mode
- Auto-generated M_PS_Editor_SplineLine material (unlit opaque, depth tested)
- Material swap: opaque when deselected, no-depth-test when selected
- Serialization of SplineColor/SplineTag in CustomProperties

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 09:44:26 +02:00
1f4b3994ff 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>
2026-04-11 22:44:04 +02:00
30 changed files with 1989 additions and 66 deletions

Binary file not shown.

View File

@@ -13,6 +13,8 @@
#include "PS_Editor_PlayerController.h"
#include "PS_Editor_Gizmo.h"
#include "PS_Editor_UndoManager.h"
#include "PS_Editor_SplineActor.h"
#include "Components/SplineComponent.h"
#include "DrawDebugHelpers.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->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 +257,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 +375,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 +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)
{
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 +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)
@@ -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
PendingClickScreenPos = GetMouseScreenPosition();
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_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 +843,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);
@@ -643,6 +967,50 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
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();
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 (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())
{
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 ----
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);
}

View File

@@ -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
};
/**
@@ -60,6 +62,12 @@ public:
/** Fired when a left-click is confirmed (mouse up without exceeding drag threshold). */
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:
virtual void BeginPlay() override;
@@ -180,6 +188,21 @@ 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 ----
TWeakObjectPtr<AActor> DraggedSplineActor;
TArray<FVector> SplinePointDragInitialState;
FVector SplinePointDragPlaneOrigin;
FVector SplinePointDragPlaneNormal;
// ---- Modifier key state ----
bool bAltHeld = false;
bool bCtrlHeld = false;
@@ -209,13 +232,15 @@ private:
void HandleTranslateMode(const FInputActionValue& Value);
void HandleRotateMode(const FInputActionValue& Value);
void HandleScaleMode(const FInputActionValue& Value);
void HandleDelete(const FInputActionValue& Value);
void HandleSnapToGround(const FInputActionValue& Value);
void HandleUndo(const FInputActionValue& Value);
void HandleRedo(const FInputActionValue& Value);
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();

View File

@@ -2,9 +2,12 @@
#include "PS_Editor_SelectionManager.h"
#include "PS_Editor_UndoManager.h"
#include "PS_Editor_SpawnManager.h"
#include "PS_Editor_SpawnCatalog.h"
#include "PS_Editor_SceneSerializer.h"
#include "PS_Editor_Gizmo.h"
#include "PS_Editor_Pawn.h"
#include "PS_Editor_SplineActor.h"
#include "PS_Editor_PointPlaceable.h"
APS_Editor_PlayerController::APS_Editor_PlayerController()
{
@@ -24,10 +27,26 @@ void APS_Editor_PlayerController::BeginPlay()
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);
UPS_Editor_SpawnCatalog* LoadedCatalog = SpawnCatalogAsset.LoadSynchronous();
SpawnManager->Initialize(this, LoadedCatalog);
SpawnManager->Initialize(this);
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->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)
{
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)
{
SelectionManager->HandleClickAtScreenPosition(ScreenPos, bAdditive);

View File

@@ -2,13 +2,17 @@
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "PS_Editor_SelectionTypes.h"
#include "PS_Editor_PlayerController.generated.h"
class UPS_Editor_SelectionManager;
class UPS_Editor_UndoManager;
class UPS_Editor_SpawnManager;
class UPS_Editor_SpawnCatalog;
class UPS_Editor_MasterCatalog;
class UPS_Editor_SceneSerializer;
class APS_Editor_SplineActor;
class IPS_Editor_PointPlaceable;
/**
* Player controller for PS_Editor runtime editing.
@@ -34,9 +38,41 @@ public:
UFUNCTION(BlueprintCallable, Category = "PS Editor")
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")
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:
virtual void BeginPlay() override;
@@ -55,5 +91,20 @@ private:
UPROPERTY()
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);
};

View File

@@ -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[]
{

View File

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

View File

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

View File

@@ -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,12 @@ void UPS_Editor_SelectionManager::SelectActor(AActor* Actor)
UpdateGizmo();
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());
}
@@ -206,6 +213,25 @@ void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
UpdateGizmo();
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());
}
}
@@ -229,6 +255,25 @@ void UPS_Editor_SelectionManager::ClearSelection()
if (AActor* Actor = Weak.Get())
{
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())
{
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 +501,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())

View File

@@ -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 // Creating or editing a point-placeable actor (spline, etc.)
};

View File

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

View File

@@ -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.

View File

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

View File

@@ -43,3 +43,19 @@ public:
void CaptureAllThumbnails();
#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;
};

View File

@@ -22,6 +22,25 @@ FText UPS_Editor_SpawnCatalogFactory::GetDisplayName() const
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
#endif

View File

@@ -19,4 +19,17 @@ public:
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

View File

@@ -5,32 +5,18 @@
#include "GameFramework/PlayerController.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;
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)
{
return;
}
const int32 BeforeCount = ResolvedEntries.Num();
for (const FPS_Editor_SpawnEntry& Entry : Catalog->Entries)
for (const FPS_Editor_SpawnEntry& Entry : InCatalog->Entries)
{
// Synchronously load the class
UClass* LoadedClass = Entry.ActorClass.LoadSynchronous();
@@ -84,6 +70,9 @@ void UPS_Editor_SpawnManager::ResolveCatalog()
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)

View File

@@ -36,8 +36,11 @@ class PS_EDITOR_API UPS_Editor_SpawnManager : public UObject
GENERATED_BODY()
public:
/** Initialize with owning controller and load catalog. */
void Initialize(APlayerController* InOwnerPC, UPS_Editor_SpawnCatalog* InCatalog);
/** Initialize with owning controller. */
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. */
AActor* SpawnFromCatalog(int32 Index);
@@ -70,9 +73,6 @@ private:
UPROPERTY()
TWeakObjectPtr<APlayerController> OwnerPC;
UPROPERTY()
TObjectPtr<UPS_Editor_SpawnCatalog> Catalog;
UPROPERTY()
TArray<FPS_Editor_ResolvedEntry> ResolvedEntries;
@@ -80,8 +80,6 @@ private:
UPROPERTY()
TArray<TWeakObjectPtr<AActor>> SpawnedActors;
/** Resolve all catalog entries (load classes, read SpawnableComponent metadata). */
void ResolveCatalog();
/** Compute spawn location in front of the camera. */
FVector ComputeSpawnLocation() const;

View File

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

View File

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

View File

@@ -6,6 +6,9 @@
#include "PS_Editor_PlayerController.h"
#include "PS_Editor_EditableComponent.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/Text/STextBlock.h"
#include "Widgets/Layout/SBorder.h"
@@ -115,20 +118,99 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
BuildOutliner()
]
]
// Bottom help bar
// Bottom notification / help bar
+ SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Bottom)
.Padding(16.0f)
[
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))
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | Z/E/R: Translate/Rotate/Scale | End: Snap | Del: Delete")))
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
SNew(SHorizontalBox)
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
[
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)
.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();
})
[
@@ -662,6 +758,7 @@ void UPS_Editor_MainWidget::PopulateSpawnCatalog()
]
];
}
}
void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
@@ -682,17 +779,92 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
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())
{
FString ModeStr;
switch (SM->GetTransformMode())
if (CurrentEditorMode == EPS_Editor_EditorMode::SplinePlacement)
{
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(TEXT("Spline Placement")));
}
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
@@ -877,16 +1049,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())

View File

@@ -87,6 +87,12 @@ private:
TSharedPtr<SVerticalBox> OutlinerListContainer;
int32 LastOutlinerActorCount = -1;
// Bottom notification bar
TSharedPtr<STextBlock> HelpBarText;
TSharedPtr<SWidget> SplineFinishButton;
TSharedPtr<SWidget> SplineEditButton;
TSharedPtr<SWidget> SplineDeletePointButton;
// ---- Helpers ----
TSharedRef<SWidget> BuildToolbar();
TSharedRef<SWidget> BuildPropertiesPanel();