Compare commits
2 Commits
7a8eb7f827
...
2b5015a9af
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b5015a9af | |||
| 83840d1626 |
@@ -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<USceneComponent>(TEXT("Root"));
|
||||||
|
RootComponent = Root;
|
||||||
|
|
||||||
|
#if WITH_EDITORONLY_DATA
|
||||||
|
SpriteComponent = CreateDefaultSubobject<UBillboardComponent>(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<UTexture2D> IconFinder(TEXT("/Engine/EditorResources/S_Note"));
|
||||||
|
if (IconFinder.Succeeded())
|
||||||
|
{
|
||||||
|
SpriteComponent->SetSprite(IconFinder.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrowComponent = CreateDefaultSubobject<UArrowComponent>(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);
|
||||||
|
}
|
||||||
@@ -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<class UBillboardComponent> SpriteComponent;
|
||||||
|
|
||||||
|
/** Arrow showing the view direction in the editor viewport. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||||
|
TObjectPtr<class UArrowComponent> ArrowComponent;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
@@ -38,6 +38,7 @@ void UPS_Editor_GameInstanceSubsystem::ReturnToEditor(const UObject* WorldContex
|
|||||||
// Restore pending from LastEdited so the editor picks them up
|
// Restore pending from LastEdited so the editor picks them up
|
||||||
PendingScenarioName = LastEditedScenarioName;
|
PendingScenarioName = LastEditedScenarioName;
|
||||||
PendingBaseLevel = LastEditedBaseLevel;
|
PendingBaseLevel = LastEditedBaseLevel;
|
||||||
|
bIsPlayMode = false;
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Returning to editor (scenario: '%s', base level: '%s')"),
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Returning to editor (scenario: '%s', base level: '%s')"),
|
||||||
*PendingScenarioName, *PendingBaseLevel);
|
*PendingScenarioName, *PendingBaseLevel);
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||||
FString EditorMapName;
|
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). */
|
/** Set pending scenario + base level (for opening the editor or gameplay). */
|
||||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
void SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel);
|
void SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel);
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ void APS_Editor_Pawn::CreateInputActions()
|
|||||||
IA_TranslateMode = NewObject<UInputAction>(this, TEXT("IA_TranslateMode"));
|
IA_TranslateMode = NewObject<UInputAction>(this, TEXT("IA_TranslateMode"));
|
||||||
IA_TranslateMode->ValueType = EInputActionValueType::Boolean;
|
IA_TranslateMode->ValueType = EInputActionValueType::Boolean;
|
||||||
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::Z);
|
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::Z);
|
||||||
|
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::W);
|
||||||
|
|
||||||
IA_RotateMode = NewObject<UInputAction>(this, TEXT("IA_RotateMode"));
|
IA_RotateMode = NewObject<UInputAction>(this, TEXT("IA_RotateMode"));
|
||||||
IA_RotateMode->ValueType = EInputActionValueType::Boolean;
|
IA_RotateMode->ValueType = EInputActionValueType::Boolean;
|
||||||
@@ -453,7 +454,7 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
|||||||
if (AActor* Actor = Selected[i].Get())
|
if (AActor* Actor = Selected[i].Get())
|
||||||
{
|
{
|
||||||
FVector NewScale = GizmoDragInitialTransforms[i].GetScale3D();
|
FVector NewScale = GizmoDragInitialTransforms[i].GetScale3D();
|
||||||
if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::XYZ)
|
if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::XYZ || Gizmo->bIsUniformScaleOnly)
|
||||||
{
|
{
|
||||||
NewScale *= ScaleFactor; // Uniform scale
|
NewScale *= ScaleFactor; // Uniform scale
|
||||||
}
|
}
|
||||||
@@ -609,7 +610,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
FCollisionQueryParams SplineParams;
|
FCollisionQueryParams SplineParams;
|
||||||
SplineParams.AddIgnoredActor(this);
|
SplineParams.AddIgnoredActor(this);
|
||||||
const FVector TraceEnd = RayOrigin + RayDir * 100000.0f;
|
const FVector TraceEnd = RayOrigin + RayDir * 100000.0f;
|
||||||
if (GetWorld()->LineTraceSingleByChannel(SplineHit, RayOrigin, TraceEnd, ECC_Visibility, SplineParams))
|
if (GetWorld()->LineTraceSingleByChannel(SplineHit, RayOrigin, TraceEnd, ECC_Camera, SplineParams))
|
||||||
{
|
{
|
||||||
if (Spline->IsCenterCube(SplineHit.GetComponent()))
|
if (Spline->IsCenterCube(SplineHit.GetComponent()))
|
||||||
{
|
{
|
||||||
@@ -670,7 +671,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
LineParams.AddIgnoredActor(this);
|
LineParams.AddIgnoredActor(this);
|
||||||
const FVector TraceEnd = RayOrigin + RayDir * 100000.0f;
|
const FVector TraceEnd = RayOrigin + RayDir * 100000.0f;
|
||||||
|
|
||||||
if (GetWorld()->LineTraceSingleByChannel(LineHit, RayOrigin, TraceEnd, ECC_Visibility, LineParams))
|
if (GetWorld()->LineTraceSingleByChannel(LineHit, RayOrigin, TraceEnd, ECC_Camera, LineParams))
|
||||||
{
|
{
|
||||||
for (APS_Editor_SplineActor* Spline : TubeCandidates)
|
for (APS_Editor_SplineActor* Spline : TubeCandidates)
|
||||||
{
|
{
|
||||||
@@ -730,7 +731,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
const FVector AddEnd = RayOrigin + RayDir * 100000.0f;
|
const FVector AddEnd = RayOrigin + RayDir * 100000.0f;
|
||||||
|
|
||||||
FVector ClickPos;
|
FVector ClickPos;
|
||||||
if (GetWorld()->LineTraceSingleByChannel(AddHit, RayOrigin, AddEnd, ECC_Visibility, AddParams))
|
if (GetWorld()->LineTraceSingleByChannel(AddHit, RayOrigin, AddEnd, ECC_Camera, AddParams))
|
||||||
{
|
{
|
||||||
ClickPos = AddHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
|
ClickPos = AddHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
|
||||||
}
|
}
|
||||||
@@ -1106,7 +1107,7 @@ void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, Params))
|
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Camera, Params))
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SnapToGround - PointLoc=%s, HitZ=%.2f, HitActor=%s, HitComp=%s"),
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SnapToGround - PointLoc=%s, HitZ=%.2f, HitActor=%s, HitComp=%s"),
|
||||||
*PointLoc.ToString(), Hit.ImpactPoint.Z,
|
*PointLoc.ToString(), Hit.ImpactPoint.Z,
|
||||||
@@ -1291,7 +1292,7 @@ void APS_Editor_Pawn::ComputeOrbitFocalPoint()
|
|||||||
FCollisionQueryParams Params;
|
FCollisionQueryParams Params;
|
||||||
Params.AddIgnoredActor(this);
|
Params.AddIgnoredActor(this);
|
||||||
|
|
||||||
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
|
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Camera, Params))
|
||||||
{
|
{
|
||||||
OrbitFocalPoint = Hit.ImpactPoint;
|
OrbitFocalPoint = Hit.ImpactPoint;
|
||||||
OrbitDistance = FVector::Dist(Start, OrbitFocalPoint);
|
OrbitDistance = FVector::Dist(Start, OrbitFocalPoint);
|
||||||
@@ -1379,10 +1380,17 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
|
|||||||
|
|
||||||
ActiveGizmoAxis = 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
|
// Store axis direction at drag start - used for entire drag duration
|
||||||
GizmoDragAxisDir = Gizmo->GetAxisDirection(Axis);
|
GizmoDragAxisDir = Gizmo->GetAxisDirection(Axis);
|
||||||
const FVector AxisDir = GizmoDragAxisDir;
|
const FVector AxisDir = GizmoDragAxisDir;
|
||||||
const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
|
|
||||||
|
|
||||||
FVector PlaneNormal;
|
FVector PlaneNormal;
|
||||||
if (Mode == EPS_Editor_TransformMode::Rotate)
|
if (Mode == EPS_Editor_TransformMode::Rotate)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include "PS_Editor_SplineActor.h"
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "PS_Editor_PointPlaceable.h"
|
#include "PS_Editor_PointPlaceable.h"
|
||||||
#include "Engine/LevelStreamingDynamic.h"
|
#include "Engine/LevelStreamingDynamic.h"
|
||||||
|
#include "Engine/PostProcessVolume.h"
|
||||||
|
#include "PS_Editor_CameraStart.h"
|
||||||
#include "PS_Editor_GameInstanceSubsystem.h"
|
#include "PS_Editor_GameInstanceSubsystem.h"
|
||||||
#include "GameFramework/GameModeBase.h"
|
#include "GameFramework/GameModeBase.h"
|
||||||
#include "GameFramework/GameStateBase.h"
|
#include "GameFramework/GameStateBase.h"
|
||||||
@@ -61,7 +63,17 @@ void APS_Editor_PlayerController::BeginPlay()
|
|||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Restoring from subsystem - BaseLevel='%s', Scenario='%s'"),
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Restoring from subsystem - BaseLevel='%s', Scenario='%s'"),
|
||||||
*Sub->PendingBaseLevel, *Sub->PendingScenarioName);
|
*Sub->PendingBaseLevel, *Sub->PendingScenarioName);
|
||||||
|
|
||||||
LoadBaseLevelAsSublevel(Sub->PendingBaseLevel);
|
// Resolve full map path from MasterCatalog
|
||||||
|
FString ResolvedMapPath;
|
||||||
|
for (const FPS_Editor_BaseLevelEntry& Entry : Master->BaseLevels)
|
||||||
|
{
|
||||||
|
if (Entry.DisplayName == Sub->PendingBaseLevel)
|
||||||
|
{
|
||||||
|
ResolvedMapPath = Entry.MapPath;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoadBaseLevelAsSublevel(Sub->PendingBaseLevel, ResolvedMapPath);
|
||||||
|
|
||||||
if (!Sub->PendingScenarioName.IsEmpty() && SpawnManager && SceneSerializer)
|
if (!Sub->PendingScenarioName.IsEmpty() && SpawnManager && SceneSerializer)
|
||||||
{
|
{
|
||||||
@@ -120,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)
|
void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
|
||||||
{
|
{
|
||||||
Super::OnPossess(InPawn);
|
Super::OnPossess(InPawn);
|
||||||
@@ -170,22 +192,25 @@ void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActo
|
|||||||
|
|
||||||
void APS_Editor_PlayerController::FinishPointPlacement()
|
void APS_Editor_PlayerController::FinishPointPlacement()
|
||||||
{
|
{
|
||||||
// Just exit the mode — actor is already tracked, all edits already have undo
|
// Exit the mode FIRST to prevent re-entrant ClearSelection loop
|
||||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
AActor* PlacementActor = ActivePlacementActor;
|
||||||
|
ActivePlacementActor = nullptr;
|
||||||
|
AppendInitialPoints.Empty();
|
||||||
|
EditorMode = EPS_Editor_EditorMode::Normal;
|
||||||
|
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(PlacementActor))
|
||||||
{
|
{
|
||||||
Spline->SetHandlesVisible(false);
|
Spline->SetHandlesVisible(false);
|
||||||
Spline->ClearHandleHighlight();
|
Spline->ClearHandleHighlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ActivePlacementActor && SelectionManager)
|
if (PlacementActor && SelectionManager)
|
||||||
{
|
{
|
||||||
SelectionManager->ClearSelection();
|
SelectionManager->ClearSelection();
|
||||||
SelectionManager->SelectActor(ActivePlacementActor);
|
SelectionManager->SelectActor(PlacementActor);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exited placement mode"));
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exited placement mode"));
|
||||||
|
|
||||||
ActivePlacementActor = nullptr;
|
|
||||||
bAppendingToActor = false;
|
bAppendingToActor = false;
|
||||||
AppendInitialPoints.Empty();
|
AppendInitialPoints.Empty();
|
||||||
EditorMode = EPS_Editor_EditorMode::Normal;
|
EditorMode = EPS_Editor_EditorMode::Normal;
|
||||||
@@ -197,7 +222,7 @@ void APS_Editor_PlayerController::CancelPointPlacement()
|
|||||||
FinishPointPlacement();
|
FinishPointPlacement();
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName)
|
void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath)
|
||||||
{
|
{
|
||||||
// Unload previous sublevel first
|
// Unload previous sublevel first
|
||||||
if (CurrentSublevel)
|
if (CurrentSublevel)
|
||||||
@@ -238,11 +263,14 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use MapPath if provided, otherwise fall back to LevelName (legacy behavior)
|
||||||
|
const FString& LevelToLoad = MapPath.IsEmpty() ? LevelName : MapPath;
|
||||||
|
|
||||||
// Load new sublevel
|
// Load new sublevel
|
||||||
bool bSuccess = false;
|
bool bSuccess = false;
|
||||||
CurrentSublevel = ULevelStreamingDynamic::LoadLevelInstance(
|
CurrentSublevel = ULevelStreamingDynamic::LoadLevelInstance(
|
||||||
GetWorld(),
|
GetWorld(),
|
||||||
LevelName,
|
LevelToLoad,
|
||||||
FVector::ZeroVector,
|
FVector::ZeroVector,
|
||||||
FRotator::ZeroRotator,
|
FRotator::ZeroRotator,
|
||||||
bSuccess);
|
bSuccess);
|
||||||
@@ -252,11 +280,56 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
|
|||||||
CurrentSublevel->SetShouldBeLoaded(true);
|
CurrentSublevel->SetShouldBeLoaded(true);
|
||||||
CurrentSublevel->SetShouldBeVisible(true);
|
CurrentSublevel->SetShouldBeVisible(true);
|
||||||
CurrentBaseLevel = LevelName;
|
CurrentBaseLevel = LevelName;
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Loaded sublevel '%s'"), *LevelName);
|
|
||||||
|
// Disable custom PP materials once sublevel actors are available
|
||||||
|
CurrentSublevel->OnLevelShown.AddUniqueDynamic(this, &APS_Editor_PlayerController::DisableSublevelPostProcessMaterials);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Loaded sublevel '%s' (path: '%s')"), *LevelName, *LevelToLoad);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load sublevel '%s'"), *LevelName);
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load sublevel '%s' (path: '%s')"), *LevelName, *LevelToLoad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
|
||||||
|
{
|
||||||
|
if (!CurrentSublevel) return;
|
||||||
|
|
||||||
|
ULevel* Level = CurrentSublevel->GetLoadedLevel();
|
||||||
|
if (!Level) return;
|
||||||
|
|
||||||
|
APS_Editor_CameraStart* CameraStart = nullptr;
|
||||||
|
|
||||||
|
for (AActor* Actor : Level->Actors)
|
||||||
|
{
|
||||||
|
if (!Actor) continue;
|
||||||
|
|
||||||
|
// Disable custom post-process materials
|
||||||
|
if (APostProcessVolume* PPVolume = Cast<APostProcessVolume>(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<APS_Editor_CameraStart>(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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +349,7 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
|
|||||||
|
|
||||||
const FVector End = WorldOrigin + WorldDirection * 100000.0f;
|
const FVector End = WorldOrigin + WorldDirection * 100000.0f;
|
||||||
FVector ClickWorldPos;
|
FVector ClickWorldPos;
|
||||||
if (GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Visibility, Params))
|
if (GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Camera, Params))
|
||||||
{
|
{
|
||||||
ClickWorldPos = Hit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
|
ClickWorldPos = Hit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
|
||||||
}
|
}
|
||||||
@@ -326,7 +399,7 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
|
|||||||
TubeParams.AddIgnoredActor(GetPawn());
|
TubeParams.AddIgnoredActor(GetPawn());
|
||||||
const FVector TubeEnd = WorldOrigin + WorldDirection * 100000.0f;
|
const FVector TubeEnd = WorldOrigin + WorldDirection * 100000.0f;
|
||||||
|
|
||||||
if (GetWorld()->LineTraceSingleByChannel(TubeHit, WorldOrigin, TubeEnd, ECC_Visibility, TubeParams))
|
if (GetWorld()->LineTraceSingleByChannel(TubeHit, WorldOrigin, TubeEnd, ECC_Camera, TubeParams))
|
||||||
{
|
{
|
||||||
if (Spline->IsSplineMesh(TubeHit.GetComponent()))
|
if (Spline->IsSplineMesh(TubeHit.GetComponent()))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,9 +47,13 @@ public:
|
|||||||
UPROPERTY(BlueprintReadWrite, Category = "PS Editor")
|
UPROPERTY(BlueprintReadWrite, Category = "PS Editor")
|
||||||
FString CurrentBaseLevel;
|
FString CurrentBaseLevel;
|
||||||
|
|
||||||
/** Load a base level as sublevel for visual context. Unloads previous sublevel. */
|
/** Load a base level as sublevel for visual context. Unloads previous sublevel.
|
||||||
|
* @param LevelName Display name stored in CurrentBaseLevel (used for scene filtering).
|
||||||
|
* @param MapPath Full package path for LoadLevelInstance (e.g. "/Game/Maps/Warehouse").
|
||||||
|
* If empty, LevelName is used as-is (legacy behavior).
|
||||||
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
void LoadBaseLevelAsSublevel(const FString& LevelName);
|
void LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath = TEXT(""));
|
||||||
|
|
||||||
// ---- Editor Mode (Normal / SplinePlacement) ----
|
// ---- Editor Mode (Normal / SplinePlacement) ----
|
||||||
|
|
||||||
@@ -85,6 +89,7 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
virtual void Tick(float DeltaTime) override;
|
||||||
virtual void OnPossess(APawn* InPawn) override;
|
virtual void OnPossess(APawn* InPawn) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -119,5 +124,9 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<ULevelStreamingDynamic> CurrentSublevel;
|
TObjectPtr<ULevelStreamingDynamic> CurrentSublevel;
|
||||||
|
|
||||||
|
/** Disable custom post-process materials in the loaded sublevel to avoid stencil conflicts. */
|
||||||
|
UFUNCTION()
|
||||||
|
void DisableSublevelPostProcessMaterials();
|
||||||
|
|
||||||
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ static void ConfigureGizmoComponent(UPrimitiveComponent* Comp)
|
|||||||
{
|
{
|
||||||
Comp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
Comp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
Comp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
Comp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||||
Comp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
Comp->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
|
||||||
Comp->SetCollisionObjectType(ECC_WorldDynamic);
|
Comp->SetCollisionObjectType(ECC_WorldDynamic);
|
||||||
Comp->SetCastShadow(false);
|
Comp->SetCastShadow(false);
|
||||||
Comp->SetRenderCustomDepth(false);
|
Comp->SetRenderCustomDepth(false);
|
||||||
@@ -142,6 +142,7 @@ void APS_Editor_Gizmo::BeginPlay()
|
|||||||
|
|
||||||
// Grey center cube for uniform scale
|
// Grey center cube for uniform scale
|
||||||
Mat_Center = MakeColorMat(FLinearColor(0.5f, 0.5f, 0.5f));
|
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);
|
ApplyMat(ScaleCube_Center, Mat_Center);
|
||||||
|
|
||||||
SetGizmoVisible(false);
|
SetGizmoVisible(false);
|
||||||
@@ -339,6 +340,13 @@ void APS_Editor_Gizmo::SetTransformMode(EPS_Editor_TransformMode NewMode)
|
|||||||
ScaleRoot->SetVisibility(bGizmoVisible && NewMode == EPS_Editor_TransformMode::Scale, true);
|
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 ----
|
// ---- Hit detection ----
|
||||||
|
|
||||||
EPS_Editor_GizmoAxis APS_Editor_Gizmo::GetAxisUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const
|
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;
|
default: break;
|
||||||
}
|
}
|
||||||
SetAxisMaterial(HighlightedAxis, OrigMat);
|
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;
|
HighlightedAxis = Axis;
|
||||||
|
|
||||||
if (Axis != EPS_Editor_GizmoAxis::None)
|
if (Axis != EPS_Editor_GizmoAxis::None)
|
||||||
|
{
|
||||||
|
// 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);
|
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_Y, LocalY, ArcY_Start, ArcY_End, ArcRadius, ArcWidth, ArcSegments);
|
||||||
CreateArcRing(Arc_Z, LocalZ, ArcZ_Start, ArcZ_End, ArcRadius, ArcWidth, ArcSegments);
|
CreateArcRing(Arc_Z, LocalZ, ArcZ_Start, ArcZ_End, ArcRadius, ArcWidth, ArcSegments);
|
||||||
|
|
||||||
// Re-apply materials
|
// Re-apply materials (respect YawOnly constraint)
|
||||||
if (Mat_X) Arc_X->SetMaterial(0, Mat_X);
|
if (Mat_X) Arc_X->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_X);
|
||||||
if (Mat_Y) Arc_Y->SetMaterial(0, Mat_Y);
|
if (Mat_Y) Arc_Y->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_Y);
|
||||||
if (Mat_Z) Arc_Z->SetMaterial(0, Mat_Z);
|
if (Mat_Z) Arc_Z->SetMaterial(0, Mat_Z);
|
||||||
|
|
||||||
if (HighlightedAxis != EPS_Editor_GizmoAxis::None)
|
if (HighlightedAxis != EPS_Editor_GizmoAxis::None)
|
||||||
@@ -512,9 +543,9 @@ void APS_Editor_Gizmo::UpdateArcFacing()
|
|||||||
PositionGuideLine(GuideLine_B, LocalY * NewOctant.Y);
|
PositionGuideLine(GuideLine_B, LocalY * NewOctant.Y);
|
||||||
PositionGuideLine(GuideLine_C, LocalZ * NewOctant.Z);
|
PositionGuideLine(GuideLine_C, LocalZ * NewOctant.Z);
|
||||||
|
|
||||||
// Re-apply materials (mesh sections are recreated)
|
// Re-apply materials (mesh sections are recreated, respect YawOnly)
|
||||||
if (Mat_X) Arc_X->SetMaterial(0, Mat_X);
|
if (Mat_X) Arc_X->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_X);
|
||||||
if (Mat_Y) Arc_Y->SetMaterial(0, Mat_Y);
|
if (Mat_Y) Arc_Y->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_Y);
|
||||||
if (Mat_Z) Arc_Z->SetMaterial(0, Mat_Z);
|
if (Mat_Z) Arc_Z->SetMaterial(0, Mat_Z);
|
||||||
|
|
||||||
// Restore highlight if needed
|
// Restore highlight if needed
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public:
|
|||||||
void SetHighlightedAxis(EPS_Editor_GizmoAxis Axis);
|
void SetHighlightedAxis(EPS_Editor_GizmoAxis Axis);
|
||||||
FVector GetAxisDirection(EPS_Editor_GizmoAxis Axis) const;
|
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). */
|
/** Set the orientation for local space mode (matches selected actor's rotation). */
|
||||||
void SetLocalOrientation(FRotator ActorRotation);
|
void SetLocalOrientation(FRotator ActorRotation);
|
||||||
|
|
||||||
@@ -116,11 +119,18 @@ private:
|
|||||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Highlight;
|
TObjectPtr<UMaterialInstanceDynamic> Mat_Highlight;
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Center;
|
TObjectPtr<UMaterialInstanceDynamic> Mat_Center;
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> Mat_Disabled;
|
||||||
|
|
||||||
// ---- State ----
|
// ---- State ----
|
||||||
EPS_Editor_TransformMode CurrentMode = EPS_Editor_TransformMode::Translate;
|
EPS_Editor_TransformMode CurrentMode = EPS_Editor_TransformMode::Translate;
|
||||||
EPS_Editor_GizmoAxis HighlightedAxis = EPS_Editor_GizmoAxis::None;
|
EPS_Editor_GizmoAxis HighlightedAxis = EPS_Editor_GizmoAxis::None;
|
||||||
bool bGizmoVisible = false;
|
bool bGizmoVisible = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool bIsYawOnly = false;
|
||||||
|
bool bIsUniformScaleOnly = false;
|
||||||
|
private:
|
||||||
float DesiredScreenSize = 0.25f;
|
float DesiredScreenSize = 0.25f;
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_EditableComponent.h"
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_SpawnableComponent.h"
|
||||||
#include "PS_Editor_SplineActor.h"
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
@@ -20,11 +21,11 @@ static TMap<FString, FString> ExportEditableProps(AActor* Actor)
|
|||||||
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
||||||
if (!EditComp) return Result;
|
if (!EditComp) return Result;
|
||||||
|
|
||||||
for (const FName& Path : EditComp->EditableProperties)
|
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||||
{
|
{
|
||||||
FString CompName, PropName;
|
FString CompName, PropName;
|
||||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName);
|
||||||
const FString Key = Path.ToString();
|
const FString Key = Entry.Path.ToString();
|
||||||
|
|
||||||
UObject* Target = CompName.IsEmpty() ? static_cast<UObject*>(Actor) : nullptr;
|
UObject* Target = CompName.IsEmpty() ? static_cast<UObject*>(Actor) : nullptr;
|
||||||
if (!Target)
|
if (!Target)
|
||||||
@@ -56,11 +57,11 @@ static void ImportEditableProps(AActor* Actor, const TMap<FString, FString>& Pro
|
|||||||
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
||||||
if (!EditComp) return;
|
if (!EditComp) return;
|
||||||
|
|
||||||
for (const FName& Path : EditComp->EditableProperties)
|
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||||
{
|
{
|
||||||
FString CompName, PropName;
|
FString CompName, PropName;
|
||||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName);
|
||||||
const FString Key = Path.ToString();
|
const FString Key = Entry.Path.ToString();
|
||||||
|
|
||||||
const FString* ValStr = Properties.Find(Key);
|
const FString* ValStr = Properties.Find(Key);
|
||||||
if (!ValStr) continue;
|
if (!ValStr) continue;
|
||||||
@@ -127,7 +128,7 @@ void UPS_Editor_SelectionManager::HandleClickAtScreenPosition(FVector2D ScreenPo
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FVector End = WorldOrigin + WorldDirection * 100000.0f;
|
const FVector End = WorldOrigin + WorldDirection * 100000.0f;
|
||||||
const bool bHit = PC->GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Visibility, Params);
|
const bool bHit = PC->GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Camera, Params);
|
||||||
|
|
||||||
if (bHit && Hit.GetActor())
|
if (bHit && Hit.GetActor())
|
||||||
{
|
{
|
||||||
@@ -456,7 +457,13 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
|||||||
|
|
||||||
FVector Origin, Extent;
|
FVector Origin, Extent;
|
||||||
Actor->GetActorBounds(false, 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<UPS_Editor_SpawnableComponent>())
|
||||||
|
{
|
||||||
|
BottomOffset += SpawnComp->GroundOffset;
|
||||||
|
}
|
||||||
|
|
||||||
const FVector Start = Actor->GetActorLocation();
|
const FVector Start = Actor->GetActorLocation();
|
||||||
const FVector End = Start - FVector::UpVector * 100000.0f;
|
const FVector End = Start - FVector::UpVector * 100000.0f;
|
||||||
@@ -467,11 +474,21 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
|||||||
Params.AddIgnoredActor(PC->GetPawn());
|
Params.AddIgnoredActor(PC->GetPawn());
|
||||||
if (Gizmo) Params.AddIgnoredActor(Gizmo);
|
if (Gizmo) Params.AddIgnoredActor(Gizmo);
|
||||||
|
|
||||||
if (Actor->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
|
if (Actor->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Camera, Params))
|
||||||
{
|
{
|
||||||
FVector NewLocation = Hit.ImpactPoint;
|
FVector NewLocation = Hit.ImpactPoint;
|
||||||
NewLocation.Z += BottomOffset;
|
NewLocation.Z += BottomOffset;
|
||||||
Actor->SetActorLocation(NewLocation);
|
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());
|
UndoAction->NewTransforms.Add(Actor->GetActorTransform());
|
||||||
@@ -514,11 +531,21 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
|
|||||||
|
|
||||||
Gizmo->SetGizmoActive(true, Pivot);
|
Gizmo->SetGizmoActive(true, Pivot);
|
||||||
|
|
||||||
// Update gizmo orientation
|
// Update gizmo orientation and yaw-only constraint
|
||||||
if (AActor* Actor = SelectedActors[0].Get())
|
if (AActor* Actor = SelectedActors[0].Get())
|
||||||
{
|
{
|
||||||
bool bUseLocal = (CurrentTransformMode == EPS_Editor_TransformMode::Scale) || (CurrentSpace == EPS_Editor_Space::Local);
|
bool bUseLocal = (CurrentTransformMode == EPS_Editor_TransformMode::Scale) || (CurrentSpace == EPS_Editor_Space::Local);
|
||||||
Gizmo->SetLocalOrientation(bUseLocal ? Actor->GetActorRotation() : FRotator::ZeroRotator);
|
Gizmo->SetLocalOrientation(bUseLocal ? Actor->GetActorRotation() : FRotator::ZeroRotator);
|
||||||
|
|
||||||
|
bool bYaw = false;
|
||||||
|
bool bUniformScale = false;
|
||||||
|
if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
|
||||||
|
{
|
||||||
|
bYaw = SpawnComp->bYawOnly;
|
||||||
|
bUniformScale = SpawnComp->bUniformScaleOnly;
|
||||||
|
}
|
||||||
|
Gizmo->SetYawOnly(bYaw);
|
||||||
|
Gizmo->bIsUniformScaleOnly = bUniformScale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "PS_Editor_SceneLoader.h"
|
#include "PS_Editor_SceneLoader.h"
|
||||||
#include "PS_Editor_SceneData.h"
|
#include "PS_Editor_SceneData.h"
|
||||||
#include "PS_Editor_EditableComponent.h"
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_EditableInterface.h"
|
||||||
#include "PS_Editor_SpawnableComponent.h"
|
#include "PS_Editor_SpawnableComponent.h"
|
||||||
#include "PS_Editor_SplineActor.h"
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "PS_Editor_PointPlaceable.h"
|
#include "PS_Editor_PointPlaceable.h"
|
||||||
@@ -147,6 +148,12 @@ AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Edit
|
|||||||
Spline->ImportFromCustomProperties(ActorData.CustomProperties);
|
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;
|
return SpawnedActor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,11 +162,11 @@ void UPS_Editor_SceneLoader::ApplyCustomProperties(AActor* Actor, const TMap<FSt
|
|||||||
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
||||||
if (!EditComp) return;
|
if (!EditComp) return;
|
||||||
|
|
||||||
for (const FName& Path : EditComp->EditableProperties)
|
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||||
{
|
{
|
||||||
FString CompName, PropName;
|
FString CompName, PropName;
|
||||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName);
|
||||||
const FString Key = Path.ToString();
|
const FString Key = Entry.Path.ToString();
|
||||||
|
|
||||||
const FString* ValueStr = CustomProperties.Find(Key);
|
const FString* ValueStr = CustomProperties.Find(Key);
|
||||||
if (!ValueStr) continue;
|
if (!ValueStr) continue;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_EditableComponent.h"
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_EditableInterface.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_SplineActor.h"
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "JsonObjectConverter.h"
|
#include "JsonObjectConverter.h"
|
||||||
@@ -55,11 +56,11 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
|||||||
// Export custom editable properties
|
// Export custom editable properties
|
||||||
if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
||||||
{
|
{
|
||||||
for (const FName& Path : EditComp->EditableProperties)
|
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||||
{
|
{
|
||||||
FString CompName, PropName;
|
FString CompName, PropName;
|
||||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName);
|
||||||
const FString Key = Path.ToString();
|
const FString Key = Entry.Path.ToString();
|
||||||
|
|
||||||
UObject* Target = CompName.IsEmpty() ? static_cast<UObject*>(Actor) : nullptr;
|
UObject* Target = CompName.IsEmpty() ? static_cast<UObject*>(Actor) : nullptr;
|
||||||
if (!Target)
|
if (!Target)
|
||||||
@@ -171,11 +172,11 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
{
|
{
|
||||||
if (UPS_Editor_EditableComponent* EditComp = SpawnedActor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
if (UPS_Editor_EditableComponent* EditComp = SpawnedActor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
||||||
{
|
{
|
||||||
for (const FName& Path : EditComp->EditableProperties)
|
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||||
{
|
{
|
||||||
FString CompName, PropName;
|
FString CompName, PropName;
|
||||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName);
|
||||||
const FString Key = Path.ToString();
|
const FString Key = Entry.Path.ToString();
|
||||||
|
|
||||||
const FString* ValueStr = ActorData.CustomProperties.Find(Key);
|
const FString* ValueStr = ActorData.CustomProperties.Find(Key);
|
||||||
if (!ValueStr) continue;
|
if (!ValueStr) continue;
|
||||||
@@ -217,6 +218,13 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
Spline->ImportFromCustomProperties(ActorData.CustomProperties);
|
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++;
|
SpawnedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,9 +166,21 @@ void UPS_Editor_EditableComponent::PostEditChangeProperty(FPropertyChangedEvent&
|
|||||||
FullPath = FName(SelComp + TEXT(".") + AddProperty.ToString());
|
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());
|
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
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,39 @@
|
|||||||
#include "Components/ActorComponent.h"
|
#include "Components/ActorComponent.h"
|
||||||
#include "PS_Editor_EditableComponent.generated.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.
|
* Marker component that declares which properties are editable in the PS_Editor runtime editor.
|
||||||
* Separate from SpawnableComponent (which handles catalogue metadata).
|
* Separate from SpawnableComponent (which handles catalogue metadata).
|
||||||
@@ -31,19 +64,16 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Properties exposed in the runtime editor.
|
* Properties exposed in the runtime editor.
|
||||||
* Format: "ComponentName.PropertyName" or just "PropertyName" for actor-level.
|
* Each entry has a path, optional display name, and optional respawn flag.
|
||||||
* Populated via the dropdowns above, or editable manually.
|
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editable Properties")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editable Properties")
|
||||||
TArray<FName> EditableProperties;
|
TArray<FPS_Editor_EditablePropertyEntry> EditableProperties;
|
||||||
|
|
||||||
/**
|
/** Returns true if the given property path requires a reinit call. */
|
||||||
* Optional display name overrides for the runtime editor UI.
|
bool RequiresReinit(const FName& PropertyPath) const;
|
||||||
* Key = property path (same as in EditableProperties), Value = label shown in UI.
|
|
||||||
* If not set, the property name is used as label.
|
/** Returns true if the given property path requires a full respawn. */
|
||||||
*/
|
bool RequiresRespawn(const FName& PropertyPath) const;
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editable Properties")
|
|
||||||
TMap<FName, FString> DisplayNames;
|
|
||||||
|
|
||||||
/** Lists available component names on the owning actor. */
|
/** Lists available component names on the owning actor. */
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -85,11 +85,18 @@ void UPS_Editor_SpawnCatalog::CaptureAllThumbnails()
|
|||||||
|
|
||||||
uint8* MipData = Texture->Source.LockMip(0);
|
uint8* MipData = Texture->Source.LockMip(0);
|
||||||
FMemory::Memcpy(MipData, PixelData.GetData(), PixelData.Num());
|
FMemory::Memcpy(MipData, PixelData.GetData(), PixelData.Num());
|
||||||
|
// Force alpha to 255 — UE thumbnails may have zeroed alpha
|
||||||
|
for (int32 Px = 0; Px < Width * Height; ++Px)
|
||||||
|
{
|
||||||
|
MipData[Px * 4 + 3] = 255;
|
||||||
|
}
|
||||||
Texture->Source.UnlockMip(0);
|
Texture->Source.UnlockMip(0);
|
||||||
|
|
||||||
Texture->SRGB = true;
|
Texture->SRGB = true;
|
||||||
Texture->CompressionSettings = TC_Default;
|
Texture->CompressionSettings = TC_Default;
|
||||||
Texture->MipGenSettings = TMGS_NoMipmaps;
|
Texture->MipGenSettings = TMGS_NoMipmaps;
|
||||||
|
Texture->NeverStream = true;
|
||||||
|
Texture->LODGroup = TEXTUREGROUP_UI;
|
||||||
Texture->PostEditChange();
|
Texture->PostEditChange();
|
||||||
Texture->UpdateResource();
|
Texture->UpdateResource();
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,28 @@ public:
|
|||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base level entry: display name + full map path.
|
||||||
|
* The MapPath is used for OpenLevel and sublevel loading.
|
||||||
|
* Example: DisplayName="Warehouse", MapPath="/Game/Maps/Warehouse"
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FPS_Editor_BaseLevelEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
/** Display name shown in the editor combo box and used for scene filtering. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||||
|
FString DisplayName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full map path used for OpenLevel and sublevel loading.
|
||||||
|
* Example: "/Game/Maps/Warehouse"
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||||
|
FString MapPath;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master catalog that groups multiple SpawnCatalogs.
|
* Master catalog that groups multiple SpawnCatalogs.
|
||||||
* Assign this to the PlayerController. Each sub-catalog can represent a domain
|
* Assign this to the PlayerController. Each sub-catalog can represent a domain
|
||||||
@@ -60,10 +82,10 @@ public:
|
|||||||
TArray<TObjectPtr<UPS_Editor_SpawnCatalog>> Catalogs;
|
TArray<TObjectPtr<UPS_Editor_SpawnCatalog>> Catalogs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of base level names available for scene editing.
|
* List of base levels available for scene editing.
|
||||||
* Used to filter saved scenes by level. Auto-matched by stripping "_Editor" from the current map name.
|
* DisplayName is used for UI display and scene filtering.
|
||||||
* Example: "Warehouse", "Office", "Parking"
|
* MapPath is the full package path for OpenLevel (e.g. "/Game/Maps/Warehouse").
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||||
TArray<FString> BaseLevels;
|
TArray<FPS_Editor_BaseLevelEntry> BaseLevels;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_SpawnableComponent.h"
|
#include "PS_Editor_SpawnableComponent.h"
|
||||||
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_EditableInterface.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
@@ -67,6 +69,11 @@ void UPS_Editor_SpawnManager::AddCatalog(UPS_Editor_SpawnCatalog* InCatalog)
|
|||||||
Resolved.Category = SpawnComp ? SpawnComp->Category : TEXT("Default");
|
Resolved.Category = SpawnComp ? SpawnComp->Category : TEXT("Default");
|
||||||
Resolved.Thumbnail = SpawnComp ? SpawnComp->Thumbnail : nullptr;
|
Resolved.Thumbnail = SpawnComp ? SpawnComp->Thumbnail : nullptr;
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved '%s' - SpawnComp=%s, Thumbnail=%s"),
|
||||||
|
*Resolved.DisplayName,
|
||||||
|
SpawnComp ? TEXT("found") : TEXT("null"),
|
||||||
|
Resolved.Thumbnail ? *Resolved.Thumbnail->GetPathName() : TEXT("null"));
|
||||||
|
|
||||||
ResolvedEntries.Add(Resolved);
|
ResolvedEntries.Add(Resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +136,36 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector
|
|||||||
// Track spawned actor
|
// Track spawned actor
|
||||||
SpawnedActors.Add(SpawnedActor);
|
SpawnedActors.Add(SpawnedActor);
|
||||||
|
|
||||||
|
// Auto-snap to ground if configured
|
||||||
|
UPS_Editor_SpawnableComponent* SpawnComp = SpawnedActor->FindComponentByClass<UPS_Editor_SpawnableComponent>();
|
||||||
|
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());
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spawned %s at %s"), *Entry.DisplayName, *WorldLocation.ToString());
|
||||||
return SpawnedActor;
|
return SpawnedActor;
|
||||||
}
|
}
|
||||||
@@ -213,6 +250,119 @@ bool UPS_Editor_SpawnManager::IsActorSpawned(AActor* Actor) const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AActor* UPS_Editor_SpawnManager::RespawnActorWithProperties(AActor* OldActor, const TMap<FString, FString>& 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<AActor>& 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<AActor>(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<UActorComponent*> 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<FProperty>(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<AActor>& 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<AActor>& 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<FString> UPS_Editor_SpawnManager::GetCategories() const
|
TArray<FString> UPS_Editor_SpawnManager::GetCategories() const
|
||||||
{
|
{
|
||||||
TArray<FString> Categories;
|
TArray<FString> Categories;
|
||||||
|
|||||||
@@ -69,6 +69,21 @@ public:
|
|||||||
/** Returns true if this actor was spawned/tracked by the editor (catalogue, duplicate, paste, load). */
|
/** Returns true if this actor was spawned/tracked by the editor (catalogue, duplicate, paste, load). */
|
||||||
bool IsActorSpawned(AActor* Actor) const;
|
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<FString, FString>& PropertyValues);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TWeakObjectPtr<APlayerController> OwnerPC;
|
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||||
|
|||||||
@@ -27,4 +27,24 @@ public:
|
|||||||
/** Optional thumbnail texture shown in the catalogue. Use "Capture All Thumbnails" on the SpawnCatalog DataAsset to auto-generate. */
|
/** Optional thumbnail texture shown in the catalogue. Use "Capture All Thumbnails" on the SpawnCatalog DataAsset to auto-generate. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
|
||||||
TObjectPtr<UTexture2D> Thumbnail;
|
TObjectPtr<UTexture2D> 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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,11 +111,18 @@ FReply FPS_Editor_SpawnableComponentCustomization::OnCaptureThumbnailClicked()
|
|||||||
|
|
||||||
uint8* MipData = Texture->Source.LockMip(0);
|
uint8* MipData = Texture->Source.LockMip(0);
|
||||||
FMemory::Memcpy(MipData, PixelData.GetData(), PixelData.Num());
|
FMemory::Memcpy(MipData, PixelData.GetData(), PixelData.Num());
|
||||||
|
// Force alpha to 255 — UE thumbnails may have zeroed alpha
|
||||||
|
for (int32 Px = 0; Px < Width * Height; ++Px)
|
||||||
|
{
|
||||||
|
MipData[Px * 4 + 3] = 255;
|
||||||
|
}
|
||||||
Texture->Source.UnlockMip(0);
|
Texture->Source.UnlockMip(0);
|
||||||
|
|
||||||
Texture->SRGB = true;
|
Texture->SRGB = true;
|
||||||
Texture->CompressionSettings = TC_Default;
|
Texture->CompressionSettings = TC_Default;
|
||||||
Texture->MipGenSettings = TMGS_NoMipmaps;
|
Texture->MipGenSettings = TMGS_NoMipmaps;
|
||||||
|
Texture->NeverStream = true;
|
||||||
|
Texture->LODGroup = TEXTUREGROUP_UI;
|
||||||
Texture->PostEditChange();
|
Texture->PostEditChange();
|
||||||
Texture->UpdateResource();
|
Texture->UpdateResource();
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ APS_Editor_SplineActor::APS_Editor_SplineActor()
|
|||||||
SplineMeshComp->SetupAttachment(Root);
|
SplineMeshComp->SetupAttachment(Root);
|
||||||
SplineMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
SplineMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
SplineMeshComp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
SplineMeshComp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||||
SplineMeshComp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
SplineMeshComp->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
|
||||||
SplineMeshComp->bUseComplexAsSimpleCollision = true;
|
SplineMeshComp->bUseComplexAsSimpleCollision = true;
|
||||||
SplineMeshComp->SetCastShadow(false);
|
SplineMeshComp->SetCastShadow(false);
|
||||||
|
|
||||||
@@ -42,9 +42,9 @@ APS_Editor_SplineActor::APS_Editor_SplineActor()
|
|||||||
|
|
||||||
// Editable: exposes SplineColor and SplineTag in the runtime property panel
|
// Editable: exposes SplineColor and SplineTag in the runtime property panel
|
||||||
EditableComp = CreateDefaultSubobject<UPS_Editor_EditableComponent>(TEXT("PS_Editor_Editable"));
|
EditableComp = CreateDefaultSubobject<UPS_Editor_EditableComponent>(TEXT("PS_Editor_Editable"));
|
||||||
EditableComp->EditableProperties.Add(FName(TEXT("SplineColor")));
|
EditableComp->EditableProperties.Add({FName(TEXT("SplineColor"))});
|
||||||
EditableComp->EditableProperties.Add(FName(TEXT("SplineSelectedColor")));
|
EditableComp->EditableProperties.Add({FName(TEXT("SplineSelectedColor"))});
|
||||||
EditableComp->EditableProperties.Add(FName(TEXT("SplineTag")));
|
EditableComp->EditableProperties.Add({FName(TEXT("SplineTag"))});
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_SplineActor::BeginPlay()
|
void APS_Editor_SplineActor::BeginPlay()
|
||||||
@@ -101,7 +101,7 @@ void APS_Editor_SplineActor::BeginPlay()
|
|||||||
CenterCube->SetWorldScale3D(FVector(CenterCubeScale));
|
CenterCube->SetWorldScale3D(FVector(CenterCubeScale));
|
||||||
CenterCube->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
CenterCube->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
CenterCube->SetCollisionResponseToAllChannels(ECR_Ignore);
|
CenterCube->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||||
CenterCube->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
CenterCube->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
|
||||||
CenterCube->SetCastShadow(false);
|
CenterCube->SetCastShadow(false);
|
||||||
if (CenterCubeMat)
|
if (CenterCubeMat)
|
||||||
{
|
{
|
||||||
@@ -316,7 +316,7 @@ UStaticMeshComponent* APS_Editor_SplineActor::CreatePointHandle(const FVector& W
|
|||||||
// Collision for raycast
|
// Collision for raycast
|
||||||
Handle->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
Handle->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
Handle->SetCollisionResponseToAllChannels(ECR_Ignore);
|
Handle->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||||
Handle->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
Handle->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
|
||||||
|
|
||||||
Handle->SetCastShadow(false);
|
Handle->SetCastShadow(false);
|
||||||
Handle->SetRenderInDepthPass(false);
|
Handle->SetRenderInDepthPass(false);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_EditableComponent.h"
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_EditableInterface.h"
|
||||||
#include "PS_Editor_SpawnableComponent.h"
|
#include "PS_Editor_SpawnableComponent.h"
|
||||||
#include "PS_Editor_SplineActor.h"
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "PS_Editor_Pawn.h"
|
#include "PS_Editor_Pawn.h"
|
||||||
@@ -607,6 +608,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
||||||
{
|
{
|
||||||
Sub->SetPendingScenario(SceneName, BaseLevel);
|
Sub->SetPendingScenario(SceneName, BaseLevel);
|
||||||
|
Sub->bIsPlayMode = true;
|
||||||
// Remember editor map for ReturnToEditor
|
// Remember editor map for ReturnToEditor
|
||||||
if (Sub->EditorMapName.IsEmpty())
|
if (Sub->EditorMapName.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -617,6 +619,12 @@ TSharedRef<SWidget> 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
|
// Unload sublevel before OpenLevel to avoid name conflict
|
||||||
EditorPC->LoadBaseLevelAsSublevel(TEXT("None"));
|
EditorPC->LoadBaseLevelAsSublevel(TEXT("None"));
|
||||||
|
|
||||||
@@ -624,8 +632,16 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
EditorPC->bShowMouseCursor = false;
|
EditorPC->bShowMouseCursor = false;
|
||||||
EditorPC->SetInputMode(FInputModeGameOnly());
|
EditorPC->SetInputMode(FInputModeGameOnly());
|
||||||
|
|
||||||
// Use full path to avoid confusion with sublevel instances
|
// Use full path from MasterCatalog, fallback to /Game/{name}
|
||||||
FString MapPath = FString::Printf(TEXT("/Game/%s"), *BaseLevel);
|
FString MapPath;
|
||||||
|
if (FString* Found = BaseLevelPathMap.Find(BaseLevel))
|
||||||
|
{
|
||||||
|
MapPath = *Found;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MapPath = FString::Printf(TEXT("/Game/%s"), *BaseLevel);
|
||||||
|
}
|
||||||
UGameplayStatics::OpenLevel(EditorPC, FName(*MapPath), true, TEXT(""));
|
UGameplayStatics::OpenLevel(EditorPC, FName(*MapPath), true, TEXT(""));
|
||||||
return FReply::Handled();
|
return FReply::Handled();
|
||||||
})
|
})
|
||||||
@@ -676,7 +692,12 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
{
|
{
|
||||||
if (EditorPC->CurrentBaseLevel != SceneLevelName)
|
if (EditorPC->CurrentBaseLevel != SceneLevelName)
|
||||||
{
|
{
|
||||||
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName);
|
FString SceneMapPath = SceneLevelName;
|
||||||
|
if (FString* Found = BaseLevelPathMap.Find(SceneLevelName))
|
||||||
|
{
|
||||||
|
SceneMapPath = *Found;
|
||||||
|
}
|
||||||
|
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath);
|
||||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||||
for (const TSharedPtr<FString>& Opt : BaseLevelOptions)
|
for (const TSharedPtr<FString>& Opt : BaseLevelOptions)
|
||||||
{
|
{
|
||||||
@@ -726,6 +747,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
[
|
[
|
||||||
SAssignNew(BaseLevelCombo, SComboBox<TSharedPtr<FString>>)
|
SAssignNew(BaseLevelCombo, SComboBox<TSharedPtr<FString>>)
|
||||||
.OptionsSource(&BaseLevelOptions)
|
.OptionsSource(&BaseLevelOptions)
|
||||||
|
.Method(EPopupMethod::UseCurrentWindow)
|
||||||
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> Selected, ESelectInfo::Type SelectInfo)
|
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> Selected, ESelectInfo::Type SelectInfo)
|
||||||
{
|
{
|
||||||
if (!Selected.IsValid()) return;
|
if (!Selected.IsValid()) return;
|
||||||
@@ -748,7 +770,12 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
SaveNameField->SetText(FText::GetEmpty());
|
SaveNameField->SetText(FText::GetEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
EditorPC->LoadBaseLevelAsSublevel(*Selected);
|
FString SublevelPath = *Selected;
|
||||||
|
if (FString* Found = BaseLevelPathMap.Find(*Selected))
|
||||||
|
{
|
||||||
|
SublevelPath = *Found;
|
||||||
|
}
|
||||||
|
EditorPC->LoadBaseLevelAsSublevel(*Selected, SublevelPath);
|
||||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||||
|
|
||||||
if (BaseLevelComboText.IsValid())
|
if (BaseLevelComboText.IsValid())
|
||||||
@@ -815,6 +842,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSpawnCatalog()
|
|||||||
.MaxDesiredHeight(400.0f)
|
.MaxDesiredHeight(400.0f)
|
||||||
[
|
[
|
||||||
SNew(SScrollBox)
|
SNew(SScrollBox)
|
||||||
|
.ConsumeMouseWheel(EConsumeMouseWheel::Always)
|
||||||
+ SScrollBox::Slot()
|
+ SScrollBox::Slot()
|
||||||
[
|
[
|
||||||
CatalogList
|
CatalogList
|
||||||
@@ -834,9 +862,13 @@ void UPS_Editor_MainWidget::NativeConstruct()
|
|||||||
BaseLevelOptions.Add(MakeShared<FString>(TEXT("None")));
|
BaseLevelOptions.Add(MakeShared<FString>(TEXT("None")));
|
||||||
if (UPS_Editor_MasterCatalog* Master = EditorPC->MasterCatalogAsset)
|
if (UPS_Editor_MasterCatalog* Master = EditorPC->MasterCatalogAsset)
|
||||||
{
|
{
|
||||||
for (const FString& Level : Master->BaseLevels)
|
for (const FPS_Editor_BaseLevelEntry& Entry : Master->BaseLevels)
|
||||||
{
|
{
|
||||||
BaseLevelOptions.Add(MakeShared<FString>(Level));
|
BaseLevelOptions.Add(MakeShared<FString>(Entry.DisplayName));
|
||||||
|
if (!Entry.MapPath.IsEmpty())
|
||||||
|
{
|
||||||
|
BaseLevelPathMap.Add(Entry.DisplayName, Entry.MapPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -915,7 +947,12 @@ void UPS_Editor_MainWidget::PopulateSceneList()
|
|||||||
{
|
{
|
||||||
if (EditorPC->CurrentBaseLevel != SceneLevelName)
|
if (EditorPC->CurrentBaseLevel != SceneLevelName)
|
||||||
{
|
{
|
||||||
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName);
|
FString SceneMapPath = SceneLevelName;
|
||||||
|
if (FString* Found = BaseLevelPathMap.Find(SceneLevelName))
|
||||||
|
{
|
||||||
|
SceneMapPath = *Found;
|
||||||
|
}
|
||||||
|
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath);
|
||||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||||
|
|
||||||
for (const TSharedPtr<FString>& Opt : BaseLevelOptions)
|
for (const TSharedPtr<FString>& Opt : BaseLevelOptions)
|
||||||
@@ -1370,6 +1407,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildOutliner()
|
|||||||
+ SVerticalBox::Slot().FillHeight(1.0f).Padding(0.0f, 4.0f, 0.0f, 0.0f)
|
+ SVerticalBox::Slot().FillHeight(1.0f).Padding(0.0f, 4.0f, 0.0f, 0.0f)
|
||||||
[
|
[
|
||||||
SNew(SScrollBox)
|
SNew(SScrollBox)
|
||||||
|
.ConsumeMouseWheel(EConsumeMouseWheel::Always)
|
||||||
+ SScrollBox::Slot()
|
+ SScrollBox::Slot()
|
||||||
[
|
[
|
||||||
SAssignNew(OutlinerListContainer, SVerticalBox)
|
SAssignNew(OutlinerListContainer, SVerticalBox)
|
||||||
@@ -1528,12 +1566,12 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
|||||||
|
|
||||||
for (int32 i = 0; i < EditComp->EditableProperties.Num(); i++)
|
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;
|
FPS_Editor_PropertyRowWidgets Row;
|
||||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, Row.ComponentName, Row.PropertyName);
|
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, Row.ComponentName, Row.PropertyName);
|
||||||
Row.DisplayName = EditComp->GetDisplayName(Path);
|
Row.DisplayName = EditComp->GetDisplayName(Entry.Path);
|
||||||
Row.Key = Path.ToString();
|
Row.Key = Entry.Path.ToString();
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolving [%s] comp='%s' prop='%s'"), *Row.Key, *Row.ComponentName, *Row.PropertyName);
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolving [%s] comp='%s' prop='%s'"), *Row.Key, *Row.ComponentName, *Row.PropertyName);
|
||||||
|
|
||||||
@@ -1573,7 +1611,61 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
|||||||
// Value widget depends on property type
|
// Value widget depends on property type
|
||||||
TSharedRef<SWidget> ValueWidget = SNullWidget::NullWidget;
|
TSharedRef<SWidget> ValueWidget = SNullWidget::NullWidget;
|
||||||
|
|
||||||
if (CastField<FBoolProperty>(Row.CachedProperty))
|
// Detect enum type (FEnumProperty for C++ enum class, FByteProperty with UEnum for legacy)
|
||||||
|
UEnum* FoundEnum = nullptr;
|
||||||
|
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
FoundEnum = EnumProp->GetEnum();
|
||||||
|
}
|
||||||
|
else if (FByteProperty* ByteProp = CastField<FByteProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
FoundEnum = ByteProp->GetIntPropertyEnum();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FoundEnum)
|
||||||
|
{
|
||||||
|
Row.CachedEnum = FoundEnum;
|
||||||
|
Row.EnumOptions = MakeShared<TArray<TSharedPtr<FString>>>();
|
||||||
|
for (int32 Idx = 0; Idx < FoundEnum->NumEnums() - 1; ++Idx) // -1 to skip _MAX
|
||||||
|
{
|
||||||
|
FString Name = FoundEnum->GetDisplayNameTextByIndex(Idx).ToString();
|
||||||
|
Row.EnumOptions->Add(MakeShared<FString>(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<TSharedPtr<FString>>)
|
||||||
|
.OptionsSource(Row.EnumOptions.Get())
|
||||||
|
.Method(EPopupMethod::UseCurrentWindow)
|
||||||
|
.IsFocusable(true)
|
||||||
|
.OnSelectionChanged_Lambda([this, RowIndex](TSharedPtr<FString> 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<FString> Item) -> TSharedRef<SWidget>
|
||||||
|
{
|
||||||
|
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<FBoolProperty>(Row.CachedProperty))
|
||||||
{
|
{
|
||||||
ValueWidget = SNew(SHorizontalBox)
|
ValueWidget = SNew(SHorizontalBox)
|
||||||
+ SHorizontalBox::Slot().AutoWidth()
|
+ SHorizontalBox::Slot().AutoWidth()
|
||||||
@@ -1609,6 +1701,14 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
|||||||
+ SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f)
|
+ SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f)
|
||||||
[ MakeTextField(Row.VecZ) ];
|
[ MakeTextField(Row.VecZ) ];
|
||||||
}
|
}
|
||||||
|
else if (StructProp->Struct == TBaseStructure<FVector2D>::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<FLinearColor>::Get())
|
else if (StructProp->Struct == TBaseStructure<FLinearColor>::Get())
|
||||||
{
|
{
|
||||||
ValueWidget = SNew(SHorizontalBox)
|
ValueWidget = SNew(SHorizontalBox)
|
||||||
@@ -1671,7 +1771,34 @@ void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
|||||||
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
||||||
if (!Target) continue;
|
if (!Target) continue;
|
||||||
|
|
||||||
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Row.CachedProperty))
|
if (Row.CachedEnum && Row.EnumCombo.IsValid() && !Row.EnumCombo->IsOpen())
|
||||||
|
{
|
||||||
|
// Read current enum value index
|
||||||
|
int64 EnumValue = 0;
|
||||||
|
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
FNumericProperty* UnderlyingProp = EnumProp->GetUnderlyingProperty();
|
||||||
|
const void* ValuePtr = Row.CachedProperty->ContainerPtrToValuePtr<void>(Target);
|
||||||
|
EnumValue = UnderlyingProp->GetSignedIntPropertyValue(ValuePtr);
|
||||||
|
}
|
||||||
|
else if (FByteProperty* ByteProp = CastField<FByteProperty>(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<FBoolProperty>(Row.CachedProperty))
|
||||||
{
|
{
|
||||||
if (Row.BoolField.IsValid())
|
if (Row.BoolField.IsValid())
|
||||||
{
|
{
|
||||||
@@ -1706,6 +1833,12 @@ void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
|||||||
SetIfNoFocus(Row.VecY, Rot.Yaw);
|
SetIfNoFocus(Row.VecY, Rot.Yaw);
|
||||||
SetIfNoFocus(Row.VecZ, Rot.Roll);
|
SetIfNoFocus(Row.VecZ, Rot.Roll);
|
||||||
}
|
}
|
||||||
|
else if (StructProp->Struct == TBaseStructure<FVector2D>::Get())
|
||||||
|
{
|
||||||
|
const FVector2D& Vec2 = *static_cast<const FVector2D*>(ValuePtr);
|
||||||
|
SetIfNoFocus(Row.VecX, Vec2.X);
|
||||||
|
SetIfNoFocus(Row.VecY, Vec2.Y);
|
||||||
|
}
|
||||||
else if (StructProp->Struct == TBaseStructure<FLinearColor>::Get())
|
else if (StructProp->Struct == TBaseStructure<FLinearColor>::Get())
|
||||||
{
|
{
|
||||||
const FLinearColor& Col = *static_cast<const FLinearColor*>(ValuePtr);
|
const FLinearColor& Col = *static_cast<const FLinearColor*>(ValuePtr);
|
||||||
@@ -1774,7 +1907,19 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex)
|
|||||||
// Build new value text from UI and import
|
// Build new value text from UI and import
|
||||||
FString NewValueText;
|
FString NewValueText;
|
||||||
|
|
||||||
if (CastField<FBoolProperty>(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<FBoolProperty>(Row.CachedProperty))
|
||||||
{
|
{
|
||||||
NewValueText = Row.BoolField.IsValid() && Row.BoolField->IsChecked() ? TEXT("True") : TEXT("False");
|
NewValueText = Row.BoolField.IsValid() && Row.BoolField->IsChecked() ? TEXT("True") : TEXT("False");
|
||||||
}
|
}
|
||||||
@@ -1789,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));
|
NewValueText = FString::Printf(TEXT("(X=%s,Y=%s,Z=%s)"), *GetText(Row.VecX), *GetText(Row.VecY), *GetText(Row.VecZ));
|
||||||
}
|
}
|
||||||
|
else if (StructProp->Struct == TBaseStructure<FVector2D>::Get())
|
||||||
|
{
|
||||||
|
NewValueText = FString::Printf(TEXT("(X=%s,Y=%s)"), *GetText(Row.VecX), *GetText(Row.VecY));
|
||||||
|
}
|
||||||
else if (StructProp->Struct == TBaseStructure<FRotator>::Get())
|
else if (StructProp->Struct == TBaseStructure<FRotator>::Get())
|
||||||
{
|
{
|
||||||
NewValueText = FString::Printf(TEXT("(Pitch=%s,Yaw=%s,Roll=%s)"), *GetText(Row.VecX), *GetText(Row.VecY), *GetText(Row.VecZ));
|
NewValueText = FString::Printf(TEXT("(Pitch=%s,Yaw=%s,Roll=%s)"), *GetText(Row.VecX), *GetText(Row.VecY), *GetText(Row.VecZ));
|
||||||
@@ -1807,10 +1956,16 @@ void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex)
|
|||||||
NewValueText = Row.SingleField->GetText().ToString();
|
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
|
// 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
|
// Refresh rendering
|
||||||
if (UActorComponent* Comp = Cast<UActorComponent>(Target))
|
if (UActorComponent* Comp = Cast<UActorComponent>(Target))
|
||||||
@@ -1842,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);
|
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<UPS_Editor_EditableComponent>();
|
||||||
|
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<APS_Editor_PlayerController>(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<FString, FString> 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
|
FString UPS_Editor_MainWidget::FloatToString(float Value) const
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ struct FPS_Editor_PropertyRowWidgets
|
|||||||
TSharedPtr<SCheckBox> BoolField; // bool
|
TSharedPtr<SCheckBox> BoolField; // bool
|
||||||
TSharedPtr<SEditableTextBox> VecX, VecY, VecZ; // FVector, FRotator
|
TSharedPtr<SEditableTextBox> VecX, VecY, VecZ; // FVector, FRotator
|
||||||
TSharedPtr<SEditableTextBox> ColR, ColG, ColB, ColA; // FLinearColor
|
TSharedPtr<SEditableTextBox> ColR, ColG, ColB, ColA; // FLinearColor
|
||||||
|
|
||||||
|
// Enum support
|
||||||
|
TSharedPtr<SComboBox<TSharedPtr<FString>>> EnumCombo;
|
||||||
|
TSharedPtr<STextBlock> EnumComboText;
|
||||||
|
TSharedPtr<TArray<TSharedPtr<FString>>> EnumOptions; // Heap-allocated so OptionsSource pointer survives Row copy
|
||||||
|
UEnum* CachedEnum = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,6 +81,8 @@ private:
|
|||||||
TArray<TSharedPtr<FString>> BaseLevelOptions;
|
TArray<TSharedPtr<FString>> BaseLevelOptions;
|
||||||
TSharedPtr<STextBlock> BaseLevelComboText;
|
TSharedPtr<STextBlock> BaseLevelComboText;
|
||||||
FString CurrentLevelFilter;
|
FString CurrentLevelFilter;
|
||||||
|
/** Maps DisplayName → full MapPath for base levels (from MasterCatalog). */
|
||||||
|
TMap<FString, FString> BaseLevelPathMap;
|
||||||
TArray<TSharedPtr<FSlateBrush>> CachedBrushes;
|
TArray<TSharedPtr<FSlateBrush>> CachedBrushes;
|
||||||
|
|
||||||
// Transform fields
|
// Transform fields
|
||||||
|
|||||||
Reference in New Issue
Block a user