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>
This commit is contained in:
2026-04-12 09:44:26 +02:00
parent 1f4b3994ff
commit ca4f7ee355
14 changed files with 841 additions and 145 deletions

Binary file not shown.

View File

@@ -14,6 +14,7 @@
#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"
@@ -576,10 +577,10 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
}
}
// Check for spline point handle hit → select point, move gizmo to it (no drag yet)
// Check for spline point handle hit → select point, move gizmo to it (only in SplineEditing mode)
if (APS_Editor_PlayerController* EdPC = Cast<APS_Editor_PlayerController>(GetController()))
{
if (EdPC->GetEditorMode() == EPS_Editor_EditorMode::Normal)
if (EdPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
{
if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager())
{
@@ -626,6 +627,62 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
}
}
// In SplineEditing 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 (UPS_Editor_SelectionManager* SM2 = EdPC2->GetSelectionManager())
{
FHitResult LineHit;
FCollisionQueryParams LineParams;
LineParams.AddIgnoredActor(this);
const FVector TraceEnd = RayOrigin + RayDir * 100000.0f;
if (GetWorld()->LineTraceSingleByChannel(LineHit, RayOrigin, TraceEnd, ECC_Visibility, LineParams))
{
for (const TWeakObjectPtr<AActor>& Weak : SM2->GetSelectedActors())
{
APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Weak.Get());
if (!Spline) continue;
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 (APS_Editor_Gizmo* G = SM2->GetGizmo())
{
G->SetActorLocation(Spline->GetPointLocation(InsertIdx));
}
SM2->SetTransformMode(EPS_Editor_TransformMode::Translate);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Inserted spline point at index %d"), InsertIdx);
return;
}
}
}
}
}
}
// Clicking elsewhere resets spline point selection
ActiveSplinePointIndex = -1;
DraggedSplineActor = nullptr;
@@ -757,7 +814,7 @@ void APS_Editor_Pawn::HandleRightClickStarted(const FInputActionValue& Value)
{
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{
EditorPC->FinishSplinePlacement();
EditorPC->FinishPointPlacement();
return;
}
}
@@ -830,6 +887,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::SplineEditing
&& 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;
@@ -904,6 +1005,50 @@ 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 (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 TraceEnd = PointLoc - FVector(0.0f, 0.0f, 10000.0f);
FHitResult Hit;
FCollisionQueryParams Params;
Params.AddIgnoredActor(Spline);
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, Params))
{
// 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();
@@ -954,11 +1099,25 @@ void APS_Editor_Pawn::HandleSplineMode(const FInputActionValue& Value)
{
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{
EditorPC->FinishSplinePlacement();
EditorPC->FinishPointPlacement();
}
else
{
EditorPC->BeginSplinePlacement();
// If a spline is selected, append points to it instead of creating a new one
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()))
{
ActiveSplinePointIndex = -1;
DraggedSplineActor = nullptr;
EditorPC->BeginPointPlacementAppend(Spline);
return;
}
}
}
EditorPC->BeginPointPlacement();
}
}
}
@@ -969,7 +1128,7 @@ void APS_Editor_Pawn::HandleConfirm(const FInputActionValue& Value)
{
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{
EditorPC->FinishSplinePlacement();
EditorPC->FinishPointPlacement();
}
}
}
@@ -980,7 +1139,11 @@ void APS_Editor_Pawn::HandleCancel(const FInputActionValue& Value)
{
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{
EditorPC->CancelSplinePlacement();
EditorPC->CancelPointPlacement();
}
else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
{
EditorPC->FinishSplineEditing();
}
}
}

View File

@@ -62,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;
@@ -192,7 +198,6 @@ private:
TObjectPtr<UInputAction> IA_Cancel;
// ---- Spline point drag state ----
int32 ActiveSplinePointIndex = -1;
TWeakObjectPtr<AActor> DraggedSplineActor;
TArray<FVector> SplinePointDragInitialState;
FVector SplinePointDragPlaneOrigin;
@@ -227,7 +232,6 @@ 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);

