From 2b5015a9af4e7f9041257f3a36be60b0a5cfebc7 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Mon, 13 Apr 2026 18:20:03 +0200 Subject: [PATCH] Editor integration: editable interface, enum support, CameraStart, spawn options, crash fix - Add IPS_Editor_EditableInterface with events: OnEditorPropertyChanged, OnEditorNeedsReinit, OnEditorNeedsRespawn, OnEditorModeChanged, OnEditorTick - Add FPS_Editor_EditablePropertyEntry struct replacing separate property/display/respawn lists with unified Path + DisplayName + bNeedsReinit + bNeedsRespawn per entry - Add enum property support in UI (SComboBox with display names) - Add FVector2D property support in UI (X/Y fields) - Add PS_Editor_CameraStart actor for initial camera position in base levels - Add SpawnableComponent options: bAutoSnapToGround, bYawOnly, bUniformScaleOnly, GroundOffset - Add bIsPlayMode to GameInstanceSubsystem for gameplay vs editor context - Add PlayerController::Tick for editor tick dispatch to spawned actors - Add SpawnManager::RespawnActorWithProperties, NotifyEditorModeChanged, TickEditorMode - Fix recursive crash in ClearSelection/FinishPointPlacement loop - Fix SScrollBox mouse wheel passthrough to camera - Fix gizmo yaw-only: grey disabled arcs, block drag, preserve highlight - Call OnEditorNeedsReinit after scene load (editor + gameplay) for property re-init Co-Authored-By: Claude Opus 4.6 (1M context) --- .../GameMode/PS_Editor_CameraStart.cpp | 36 ++++ .../GameMode/PS_Editor_CameraStart.h | 30 +++ .../PS_Editor_GameInstanceSubsystem.cpp | 1 + .../PS_Editor_GameInstanceSubsystem.h | 4 + .../PS_Editor/GameMode/PS_Editor_Pawn.cpp | 11 +- .../GameMode/PS_Editor_PlayerController.cpp | 57 ++++- .../GameMode/PS_Editor_PlayerController.h | 1 + .../PS_Editor/Gizmo/PS_Editor_Gizmo.cpp | 45 +++- .../Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h | 10 + .../Selection/PS_Editor_SelectionManager.cpp | 43 +++- .../Serialization/PS_Editor_SceneLoader.cpp | 13 +- .../PS_Editor_SceneSerializer.cpp | 20 +- .../Spawn/PS_Editor_EditableComponent.cpp | 46 +++- .../Spawn/PS_Editor_EditableComponent.h | 50 ++++- .../Spawn/PS_Editor_EditableInterface.h | 62 ++++++ .../Spawn/PS_Editor_SpawnManager.cpp | 145 +++++++++++++ .../PS_Editor/Spawn/PS_Editor_SpawnManager.h | 15 ++ .../Spawn/PS_Editor_SpawnableComponent.h | 20 ++ .../Spline/PS_Editor_SplineActor.cpp | 6 +- .../UI/Widgets/PS_Editor_MainWidget.cpp | 201 +++++++++++++++++- .../UI/Widgets/PS_Editor_MainWidget.h | 6 + 21 files changed, 759 insertions(+), 63 deletions(-) create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_CameraStart.cpp create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_CameraStart.h create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_CameraStart.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_CameraStart.cpp new file mode 100644 index 0000000..7f59091 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_CameraStart.cpp @@ -0,0 +1,36 @@ +#include "PS_Editor_CameraStart.h" +#include "Components/ArrowComponent.h" +#include "Components/BillboardComponent.h" + +APS_Editor_CameraStart::APS_Editor_CameraStart() +{ + PrimaryActorTick.bCanEverTick = false; + + USceneComponent* Root = CreateDefaultSubobject(TEXT("Root")); + RootComponent = Root; + +#if WITH_EDITORONLY_DATA + SpriteComponent = CreateDefaultSubobject(TEXT("Sprite")); + SpriteComponent->SetupAttachment(Root); + SpriteComponent->SetHiddenInGame(true); + SpriteComponent->bIsScreenSizeScaled = true; + SpriteComponent->ScreenSize = 0.0015f; + + // Default icon — can be overridden in derived Blueprints + static ConstructorHelpers::FObjectFinder IconFinder(TEXT("/Engine/EditorResources/S_Note")); + if (IconFinder.Succeeded()) + { + SpriteComponent->SetSprite(IconFinder.Object); + } + + ArrowComponent = CreateDefaultSubobject(TEXT("Arrow")); + ArrowComponent->SetupAttachment(Root); + ArrowComponent->ArrowColor = FColor(0, 180, 255); + ArrowComponent->ArrowSize = 1.5f; + ArrowComponent->bIsScreenSizeScaled = true; + ArrowComponent->SetHiddenInGame(true); +#endif + + // Hide in game — this is just a marker + SetActorHiddenInGame(true); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_CameraStart.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_CameraStart.h new file mode 100644 index 0000000..1ae0a08 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_CameraStart.h @@ -0,0 +1,30 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "PS_Editor_CameraStart.generated.h" + +/** + * Place this actor in a base level to define the initial camera position + * when the PS_Editor loads that level as a sublevel. + * The pawn will be teleported to this actor's location and rotation. + */ +UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "PS Editor Camera Start")) +class PS_EDITOR_API APS_Editor_CameraStart : public AActor +{ + GENERATED_BODY() + +public: + APS_Editor_CameraStart(); + +#if WITH_EDITORONLY_DATA +protected: + /** Icon visible in the editor viewport. Override the sprite in a derived Blueprint if needed. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") + TObjectPtr SpriteComponent; + + /** Arrow showing the view direction in the editor viewport. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") + TObjectPtr ArrowComponent; +#endif +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_GameInstanceSubsystem.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_GameInstanceSubsystem.cpp index 1d85e09..f24d09b 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_GameInstanceSubsystem.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_GameInstanceSubsystem.cpp @@ -38,6 +38,7 @@ void UPS_Editor_GameInstanceSubsystem::ReturnToEditor(const UObject* WorldContex // Restore pending from LastEdited so the editor picks them up PendingScenarioName = LastEditedScenarioName; PendingBaseLevel = LastEditedBaseLevel; + bIsPlayMode = false; UE_LOG(LogTemp, Log, TEXT("PS_Editor: Returning to editor (scenario: '%s', base level: '%s')"), *PendingScenarioName, *PendingBaseLevel); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_GameInstanceSubsystem.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_GameInstanceSubsystem.h index e9ef7eb..1819833 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_GameInstanceSubsystem.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_GameInstanceSubsystem.h @@ -36,6 +36,10 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") FString EditorMapName; + /** True when the Play button was pressed (gameplay mode). False when just loading/editing a scenario. */ + UPROPERTY(BlueprintReadWrite, Category = "PS Editor") + bool bIsPlayMode = false; + /** Set pending scenario + base level (for opening the editor or gameplay). */ UFUNCTION(BlueprintCallable, Category = "PS Editor") void SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel); 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 d2d34d9..bf61421 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 @@ -454,7 +454,7 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value) if (AActor* Actor = Selected[i].Get()) { FVector NewScale = GizmoDragInitialTransforms[i].GetScale3D(); - if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::XYZ) + if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::XYZ || Gizmo->bIsUniformScaleOnly) { NewScale *= ScaleFactor; // Uniform scale } @@ -1380,10 +1380,17 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis) ActiveGizmoAxis = Axis; + // Block rotation on non-yaw axes if YawOnly is set + const EPS_Editor_TransformMode Mode = SM->GetTransformMode(); + if (Mode == EPS_Editor_TransformMode::Rotate && Gizmo->bIsYawOnly + && Axis != EPS_Editor_GizmoAxis::Z) + { + return; + } + // Store axis direction at drag start - used for entire drag duration GizmoDragAxisDir = Gizmo->GetAxisDirection(Axis); const FVector AxisDir = GizmoDragAxisDir; - const EPS_Editor_TransformMode Mode = SM->GetTransformMode(); FVector PlaneNormal; if (Mode == EPS_Editor_TransformMode::Rotate) 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 30458d4..f6930d5 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 @@ -10,6 +10,7 @@ #include "PS_Editor_PointPlaceable.h" #include "Engine/LevelStreamingDynamic.h" #include "Engine/PostProcessVolume.h" +#include "PS_Editor_CameraStart.h" #include "PS_Editor_GameInstanceSubsystem.h" #include "GameFramework/GameModeBase.h" #include "GameFramework/GameStateBase.h" @@ -131,6 +132,16 @@ void APS_Editor_PlayerController::BeginPlay() }); } +void APS_Editor_PlayerController::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + if (SpawnManager) + { + SpawnManager->TickEditorMode(DeltaTime); + } +} + void APS_Editor_PlayerController::OnPossess(APawn* InPawn) { Super::OnPossess(InPawn); @@ -181,22 +192,25 @@ void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActo void APS_Editor_PlayerController::FinishPointPlacement() { - // Just exit the mode — actor is already tracked, all edits already have undo - if (APS_Editor_SplineActor* Spline = Cast(ActivePlacementActor)) + // Exit the mode FIRST to prevent re-entrant ClearSelection loop + AActor* PlacementActor = ActivePlacementActor; + ActivePlacementActor = nullptr; + AppendInitialPoints.Empty(); + EditorMode = EPS_Editor_EditorMode::Normal; + + if (APS_Editor_SplineActor* Spline = Cast(PlacementActor)) { Spline->SetHandlesVisible(false); Spline->ClearHandleHighlight(); } - if (ActivePlacementActor && SelectionManager) + if (PlacementActor && SelectionManager) { SelectionManager->ClearSelection(); - SelectionManager->SelectActor(ActivePlacementActor); + SelectionManager->SelectActor(PlacementActor); } UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exited placement mode")); - - ActivePlacementActor = nullptr; bAppendingToActor = false; AppendInitialPoints.Empty(); EditorMode = EPS_Editor_EditorMode::Normal; @@ -285,14 +299,37 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials() ULevel* Level = CurrentSublevel->GetLoadedLevel(); if (!Level) return; + APS_Editor_CameraStart* CameraStart = nullptr; + for (AActor* Actor : Level->Actors) { if (!Actor) continue; - APostProcessVolume* PPVolume = Cast(Actor); - if (!PPVolume) continue; - PPVolume->Settings.WeightedBlendables.Array.Empty(); - UE_LOG(LogTemp, Log, TEXT("PS_Editor: Disabled PP materials on '%s'"), *PPVolume->GetName()); + // Disable custom post-process materials + if (APostProcessVolume* PPVolume = Cast(Actor)) + { + PPVolume->Settings.WeightedBlendables.Array.Empty(); + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Disabled PP materials on '%s'"), *PPVolume->GetName()); + } + + // Find camera start marker + if (!CameraStart) + { + CameraStart = Cast(Actor); + } + } + + // Teleport pawn to camera start position (yaw only — keep camera level) + if (CameraStart) + { + if (APawn* MyPawn = GetPawn()) + { + const FRotator YawOnly(CameraStart->GetActorRotation().Pitch, CameraStart->GetActorRotation().Yaw, 0.0f); + MyPawn->SetActorLocationAndRotation(CameraStart->GetActorLocation(), YawOnly); + SetControlRotation(YawOnly); + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Camera moved to CameraStart at %s"), + *CameraStart->GetActorLocation().ToString()); + } } } 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 e2d8a59..c5246f3 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 @@ -89,6 +89,7 @@ public: protected: virtual void BeginPlay() override; + virtual void Tick(float DeltaTime) override; virtual void OnPossess(APawn* InPawn) override; private: diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp index 7020f11..2103ff8 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp @@ -142,6 +142,7 @@ void APS_Editor_Gizmo::BeginPlay() // Grey center cube for uniform scale Mat_Center = MakeColorMat(FLinearColor(0.5f, 0.5f, 0.5f)); + Mat_Disabled = MakeColorMat(FLinearColor(0.15f, 0.15f, 0.15f)); ApplyMat(ScaleCube_Center, Mat_Center); SetGizmoVisible(false); @@ -339,6 +340,13 @@ void APS_Editor_Gizmo::SetTransformMode(EPS_Editor_TransformMode NewMode) ScaleRoot->SetVisibility(bGizmoVisible && NewMode == EPS_Editor_TransformMode::Scale, true); } +void APS_Editor_Gizmo::SetYawOnly(bool bYawOnly) +{ + bIsYawOnly = bYawOnly; + if (Arc_X) Arc_X->SetMaterial(0, bYawOnly && Mat_Disabled ? Mat_Disabled : Mat_X); + if (Arc_Y) Arc_Y->SetMaterial(0, bYawOnly && Mat_Disabled ? Mat_Disabled : Mat_Y); +} + // ---- Hit detection ---- EPS_Editor_GizmoAxis APS_Editor_Gizmo::GetAxisUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const @@ -421,13 +429,36 @@ void APS_Editor_Gizmo::SetHighlightedAxis(EPS_Editor_GizmoAxis Axis) default: break; } SetAxisMaterial(HighlightedAxis, OrigMat); + + // Restore disabled arcs to grey after un-highlight + if (bIsYawOnly && Mat_Disabled) + { + if (HighlightedAxis == EPS_Editor_GizmoAxis::X && Arc_X) Arc_X->SetMaterial(0, Mat_Disabled); + if (HighlightedAxis == EPS_Editor_GizmoAxis::Y && Arc_Y) Arc_Y->SetMaterial(0, Mat_Disabled); + } } HighlightedAxis = Axis; if (Axis != EPS_Editor_GizmoAxis::None) { - SetAxisMaterial(Axis, Mat_Highlight); + // In yaw-only mode, don't highlight rotation arcs on X/Y but still highlight translate/scale + if (bIsYawOnly && CurrentMode == EPS_Editor_TransformMode::Rotate + && (Axis == EPS_Editor_GizmoAxis::X || Axis == EPS_Editor_GizmoAxis::Y)) + { + // Don't highlight at all in rotate mode for disabled axes + } + else + { + SetAxisMaterial(Axis, Mat_Highlight); + + // Re-grey the arcs even if translate/scale axes are highlighted + if (bIsYawOnly && Mat_Disabled) + { + if (Axis == EPS_Editor_GizmoAxis::X && Arc_X) Arc_X->SetMaterial(0, Mat_Disabled); + if (Axis == EPS_Editor_GizmoAxis::Y && Arc_Y) Arc_Y->SetMaterial(0, Mat_Disabled); + } + } } } @@ -490,9 +521,9 @@ void APS_Editor_Gizmo::UpdateArcFacing() CreateArcRing(Arc_Y, LocalY, ArcY_Start, ArcY_End, ArcRadius, ArcWidth, ArcSegments); CreateArcRing(Arc_Z, LocalZ, ArcZ_Start, ArcZ_End, ArcRadius, ArcWidth, ArcSegments); - // Re-apply materials - if (Mat_X) Arc_X->SetMaterial(0, Mat_X); - if (Mat_Y) Arc_Y->SetMaterial(0, Mat_Y); + // Re-apply materials (respect YawOnly constraint) + if (Mat_X) Arc_X->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_X); + if (Mat_Y) Arc_Y->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_Y); if (Mat_Z) Arc_Z->SetMaterial(0, Mat_Z); if (HighlightedAxis != EPS_Editor_GizmoAxis::None) @@ -512,9 +543,9 @@ void APS_Editor_Gizmo::UpdateArcFacing() PositionGuideLine(GuideLine_B, LocalY * NewOctant.Y); PositionGuideLine(GuideLine_C, LocalZ * NewOctant.Z); - // Re-apply materials (mesh sections are recreated) - if (Mat_X) Arc_X->SetMaterial(0, Mat_X); - if (Mat_Y) Arc_Y->SetMaterial(0, Mat_Y); + // Re-apply materials (mesh sections are recreated, respect YawOnly) + if (Mat_X) Arc_X->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_X); + if (Mat_Y) Arc_Y->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_Y); if (Mat_Z) Arc_Z->SetMaterial(0, Mat_Z); // Restore highlight if needed diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h index 558c5fa..6c010d9 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h @@ -31,6 +31,9 @@ public: void SetHighlightedAxis(EPS_Editor_GizmoAxis Axis); FVector GetAxisDirection(EPS_Editor_GizmoAxis Axis) const; + /** Constrain rotation to yaw only: hides X/Y rotation arcs and blocks drag on those axes. */ + void SetYawOnly(bool bYawOnly); + /** Set the orientation for local space mode (matches selected actor's rotation). */ void SetLocalOrientation(FRotator ActorRotation); @@ -116,11 +119,18 @@ private: TObjectPtr Mat_Highlight; UPROPERTY() TObjectPtr Mat_Center; + UPROPERTY() + TObjectPtr Mat_Disabled; // ---- State ---- EPS_Editor_TransformMode CurrentMode = EPS_Editor_TransformMode::Translate; EPS_Editor_GizmoAxis HighlightedAxis = EPS_Editor_GizmoAxis::None; bool bGizmoVisible = false; + +public: + bool bIsYawOnly = false; + bool bIsUniformScaleOnly = false; +private: float DesiredScreenSize = 0.25f; // ---- Helpers ---- 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 883c4a2..3a38905 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_SpawnableComponent.h" #include "PS_Editor_SplineActor.h" #include "GameFramework/PlayerController.h" #include "Engine/World.h" @@ -20,11 +21,11 @@ static TMap ExportEditableProps(AActor* Actor) UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass(); if (!EditComp) return Result; - for (const FName& Path : EditComp->EditableProperties) + for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties) { FString CompName, PropName; - UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName); - const FString Key = Path.ToString(); + UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName); + const FString Key = Entry.Path.ToString(); UObject* Target = CompName.IsEmpty() ? static_cast(Actor) : nullptr; if (!Target) @@ -56,11 +57,11 @@ static void ImportEditableProps(AActor* Actor, const TMap& Pro UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass(); if (!EditComp) return; - for (const FName& Path : EditComp->EditableProperties) + for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties) { FString CompName, PropName; - UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName); - const FString Key = Path.ToString(); + UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName); + const FString Key = Entry.Path.ToString(); const FString* ValStr = Properties.Find(Key); if (!ValStr) continue; @@ -456,7 +457,13 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround() FVector Origin, Extent; Actor->GetActorBounds(false, Origin, Extent); - const float BottomOffset = Extent.Z; + float BottomOffset = Extent.Z; + + // Add custom ground offset from SpawnableComponent if available + if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass()) + { + BottomOffset += SpawnComp->GroundOffset; + } const FVector Start = Actor->GetActorLocation(); const FVector End = Start - FVector::UpVector * 100000.0f; @@ -472,6 +479,16 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround() FVector NewLocation = Hit.ImpactPoint; NewLocation.Z += BottomOffset; Actor->SetActorLocation(NewLocation); + + UE_LOG(LogTemp, Log, TEXT("PS_Editor: SnapToGround '%s' -> Hit '%s' (%s) at Z=%.1f, BottomOffset=%.1f, NewZ=%.1f"), + *Actor->GetName(), + *Hit.GetActor()->GetName(), + *Hit.GetComponent()->GetName(), + Hit.ImpactPoint.Z, BottomOffset, NewLocation.Z); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: SnapToGround '%s' -> No hit"), *Actor->GetName()); } UndoAction->NewTransforms.Add(Actor->GetActorTransform()); @@ -514,11 +531,21 @@ void UPS_Editor_SelectionManager::UpdateGizmo() Gizmo->SetGizmoActive(true, Pivot); - // Update gizmo orientation + // Update gizmo orientation and yaw-only constraint if (AActor* Actor = SelectedActors[0].Get()) { bool bUseLocal = (CurrentTransformMode == EPS_Editor_TransformMode::Scale) || (CurrentSpace == EPS_Editor_Space::Local); Gizmo->SetLocalOrientation(bUseLocal ? Actor->GetActorRotation() : FRotator::ZeroRotator); + + bool bYaw = false; + bool bUniformScale = false; + if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass()) + { + bYaw = SpawnComp->bYawOnly; + bUniformScale = SpawnComp->bUniformScaleOnly; + } + Gizmo->SetYawOnly(bYaw); + Gizmo->bIsUniformScaleOnly = bUniformScale; } } else 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 7eb7f79..679bdcf 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 @@ -1,6 +1,7 @@ #include "PS_Editor_SceneLoader.h" #include "PS_Editor_SceneData.h" #include "PS_Editor_EditableComponent.h" +#include "PS_Editor_EditableInterface.h" #include "PS_Editor_SpawnableComponent.h" #include "PS_Editor_SplineActor.h" #include "PS_Editor_PointPlaceable.h" @@ -147,6 +148,12 @@ AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Edit Spline->ImportFromCustomProperties(ActorData.CustomProperties); } + // Notify the actor so it can re-initialize with the loaded property values + if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) + { + IPS_Editor_EditableInterface::Execute_OnEditorNeedsReinit(SpawnedActor); + } + return SpawnedActor; } @@ -155,11 +162,11 @@ void UPS_Editor_SceneLoader::ApplyCustomProperties(AActor* Actor, const TMapFindComponentByClass(); if (!EditComp) return; - for (const FName& Path : EditComp->EditableProperties) + for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties) { FString CompName, PropName; - UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName); - const FString Key = Path.ToString(); + UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName); + const FString Key = Entry.Path.ToString(); const FString* ValueStr = CustomProperties.Find(Key); if (!ValueStr) continue; 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 66d0a1c..c3b2f36 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_EditableInterface.h" #include "PS_Editor_PlayerController.h" #include "PS_Editor_SplineActor.h" #include "JsonObjectConverter.h" @@ -55,11 +56,11 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_ // Export custom editable properties if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass()) { - for (const FName& Path : EditComp->EditableProperties) + for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties) { FString CompName, PropName; - UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName); - const FString Key = Path.ToString(); + UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName); + const FString Key = Entry.Path.ToString(); UObject* Target = CompName.IsEmpty() ? static_cast(Actor) : nullptr; if (!Target) @@ -171,11 +172,11 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_ { if (UPS_Editor_EditableComponent* EditComp = SpawnedActor->FindComponentByClass()) { - for (const FName& Path : EditComp->EditableProperties) + for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties) { FString CompName, PropName; - UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName); - const FString Key = Path.ToString(); + UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName); + const FString Key = Entry.Path.ToString(); const FString* ValueStr = ActorData.CustomProperties.Find(Key); if (!ValueStr) continue; @@ -217,6 +218,13 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_ Spline->ImportFromCustomProperties(ActorData.CustomProperties); } + // Notify the actor that properties were loaded and we are in editor mode + if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) + { + IPS_Editor_EditableInterface::Execute_OnEditorNeedsReinit(SpawnedActor); + IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true); + } + SpawnedCount++; } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.cpp index 543c260..8b52adc 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.cpp @@ -166,9 +166,21 @@ void UPS_Editor_EditableComponent::PostEditChangeProperty(FPropertyChangedEvent& FullPath = FName(SelComp + TEXT(".") + AddProperty.ToString()); } - if (!EditableProperties.Contains(FullPath)) + // Check if already exists + bool bExists = false; + for (const FPS_Editor_EditablePropertyEntry& Entry : EditableProperties) { - EditableProperties.Add(FullPath); + if (Entry.Path == FullPath) + { + bExists = true; + break; + } + } + if (!bExists) + { + FPS_Editor_EditablePropertyEntry NewEntry; + NewEntry.Path = FullPath; + EditableProperties.Add(NewEntry); UE_LOG(LogTemp, Log, TEXT("PS_Editor: Added editable property: %s"), *FullPath.ToString()); } @@ -190,13 +202,37 @@ void UPS_Editor_EditableComponent::ParsePropertyPath(const FName& Path, FString& } } +bool UPS_Editor_EditableComponent::RequiresReinit(const FName& PropertyPath) const +{ + for (const FPS_Editor_EditablePropertyEntry& Entry : EditableProperties) + { + if (Entry.Path == PropertyPath) + { + return Entry.bNeedsReinit; + } + } + return false; +} + +bool UPS_Editor_EditableComponent::RequiresRespawn(const FName& PropertyPath) const +{ + for (const FPS_Editor_EditablePropertyEntry& Entry : EditableProperties) + { + if (Entry.Path == PropertyPath) + { + return Entry.bNeedsRespawn; + } + } + return false; +} + FString UPS_Editor_EditableComponent::GetDisplayName(const FName& Path) const { - if (const FString* Override = DisplayNames.Find(Path)) + for (const FPS_Editor_EditablePropertyEntry& Entry : EditableProperties) { - if (!Override->IsEmpty()) + if (Entry.Path == Path && !Entry.DisplayName.IsEmpty()) { - return *Override; + return Entry.DisplayName; } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.h index 4d67375..eb26c7e 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.h @@ -4,6 +4,39 @@ #include "Components/ActorComponent.h" #include "PS_Editor_EditableComponent.generated.h" +/** + * A single editable property entry with optional respawn flag. + */ +USTRUCT(BlueprintType) +struct FPS_Editor_EditablePropertyEntry +{ + GENERATED_BODY() + + /** + * Property path: "ComponentName.PropertyName" or just "PropertyName" for actor-level. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") + FName Path; + + /** Display name override for the runtime editor UI. If empty, the property name is used. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") + FString DisplayName; + + /** + * If true, calls OnEditorPropertiesChanged (IPS_Editor_EditableInterface) + * after changing this property, so the actor can re-run its initialization logic. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") + bool bNeedsReinit = false; + + /** + * If true, the actor is destroyed and respawned with all current properties applied, + * then OnEditorPropertiesChanged is called. Use when reinit alone is not enough. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor") + bool bNeedsRespawn = false; +}; + /** * Marker component that declares which properties are editable in the PS_Editor runtime editor. * Separate from SpawnableComponent (which handles catalogue metadata). @@ -31,19 +64,16 @@ public: /** * Properties exposed in the runtime editor. - * Format: "ComponentName.PropertyName" or just "PropertyName" for actor-level. - * Populated via the dropdowns above, or editable manually. + * Each entry has a path, optional display name, and optional respawn flag. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editable Properties") - TArray EditableProperties; + TArray EditableProperties; - /** - * Optional display name overrides for the runtime editor UI. - * Key = property path (same as in EditableProperties), Value = label shown in UI. - * If not set, the property name is used as label. - */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editable Properties") - TMap DisplayNames; + /** Returns true if the given property path requires a reinit call. */ + bool RequiresReinit(const FName& PropertyPath) const; + + /** Returns true if the given property path requires a full respawn. */ + bool RequiresRespawn(const FName& PropertyPath) const; /** Lists available component names on the owning actor. */ UFUNCTION() diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h new file mode 100644 index 0000000..1cabd50 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h @@ -0,0 +1,62 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "PS_Editor_EditableInterface.generated.h" + +UINTERFACE(BlueprintType, meta = (DisplayName = "PS Editor Editable")) +class PS_EDITOR_API UPS_Editor_EditableInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for actors that need to react to property changes + * made at runtime in the PS_Editor. + * + * Implement OnEditorPropertiesChanged in your Blueprint to handle + * re-initialization or any post-edit logic. + */ +class PS_EDITOR_API IPS_Editor_EditableInterface +{ + GENERATED_BODY() + +public: + /** + * Called every time an editable property is modified at runtime. + * The new value is already applied when this is called. + * @param PropertyPath The property that changed (e.g. "CharacterType" or "MyComponent.SomeProperty"). + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor") + void OnEditorPropertyChanged(const FString& PropertyPath); + + /** + * Called when a property with bNeedsReinit is changed. + * The new value is already applied. Use this to re-run initialization logic. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor") + void OnEditorNeedsReinit(); + + /** + * Called on the NEW actor after a respawn triggered by bNeedsRespawn. + * All properties have been applied. Use this for post-respawn setup. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor") + void OnEditorNeedsRespawn(); + + /** + * Called when the actor enters or exits editor mode. + * Use this to show/hide editor-only helpers (VR guides, placement aids, debug visuals, etc.). + * @param bIsEditing True when entering edit mode, false when leaving (Play, return to game). + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor") + void OnEditorModeChanged(bool bIsEditing); + + /** + * Called every frame while in editor mode. + * Use this to update visuals dynamically based on property changes, animate helpers, etc. + * @param DeltaTime Time since last frame. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor") + void OnEditorTick(float DeltaTime); +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp index b06ccc3..cc8fa98 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp @@ -1,5 +1,7 @@ #include "PS_Editor_SpawnManager.h" #include "PS_Editor_SpawnableComponent.h" +#include "PS_Editor_EditableComponent.h" +#include "PS_Editor_EditableInterface.h" #include "PS_Editor_UndoManager.h" #include "PS_Editor_PlayerController.h" #include "GameFramework/PlayerController.h" @@ -134,6 +136,36 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector // Track spawned actor SpawnedActors.Add(SpawnedActor); + // Auto-snap to ground if configured + UPS_Editor_SpawnableComponent* SpawnComp = SpawnedActor->FindComponentByClass(); + if (SpawnComp && SpawnComp->bAutoSnapToGround && PC->GetWorld()) + { + FVector Origin, Extent; + SpawnedActor->GetActorBounds(false, Origin, Extent); + float BottomOffset = Extent.Z + SpawnComp->GroundOffset; + + const FVector Start = SpawnedActor->GetActorLocation(); + const FVector End = Start - FVector::UpVector * 100000.0f; + + FHitResult Hit; + FCollisionQueryParams SnapParams; + SnapParams.AddIgnoredActor(SpawnedActor); + SnapParams.AddIgnoredActor(PC->GetPawn()); + + if (PC->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Camera, SnapParams)) + { + FVector NewLoc = Hit.ImpactPoint; + NewLoc.Z += BottomOffset; + SpawnedActor->SetActorLocation(NewLoc); + } + } + + // Notify: we are in editor mode + if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) + { + IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true); + } + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spawned %s at %s"), *Entry.DisplayName, *WorldLocation.ToString()); return SpawnedActor; } @@ -218,6 +250,119 @@ bool UPS_Editor_SpawnManager::IsActorSpawned(AActor* Actor) const return false; } +AActor* UPS_Editor_SpawnManager::RespawnActorWithProperties(AActor* OldActor, const TMap& PropertyValues) +{ + if (!OldActor) return nullptr; + + APlayerController* PC = OwnerPC.Get(); + if (!PC || !PC->GetWorld()) return nullptr; + + UClass* ActorClass = OldActor->GetClass(); + const FTransform OldTransform = OldActor->GetActorTransform(); + + // 1. Remove old actor from tracking and destroy + SpawnedActors.RemoveAll([OldActor](const TWeakObjectPtr& Weak) + { + return Weak.Get() == OldActor; + }); + OldActor->Destroy(); + + // 2. Spawn fresh actor (full spawn — BP components are created, BeginPlay runs with defaults) + FActorSpawnParameters SpawnParams; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn; + AActor* NewActor = PC->GetWorld()->SpawnActor(ActorClass, OldTransform, SpawnParams); + + if (!NewActor) + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: RespawnActorWithProperties failed to spawn %s"), *ActorClass->GetName()); + return nullptr; + } + + // 3. Apply all property values on the spawned actor (BP components now exist) + for (const auto& Pair : PropertyValues) + { + FString CompName, PropName; + UPS_Editor_EditableComponent::ParsePropertyPath(FName(*Pair.Key), CompName, PropName); + + UObject* Target = nullptr; + if (CompName.IsEmpty()) + { + Target = NewActor; + } + else + { + // Match component: by instance name, by class name, or by class name without _C suffix + TArray Components; + NewActor->GetComponents(Components); + for (UActorComponent* Comp : Components) + { + if (!Comp) continue; + const FString InstanceName = Comp->GetName(); + const FString ClassName = Comp->GetClass()->GetName(); + FString CleanClassName = ClassName; + CleanClassName.RemoveFromEnd(TEXT("_C")); + + if (InstanceName == CompName || ClassName == CompName || CleanClassName == CompName) + { + Target = Comp; + break; + } + } + } + + if (Target) + { + FProperty* Prop = FindFProperty(Target->GetClass(), *PropName); + if (Prop) + { + Prop->ImportText_InContainer(*Pair.Value, Target, Target, PPF_None); + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Respawn apply [%s] = '%s' on %s"), *Pair.Key, *Pair.Value, *Target->GetName()); + } + } + } + + // 4. Notify the new actor after respawn + if (NewActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) + { + IPS_Editor_EditableInterface::Execute_OnEditorNeedsRespawn(NewActor); + } + + if (USceneComponent* Root = NewActor->GetRootComponent()) + { + Root->SetMobility(EComponentMobility::Movable); + } + + SpawnedActors.Add(NewActor); + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Respawned %s with %d properties"), *ActorClass->GetName(), PropertyValues.Num()); + return NewActor; +} + +void UPS_Editor_SpawnManager::NotifyEditorModeChanged(bool bIsEditing) +{ + for (const TWeakObjectPtr& Weak : SpawnedActors) + { + AActor* Actor = Weak.Get(); + if (!Actor) continue; + if (Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) + { + IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(Actor, bIsEditing); + } + } +} + +void UPS_Editor_SpawnManager::TickEditorMode(float DeltaTime) +{ + for (const TWeakObjectPtr& Weak : SpawnedActors) + { + AActor* Actor = Weak.Get(); + if (!Actor) continue; + if (Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) + { + IPS_Editor_EditableInterface::Execute_OnEditorTick(Actor, DeltaTime); + } + } +} + TArray UPS_Editor_SpawnManager::GetCategories() const { TArray Categories; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h index 15c929c..c05b6c5 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h @@ -69,6 +69,21 @@ public: /** Returns true if this actor was spawned/tracked by the editor (catalogue, duplicate, paste, load). */ bool IsActorSpawned(AActor* Actor) const; + /** Notify all spawned actors that implement IPS_Editor_EditableInterface of editor mode change. */ + void NotifyEditorModeChanged(bool bIsEditing); + + /** Tick all spawned actors that implement IPS_Editor_EditableInterface. */ + void TickEditorMode(float DeltaTime); + + /** + * Respawn an actor: destroy it and create a new one of the same class, + * applying all given property values BEFORE BeginPlay (via SpawnActorDeferred). + * @param OldActor The actor to replace. + * @param PropertyValues Map of "ComponentName.PropertyName" -> exported text value. + * @return The newly spawned actor, or nullptr on failure. + */ + AActor* RespawnActorWithProperties(AActor* OldActor, const TMap& PropertyValues); + private: UPROPERTY() TWeakObjectPtr OwnerPC; 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 c27f71b..547398f 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 @@ -27,4 +27,24 @@ public: /** Optional thumbnail texture shown in the catalogue. Use "Capture All Thumbnails" on the SpawnCatalog DataAsset to auto-generate. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable") TObjectPtr Thumbnail; + + /** + * Z offset applied when snapping to ground (End key). + * 0 = actor pivot sits on the ground. Adjust for actors whose pivot is not at the bottom. + * Negative values sink the actor below the ground. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable") + float GroundOffset = 0.0f; + + /** If true, the actor is automatically snapped to ground when spawned from the catalogue. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable") + bool bAutoSnapToGround = false; + + /** If true, rotation is constrained to the vertical axis (Yaw only). Useful for characters and upright objects. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable") + bool bYawOnly = false; + + /** 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; }; 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 ee7180c..cf4c658 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 @@ -42,9 +42,9 @@ APS_Editor_SplineActor::APS_Editor_SplineActor() // Editable: exposes SplineColor and SplineTag in the runtime property panel EditableComp = CreateDefaultSubobject(TEXT("PS_Editor_Editable")); - EditableComp->EditableProperties.Add(FName(TEXT("SplineColor"))); - EditableComp->EditableProperties.Add(FName(TEXT("SplineSelectedColor"))); - EditableComp->EditableProperties.Add(FName(TEXT("SplineTag"))); + EditableComp->EditableProperties.Add({FName(TEXT("SplineColor"))}); + EditableComp->EditableProperties.Add({FName(TEXT("SplineSelectedColor"))}); + EditableComp->EditableProperties.Add({FName(TEXT("SplineTag"))}); } void APS_Editor_SplineActor::BeginPlay() 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 e086b35..5fc226c 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_UndoManager.h" #include "PS_Editor_PlayerController.h" #include "PS_Editor_EditableComponent.h" +#include "PS_Editor_EditableInterface.h" #include "PS_Editor_SpawnableComponent.h" #include "PS_Editor_SplineActor.h" #include "PS_Editor_Pawn.h" @@ -607,6 +608,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem()) { Sub->SetPendingScenario(SceneName, BaseLevel); + Sub->bIsPlayMode = true; // Remember editor map for ReturnToEditor if (Sub->EditorMapName.IsEmpty()) { @@ -617,6 +619,12 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() } } + // Notify all spawned actors: leaving editor mode + if (UPS_Editor_SpawnManager* PlaySM2 = EditorPC->GetSpawnManager()) + { + PlaySM2->NotifyEditorModeChanged(false); + } + // Unload sublevel before OpenLevel to avoid name conflict EditorPC->LoadBaseLevelAsSublevel(TEXT("None")); @@ -739,6 +747,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() [ SAssignNew(BaseLevelCombo, SComboBox>) .OptionsSource(&BaseLevelOptions) + .Method(EPopupMethod::UseCurrentWindow) .OnSelectionChanged_Lambda([this](TSharedPtr Selected, ESelectInfo::Type SelectInfo) { if (!Selected.IsValid()) return; @@ -833,6 +842,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSpawnCatalog() .MaxDesiredHeight(400.0f) [ SNew(SScrollBox) + .ConsumeMouseWheel(EConsumeMouseWheel::Always) + SScrollBox::Slot() [ CatalogList @@ -1397,6 +1407,7 @@ TSharedRef UPS_Editor_MainWidget::BuildOutliner() + SVerticalBox::Slot().FillHeight(1.0f).Padding(0.0f, 4.0f, 0.0f, 0.0f) [ SNew(SScrollBox) + .ConsumeMouseWheel(EConsumeMouseWheel::Always) + SScrollBox::Slot() [ SAssignNew(OutlinerListContainer, SVerticalBox) @@ -1555,12 +1566,12 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties() for (int32 i = 0; i < EditComp->EditableProperties.Num(); i++) { - const FName& Path = EditComp->EditableProperties[i]; + const FPS_Editor_EditablePropertyEntry& Entry = EditComp->EditableProperties[i]; FPS_Editor_PropertyRowWidgets Row; - UPS_Editor_EditableComponent::ParsePropertyPath(Path, Row.ComponentName, Row.PropertyName); - Row.DisplayName = EditComp->GetDisplayName(Path); - Row.Key = Path.ToString(); + UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, Row.ComponentName, Row.PropertyName); + Row.DisplayName = EditComp->GetDisplayName(Entry.Path); + Row.Key = Entry.Path.ToString(); UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolving [%s] comp='%s' prop='%s'"), *Row.Key, *Row.ComponentName, *Row.PropertyName); @@ -1600,7 +1611,61 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties() // Value widget depends on property type TSharedRef ValueWidget = SNullWidget::NullWidget; - if (CastField(Row.CachedProperty)) + // Detect enum type (FEnumProperty for C++ enum class, FByteProperty with UEnum for legacy) + UEnum* FoundEnum = nullptr; + if (FEnumProperty* EnumProp = CastField(Row.CachedProperty)) + { + FoundEnum = EnumProp->GetEnum(); + } + else if (FByteProperty* ByteProp = CastField(Row.CachedProperty)) + { + FoundEnum = ByteProp->GetIntPropertyEnum(); + } + + if (FoundEnum) + { + Row.CachedEnum = FoundEnum; + Row.EnumOptions = MakeShared>>(); + for (int32 Idx = 0; Idx < FoundEnum->NumEnums() - 1; ++Idx) // -1 to skip _MAX + { + FString Name = FoundEnum->GetDisplayNameTextByIndex(Idx).ToString(); + Row.EnumOptions->Add(MakeShared(Name)); + } + + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Enum '%s' for property '%s' — %d options, OptionsPtr=%p"), + *FoundEnum->GetName(), *Row.PropertyName, Row.EnumOptions->Num(), Row.EnumOptions.Get()); + for (int32 Dbg = 0; Dbg < Row.EnumOptions->Num(); ++Dbg) + { + UE_LOG(LogTemp, Warning, TEXT(" [%d] %s"), Dbg, **(*Row.EnumOptions)[Dbg]); + } + + ValueWidget = SAssignNew(Row.EnumCombo, SComboBox>) + .OptionsSource(Row.EnumOptions.Get()) + .Method(EPopupMethod::UseCurrentWindow) + .IsFocusable(true) + .OnSelectionChanged_Lambda([this, RowIndex](TSharedPtr Selected, ESelectInfo::Type SelectInfo) + { + if (!Selected.IsValid() || SelectInfo == ESelectInfo::Direct) return; + FPS_Editor_PropertyRowWidgets& R = DynamicPropertyRows[RowIndex]; + if (R.EnumComboText.IsValid()) + { + R.EnumComboText->SetText(FText::FromString(*Selected)); + } + ApplyPropertyFromUI(RowIndex); + }) + .OnGenerateWidget_Lambda([](TSharedPtr Item) -> TSharedRef + { + return SNew(STextBlock) + .Text(FText::FromString(Item.IsValid() ? *Item : TEXT(""))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)); + }) + [ + SAssignNew(Row.EnumComboText, STextBlock) + .Text(FText::FromString(TEXT("Select..."))) + .Font(ValueFont) + ]; + } + else if (CastField(Row.CachedProperty)) { ValueWidget = SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth() @@ -1636,6 +1701,14 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties() + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) [ MakeTextField(Row.VecZ) ]; } + else if (StructProp->Struct == TBaseStructure::Get()) + { + ValueWidget = SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) + [ MakeTextField(Row.VecX) ] + + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) + [ MakeTextField(Row.VecY) ]; + } else if (StructProp->Struct == TBaseStructure::Get()) { ValueWidget = SNew(SHorizontalBox) @@ -1698,7 +1771,34 @@ void UPS_Editor_MainWidget::RefreshDynamicPropertyValues() UObject* Target = FindPropertyTarget(Actor, Row.ComponentName); if (!Target) continue; - if (FBoolProperty* BoolProp = CastField(Row.CachedProperty)) + if (Row.CachedEnum && Row.EnumCombo.IsValid() && !Row.EnumCombo->IsOpen()) + { + // Read current enum value index + int64 EnumValue = 0; + if (FEnumProperty* EnumProp = CastField(Row.CachedProperty)) + { + FNumericProperty* UnderlyingProp = EnumProp->GetUnderlyingProperty(); + const void* ValuePtr = Row.CachedProperty->ContainerPtrToValuePtr(Target); + EnumValue = UnderlyingProp->GetSignedIntPropertyValue(ValuePtr); + } + else if (FByteProperty* ByteProp = CastField(Row.CachedProperty)) + { + EnumValue = ByteProp->GetPropertyValue_InContainer(Target); + } + + int32 DisplayIdx = Row.CachedEnum->GetIndexByValue(EnumValue); + if (Row.EnumOptions.IsValid() && Row.EnumOptions->IsValidIndex(DisplayIdx)) + { + // Only update if value actually changed + FString NewText = *(*Row.EnumOptions)[DisplayIdx]; + if (Row.EnumComboText.IsValid() && Row.EnumComboText->GetText().ToString() != NewText) + { + Row.EnumCombo->SetSelectedItem((*Row.EnumOptions)[DisplayIdx]); + Row.EnumComboText->SetText(FText::FromString(NewText)); + } + } + } + else if (FBoolProperty* BoolProp = CastField(Row.CachedProperty)) { if (Row.BoolField.IsValid()) { @@ -1733,6 +1833,12 @@ void UPS_Editor_MainWidget::RefreshDynamicPropertyValues() SetIfNoFocus(Row.VecY, Rot.Yaw); SetIfNoFocus(Row.VecZ, Rot.Roll); } + else if (StructProp->Struct == TBaseStructure::Get()) + { + const FVector2D& Vec2 = *static_cast(ValuePtr); + SetIfNoFocus(Row.VecX, Vec2.X); + SetIfNoFocus(Row.VecY, Vec2.Y); + } else if (StructProp->Struct == TBaseStructure::Get()) { const FLinearColor& Col = *static_cast(ValuePtr); @@ -1801,7 +1907,19 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex) // Build new value text from UI and import FString NewValueText; - if (CastField(Row.CachedProperty)) + if (Row.CachedEnum && Row.EnumComboText.IsValid()) + { + FString DisplayName = Row.EnumComboText->GetText().ToString(); + for (int32 Idx = 0; Idx < Row.CachedEnum->NumEnums() - 1; ++Idx) + { + if (Row.CachedEnum->GetDisplayNameTextByIndex(Idx).ToString() == DisplayName) + { + NewValueText = Row.CachedEnum->GetNameStringByIndex(Idx); + break; + } + } + } + else if (CastField(Row.CachedProperty)) { NewValueText = Row.BoolField.IsValid() && Row.BoolField->IsChecked() ? TEXT("True") : TEXT("False"); } @@ -1816,6 +1934,10 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex) { NewValueText = FString::Printf(TEXT("(X=%s,Y=%s,Z=%s)"), *GetText(Row.VecX), *GetText(Row.VecY), *GetText(Row.VecZ)); } + else if (StructProp->Struct == TBaseStructure::Get()) + { + NewValueText = FString::Printf(TEXT("(X=%s,Y=%s)"), *GetText(Row.VecX), *GetText(Row.VecY)); + } else if (StructProp->Struct == TBaseStructure::Get()) { NewValueText = FString::Printf(TEXT("(Pitch=%s,Yaw=%s,Roll=%s)"), *GetText(Row.VecX), *GetText(Row.VecY), *GetText(Row.VecZ)); @@ -1834,10 +1956,16 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex) NewValueText = Row.SingleField->GetText().ToString(); } - if (NewValueText.IsEmpty()) return; + if (NewValueText.IsEmpty()) + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ApplyProperty '%s' - NewValueText is EMPTY, aborting"), *Row.Key); + return; + } // Apply the new value - Row.CachedProperty->ImportText_InContainer(*NewValueText, Target, Target, PPF_None); + const TCHAR* ImportResult = Row.CachedProperty->ImportText_InContainer(*NewValueText, Target, Target, PPF_None); + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ApplyProperty '%s' = '%s' -> Import %s"), + *Row.Key, *NewValueText, ImportResult ? TEXT("OK") : TEXT("FAILED")); // Refresh rendering if (UActorComponent* Comp = Cast(Target)) @@ -1869,6 +1997,61 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex) } UE_LOG(LogTemp, Log, TEXT("PS_Editor: Property %s changed: %s → %s"), *Row.Key, *OldValue, *NewValue); + + // Always notify property changed + const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()); + if (bImplementsInterface) + { + IPS_Editor_EditableInterface::Execute_OnEditorPropertyChanged(Actor, Row.Key); + } + + // Check if this property requires reinit or respawn + UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass(); + if (!EditComp) return; + + const FName PropertyPath(*Row.Key); + const bool bRespawn = EditComp->RequiresRespawn(PropertyPath); + const bool bReinit = EditComp->RequiresReinit(PropertyPath); + + if (bRespawn) + { + APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer()); + if (!EditorPC) return; + UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager(); + if (!SpawnMgr) return; + + // Collect ALL current editable property values (including the one just changed) + TMap AllProps; + for (const FPS_Editor_PropertyRowWidgets& PropRow : DynamicPropertyRows) + { + if (!PropRow.CachedProperty) continue; + UObject* PropTarget = FindPropertyTarget(Actor, PropRow.ComponentName); + if (!PropTarget) continue; + + FString ExportedVal; + PropRow.CachedProperty->ExportText_InContainer(0, ExportedVal, PropTarget, PropTarget, nullptr, PPF_None); + AllProps.Add(PropRow.Key, ExportedVal); + } + + // Deselect before destroying + UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get(); + if (SelMgr) SelMgr->ClearSelection(); + + // Respawn: destroy + spawn + apply all props + AActor* NewActor = SpawnMgr->RespawnActorWithProperties(Actor, AllProps); + if (NewActor && SelMgr) + { + SelMgr->SelectActor(NewActor); + } + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Respawned actor for property '%s'"), *Row.Key); + } + else if (bReinit) + { + if (bImplementsInterface) + { + IPS_Editor_EditableInterface::Execute_OnEditorNeedsReinit(Actor); + } + } } FString UPS_Editor_MainWidget::FloatToString(float Value) const 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 842beb9..e556224 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 @@ -23,6 +23,12 @@ struct FPS_Editor_PropertyRowWidgets TSharedPtr BoolField; // bool TSharedPtr VecX, VecY, VecZ; // FVector, FRotator TSharedPtr ColR, ColG, ColB, ColA; // FLinearColor + + // Enum support + TSharedPtr>> EnumCombo; + TSharedPtr EnumComboText; + TSharedPtr>> EnumOptions; // Heap-allocated so OptionsSource pointer survives Row copy + UEnum* CachedEnum = nullptr; }; /**