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.
|
**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:
|
Key decisions:
|
||||||
- UI: UMG driven by C++ (not Blueprints)
|
- UI: UMG driven by C++ (Slate)
|
||||||
- Gizmos: InteractiveToolsFramework (UE5 built-in)
|
- Gizmos: Custom actor with BasicShapes + unlit material (InteractiveToolsFramework is editor-only)
|
||||||
- Serialization: JSON via Json/JsonUtilities modules
|
- Serialization: JSON via Json/JsonUtilities modules (future)
|
||||||
- Naming: PS_Editor_ prefix on all files/classes
|
- Naming: PS_Editor_ prefix on all files/classes
|
||||||
- Plugin path: Unreal/Plugins/PS_Editor/
|
- 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.
|
**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_Gizmo.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "DrawDebugHelpers.h"
|
#include "DrawDebugHelpers.h"
|
||||||
|
#include "Misc/MessageDialog.h"
|
||||||
|
|
||||||
APS_Editor_Pawn::APS_Editor_Pawn()
|
APS_Editor_Pawn::APS_Editor_Pawn()
|
||||||
{
|
{
|
||||||
@@ -573,6 +574,18 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
|||||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||||
if (!SM || !SM->IsAnythingSelected()) return;
|
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)
|
// Soft-delete: hide actors instead of destroying (enables undo)
|
||||||
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
|
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
@@ -584,14 +597,14 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
|
|
||||||
SM->ClearSelection();
|
SM->ClearSelection();
|
||||||
Action->Execute(); // Hide the actors
|
Action->Execute();
|
||||||
|
|
||||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
{
|
{
|
||||||
UM->RecordAction(Action);
|
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)
|
void APS_Editor_Pawn::HandleUndo(const FInputActionValue& Value)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_Gizmo.h"
|
#include "PS_Editor_Gizmo.h"
|
||||||
#include "PS_Editor_Pawn.h"
|
#include "PS_Editor_Pawn.h"
|
||||||
|
|
||||||
@@ -22,14 +23,43 @@ void APS_Editor_PlayerController::BeginPlay()
|
|||||||
|
|
||||||
UndoManager = NewObject<UPS_Editor_UndoManager>(this);
|
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]()
|
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<TWeakObjectPtr<AActor>> ToDeselect;
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SelectionManager->GetSelectedActors())
|
||||||
{
|
{
|
||||||
Gizmo->SetActorLocation(SelectionManager->GetSelectionPivot());
|
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_SelectionManager;
|
||||||
class UPS_Editor_UndoManager;
|
class UPS_Editor_UndoManager;
|
||||||
|
class UPS_Editor_SpawnManager;
|
||||||
|
class UPS_Editor_SpawnCatalog;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player controller for PS_Editor runtime editing.
|
* Player controller for PS_Editor runtime editing.
|
||||||
@@ -25,6 +27,13 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
UPS_Editor_UndoManager* GetUndoManager() const { return UndoManager; }
|
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:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
virtual void OnPossess(APawn* InPawn) override;
|
virtual void OnPossess(APawn* InPawn) override;
|
||||||
@@ -36,5 +45,8 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UPS_Editor_UndoManager> UndoManager;
|
TObjectPtr<UPS_Editor_UndoManager> UndoManager;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UPS_Editor_SpawnManager> SpawnManager;
|
||||||
|
|
||||||
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
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, "UI", "Widgets"));
|
||||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Selection"));
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Selection"));
|
||||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo"));
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo"));
|
||||||
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Spawn"));
|
||||||
|
|
||||||
PublicDependencyModuleNames.AddRange(new string[]
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
{
|
{
|
||||||
@@ -31,5 +32,11 @@ public class PS_Editor : ModuleRules
|
|||||||
"Slate",
|
"Slate",
|
||||||
"SlateCore"
|
"SlateCore"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Editor-only: Factory for SpawnCatalog DataAsset creation
|
||||||
|
if (Target.bBuildEditor)
|
||||||
|
{
|
||||||
|
PrivateDependencyModuleNames.Add("UnrealEd");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,16 +89,30 @@ void UPS_Editor_UndoManager::PruneHistory()
|
|||||||
|
|
||||||
void UPS_Editor_UndoManager::CleanupDeleteAction(TSharedPtr<FPS_Editor_Action> Action)
|
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()) return;
|
||||||
if (!Action.IsValid() || Action->ActionType != EPS_Editor_ActionType::Delete) return;
|
|
||||||
FPS_Editor_DeleteAction* DeleteAction = static_cast<FPS_Editor_DeleteAction*>(Action.Get());
|
// Truly destroy hidden actors when their action is pruned from history
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : DeleteAction->Actors)
|
TArray<TWeakObjectPtr<AActor>>* ActorsToClean = nullptr;
|
||||||
|
|
||||||
|
if (Action->ActionType == EPS_Editor_ActionType::Delete)
|
||||||
{
|
{
|
||||||
if (AActor* Actor = Weak.Get())
|
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 (Actor->IsHidden())
|
if (AActor* Actor = Weak.Get())
|
||||||
{
|
{
|
||||||
Actor->Destroy();
|
if (Actor->IsHidden())
|
||||||
|
{
|
||||||
|
Actor->Destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
enum class EPS_Editor_ActionType : uint8
|
enum class EPS_Editor_ActionType : uint8
|
||||||
{
|
{
|
||||||
Transform,
|
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.
|
* Runtime undo/redo manager for PS_Editor.
|
||||||
* Maintains a linear action stack with a cursor.
|
* 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))
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||||
{
|
{
|
||||||
MainWidget->SetSelectionManager(EditorPC->GetSelectionManager());
|
MainWidget->SetSelectionManager(EditorPC->GetSelectionManager());
|
||||||
|
MainWidget->SetSpawnManager(EditorPC->GetSpawnManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWidget->AddToViewport(0);
|
MainWidget->AddToViewport(0);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "PS_Editor_MainWidget.h"
|
#include "PS_Editor_MainWidget.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "Widgets/SBoxPanel.h"
|
#include "Widgets/SBoxPanel.h"
|
||||||
@@ -7,6 +8,8 @@
|
|||||||
#include "Widgets/Layout/SBorder.h"
|
#include "Widgets/Layout/SBorder.h"
|
||||||
#include "Widgets/Layout/SScrollBox.h"
|
#include "Widgets/Layout/SScrollBox.h"
|
||||||
#include "Widgets/Input/SButton.h"
|
#include "Widgets/Input/SButton.h"
|
||||||
|
#include "Widgets/Images/SImage.h"
|
||||||
|
#include "Engine/Texture2D.h"
|
||||||
#include "Widgets/Input/SEditableTextBox.h"
|
#include "Widgets/Input/SEditableTextBox.h"
|
||||||
#include "Widgets/SOverlay.h"
|
#include "Widgets/SOverlay.h"
|
||||||
|
|
||||||
@@ -15,6 +18,11 @@ void UPS_Editor_MainWidget::SetSelectionManager(UPS_Editor_SelectionManager* InS
|
|||||||
SelectionManager = InSelectionManager;
|
SelectionManager = InSelectionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager)
|
||||||
|
{
|
||||||
|
SpawnManager = InSpawnManager;
|
||||||
|
}
|
||||||
|
|
||||||
TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||||
{
|
{
|
||||||
return SNew(SOverlay)
|
return SNew(SOverlay)
|
||||||
@@ -63,6 +71,14 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
BuildToolbar()
|
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
|
// Right panel: Properties
|
||||||
+ SOverlay::Slot()
|
+ SOverlay::Slot()
|
||||||
.HAlign(HAlign_Right)
|
.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()
|
void UPS_Editor_MainWidget::NativeConstruct()
|
||||||
{
|
{
|
||||||
Super::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)
|
void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "PS_Editor_MainWidget.generated.h"
|
#include "PS_Editor_MainWidget.generated.h"
|
||||||
|
|
||||||
class UPS_Editor_SelectionManager;
|
class UPS_Editor_SelectionManager;
|
||||||
|
class UPS_Editor_SpawnManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main editor widget for PS_Editor.
|
* Main editor widget for PS_Editor.
|
||||||
@@ -18,8 +19,9 @@ class PS_EDITOR_API UPS_Editor_MainWidget : public UUserWidget
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
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 SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager);
|
||||||
|
void SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||||
@@ -30,10 +32,17 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TWeakObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
|
TWeakObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TWeakObjectPtr<UPS_Editor_SpawnManager> SpawnManager;
|
||||||
|
|
||||||
// ---- Slate widget refs for dynamic updates ----
|
// ---- Slate widget refs for dynamic updates ----
|
||||||
TSharedPtr<STextBlock> ModeText;
|
TSharedPtr<STextBlock> ModeText;
|
||||||
TSharedPtr<STextBlock> SelectionInfoText;
|
TSharedPtr<STextBlock> SelectionInfoText;
|
||||||
|
|
||||||
|
// Spawn catalog
|
||||||
|
TSharedPtr<SVerticalBox> SpawnListContainer;
|
||||||
|
TArray<TSharedPtr<FSlateBrush>> CachedBrushes;
|
||||||
|
|
||||||
// Transform fields
|
// Transform fields
|
||||||
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
||||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||||
@@ -42,6 +51,8 @@ private:
|
|||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
TSharedRef<SWidget> BuildToolbar();
|
TSharedRef<SWidget> BuildToolbar();
|
||||||
TSharedRef<SWidget> BuildPropertiesPanel();
|
TSharedRef<SWidget> BuildPropertiesPanel();
|
||||||
|
TSharedRef<SWidget> BuildSpawnCatalog();
|
||||||
|
void PopulateSpawnCatalog();
|
||||||
void RefreshPropertiesFromSelection();
|
void RefreshPropertiesFromSelection();
|
||||||
void ApplyTransformFromUI();
|
void ApplyTransformFromUI();
|
||||||
FString FloatToString(float Value) const;
|
FString FloatToString(float Value) const;
|
||||||
|
|||||||
Reference in New Issue
Block a user