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
|
||||
PendingScenarioName = LastEditedScenarioName;
|
||||
PendingBaseLevel = LastEditedBaseLevel;
|
||||
bIsPlayMode = false;
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Returning to editor (scenario: '%s', base level: '%s')"),
|
||||
*PendingScenarioName, *PendingBaseLevel);
|
||||
|
||||
@@ -36,6 +36,10 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
FString EditorMapName;
|
||||
|
||||
/** True when the Play button was pressed (gameplay mode). False when just loading/editing a scenario. */
|
||||
UPROPERTY(BlueprintReadWrite, Category = "PS Editor")
|
||||
bool bIsPlayMode = false;
|
||||
|
||||
/** Set pending scenario + base level (for opening the editor or gameplay). */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
void SetPendingScenario(const FString& ScenarioName, const FString& BaseLevel);
|
||||
|
||||
@@ -163,6 +163,7 @@ void APS_Editor_Pawn::CreateInputActions()
|
||||
IA_TranslateMode = NewObject<UInputAction>(this, TEXT("IA_TranslateMode"));
|
||||
IA_TranslateMode->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::Z);
|
||||
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::W);
|
||||
|
||||
IA_RotateMode = NewObject<UInputAction>(this, TEXT("IA_RotateMode"));
|
||||
IA_RotateMode->ValueType = EInputActionValueType::Boolean;
|
||||
@@ -453,7 +454,7 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
if (AActor* Actor = Selected[i].Get())
|
||||
{
|
||||
FVector NewScale = GizmoDragInitialTransforms[i].GetScale3D();
|
||||
if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::XYZ)
|
||||
if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::XYZ || Gizmo->bIsUniformScaleOnly)
|
||||
{
|
||||
NewScale *= ScaleFactor; // Uniform scale
|
||||
}
|
||||
@@ -609,7 +610,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
||||
FCollisionQueryParams SplineParams;
|
||||
SplineParams.AddIgnoredActor(this);
|
||||
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()))
|
||||
{
|
||||
@@ -670,7 +671,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
||||
LineParams.AddIgnoredActor(this);
|
||||
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)
|
||||
{
|
||||
@@ -730,7 +731,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
||||
const FVector AddEnd = RayOrigin + RayDir * 100000.0f;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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"),
|
||||
*PointLoc.ToString(), Hit.ImpactPoint.Z,
|
||||
@@ -1291,7 +1292,7 @@ void APS_Editor_Pawn::ComputeOrbitFocalPoint()
|
||||
FCollisionQueryParams Params;
|
||||
Params.AddIgnoredActor(this);
|
||||
|
||||
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
|
||||
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Camera, Params))
|
||||
{
|
||||
OrbitFocalPoint = Hit.ImpactPoint;
|
||||
OrbitDistance = FVector::Dist(Start, OrbitFocalPoint);
|
||||
@@ -1379,10 +1380,17 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
|
||||
|
||||
ActiveGizmoAxis = Axis;
|
||||
|
||||
// Block rotation on non-yaw axes if YawOnly is set
|
||||
const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
|
||||
if (Mode == EPS_Editor_TransformMode::Rotate && Gizmo->bIsYawOnly
|
||||
&& Axis != EPS_Editor_GizmoAxis::Z)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Store axis direction at drag start - used for entire drag duration
|
||||
GizmoDragAxisDir = Gizmo->GetAxisDirection(Axis);
|
||||
const FVector AxisDir = GizmoDragAxisDir;
|
||||
const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
|
||||
|
||||
FVector PlaneNormal;
|
||||
if (Mode == EPS_Editor_TransformMode::Rotate)
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
#include "PS_Editor_PointPlaceable.h"
|
||||
#include "Engine/LevelStreamingDynamic.h"
|
||||
#include "Engine/PostProcessVolume.h"
|
||||
#include "PS_Editor_CameraStart.h"
|
||||
#include "PS_Editor_GameInstanceSubsystem.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "GameFramework/GameStateBase.h"
|
||||
@@ -61,7 +63,17 @@ void APS_Editor_PlayerController::BeginPlay()
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Restoring from subsystem - BaseLevel='%s', Scenario='%s'"),
|
||||
*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)
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
Super::OnPossess(InPawn);
|
||||
@@ -170,22 +192,25 @@ void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActo
|
||||
|
||||
void APS_Editor_PlayerController::FinishPointPlacement()
|
||||
{
|
||||
// Just exit the mode — actor is already tracked, all edits already have undo
|
||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
||||
// Exit the mode FIRST to prevent re-entrant ClearSelection loop
|
||||
AActor* PlacementActor = ActivePlacementActor;
|
||||
ActivePlacementActor = nullptr;
|
||||
AppendInitialPoints.Empty();
|
||||
EditorMode = EPS_Editor_EditorMode::Normal;
|
||||
|
||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(PlacementActor))
|
||||
{
|
||||
Spline->SetHandlesVisible(false);
|
||||
Spline->ClearHandleHighlight();
|
||||
}
|
||||
|
||||
if (ActivePlacementActor && SelectionManager)
|
||||
if (PlacementActor && SelectionManager)
|
||||
{
|
||||
SelectionManager->ClearSelection();
|
||||
SelectionManager->SelectActor(ActivePlacementActor);
|
||||
SelectionManager->SelectActor(PlacementActor);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exited placement mode"));
|
||||
|
||||
ActivePlacementActor = nullptr;
|
||||
bAppendingToActor = false;
|
||||
AppendInitialPoints.Empty();
|
||||
EditorMode = EPS_Editor_EditorMode::Normal;
|
||||
@@ -197,7 +222,7 @@ void APS_Editor_PlayerController::CancelPointPlacement()
|
||||
FinishPointPlacement();
|
||||
}
|
||||
|
||||
void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName)
|
||||
void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath)
|
||||
{
|
||||
// Unload previous sublevel first
|
||||
if (CurrentSublevel)
|
||||
@@ -238,11 +263,14 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
|
||||
return;
|
||||
}
|
||||
|
||||
// Use MapPath if provided, otherwise fall back to LevelName (legacy behavior)
|
||||
const FString& LevelToLoad = MapPath.IsEmpty() ? LevelName : MapPath;
|
||||
|
||||
// Load new sublevel
|
||||
bool bSuccess = false;
|
||||
CurrentSublevel = ULevelStreamingDynamic::LoadLevelInstance(
|
||||
GetWorld(),
|
||||
LevelName,
|
||||
LevelToLoad,
|
||||
FVector::ZeroVector,
|
||||
FRotator::ZeroRotator,
|
||||
bSuccess);
|
||||
@@ -252,11 +280,56 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
|
||||
CurrentSublevel->SetShouldBeLoaded(true);
|
||||
CurrentSublevel->SetShouldBeVisible(true);
|
||||
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
|
||||
{
|
||||
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;
|
||||
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);
|
||||
}
|
||||
@@ -326,7 +399,7 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
|
||||
TubeParams.AddIgnoredActor(GetPawn());
|
||||
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()))
|
||||
{
|
||||
|
||||
@@ -47,9 +47,13 @@ public:
|
||||
UPROPERTY(BlueprintReadWrite, Category = "PS Editor")
|
||||
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")
|
||||
void LoadBaseLevelAsSublevel(const FString& LevelName);
|
||||
void LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath = TEXT(""));
|
||||
|
||||
// ---- Editor Mode (Normal / SplinePlacement) ----
|
||||
|
||||
@@ -85,6 +89,7 @@ public:
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
|
||||
private:
|
||||
@@ -119,5 +124,9 @@ private:
|
||||
UPROPERTY()
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ static void ConfigureGizmoComponent(UPrimitiveComponent* Comp)
|
||||
{
|
||||
Comp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
Comp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
Comp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||
Comp->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
|
||||
Comp->SetCollisionObjectType(ECC_WorldDynamic);
|
||||
Comp->SetCastShadow(false);
|
||||
Comp->SetRenderCustomDepth(false);
|
||||
@@ -142,6 +142,7 @@ void APS_Editor_Gizmo::BeginPlay()
|
||||
|
||||
// Grey center cube for uniform scale
|
||||
Mat_Center = MakeColorMat(FLinearColor(0.5f, 0.5f, 0.5f));
|
||||
Mat_Disabled = MakeColorMat(FLinearColor(0.15f, 0.15f, 0.15f));
|
||||
ApplyMat(ScaleCube_Center, Mat_Center);
|
||||
|
||||
SetGizmoVisible(false);
|
||||
@@ -339,6 +340,13 @@ void APS_Editor_Gizmo::SetTransformMode(EPS_Editor_TransformMode NewMode)
|
||||
ScaleRoot->SetVisibility(bGizmoVisible && NewMode == EPS_Editor_TransformMode::Scale, true);
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::SetYawOnly(bool bYawOnly)
|
||||
{
|
||||
bIsYawOnly = bYawOnly;
|
||||
if (Arc_X) Arc_X->SetMaterial(0, bYawOnly && Mat_Disabled ? Mat_Disabled : Mat_X);
|
||||
if (Arc_Y) Arc_Y->SetMaterial(0, bYawOnly && Mat_Disabled ? Mat_Disabled : Mat_Y);
|
||||
}
|
||||
|
||||
// ---- Hit detection ----
|
||||
|
||||
EPS_Editor_GizmoAxis APS_Editor_Gizmo::GetAxisUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const
|
||||
@@ -421,13 +429,36 @@ void APS_Editor_Gizmo::SetHighlightedAxis(EPS_Editor_GizmoAxis Axis)
|
||||
default: break;
|
||||
}
|
||||
SetAxisMaterial(HighlightedAxis, OrigMat);
|
||||
|
||||
// Restore disabled arcs to grey after un-highlight
|
||||
if (bIsYawOnly && Mat_Disabled)
|
||||
{
|
||||
if (HighlightedAxis == EPS_Editor_GizmoAxis::X && Arc_X) Arc_X->SetMaterial(0, Mat_Disabled);
|
||||
if (HighlightedAxis == EPS_Editor_GizmoAxis::Y && Arc_Y) Arc_Y->SetMaterial(0, Mat_Disabled);
|
||||
}
|
||||
}
|
||||
|
||||
HighlightedAxis = Axis;
|
||||
|
||||
if (Axis != EPS_Editor_GizmoAxis::None)
|
||||
{
|
||||
SetAxisMaterial(Axis, Mat_Highlight);
|
||||
// In yaw-only mode, don't highlight rotation arcs on X/Y but still highlight translate/scale
|
||||
if (bIsYawOnly && CurrentMode == EPS_Editor_TransformMode::Rotate
|
||||
&& (Axis == EPS_Editor_GizmoAxis::X || Axis == EPS_Editor_GizmoAxis::Y))
|
||||
{
|
||||
// Don't highlight at all in rotate mode for disabled axes
|
||||
}
|
||||
else
|
||||
{
|
||||
SetAxisMaterial(Axis, Mat_Highlight);
|
||||
|
||||
// Re-grey the arcs even if translate/scale axes are highlighted
|
||||
if (bIsYawOnly && Mat_Disabled)
|
||||
{
|
||||
if (Axis == EPS_Editor_GizmoAxis::X && Arc_X) Arc_X->SetMaterial(0, Mat_Disabled);
|
||||
if (Axis == EPS_Editor_GizmoAxis::Y && Arc_Y) Arc_Y->SetMaterial(0, Mat_Disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,9 +521,9 @@ void APS_Editor_Gizmo::UpdateArcFacing()
|
||||
CreateArcRing(Arc_Y, LocalY, ArcY_Start, ArcY_End, ArcRadius, ArcWidth, ArcSegments);
|
||||
CreateArcRing(Arc_Z, LocalZ, ArcZ_Start, ArcZ_End, ArcRadius, ArcWidth, ArcSegments);
|
||||
|
||||
// Re-apply materials
|
||||
if (Mat_X) Arc_X->SetMaterial(0, Mat_X);
|
||||
if (Mat_Y) Arc_Y->SetMaterial(0, Mat_Y);
|
||||
// Re-apply materials (respect YawOnly constraint)
|
||||
if (Mat_X) Arc_X->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_X);
|
||||
if (Mat_Y) Arc_Y->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_Y);
|
||||
if (Mat_Z) Arc_Z->SetMaterial(0, Mat_Z);
|
||||
|
||||
if (HighlightedAxis != EPS_Editor_GizmoAxis::None)
|
||||
@@ -512,9 +543,9 @@ void APS_Editor_Gizmo::UpdateArcFacing()
|
||||
PositionGuideLine(GuideLine_B, LocalY * NewOctant.Y);
|
||||
PositionGuideLine(GuideLine_C, LocalZ * NewOctant.Z);
|
||||
|
||||
// Re-apply materials (mesh sections are recreated)
|
||||
if (Mat_X) Arc_X->SetMaterial(0, Mat_X);
|
||||
if (Mat_Y) Arc_Y->SetMaterial(0, Mat_Y);
|
||||
// Re-apply materials (mesh sections are recreated, respect YawOnly)
|
||||
if (Mat_X) Arc_X->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_X);
|
||||
if (Mat_Y) Arc_Y->SetMaterial(0, bIsYawOnly && Mat_Disabled ? Mat_Disabled : Mat_Y);
|
||||
if (Mat_Z) Arc_Z->SetMaterial(0, Mat_Z);
|
||||
|
||||
// Restore highlight if needed
|
||||
|
||||
@@ -31,6 +31,9 @@ public:
|
||||
void SetHighlightedAxis(EPS_Editor_GizmoAxis Axis);
|
||||
FVector GetAxisDirection(EPS_Editor_GizmoAxis Axis) const;
|
||||
|
||||
/** Constrain rotation to yaw only: hides X/Y rotation arcs and blocks drag on those axes. */
|
||||
void SetYawOnly(bool bYawOnly);
|
||||
|
||||
/** Set the orientation for local space mode (matches selected actor's rotation). */
|
||||
void SetLocalOrientation(FRotator ActorRotation);
|
||||
|
||||
@@ -116,11 +119,18 @@ private:
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Highlight;
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Center;
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Disabled;
|
||||
|
||||
// ---- State ----
|
||||
EPS_Editor_TransformMode CurrentMode = EPS_Editor_TransformMode::Translate;
|
||||
EPS_Editor_GizmoAxis HighlightedAxis = EPS_Editor_GizmoAxis::None;
|
||||
bool bGizmoVisible = false;
|
||||
|
||||
public:
|
||||
bool bIsYawOnly = false;
|
||||
bool bIsUniformScaleOnly = false;
|
||||
private:
|
||||
float DesiredScreenSize = 0.25f;
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "PS_Editor_SpawnableComponent.h"
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Engine/World.h"
|
||||
@@ -20,11 +21,11 @@ static TMap<FString, FString> ExportEditableProps(AActor* Actor)
|
||||
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
||||
if (!EditComp) return Result;
|
||||
|
||||
for (const FName& Path : EditComp->EditableProperties)
|
||||
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||
{
|
||||
FString CompName, PropName;
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
||||
const FString Key = Path.ToString();
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName);
|
||||
const FString Key = Entry.Path.ToString();
|
||||
|
||||
UObject* Target = CompName.IsEmpty() ? static_cast<UObject*>(Actor) : nullptr;
|
||||
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>();
|
||||
if (!EditComp) return;
|
||||
|
||||
for (const FName& Path : EditComp->EditableProperties)
|
||||
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||
{
|
||||
FString CompName, PropName;
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
||||
const FString Key = Path.ToString();
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName);
|
||||
const FString Key = Entry.Path.ToString();
|
||||
|
||||
const FString* ValStr = Properties.Find(Key);
|
||||
if (!ValStr) continue;
|
||||
@@ -127,7 +128,7 @@ void UPS_Editor_SelectionManager::HandleClickAtScreenPosition(FVector2D ScreenPo
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
@@ -456,7 +457,13 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
||||
|
||||
FVector Origin, Extent;
|
||||
Actor->GetActorBounds(false, Origin, Extent);
|
||||
const float BottomOffset = Extent.Z;
|
||||
float BottomOffset = Extent.Z;
|
||||
|
||||
// Add custom ground offset from SpawnableComponent if available
|
||||
if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
|
||||
{
|
||||
BottomOffset += SpawnComp->GroundOffset;
|
||||
}
|
||||
|
||||
const FVector Start = Actor->GetActorLocation();
|
||||
const FVector End = Start - FVector::UpVector * 100000.0f;
|
||||
@@ -467,11 +474,21 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
||||
Params.AddIgnoredActor(PC->GetPawn());
|
||||
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;
|
||||
NewLocation.Z += BottomOffset;
|
||||
Actor->SetActorLocation(NewLocation);
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SnapToGround '%s' -> Hit '%s' (%s) at Z=%.1f, BottomOffset=%.1f, NewZ=%.1f"),
|
||||
*Actor->GetName(),
|
||||
*Hit.GetActor()->GetName(),
|
||||
*Hit.GetComponent()->GetName(),
|
||||
Hit.ImpactPoint.Z, BottomOffset, NewLocation.Z);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: SnapToGround '%s' -> No hit"), *Actor->GetName());
|
||||
}
|
||||
|
||||
UndoAction->NewTransforms.Add(Actor->GetActorTransform());
|
||||
@@ -514,11 +531,21 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
|
||||
|
||||
Gizmo->SetGizmoActive(true, Pivot);
|
||||
|
||||
// Update gizmo orientation
|
||||
// Update gizmo orientation and yaw-only constraint
|
||||
if (AActor* Actor = SelectedActors[0].Get())
|
||||
{
|
||||
bool bUseLocal = (CurrentTransformMode == EPS_Editor_TransformMode::Scale) || (CurrentSpace == EPS_Editor_Space::Local);
|
||||
Gizmo->SetLocalOrientation(bUseLocal ? Actor->GetActorRotation() : FRotator::ZeroRotator);
|
||||
|
||||
bool bYaw = false;
|
||||
bool bUniformScale = false;
|
||||
if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
|
||||
{
|
||||
bYaw = SpawnComp->bYawOnly;
|
||||
bUniformScale = SpawnComp->bUniformScaleOnly;
|
||||
}
|
||||
Gizmo->SetYawOnly(bYaw);
|
||||
Gizmo->bIsUniformScaleOnly = bUniformScale;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "PS_Editor_SceneLoader.h"
|
||||
#include "PS_Editor_SceneData.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "PS_Editor_EditableInterface.h"
|
||||
#include "PS_Editor_SpawnableComponent.h"
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
#include "PS_Editor_PointPlaceable.h"
|
||||
@@ -147,6 +148,12 @@ AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Edit
|
||||
Spline->ImportFromCustomProperties(ActorData.CustomProperties);
|
||||
}
|
||||
|
||||
// Notify the actor so it can re-initialize with the loaded property values
|
||||
if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
||||
{
|
||||
IPS_Editor_EditableInterface::Execute_OnEditorNeedsReinit(SpawnedActor);
|
||||
}
|
||||
|
||||
return SpawnedActor;
|
||||
}
|
||||
|
||||
@@ -155,11 +162,11 @@ void UPS_Editor_SceneLoader::ApplyCustomProperties(AActor* Actor, const TMap<FSt
|
||||
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
||||
if (!EditComp) return;
|
||||
|
||||
for (const FName& Path : EditComp->EditableProperties)
|
||||
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||
{
|
||||
FString CompName, PropName;
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
||||
const FString Key = Path.ToString();
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName);
|
||||
const FString Key = Entry.Path.ToString();
|
||||
|
||||
const FString* ValueStr = CustomProperties.Find(Key);
|
||||
if (!ValueStr) continue;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "PS_Editor_EditableInterface.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
#include "JsonObjectConverter.h"
|
||||
@@ -55,11 +56,11 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
||||
// Export custom editable properties
|
||||
if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
||||
{
|
||||
for (const FName& Path : EditComp->EditableProperties)
|
||||
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||
{
|
||||
FString CompName, PropName;
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
||||
const FString Key = Path.ToString();
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName);
|
||||
const FString Key = Entry.Path.ToString();
|
||||
|
||||
UObject* Target = CompName.IsEmpty() ? static_cast<UObject*>(Actor) : nullptr;
|
||||
if (!Target)
|
||||
@@ -171,11 +172,11 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
||||
{
|
||||
if (UPS_Editor_EditableComponent* EditComp = SpawnedActor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
||||
{
|
||||
for (const FName& Path : EditComp->EditableProperties)
|
||||
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||
{
|
||||
FString CompName, PropName;
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
||||
const FString Key = Path.ToString();
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, CompName, PropName);
|
||||
const FString Key = Entry.Path.ToString();
|
||||
|
||||
const FString* ValueStr = ActorData.CustomProperties.Find(Key);
|
||||
if (!ValueStr) continue;
|
||||
@@ -217,6 +218,13 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
||||
Spline->ImportFromCustomProperties(ActorData.CustomProperties);
|
||||
}
|
||||
|
||||
// Notify the actor that properties were loaded and we are in editor mode
|
||||
if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
||||
{
|
||||
IPS_Editor_EditableInterface::Execute_OnEditorNeedsReinit(SpawnedActor);
|
||||
IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true);
|
||||
}
|
||||
|
||||
SpawnedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,9 +166,21 @@ void UPS_Editor_EditableComponent::PostEditChangeProperty(FPropertyChangedEvent&
|
||||
FullPath = FName(SelComp + TEXT(".") + AddProperty.ToString());
|
||||
}
|
||||
|
||||
if (!EditableProperties.Contains(FullPath))
|
||||
// Check if already exists
|
||||
bool bExists = false;
|
||||
for (const FPS_Editor_EditablePropertyEntry& Entry : EditableProperties)
|
||||
{
|
||||
EditableProperties.Add(FullPath);
|
||||
if (Entry.Path == FullPath)
|
||||
{
|
||||
bExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bExists)
|
||||
{
|
||||
FPS_Editor_EditablePropertyEntry NewEntry;
|
||||
NewEntry.Path = FullPath;
|
||||
EditableProperties.Add(NewEntry);
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Added editable property: %s"), *FullPath.ToString());
|
||||
}
|
||||
|
||||
@@ -190,13 +202,37 @@ void UPS_Editor_EditableComponent::ParsePropertyPath(const FName& Path, FString&
|
||||
}
|
||||
}
|
||||
|
||||
bool UPS_Editor_EditableComponent::RequiresReinit(const FName& PropertyPath) const
|
||||
{
|
||||
for (const FPS_Editor_EditablePropertyEntry& Entry : EditableProperties)
|
||||
{
|
||||
if (Entry.Path == PropertyPath)
|
||||
{
|
||||
return Entry.bNeedsReinit;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UPS_Editor_EditableComponent::RequiresRespawn(const FName& PropertyPath) const
|
||||
{
|
||||
for (const FPS_Editor_EditablePropertyEntry& Entry : EditableProperties)
|
||||
{
|
||||
if (Entry.Path == PropertyPath)
|
||||
{
|
||||
return Entry.bNeedsRespawn;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FString UPS_Editor_EditableComponent::GetDisplayName(const FName& Path) const
|
||||
{
|
||||
if (const FString* Override = DisplayNames.Find(Path))
|
||||
for (const FPS_Editor_EditablePropertyEntry& Entry : EditableProperties)
|
||||
{
|
||||
if (!Override->IsEmpty())
|
||||
if (Entry.Path == Path && !Entry.DisplayName.IsEmpty())
|
||||
{
|
||||
return *Override;
|
||||
return Entry.DisplayName;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,39 @@
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "PS_Editor_EditableComponent.generated.h"
|
||||
|
||||
/**
|
||||
* A single editable property entry with optional respawn flag.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct FPS_Editor_EditablePropertyEntry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Property path: "ComponentName.PropertyName" or just "PropertyName" for actor-level.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
FName Path;
|
||||
|
||||
/** Display name override for the runtime editor UI. If empty, the property name is used. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
FString DisplayName;
|
||||
|
||||
/**
|
||||
* If true, calls OnEditorPropertiesChanged (IPS_Editor_EditableInterface)
|
||||
* after changing this property, so the actor can re-run its initialization logic.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
bool bNeedsReinit = false;
|
||||
|
||||
/**
|
||||
* If true, the actor is destroyed and respawned with all current properties applied,
|
||||
* then OnEditorPropertiesChanged is called. Use when reinit alone is not enough.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
bool bNeedsRespawn = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Marker component that declares which properties are editable in the PS_Editor runtime editor.
|
||||
* Separate from SpawnableComponent (which handles catalogue metadata).
|
||||
@@ -31,19 +64,16 @@ public:
|
||||
|
||||
/**
|
||||
* Properties exposed in the runtime editor.
|
||||
* Format: "ComponentName.PropertyName" or just "PropertyName" for actor-level.
|
||||
* Populated via the dropdowns above, or editable manually.
|
||||
* Each entry has a path, optional display name, and optional respawn flag.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editable Properties")
|
||||
TArray<FName> EditableProperties;
|
||||
TArray<FPS_Editor_EditablePropertyEntry> EditableProperties;
|
||||
|
||||
/**
|
||||
* Optional display name overrides for the runtime editor UI.
|
||||
* Key = property path (same as in EditableProperties), Value = label shown in UI.
|
||||
* If not set, the property name is used as label.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editable Properties")
|
||||
TMap<FName, FString> DisplayNames;
|
||||
/** Returns true if the given property path requires a reinit call. */
|
||||
bool RequiresReinit(const FName& PropertyPath) const;
|
||||
|
||||
/** Returns true if the given property path requires a full respawn. */
|
||||
bool RequiresRespawn(const FName& PropertyPath) const;
|
||||
|
||||
/** Lists available component names on the owning actor. */
|
||||
UFUNCTION()
|
||||
|
||||
@@ -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);
|
||||
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->SRGB = true;
|
||||
Texture->CompressionSettings = TC_Default;
|
||||
Texture->MipGenSettings = TMGS_NoMipmaps;
|
||||
Texture->NeverStream = true;
|
||||
Texture->LODGroup = TEXTUREGROUP_UI;
|
||||
Texture->PostEditChange();
|
||||
Texture->UpdateResource();
|
||||
|
||||
|
||||
@@ -44,6 +44,28 @@ public:
|
||||
#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.
|
||||
* Assign this to the PlayerController. Each sub-catalog can represent a domain
|
||||
@@ -60,10 +82,10 @@ public:
|
||||
TArray<TObjectPtr<UPS_Editor_SpawnCatalog>> Catalogs;
|
||||
|
||||
/**
|
||||
* List of base level names available for scene editing.
|
||||
* Used to filter saved scenes by level. Auto-matched by stripping "_Editor" from the current map name.
|
||||
* Example: "Warehouse", "Office", "Parking"
|
||||
* List of base levels available for scene editing.
|
||||
* DisplayName is used for UI display and scene filtering.
|
||||
* MapPath is the full package path for OpenLevel (e.g. "/Game/Maps/Warehouse").
|
||||
*/
|
||||
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_SpawnableComponent.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "PS_Editor_EditableInterface.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
@@ -67,6 +69,11 @@ void UPS_Editor_SpawnManager::AddCatalog(UPS_Editor_SpawnCatalog* InCatalog)
|
||||
Resolved.Category = SpawnComp ? SpawnComp->Category : TEXT("Default");
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -129,6 +136,36 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector
|
||||
// Track spawned actor
|
||||
SpawnedActors.Add(SpawnedActor);
|
||||
|
||||
// Auto-snap to ground if configured
|
||||
UPS_Editor_SpawnableComponent* SpawnComp = SpawnedActor->FindComponentByClass<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());
|
||||
return SpawnedActor;
|
||||
}
|
||||
@@ -213,6 +250,119 @@ bool UPS_Editor_SpawnManager::IsActorSpawned(AActor* Actor) const
|
||||
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> Categories;
|
||||
|
||||
@@ -69,6 +69,21 @@ public:
|
||||
/** Returns true if this actor was spawned/tracked by the editor (catalogue, duplicate, paste, load). */
|
||||
bool IsActorSpawned(AActor* Actor) const;
|
||||
|
||||
/** Notify all spawned actors that implement IPS_Editor_EditableInterface of editor mode change. */
|
||||
void NotifyEditorModeChanged(bool bIsEditing);
|
||||
|
||||
/** Tick all spawned actors that implement IPS_Editor_EditableInterface. */
|
||||
void TickEditorMode(float DeltaTime);
|
||||
|
||||
/**
|
||||
* Respawn an actor: destroy it and create a new one of the same class,
|
||||
* applying all given property values BEFORE BeginPlay (via SpawnActorDeferred).
|
||||
* @param OldActor The actor to replace.
|
||||
* @param PropertyValues Map of "ComponentName.PropertyName" -> exported text value.
|
||||
* @return The newly spawned actor, or nullptr on failure.
|
||||
*/
|
||||
AActor* RespawnActorWithProperties(AActor* OldActor, const TMap<FString, FString>& PropertyValues);
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
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. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
|
||||
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);
|
||||
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->SRGB = true;
|
||||
Texture->CompressionSettings = TC_Default;
|
||||
Texture->MipGenSettings = TMGS_NoMipmaps;
|
||||
Texture->NeverStream = true;
|
||||
Texture->LODGroup = TEXTUREGROUP_UI;
|
||||
Texture->PostEditChange();
|
||||
Texture->UpdateResource();
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ APS_Editor_SplineActor::APS_Editor_SplineActor()
|
||||
SplineMeshComp->SetupAttachment(Root);
|
||||
SplineMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
SplineMeshComp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
SplineMeshComp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||
SplineMeshComp->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
|
||||
SplineMeshComp->bUseComplexAsSimpleCollision = true;
|
||||
SplineMeshComp->SetCastShadow(false);
|
||||
|
||||
@@ -42,9 +42,9 @@ APS_Editor_SplineActor::APS_Editor_SplineActor()
|
||||
|
||||
// Editable: exposes SplineColor and SplineTag in the runtime property panel
|
||||
EditableComp = CreateDefaultSubobject<UPS_Editor_EditableComponent>(TEXT("PS_Editor_Editable"));
|
||||
EditableComp->EditableProperties.Add(FName(TEXT("SplineColor")));
|
||||
EditableComp->EditableProperties.Add(FName(TEXT("SplineSelectedColor")));
|
||||
EditableComp->EditableProperties.Add(FName(TEXT("SplineTag")));
|
||||
EditableComp->EditableProperties.Add({FName(TEXT("SplineColor"))});
|
||||
EditableComp->EditableProperties.Add({FName(TEXT("SplineSelectedColor"))});
|
||||
EditableComp->EditableProperties.Add({FName(TEXT("SplineTag"))});
|
||||
}
|
||||
|
||||
void APS_Editor_SplineActor::BeginPlay()
|
||||
@@ -101,7 +101,7 @@ void APS_Editor_SplineActor::BeginPlay()
|
||||
CenterCube->SetWorldScale3D(FVector(CenterCubeScale));
|
||||
CenterCube->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
CenterCube->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
CenterCube->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||
CenterCube->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
|
||||
CenterCube->SetCastShadow(false);
|
||||
if (CenterCubeMat)
|
||||
{
|
||||
@@ -316,7 +316,7 @@ UStaticMeshComponent* APS_Editor_SplineActor::CreatePointHandle(const FVector& W
|
||||
// Collision for raycast
|
||||
Handle->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
Handle->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
Handle->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||
Handle->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
|
||||
|
||||
Handle->SetCastShadow(false);
|
||||
Handle->SetRenderInDepthPass(false);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "PS_Editor_EditableInterface.h"
|
||||
#include "PS_Editor_SpawnableComponent.h"
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
#include "PS_Editor_Pawn.h"
|
||||
@@ -607,6 +608,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem<UPS_Editor_GameInstanceSubsystem>())
|
||||
{
|
||||
Sub->SetPendingScenario(SceneName, BaseLevel);
|
||||
Sub->bIsPlayMode = true;
|
||||
// Remember editor map for ReturnToEditor
|
||||
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
|
||||
EditorPC->LoadBaseLevelAsSublevel(TEXT("None"));
|
||||
|
||||
@@ -624,8 +632,16 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
EditorPC->bShowMouseCursor = false;
|
||||
EditorPC->SetInputMode(FInputModeGameOnly());
|
||||
|
||||
// Use full path to avoid confusion with sublevel instances
|
||||
FString MapPath = FString::Printf(TEXT("/Game/%s"), *BaseLevel);
|
||||
// Use full path from MasterCatalog, fallback to /Game/{name}
|
||||
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(""));
|
||||
return FReply::Handled();
|
||||
})
|
||||
@@ -676,7 +692,12 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
{
|
||||
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;
|
||||
for (const TSharedPtr<FString>& Opt : BaseLevelOptions)
|
||||
{
|
||||
@@ -726,6 +747,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
[
|
||||
SAssignNew(BaseLevelCombo, SComboBox<TSharedPtr<FString>>)
|
||||
.OptionsSource(&BaseLevelOptions)
|
||||
.Method(EPopupMethod::UseCurrentWindow)
|
||||
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> Selected, ESelectInfo::Type SelectInfo)
|
||||
{
|
||||
if (!Selected.IsValid()) return;
|
||||
@@ -748,7 +770,12 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
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;
|
||||
|
||||
if (BaseLevelComboText.IsValid())
|
||||
@@ -815,6 +842,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSpawnCatalog()
|
||||
.MaxDesiredHeight(400.0f)
|
||||
[
|
||||
SNew(SScrollBox)
|
||||
.ConsumeMouseWheel(EConsumeMouseWheel::Always)
|
||||
+ SScrollBox::Slot()
|
||||
[
|
||||
CatalogList
|
||||
@@ -834,9 +862,13 @@ void UPS_Editor_MainWidget::NativeConstruct()
|
||||
BaseLevelOptions.Add(MakeShared<FString>(TEXT("None")));
|
||||
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)
|
||||
{
|
||||
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName);
|
||||
FString SceneMapPath = SceneLevelName;
|
||||
if (FString* Found = BaseLevelPathMap.Find(SceneLevelName))
|
||||
{
|
||||
SceneMapPath = *Found;
|
||||
}
|
||||
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath);
|
||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||
|
||||
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)
|
||||
[
|
||||
SNew(SScrollBox)
|
||||
.ConsumeMouseWheel(EConsumeMouseWheel::Always)
|
||||
+ SScrollBox::Slot()
|
||||
[
|
||||
SAssignNew(OutlinerListContainer, SVerticalBox)
|
||||
@@ -1528,12 +1566,12 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
||||
|
||||
for (int32 i = 0; i < EditComp->EditableProperties.Num(); i++)
|
||||
{
|
||||
const FName& Path = EditComp->EditableProperties[i];
|
||||
const FPS_Editor_EditablePropertyEntry& Entry = EditComp->EditableProperties[i];
|
||||
|
||||
FPS_Editor_PropertyRowWidgets Row;
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Path, Row.ComponentName, Row.PropertyName);
|
||||
Row.DisplayName = EditComp->GetDisplayName(Path);
|
||||
Row.Key = Path.ToString();
|
||||
UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, Row.ComponentName, Row.PropertyName);
|
||||
Row.DisplayName = EditComp->GetDisplayName(Entry.Path);
|
||||
Row.Key = Entry.Path.ToString();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolving [%s] comp='%s' prop='%s'"), *Row.Key, *Row.ComponentName, *Row.PropertyName);
|
||||
|
||||
@@ -1573,7 +1611,61 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
||||
// Value widget depends on property type
|
||||
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)
|
||||
+ SHorizontalBox::Slot().AutoWidth()
|
||||
@@ -1609,6 +1701,14 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
||||
+ SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f)
|
||||
[ MakeTextField(Row.VecZ) ];
|
||||
}
|
||||
else if (StructProp->Struct == TBaseStructure<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())
|
||||
{
|
||||
ValueWidget = SNew(SHorizontalBox)
|
||||
@@ -1671,7 +1771,34 @@ void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
||||
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
||||
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())
|
||||
{
|
||||
@@ -1706,6 +1833,12 @@ void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
||||
SetIfNoFocus(Row.VecY, Rot.Yaw);
|
||||
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())
|
||||
{
|
||||
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
|
||||
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");
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
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())
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
if (NewValueText.IsEmpty()) return;
|
||||
if (NewValueText.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ApplyProperty '%s' - NewValueText is EMPTY, aborting"), *Row.Key);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply the new value
|
||||
Row.CachedProperty->ImportText_InContainer(*NewValueText, Target, Target, PPF_None);
|
||||
const TCHAR* ImportResult = Row.CachedProperty->ImportText_InContainer(*NewValueText, Target, Target, PPF_None);
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ApplyProperty '%s' = '%s' -> Import %s"),
|
||||
*Row.Key, *NewValueText, ImportResult ? TEXT("OK") : TEXT("FAILED"));
|
||||
|
||||
// Refresh rendering
|
||||
if (UActorComponent* Comp = Cast<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);
|
||||
|
||||
// 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
|
||||
|
||||
@@ -23,6 +23,12 @@ struct FPS_Editor_PropertyRowWidgets
|
||||
TSharedPtr<SCheckBox> BoolField; // bool
|
||||
TSharedPtr<SEditableTextBox> VecX, VecY, VecZ; // FVector, FRotator
|
||||
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;
|
||||
TSharedPtr<STextBlock> BaseLevelComboText;
|
||||
FString CurrentLevelFilter;
|
||||
/** Maps DisplayName → full MapPath for base levels (from MasterCatalog). */
|
||||
TMap<FString, FString> BaseLevelPathMap;
|
||||
TArray<TSharedPtr<FSlateBrush>> CachedBrushes;
|
||||
|
||||
// Transform fields
|
||||
|
||||
Reference in New Issue
Block a user