View File

@@ -6,6 +6,7 @@
#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()
{
@@ -81,70 +82,165 @@ void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
}
}
void APS_Editor_PlayerController::BeginSplinePlacement()
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();
}
ActiveSplineActor = nullptr;
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: Spline placement mode started"));
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Point placement mode started (class: %s)"), *PlacementActorClass->GetName());
}
void APS_Editor_PlayerController::FinishSplinePlacement()
void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActor)
{
if (!ActiveSplineActor || ActiveSplineActor->GetNumPoints() < 2)
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))
{
CancelSplinePlacement();
AppendInitialPoints = Spline->GetAllPointLocations();
Spline->SetHandlesVisible(true);
}
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Appending points to %s"), *ExistingActor->GetName());
}
void APS_Editor_PlayerController::FinishPointPlacement()
{
IPS_Editor_PointPlaceable* Placeable = Cast<IPS_Editor_PointPlaceable>(ActivePlacementActor);
if (!ActivePlacementActor || !Placeable || Placeable->GetCurrentPointCount() < Placeable->GetMinimumPoints())
{
CancelPointPlacement();
return;
}
// Hide handles after placement (shown again on selection)
ActiveSplineActor->SetHandlesVisible(false);
Placeable->OnPlacementFinished();
// Track in SpawnManager — CRITICAL for outliner, save/load, and scene clear
if (SpawnManager)
if (bAppendingToActor)
{
SpawnManager->TrackSpawnedActor(ActiveSplineActor);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline tracked by SpawnManager (total: %d)"), SpawnManager->GetSpawnedActors().Num());
// Appending: record undo for the added points
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);
}
}
if (SelectionManager)
{
SelectionManager->ClearSelection();
SelectionManager->SelectActor(ActivePlacementActor);
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("PS_Editor: SpawnManager is null! Spline will NOT be tracked."));
// 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);
}
// Record spawn action for undo
if (UndoManager)
{
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
Action->Actors.Add(ActiveSplineActor);
Action->Description = FString::Printf(TEXT("Create spline (%d points)"), ActiveSplineActor->GetNumPoints());
Action->Actors.Add(ActivePlacementActor);
Action->Description = FString::Printf(TEXT("Create %s (%d points)"),
*ActivePlacementActor->GetClass()->GetName(), Placeable->GetCurrentPointCount());
UndoManager->RecordAction(Action);
}
// Select the new spline
if (SelectionManager)
{
SelectionManager->SelectActor(ActiveSplineActor);
SelectionManager->SelectActor(ActivePlacementActor);
}
}
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline placed with %d points, class=%s"),
ActiveSplineActor->GetNumPoints(), *ActiveSplineActor->GetClass()->GetPathName());
ActiveSplineActor = nullptr;
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Placement finished (%d points)"), Placeable->GetCurrentPointCount());
ActivePlacementActor = nullptr;
bAppendingToActor = false;
AppendInitialPoints.Empty();
EditorMode = EPS_Editor_EditorMode::Normal;
}
void APS_Editor_PlayerController::CancelSplinePlacement()
void APS_Editor_PlayerController::CancelPointPlacement()
{
if (ActiveSplineActor)
if (ActivePlacementActor)
{
ActiveSplineActor->Destroy();
ActiveSplineActor = nullptr;
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: Spline placement cancelled"));
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"));
}
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)
@@ -159,33 +255,34 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
FHitResult Hit;
FCollisionQueryParams Params;
Params.AddIgnoredActor(GetPawn());
if (ActiveSplineActor) Params.AddIgnoredActor(ActiveSplineActor);
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;
ClickWorldPos = Hit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
}
else
{
ClickWorldPos = WorldOrigin + WorldDirection * 1000.0f;
}
if (!ActiveSplineActor)
if (!ActivePlacementActor)
{
// First click: spawn the spline actor
// First click: spawn the actor
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
ActiveSplineActor = GetWorld()->SpawnActor<APS_Editor_SplineActor>(
APS_Editor_SplineActor::StaticClass(), ClickWorldPos, FRotator::ZeroRotator, SpawnParams);
UClass* ClassToSpawn = PlacementActorClass ? PlacementActorClass.Get() : APS_Editor_SplineActor::StaticClass();
ActivePlacementActor = GetWorld()->SpawnActor<AActor>(ClassToSpawn, ClickWorldPos, FRotator::ZeroRotator, SpawnParams);
}
if (ActiveSplineActor)
// Add point via the interface
if (IPS_Editor_PointPlaceable* Placeable = Cast<IPS_Editor_PointPlaceable>(ActivePlacementActor))
{
ActiveSplineActor->AddPoint(ClickWorldPos);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline point %d added at %s"),
ActiveSplineActor->GetNumPoints(), *ClickWorldPos.ToString());
Placeable->AddPlacementPoint(ClickWorldPos);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Point %d added at %s"),
Placeable->GetCurrentPointCount(), *ClickWorldPos.ToString());
}
return;
}

