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>
This commit is contained in:
2026-04-12 11:06:04 +02:00
parent ca4f7ee355
commit 861c15c193
20 changed files with 358 additions and 328 deletions

Binary file not shown.

View File

@@ -577,19 +577,34 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
}
}
// Check for spline point handle hit → select point, move gizmo to it (only in SplineEditing mode)
// 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()))
{
if (EdPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
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* Spline = Cast<APS_Editor_SplineActor>(Weak.Get()))
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)
{
// 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);
@@ -598,13 +613,13 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
{
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)
@@ -613,26 +628,42 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
DraggedSplineActor = Spline;
Spline->HighlightHandle(HandleIdx);
if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager())
{
if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo())
{
Gizmo->SetActorLocation(Spline->GetPointLocation(HandleIdx));
// 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 SplineEditing mode: click on spline tube mesh → insert a CP
// 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::SplineEditing)
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;
@@ -641,10 +672,8 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
if (GetWorld()->LineTraceSingleByChannel(LineHit, RayOrigin, TraceEnd, ECC_Visibility, LineParams))
{
for (const TWeakObjectPtr<AActor>& Weak : SM2->GetSelectedActors())
for (APS_Editor_SplineActor* Spline : TubeCandidates)
{
APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Weak.Get());
if (!Spline) continue;
if (Spline->IsSplineMesh(LineHit.GetComponent()))
{
@@ -668,11 +697,14 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
DraggedSplineActor = Spline;
Spline->HighlightHandle(InsertIdx);
if (APS_Editor_Gizmo* G = SM2->GetGizmo())
if (UPS_Editor_SelectionManager* InsertSM = EdPC2->GetSelectionManager())
{
G->SetActorLocation(Spline->GetPointLocation(InsertIdx));
if (APS_Editor_Gizmo* G = InsertSM->GetGizmo())
{
G->SetGizmoActive(true, Spline->GetPointLocation(InsertIdx));
}
InsertSM->SetTransformMode(EPS_Editor_TransformMode::Translate);
}
SM2->SetTransformMode(EPS_Editor_TransformMode::Translate);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Inserted spline point at index %d"), InsertIdx);
return;
@@ -683,6 +715,63 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
}
}
// 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;
@@ -809,15 +898,6 @@ 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->FinishPointPlacement();
return;
}
}
SetCameraMode(EPS_Editor_CameraMode::Fly);
}
@@ -889,7 +969,7 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
if (!EditorPC) return;
// In SplineEditing mode with a point selected: delete that point
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement
&& ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{
APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get());
@@ -1005,21 +1085,34 @@ 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, snap that point to ground
// 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, 500.0f);
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();
@@ -1103,21 +1196,23 @@ void APS_Editor_Pawn::HandleSplineMode(const FInputActionValue& Value)
}
else
{
// If a spline is selected, append points to it instead of creating a new one
// 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 (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Weak.Get()))
if (AActor* Actor = Weak.Get())
{
if (Actor->Implements<UPS_Editor_PointPlaceable>())
{
ActiveSplinePointIndex = -1;
DraggedSplineActor = nullptr;
EditorPC->BeginPointPlacementAppend(Spline);
EditorPC->BeginPointPlacementAppend(Actor);
return;
}
}
}
EditorPC->BeginPointPlacement();
}
}
}
}
@@ -1141,7 +1236,7 @@ void APS_Editor_Pawn::HandleCancel(const FInputActionValue& Value)
{
EditorPC->CancelPointPlacement();
}
else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{
EditorPC->FinishSplineEditing();
}

View File

