diff --git a/.claude/memory/project_ps_editor.md b/.claude/memory/project_ps_editor.md index 2de4f48..eddcbbd 100644 --- a/.claude/memory/project_ps_editor.md +++ b/.claude/memory/project_ps_editor.md @@ -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 diff --git a/.claude/memory/reference_build.md b/.claude/memory/reference_build.md index ccf3e45..f522f1d 100644 --- a/.claude/memory/reference_build.md +++ b/.claude/memory/reference_build.md @@ -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 diff --git a/Unreal/Content/test.umap b/Unreal/Content/test.umap index e87f29c..33c7b5b 100644 Binary files a/Unreal/Content/test.umap and b/Unreal/Content/test.umap differ diff --git a/Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset b/Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset new file mode 100644 index 0000000..5b3fc78 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset differ diff --git a/Unreal/Plugins/PS_Editor/Content/NewPS_Editor_SpawnCatalog.uasset b/Unreal/Plugins/PS_Editor/Content/NewPS_Editor_SpawnCatalog.uasset new file mode 100644 index 0000000..f78e256 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/NewPS_Editor_SpawnCatalog.uasset differ diff --git a/Unreal/Plugins/PS_Editor/Content/PS_Editor_PlayerController.uasset b/Unreal/Plugins/PS_Editor/Content/PS_Editor_PlayerController.uasset index 7f4dc85..8459e9f 100644 Binary files a/Unreal/Plugins/PS_Editor/Content/PS_Editor_PlayerController.uasset and b/Unreal/Plugins/PS_Editor/Content/PS_Editor_PlayerController.uasset differ diff --git a/Unreal/Plugins/PS_Editor/Content/Sphere.uasset b/Unreal/Plugins/PS_Editor/Content/Sphere.uasset new file mode 100644 index 0000000..904db76 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/Sphere.uasset differ diff --git a/Unreal/Plugins/PS_Editor/Content/T_Cubeezrzea_Thumb.uasset b/Unreal/Plugins/PS_Editor/Content/T_Cubeezrzea_Thumb.uasset new file mode 100644 index 0000000..5530dd6 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/T_Cubeezrzea_Thumb.uasset differ diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp index f80c25c..773438e 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp @@ -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 Action = MakeShared(); for (const TWeakObjectPtr& 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) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp index 3beed29..d3196e6 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp @@ -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,14 +23,43 @@ void APS_Editor_PlayerController::BeginPlay() UndoManager = NewObject(this); - // After undo/redo, refresh the gizmo position + // Load spawn catalog and create spawn manager + SpawnManager = NewObject(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) { - if (APS_Editor_Gizmo* Gizmo = SelectionManager->GetGizmo()) + // Deselect any actors that are now hidden (undone spawn or redone delete) + TArray> ToDeselect; + for (const TWeakObjectPtr& Weak : SelectionManager->GetSelectedActors()) { - Gizmo->SetActorLocation(SelectionManager->GetSelectionPivot()); + if (AActor* Actor = Weak.Get()) + { + if (Actor->IsHidden()) + { + ToDeselect.Add(Actor); + } + } + } + for (const TWeakObjectPtr& 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()); + } } } }); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h index 4c45df5..37385e7 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h @@ -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 SpawnCatalogAsset; + protected: virtual void BeginPlay() override; virtual void OnPossess(APawn* InPawn) override; @@ -36,5 +45,8 @@ private: UPROPERTY() TObjectPtr UndoManager; + UPROPERTY() + TObjectPtr SpawnManager; + void HandleEditorClick(FVector2D ScreenPos, bool bAdditive); }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs index 9af7921..4b34e74 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs @@ -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"); + } } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp index 87de4a6..6135a02 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp @@ -89,16 +89,30 @@ void UPS_Editor_UndoManager::PruneHistory() void UPS_Editor_UndoManager::CleanupDeleteAction(TSharedPtr 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(Action.Get()); - for (const TWeakObjectPtr& Weak : DeleteAction->Actors) + if (!Action.IsValid()) return; + + // Truly destroy hidden actors when their action is pruned from history + TArray>* ActorsToClean = nullptr; + + if (Action->ActionType == EPS_Editor_ActionType::Delete) { - if (AActor* Actor = Weak.Get()) + ActorsToClean = &static_cast(Action.Get())->Actors; + } + else if (Action->ActionType == EPS_Editor_ActionType::Spawn) + { + ActorsToClean = &static_cast(Action.Get())->Actors; + } + + if (ActorsToClean) + { + for (const TWeakObjectPtr& Weak : *ActorsToClean) { - if (Actor->IsHidden()) + if (AActor* Actor = Weak.Get()) { - Actor->Destroy(); + if (Actor->IsHidden()) + { + Actor->Destroy(); + } } } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h index e6c42be..03b0848 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h @@ -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> Actors; + + virtual void Execute() override + { + // Redo: show the spawned actors + for (const TWeakObjectPtr& 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& 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. diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.cpp new file mode 100644 index 0000000..a4dac6a --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.cpp @@ -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 ClassSubObjects; + GetObjectsWithOuter(LoadedClass, ClassSubObjects, true); + for (UObject* Sub : ClassSubObjects) + { + SpawnComp = Cast(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(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& 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(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 diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h new file mode 100644 index 0000000..c546003 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h @@ -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 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 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 +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalogFactory.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalogFactory.cpp new file mode 100644 index 0000000..7e0759c --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalogFactory.cpp @@ -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(InParent, InClass, InName, Flags); +} + +FText UPS_Editor_SpawnCatalogFactory::GetDisplayName() const +{ + return LOCTEXT("SpawnCatalogDisplayName", "PS Editor Spawn Catalog"); +} + +#undef LOCTEXT_NAMESPACE + +#endif diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalogFactory.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalogFactory.h new file mode 100644 index 0000000..1074972 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalogFactory.h @@ -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 diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp new file mode 100644 index 0000000..67c6673 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp @@ -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 ClassSubObjects; + GetObjectsWithOuter(LoadedClass, ClassSubObjects, true); + for (UObject* Sub : ClassSubObjects) + { + SpawnComp = Cast(Sub); + if (SpawnComp) + { + break; + } + } + + // Fallback: check CDO subobjects (C++ constructor components) + if (!SpawnComp) + { + AActor* CDO = LoadedClass->GetDefaultObject(); + if (CDO) + { + SpawnComp = CDO->FindComponentByClass(); + } + } + + 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(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(PC)) + { + if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) + { + TSharedPtr Action = MakeShared(); + 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 UPS_Editor_SpawnManager::GetCategories() const +{ + TArray Categories; + for (const FPS_Editor_ResolvedEntry& Entry : ResolvedEntries) + { + Categories.AddUnique(Entry.Category); + } + Categories.Sort(); + return Categories; +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h new file mode 100644 index 0000000..2dc9ed8 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h @@ -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 ActorClass; + + FString DisplayName; + FString Category; + + UPROPERTY() + TObjectPtr 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& GetResolvedEntries() const { return ResolvedEntries; } + + /** Get unique categories for filtering. */ + TArray GetCategories() const; + +private: + UPROPERTY() + TWeakObjectPtr OwnerPC; + + UPROPERTY() + TObjectPtr Catalog; + + UPROPERTY() + TArray ResolvedEntries; + + /** Resolve all catalog entries (load classes, read SpawnableComponent metadata). */ + void ResolveCatalog(); + + /** Compute spawn location in front of the camera. */ + FVector ComputeSpawnLocation() const; +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponent.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponent.cpp new file mode 100644 index 0000000..333a101 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponent.cpp @@ -0,0 +1,6 @@ +#include "PS_Editor_SpawnableComponent.h" + +UPS_Editor_SpawnableComponent::UPS_Editor_SpawnableComponent() +{ + PrimaryComponentTick.bCanEverTick = false; +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponent.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponent.h new file mode 100644 index 0000000..c27f71b --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponent.h @@ -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 Thumbnail; +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponentCustomization.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponentCustomization.cpp new file mode 100644 index 0000000..5206c38 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponentCustomization.cpp @@ -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 FPS_Editor_SpawnableComponentCustomization::MakeInstance() +{ + return MakeShareable(new FPS_Editor_SpawnableComponentCustomization); +} + +void FPS_Editor_SpawnableComponentCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + TArray> 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(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(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& 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(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 diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponentCustomization.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponentCustomization.h new file mode 100644 index 0000000..8d0a739 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnableComponentCustomization.h @@ -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 MakeInstance(); + + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; + +private: + FReply OnCaptureThumbnailClicked(); + + TWeakObjectPtr EditedComponent; +}; + +#endif diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp index 14d19d5..5bc7129 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp @@ -17,6 +17,7 @@ void APS_Editor_HUD::BeginPlay() if (APS_Editor_PlayerController* EditorPC = Cast(PC)) { MainWidget->SetSelectionManager(EditorPC->GetSelectionManager()); + MainWidget->SetSpawnManager(EditorPC->GetSpawnManager()); } MainWidget->AddToViewport(0); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index a003408..5222842 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -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 UPS_Editor_MainWidget::RebuildWidget() { return SNew(SOverlay) @@ -63,6 +71,14 @@ TSharedRef 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 UPS_Editor_MainWidget::BuildPropertiesPanel() ]; } +TSharedRef UPS_Editor_MainWidget::BuildSpawnCatalog() +{ + TSharedRef 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& 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 CardContent = SNew(SVerticalBox); + + // Thumbnail + if (Entry.Thumbnail) + { + TSharedPtr Brush = MakeShared(); + Brush->SetResourceObject(Entry.Thumbnail); + Brush->ImageSize = FVector2D(CardSize, CardSize); + Brush->DrawAs = ESlateBrushDrawType::Image; + Brush->Tiling = ESlateBrushTileType::NoTile; + CachedBrushes.Add(Brush); + + TWeakPtr 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) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h index d9a4f73..b737501 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h @@ -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 RebuildWidget() override; @@ -30,10 +32,17 @@ private: UPROPERTY() TWeakObjectPtr SelectionManager; + UPROPERTY() + TWeakObjectPtr SpawnManager; + // ---- Slate widget refs for dynamic updates ---- TSharedPtr ModeText; TSharedPtr SelectionInfoText; + // Spawn catalog + TSharedPtr SpawnListContainer; + TArray> CachedBrushes; + // Transform fields TSharedPtr LocX, LocY, LocZ; TSharedPtr RotX, RotY, RotZ; @@ -42,6 +51,8 @@ private: // ---- Helpers ---- TSharedRef BuildToolbar(); TSharedRef BuildPropertiesPanel(); + TSharedRef BuildSpawnCatalog(); + void PopulateSpawnCatalog(); void RefreshPropertiesFromSelection(); void ApplyTransformFromUI(); FString FloatToString(float Value) const;