View File

@@ -11,6 +11,7 @@ class UPS_Editor_SpawnManager;
class UPS_Editor_SpawnCatalog;
class UPS_Editor_SceneSerializer;
class APS_Editor_SplineActor;
class IPS_Editor_PointPlaceable;
/**
* Player controller for PS_Editor runtime editing.
@@ -45,17 +46,29 @@ public:
UFUNCTION(BlueprintCallable, Category = "PS Editor")
EPS_Editor_EditorMode GetEditorMode() const { return EditorMode; }
/** Enter spline placement mode. Clicks will add points. */
void BeginSplinePlacement();
/** Enter point-placement mode for any actor implementing IPS_Editor_PointPlaceable. */
void BeginPointPlacement(TSubclassOf<AActor> InActorClass = nullptr);
/** Finish current spline (min 2 points required). Returns to Normal mode. */
void FinishSplinePlacement();
/** Enter point-placement mode appending to an existing placeable actor. */
void BeginPointPlacementAppend(AActor* ExistingActor);
/** Cancel current spline placement (destroys in-progress spline). */
void CancelSplinePlacement();
/** Finish current placement (min points required). Returns to Normal mode. */
void FinishPointPlacement();
/** Get the spline currently being placed (nullptr if not in SplinePlacement mode). */
APS_Editor_SplineActor* GetActiveSplineActor() const { return ActiveSplineActor; }
/** 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; }
// 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);
/** Exit spline editing mode. */
void FinishSplineEditing();
protected:
virtual void BeginPlay() override;
@@ -77,7 +90,17 @@ private:
EPS_Editor_EditorMode EditorMode = EPS_Editor_EditorMode::Normal;
UPROPERTY()
TObjectPtr<APS_Editor_SplineActor> ActiveSplineActor;
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

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

@@ -186,10 +186,9 @@ void UPS_Editor_SelectionManager::SelectActor(AActor* Actor)
UpdateGizmo();
OnSelectionChanged.Broadcast();
// Show spline handles and mark selected
// Mark spline as selected (handles shown only in SplineEditing mode, not here)
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
{
Spline->SetHandlesVisible(true);
Spline->SetSelected(true);
}
@@ -214,13 +213,25 @@ void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
UpdateGizmo();
OnSelectionChanged.Broadcast();
// Hide spline handles and unmark
// 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::SplineEditing && EditorPC->GetActivePlacementActor() == Actor)
{
EditorPC->FinishSplineEditing();
}
}
}
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
}
}
@@ -244,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::SplineEditing)
{
EditorPC->FinishSplineEditing();
}
}
}
}
}

View File

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

View File

