diff --git a/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_SplineLine.uasset b/Unreal/Plugins/PS_Editor/Content/M_PS_Editor_SplineLine.uasset new file mode 100644 index 0000000..38ccfde Binary files /dev/null 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 07b3ab2..bb28556 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 @@ -13,6 +13,7 @@ #include "PS_Editor_PlayerController.h" #include "PS_Editor_Gizmo.h" #include "PS_Editor_UndoManager.h" +#include "PS_Editor_SplineActor.h" #include "DrawDebugHelpers.h" #include "PS_Editor_ConfirmDialog.h" @@ -199,6 +200,19 @@ void APS_Editor_Pawn::CreateInputActions() IA_Paste = NewObject(this, TEXT("IA_Paste")); IA_Paste->ValueType = EInputActionValueType::Boolean; EditorMappingContext->MapKey(IA_Paste, EKeys::V); + + // Spline mode / Confirm / Cancel + IA_SplineMode = NewObject(this, TEXT("IA_SplineMode")); + IA_SplineMode->ValueType = EInputActionValueType::Boolean; + EditorMappingContext->MapKey(IA_SplineMode, EKeys::S); + + IA_Confirm = NewObject(this, TEXT("IA_Confirm")); + IA_Confirm->ValueType = EInputActionValueType::Boolean; + EditorMappingContext->MapKey(IA_Confirm, EKeys::Enter); + + IA_Cancel = NewObject(this, TEXT("IA_Cancel")); + IA_Cancel->ValueType = EInputActionValueType::Boolean; + EditorMappingContext->MapKey(IA_Cancel, EKeys::Escape); } void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) @@ -242,6 +256,9 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp EIC->BindAction(IA_Duplicate, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDuplicate); EIC->BindAction(IA_Copy, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCopy); EIC->BindAction(IA_Paste, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandlePaste); + EIC->BindAction(IA_SplineMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSplineMode); + EIC->BindAction(IA_Confirm, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleConfirm); + EIC->BindAction(IA_Cancel, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCancel); } // ---- Input Handlers ---- @@ -357,11 +374,26 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value) } const FVector ConstrainedDelta = DisplacementAlongAxis * AxisDir; - for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i) + // Spline point mode: move the single point instead of actors + if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) { - if (AActor* Actor = Selected[i].Get()) + if (APS_Editor_SplineActor* SplineActor = Cast(DraggedSplineActor.Get())) { - Actor->SetActorLocation(GizmoDragInitialTransforms[i].GetLocation() + ConstrainedDelta); + if (SplinePointDragInitialState.IsValidIndex(ActiveSplinePointIndex)) + { + const FVector NewPos = SplinePointDragInitialState[ActiveSplinePointIndex] + ConstrainedDelta; + SplineActor->MovePoint(ActiveSplinePointIndex, NewPos); + } + } + } + else + { + for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i) + { + if (AActor* Actor = Selected[i].Get()) + { + Actor->SetActorLocation(GizmoDragInitialTransforms[i].GetLocation() + ConstrainedDelta); + } } } } @@ -432,10 +464,33 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value) } } - // In translate mode, gizmo follows the selection. In rotate/scale, stay at initial position. + // In translate mode, gizmo follows the target if (Mode == EPS_Editor_TransformMode::Translate) { - Gizmo->SetActorLocation(SM->GetSelectionPivot()); + if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) + { + // Following a single spline point + if (APS_Editor_SplineActor* SplineActor = Cast(DraggedSplineActor.Get())) + { + Gizmo->SetActorLocation(SplineActor->GetPointLocation(ActiveSplinePointIndex)); + } + } + else if (Selected.Num() == 1) + { + // Single actor: use centroid for splines, location for others + if (APS_Editor_SplineActor* SplineActor = Cast(Selected[0].Get())) + { + Gizmo->SetActorLocation(SplineActor->GetCentroid()); + } + else + { + Gizmo->SetActorLocation(SM->GetSelectionPivot()); + } + } + else + { + Gizmo->SetActorLocation(SM->GetSelectionPivot()); + } } // Update local orientation in real-time during rotation drag @@ -447,6 +502,25 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value) } } } + else if (CameraMode == EPS_Editor_CameraMode::SplinePointDrag) + { + // Drag spline point along horizontal plane + APS_Editor_SplineActor* Spline = Cast(DraggedSplineActor.Get()); + if (!Spline || ActiveSplinePointIndex < 0) return; + + FVector RayOrigin, RayDir; + if (!GetMouseWorldRay(RayOrigin, RayDir)) return; + + // Ray-plane intersection + const float Denom = FVector::DotProduct(RayDir, SplinePointDragPlaneNormal); + if (FMath::Abs(Denom) < KINDA_SMALL_NUMBER) return; + + const float T = FVector::DotProduct(SplinePointDragPlaneOrigin - RayOrigin, SplinePointDragPlaneNormal) / Denom; + if (T < 0.0f) return; + + const FVector NewPos = RayOrigin + RayDir * T; + Spline->MovePoint(ActiveSplinePointIndex, NewPos); + } } void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value) @@ -502,6 +576,60 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value) } } + // Check for spline point handle hit → select point, move gizmo to it (no drag yet) + if (APS_Editor_PlayerController* EdPC = Cast(GetController())) + { + if (EdPC->GetEditorMode() == EPS_Editor_EditorMode::Normal) + { + if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager()) + { + for (const TWeakObjectPtr& Weak : SM->GetSelectedActors()) + { + if (APS_Editor_SplineActor* Spline = Cast(Weak.Get())) + { + // First check: did we hit the center cube? If so, skip handle detection + // (let the click fall through to normal selection → gizmo at actor pivot) + FHitResult SplineHit; + FCollisionQueryParams SplineParams; + SplineParams.AddIgnoredActor(this); + const FVector TraceEnd = RayOrigin + RayDir * 100000.0f; + if (GetWorld()->LineTraceSingleByChannel(SplineHit, RayOrigin, TraceEnd, ECC_Visibility, SplineParams)) + { + if (Spline->IsCenterCube(SplineHit.GetComponent())) + { + // Center cube hit → reset point selection, let normal flow handle it + ActiveSplinePointIndex = -1; + DraggedSplineActor = nullptr; + Spline->ClearHandleHighlight(); + break; + } + } + + const int32 HandleIdx = Spline->GetHandleIndexUnderCursor(RayOrigin, RayDir); + if (HandleIdx >= 0) + { + ActiveSplinePointIndex = HandleIdx; + DraggedSplineActor = Spline; + Spline->HighlightHandle(HandleIdx); + + if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo()) + { + Gizmo->SetActorLocation(Spline->GetPointLocation(HandleIdx)); + } + + SM->SetTransformMode(EPS_Editor_TransformMode::Translate); + return; + } + } + } + } + } + } + + // Clicking elsewhere resets spline point selection + ActiveSplinePointIndex = -1; + DraggedSplineActor = nullptr; + // No gizmo hit: enter PendingClick PendingClickScreenPos = GetMouseScreenPosition(); SetCameraMode(EPS_Editor_CameraMode::PendingClick); @@ -522,8 +650,22 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value) { if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) { - if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager()) + // Spline point drag: record SplinePointAction + if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) { + if (APS_Editor_SplineActor* SplineActor = Cast(DraggedSplineActor.Get())) + { + TSharedPtr Action = MakeShared(); + Action->SplineActor = SplineActor; + Action->OldPoints = SplinePointDragInitialState; + Action->NewPoints = SplineActor->GetAllPointLocations(); + Action->Description = TEXT("Move spline point"); + UM->RecordAction(Action); + } + } + else if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager()) + { + // Normal: record TransformAction TSharedPtr Action = MakeShared(); const TArray>& Selected = SM->GetSelectedActors(); @@ -555,17 +697,53 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value) } ActiveGizmoAxis = EPS_Editor_GizmoAxis::None; - // Refresh gizmo orientation (local mode needs to track actor's new rotation) + // Refresh gizmo: keep at spline point if editing, else refresh orientation if (APS_Editor_PlayerController* PC2 = Cast(GetController())) { if (UPS_Editor_SelectionManager* SM2 = PC2->GetSelectionManager()) { - SM2->RefreshGizmoOrientation(); + if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) + { + // Keep gizmo at the active spline point + if (APS_Editor_SplineActor* SplineActor = Cast(DraggedSplineActor.Get())) + { + if (APS_Editor_Gizmo* G = SM2->GetGizmo()) + { + G->SetActorLocation(SplineActor->GetPointLocation(ActiveSplinePointIndex)); + } + } + } + else + { + SM2->RefreshGizmoOrientation(); + } } } SetCameraMode(EPS_Editor_CameraMode::Idle); } + else if (CameraMode == EPS_Editor_CameraMode::SplinePointDrag) + { + // Record undo for spline point move + if (APS_Editor_SplineActor* Spline = Cast(DraggedSplineActor.Get())) + { + if (APS_Editor_PlayerController* EditorPC = Cast(GetController())) + { + if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) + { + TSharedPtr Action = MakeShared(); + Action->SplineActor = Spline; + Action->OldPoints = SplinePointDragInitialState; + Action->NewPoints = Spline->GetAllPointLocations(); + Action->Description = TEXT("Move spline point"); + UM->RecordAction(Action); + } + } + } + ActiveSplinePointIndex = -1; + DraggedSplineActor = nullptr; + SetCameraMode(EPS_Editor_CameraMode::Idle); + } else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit) { SetCameraMode(EPS_Editor_CameraMode::Idle); @@ -574,6 +752,15 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value) void APS_Editor_Pawn::HandleRightClickStarted(const FInputActionValue& Value) { + // In spline placement mode, right-click finishes the spline + if (APS_Editor_PlayerController* EditorPC = Cast(GetController())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + { + EditorPC->FinishSplinePlacement(); + return; + } + } SetCameraMode(EPS_Editor_CameraMode::Fly); } @@ -760,6 +947,44 @@ void APS_Editor_Pawn::HandlePaste(const FInputActionValue& Value) } } +void APS_Editor_Pawn::HandleSplineMode(const FInputActionValue& Value) +{ + if (CameraMode != EPS_Editor_CameraMode::Idle) return; + if (APS_Editor_PlayerController* EditorPC = Cast(GetController())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + { + EditorPC->FinishSplinePlacement(); + } + else + { + EditorPC->BeginSplinePlacement(); + } + } +} + +void APS_Editor_Pawn::HandleConfirm(const FInputActionValue& Value) +{ + if (APS_Editor_PlayerController* EditorPC = Cast(GetController())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + { + EditorPC->FinishSplinePlacement(); + } + } +} + +void APS_Editor_Pawn::HandleCancel(const FInputActionValue& Value) +{ + if (APS_Editor_PlayerController* EditorPC = Cast(GetController())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + { + EditorPC->CancelSplinePlacement(); + } + } +} + // ---- Helpers ---- void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode) @@ -944,5 +1169,14 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis) } } + // Store initial spline state if editing a point + if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) + { + if (APS_Editor_SplineActor* Spline = Cast(DraggedSplineActor.Get())) + { + SplinePointDragInitialState = Spline->GetAllPointLocations(); + } + } + SetCameraMode(EPS_Editor_CameraMode::GizmoDrag); } 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 b6c8f84..742f0bc 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 @@ -31,7 +31,9 @@ enum class EPS_Editor_CameraMode : uint8 /** Alt + left-click or middle-click: orbit around focal point. */ Orbit, /** Dragging a gizmo handle. */ - GizmoDrag + GizmoDrag, + /** Dragging a spline point handle. */ + SplinePointDrag }; /** @@ -180,6 +182,22 @@ private: UPROPERTY() TObjectPtr IA_Paste; + UPROPERTY() + TObjectPtr IA_SplineMode; + + UPROPERTY() + TObjectPtr IA_Confirm; + + UPROPERTY() + TObjectPtr IA_Cancel; + + // ---- Spline point drag state ---- + int32 ActiveSplinePointIndex = -1; + TWeakObjectPtr DraggedSplineActor; + TArray SplinePointDragInitialState; + FVector SplinePointDragPlaneOrigin; + FVector SplinePointDragPlaneNormal; + // ---- Modifier key state ---- bool bAltHeld = false; bool bCtrlHeld = false; @@ -216,6 +234,9 @@ private: void HandleDuplicate(const FInputActionValue& Value); void HandleCopy(const FInputActionValue& Value); void HandlePaste(const FInputActionValue& Value); + void HandleSplineMode(const FInputActionValue& Value); + void HandleConfirm(const FInputActionValue& Value); + void HandleCancel(const FInputActionValue& Value); // ---- Helpers ---- void CreateInputActions(); 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 859987c..64fe95f 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 @@ -5,6 +5,7 @@ #include "PS_Editor_SceneSerializer.h" #include "PS_Editor_Gizmo.h" #include "PS_Editor_Pawn.h" +#include "PS_Editor_SplineActor.h" APS_Editor_PlayerController::APS_Editor_PlayerController() { @@ -80,8 +81,116 @@ void APS_Editor_PlayerController::OnPossess(APawn* InPawn) } } +void APS_Editor_PlayerController::BeginSplinePlacement() +{ + if (SelectionManager) + { + SelectionManager->ClearSelection(); + } + ActiveSplineActor = nullptr; + EditorMode = EPS_Editor_EditorMode::SplinePlacement; + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline placement mode started")); +} + +void APS_Editor_PlayerController::FinishSplinePlacement() +{ + if (!ActiveSplineActor || ActiveSplineActor->GetNumPoints() < 2) + { + CancelSplinePlacement(); + return; + } + + // Hide handles after placement (shown again on selection) + ActiveSplineActor->SetHandlesVisible(false); + + // Track in SpawnManager — CRITICAL for outliner, save/load, and scene clear + if (SpawnManager) + { + SpawnManager->TrackSpawnedActor(ActiveSplineActor); + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline tracked by SpawnManager (total: %d)"), SpawnManager->GetSpawnedActors().Num()); + } + else + { + UE_LOG(LogTemp, Error, TEXT("PS_Editor: SpawnManager is null! Spline will NOT be tracked.")); + } + + // 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; + EditorMode = EPS_Editor_EditorMode::Normal; +} + +void APS_Editor_PlayerController::CancelSplinePlacement() +{ + if (ActiveSplineActor) + { + ActiveSplineActor->Destroy(); + ActiveSplineActor = nullptr; + } + EditorMode = EPS_Editor_EditorMode::Normal; + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline placement cancelled")); +} + void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive) { + if (EditorMode == EPS_Editor_EditorMode::SplinePlacement) + { + // Deproject click to world position + FVector WorldOrigin, WorldDirection; + if (!DeprojectScreenPositionToWorld(ScreenPos.X, ScreenPos.Y, WorldOrigin, WorldDirection)) + return; + + FHitResult Hit; + FCollisionQueryParams Params; + Params.AddIgnoredActor(GetPawn()); + if (ActiveSplineActor) Params.AddIgnoredActor(ActiveSplineActor); + + const FVector End = WorldOrigin + WorldDirection * 100000.0f; + FVector ClickWorldPos; + if (GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Visibility, Params)) + { + ClickWorldPos = Hit.ImpactPoint; + } + else + { + ClickWorldPos = WorldOrigin + WorldDirection * 1000.0f; + } + + if (!ActiveSplineActor) + { + // First click: spawn the spline actor + FActorSpawnParameters SpawnParams; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + ActiveSplineActor = GetWorld()->SpawnActor( + APS_Editor_SplineActor::StaticClass(), ClickWorldPos, FRotator::ZeroRotator, SpawnParams); + } + + if (ActiveSplineActor) + { + ActiveSplineActor->AddPoint(ClickWorldPos); + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline point %d added at %s"), + ActiveSplineActor->GetNumPoints(), *ClickWorldPos.ToString()); + } + return; + } + + // Normal mode: selection if (SelectionManager) { SelectionManager->HandleClickAtScreenPosition(ScreenPos, bAdditive); 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 1be1722..e5afd08 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 @@ -2,6 +2,7 @@ #include "CoreMinimal.h" #include "GameFramework/PlayerController.h" +#include "PS_Editor_SelectionTypes.h" #include "PS_Editor_PlayerController.generated.h" class UPS_Editor_SelectionManager; @@ -9,6 +10,7 @@ class UPS_Editor_UndoManager; class UPS_Editor_SpawnManager; class UPS_Editor_SpawnCatalog; class UPS_Editor_SceneSerializer; +class APS_Editor_SplineActor; /** * Player controller for PS_Editor runtime editing. @@ -38,6 +40,23 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") TSoftObjectPtr SpawnCatalogAsset; + // ---- Editor Mode (Normal / SplinePlacement) ---- + + UFUNCTION(BlueprintCallable, Category = "PS Editor") + EPS_Editor_EditorMode GetEditorMode() const { return EditorMode; } + + /** Enter spline placement mode. Clicks will add points. */ + void BeginSplinePlacement(); + + /** Finish current spline (min 2 points required). Returns to Normal mode. */ + void FinishSplinePlacement(); + + /** Cancel current spline placement (destroys in-progress spline). */ + void CancelSplinePlacement(); + + /** Get the spline currently being placed (nullptr if not in SplinePlacement mode). */ + APS_Editor_SplineActor* GetActiveSplineActor() const { return ActiveSplineActor; } + protected: virtual void BeginPlay() override; virtual void OnPossess(APawn* InPawn) override; @@ -55,5 +74,10 @@ private: UPROPERTY() TObjectPtr SceneSerializer; + EPS_Editor_EditorMode EditorMode = EPS_Editor_EditorMode::Normal; + + UPROPERTY() + TObjectPtr ActiveSplineActor; + void HandleEditorClick(FVector2D ScreenPos, bool bAdditive); }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs index 4bbc3e7..f11ee1a 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs @@ -17,6 +17,7 @@ public class PS_Editor : ModuleRules PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo")); PublicIncludePaths.Add(Path.Combine(SourceDir, "Spawn")); PublicIncludePaths.Add(Path.Combine(SourceDir, "Serialization")); + PublicIncludePaths.Add(Path.Combine(SourceDir, "Spline")); PublicDependencyModuleNames.AddRange(new string[] { diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.cpp index 80c694e..58d3d62 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.cpp @@ -15,6 +15,7 @@ #include "Materials/MaterialExpressionLinearInterpolate.h" #include "Materials/MaterialExpressionComponentMask.h" #include "Materials/MaterialExpressionAppendVector.h" +#include "Materials/MaterialExpressionVectorParameter.h" #include "AssetRegistry/AssetRegistryModule.h" #include "UObject/SavePackage.h" #include "Misc/PackageName.h" @@ -218,6 +219,48 @@ static void CreateOutlineMaterialAsset() UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material auto-generated (8-sample, dynamic resolution)")); } + +static void CreateSplineLineMaterialAsset() +{ + const FString MaterialPath = TEXT("/PS_Editor/M_PS_Editor_SplineLine"); + const FString AssetName = TEXT("M_PS_Editor_SplineLine"); + + if (LoadObject(nullptr, *(MaterialPath + TEXT(".") + AssetName))) + { + return; // Already exists + } + + UPackage* Package = CreatePackage(*MaterialPath); + UMaterial* Mat = NewObject(Package, *AssetName, RF_Public | RF_Standalone); + Mat->MaterialDomain = EMaterialDomain::MD_Surface; + Mat->SetShadingModel(EMaterialShadingModel::MSM_Unlit); + Mat->BlendMode = EBlendMode::BLEND_Opaque; + Mat->TwoSided = true; + + auto AddExpr = [&](UMaterialExpression* Expr) { Mat->GetExpressionCollection().AddExpression(Expr); }; + + // Vector parameter "Color" (default orange) + UMaterialExpressionVectorParameter* ColorParam = NewObject(Mat); + ColorParam->ParameterName = TEXT("Color"); + ColorParam->DefaultValue = FLinearColor(1.0f, 0.4f, 0.0f, 1.0f); + AddExpr(ColorParam); + ColorParam->MaterialExpressionEditorX = -200; + ColorParam->MaterialExpressionEditorY = 0; + + // Connect Color → Emissive + Mat->GetEditorOnlyData()->EmissiveColor.Connect(0, ColorParam); + + Mat->PreEditChange(nullptr); + Mat->PostEditChange(); + + FString PackageFilename = FPackageName::LongPackageNameToFilename(MaterialPath, FPackageName::GetAssetPackageExtension()); + FSavePackageArgs SaveArgs; + SaveArgs.TopLevelFlags = RF_Public | RF_Standalone; + UPackage::SavePackage(Package, Mat, *PackageFilename, SaveArgs); + FAssetRegistryModule::AssetCreated(Mat); + + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline line material auto-generated (unlit opaque with Color parameter)")); +} #endif void FPS_EditorModule::StartupModule() @@ -226,6 +269,7 @@ void FPS_EditorModule::StartupModule() #if WITH_EDITOR FCoreDelegates::OnPostEngineInit.AddStatic(&CreateOutlineMaterialAsset); + FCoreDelegates::OnPostEngineInit.AddStatic(&CreateSplineLineMaterialAsset); #endif } 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 56ac1c9..e6ad249 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 @@ -4,6 +4,7 @@ #include "PS_Editor_PlayerController.h" #include "PS_Editor_SpawnManager.h" #include "PS_Editor_EditableComponent.h" +#include "PS_Editor_SplineActor.h" #include "GameFramework/PlayerController.h" #include "Engine/World.h" #include "GameFramework/Pawn.h" @@ -185,6 +186,13 @@ void UPS_Editor_SelectionManager::SelectActor(AActor* Actor) UpdateGizmo(); OnSelectionChanged.Broadcast(); + // Show spline handles and mark selected + if (APS_Editor_SplineActor* Spline = Cast(Actor)) + { + Spline->SetHandlesVisible(true); + Spline->SetSelected(true); + } + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selected %s"), *Actor->GetName()); } @@ -206,6 +214,13 @@ void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor) UpdateGizmo(); OnSelectionChanged.Broadcast(); + // Hide spline handles and unmark + if (APS_Editor_SplineActor* Spline = Cast(Actor)) + { + Spline->SetHandlesVisible(false); + Spline->SetSelected(false); + } + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deselected %s"), *Actor->GetName()); } } @@ -269,7 +284,15 @@ FVector UPS_Editor_SelectionManager::GetSelectionPivot() const { if (AActor* Actor = Weak.Get()) { - Sum += Actor->GetActorLocation(); + // For SplineActors, use the centroid of all points as pivot + if (APS_Editor_SplineActor* Spline = Cast(Actor)) + { + Sum += Spline->GetCentroid(); + } + else + { + Sum += Actor->GetActorLocation(); + } Count++; } } @@ -448,7 +471,18 @@ void UPS_Editor_SelectionManager::UpdateGizmo() if (IsAnythingSelected()) { - Gizmo->SetGizmoActive(true, GetSelectionPivot()); + FVector Pivot = GetSelectionPivot(); + + // Override pivot for SplineActors: use centroid so gizmo aligns with center cube + if (SelectedActors.Num() == 1) + { + if (APS_Editor_SplineActor* Spline = Cast(SelectedActors[0].Get())) + { + Pivot = Spline->GetCentroid(); + } + } + + Gizmo->SetGizmoActive(true, Pivot); // Update gizmo orientation if (AActor* Actor = SelectedActors[0].Get()) 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 6a767c4..5837a43 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 @@ -33,3 +33,11 @@ enum class EPS_Editor_Space : uint8 World, Local }; + +/** Top-level editor mode (separate from transform mode). */ +UENUM(BlueprintType) +enum class EPS_Editor_EditorMode : uint8 +{ + Normal, // Standard select/transform behavior + SplinePlacement // Clicks add points to current spline +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp index a48a31d..e43f919 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp @@ -1,7 +1,24 @@ #include "PS_Editor_UndoManager.h" +#include "PS_Editor_SplineActor.h" #include "UObject/UnrealType.h" #include "Components/ActorComponent.h" +void FPS_Editor_SplinePointAction::Execute() +{ + if (APS_Editor_SplineActor* Spline = SplineActor.Get()) + { + Spline->SetAllPointLocations(NewPoints); + } +} + +void FPS_Editor_SplinePointAction::Undo() +{ + if (APS_Editor_SplineActor* Spline = SplineActor.Get()) + { + Spline->SetAllPointLocations(OldPoints); + } +} + void FPS_Editor_PropertyAction::ApplyValue(const FString& ValueStr) { AActor* Act = Actor.Get(); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h index 9362db0..1e970e5 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h @@ -9,7 +9,8 @@ enum class EPS_Editor_ActionType : uint8 Transform, Delete, Spawn, - PropertyChange + PropertyChange, + SplinePointChange }; /** @@ -157,6 +158,26 @@ struct FPS_Editor_SpawnAction : public FPS_Editor_Action } }; +class APS_Editor_SplineActor; + +/** + * Records a spline point state change (move, add, remove). + * Stores full state snapshots (all points before/after) for simplicity. + */ +struct FPS_Editor_SplinePointAction : public FPS_Editor_Action +{ + FPS_Editor_SplinePointAction() : FPS_Editor_Action(EPS_Editor_ActionType::SplinePointChange) {} + + TWeakObjectPtr SplineActor; + TArray OldPoints; + TArray NewPoints; + FString Description; + + virtual void Execute() override; + virtual void Undo() override; + virtual FString GetDescription() const override { return Description; } +}; + /** * Records a single property value change on a component or actor. * Stores text-serialized old/new values for undo/redo via ImportText. diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp index 7d45e25..1a71fef 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp @@ -2,6 +2,7 @@ #include "PS_Editor_SpawnManager.h" #include "PS_Editor_SelectionManager.h" #include "PS_Editor_EditableComponent.h" +#include "PS_Editor_SplineActor.h" #include "JsonObjectConverter.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" @@ -36,6 +37,14 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_ ActorData.Rotation = Actor->GetActorRotation(); ActorData.Scale = Actor->GetActorScale3D(); + // Export spline data + if (APS_Editor_SplineActor* Spline = Cast(Actor)) + { + Spline->ExportToCustomProperties(ActorData.CustomProperties); + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exported spline with %d points, class=%s"), + Spline->GetNumPoints(), *ActorData.ClassPath); + } + // Export custom editable properties if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass()) { @@ -137,6 +146,11 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_ { UClass* LoadedClass = LoadObject(nullptr, *ActorData.ClassPath); if (!LoadedClass) + { + // Fallback for C++ native classes (e.g. /Script/PS_Editor.PS_Editor_SplineActor) + LoadedClass = FindObject(nullptr, *ActorData.ClassPath); + } + if (!LoadedClass) { UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load class: %s"), *ActorData.ClassPath); continue; @@ -190,6 +204,12 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_ } } + // Import spline data + if (APS_Editor_SplineActor* Spline = Cast(SpawnedActor)) + { + Spline->ImportFromCustomProperties(ActorData.CustomProperties); + } + SpawnedCount++; } } 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 new file mode 100644 index 0000000..88e2e8a --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.cpp @@ -0,0 +1,493 @@ +#include "PS_Editor_SplineActor.h" +#include "Components/SplineComponent.h" +#include "ProceduralMeshComponent.h" +#include "Components/StaticMeshComponent.h" +#include "Engine/StaticMesh.h" +#include "Materials/MaterialInstanceDynamic.h" + +APS_Editor_SplineActor::APS_Editor_SplineActor() +{ + PrimaryActorTick.bCanEverTick = false; + + // Root + USceneComponent* Root = CreateDefaultSubobject(TEXT("Root")); + SetRootComponent(Root); + Root->SetMobility(EComponentMobility::Movable); + + // Spline data + SplineComp = CreateDefaultSubobject(TEXT("SplineComp")); + SplineComp->SetupAttachment(Root); + SplineComp->ClearSplinePoints(false); + SplineComp->SetClosedLoop(false); + + // Line mesh + SplineMeshComp = CreateDefaultSubobject(TEXT("SplineMesh")); + SplineMeshComp->SetupAttachment(Root); + SplineMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + SplineMeshComp->SetCollisionResponseToAllChannels(ECR_Ignore); + SplineMeshComp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block); + SplineMeshComp->bUseComplexAsSimpleCollision = true; + SplineMeshComp->SetCastShadow(false); + + // Handle parent + HandleRoot = CreateDefaultSubobject(TEXT("HandleRoot")); + HandleRoot->SetupAttachment(Root); +} + +void APS_Editor_SplineActor::BeginPlay() +{ + Super::BeginPlay(); + + // Gizmo material (no depth test, renders on top) for handles and selected state + UMaterialInterface* GizmoMat = LoadObject( + nullptr, TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo")); + + // Auto-generated opaque unlit material (depth tested) for spline line + UMaterialInterface* SplineLineMat = LoadObject( + nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine.M_PS_Editor_SplineLine")); + + // Spline line: normal depth-tested material (orange) + if (SplineLineMat) + { + SplineMat = UMaterialInstanceDynamic::Create(SplineLineMat, this); + SplineMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 0.4f, 0.0f)); + } + + // Spline line selected: no depth test (on top, bright orange) + if (GizmoMat) + { + SplineMatSelected = UMaterialInstanceDynamic::Create(GizmoMat, this); + SplineMatSelected->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 0.6f, 0.0f)); + + // Handles always use gizmo material (on top, clickable) + HandleMat = UMaterialInstanceDynamic::Create(GizmoMat, this); + HandleMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 1.0f, 1.0f)); // White + + HandleHighlightMat = UMaterialInstanceDynamic::Create(GizmoMat, this); + HandleHighlightMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 1.0f, 0.0f)); // Yellow + + CenterCubeMat = UMaterialInstanceDynamic::Create(GizmoMat, this); + CenterCubeMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 0.4f, 0.0f)); // Orange + } + + // Create center cube (selection handle for whole spline) + CenterCube = NewObject(this); + CenterCube->SetupAttachment(GetRootComponent()); + CenterCube->RegisterComponent(); + UStaticMesh* CubeMesh = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); + if (CubeMesh) + { + CenterCube->SetStaticMesh(CubeMesh); + } + CenterCube->SetWorldScale3D(FVector(CenterCubeScale)); + CenterCube->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + CenterCube->SetCollisionResponseToAllChannels(ECR_Ignore); + CenterCube->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block); + CenterCube->SetCastShadow(false); + if (CenterCubeMat) + { + CenterCube->SetMaterial(0, CenterCubeMat); + } +} + +// ---- Point manipulation ---- + +void APS_Editor_SplineActor::AddPoint(const FVector& WorldPosition) +{ + // Convert world to local space + const FVector LocalPos = GetActorTransform().InverseTransformPosition(WorldPosition); + SplineComp->AddSplinePoint(LocalPos, ESplineCoordinateSpace::Local, true); + + // Create handle + UStaticMeshComponent* Handle = CreatePointHandle(WorldPosition); + PointHandles.Add(Handle); + + UpdateVisualization(); +} + +void APS_Editor_SplineActor::MovePoint(int32 Index, const FVector& NewWorldPosition) +{ + if (Index < 0 || Index >= GetNumPoints()) return; + + const FVector LocalPos = GetActorTransform().InverseTransformPosition(NewWorldPosition); + SplineComp->SetLocationAtSplinePoint(Index, LocalPos, ESplineCoordinateSpace::Local, true); + + if (PointHandles.IsValidIndex(Index) && PointHandles[Index]) + { + PointHandles[Index]->SetWorldLocation(NewWorldPosition); + } + + UpdateVisualization(); +} + +void APS_Editor_SplineActor::RemovePoint(int32 Index) +{ + if (Index < 0 || Index >= GetNumPoints()) return; + + SplineComp->RemoveSplinePoint(Index, true); + + if (PointHandles.IsValidIndex(Index)) + { + if (PointHandles[Index]) + { + PointHandles[Index]->DestroyComponent(); + } + PointHandles.RemoveAt(Index); + } + + UpdateVisualization(); +} + +int32 APS_Editor_SplineActor::GetNumPoints() const +{ + return SplineComp ? SplineComp->GetNumberOfSplinePoints() : 0; +} + +FVector APS_Editor_SplineActor::GetPointLocation(int32 Index) const +{ + if (!SplineComp || Index < 0 || Index >= GetNumPoints()) return FVector::ZeroVector; + return SplineComp->GetLocationAtSplinePoint(Index, ESplineCoordinateSpace::World); +} + +TArray APS_Editor_SplineActor::GetAllPointLocations() const +{ + TArray Result; + const int32 Num = GetNumPoints(); + Result.Reserve(Num); + for (int32 i = 0; i < Num; i++) + { + Result.Add(GetPointLocation(i)); + } + return Result; +} + +void APS_Editor_SplineActor::SetAllPointLocations(const TArray& Points) +{ + if (!SplineComp) return; + + SplineComp->ClearSplinePoints(false); + for (const FVector& WorldPos : Points) + { + const FVector LocalPos = GetActorTransform().InverseTransformPosition(WorldPos); + SplineComp->AddSplinePoint(LocalPos, ESplineCoordinateSpace::Local, false); + } + SplineComp->UpdateSpline(); + + RebuildHandles(); + UpdateVisualization(); +} + +// ---- Visualization ---- + +void APS_Editor_SplineActor::UpdateVisualization() +{ + if (!SplineMeshComp || !SplineComp || GetNumPoints() < 2) + { + if (SplineMeshComp) SplineMeshComp->ClearAllMeshSections(); + return; + } + + const float SplineLength = SplineComp->GetSplineLength(); + const int32 NumSamples = FMath::Max(2, FMath::CeilToInt(SplineLength / SampleInterval)); + + 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); + + 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 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)); + + // 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)); + } + + // Generate triangles for both ribbons (double-sided) + Triangles.Reserve((NumSamples - 1) * 24); + 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; + + // 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); + } + + SplineMeshComp->ClearAllMeshSections(); + SplineMeshComp->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, TArray(), TArray(), false); + + // Apply the current material (normal or selected) + // Check if handles are visible as a proxy for "selected" state + const bool bCurrentlySelected = (PointHandles.Num() > 0 && PointHandles[0] && PointHandles[0]->IsVisible()); + UMaterialInstanceDynamic* LineMat = (bCurrentlySelected && SplineMatSelected) ? SplineMatSelected : SplineMat; + if (LineMat) + { + SplineMeshComp->SetMaterial(0, LineMat); + } + + UpdateCenterCube(); +} + +void APS_Editor_SplineActor::UpdateCenterCube() +{ + if (!CenterCube) return; + + const int32 Num = GetNumPoints(); + if (Num == 0) + { + CenterCube->SetVisibility(false); + return; + } + + // Position at centroid of all points + FVector Centroid = FVector::ZeroVector; + for (int32 i = 0; i < Num; i++) + { + Centroid += GetPointLocation(i); + } + Centroid /= Num; + CenterCube->SetWorldLocation(Centroid); + CenterCube->SetVisibility(true); +} + +// ---- Handle interaction ---- + +UStaticMeshComponent* APS_Editor_SplineActor::CreatePointHandle(const FVector& WorldPosition) +{ + UStaticMeshComponent* Handle = NewObject(this); + Handle->SetupAttachment(HandleRoot); + Handle->RegisterComponent(); + + // Load sphere mesh + UStaticMesh* SphereMesh = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere")); + if (SphereMesh) + { + Handle->SetStaticMesh(SphereMesh); + } + + Handle->SetWorldLocation(WorldPosition); + Handle->SetWorldScale3D(FVector(HandleScale)); + + // Collision for raycast + Handle->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + Handle->SetCollisionResponseToAllChannels(ECR_Ignore); + Handle->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block); + + Handle->SetCastShadow(false); + Handle->SetRenderInDepthPass(false); + + if (HandleMat) + { + Handle->SetMaterial(0, HandleMat); + } + + return Handle; +} + +void APS_Editor_SplineActor::RebuildHandles() +{ + // Destroy existing handles + for (UStaticMeshComponent* Handle : PointHandles) + { + if (Handle) + { + Handle->DestroyComponent(); + } + } + PointHandles.Empty(); + + // Create new handles at each point + const int32 Num = GetNumPoints(); + for (int32 i = 0; i < Num; i++) + { + UStaticMeshComponent* Handle = CreatePointHandle(GetPointLocation(i)); + PointHandles.Add(Handle); + } +} + +int32 APS_Editor_SplineActor::GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius) const +{ + int32 BestIndex = -1; + float BestDist = MAX_FLT; + + for (int32 i = 0; i < PointHandles.Num(); i++) + { + if (!PointHandles[i] || !PointHandles[i]->IsVisible()) continue; + + const FVector HandlePos = PointHandles[i]->GetComponentLocation(); + + // Sphere-ray intersection + const FVector ToHandle = HandlePos - RayOrigin; + const float Proj = FVector::DotProduct(ToHandle, RayDirection); + if (Proj < 0.0f) continue; // Behind camera + + const FVector ClosestPoint = RayOrigin + RayDirection * Proj; + const float Dist = FVector::Dist(ClosestPoint, HandlePos); + + if (Dist < SphereRadius && Proj < BestDist) + { + BestDist = Proj; + BestIndex = i; + } + } + + return BestIndex; +} + +void APS_Editor_SplineActor::SetHandlesVisible(bool bVisible) +{ + for (UStaticMeshComponent* Handle : PointHandles) + { + if (Handle) + { + Handle->SetVisibility(bVisible); + Handle->SetCollisionEnabled(bVisible ? ECollisionEnabled::QueryOnly : ECollisionEnabled::NoCollision); + } + } +} + +void APS_Editor_SplineActor::HighlightHandle(int32 Index) +{ + for (int32 i = 0; i < PointHandles.Num(); i++) + { + if (PointHandles[i]) + { + PointHandles[i]->SetMaterial(0, (i == Index) ? HandleHighlightMat : HandleMat); + } + } +} + +void APS_Editor_SplineActor::ClearHandleHighlight() +{ + for (UStaticMeshComponent* Handle : PointHandles) + { + if (Handle && HandleMat) + { + Handle->SetMaterial(0, HandleMat); + } + } +} + +void APS_Editor_SplineActor::SetSelected(bool bSelected) +{ + // Swap line material: depth-tested (normal) vs no-depth-test (on top when selected) + if (SplineMeshComp) + { + UMaterialInstanceDynamic* Mat = bSelected ? SplineMatSelected : SplineMat; + if (Mat) + { + SplineMeshComp->SetMaterial(0, Mat); + } + } + + // Center cube: bright when selected + 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); + } +} + +bool APS_Editor_SplineActor::IsCenterCube(const UPrimitiveComponent* Comp) const +{ + return Comp && Comp == CenterCube; +} + +FVector APS_Editor_SplineActor::GetCentroid() const +{ + const int32 Num = GetNumPoints(); + if (Num == 0) return GetActorLocation(); + + FVector Centroid = FVector::ZeroVector; + for (int32 i = 0; i < Num; i++) + { + Centroid += GetPointLocation(i); + } + return Centroid / Num; +} + +// ---- Serialization ---- + +void APS_Editor_SplineActor::ExportToCustomProperties(TMap& OutProperties) const +{ + const int32 Num = GetNumPoints(); + OutProperties.Add(TEXT("SplinePointCount"), FString::FromInt(Num)); + + for (int32 i = 0; i < Num; i++) + { + const FVector Loc = GetPointLocation(i); + OutProperties.Add( + FString::Printf(TEXT("SplinePoint_%d"), i), + FString::Printf(TEXT("X=%.4f Y=%.4f Z=%.4f"), Loc.X, Loc.Y, Loc.Z)); + } +} + +void APS_Editor_SplineActor::ImportFromCustomProperties(const TMap& Properties) +{ + const FString* CountStr = Properties.Find(TEXT("SplinePointCount")); + if (!CountStr) return; + + const int32 Num = FCString::Atoi(**CountStr); + if (Num < 2) return; + + TArray Points; + for (int32 i = 0; i < Num; i++) + { + const FString Key = FString::Printf(TEXT("SplinePoint_%d"), i); + const FString* ValStr = Properties.Find(Key); + if (!ValStr) continue; + + FVector Loc; + if (Loc.InitFromString(*ValStr)) + { + Points.Add(Loc); + } + } + + if (Points.Num() >= 2) + { + SetAllPointLocations(Points); + } +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.h new file mode 100644 index 0000000..119e51c --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.h @@ -0,0 +1,130 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "PS_Editor_SplineActor.generated.h" + +class USplineComponent; +class UProceduralMeshComponent; + +/** + * Runtime spline actor for path/route editing in PS_Editor. + * Renders as a visible quad strip with clickable point handles. + */ +UCLASS() +class PS_EDITOR_API APS_Editor_SplineActor : public AActor +{ + GENERATED_BODY() + +public: + APS_Editor_SplineActor(); + + // ---- Point manipulation ---- + + /** Add a point at the end of the spline (world space). */ + void AddPoint(const FVector& WorldPosition); + + /** Move an existing point to a new world position. */ + void MovePoint(int32 Index, const FVector& NewWorldPosition); + + /** Remove a point by index. */ + void RemovePoint(int32 Index); + + /** Get the number of spline points. */ + int32 GetNumPoints() const; + + /** Get the world position of a spline point. */ + FVector GetPointLocation(int32 Index) const; + + /** Get all point world positions (for undo snapshots). */ + TArray GetAllPointLocations() const; + + /** Set all point positions at once (for undo restore). Rebuilds handles and visualization. */ + void SetAllPointLocations(const TArray& Points); + + // ---- Visualization ---- + + /** Regenerate the line mesh and reposition all handles. */ + void UpdateVisualization(); + + // ---- Handle interaction ---- + + /** Find which point handle is under the cursor ray. Returns -1 if none. */ + int32 GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius = 15.0f) const; + + /** Show or hide all point handles and center cube. */ + void SetHandlesVisible(bool bVisible); + + /** Set the spline's selected state (changes line + cube color). */ + void SetSelected(bool bSelected); + + /** Returns true if the given component is the center cube. */ + bool IsCenterCube(const UPrimitiveComponent* Comp) const; + + /** Highlight a specific handle (yellow), others go back to normal (white). */ + void HighlightHandle(int32 Index); + + /** Clear all handle highlights. */ + void ClearHandleHighlight(); + + // ---- Serialization ---- + + /** Get the centroid of all spline points (world space). Used as pivot for gizmo. */ + FVector GetCentroid() const; + + /** Export spline data to a key-value map (for JSON serialization). */ + void ExportToCustomProperties(TMap& OutProperties) const; + + /** Import spline data from a key-value map (for scene loading). */ + void ImportFromCustomProperties(const TMap& Properties); + +protected: + virtual void BeginPlay() override; + +private: + UPROPERTY(VisibleAnywhere) + TObjectPtr SplineComp; + + UPROPERTY(VisibleAnywhere) + TObjectPtr SplineMeshComp; + + UPROPERTY(VisibleAnywhere) + TObjectPtr HandleRoot; + + UPROPERTY() + TArray> PointHandles; + + // Materials + UPROPERTY() + TObjectPtr SplineMat; // Normal (depth tested) + + UPROPERTY() + TObjectPtr SplineMatSelected; // Selected (no depth test, on top) + + UPROPERTY() + TObjectPtr HandleMat; + + UPROPERTY() + TObjectPtr HandleHighlightMat; + + UPROPERTY() + TObjectPtr CenterCube; + + /** Create a handle sphere for a point at the given world position. */ + UStaticMeshComponent* CreatePointHandle(const FVector& WorldPosition); + + /** Rebuild all handles from current spline points. */ + void RebuildHandles(); + + /** Update center cube position to the centroid of all points. */ + void UpdateCenterCube(); + + UPROPERTY() + TObjectPtr CenterCubeMat; + + /** Line rendering settings. */ + float LineHalfWidth = 2.0f; + 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 d4b921f..8884057 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 @@ -6,6 +6,7 @@ #include "PS_Editor_PlayerController.h" #include "PS_Editor_EditableComponent.h" #include "PS_Editor_SpawnableComponent.h" +#include "PS_Editor_SplineActor.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SBorder.h" @@ -239,6 +240,31 @@ TSharedRef UPS_Editor_MainWidget::BuildToolbar() }) ] ] + // New Spline button + + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + { + EditorPC->FinishSplinePlacement(); + } + else + { + EditorPC->BeginSplinePlacement(); + } + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("New Spline (S)"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + ] ]; } @@ -685,14 +711,28 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection() // Update mode text if (ModeText.IsValid()) { - FString ModeStr; - switch (SM->GetTransformMode()) + // Check for spline placement mode first + bool bSplineMode = false; + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) { - case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (Z)"); break; - case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (E)"); break; - case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (R)"); break; + 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; + } + } + + if (!bSplineMode) + { + FString ModeStr; + switch (SM->GetTransformMode()) + { + case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (Z)"); break; + case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (E)"); break; + case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (R)"); break; + } + ModeText->SetText(FText::FromString(ModeStr)); } - ModeText->SetText(FText::FromString(ModeStr)); } // Update space button text @@ -877,16 +917,17 @@ void UPS_Editor_MainWidget::RefreshOutliner() AActor* Actor = SpawnedActors[i].Get(); if (!Actor || Actor->IsHidden()) continue; - // Build display name: SpawnableComponent DisplayName > Class name (cleaned) + // Build display name FString DisplayName; - if (UActorComponent* SpawnComp = Actor->FindComponentByClass(UPS_Editor_SpawnableComponent::StaticClass())) + if (Cast(Actor)) { - if (UPS_Editor_SpawnableComponent* SC = Cast(SpawnComp)) + DisplayName = TEXT("Spline"); + } + else if (UPS_Editor_SpawnableComponent* SC = Actor->FindComponentByClass()) + { + if (!SC->DisplayName.IsEmpty()) { - if (!SC->DisplayName.IsEmpty()) - { - DisplayName = SC->DisplayName; - } + DisplayName = SC->DisplayName; } } if (DisplayName.IsEmpty())