@@ -2,6 +2,7 @@
#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"
@@ -26,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);
@@ -121,67 +138,21 @@ void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActo
void APS_Editor_PlayerController::FinishPointPlacement()
{
IPS_Editor_PointPlaceable* Placeable = Cast<IPS_Editor_PointPlaceable>(ActivePlacementActor);
if (!ActivePlacementActor || !Placeable || Placeable->GetCurrentPointCount() < Placeable->GetMinimumPoints())
{
CancelPointPlacement();
return;
}
Placeable->OnPlacementFinished();
if (bAppendingToActor)
{
// Appending: record undo for the added points
// 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);
if (UndoManager)
{
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
Action->SplineActor = Spline;
Action->OldPoints = AppendInitialPoints;
Action->NewPoints = Spline->GetAllPointLocations();
Action->Description = FString::Printf(TEXT("Add %d point(s)"),
Spline->GetNumPoints() - AppendInitialPoints.Num());
UndoManager->RecordAction(Action);
Spline->ClearHandleHighlight();
}
}
if (SelectionManager)
if (ActivePlacementActor && SelectionManager)
{
SelectionManager->ClearSelection();
SelectionManager->SelectActor(ActivePlacementActor);
}
}
else
{
// New actor: track and record spawn undo
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
{
Spline->SetHandlesVisible(false);
}
if (SpawnManager)
{
SpawnManager->TrackSpawnedActor(ActivePlacementActor);
}
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exited placement mode"));
if (UndoManager)
{
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
Action->Actors.Add(ActivePlacementActor);
Action->Description = FString::Printf(TEXT("Create %s (%d points)"),
*ActivePlacementActor->GetClass()->GetName(), Placeable->GetCurrentPointCount());
UndoManager->RecordAction(Action);
}
if (SelectionManager)
{
SelectionManager->SelectActor(ActivePlacementActor);
}
}
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Placement finished (%d points)"), Placeable->GetCurrentPointCount());
ActivePlacementActor = nullptr;
bAppendingToActor = false;
AppendInitialPoints.Empty();
@@ -190,57 +161,8 @@ void APS_Editor_PlayerController::FinishPointPlacement()
void APS_Editor_PlayerController::CancelPointPlacement()
{
if (ActivePlacementActor)
{
IPS_Editor_PointPlaceable* Placeable = Cast<IPS_Editor_PointPlaceable>(ActivePlacementActor);
if (Placeable) Placeable->OnPlacementCancelled();
if (bAppendingToActor)
{
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
{
Spline->SetAllPointLocations(AppendInitialPoints);
Spline->SetHandlesVisible(false);
}
if (SelectionManager)
{
SelectionManager->ClearSelection();
SelectionManager->SelectActor(ActivePlacementActor);
}
}
else
{
ActivePlacementActor->Destroy();
}
ActivePlacementActor = nullptr;
}
bAppendingToActor = false;
AppendInitialPoints.Empty();
EditorMode = EPS_Editor_EditorMode::Normal;
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Placement cancelled"));
}
void APS_Editor_PlayerController::BeginSplineEditing(APS_Editor_SplineActor* Spline)
{
if (!Spline) return;
ActivePlacementActor = Spline;
EditorMode = EPS_Editor_EditorMode::SplineEditing;
Spline->SetHandlesVisible(true);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline editing mode started (%d points)"), Spline->GetNumPoints());
}
void APS_Editor_PlayerController::FinishSplineEditing()
{
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
{
Spline->SetHandlesVisible(false);
Spline->ClearHandleHighlight();
}
ActivePlacementActor = nullptr;
EditorMode = EPS_Editor_EditorMode::Normal;
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline editing mode ended"));
// Same as finish — just exit mode. Undo handles reverting if needed.
FinishPointPlacement();
}
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)
@@ -270,19 +192,77 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
if (!ActivePlacementActor)
{
// First click: spawn the actor
// 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);
}
// Add point via the interface
if (IPS_Editor_PointPlaceable* Placeable = Cast<IPS_Editor_PointPlaceable>(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);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Point %d added at %s"),
Placeable->GetCurrentPointCount(), *ClickWorldPos.ToString());
}
return;
}

View File

@@ -9,6 +9,7 @@ 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;
@@ -37,9 +38,9 @@ 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) ----
@@ -61,14 +62,17 @@ public:
/** 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;
/** Enter spline point editing mode for a selected spline. */
void BeginSplineEditing(APS_Editor_SplineActor* Spline);
/** Convenience: enter point placement for an existing spline. */
void BeginSplineEditing(AActor* Spline) { BeginPointPlacementAppend(Spline); }
/** Exit spline editing mode. */
void FinishSplineEditing();
/** Convenience: exit spline editing. */
void FinishSplineEditing() { FinishPointPlacement(); }
protected:
virtual void BeginPlay() override;

View File

