From ca4f7ee35577feb4e09e561fbc0582c91667b734 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Sun, 12 Apr 2026 09:44:26 +0200 Subject: [PATCH] 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) --- Unreal/Content/test.umap | Bin 54794 -> 54794 bytes .../Content/M_PS_Editor_Gizmo.uasset | Bin 11245 -> 11871 bytes .../Content/M_PS_Editor_SplineLine.uasset | Bin 5255 -> 13798 bytes .../PS_Editor/GameMode/PS_Editor_Pawn.cpp | 177 ++++++++++- .../PS_Editor/GameMode/PS_Editor_Pawn.h | 8 +- .../GameMode/PS_Editor_PlayerController.cpp | 191 +++++++++--- .../GameMode/PS_Editor_PlayerController.h | 41 ++- .../Selection/PS_Editor_PointPlaceable.h | 39 +++ .../Selection/PS_Editor_SelectionManager.cpp | 36 ++- .../Selection/PS_Editor_SelectionTypes.h | 3 +- .../Spline/PS_Editor_SplineActor.cpp | 165 +++++++---- .../PS_Editor/Spline/PS_Editor_SplineActor.h | 42 ++- .../UI/Widgets/PS_Editor_MainWidget.cpp | 278 ++++++++++++++++-- .../UI/Widgets/PS_Editor_MainWidget.h | 6 + 14 files changed, 841 insertions(+), 145 deletions(-) create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_PointPlaceable.h diff --git a/Unreal/Content/test.umap b/Unreal/Content/test.umap index 78cdd07d7e5818a45278dd1fd96d9bc560f1de63..17beaffac203855f461e26a9929f62fec7bc09d6 100644 GIT binary patch delta 148 zcmV;F0Biq>tOJUy1F*mW5W@pYUki)vO^c2lb2>n#*R#+89AyVFE-*PRF)*|9X1p2@ zFl^4tMi#l^)Bv%i7o3lJQ;!!rCSYbl99*TFLr%K3jlc9Vf=6O(VL9S|@8K>h!JF~C>=VB&g|N5HcTto|wa CKtuKb delta 148 zcmV;F0Biq>tOJUy1F*mW5TQGo!8=)OO0Dr;5SME-*|X3A9AyVEE;TnUFf+6BX1p2@ zi(BahCzv05&QpF4t0M~Hv%i7o3lOrZdqCf(K7KRV(oldtV=j}wfhn`!jno|(%(qi= zn3#P)>4i*GDj$Wwszi?o_CbU|c9Vf=6O(VL9S{Zp;8VEXK)@CNppn8gRY0>0to|vQ CpFq(7 diff --git a/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset b/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset index c3c290019e3a88784e8c79889c5eef26db8add72..0f174b665b23bb52be8e112cecd8a6e526c16310 100644 GIT binary patch delta 3819 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 C;5BCe delta 3418 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 Q3#uxFiL@xL2zt!6KY8*p82|tP diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp index bb28556..0cadbe7 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp @@ -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(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(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& Weak : SM2->GetSelectedActors()) + { + APS_Editor_SplineActor* Spline = Cast(Weak.Get()); + if (!Spline) continue; + + if (Spline->IsSplineMesh(LineHit.GetComponent())) + { + const FVector HitPos = LineHit.ImpactPoint; + + TArray OldPoints = Spline->GetAllPointLocations(); + const int32 InsertIdx = Spline->FindInsertIndex(HitPos); + Spline->InsertPoint(InsertIdx, HitPos); + + if (UPS_Editor_UndoManager* UM = EdPC2->GetUndoManager()) + { + TSharedPtr Action = MakeShared(); + 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(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(DraggedSplineActor.Get()); + if (Spline && Spline->GetNumPoints() > 2) + { + TArray OldPoints = Spline->GetAllPointLocations(); + Spline->RemovePoint(ActiveSplinePointIndex); + + if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) + { + TSharedPtr Action = MakeShared(); + 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(GetController())) { + // If a spline point is selected, snap that point to ground + if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) + { + if (APS_Editor_SplineActor* Spline = Cast(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 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 Action = MakeShared(); + 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& Weak : SM->GetSelectedActors()) + { + if (APS_Editor_SplineActor* Spline = Cast(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(); } } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h index 742f0bc..39e5bb8 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h @@ -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 IA_Cancel; // ---- Spline point drag state ---- - int32 ActiveSplinePointIndex = -1; TWeakObjectPtr DraggedSplineActor; TArray 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); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp index 64fe95f..51783e7 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp @@ -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(ActivePlacementActor); +} + +void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf InActorClass) { if (SelectionManager) { SelectionManager->ClearSelection(); } - ActiveSplineActor = nullptr; + ActivePlacementActor = nullptr; + PlacementActorClass = InActorClass ? InActorClass : TSubclassOf(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()) return; + + ActivePlacementActor = ExistingActor; + bAppendingToActor = true; + EditorMode = EPS_Editor_EditorMode::SplinePlacement; + + // Store initial state for undo (spline-specific) + if (APS_Editor_SplineActor* Spline = Cast(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(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(ActivePlacementActor)) + { + Spline->SetHandlesVisible(false); + if (UndoManager) + { + TSharedPtr Action = MakeShared(); + 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(ActivePlacementActor)) + { + Spline->SetHandlesVisible(false); + } + + if (SpawnManager) + { + SpawnManager->TrackSpawnedActor(ActivePlacementActor); + } + + if (UndoManager) + { + TSharedPtr Action = MakeShared(); + 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); + } } - // Record spawn action for undo - if (UndoManager) - { - TSharedPtr Action = MakeShared(); - Action->Actors.Add(ActiveSplineActor); - Action->Description = FString::Printf(TEXT("Create spline (%d points)"), ActiveSplineActor->GetNumPoints()); - UndoManager->RecordAction(Action); - } - - // Select the new spline - if (SelectionManager) - { - SelectionManager->SelectActor(ActiveSplineActor); - } - - UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline placed with %d points, class=%s"), - ActiveSplineActor->GetNumPoints(), *ActiveSplineActor->GetClass()->GetPathName()); - - ActiveSplineActor = nullptr; + 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(ActivePlacementActor); + if (Placeable) Placeable->OnPlacementCancelled(); + + if (bAppendingToActor) + { + if (APS_Editor_SplineActor* Spline = Cast(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(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::StaticClass(), ClickWorldPos, FRotator::ZeroRotator, SpawnParams); + UClass* ClassToSpawn = PlacementActorClass ? PlacementActorClass.Get() : APS_Editor_SplineActor::StaticClass(); + ActivePlacementActor = GetWorld()->SpawnActor(ClassToSpawn, ClickWorldPos, FRotator::ZeroRotator, SpawnParams); } - if (ActiveSplineActor) + // Add point via the interface + if (IPS_Editor_PointPlaceable* Placeable = Cast(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; } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h index e5afd08..c3a3b5e 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h @@ -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 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 ActiveSplineActor; + TObjectPtr ActivePlacementActor; + + /** Class to use when spawning the placement actor. */ + UPROPERTY() + TSubclassOf PlacementActorClass; + + /** True when appending to an existing actor (vs creating new). */ + bool bAppendingToActor = false; + + /** Snapshot of spline points before append, for undo. */ + TArray AppendInitialPoints; void HandleEditorClick(FVector2D ScreenPos, bool bAdditive); }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_PointPlaceable.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_PointPlaceable.h new file mode 100644 index 0000000..a04ec5f --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_PointPlaceable.h @@ -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() {} +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp index e6ad249..935269f 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp @@ -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(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(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(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(Actor)) + { + Spline->SetHandlesVisible(false); + Spline->SetSelected(false); + } + + // Exit spline editing if needed + if (APlayerController* PC = OwnerPC.Get()) + { + if (APS_Editor_PlayerController* EditorPC = Cast(PC)) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing) + { + EditorPC->FinishSplineEditing(); + } + } + } } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionTypes.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionTypes.h index 5837a43..9752d07 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionTypes.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionTypes.h @@ -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 }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.cpp index 88e2e8a..85842e1 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.cpp @@ -46,18 +46,24 @@ void APS_Editor_SplineActor::BeginPlay() UMaterialInterface* SplineLineMat = LoadObject( 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(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 Vertices; TArray Triangles; TArray Normals; TArray 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(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(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(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(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 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& 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& Properties) @@ -490,4 +536,19 @@ void APS_Editor_SplineActor::ImportFromCustomProperties(const TMap 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; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index 8884057..16bc331 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -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,20 +118,99 @@ TSharedRef 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(GetOwningPlayer())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing) + { + EditorPC->FinishSplineEditing(); + } + else if (UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get()) + { + for (const TWeakObjectPtr& Weak : SelMgr->GetSelectedActors()) + { + if (APS_Editor_SplineActor* Spline = Cast(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(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(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 UPS_Editor_MainWidget::BuildToolbar() { if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) { - EditorPC->FinishSplinePlacement(); + EditorPC->FinishPointPlacement(); } else { - EditorPC->BeginSplinePlacement(); + EditorPC->BeginPointPlacement(); } } return FReply::Handled(); @@ -673,10 +754,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& 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(GetOwningPlayer())) + { + EditorPC->BeginPointPlacement(AllEntries[EntryIndex].ActorClass); + } + return FReply::Handled(); + } } + + Mgr->SpawnFromCatalog(EntryIndex); return FReply::Handled(); }) [ @@ -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(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& Weak : SelMgr->GetSelectedActors()) + { + if (APS_Editor_SplineActor* Spline = Cast(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 + // Update mode text + help bar based on editor mode + EPS_Editor_EditorMode CurrentEditorMode = EPS_Editor_EditorMode::Normal; + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + CurrentEditorMode = EditorPC->GetEditorMode(); + } + if (ModeText.IsValid()) { - // Check for spline placement mode first - bool bSplineMode = false; - if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + if (CurrentEditorMode == EPS_Editor_EditorMode::SplinePlacement) { - 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; - } + ModeText->SetText(FText::FromString(TEXT("Spline Placement"))); } - - if (!bSplineMode) + 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& Weak : SM->GetSelectedActors()) + { + if (Cast(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(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()) { diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h index 59abe9d..1abc061 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h @@ -87,6 +87,12 @@ private: TSharedPtr OutlinerListContainer; int32 LastOutlinerActorCount = -1; + // Bottom notification bar + TSharedPtr HelpBarText; + TSharedPtr SplineFinishButton; + TSharedPtr SplineEditButton; + TSharedPtr SplineDeletePointButton; + // ---- Helpers ---- TSharedRef BuildToolbar(); TSharedRef BuildPropertiesPanel();