diff --git a/Unreal/Content/test.umap b/Unreal/Content/test.umap index 78cdd07..17beaff 100644 Binary files a/Unreal/Content/test.umap and b/Unreal/Content/test.umap differ 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 c3c2900..0f174b6 100644 Binary files a/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset and b/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset differ diff --git a/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_SplineLine.uasset b/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_SplineLine.uasset index 38ccfde..994eff0 100644 Binary files a/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_SplineLine.uasset and b/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_SplineLine.uasset differ 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();