@@ -46,18 +46,24 @@ void APS_Editor_SplineActor::BeginPlay()
UMaterialInterface* SplineLineMat = LoadObject<UMaterialInterface>(
nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine.M_PS_Editor_SplineLine"));
// Spline line: normal depth-tested material (orange)
if (!SplineLineMat)
{
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"), FLinearColor(1.0f, 0.4f, 0.0f));
SplineMat->SetVectorParameterValue(TEXT("Color"), SplineColor);
}
// Spline line selected: no depth test (on top, bright orange)
// Spline line selected: no depth test (uses SplineSelectedColor property)
if (GizmoMat)
{
SplineMatSelected = UMaterialInstanceDynamic::Create(GizmoMat, this);
SplineMatSelected->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 0.6f, 0.0f));
SplineMatSelected->SetVectorParameterValue(TEXT("Color"), SplineSelectedColor);
// Handles always use gizmo material (on top, clickable)
HandleMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
@@ -67,7 +73,7 @@ void APS_Editor_SplineActor::BeginPlay()
HandleHighlightMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 1.0f, 0.0f)); // Yellow
CenterCubeMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 0.4f, 0.0f)); // Orange
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), SplineColor);
}
// Create center cube (selection handle for whole spline)
@@ -189,70 +195,54 @@ void APS_Editor_SplineActor::UpdateVisualization()
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;
// 4 vertices per sample: 2 for vertical ribbon + 2 for horizontal ribbon (cross-section)
Vertices.Reserve(NumSamples * 4);
Normals.Reserve(NumSamples * 4);
UVs.Reserve(NumSamples * 4);
// 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 Right = SplineComp->GetRightVectorAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::Local);
const FVector Up = FVector(0.0f, 0.0f, LineHalfWidth);
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);
// Vertical ribbon (visible from sides)
Vertices.Add(Location - Up);
Vertices.Add(Location + Up);
Normals.Add(Right);
Normals.Add(Right);
UVs.Add(FVector2D(U, 0.0f));
UVs.Add(FVector2D(U, 1.0f));
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;
// Horizontal ribbon (visible from above/below)
Vertices.Add(Location - Right * LineHalfWidth);
Vertices.Add(Location + Right * LineHalfWidth);
Normals.Add(FVector::UpVector);
Normals.Add(FVector::UpVector);
UVs.Add(FVector2D(U, 0.0f));
UVs.Add(FVector2D(U, 1.0f));
Vertices.Add(VertPos);
Normals.Add(Normal);
UVs.Add(FVector2D(U, static_cast<float>(j) / Segs));
}
}
// Generate triangles for both ribbons (double-sided)
Triangles.Reserve((NumSamples - 1) * 24);
// Generate triangles connecting consecutive rings
Triangles.Reserve((NumSamples - 1) * Segs * 6);
for (int32 i = 0; i < NumSamples - 1; i++)
{
// Vertical ribbon indices
const int32 V0 = i * 4;
const int32 V1 = i * 4 + 1;
const int32 V2 = (i + 1) * 4;
const int32 V3 = (i + 1) * 4 + 1;
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;
// Horizontal ribbon indices
const int32 H0 = i * 4 + 2;
const int32 H1 = i * 4 + 3;
const int32 H2 = (i + 1) * 4 + 2;
const int32 H3 = (i + 1) * 4 + 3;
// Vertical: front + back
Triangles.Add(V0); Triangles.Add(V2); Triangles.Add(V1);
Triangles.Add(V1); Triangles.Add(V2); Triangles.Add(V3);
Triangles.Add(V0); Triangles.Add(V1); Triangles.Add(V2);
Triangles.Add(V1); Triangles.Add(V3); Triangles.Add(V2);
// Horizontal: front + back
Triangles.Add(H0); Triangles.Add(H2); Triangles.Add(H1);
Triangles.Add(H1); Triangles.Add(H2); Triangles.Add(H3);
Triangles.Add(H0); Triangles.Add(H1); Triangles.Add(H2);
Triangles.Add(H1); Triangles.Add(H3); Triangles.Add(H2);
Triangles.Add(A); Triangles.Add(C); Triangles.Add(B);
Triangles.Add(B); Triangles.Add(C); Triangles.Add(D);
}
}
SplineMeshComp->ClearAllMeshSections();
@@ -416,17 +406,21 @@ void APS_Editor_SplineActor::SetSelected(bool bSelected)
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: bright when selected
// Center cube: selected color when selected, normal color otherwise
if (CenterCubeMat)
{
const FLinearColor Color = bSelected ? FLinearColor(1.0f, 0.8f, 0.0f) : FLinearColor(1.0f, 0.4f, 0.0f);
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), Color);
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), bSelected ? SplineSelectedColor : SplineColor);
}
}
@@ -435,6 +429,50 @@ 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();
@@ -462,6 +500,14 @@ void APS_Editor_SplineActor::ExportToCustomProperties(TMap<FString, FString>& Ou
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)
@@ -490,4 +536,19 @@ void APS_Editor_SplineActor::ImportFromCustomProperties(const TMap<FString, FStr
{
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

@@ -2,6 +2,7 @@
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PS_Editor_PointPlaceable.h"
#include "PS_Editor_SplineActor.generated.h"
class USplineComponent;
@@ -12,13 +13,34 @@ class UProceduralMeshComponent;
* Renders as a visible quad strip with clickable point handles.
*/
UCLASS()
class PS_EDITOR_API APS_Editor_SplineActor : public AActor
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). */
@@ -47,6 +69,10 @@ public:
/** 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. */
@@ -69,6 +95,15 @@ public:
// ---- 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;
@@ -122,8 +157,9 @@ private:
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> CenterCubeMat;
/** Line rendering settings. */
float LineHalfWidth = 2.0f;
/** Tube rendering settings. */
float TubeRadius = 2.0f;
int32 TubeSegments = 8;
float SampleInterval = 10.0f;
float HandleScale = 0.12f;
float CenterCubeScale = 0.10f;

View File

@@ -7,6 +7,8 @@
#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"
@@ -116,21 +118,100 @@ 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")))
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::SplineEditing)
{
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::SplineEditing)
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))
]
]
]
];
}
@@ -250,11 +331,11 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
{
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{
EditorPC->FinishSplinePlacement();
EditorPC->FinishPointPlacement();
}
else
{
EditorPC->BeginSplinePlacement();
EditorPC->BeginPointPlacement();
}
}
return FReply::Handled();
@@ -673,11 +754,25 @@ 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();
})
[
SNew(SBox)
@@ -688,6 +783,74 @@ 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)
@@ -708,21 +871,20 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
return;
}
// Update mode text
if (ModeText.IsValid())
{
// Check for spline placement mode first
bool bSplineMode = false;
// 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()))
{
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
{
ModeText->SetText(FText::FromString(TEXT("Spline Placement (click to add, RMB/Enter to finish, Esc to cancel)")));
bSplineMode = true;
}
CurrentEditorMode = EditorPC->GetEditorMode();
}
if (!bSplineMode)
if (ModeText.IsValid())
{
if (CurrentEditorMode == EPS_Editor_EditorMode::SplinePlacement)
{
ModeText->SetText(FText::FromString(TEXT("Spline Placement")));
}
else
{
FString ModeStr;
switch (SM->GetTransformMode())
@@ -735,6 +897,80 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
}
}
// 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)
{
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()))
{
bPointSelected = (Pawn->ActiveSplinePointIndex >= 0);
}
if (HelpBarText.IsValid())
{
if (bPointSelected)
{
HelpBarText->SetText(FText::FromString(TEXT("EDIT SPLINE: Drag gizmo to move point | Del: delete point | Click line to insert | Escape: done")));
}
else
{
HelpBarText->SetText(FText::FromString(TEXT("EDIT SPLINE: Click handle to select point | Click line to insert | End: snap to ground | Escape: done")));
}
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
if (SpaceText.IsValid())
{

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