From 1a2cc87160e9a0fa6686aea64d3de48b8ae43dac Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Wed, 15 Apr 2026 18:02:17 +0200 Subject: [PATCH] Add ChildMovable interface, preload filter, IsInEditorMode, lightmap init - Add IPS_Editor_ChildMovable interface for per-child editing of sub-elements (VR placement spots, etc.) with full gizmo, undo, and serialization support - Add "Edit Children" button in toolbar when a ChildMovable actor is selected - Add bPreload tag on SpawnableComponent for briefing/placement phase actors - Add EPS_Editor_SceneLoadFilter enum (All, PreloadOnly, ExcludePreload) to LoadScene for selective loading of preload vs gameplay actors - Add bPreload field to FPS_Editor_ActorData for JSON persistence - Add static IsInEditorMode() getter on SceneLoader (BlueprintPure) - Add Level::InitializeRenderingResources() call on sublevel load for lightmaps - Update Pawn, PlayerController, UndoManager, SceneSerializer, SceneLoader, MainWidget to support ChildMovable alongside existing SplineEditable Co-Authored-By: Claude Opus 4.6 (1M context) --- .../PS_Editor/GameMode/PS_Editor_Pawn.cpp | 75 ++++++++++++++++--- .../GameMode/PS_Editor_PlayerController.cpp | 27 ++++++- .../Selection/PS_Editor_UndoManager.cpp | 9 +++ .../Serialization/PS_Editor_SceneData.h | 4 + .../Serialization/PS_Editor_SceneLoader.cpp | 22 ++++-- .../Serialization/PS_Editor_SceneLoader.h | 26 ++++++- .../PS_Editor_SceneSerializer.cpp | 29 +++++++ .../PS_Editor/Spawn/PS_Editor_ChildMovable.h | 71 ++++++++++++++++++ .../Spawn/PS_Editor_SpawnableComponent.h | 5 ++ .../UI/Widgets/PS_Editor_MainWidget.cpp | 57 +++++++++++++- .../UI/Widgets/PS_Editor_MainWidget.h | 1 + 11 files changed, 304 insertions(+), 22 deletions(-) create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_ChildMovable.h 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 c63e5a5..7329f71 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 @@ -15,6 +15,7 @@ #include "PS_Editor_UndoManager.h" #include "PS_Editor_SplineActor.h" #include "PS_Editor_SplineEditable.h" +#include "PS_Editor_ChildMovable.h" #include "Components/SplineComponent.h" #include "DrawDebugHelpers.h" #include "PS_Editor_ConfirmDialog.h" @@ -380,16 +381,21 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value) } const FVector ConstrainedDelta = DisplacementAlongAxis * AxisDir; - // Spline point mode: move the single point instead of actors + // Spline point / ChildMovable mode: move the single sub-element instead of actors if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) { - if (IPS_Editor_SplineEditable* SplineActor = Cast(DraggedSplineActor.Get())) + if (SplinePointDragInitialState.IsValidIndex(ActiveSplinePointIndex)) { - if (SplinePointDragInitialState.IsValidIndex(ActiveSplinePointIndex)) + const FVector NewPos = SplinePointDragInitialState[ActiveSplinePointIndex] + ConstrainedDelta; + + if (IPS_Editor_SplineEditable* SplineActor = Cast(DraggedSplineActor.Get())) { - const FVector NewPos = SplinePointDragInitialState[ActiveSplinePointIndex] + ConstrainedDelta; SplineActor->MovePoint(ActiveSplinePointIndex, NewPos); } + else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + IPS_Editor_ChildMovable::Execute_MoveChild(DraggedSplineActor.Get(), ActiveSplinePointIndex, NewPos); + } } } else @@ -475,11 +481,15 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value) { if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) { - // Following a single spline point + // Following a single spline point or child movable element if (IPS_Editor_SplineEditable* SplineActor = Cast(DraggedSplineActor.Get())) { Gizmo->SetActorLocation(SplineActor->GetPointLocation(ActiveSplinePointIndex)); } + else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + Gizmo->SetActorLocation(IPS_Editor_ChildMovable::Execute_GetChildLocation(DraggedSplineActor.Get(), ActiveSplinePointIndex)); + } } else if (Selected.Num() == 1) { @@ -648,6 +658,35 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value) } } + // Check for ChildMovable child handle hit + if (APS_Editor_PlayerController* EdPC_CM = Cast(GetController())) + { + if (EdPC_CM->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + { + AActor* ActiveActor = EdPC_CM->GetActivePlacementActor(); + if (ActiveActor && ActiveActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + const int32 ChildIdx = IPS_Editor_ChildMovable::Execute_GetChildIndexUnderCursor(ActiveActor, RayOrigin, RayDir); + if (ChildIdx >= 0) + { + ActiveSplinePointIndex = ChildIdx; + DraggedSplineActor = ActiveActor; + IPS_Editor_ChildMovable::Execute_HighlightChild(ActiveActor, ChildIdx); + + if (UPS_Editor_SelectionManager* SM = EdPC_CM->GetSelectionManager()) + { + if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo()) + { + Gizmo->SetGizmoActive(true, IPS_Editor_ChildMovable::Execute_GetChildLocation(ActiveActor, ChildIdx)); + } + SM->SetTransformMode(EPS_Editor_TransformMode::Translate); + } + return; + } + } + } + } + // In SplinePlacement mode: click on spline tube mesh → insert a CP if (APS_Editor_PlayerController* EdPC2 = Cast(GetController())) { @@ -804,13 +843,27 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value) // Spline point drag: record SplinePointAction if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) { + TArray NewPoints; + FString Desc; + if (IPS_Editor_SplineEditable* SplineActor = Cast(DraggedSplineActor.Get())) + { + NewPoints = SplineActor->GetAllPointLocations(); + Desc = TEXT("Move spline point"); + } + else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + NewPoints = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get()); + Desc = TEXT("Move child element"); + } + + if (NewPoints.Num() > 0) { TSharedPtr Action = MakeShared(); - Action->SplineActor = Cast(SplineActor); + Action->SplineActor = DraggedSplineActor.Get(); Action->OldPoints = SplinePointDragInitialState; - Action->NewPoints = SplineActor->GetAllPointLocations(); - Action->Description = TEXT("Move spline point"); + Action->NewPoints = NewPoints; + Action->Description = Desc; UM->RecordAction(Action); } } @@ -1439,13 +1492,17 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis) } } - // Store initial spline state if editing a point + // Store initial sub-element state if editing a point/child if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) { if (IPS_Editor_SplineEditable* Spline = Cast(DraggedSplineActor.Get())) { SplinePointDragInitialState = Spline->GetAllPointLocations(); } + else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + SplinePointDragInitialState = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get()); + } } SetCameraMode(EPS_Editor_CameraMode::GizmoDrag); 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 8c48761..4823cc3 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 @@ -12,6 +12,8 @@ #include "Engine/LevelStreamingDynamic.h" #include "Engine/PostProcessVolume.h" #include "PS_Editor_CameraStart.h" +#include "PS_Editor_ChildMovable.h" +#include "PS_Editor_SceneLoader.h" #include "PS_Editor_HUD.h" #include "PS_Editor_MainWidget.h" #include "Camera/PlayerCameraManager.h" @@ -32,6 +34,7 @@ void APS_Editor_PlayerController::BeginPlay() { Super::BeginPlay(); + UPS_Editor_SceneLoader::bIsEditorModeActive = true; SetInputMode(FInputModeGameAndUI().SetHideCursorDuringCapture(false)); // AI logic will be stopped per-pawn when spawned (see SpawnManager) @@ -190,6 +193,7 @@ void APS_Editor_PlayerController::CloseEditor() } // Notify actors: leaving editor mode + UPS_Editor_SceneLoader::bIsEditorModeActive = false; if (SpawnManager) { SpawnManager->NotifyEditorModeChanged(false); @@ -303,20 +307,29 @@ void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf InActo void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActor) { - if (!ExistingActor || !ExistingActor->Implements()) return; + if (!ExistingActor) return; + + const bool bIsPointPlaceable = ExistingActor->Implements(); + const bool bIsChildMovable = ExistingActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()); + if (!bIsPointPlaceable && !bIsChildMovable) return; ActivePlacementActor = ExistingActor; bAppendingToActor = true; EditorMode = EPS_Editor_EditorMode::SplinePlacement; - // Store initial state for undo (spline-specific) + // Store initial state and show handles if (IPS_Editor_SplineEditable* Spline = Cast(ExistingActor)) { AppendInitialPoints = Spline->GetAllPointLocations(); Spline->SetHandlesVisible(true); } + else if (bIsChildMovable) + { + AppendInitialPoints = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(ExistingActor); + IPS_Editor_ChildMovable::Execute_SetChildHandlesVisible(ExistingActor, true); + } - UE_LOG(LogTemp, Log, TEXT("PS_Editor: Appending points to %s"), *ExistingActor->GetName()); + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Editing sub-elements of %s"), *ExistingActor->GetName()); } void APS_Editor_PlayerController::FinishPointPlacement() @@ -332,6 +345,11 @@ void APS_Editor_PlayerController::FinishPointPlacement() Spline->SetHandlesVisible(false); Spline->ClearHandleHighlight(); } + else if (PlacementActor && PlacementActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + IPS_Editor_ChildMovable::Execute_SetChildHandlesVisible(PlacementActor, false); + IPS_Editor_ChildMovable::Execute_ClearChildHighlight(PlacementActor); + } if (PlacementActor && SelectionManager) { @@ -452,6 +470,9 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials() ULevel* Level = CurrentSublevel->GetLoadedLevel(); if (!Level) return; + // Force lighting/rendering resources to initialize properly for baked lightmaps + Level->InitializeRenderingResources(); + APS_Editor_CameraStart* CameraStart = nullptr; for (AActor* Actor : Level->Actors) 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 4d5220d..cc41674 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,5 +1,6 @@ #include "PS_Editor_UndoManager.h" #include "PS_Editor_SplineEditable.h" +#include "PS_Editor_ChildMovable.h" #include "UObject/UnrealType.h" #include "Components/ActorComponent.h" @@ -9,6 +10,10 @@ void FPS_Editor_SplinePointAction::Execute() { Spline->SetAllPointLocations(NewPoints); } + else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), NewPoints); + } } void FPS_Editor_SplinePointAction::Undo() @@ -17,6 +22,10 @@ void FPS_Editor_SplinePointAction::Undo() { Spline->SetAllPointLocations(OldPoints); } + else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), OldPoints); + } } void FPS_Editor_PropertyAction::ApplyValue(const FString& ValueStr) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h index c31dbf1..491d34f 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h @@ -25,6 +25,10 @@ struct FPS_Editor_ActorData /** Custom property values: key = "ComponentName.PropertyName", value = text-serialized value. */ UPROPERTY() TMap CustomProperties; + + /** True if this actor is tagged as preload (briefing/placement phase). */ + UPROPERTY() + bool bPreload = false; }; /** Serialized data for a complete scene. */ diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp index 53f1cad..d4d0d96 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp @@ -2,6 +2,7 @@ #include "PS_Editor_SceneData.h" #include "PS_Editor_EditableComponent.h" #include "PS_Editor_EditableInterface.h" +#include "PS_Editor_ChildMovable.h" #include "PS_Editor_SpawnableComponent.h" #include "PS_Editor_SplineActor.h" #include "PS_Editor_SplineEditable.h" @@ -12,7 +13,7 @@ #include "UObject/UnrealType.h" #include "Components/ActorComponent.h" -bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray& OutActors, bool bStripEditorComponents) +bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray& OutActors, bool bStripEditorComponents, EPS_Editor_SceneLoadFilter Filter) { OutActors.Empty(); @@ -44,7 +45,7 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const return false; } - if (!LoadSceneFromData(World, SceneData, OutActors)) + if (!LoadSceneFromData(World, SceneData, OutActors, Filter)) { return false; } @@ -87,7 +88,7 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const return true; } -bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray& OutActors) +bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray& OutActors, EPS_Editor_SceneLoadFilter Filter) { if (!World) { @@ -99,7 +100,7 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S bIsCurrentlyLoading = true; for (const FPS_Editor_ActorData& ActorData : SceneData.Actors) { - AActor* SpawnedActor = SpawnActorFromData(World, ActorData); + AActor* SpawnedActor = SpawnActorFromData(World, ActorData, Filter); if (SpawnedActor) { OutActors.Add(SpawnedActor); @@ -113,8 +114,12 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S return SpawnedCount > 0; } -AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Editor_ActorData& ActorData) +AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Editor_ActorData& ActorData, EPS_Editor_SceneLoadFilter Filter) { + // Apply preload filter + if (Filter == EPS_Editor_SceneLoadFilter::PreloadOnly && !ActorData.bPreload) return nullptr; + if (Filter == EPS_Editor_SceneLoadFilter::ExcludePreload && ActorData.bPreload) return nullptr; + // Load class (supports both Blueprint and C++ classes) UClass* LoadedClass = LoadObject(nullptr, *ActorData.ClassPath); if (!LoadedClass) @@ -156,6 +161,12 @@ AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Edit Spline->ImportFromCustomProperties(ActorData.CustomProperties); } + // Import child movable data + if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + IPS_Editor_ChildMovable::Execute_ImportChildLocations(SpawnedActor, ActorData.CustomProperties); + } + // Notify the actor that all properties are ready if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) { @@ -211,6 +222,7 @@ void UPS_Editor_SceneLoader::ApplyCustomProperties(AActor* Actor, const TMap& OutActors, bool bStripEditorComponents = true); + static bool LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray& OutActors, + bool bStripEditorComponents = true, EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All); /** * Load scene from an already-parsed SceneData struct. * Useful if you want to parse the JSON yourself or modify data before spawning. */ - static bool LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray& OutActors); + static bool LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray& OutActors, + EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All); /** * Get the directory where scenes are stored. @@ -80,6 +91,13 @@ public: /** Static flag: true while SceneLoader or SceneSerializer is spawning actors. */ static bool bIsCurrentlyLoading; + /** Static flag: true while the PS_Editor runtime editor is active. */ + static bool bIsEditorModeActive; + + /** Returns true if the PS_Editor runtime editor is currently active. Callable from any BP. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS Editor") + static bool IsInEditorMode() { return bIsEditorModeActive; } + /** * Get all saved scenes for the current level. */ @@ -87,8 +105,8 @@ public: static TArray GetSavedSceneNamesForCurrentLevel(const UObject* WorldContextObject); private: - /** Spawn a single actor from saved data and apply all properties. */ - static AActor* SpawnActorFromData(UWorld* World, const FPS_Editor_ActorData& ActorData); + /** Spawn a single actor from saved data and apply all properties. Returns nullptr if filtered out. */ + static AActor* SpawnActorFromData(UWorld* World, const FPS_Editor_ActorData& ActorData, EPS_Editor_SceneLoadFilter Filter); /** Apply custom editable properties to a spawned actor. */ static void ApplyCustomProperties(AActor* Actor, const TMap& CustomProperties); 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 ae90887..2ea775c 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 @@ -3,6 +3,8 @@ #include "PS_Editor_SelectionManager.h" #include "PS_Editor_EditableComponent.h" #include "PS_Editor_EditableInterface.h" +#include "PS_Editor_SpawnableComponent.h" +#include "PS_Editor_ChildMovable.h" #include "PS_Editor_PlayerController.h" #include "PS_Editor_SceneLoader.h" #include "PS_Editor_SplineActor.h" @@ -49,6 +51,12 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_ ActorData.Rotation = Actor->GetActorRotation(); ActorData.Scale = Actor->GetActorScale3D(); + // Save preload tag + if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass()) + { + ActorData.bPreload = SpawnComp->bPreload; + } + // Export spline data if (IPS_Editor_SplineEditable* Spline = Cast(Actor)) { @@ -57,6 +65,12 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_ Spline->GetNumPoints(), *ActorData.ClassPath); } + // Export child movable data + if (Actor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + IPS_Editor_ChildMovable::Execute_ExportChildLocations(Actor, ActorData.CustomProperties); + } + // Export custom editable properties if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass()) { @@ -171,6 +185,15 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_ AActor* SpawnedActor = SpawnManager->SpawnActorDirect(LoadedClass, ActorData.Location, ActorData.Rotation, ActorData.Scale); if (SpawnedActor) { + // Restore preload tag + if (ActorData.bPreload) + { + if (UPS_Editor_SpawnableComponent* SpawnComp = SpawnedActor->FindComponentByClass()) + { + SpawnComp->bPreload = true; + } + } + // Import custom properties if (ActorData.CustomProperties.Num() > 0) { @@ -222,6 +245,12 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_ Spline->ImportFromCustomProperties(ActorData.CustomProperties); } + // Import child movable data + if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + IPS_Editor_ChildMovable::Execute_ImportChildLocations(SpawnedActor, ActorData.CustomProperties); + } + // Notify the actor that properties were loaded and we are in editor mode if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) { diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_ChildMovable.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_ChildMovable.h new file mode 100644 index 0000000..5452f57 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_ChildMovable.h @@ -0,0 +1,71 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "PS_Editor_ChildMovable.generated.h" + +UINTERFACE(BlueprintType, meta = (DisplayName = "PS Editor Child Movable")) +class PS_EDITOR_API UPS_Editor_ChildMovable : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for actors containing sub-elements (child actors, components) + * that can be individually selected and moved in the PS_Editor runtime editor. + * + * Implement this in your Blueprint to enable per-child editing. + * The plugin handles selection, gizmo, undo, and serialization. + * + * Example: VR placement root with 10 movable placement spots. + */ +class PS_EDITOR_API IPS_Editor_ChildMovable +{ + GENERATED_BODY() + +public: + /** Number of movable sub-elements. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable") + int32 GetMovableChildCount() const; + + /** World location of the sub-element at the given index. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable") + FVector GetChildLocation(int32 Index) const; + + /** Move the sub-element at the given index to a new world position. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable") + void MoveChild(int32 Index, const FVector& NewWorldPosition); + + /** Return all sub-element positions (used for undo snapshots). */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable") + TArray GetAllChildLocations() const; + + /** Restore all sub-element positions (used for undo). */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable") + void SetAllChildLocations(const TArray& Positions); + + /** Index of the sub-element under the cursor ray (-1 if none). + * Implement with sphere-ray intersection or any hit detection that suits your visuals. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable") + int32 GetChildIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const; + + /** Show or hide visual handles for the sub-elements. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable") + void SetChildHandlesVisible(bool bVisible); + + /** Highlight a specific sub-element (e.g. change handle color). */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable") + void HighlightChild(int32 Index); + + /** Clear all sub-element highlights. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable") + void ClearChildHighlight(); + + /** Export sub-element positions to the CustomProperties map for scene saving. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable") + void ExportChildLocations(TMap& OutProperties) const; + + /** Import sub-element positions from the CustomProperties map on scene load. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable") + void ImportChildLocations(const TMap& Properties); +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponent.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponent.h index 547398f..5e6f23c 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponent.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponent.h @@ -47,4 +47,9 @@ public: /** If true, scaling is always uniform on all axes. Dragging any single axis scales all three equally. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable") bool bUniformScaleOnly = false; + + /** If true, this actor belongs to the preload/briefing phase. + * Use scene loading filters to load only preload actors or exclude them. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable") + bool bPreload = false; }; 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 cd2c211..43d9fd3 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 @@ -10,6 +10,7 @@ #include "PS_Editor_SpawnableComponent.h" #include "PS_Editor_SplineActor.h" #include "PS_Editor_SplineEditable.h" +#include "PS_Editor_ChildMovable.h" #include "PS_Editor_Pawn.h" #include "PS_Editor_PointPlaceable.h" #include "Widgets/SBoxPanel.h" @@ -222,6 +223,39 @@ TSharedRef UPS_Editor_MainWidget::RebuildWidget() .ColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f)) ] ] + + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) + [ + SAssignNew(ChildEditButton, 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 (UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get()) + { + for (const TWeakObjectPtr& Weak : SelMgr->GetSelectedActors()) + { + if (Weak.IsValid() && Weak->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + EditorPC->BeginPointPlacementAppend(Weak.Get()); + break; + } + } + } + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Edit Children"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .ColorAndOpacity(FLinearColor(0.3f, 0.8f, 1.0f)) + ] + ] + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) [ SAssignNew(SplineDeletePointButton, SButton) @@ -831,6 +865,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() } // Notify all spawned actors: leaving editor mode + UPS_Editor_SceneLoader::bIsEditorModeActive = false; if (UPS_Editor_SpawnManager* PlaySM2 = EditorPC->GetSpawnManager()) { PlaySM2->NotifyEditorModeChanged(false); @@ -1425,8 +1460,9 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection() } } - // Determine if a spline is selected (for Edit button) + // Determine if a spline or child-movable is selected (for Edit buttons) bool bSplineSelected = false; + bool bChildMovableSelected = false; if (CurrentEditorMode == EPS_Editor_EditorMode::Normal && SM->IsAnythingSelected()) { for (const TWeakObjectPtr& Weak : SM->GetSelectedActors()) @@ -1436,6 +1472,11 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection() bSplineSelected = true; break; } + if (Weak.IsValid() && Weak->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + bChildMovableSelected = true; + break; + } } } @@ -1461,9 +1502,22 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection() HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f)); } if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed); + if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed); if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(bPointSelected ? EVisibility::Visible : EVisibility::Collapsed); if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible); } + else if (bChildMovableSelected) + { + if (HelpBarText.IsValid()) + { + HelpBarText->SetText(FText::FromString(TEXT("Child-movable selected — click Edit Children to move sub-elements"))); + HelpBarText->SetColorAndOpacity(FLinearColor(0.3f, 0.8f, 1.0f)); + } + if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed); + if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Visible); + if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed); + if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Collapsed); + } else if (bSplineSelected) { if (HelpBarText.IsValid()) @@ -1483,6 +1537,7 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection() HelpBarText->SetColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f)); } if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed); + if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed); if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed); if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Collapsed); } 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 356ebc5..384e695 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 @@ -113,6 +113,7 @@ private: TSharedPtr SplineFinishButton; TSharedPtr SplineEditButton; TSharedPtr SplineDeletePointButton; + TSharedPtr ChildEditButton; // Simulate button TSharedPtr SimulateButtonText;