@@ -225,7 +225,7 @@ void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
{
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
{
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing && EditorPC->GetActivePlacementActor() == Actor)
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement && EditorPC->GetActivePlacementActor() == Actor)
{
EditorPC->FinishSplineEditing();
}
@@ -268,7 +268,7 @@ void UPS_Editor_SelectionManager::ClearSelection()
{
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
{
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{
EditorPC->FinishSplineEditing();
}

View File

@@ -39,6 +39,5 @@ UENUM(BlueprintType)
enum class EPS_Editor_EditorMode : uint8
{
Normal, // Standard select/transform behavior
SplinePlacement, // Clicks add points to current spline
SplineEditing // Editing individual points of a selected spline
SplinePlacement // Creating or editing a point-placeable actor (spline, etc.)
};

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

@@ -1,4 +1,6 @@
#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"
@@ -32,6 +34,17 @@ APS_Editor_SplineActor::APS_Editor_SplineActor()
// 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()
@@ -100,9 +113,7 @@ void APS_Editor_SplineActor::BeginPlay()
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);
SplineComp->AddSplinePoint(WorldPosition, ESplineCoordinateSpace::World, true);
// Create handle
UStaticMeshComponent* Handle = CreatePointHandle(WorldPosition);
@@ -115,8 +126,7 @@ void APS_Editor_SplineActor::MovePoint(int32 Index, const FVector& NewWorldPosit
{
if (Index < 0 || Index >= GetNumPoints()) return;
const FVector LocalPos = GetActorTransform().InverseTransformPosition(NewWorldPosition);
SplineComp->SetLocationAtSplinePoint(Index, LocalPos, ESplineCoordinateSpace::Local, true);
SplineComp->SetLocationAtSplinePoint(Index, NewWorldPosition, ESplineCoordinateSpace::World, true);
if (PointHandles.IsValidIndex(Index) && PointHandles[Index])
{
@@ -174,8 +184,7 @@ void APS_Editor_SplineActor::SetAllPointLocations(const TArray<FVector>& Points)
SplineComp->ClearSplinePoints(false);
for (const FVector& WorldPos : Points)
{
const FVector LocalPos = GetActorTransform().InverseTransformPosition(WorldPos);
SplineComp->AddSplinePoint(LocalPos, ESplineCoordinateSpace::Local, false);
SplineComp->AddSplinePoint(WorldPos, ESplineCoordinateSpace::World, false);
}
SplineComp->UpdateSpline();
@@ -246,7 +255,11 @@ void APS_Editor_SplineActor::UpdateVisualization()
}
SplineMeshComp->ClearAllMeshSections();
SplineMeshComp->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, TArray<FColor>(), TArray<FProcMeshTangent>(), false);
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

View File

@@ -7,6 +7,8 @@
class USplineComponent;
class UProceduralMeshComponent;
class UPS_Editor_SpawnableComponent;
class UPS_Editor_EditableComponent;
/**
* Runtime spline actor for path/route editing in PS_Editor.
@@ -157,6 +159,12 @@ private:
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;

View File

@@ -144,7 +144,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
{
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{
EditorPC->FinishSplineEditing();
}
@@ -199,7 +199,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
{
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
EditorPC->FinishPointPlacement();
else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
EditorPC->FinishSplineEditing();
}
return FReply::Handled();
@@ -321,31 +321,6 @@ 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->FinishPointPlacement();
}
else
{
EditorPC->BeginPointPlacement();
}
}
return FReply::Handled();
})
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("New Spline (S)")))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
]
]
];
}
@@ -784,73 +759,6 @@ void UPS_Editor_MainWidget::PopulateSpawnCatalog()
];
}
// ---- Tools section (built-in tools) ----
SpawnListContainer->AddSlot()
.AutoHeight()
.Padding(0.0f, 10.0f, 0.0f, 2.0f)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Tools")))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 9))
.ColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f))
];
// Spline tool
SpawnListContainer->AddSlot()
.AutoHeight()
.Padding(4.0f, 4.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->FinishPointPlacement();
}
else
{
// If a spline is selected, append to it
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->BeginPointPlacementAppend(Spline);
return FReply::Handled();
}
}
}
EditorPC->BeginPointPlacement();
}
}
return FReply::Handled();
})
[
SNew(SBox)
.MinDesiredWidth(88.0f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("~")))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 28))
.ColorAndOpacity(FLinearColor(1.0f, 0.4f, 0.0f))
]
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 2.0f, 0.0f, 0.0f)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Spline")))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
.Justification(ETextJustify::Center)
.ColorAndOpacity(FLinearColor::White)
]
]
]
];
}
void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
@@ -914,18 +822,6 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
// Update help bar text and button visibility
if (CurrentEditorMode == EPS_Editor_EditorMode::SplinePlacement)
{
if (HelpBarText.IsValid())
{
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: LMB to add points | RMB or Enter to finish | Escape to cancel")));
HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f));
}
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed);
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible);
}
else if (CurrentEditorMode == EPS_Editor_EditorMode::SplineEditing)
{
// Check if a specific point is selected (for Delete Point button)
bool bPointSelected = false;
if (APS_Editor_Pawn* Pawn = Cast<APS_Editor_Pawn>(GetOwningPlayerPawn()))
{
@@ -936,11 +832,11 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
{
if (bPointSelected)
{
HelpBarText->SetText(FText::FromString(TEXT("EDIT SPLINE: Drag gizmo to move point | Del: delete point | Click line to insert | Escape: done")));
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("EDIT SPLINE: Click handle to select point | Click line to insert | End: snap to ground | Escape: done")));
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));
}