Add Phase 3: object spawn system with catalogue UI
Spawn System: - SpawnableComponent: marker component for Blueprint actors (name, category, thumbnail) - SpawnCatalog DataAsset: lists all spawnable Blueprints with Factory for easy creation - SpawnManager: loads catalog, spawns actors in front of camera, ensures Movable mobility - Reads metadata from Blueprint component templates (no temp spawn needed) Catalogue UI: - Left panel with scrollable list grouped by category - Thumbnail cards (80x80) with name below, click to spawn - "Capture All Thumbnails" button on DataAsset: auto-generates from UE thumbnails Undo/Redo improvements: - SpawnAction: undo hides spawned actor, redo shows it - Delete confirmation dialog (Yes/No) before soft-delete - Undo/redo auto-deselects hidden actors and refreshes gizmo - Proper cleanup of hidden actors when actions pruned from stack Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,35 @@ PS_Editor is a runtime editor plugin for Unreal Engine 5.5 that will be integrat
|
||||
|
||||
**Why:** Enable runtime scene editing in packaged builds - place objects, configure properties, define scenarios, manipulate splines, modify lighting, and save/load scenes as JSON.
|
||||
|
||||
**How to apply:** All development is incremental, phase by phase. Phase 1 = GameMode + Camera (completed). Future phases: object spawning/transform, JSON serialization, splines, lighting, AI characters.
|
||||
|
||||
Key decisions:
|
||||
- UI: UMG driven by C++ (not Blueprints)
|
||||
- Gizmos: InteractiveToolsFramework (UE5 built-in)
|
||||
- Serialization: JSON via Json/JsonUtilities modules
|
||||
- UI: UMG driven by C++ (Slate)
|
||||
- Gizmos: Custom actor with BasicShapes + unlit material (InteractiveToolsFramework is editor-only)
|
||||
- Serialization: JSON via Json/JsonUtilities modules (future)
|
||||
- Naming: PS_Editor_ prefix on all files/classes
|
||||
- Plugin path: Unreal/Plugins/PS_Editor/
|
||||
- AZERTY keyboard layout: arrows + A/Q for movement, E/R/T for modes
|
||||
|
||||
## Completed Phases
|
||||
|
||||
### Phase 1 - GameMode + Camera
|
||||
- PS_Editor_GameMode, PS_Editor_Pawn, PS_Editor_PlayerController, PS_Editor_HUD
|
||||
- Camera: Fly (RMB), Pan (LMB drag), Orbit (Alt+LMB / MMB)
|
||||
- Movement: arrows + A (up) / Q (down)
|
||||
|
||||
### Phase 2 - Selection + Transform + Undo
|
||||
- SelectionManager: raycast click-to-select, Ctrl+click multi-select, Movable actors only
|
||||
- Gizmo: custom actor with colored arrows (R/G/B), cones, SDPG_Foreground, constant screen size
|
||||
- Translate/Rotate/Scale via gizmo drag, hotkeys E/R/T
|
||||
- Snap to ground (End key), soft-delete (Delete key)
|
||||
- Undo/Redo system: custom UndoManager, Ctrl+Z / Ctrl+Y, 50 action stack
|
||||
- Properties panel: editable Location/Rotation/Scale fields
|
||||
- Toolbar: mode buttons + snap ground
|
||||
- Material: M_PS_Editor_Gizmo (Unlit, Vector Parameter "Color" → Emissive, Translucent, Disable Depth Test)
|
||||
|
||||
## Next: Phase 3 - Object Spawning
|
||||
- Catalog/list of objects to place
|
||||
- Drag & drop or click to spawn
|
||||
- Duplication of existing objects
|
||||
|
||||
## Next: Phase 4 - JSON Serialization
|
||||
- Save/load scenes as JSON
|
||||
|
||||
@@ -12,3 +12,7 @@ Build command (Editor target, Development):
|
||||
```
|
||||
|
||||
**How to apply:** Use this command to compile after code changes. Timeout should be set to 600000ms (10 min) for safety. The target name is `PS_ProserveEditorEditor` (Editor suffix) for editor builds.
|
||||
|
||||
**When to use CLI vs Live Coding:**
|
||||
- Live Coding (Ctrl+Alt+F11): .cpp only changes (no new UCLASS, USTRUCT, UENUM, UPROPERTY in headers)
|
||||
- Full CLI build (UE must be closed): new files, header changes with new reflected types, Build.cs changes
|
||||
|
||||
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/Sphere.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/Sphere.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/T_Cubeezrzea_Thumb.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/T_Cubeezrzea_Thumb.uasset
Normal file
Binary file not shown.
@@ -13,6 +13,7 @@
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "Misc/MessageDialog.h"
|
||||
|
||||
APS_Editor_Pawn::APS_Editor_Pawn()
|
||||
{
|
||||
@@ -573,6 +574,18 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||
if (!SM || !SM->IsAnythingSelected()) return;
|
||||
|
||||
// Confirmation dialog
|
||||
const int32 Count = SM->GetSelectedActors().Num();
|
||||
const FText Message = FText::Format(
|
||||
NSLOCTEXT("PS_Editor", "DeleteConfirm", "Delete {0} selected object(s)?"),
|
||||
FText::AsNumber(Count));
|
||||
|
||||
EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, Message);
|
||||
if (Result != EAppReturnType::Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Soft-delete: hide actors instead of destroying (enables undo)
|
||||
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||
@@ -584,14 +597,14 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
||||
}
|
||||
|
||||
SM->ClearSelection();
|
||||
Action->Execute(); // Hide the actors
|
||||
Action->Execute();
|
||||
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
{
|
||||
UM->RecordAction(Action);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Soft-deleted %d actor(s)"), Action->Actors.Num());
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Soft-deleted %d actor(s)"), Count);
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleUndo(const FInputActionValue& Value)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_Pawn.h"
|
||||
|
||||
@@ -22,16 +23,45 @@ void APS_Editor_PlayerController::BeginPlay()
|
||||
|
||||
UndoManager = NewObject<UPS_Editor_UndoManager>(this);
|
||||
|
||||
// After undo/redo, refresh the gizmo position
|
||||
// Load spawn catalog and create spawn manager
|
||||
SpawnManager = NewObject<UPS_Editor_SpawnManager>(this);
|
||||
UPS_Editor_SpawnCatalog* LoadedCatalog = SpawnCatalogAsset.LoadSynchronous();
|
||||
SpawnManager->Initialize(this, LoadedCatalog);
|
||||
|
||||
// After undo/redo, deselect hidden actors and refresh gizmo
|
||||
UndoManager->OnUndoRedo.AddWeakLambda(this, [this]()
|
||||
{
|
||||
if (SelectionManager && SelectionManager->IsAnythingSelected())
|
||||
if (SelectionManager)
|
||||
{
|
||||
// Deselect any actors that are now hidden (undone spawn or redone delete)
|
||||
TArray<TWeakObjectPtr<AActor>> ToDeselect;
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SelectionManager->GetSelectedActors())
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
if (Actor->IsHidden())
|
||||
{
|
||||
ToDeselect.Add(Actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const TWeakObjectPtr<AActor>& Weak : ToDeselect)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
SelectionManager->DeselectActor(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh gizmo position
|
||||
if (SelectionManager->IsAnythingSelected())
|
||||
{
|
||||
if (APS_Editor_Gizmo* Gizmo = SelectionManager->GetGizmo())
|
||||
{
|
||||
Gizmo->SetActorLocation(SelectionManager->GetSelectionPivot());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
class UPS_Editor_SelectionManager;
|
||||
class UPS_Editor_UndoManager;
|
||||
class UPS_Editor_SpawnManager;
|
||||
class UPS_Editor_SpawnCatalog;
|
||||
|
||||
/**
|
||||
* Player controller for PS_Editor runtime editing.
|
||||
@@ -25,6 +27,13 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
UPS_Editor_UndoManager* GetUndoManager() const { return UndoManager; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
UPS_Editor_SpawnManager* GetSpawnManager() const { return SpawnManager; }
|
||||
|
||||
/** The spawn catalog DataAsset to load. Set this in the Blueprint or World Settings. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
TSoftObjectPtr<UPS_Editor_SpawnCatalog> SpawnCatalogAsset;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
@@ -36,5 +45,8 @@ private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UPS_Editor_UndoManager> UndoManager;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UPS_Editor_SpawnManager> SpawnManager;
|
||||
|
||||
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ public class PS_Editor : ModuleRules
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "UI", "Widgets"));
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Selection"));
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo"));
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Spawn"));
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
@@ -31,5 +32,11 @@ public class PS_Editor : ModuleRules
|
||||
"Slate",
|
||||
"SlateCore"
|
||||
});
|
||||
|
||||
// Editor-only: Factory for SpawnCatalog DataAsset creation
|
||||
if (Target.bBuildEditor)
|
||||
{
|
||||
PrivateDependencyModuleNames.Add("UnrealEd");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,10 +89,23 @@ void UPS_Editor_UndoManager::PruneHistory()
|
||||
|
||||
void UPS_Editor_UndoManager::CleanupDeleteAction(TSharedPtr<FPS_Editor_Action> Action)
|
||||
{
|
||||
// If a delete action is being pruned from history, truly destroy the hidden actors
|
||||
if (!Action.IsValid() || Action->ActionType != EPS_Editor_ActionType::Delete) return;
|
||||
FPS_Editor_DeleteAction* DeleteAction = static_cast<FPS_Editor_DeleteAction*>(Action.Get());
|
||||
for (const TWeakObjectPtr<AActor>& Weak : DeleteAction->Actors)
|
||||
if (!Action.IsValid()) return;
|
||||
|
||||
// Truly destroy hidden actors when their action is pruned from history
|
||||
TArray<TWeakObjectPtr<AActor>>* ActorsToClean = nullptr;
|
||||
|
||||
if (Action->ActionType == EPS_Editor_ActionType::Delete)
|
||||
{
|
||||
ActorsToClean = &static_cast<FPS_Editor_DeleteAction*>(Action.Get())->Actors;
|
||||
}
|
||||
else if (Action->ActionType == EPS_Editor_ActionType::Spawn)
|
||||
{
|
||||
ActorsToClean = &static_cast<FPS_Editor_SpawnAction*>(Action.Get())->Actors;
|
||||
}
|
||||
|
||||
if (ActorsToClean)
|
||||
{
|
||||
for (const TWeakObjectPtr<AActor>& Weak : *ActorsToClean)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
@@ -102,4 +115,5 @@ void UPS_Editor_UndoManager::CleanupDeleteAction(TSharedPtr<FPS_Editor_Action> A
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
enum class EPS_Editor_ActionType : uint8
|
||||
{
|
||||
Transform,
|
||||
Delete
|
||||
Delete,
|
||||
Spawn
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -110,6 +111,49 @@ struct FPS_Editor_DeleteAction : public FPS_Editor_Action
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a spawn action. Undo hides the spawned actor, redo shows it again.
|
||||
*/
|
||||
struct FPS_Editor_SpawnAction : public FPS_Editor_Action
|
||||
{
|
||||
FPS_Editor_SpawnAction() : FPS_Editor_Action(EPS_Editor_ActionType::Spawn) {}
|
||||
|
||||
TArray<TWeakObjectPtr<AActor>> Actors;
|
||||
|
||||
virtual void Execute() override
|
||||
{
|
||||
// Redo: show the spawned actors
|
||||
for (const TWeakObjectPtr<AActor>& Weak : Actors)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
Actor->SetActorHiddenInGame(false);
|
||||
Actor->SetActorEnableCollision(true);
|
||||
Actor->SetActorTickEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void Undo() override
|
||||
{
|
||||
// Undo spawn: hide the actors
|
||||
for (const TWeakObjectPtr<AActor>& Weak : Actors)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
Actor->SetActorHiddenInGame(true);
|
||||
Actor->SetActorEnableCollision(false);
|
||||
Actor->SetActorTickEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return FString::Printf(TEXT("Spawn %d actor(s)"), Actors.Num());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Runtime undo/redo manager for PS_Editor.
|
||||
* Maintains a linear action stack with a cursor.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "PS_Editor_SpawnCatalog.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "PS_Editor_SpawnableComponent.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "ThumbnailRendering/ThumbnailManager.h"
|
||||
#include "ObjectTools.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "UObject/SavePackage.h"
|
||||
#include "Misc/PackageName.h"
|
||||
|
||||
void UPS_Editor_SpawnCatalog::CaptureAllThumbnails()
|
||||
{
|
||||
int32 Captured = 0;
|
||||
|
||||
for (FPS_Editor_SpawnEntry& Entry : Entries)
|
||||
{
|
||||
UClass* LoadedClass = Entry.ActorClass.LoadSynchronous();
|
||||
if (!LoadedClass)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load class for catalog entry"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the SpawnableComponent template
|
||||
UPS_Editor_SpawnableComponent* SpawnComp = nullptr;
|
||||
TArray<UObject*> ClassSubObjects;
|
||||
GetObjectsWithOuter(LoadedClass, ClassSubObjects, true);
|
||||
for (UObject* Sub : ClassSubObjects)
|
||||
{
|
||||
SpawnComp = Cast<UPS_Editor_SpawnableComponent>(Sub);
|
||||
if (SpawnComp) break;
|
||||
}
|
||||
|
||||
if (!SpawnComp)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: No SpawnableComponent on %s, skipping"), *LoadedClass->GetName());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if already has a custom thumbnail
|
||||
if (SpawnComp->Thumbnail)
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: %s already has a thumbnail, skipping"), *LoadedClass->GetName());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the Blueprint
|
||||
UBlueprint* Blueprint = Cast<UBlueprint>(LoadedClass->ClassGeneratedBy);
|
||||
if (!Blueprint)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: %s is not a Blueprint, skipping"), *LoadedClass->GetName());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate thumbnail
|
||||
FObjectThumbnail* ObjThumb = ThumbnailTools::GenerateThumbnailForObjectToSaveToDisk(Blueprint);
|
||||
if (!ObjThumb || ObjThumb->GetImageWidth() <= 0 || ObjThumb->GetImageHeight() <= 0)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to generate thumbnail for %s"), *Blueprint->GetName());
|
||||
continue;
|
||||
}
|
||||
|
||||
const int32 Width = ObjThumb->GetImageWidth();
|
||||
const int32 Height = ObjThumb->GetImageHeight();
|
||||
const TArray<uint8>& PixelData = ObjThumb->GetUncompressedImageData();
|
||||
|
||||
if (PixelData.Num() != Width * Height * 4)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save texture next to the Blueprint
|
||||
FString BlueprintPath = Blueprint->GetOutermost()->GetName();
|
||||
FString BasePath = FPaths::GetPath(BlueprintPath);
|
||||
FString AssetName = FString::Printf(TEXT("T_%s_Thumb"), *Blueprint->GetName());
|
||||
FString FullPath = FString::Printf(TEXT("%s/%s"), *BasePath, *AssetName);
|
||||
|
||||
UPackage* Package = CreatePackage(*FullPath);
|
||||
if (!Package) continue;
|
||||
|
||||
UTexture2D* Texture = NewObject<UTexture2D>(Package, *AssetName, RF_Public | RF_Standalone);
|
||||
Texture->Source.Init(Width, Height, 1, 1, TSF_BGRA8);
|
||||
|
||||
uint8* MipData = Texture->Source.LockMip(0);
|
||||
FMemory::Memcpy(MipData, PixelData.GetData(), PixelData.Num());
|
||||
Texture->Source.UnlockMip(0);
|
||||
|
||||
Texture->SRGB = true;
|
||||
Texture->CompressionSettings = TC_Default;
|
||||
Texture->MipGenSettings = TMGS_NoMipmaps;
|
||||
Texture->PostEditChange();
|
||||
Texture->UpdateResource();
|
||||
|
||||
FString PackageFilename = FPackageName::LongPackageNameToFilename(FullPath, FPackageName::GetAssetPackageExtension());
|
||||
FSavePackageArgs SaveArgs;
|
||||
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
||||
UPackage::SavePackage(Package, Texture, *PackageFilename, SaveArgs);
|
||||
FAssetRegistryModule::AssetCreated(Texture);
|
||||
|
||||
// Assign to the component and mark Blueprint dirty
|
||||
SpawnComp->Thumbnail = Texture;
|
||||
Blueprint->MarkPackageDirty();
|
||||
|
||||
Captured++;
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Captured thumbnail for %s -> %s"), *Blueprint->GetName(), *FullPath);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Captured %d thumbnails"), Captured);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "Engine/AssetManager.h"
|
||||
#include "PS_Editor_SpawnCatalog.generated.h"
|
||||
|
||||
/**
|
||||
* A single entry in the spawn catalogue.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct FPS_Editor_SpawnEntry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** The Blueprint class to spawn. Must have a PS_Editor_SpawnableComponent for name/category. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
TSoftClassPtr<AActor> ActorClass;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data asset listing all Blueprints available for spawning in the PS_Editor runtime catalogue.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Create a DataAsset of type PS_Editor_SpawnCatalog in Content Browser
|
||||
* 2. Add entries pointing to your prepared Blueprints
|
||||
* 3. Click "Capture All Thumbnails" to auto-generate thumbnails
|
||||
* 4. Reference this DataAsset in the PlayerController's SpawnCatalogAsset property
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class PS_EDITOR_API UPS_Editor_SpawnCatalog : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** List of spawnable actors. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
TArray<FPS_Editor_SpawnEntry> Entries;
|
||||
|
||||
#if WITH_EDITOR
|
||||
/** Captures UE-generated thumbnails for all Blueprints in the catalog and assigns them to their SpawnableComponents. */
|
||||
UFUNCTION(CallInEditor, Category = "PS Editor")
|
||||
void CaptureAllThumbnails();
|
||||
#endif
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "PS_Editor_SpawnCatalogFactory.h"
|
||||
#include "PS_Editor_SpawnCatalog.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "PS_Editor"
|
||||
|
||||
UPS_Editor_SpawnCatalogFactory::UPS_Editor_SpawnCatalogFactory()
|
||||
{
|
||||
SupportedClass = UPS_Editor_SpawnCatalog::StaticClass();
|
||||
bCreateNew = true;
|
||||
bEditAfterNew = true;
|
||||
}
|
||||
|
||||
UObject* UPS_Editor_SpawnCatalogFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||
{
|
||||
return NewObject<UPS_Editor_SpawnCatalog>(InParent, InClass, InName, Flags);
|
||||
}
|
||||
|
||||
FText UPS_Editor_SpawnCatalogFactory::GetDisplayName() const
|
||||
{
|
||||
return LOCTEXT("SpawnCatalogDisplayName", "PS Editor Spawn Catalog");
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Factories/Factory.h"
|
||||
#include "PS_Editor_SpawnCatalogFactory.generated.h"
|
||||
|
||||
UCLASS(HideCategories = Object)
|
||||
class UPS_Editor_SpawnCatalogFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_Editor_SpawnCatalogFactory();
|
||||
|
||||
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
|
||||
virtual bool ShouldShowInNewMenu() const override { return true; }
|
||||
virtual FText GetDisplayName() const override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_SpawnableComponent.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
void UPS_Editor_SpawnManager::Initialize(APlayerController* InOwnerPC, UPS_Editor_SpawnCatalog* InCatalog)
|
||||
{
|
||||
OwnerPC = InOwnerPC;
|
||||
Catalog = InCatalog;
|
||||
|
||||
if (Catalog)
|
||||
{
|
||||
ResolveCatalog();
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SpawnCatalog loaded with %d entries"), ResolvedEntries.Num());
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: No SpawnCatalog assigned. Spawn catalogue will be empty."));
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_SpawnManager::ResolveCatalog()
|
||||
{
|
||||
ResolvedEntries.Empty();
|
||||
|
||||
if (!Catalog)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const FPS_Editor_SpawnEntry& Entry : Catalog->Entries)
|
||||
{
|
||||
// Synchronously load the class
|
||||
UClass* LoadedClass = Entry.ActorClass.LoadSynchronous();
|
||||
if (!LoadedClass)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load class for catalog entry"));
|
||||
continue;
|
||||
}
|
||||
|
||||
FPS_Editor_ResolvedEntry Resolved;
|
||||
Resolved.ActorClass = LoadedClass;
|
||||
|
||||
// Find the SpawnableComponent template in the Blueprint class hierarchy.
|
||||
// Blueprint-added components are stored as subobjects of the UBlueprintGeneratedClass.
|
||||
UPS_Editor_SpawnableComponent* SpawnComp = nullptr;
|
||||
|
||||
// Search BPGC subobjects (Blueprint-added components)
|
||||
TArray<UObject*> ClassSubObjects;
|
||||
GetObjectsWithOuter(LoadedClass, ClassSubObjects, true);
|
||||
for (UObject* Sub : ClassSubObjects)
|
||||
{
|
||||
SpawnComp = Cast<UPS_Editor_SpawnableComponent>(Sub);
|
||||
if (SpawnComp)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check CDO subobjects (C++ constructor components)
|
||||
if (!SpawnComp)
|
||||
{
|
||||
AActor* CDO = LoadedClass->GetDefaultObject<AActor>();
|
||||
if (CDO)
|
||||
{
|
||||
SpawnComp = CDO->FindComponentByClass<UPS_Editor_SpawnableComponent>();
|
||||
}
|
||||
}
|
||||
|
||||
if (SpawnComp && !SpawnComp->DisplayName.IsEmpty())
|
||||
{
|
||||
Resolved.DisplayName = SpawnComp->DisplayName;
|
||||
}
|
||||
else
|
||||
{
|
||||
Resolved.DisplayName = LoadedClass->GetName();
|
||||
Resolved.DisplayName.RemoveFromEnd(TEXT("_C"));
|
||||
}
|
||||
|
||||
Resolved.Category = SpawnComp ? SpawnComp->Category : TEXT("Default");
|
||||
Resolved.Thumbnail = SpawnComp ? SpawnComp->Thumbnail : nullptr;
|
||||
|
||||
ResolvedEntries.Add(Resolved);
|
||||
}
|
||||
}
|
||||
|
||||
AActor* UPS_Editor_SpawnManager::SpawnFromCatalog(int32 Index)
|
||||
{
|
||||
return SpawnFromCatalogAtLocation(Index, ComputeSpawnLocation());
|
||||
}
|
||||
|
||||
AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector WorldLocation)
|
||||
{
|
||||
if (!ResolvedEntries.IsValidIndex(Index))
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Invalid catalog index %d"), Index);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
APlayerController* PC = OwnerPC.Get();
|
||||
if (!PC || !PC->GetWorld())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const FPS_Editor_ResolvedEntry& Entry = ResolvedEntries[Index];
|
||||
if (!Entry.ActorClass)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||
|
||||
AActor* SpawnedActor = PC->GetWorld()->SpawnActor<AActor>(Entry.ActorClass, WorldLocation, FRotator::ZeroRotator, SpawnParams);
|
||||
if (!SpawnedActor)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to spawn %s"), *Entry.DisplayName);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Ensure the spawned actor is Movable (so it's selectable)
|
||||
if (USceneComponent* Root = SpawnedActor->GetRootComponent())
|
||||
{
|
||||
Root->SetMobility(EComponentMobility::Movable);
|
||||
}
|
||||
|
||||
// Record undo action (undo = hide the spawned actor)
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||
{
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
{
|
||||
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
|
||||
Action->Actors.Add(SpawnedActor);
|
||||
UM->RecordAction(Action);
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spawned %s at %s"), *Entry.DisplayName, *WorldLocation.ToString());
|
||||
return SpawnedActor;
|
||||
}
|
||||
|
||||
FVector UPS_Editor_SpawnManager::ComputeSpawnLocation() const
|
||||
{
|
||||
APlayerController* PC = OwnerPC.Get();
|
||||
if (!PC)
|
||||
{
|
||||
return FVector::ZeroVector;
|
||||
}
|
||||
|
||||
FVector CameraLocation;
|
||||
FRotator CameraRotation;
|
||||
PC->GetPlayerViewPoint(CameraLocation, CameraRotation);
|
||||
|
||||
// Spawn 500 units in front of the camera
|
||||
return CameraLocation + CameraRotation.Vector() * 500.0f;
|
||||
}
|
||||
|
||||
TArray<FString> UPS_Editor_SpawnManager::GetCategories() const
|
||||
{
|
||||
TArray<FString> Categories;
|
||||
for (const FPS_Editor_ResolvedEntry& Entry : ResolvedEntries)
|
||||
{
|
||||
Categories.AddUnique(Entry.Category);
|
||||
}
|
||||
Categories.Sort();
|
||||
return Categories;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/NoExportTypes.h"
|
||||
#include "PS_Editor_SpawnCatalog.h"
|
||||
#include "PS_Editor_SpawnManager.generated.h"
|
||||
|
||||
class APlayerController;
|
||||
class UPS_Editor_UndoManager;
|
||||
|
||||
/**
|
||||
* Resolved catalog entry with display info ready for UI.
|
||||
*/
|
||||
USTRUCT()
|
||||
struct FPS_Editor_ResolvedEntry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
TSubclassOf<AActor> ActorClass;
|
||||
|
||||
FString DisplayName;
|
||||
FString Category;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UTexture2D> Thumbnail;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages spawning of prepared Blueprint actors at runtime.
|
||||
* Loads a SpawnCatalog DataAsset and provides spawn functions.
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API UPS_Editor_SpawnManager : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Initialize with owning controller and load catalog. */
|
||||
void Initialize(APlayerController* InOwnerPC, UPS_Editor_SpawnCatalog* InCatalog);
|
||||
|
||||
/** Spawn the actor at the given catalog index, positioned in front of the camera. Returns the spawned actor. */
|
||||
AActor* SpawnFromCatalog(int32 Index);
|
||||
|
||||
/** Spawn the actor at a specific world location. */
|
||||
AActor* SpawnFromCatalogAtLocation(int32 Index, FVector WorldLocation);
|
||||
|
||||
/** Get the resolved catalog entries for UI display. */
|
||||
const TArray<FPS_Editor_ResolvedEntry>& GetResolvedEntries() const { return ResolvedEntries; }
|
||||
|
||||
/** Get unique categories for filtering. */
|
||||
TArray<FString> GetCategories() const;
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UPS_Editor_SpawnCatalog> Catalog;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FPS_Editor_ResolvedEntry> ResolvedEntries;
|
||||
|
||||
/** Resolve all catalog entries (load classes, read SpawnableComponent metadata). */
|
||||
void ResolveCatalog();
|
||||
|
||||
/** Compute spawn location in front of the camera. */
|
||||
FVector ComputeSpawnLocation() const;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "PS_Editor_SpawnableComponent.h"
|
||||
|
||||
UPS_Editor_SpawnableComponent::UPS_Editor_SpawnableComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "PS_Editor_SpawnableComponent.generated.h"
|
||||
|
||||
/**
|
||||
* Marker component for Blueprints that can be spawned via the PS_Editor catalogue.
|
||||
* Add this component to any Blueprint to make it appear in the runtime spawn catalogue.
|
||||
*/
|
||||
UCLASS(ClassGroup = (PSEditor), meta = (BlueprintSpawnableComponent, DisplayName = "PS Editor Spawnable"))
|
||||
class PS_EDITOR_API UPS_Editor_SpawnableComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_Editor_SpawnableComponent();
|
||||
|
||||
/** Display name shown in the spawn catalogue UI. If empty, uses the actor's class name. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
|
||||
FString DisplayName;
|
||||
|
||||
/** Category for filtering in the catalogue (e.g. "Characters", "Props", "Vehicles"). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
|
||||
FString Category = TEXT("Default");
|
||||
|
||||
/** 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;
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "PS_Editor_SpawnableComponentCustomization.h"
|
||||
#include "PS_Editor_SpawnableComponent.h"
|
||||
#include "DetailLayoutBuilder.h"
|
||||
#include "DetailCategoryBuilder.h"
|
||||
#include "DetailWidgetRow.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "ThumbnailRendering/ThumbnailManager.h"
|
||||
#include "ObjectTools.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "UObject/SavePackage.h"
|
||||
#include "Misc/PackageName.h"
|
||||
#include "HAL/FileManager.h"
|
||||
|
||||
TSharedRef<IDetailCustomization> FPS_Editor_SpawnableComponentCustomization::MakeInstance()
|
||||
{
|
||||
return MakeShareable(new FPS_Editor_SpawnableComponentCustomization);
|
||||
}
|
||||
|
||||
void FPS_Editor_SpawnableComponentCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
|
||||
{
|
||||
TArray<TWeakObjectPtr<UObject>> Objects;
|
||||
DetailBuilder.GetObjectsBeingCustomized(Objects);
|
||||
if (Objects.Num() > 0)
|
||||
{
|
||||
EditedComponent = Objects[0];
|
||||
}
|
||||
|
||||
IDetailCategoryBuilder& Category = DetailBuilder.EditCategory("PS Editor|Spawnable");
|
||||
Category.AddCustomRow(FText::FromString(TEXT("Capture Thumbnail")))
|
||||
.NameContent()
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Auto Thumbnail")))
|
||||
.Font(IDetailLayoutBuilder::GetDetailFont())
|
||||
]
|
||||
.ValueContent()
|
||||
.MinDesiredWidth(150.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.HAlign(HAlign_Center)
|
||||
.OnClicked(FOnClicked::CreateRaw(this, &FPS_Editor_SpawnableComponentCustomization::OnCaptureThumbnailClicked))
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Capture Thumbnail")))
|
||||
.Font(IDetailLayoutBuilder::GetDetailFont())
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
FReply FPS_Editor_SpawnableComponentCustomization::OnCaptureThumbnailClicked()
|
||||
{
|
||||
UPS_Editor_SpawnableComponent* Comp = Cast<UPS_Editor_SpawnableComponent>(EditedComponent.Get());
|
||||
if (!Comp)
|
||||
{
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
// Find the owning Blueprint
|
||||
AActor* OwnerActor = Comp->GetOwner();
|
||||
if (!OwnerActor)
|
||||
{
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
UClass* ActorClass = OwnerActor->GetClass();
|
||||
UBlueprint* Blueprint = Cast<UBlueprint>(ActorClass->ClassGeneratedBy);
|
||||
if (!Blueprint)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Cannot capture thumbnail - not a Blueprint actor"));
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
// Generate thumbnail
|
||||
FObjectThumbnail* ObjThumb = ThumbnailTools::GenerateThumbnailForObjectToSaveToDisk(Blueprint);
|
||||
if (!ObjThumb || ObjThumb->GetImageWidth() <= 0 || ObjThumb->GetImageHeight() <= 0)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to generate thumbnail for %s"), *Blueprint->GetName());
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
const int32 Width = ObjThumb->GetImageWidth();
|
||||
const int32 Height = ObjThumb->GetImageHeight();
|
||||
const TArray<uint8>& PixelData = ObjThumb->GetUncompressedImageData();
|
||||
|
||||
if (PixelData.Num() != Width * Height * 4)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Invalid thumbnail pixel data"));
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
// Save as a persistent texture asset next to the Blueprint
|
||||
FString BlueprintPath = Blueprint->GetOutermost()->GetName();
|
||||
FString BasePath = FPaths::GetPath(BlueprintPath);
|
||||
FString AssetName = FString::Printf(TEXT("T_%s_Thumb"), *Blueprint->GetName());
|
||||
FString FullPath = FString::Printf(TEXT("%s/%s"), *BasePath, *AssetName);
|
||||
|
||||
UPackage* Package = CreatePackage(*FullPath);
|
||||
if (!Package)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to create package for thumbnail"));
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
UTexture2D* Texture = NewObject<UTexture2D>(Package, *AssetName, RF_Public | RF_Standalone);
|
||||
Texture->Source.Init(Width, Height, 1, 1, TSF_BGRA8);
|
||||
|
||||
uint8* MipData = Texture->Source.LockMip(0);
|
||||
FMemory::Memcpy(MipData, PixelData.GetData(), PixelData.Num());
|
||||
Texture->Source.UnlockMip(0);
|
||||
|
||||
Texture->SRGB = true;
|
||||
Texture->CompressionSettings = TC_Default;
|
||||
Texture->MipGenSettings = TMGS_NoMipmaps;
|
||||
Texture->PostEditChange();
|
||||
Texture->UpdateResource();
|
||||
|
||||
// Save the package
|
||||
FString PackageFilename = FPackageName::LongPackageNameToFilename(FullPath, FPackageName::GetAssetPackageExtension());
|
||||
FSavePackageArgs SaveArgs;
|
||||
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
||||
UPackage::SavePackage(Package, Texture, *PackageFilename, SaveArgs);
|
||||
|
||||
FAssetRegistryModule::AssetCreated(Texture);
|
||||
|
||||
// Assign to the component
|
||||
Comp->Thumbnail = Texture;
|
||||
Comp->MarkPackageDirty();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Thumbnail captured and saved to %s"), *FullPath);
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "IDetailCustomization.h"
|
||||
|
||||
/**
|
||||
* Detail panel customization for UPS_Editor_SpawnableComponent.
|
||||
* Adds a "Capture Thumbnail" button that grabs the Blueprint's auto-generated
|
||||
* thumbnail and saves it as a persistent UTexture2D asset.
|
||||
*/
|
||||
class FPS_Editor_SpawnableComponentCustomization : public IDetailCustomization
|
||||
{
|
||||
public:
|
||||
static TSharedRef<IDetailCustomization> MakeInstance();
|
||||
|
||||
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
|
||||
|
||||
private:
|
||||
FReply OnCaptureThumbnailClicked();
|
||||
|
||||
TWeakObjectPtr<UObject> EditedComponent;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -17,6 +17,7 @@ void APS_Editor_HUD::BeginPlay()
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||
{
|
||||
MainWidget->SetSelectionManager(EditorPC->GetSelectionManager());
|
||||
MainWidget->SetSpawnManager(EditorPC->GetSpawnManager());
|
||||
}
|
||||
|
||||
MainWidget->AddToViewport(0);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "PS_Editor_MainWidget.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
@@ -7,6 +8,8 @@
|
||||
#include "Widgets/Layout/SBorder.h"
|
||||
#include "Widgets/Layout/SScrollBox.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Images/SImage.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "Widgets/Input/SEditableTextBox.h"
|
||||
#include "Widgets/SOverlay.h"
|
||||
|
||||
@@ -15,6 +18,11 @@ void UPS_Editor_MainWidget::SetSelectionManager(UPS_Editor_SelectionManager* InS
|
||||
SelectionManager = InSelectionManager;
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager)
|
||||
{
|
||||
SpawnManager = InSpawnManager;
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||
{
|
||||
return SNew(SOverlay)
|
||||
@@ -63,6 +71,14 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||
BuildToolbar()
|
||||
]
|
||||
]
|
||||
// Left panel: Spawn Catalogue
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(HAlign_Left)
|
||||
.VAlign(VAlign_Center)
|
||||
.Padding(16.0f, 80.0f, 0.0f, 60.0f)
|
||||
[
|
||||
BuildSpawnCatalog()
|
||||
]
|
||||
// Right panel: Properties
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(HAlign_Right)
|
||||
@@ -242,9 +258,175 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildPropertiesPanel()
|
||||
];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSpawnCatalog()
|
||||
{
|
||||
TSharedRef<SVerticalBox> CatalogList = SNew(SVerticalBox);
|
||||
|
||||
// Header
|
||||
CatalogList->AddSlot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 0.0f, 0.0f, 4.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Spawn Catalogue")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||
.ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f))
|
||||
];
|
||||
|
||||
// Note: entries will be populated in NativeConstruct when SpawnManager is set.
|
||||
// For now, show a placeholder. The actual list is built dynamically.
|
||||
CatalogList->AddSlot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SAssignNew(SpawnListContainer, SVerticalBox)
|
||||
];
|
||||
|
||||
return SNew(SBorder)
|
||||
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||
.Padding(FMargin(10.0f, 8.0f))
|
||||
[
|
||||
SNew(SBox)
|
||||
.MinDesiredWidth(180.0f)
|
||||
.MaxDesiredHeight(400.0f)
|
||||
[
|
||||
SNew(SScrollBox)
|
||||
+ SScrollBox::Slot()
|
||||
[
|
||||
CatalogList
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
PopulateSpawnCatalog();
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::PopulateSpawnCatalog()
|
||||
{
|
||||
if (!SpawnListContainer.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SpawnListContainer->ClearChildren();
|
||||
|
||||
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||
if (!SM)
|
||||
{
|
||||
SpawnListContainer->AddSlot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("No catalog loaded")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Italic", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
const TArray<FPS_Editor_ResolvedEntry>& Entries = SM->GetResolvedEntries();
|
||||
if (Entries.Num() == 0)
|
||||
{
|
||||
SpawnListContainer->AddSlot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Catalog is empty")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Italic", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by category
|
||||
FString CurrentCategory;
|
||||
for (int32 i = 0; i < Entries.Num(); ++i)
|
||||
{
|
||||
const FPS_Editor_ResolvedEntry& Entry = Entries[i];
|
||||
|
||||
// Category header
|
||||
if (Entry.Category != CurrentCategory)
|
||||
{
|
||||
CurrentCategory = Entry.Category;
|
||||
SpawnListContainer->AddSlot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 6.0f, 0.0f, 2.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(CurrentCategory))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f))
|
||||
];
|
||||
}
|
||||
|
||||
// Spawn card: thumbnail on top, name below
|
||||
const int32 EntryIndex = i;
|
||||
const float CardSize = 80.0f;
|
||||
|
||||
TSharedRef<SVerticalBox> CardContent = SNew(SVerticalBox);
|
||||
|
||||
// Thumbnail
|
||||
if (Entry.Thumbnail)
|
||||
{
|
||||
TSharedPtr<FSlateBrush> Brush = MakeShared<FSlateBrush>();
|
||||
Brush->SetResourceObject(Entry.Thumbnail);
|
||||
Brush->ImageSize = FVector2D(CardSize, CardSize);
|
||||
Brush->DrawAs = ESlateBrushDrawType::Image;
|
||||
Brush->Tiling = ESlateBrushTileType::NoTile;
|
||||
CachedBrushes.Add(Brush);
|
||||
|
||||
TWeakPtr<FSlateBrush> WeakBrush = Brush;
|
||||
CardContent->AddSlot()
|
||||
.AutoHeight()
|
||||
.HAlign(HAlign_Center)
|
||||
[
|
||||
SNew(SImage)
|
||||
.Image_Lambda([WeakBrush]() -> const FSlateBrush*
|
||||
{
|
||||
return WeakBrush.IsValid() ? WeakBrush.Pin().Get() : nullptr;
|
||||
})
|
||||
.DesiredSizeOverride(FVector2D(CardSize, CardSize))
|
||||
];
|
||||
}
|
||||
|
||||
// Name below thumbnail
|
||||
CardContent->AddSlot()
|
||||
.AutoHeight()
|
||||
.HAlign(HAlign_Center)
|
||||
.Padding(0.0f, 4.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(Entry.DisplayName))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||
.Justification(ETextJustify::Center)
|
||||
.ColorAndOpacity(FLinearColor::White)
|
||||
];
|
||||
|
||||
SpawnListContainer->AddSlot()
|
||||
.AutoHeight()
|
||||
.Padding(4.0f, 4.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this, EntryIndex]() -> FReply
|
||||
{
|
||||
if (UPS_Editor_SpawnManager* Mgr = SpawnManager.Get())
|
||||
{
|
||||
Mgr->SpawnFromCatalog(EntryIndex);
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SNew(SBox)
|
||||
.MinDesiredWidth(CardSize + 8.0f)
|
||||
[
|
||||
CardContent
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "PS_Editor_MainWidget.generated.h"
|
||||
|
||||
class UPS_Editor_SelectionManager;
|
||||
class UPS_Editor_SpawnManager;
|
||||
|
||||
/**
|
||||
* Main editor widget for PS_Editor.
|
||||
@@ -18,8 +19,9 @@ class PS_EDITOR_API UPS_Editor_MainWidget : public UUserWidget
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Set the selection manager reference. Must be called after construction. */
|
||||
/** Set manager references. Must be called after construction. */
|
||||
void SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager);
|
||||
void SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager);
|
||||
|
||||
protected:
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
@@ -30,10 +32,17 @@ private:
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
|
||||
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<UPS_Editor_SpawnManager> SpawnManager;
|
||||
|
||||
// ---- Slate widget refs for dynamic updates ----
|
||||
TSharedPtr<STextBlock> ModeText;
|
||||
TSharedPtr<STextBlock> SelectionInfoText;
|
||||
|
||||
// Spawn catalog
|
||||
TSharedPtr<SVerticalBox> SpawnListContainer;
|
||||
TArray<TSharedPtr<FSlateBrush>> CachedBrushes;
|
||||
|
||||
// Transform fields
|
||||
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||
@@ -42,6 +51,8 @@ private:
|
||||
// ---- Helpers ----
|
||||
TSharedRef<SWidget> BuildToolbar();
|
||||
TSharedRef<SWidget> BuildPropertiesPanel();
|
||||
TSharedRef<SWidget> BuildSpawnCatalog();
|
||||
void PopulateSpawnCatalog();
|
||||
void RefreshPropertiesFromSelection();
|
||||
void ApplyTransformFromUI();
|
||||
FString FloatToString(float Value) const;
|
||||
|
||||
Reference in New Issue
Block a user