Compare commits
11 Commits
17e8d5b194
...
88544c4c85
| Author | SHA1 | Date | |
|---|---|---|---|
| 88544c4c85 | |||
| 5be3a777f9 | |||
| 36b04bee61 | |||
| 9f1604f57c | |||
| ccb154c44f | |||
| 20017ff3fd | |||
| 30c0078a1d | |||
| 7884d1de59 | |||
| 8b2609669f | |||
| cf1e2f8ccf | |||
| b089a20512 |
4
.claude/memory/MEMORY.md
Normal file
4
.claude/memory/MEMORY.md
Normal file
@@ -0,0 +1,4 @@
|
||||
- [PS_Editor plugin project](project_ps_editor.md) — Runtime editor plugin for UE 5.5, Phases 1-4 done, outline material pending
|
||||
- [Underscore naming](feedback_naming.md) — Always use PS_Editor_ prefix with underscores in file/class names
|
||||
- [UE5 build process](reference_build.md) — CLI build command for the project (Build.bat path, target name, flags)
|
||||
- [Build workflow](feedback_build_workflow.md) — Prefer Live Coding (Ctrl+Alt+F11) when UE is open, CLI build only when UE is closed
|
||||
11
.claude/memory/feedback_build_workflow.md
Normal file
11
.claude/memory/feedback_build_workflow.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Build workflow preference
|
||||
description: User prefers Live Coding (Ctrl+Alt+F11) over CLI build when UE editor is open
|
||||
type: feedback
|
||||
---
|
||||
|
||||
When UE editor is open, ask the user to do a Live Coding compile (Ctrl+Alt+F11) instead of trying CLI build. Only ask to close UE for a full rebuild when Live Coding is not sufficient (e.g., header changes that require UHT, new .generated.h files, Build.cs changes).
|
||||
|
||||
**Why:** User finds Live Coding simpler and faster for iteration. CLI build fails when Live Coding is active.
|
||||
|
||||
**How to apply:** After code changes, tell the user to press Ctrl+Alt+F11. If changes involve new UCLASS/USTRUCT/UENUM declarations, new files, or Build.cs modifications, warn that a full restart of UE + CLI build is needed instead.
|
||||
11
.claude/memory/feedback_naming.md
Normal file
11
.claude/memory/feedback_naming.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Underscore naming convention
|
||||
description: User requires PS_Editor_ prefix with underscores in all file and class names
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Keep the underscore in file and class names: PS_Editor_ prefix.
|
||||
|
||||
**Why:** User explicitly corrected when I used PSEditor (camelCase) format. They want PS_Editor_GameMode, PS_Editor_Pawn, etc.
|
||||
|
||||
**How to apply:** Always use PS_Editor_ prefix for any new file or class in the plugin. Never use PSEditor or PS_EditorXyz without the separating underscore.
|
||||
47
.claude/memory/project_ps_editor.md
Normal file
47
.claude/memory/project_ps_editor.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: PS_Editor plugin project
|
||||
description: Runtime editor plugin for UE 5.5 - object placement, scene editing, JSON save/load in packaged builds
|
||||
type: project
|
||||
---
|
||||
|
||||
PS_Editor is a runtime editor plugin for Unreal Engine 5.5 for the PROSERVE project.
|
||||
|
||||
Key decisions:
|
||||
- UI: UMG driven by C++ (Slate)
|
||||
- Gizmos: Custom actor with BasicShapes + unlit material (ITF is editor-only)
|
||||
- Serialization: JSON via Json/JsonUtilities (FJsonObjectConverter)
|
||||
- Naming: PS_Editor_ prefix on all files/classes
|
||||
- Plugin path: Unreal/Plugins/PS_Editor/
|
||||
- AZERTY keyboard: arrows + A/Q for movement, E/R/T for modes
|
||||
|
||||
## Completed Phases
|
||||
|
||||
### Phase 1 - GameMode + Camera
|
||||
- Camera: Fly (RMB), Pan (LMB drag), Orbit (Alt+LMB / MMB)
|
||||
- Movement: arrows + A (up) / Q (down)
|
||||
|
||||
### Phase 2 - Selection + Transform + Undo
|
||||
- Click-to-select (Movable actors only), Ctrl+click multi-select
|
||||
- Custom gizmo (R/G/B arrows + cones, SDPG_Foreground, constant screen size)
|
||||
- Translate/Rotate/Scale via gizmo drag, hotkeys E/R/T
|
||||
- Snap to ground (End), soft-delete (Delete + confirmation dialog)
|
||||
- Undo/Redo (Ctrl+Z/Y, 50 action stack)
|
||||
- Properties panel + toolbar UI
|
||||
- Material: M_PS_Editor_Gizmo (Unlit, "Color" param, Translucent, Disable Depth Test)
|
||||
|
||||
### Phase 3 - Object Spawning
|
||||
- SpawnableComponent: marker on BPs (name, category, thumbnail)
|
||||
- SpawnCatalog DataAsset + Factory for easy creation
|
||||
- SpawnManager: spawns in front of camera, tracks all spawned actors
|
||||
- Catalogue UI: thumbnail cards, click to spawn, grouped by category
|
||||
- CaptureAllThumbnails on DataAsset: auto-generates from UE thumbnails
|
||||
- Spawn undo/redo, auto-deselect hidden actors
|
||||
|
||||
### Phase 4 - JSON Serialization
|
||||
- SceneData USTRUCT (Version, SceneName, Timestamp, Actors[ClassPath, Location, Rotation, Scale])
|
||||
- SceneSerializer: Save/Load/Clear/GetSavedSceneNames
|
||||
- Files in Saved/PS_Editor/Scenes/{name}.json
|
||||
- UI: Save field + button, New Scene, saved scenes list with click-to-load
|
||||
|
||||
## Next: Phase 5 - Advanced Editing
|
||||
- Splines, lighting, AI characters, property editing
|
||||
18
.claude/memory/reference_build.md
Normal file
18
.claude/memory/reference_build.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: UE5 build process
|
||||
description: How to build the PS_ProserveEditor UE 5.5 project from command line on Windows
|
||||
type: reference
|
||||
---
|
||||
|
||||
UE 5.5 is installed at `C:\Program Files\Epic Games\UE_5.5` (found via registry key `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.5`).
|
||||
|
||||
Build command (Editor target, Development):
|
||||
```
|
||||
"C:/Program Files/Epic Games/UE_5.5/Engine/Build/BatchFiles/Build.bat" PS_ProserveEditorEditor Win64 Development -Project="C:/ASTERION/GIT/PS_ProserveEditor/Unreal/PS_ProserveEditor.uproject" -WaitMutex -FromMsBuild
|
||||
```
|
||||
|
||||
**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
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,4 +1,17 @@
|
||||
# Visual Studio
|
||||
Unreal/.vs/
|
||||
|
||||
# UE build artifacts (project)
|
||||
Unreal/Binaries/
|
||||
Unreal/Intermediate/
|
||||
Unreal/Saved/
|
||||
Unreal/DerivedDataCache/
|
||||
|
||||
# UE build artifacts (plugins)
|
||||
Unreal/Plugins/*/Binaries/
|
||||
Unreal/Plugins/*/Intermediate/
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
.DS_Store
|
||||
|
||||
40
CLAUDE.md
Normal file
40
CLAUDE.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# PS_ProserveEditor - Project Guidelines
|
||||
|
||||
## Project Overview
|
||||
Unreal Engine 5.5 project with runtime editor plugin (PS_Editor). The plugin enables runtime scene editing in packaged builds: object placement, property editing, scenario definition, splines, lighting, and JSON scene save/load.
|
||||
|
||||
## Architecture
|
||||
- **Plugin**: PS_Editor (runtime plugin, must work in packaged builds)
|
||||
- **UI**: UMG driven by C++ (Slate)
|
||||
- **Gizmos**: Custom actor with BasicShapes + unlit material (InteractiveToolsFramework is editor-only)
|
||||
- **Serialization**: JSON via Json/JsonUtilities modules (FJsonObjectConverter)
|
||||
- **Input**: Enhanced Input system (AZERTY: arrows + A/Q movement, E/R/T modes)
|
||||
|
||||
## Naming Conventions
|
||||
- All plugin files and classes use the `PS_Editor_` prefix (e.g., `PS_Editor_GameMode`, `PS_Editor_Pawn`)
|
||||
- Code and comments in English
|
||||
- Standard UE5 prefixes: `A` for Actors, `U` for UObjects, `F` for structs, `E` for enums, `I` for interfaces
|
||||
|
||||
## Code Quality
|
||||
- Prefer correct, complete implementations over minimal ones.
|
||||
- Use appropriate data structures and algorithms — don't brute-force what has a known better solution.
|
||||
- When fixing a bug, fix the root cause, not the symptom.
|
||||
- If something requires error handling or validation to work reliably, include it without asking.
|
||||
|
||||
## Build
|
||||
- Engine: Unreal Engine 5.5
|
||||
- Plugin location: `Unreal/Plugins/PS_Editor/`
|
||||
- Game module: `PS_ProserveEditor` (in `Unreal/Source/PS_ProserveEditor/`)
|
||||
- CLI build command (Editor, Development):
|
||||
```
|
||||
"<UE_INSTALL>/Engine/Build/BatchFiles/Build.bat" PS_ProserveEditorEditor Win64 Development -Project="<REPO>/Unreal/PS_ProserveEditor.uproject" -WaitMutex -FromMsBuild
|
||||
```
|
||||
UE install path can be found via registry: `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.5` > `InstalledDirectory`
|
||||
|
||||
## Project Memory
|
||||
Shared project memory is stored in `.claude/memory/` (versioned in the repository). When starting a new session on any machine, read these files to restore context:
|
||||
|
||||
- `MEMORY.md` — Index of all memory files with one-line summaries
|
||||
- `project_ps_editor.md` — Project scope, technical decisions, current phase
|
||||
- `feedback_naming.md` — Naming conventions and user preferences
|
||||
- `reference_build.md` — Full build command and UE install path details
|
||||
@@ -1,7 +1,9 @@
|
||||
|
||||
|
||||
[/Script/EngineSettings.GameMapsSettings]
|
||||
GameDefaultMap=/Engine/Maps/Templates/OpenWorld
|
||||
GameDefaultMap=/Game/test.test
|
||||
EditorStartupMap=/Game/test.test
|
||||
GlobalDefaultGameMode=/Script/PS_Editor.PS_Editor_GameMode
|
||||
|
||||
[/Script/Engine.RendererSettings]
|
||||
r.AllowStaticLighting=False
|
||||
@@ -14,6 +16,8 @@ r.ReflectionMethod=1
|
||||
|
||||
r.SkinCache.CompileShaders=True
|
||||
|
||||
r.CustomDepth=3
|
||||
|
||||
r.RayTracing=True
|
||||
|
||||
r.Shadow.Virtual.Enable=1
|
||||
|
||||
BIN
Unreal/Content/test.umap
Normal file
BIN
Unreal/Content/test.umap
Normal file
Binary file not shown.
@@ -17,6 +17,10 @@
|
||||
"TargetAllowList": [
|
||||
"Editor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "PS_Editor",
|
||||
"Enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/M_PS_Editor_Gizmo.uasset
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/PS_Editor_GameMode.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/PS_Editor_GameMode.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/PS_Editor_Pawn.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/PS_Editor_Pawn.uasset
Normal file
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.
25
Unreal/Plugins/PS_Editor/PS_Editor.uplugin
Normal file
25
Unreal/Plugins/PS_Editor/PS_Editor.uplugin
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "0.1.0",
|
||||
"FriendlyName": "PS Editor",
|
||||
"Description": "Runtime scene editor plugin for Proserve. Enables object placement, property editing, scenario definition, and JSON scene save/load in packaged builds.",
|
||||
"Category": "Proserve",
|
||||
"CreatedBy": "Asterion",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": true,
|
||||
"Installed": false,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "PS_Editor",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default"
|
||||
}
|
||||
],
|
||||
"Plugins": [
|
||||
{
|
||||
"Name": "EnhancedInput",
|
||||
"Enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "PS_Editor_GameMode.h"
|
||||
#include "PS_Editor_Pawn.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_HUD.h"
|
||||
|
||||
APS_Editor_GameMode::APS_Editor_GameMode()
|
||||
{
|
||||
DefaultPawnClass = APS_Editor_Pawn::StaticClass();
|
||||
PlayerControllerClass = APS_Editor_PlayerController::StaticClass();
|
||||
HUDClass = APS_Editor_HUD::StaticClass();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "PS_Editor_GameMode.generated.h"
|
||||
|
||||
/**
|
||||
* Game mode for the PS_Editor runtime editing mode.
|
||||
* Sets up the editor pawn, controller, and HUD.
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API APS_Editor_GameMode : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APS_Editor_GameMode();
|
||||
|
||||
/** Whether the editor is currently in edit mode (vs play/preview mode). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
bool bIsEditMode = true;
|
||||
};
|
||||
@@ -0,0 +1,832 @@
|
||||
#include "PS_Editor_Pawn.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
#include "Components/PostProcessComponent.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "InputAction.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "InputModifiers.h"
|
||||
#include "InputTriggers.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "Misc/MessageDialog.h"
|
||||
|
||||
APS_Editor_Pawn::APS_Editor_Pawn()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
|
||||
SetRootComponent(RootScene);
|
||||
|
||||
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
|
||||
SpringArm->SetupAttachment(RootScene);
|
||||
SpringArm->TargetArmLength = 0.0f;
|
||||
SpringArm->bDoCollisionTest = false;
|
||||
SpringArm->bEnableCameraLag = false;
|
||||
SpringArm->bUsePawnControlRotation = false;
|
||||
|
||||
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
|
||||
Camera->SetupAttachment(SpringArm);
|
||||
|
||||
OutlinePostProcess = CreateDefaultSubobject<UPostProcessComponent>(TEXT("OutlinePostProcess"));
|
||||
OutlinePostProcess->SetupAttachment(RootScene);
|
||||
OutlinePostProcess->bUnbound = true; // Apply to entire scene
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Load outline post-process material
|
||||
UMaterialInterface* OutlineMat = LoadObject<UMaterialInterface>(
|
||||
nullptr, TEXT("/PS_Editor/M_PS_Editor_SelectionOutline.M_PS_Editor_SelectionOutline"));
|
||||
if (OutlineMat && OutlinePostProcess)
|
||||
{
|
||||
OutlinePostProcess->Settings.WeightedBlendables.Array.Add(FWeightedBlendable(1.0f, OutlineMat));
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material loaded"));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Outline material /PS_Editor/M_PS_Editor_SelectionOutline not found. "
|
||||
"Create a Post Process material in Plugins/PS_Editor/Content/"));
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
||||
{
|
||||
bAltHeld = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt);
|
||||
bCtrlHeld = PC->IsInputKeyDown(EKeys::LeftControl) || PC->IsInputKeyDown(EKeys::RightControl);
|
||||
|
||||
// Gizmo hover highlighting in Idle mode
|
||||
if (CameraMode == EPS_Editor_CameraMode::Idle)
|
||||
{
|
||||
UpdateGizmoHover();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::CreateInputActions()
|
||||
{
|
||||
IA_Move = NewObject<UInputAction>(this, TEXT("IA_Move"));
|
||||
IA_Move->ValueType = EInputActionValueType::Axis3D;
|
||||
IA_Move->bConsumeInput = false;
|
||||
|
||||
IA_Look = NewObject<UInputAction>(this, TEXT("IA_Look"));
|
||||
IA_Look->ValueType = EInputActionValueType::Axis2D;
|
||||
|
||||
IA_Scroll = NewObject<UInputAction>(this, TEXT("IA_Scroll"));
|
||||
IA_Scroll->ValueType = EInputActionValueType::Axis1D;
|
||||
|
||||
IA_LeftClick = NewObject<UInputAction>(this, TEXT("IA_LeftClick"));
|
||||
IA_LeftClick->ValueType = EInputActionValueType::Boolean;
|
||||
|
||||
IA_RightClick = NewObject<UInputAction>(this, TEXT("IA_RightClick"));
|
||||
IA_RightClick->ValueType = EInputActionValueType::Boolean;
|
||||
|
||||
IA_MiddleClick = NewObject<UInputAction>(this, TEXT("IA_MiddleClick"));
|
||||
IA_MiddleClick->ValueType = EInputActionValueType::Boolean;
|
||||
|
||||
EditorMappingContext = NewObject<UInputMappingContext>(this, TEXT("EditorMappingContext"));
|
||||
|
||||
// Movement helpers
|
||||
auto MapForward = [&](FKey Key)
|
||||
{
|
||||
FEnhancedActionKeyMapping& Mapping = EditorMappingContext->MapKey(IA_Move, Key);
|
||||
UInputModifierSwizzleAxis* Swizzle = NewObject<UInputModifierSwizzleAxis>(this);
|
||||
Swizzle->Order = EInputAxisSwizzle::YXZ;
|
||||
Mapping.Modifiers.Add(Swizzle);
|
||||
};
|
||||
|
||||
auto MapBackward = [&](FKey Key)
|
||||
{
|
||||
FEnhancedActionKeyMapping& Mapping = EditorMappingContext->MapKey(IA_Move, Key);
|
||||
UInputModifierSwizzleAxis* Swizzle = NewObject<UInputModifierSwizzleAxis>(this);
|
||||
Swizzle->Order = EInputAxisSwizzle::YXZ;
|
||||
Mapping.Modifiers.Add(Swizzle);
|
||||
Mapping.Modifiers.Add(NewObject<UInputModifierNegate>(this));
|
||||
};
|
||||
|
||||
auto MapRight = [&](FKey Key)
|
||||
{
|
||||
EditorMappingContext->MapKey(IA_Move, Key);
|
||||
};
|
||||
|
||||
auto MapLeft = [&](FKey Key)
|
||||
{
|
||||
FEnhancedActionKeyMapping& Mapping = EditorMappingContext->MapKey(IA_Move, Key);
|
||||
Mapping.Modifiers.Add(NewObject<UInputModifierNegate>(this));
|
||||
};
|
||||
|
||||
// Arrow keys only (letter keys reserved for hotkeys)
|
||||
MapForward(EKeys::Up);
|
||||
MapBackward(EKeys::Down);
|
||||
MapRight(EKeys::Right);
|
||||
MapLeft(EKeys::Left);
|
||||
|
||||
// A - Up (+Z)
|
||||
{
|
||||
FEnhancedActionKeyMapping& Mapping = EditorMappingContext->MapKey(IA_Move, EKeys::A);
|
||||
UInputModifierSwizzleAxis* Swizzle = NewObject<UInputModifierSwizzleAxis>(this);
|
||||
Swizzle->Order = EInputAxisSwizzle::ZYX;
|
||||
Mapping.Modifiers.Add(Swizzle);
|
||||
}
|
||||
|
||||
// Q - Down (-Z)
|
||||
{
|
||||
FEnhancedActionKeyMapping& Mapping = EditorMappingContext->MapKey(IA_Move, EKeys::Q);
|
||||
UInputModifierSwizzleAxis* Swizzle = NewObject<UInputModifierSwizzleAxis>(this);
|
||||
Swizzle->Order = EInputAxisSwizzle::ZYX;
|
||||
Mapping.Modifiers.Add(Swizzle);
|
||||
Mapping.Modifiers.Add(NewObject<UInputModifierNegate>(this));
|
||||
}
|
||||
|
||||
// Mouse
|
||||
EditorMappingContext->MapKey(IA_Look, EKeys::Mouse2D);
|
||||
EditorMappingContext->MapKey(IA_Scroll, EKeys::MouseWheelAxis);
|
||||
EditorMappingContext->MapKey(IA_LeftClick, EKeys::LeftMouseButton);
|
||||
EditorMappingContext->MapKey(IA_RightClick, EKeys::RightMouseButton);
|
||||
EditorMappingContext->MapKey(IA_MiddleClick, EKeys::MiddleMouseButton);
|
||||
|
||||
// Transform mode hotkeys (E/R/T)
|
||||
IA_TranslateMode = NewObject<UInputAction>(this, TEXT("IA_TranslateMode"));
|
||||
IA_TranslateMode->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::E);
|
||||
|
||||
IA_RotateMode = NewObject<UInputAction>(this, TEXT("IA_RotateMode"));
|
||||
IA_RotateMode->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_RotateMode, EKeys::R);
|
||||
|
||||
IA_ScaleMode = NewObject<UInputAction>(this, TEXT("IA_ScaleMode"));
|
||||
IA_ScaleMode->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_ScaleMode, EKeys::T);
|
||||
|
||||
IA_Delete = NewObject<UInputAction>(this, TEXT("IA_Delete"));
|
||||
IA_Delete->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_Delete, EKeys::Delete);
|
||||
|
||||
IA_SnapToGround = NewObject<UInputAction>(this, TEXT("IA_SnapToGround"));
|
||||
IA_SnapToGround->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_SnapToGround, EKeys::End);
|
||||
|
||||
// Undo/Redo (Ctrl+Z / Ctrl+Y handled via key state check in handler)
|
||||
IA_Undo = NewObject<UInputAction>(this, TEXT("IA_Undo"));
|
||||
IA_Undo->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_Undo, EKeys::Z);
|
||||
|
||||
IA_Redo = NewObject<UInputAction>(this, TEXT("IA_Redo"));
|
||||
IA_Redo->ValueType = EInputActionValueType::Boolean;
|
||||
EditorMappingContext->MapKey(IA_Redo, EKeys::Y);
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||
{
|
||||
Super::SetupPlayerInputComponent(PlayerInputComponent);
|
||||
|
||||
CreateInputActions();
|
||||
|
||||
if (APlayerController* PC = Cast<APlayerController>(GetController()))
|
||||
{
|
||||
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
|
||||
{
|
||||
Subsystem->ClearAllMappings();
|
||||
Subsystem->AddMappingContext(EditorMappingContext, 0);
|
||||
}
|
||||
}
|
||||
|
||||
UEnhancedInputComponent* EIC = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);
|
||||
|
||||
EIC->BindAction(IA_Move, ETriggerEvent::Triggered, this, &APS_Editor_Pawn::HandleMove);
|
||||
EIC->BindAction(IA_Look, ETriggerEvent::Triggered, this, &APS_Editor_Pawn::HandleLook);
|
||||
EIC->BindAction(IA_Scroll, ETriggerEvent::Triggered, this, &APS_Editor_Pawn::HandleScroll);
|
||||
|
||||
EIC->BindAction(IA_LeftClick, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleLeftClickStarted);
|
||||
EIC->BindAction(IA_LeftClick, ETriggerEvent::Completed, this, &APS_Editor_Pawn::HandleLeftClickCompleted);
|
||||
|
||||
EIC->BindAction(IA_RightClick, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleRightClickStarted);
|
||||
EIC->BindAction(IA_RightClick, ETriggerEvent::Completed, this, &APS_Editor_Pawn::HandleRightClickCompleted);
|
||||
|
||||
EIC->BindAction(IA_MiddleClick, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleMiddleClickStarted);
|
||||
EIC->BindAction(IA_MiddleClick, ETriggerEvent::Completed, this, &APS_Editor_Pawn::HandleMiddleClickCompleted);
|
||||
|
||||
// Transform mode hotkeys (only trigger on press, not hold)
|
||||
EIC->BindAction(IA_TranslateMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleTranslateMode);
|
||||
EIC->BindAction(IA_RotateMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleRotateMode);
|
||||
EIC->BindAction(IA_ScaleMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleScaleMode);
|
||||
EIC->BindAction(IA_Delete, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDelete);
|
||||
EIC->BindAction(IA_SnapToGround, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSnapToGround);
|
||||
EIC->BindAction(IA_Undo, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleUndo);
|
||||
EIC->BindAction(IA_Redo, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleRedo);
|
||||
}
|
||||
|
||||
// ---- Input Handlers ----
|
||||
|
||||
void APS_Editor_Pawn::HandleMove(const FInputActionValue& Value)
|
||||
{
|
||||
const FVector MoveInput = Value.Get<FVector>();
|
||||
const float DeltaTime = GetWorld()->GetDeltaSeconds();
|
||||
|
||||
const FRotator Rotation = GetActorRotation();
|
||||
const FVector Forward = FRotationMatrix(Rotation).GetUnitAxis(EAxis::X);
|
||||
const FVector Right = FRotationMatrix(Rotation).GetUnitAxis(EAxis::Y);
|
||||
const FVector Up = FVector::UpVector;
|
||||
|
||||
const FVector Movement = (Forward * MoveInput.Y) + (Right * MoveInput.X) + (Up * MoveInput.Z);
|
||||
AddActorWorldOffset(Movement * FlySpeed * DeltaTime, true);
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
||||
{
|
||||
if (CameraMode == EPS_Editor_CameraMode::Idle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FVector2D LookInput = Value.Get<FVector2D>();
|
||||
|
||||
if (CameraMode == EPS_Editor_CameraMode::PendingClick)
|
||||
{
|
||||
// Check if mouse moved enough to become a drag
|
||||
const FVector2D CurrentPos = GetMouseScreenPosition();
|
||||
const float PixelDistance = FVector2D::Distance(PendingClickScreenPos, CurrentPos);
|
||||
|
||||
if (PixelDistance > ClickDragThreshold)
|
||||
{
|
||||
// Exceeded threshold: resolve to Pan or Orbit
|
||||
if (bAltHeld)
|
||||
{
|
||||
ComputeOrbitFocalPoint();
|
||||
SetCameraMode(EPS_Editor_CameraMode::Orbit);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetCameraMode(EPS_Editor_CameraMode::Pan);
|
||||
}
|
||||
}
|
||||
// If still under threshold, do nothing (wait for more movement or mouse up)
|
||||
return;
|
||||
}
|
||||
|
||||
if (CameraMode == EPS_Editor_CameraMode::Pan)
|
||||
{
|
||||
const float YawDelta = LookInput.X * PanYawSensitivity;
|
||||
FRotator CurrentRotation = GetActorRotation();
|
||||
CurrentRotation.Yaw += YawDelta;
|
||||
SetActorRotation(CurrentRotation);
|
||||
|
||||
const FVector HorizontalForward = FVector(GetActorForwardVector().X, GetActorForwardVector().Y, 0.0f).GetSafeNormal();
|
||||
const FVector PanMovement = HorizontalForward * LookInput.Y * PanSpeed;
|
||||
AddActorWorldOffset(PanMovement, true);
|
||||
}
|
||||
else if (CameraMode == EPS_Editor_CameraMode::Fly)
|
||||
{
|
||||
const float YawDelta = LookInput.X * LookSensitivity;
|
||||
const float PitchDelta = LookInput.Y * LookSensitivity;
|
||||
|
||||
FRotator CurrentRotation = GetActorRotation();
|
||||
CurrentRotation.Yaw += YawDelta;
|
||||
CurrentRotation.Pitch = FMath::Clamp(CurrentRotation.Pitch + PitchDelta, -89.0f, 89.0f);
|
||||
CurrentRotation.Roll = 0.0f;
|
||||
SetActorRotation(CurrentRotation);
|
||||
}
|
||||
else if (CameraMode == EPS_Editor_CameraMode::Orbit)
|
||||
{
|
||||
OrbitYaw += LookInput.X * OrbitSensitivity;
|
||||
OrbitPitch = FMath::Clamp(OrbitPitch + (LookInput.Y * OrbitSensitivity), -89.0f, 89.0f);
|
||||
|
||||
const FRotator OrbitRotation(OrbitPitch, OrbitYaw, 0.0f);
|
||||
const FVector Offset = OrbitRotation.Vector() * -OrbitDistance;
|
||||
|
||||
SetActorLocation(OrbitFocalPoint + Offset);
|
||||
SetActorRotation((OrbitFocalPoint - GetActorLocation()).Rotation());
|
||||
}
|
||||
else if (CameraMode == EPS_Editor_CameraMode::GizmoDrag)
|
||||
{
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||
if (!EditorPC) return;
|
||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||
if (!SM) return;
|
||||
APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
|
||||
if (!Gizmo) return;
|
||||
|
||||
const FVector AxisDir = Gizmo->GetAxisDirection(ActiveGizmoAxis);
|
||||
const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
|
||||
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
||||
|
||||
if (Mode == EPS_Editor_TransformMode::Translate)
|
||||
{
|
||||
// Ray-plane intersection for precise axis-constrained translation
|
||||
FVector RayOrigin, RayDir;
|
||||
if (!GetMouseWorldRay(RayOrigin, RayDir)) return;
|
||||
FVector CurrentIntersection;
|
||||
if (!RayPlaneIntersection(RayOrigin, RayDir, GizmoDragPlaneOrigin, GizmoDragPlaneNormal, CurrentIntersection)) return;
|
||||
|
||||
const FVector TotalDelta = CurrentIntersection - GizmoDragInitialIntersection;
|
||||
const FVector ConstrainedDelta = FVector::DotProduct(TotalDelta, AxisDir) * AxisDir;
|
||||
|
||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||
{
|
||||
if (AActor* Actor = Selected[i].Get())
|
||||
{
|
||||
Actor->SetActorLocation(GizmoDragInitialTransforms[i].GetLocation() + ConstrainedDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Mode == EPS_Editor_TransformMode::Rotate)
|
||||
{
|
||||
// Use combined mouse X+Y screen movement to drive rotation angle
|
||||
const float RotationSpeed = 1.0f;
|
||||
const float AngleDelta = (LookInput.X + LookInput.Y) * RotationSpeed;
|
||||
|
||||
// Accumulate total angle since drag start
|
||||
GizmoDragAccumulator += AngleDelta;
|
||||
|
||||
const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(GizmoDragAccumulator));
|
||||
const FVector GizmoLoc = GizmoDragPlaneOrigin;
|
||||
|
||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||
{
|
||||
if (AActor* Actor = Selected[i].Get())
|
||||
{
|
||||
const FVector Offset = GizmoDragInitialTransforms[i].GetLocation() - GizmoLoc;
|
||||
Actor->SetActorLocation(GizmoLoc + RotationQuat.RotateVector(Offset));
|
||||
|
||||
const FQuat OrigRot = GizmoDragInitialTransforms[i].GetRotation();
|
||||
Actor->SetActorRotation((RotationQuat * OrigRot).Rotator());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Mode == EPS_Editor_TransformMode::Scale)
|
||||
{
|
||||
// Use combined mouse X+Y screen movement to drive scale
|
||||
const float ScaleSpeed = 0.01f;
|
||||
GizmoDragAccumulator += (LookInput.X + LookInput.Y) * ScaleSpeed;
|
||||
|
||||
const float ScaleFactor = FMath::Clamp(1.0f + GizmoDragAccumulator, 0.01f, 100.0f);
|
||||
|
||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||
{
|
||||
if (AActor* Actor = Selected[i].Get())
|
||||
{
|
||||
FVector NewScale = GizmoDragInitialTransforms[i].GetScale3D();
|
||||
if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::X) NewScale.X *= ScaleFactor;
|
||||
else if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::Y) NewScale.Y *= ScaleFactor;
|
||||
else if (ActiveGizmoAxis == EPS_Editor_GizmoAxis::Z) NewScale.Z *= ScaleFactor;
|
||||
Actor->SetActorScale3D(NewScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Gizmo->SetActorLocation(SM->GetSelectionPivot());
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value)
|
||||
{
|
||||
const float ScrollValue = Value.Get<float>();
|
||||
|
||||
if (CameraMode == EPS_Editor_CameraMode::Fly)
|
||||
{
|
||||
FlySpeed = FMath::Clamp(FlySpeed + ScrollValue * FlySpeedScrollStep, FlySpeedMin, FlySpeedMax);
|
||||
}
|
||||
else if (CameraMode == EPS_Editor_CameraMode::Orbit)
|
||||
{
|
||||
OrbitDistance = FMath::Clamp(OrbitDistance - ScrollValue * OrbitZoomStep, OrbitDistanceMin, OrbitDistanceMax);
|
||||
|
||||
const FRotator OrbitRotation(OrbitPitch, OrbitYaw, 0.0f);
|
||||
const FVector Offset = OrbitRotation.Vector() * -OrbitDistance;
|
||||
SetActorLocation(OrbitFocalPoint + Offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
const FVector Forward = GetActorForwardVector();
|
||||
AddActorWorldOffset(Forward * ScrollValue * OrbitZoomStep * 5.0f, true);
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
||||
{
|
||||
if (bAltHeld)
|
||||
{
|
||||
ComputeOrbitFocalPoint();
|
||||
SetCameraMode(EPS_Editor_CameraMode::Orbit);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if clicking on a gizmo handle
|
||||
FVector RayOrigin, RayDir;
|
||||
if (GetMouseWorldRay(RayOrigin, RayDir))
|
||||
{
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||
{
|
||||
if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo())
|
||||
{
|
||||
EPS_Editor_GizmoAxis Axis = Gizmo->GetAxisUnderCursor(RayOrigin, RayDir);
|
||||
if (Axis != EPS_Editor_GizmoAxis::None)
|
||||
{
|
||||
BeginGizmoDrag(Axis);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No gizmo hit: enter PendingClick
|
||||
PendingClickScreenPos = GetMouseScreenPosition();
|
||||
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
||||
{
|
||||
if (CameraMode == EPS_Editor_CameraMode::PendingClick)
|
||||
{
|
||||
// Mouse released without exceeding drag threshold = this is a click (selection)
|
||||
OnEditorClick.ExecuteIfBound(PendingClickScreenPos, bCtrlHeld);
|
||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||
}
|
||||
else if (CameraMode == EPS_Editor_CameraMode::GizmoDrag)
|
||||
{
|
||||
// Record undo action for the completed gizmo drag
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||
{
|
||||
TSharedPtr<FPS_Editor_TransformAction> Action = MakeShared<FPS_Editor_TransformAction>();
|
||||
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
||||
|
||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||
{
|
||||
if (AActor* Actor = Selected[i].Get())
|
||||
{
|
||||
Action->Actors.Add(Actor);
|
||||
Action->OldTransforms.Add(GizmoDragInitialTransforms[i]);
|
||||
Action->NewTransforms.Add(Actor->GetActorTransform());
|
||||
}
|
||||
}
|
||||
|
||||
FString ModeStr;
|
||||
switch (SM->GetTransformMode())
|
||||
{
|
||||
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate"); break;
|
||||
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate"); break;
|
||||
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale"); break;
|
||||
}
|
||||
Action->Description = FString::Printf(TEXT("%s %d actor(s)"), *ModeStr, Action->Actors.Num());
|
||||
|
||||
if (Action->Actors.Num() > 0)
|
||||
{
|
||||
UM->RecordAction(Action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
|
||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||
}
|
||||
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit)
|
||||
{
|
||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleRightClickStarted(const FInputActionValue& Value)
|
||||
{
|
||||
SetCameraMode(EPS_Editor_CameraMode::Fly);
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleRightClickCompleted(const FInputActionValue& Value)
|
||||
{
|
||||
if (CameraMode == EPS_Editor_CameraMode::Fly)
|
||||
{
|
||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleMiddleClickStarted(const FInputActionValue& Value)
|
||||
{
|
||||
ComputeOrbitFocalPoint();
|
||||
SetCameraMode(EPS_Editor_CameraMode::Orbit);
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleMiddleClickCompleted(const FInputActionValue& Value)
|
||||
{
|
||||
if (CameraMode == EPS_Editor_CameraMode::Orbit)
|
||||
{
|
||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleTranslateMode(const FInputActionValue& Value)
|
||||
{
|
||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||
{
|
||||
SM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Translate mode"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleRotateMode(const FInputActionValue& Value)
|
||||
{
|
||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||
{
|
||||
SM->SetTransformMode(EPS_Editor_TransformMode::Rotate);
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Rotate mode"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleScaleMode(const FInputActionValue& Value)
|
||||
{
|
||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||
{
|
||||
SM->SetTransformMode(EPS_Editor_TransformMode::Scale);
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scale mode"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
||||
{
|
||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||
if (!EditorPC) return;
|
||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||
if (!SM || !SM->IsAnythingSelected()) return;
|
||||
|
||||
// Confirmation dialog
|
||||
const int32 Count = SM->GetSelectedActors().Num();
|
||||
const FText Message = FText::Format(
|
||||
NSLOCTEXT("PS_Editor", "DeleteConfirm", "Delete {0} selected object(s)?"),
|
||||
FText::AsNumber(Count));
|
||||
|
||||
EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, Message);
|
||||
if (Result != EAppReturnType::Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Soft-delete: hide actors instead of destroying (enables undo)
|
||||
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
Action->Actors.Add(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
SM->ClearSelection();
|
||||
Action->Execute();
|
||||
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
{
|
||||
UM->RecordAction(Action);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Soft-deleted %d actor(s)"), Count);
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleUndo(const FInputActionValue& Value)
|
||||
{
|
||||
if (!bCtrlHeld || CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
{
|
||||
UM->Undo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleRedo(const FInputActionValue& Value)
|
||||
{
|
||||
if (!bCtrlHeld || CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
{
|
||||
UM->Redo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
|
||||
{
|
||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||
{
|
||||
SM->SnapSelectionToGround();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
||||
{
|
||||
CameraMode = NewMode;
|
||||
|
||||
if (NewMode == EPS_Editor_CameraMode::Orbit)
|
||||
{
|
||||
const FRotator CurrentRotation = GetActorRotation();
|
||||
OrbitPitch = CurrentRotation.Pitch;
|
||||
OrbitYaw = CurrentRotation.Yaw;
|
||||
}
|
||||
|
||||
UpdateCursorVisibility();
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::UpdateCursorVisibility()
|
||||
{
|
||||
APlayerController* PC = Cast<APlayerController>(GetController());
|
||||
if (!PC)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const bool bShowCursor = (CameraMode == EPS_Editor_CameraMode::Idle
|
||||
|| CameraMode == EPS_Editor_CameraMode::PendingClick);
|
||||
PC->bShowMouseCursor = bShowCursor;
|
||||
|
||||
if (bShowCursor)
|
||||
{
|
||||
PC->SetInputMode(FInputModeGameAndUI().SetHideCursorDuringCapture(false));
|
||||
}
|
||||
else
|
||||
{
|
||||
PC->SetInputMode(FInputModeGameOnly());
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::ComputeOrbitFocalPoint()
|
||||
{
|
||||
const FVector Start = GetActorLocation();
|
||||
const FVector Forward = GetActorForwardVector();
|
||||
const FVector End = Start + Forward * OrbitDistance;
|
||||
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams Params;
|
||||
Params.AddIgnoredActor(this);
|
||||
|
||||
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
|
||||
{
|
||||
OrbitFocalPoint = Hit.ImpactPoint;
|
||||
OrbitDistance = FVector::Dist(Start, OrbitFocalPoint);
|
||||
}
|
||||
else
|
||||
{
|
||||
OrbitFocalPoint = End;
|
||||
}
|
||||
}
|
||||
|
||||
FVector2D APS_Editor_Pawn::GetMouseScreenPosition() const
|
||||
{
|
||||
APlayerController* PC = Cast<APlayerController>(GetController());
|
||||
if (!PC)
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
|
||||
float MouseX = 0.0f;
|
||||
float MouseY = 0.0f;
|
||||
PC->GetMousePosition(MouseX, MouseY);
|
||||
return FVector2D(MouseX, MouseY);
|
||||
}
|
||||
|
||||
bool APS_Editor_Pawn::GetMouseWorldRay(FVector& OutOrigin, FVector& OutDirection) const
|
||||
{
|
||||
APlayerController* PC = Cast<APlayerController>(GetController());
|
||||
if (!PC)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FVector2D ScreenPos = GetMouseScreenPosition();
|
||||
return PC->DeprojectScreenPositionToWorld(ScreenPos.X, ScreenPos.Y, OutOrigin, OutDirection);
|
||||
}
|
||||
|
||||
bool APS_Editor_Pawn::RayPlaneIntersection(const FVector& RayOrigin, const FVector& RayDir,
|
||||
const FVector& PlaneOrigin, const FVector& PlaneNormal, FVector& OutIntersection) const
|
||||
{
|
||||
const float Denom = FVector::DotProduct(RayDir, PlaneNormal);
|
||||
if (FMath::Abs(Denom) < KINDA_SMALL_NUMBER)
|
||||
{
|
||||
return false; // Ray is parallel to plane
|
||||
}
|
||||
|
||||
const float T = FVector::DotProduct(PlaneOrigin - RayOrigin, PlaneNormal) / Denom;
|
||||
if (T < 0.0f)
|
||||
{
|
||||
return false; // Intersection is behind the ray
|
||||
}
|
||||
|
||||
OutIntersection = RayOrigin + RayDir * T;
|
||||
return true;
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::UpdateGizmoHover()
|
||||
{
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||
if (!EditorPC) return;
|
||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||
if (!SM) return;
|
||||
APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
|
||||
if (!Gizmo) return;
|
||||
|
||||
FVector RayOrigin, RayDir;
|
||||
if (GetMouseWorldRay(RayOrigin, RayDir))
|
||||
{
|
||||
EPS_Editor_GizmoAxis Axis = Gizmo->GetAxisUnderCursor(RayOrigin, RayDir);
|
||||
Gizmo->SetHighlightedAxis(Axis);
|
||||
}
|
||||
else
|
||||
{
|
||||
Gizmo->SetHighlightedAxis(EPS_Editor_GizmoAxis::None);
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
|
||||
{
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||
if (!EditorPC) return;
|
||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||
if (!SM) return;
|
||||
APS_Editor_Gizmo* Gizmo = SM->GetGizmo();
|
||||
if (!Gizmo) return;
|
||||
|
||||
ActiveGizmoAxis = Axis;
|
||||
|
||||
const FVector AxisDir = Gizmo->GetAxisDirection(Axis);
|
||||
const EPS_Editor_TransformMode Mode = SM->GetTransformMode();
|
||||
|
||||
FVector PlaneNormal;
|
||||
if (Mode == EPS_Editor_TransformMode::Rotate)
|
||||
{
|
||||
// For rotation: plane perpendicular to the rotation axis
|
||||
PlaneNormal = AxisDir;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For translate/scale: plane containing the axis, facing the camera
|
||||
const FVector CameraForward = GetActorForwardVector();
|
||||
PlaneNormal = FVector::CrossProduct(AxisDir, CameraForward).GetSafeNormal();
|
||||
if (PlaneNormal.IsNearlyZero())
|
||||
{
|
||||
PlaneNormal = FVector::CrossProduct(AxisDir, FVector::UpVector).GetSafeNormal();
|
||||
}
|
||||
PlaneNormal = FVector::CrossProduct(AxisDir, PlaneNormal).GetSafeNormal();
|
||||
}
|
||||
|
||||
GizmoDragPlaneNormal = PlaneNormal;
|
||||
GizmoDragPlaneOrigin = Gizmo->GetActorLocation();
|
||||
GizmoDragAccumulator = 0.0f;
|
||||
|
||||
// Get initial mouse-plane intersection
|
||||
FVector RayOrigin, RayDir;
|
||||
if (GetMouseWorldRay(RayOrigin, RayDir))
|
||||
{
|
||||
RayPlaneIntersection(RayOrigin, RayDir, GizmoDragPlaneOrigin, GizmoDragPlaneNormal, GizmoDragInitialIntersection);
|
||||
}
|
||||
|
||||
// Store initial transforms of all selected actors
|
||||
GizmoDragInitialTransforms.Empty();
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
GizmoDragInitialTransforms.Add(Actor->GetActorTransform());
|
||||
}
|
||||
else
|
||||
{
|
||||
GizmoDragInitialTransforms.Add(FTransform::Identity);
|
||||
}
|
||||
}
|
||||
|
||||
SetCameraMode(EPS_Editor_CameraMode::GizmoDrag);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "InputActionValue.h"
|
||||
#include "PS_Editor_SelectionTypes.h"
|
||||
#include "PS_Editor_Pawn.generated.h"
|
||||
|
||||
class UCameraComponent;
|
||||
class USpringArmComponent;
|
||||
class UPostProcessComponent;
|
||||
class UInputAction;
|
||||
class UInputMappingContext;
|
||||
|
||||
DECLARE_DELEGATE_TwoParams(FPS_Editor_OnClick, FVector2D /*ScreenPos*/, bool /*bAdditive*/);
|
||||
|
||||
/**
|
||||
* Camera modes for the editor pawn.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_Editor_CameraMode : uint8
|
||||
{
|
||||
/** Default mode - cursor visible, ZQSD/arrows still move. */
|
||||
Idle,
|
||||
/** Left mouse down, waiting to determine click vs drag (pixel threshold). */
|
||||
PendingClick,
|
||||
/** Left-click drag: mouse Y = forward/back on horizontal plane, mouse X = yaw rotation. */
|
||||
Pan,
|
||||
/** Right-click held: full free look + ZQSD movement. */
|
||||
Fly,
|
||||
/** Alt + left-click or middle-click: orbit around focal point. */
|
||||
Orbit,
|
||||
/** Dragging a gizmo handle. */
|
||||
GizmoDrag
|
||||
};
|
||||
|
||||
/**
|
||||
* Editor camera pawn mimicking UE viewport navigation.
|
||||
*
|
||||
* Controls:
|
||||
* - Left-click: Select object (or pan if dragged)
|
||||
* - Right-click held: Fly (ZQSD + free mouse look)
|
||||
* - Alt + left-click held: Orbit around focal point
|
||||
* - Middle-click held: Orbit around focal point (alternative)
|
||||
* - ZQSD / Arrows: Always active movement
|
||||
* - E/A: Up/Down
|
||||
* - Mouse wheel: Zoom / adjust fly speed
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API APS_Editor_Pawn : public APawn
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APS_Editor_Pawn();
|
||||
|
||||
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
|
||||
/** Fired when a left-click is confirmed (mouse up without exceeding drag threshold). */
|
||||
FPS_Editor_OnClick OnEditorClick;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
// ---- Components ----
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<USceneComponent> RootScene;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<USpringArmComponent> SpringArm;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<UCameraComponent> Camera;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<UPostProcessComponent> OutlinePostProcess;
|
||||
|
||||
// ---- Camera Settings ----
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||
float FlySpeed = 1000.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||
float FlySpeedMin = 100.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||
float FlySpeedMax = 10000.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||
float LookSensitivity = 2.5f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||
float FlySpeedScrollStep = 200.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Pan")
|
||||
float PanSpeed = 5.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Pan")
|
||||
float PanYawSensitivity = 1.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||
float OrbitDistance = 500.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||
float OrbitZoomStep = 50.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||
float OrbitDistanceMin = 50.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||
float OrbitDistanceMax = 5000.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Orbit")
|
||||
float OrbitSensitivity = 1.0f;
|
||||
|
||||
/** Pixel distance threshold to distinguish click from drag. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Selection")
|
||||
float ClickDragThreshold = 8.0f;
|
||||
|
||||
// ---- State ----
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
EPS_Editor_CameraMode CameraMode = EPS_Editor_CameraMode::Idle;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera|Orbit")
|
||||
FVector OrbitFocalPoint = FVector::ZeroVector;
|
||||
|
||||
private:
|
||||
// ---- Input Actions (created at runtime) ----
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputMappingContext> EditorMappingContext;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Move;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Look;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Scroll;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_LeftClick;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_RightClick;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_MiddleClick;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_TranslateMode;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_RotateMode;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_ScaleMode;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Delete;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_SnapToGround;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Undo;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Redo;
|
||||
|
||||
// ---- Modifier key state ----
|
||||
bool bAltHeld = false;
|
||||
bool bCtrlHeld = false;
|
||||
|
||||
// ---- Click vs drag state ----
|
||||
FVector2D PendingClickScreenPos = FVector2D::ZeroVector;
|
||||
|
||||
// ---- Gizmo drag state ----
|
||||
EPS_Editor_GizmoAxis ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
|
||||
FVector GizmoDragPlaneNormal = FVector::ZeroVector;
|
||||
FVector GizmoDragPlaneOrigin = FVector::ZeroVector;
|
||||
FVector GizmoDragInitialIntersection = FVector::ZeroVector;
|
||||
TArray<FTransform> GizmoDragInitialTransforms;
|
||||
float GizmoDragAccumulator = 0.0f;
|
||||
|
||||
// ---- Input Handlers ----
|
||||
void HandleMove(const FInputActionValue& Value);
|
||||
void HandleLook(const FInputActionValue& Value);
|
||||
void HandleScroll(const FInputActionValue& Value);
|
||||
void HandleLeftClickStarted(const FInputActionValue& Value);
|
||||
void HandleLeftClickCompleted(const FInputActionValue& Value);
|
||||
void HandleRightClickStarted(const FInputActionValue& Value);
|
||||
void HandleRightClickCompleted(const FInputActionValue& Value);
|
||||
void HandleMiddleClickStarted(const FInputActionValue& Value);
|
||||
void HandleMiddleClickCompleted(const FInputActionValue& Value);
|
||||
void HandleTranslateMode(const FInputActionValue& Value);
|
||||
void HandleRotateMode(const FInputActionValue& Value);
|
||||
void HandleScaleMode(const FInputActionValue& Value);
|
||||
void HandleDelete(const FInputActionValue& Value);
|
||||
void HandleSnapToGround(const FInputActionValue& Value);
|
||||
void HandleUndo(const FInputActionValue& Value);
|
||||
void HandleRedo(const FInputActionValue& Value);
|
||||
|
||||
// ---- Helpers ----
|
||||
void CreateInputActions();
|
||||
void SetCameraMode(EPS_Editor_CameraMode NewMode);
|
||||
void UpdateCursorVisibility();
|
||||
void ComputeOrbitFocalPoint();
|
||||
FVector2D GetMouseScreenPosition() const;
|
||||
bool GetMouseWorldRay(FVector& OutOrigin, FVector& OutDirection) const;
|
||||
bool RayPlaneIntersection(const FVector& RayOrigin, const FVector& RayDir, const FVector& PlaneOrigin, const FVector& PlaneNormal, FVector& OutIntersection) const;
|
||||
void UpdateGizmoHover();
|
||||
void BeginGizmoDrag(EPS_Editor_GizmoAxis Axis);
|
||||
|
||||
float OrbitPitch = 0.0f;
|
||||
float OrbitYaw = 0.0f;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_SceneSerializer.h"
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_Pawn.h"
|
||||
|
||||
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
||||
{
|
||||
bShowMouseCursor = true;
|
||||
DefaultMouseCursor = EMouseCursor::Default;
|
||||
}
|
||||
|
||||
void APS_Editor_PlayerController::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
SetInputMode(FInputModeGameAndUI().SetHideCursorDuringCapture(false));
|
||||
|
||||
// Create selection manager
|
||||
SelectionManager = NewObject<UPS_Editor_SelectionManager>(this);
|
||||
SelectionManager->Initialize(this);
|
||||
|
||||
UndoManager = NewObject<UPS_Editor_UndoManager>(this);
|
||||
|
||||
// Load spawn catalog and create spawn manager
|
||||
SpawnManager = NewObject<UPS_Editor_SpawnManager>(this);
|
||||
UPS_Editor_SpawnCatalog* LoadedCatalog = SpawnCatalogAsset.LoadSynchronous();
|
||||
SpawnManager->Initialize(this, LoadedCatalog);
|
||||
|
||||
SceneSerializer = NewObject<UPS_Editor_SceneSerializer>(this);
|
||||
SceneSerializer->SetSelectionManager(SelectionManager);
|
||||
|
||||
// After undo/redo, deselect hidden actors and refresh gizmo
|
||||
UndoManager->OnUndoRedo.AddWeakLambda(this, [this]()
|
||||
{
|
||||
if (SelectionManager)
|
||||
{
|
||||
// Deselect any actors that are now hidden (undone spawn or redone delete)
|
||||
TArray<TWeakObjectPtr<AActor>> ToDeselect;
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SelectionManager->GetSelectedActors())
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
if (Actor->IsHidden())
|
||||
{
|
||||
ToDeselect.Add(Actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const TWeakObjectPtr<AActor>& Weak : ToDeselect)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
SelectionManager->DeselectActor(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh gizmo position
|
||||
if (SelectionManager->IsAnythingSelected())
|
||||
{
|
||||
if (APS_Editor_Gizmo* Gizmo = SelectionManager->GetGizmo())
|
||||
{
|
||||
Gizmo->SetActorLocation(SelectionManager->GetSelectionPivot());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
|
||||
{
|
||||
Super::OnPossess(InPawn);
|
||||
|
||||
// Bind pawn's click delegate to the selection manager
|
||||
if (APS_Editor_Pawn* EditorPawn = Cast<APS_Editor_Pawn>(InPawn))
|
||||
{
|
||||
EditorPawn->OnEditorClick.BindUObject(this, &APS_Editor_PlayerController::HandleEditorClick);
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)
|
||||
{
|
||||
if (SelectionManager)
|
||||
{
|
||||
SelectionManager->HandleClickAtScreenPosition(ScreenPos, bAdditive);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "PS_Editor_PlayerController.generated.h"
|
||||
|
||||
class UPS_Editor_SelectionManager;
|
||||
class UPS_Editor_UndoManager;
|
||||
class UPS_Editor_SpawnManager;
|
||||
class UPS_Editor_SpawnCatalog;
|
||||
class UPS_Editor_SceneSerializer;
|
||||
|
||||
/**
|
||||
* Player controller for PS_Editor runtime editing.
|
||||
* Owns the SelectionManager and wires it to the Pawn's click delegate.
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API APS_Editor_PlayerController : public APlayerController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APS_Editor_PlayerController();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
UPS_Editor_SelectionManager* GetSelectionManager() const { return SelectionManager; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
UPS_Editor_UndoManager* GetUndoManager() const { return UndoManager; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
UPS_Editor_SpawnManager* GetSpawnManager() const { return SpawnManager; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
UPS_Editor_SceneSerializer* GetSceneSerializer() const { return SceneSerializer; }
|
||||
|
||||
/** The spawn catalog DataAsset to load. Set this in the Blueprint or World Settings. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
TSoftObjectPtr<UPS_Editor_SpawnCatalog> SpawnCatalogAsset;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UPS_Editor_UndoManager> UndoManager;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UPS_Editor_SpawnManager> SpawnManager;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UPS_Editor_SceneSerializer> SceneSerializer;
|
||||
|
||||
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
||||
};
|
||||
@@ -0,0 +1,303 @@
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Materials/MaterialInstanceDynamic.h"
|
||||
#include "UObject/ConstructorHelpers.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
|
||||
APS_Editor_Gizmo::APS_Editor_Gizmo()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
GizmoRoot = CreateDefaultSubobject<USceneComponent>(TEXT("GizmoRoot"));
|
||||
SetRootComponent(GizmoRoot);
|
||||
|
||||
TranslateRoot = CreateDefaultSubobject<USceneComponent>(TEXT("TranslateRoot"));
|
||||
TranslateRoot->SetupAttachment(GizmoRoot);
|
||||
|
||||
const FRotator RotToX = FRotationMatrix::MakeFromZ(FVector::ForwardVector).Rotator();
|
||||
const FRotator RotToY = FRotationMatrix::MakeFromZ(FVector::RightVector).Rotator();
|
||||
const FRotator RotToZ = FRotator::ZeroRotator;
|
||||
|
||||
Arrow_X = CreateArrow(TEXT("Arrow_X"), TranslateRoot, RotToX);
|
||||
Arrow_Y = CreateArrow(TEXT("Arrow_Y"), TranslateRoot, RotToY);
|
||||
Arrow_Z = CreateArrow(TEXT("Arrow_Z"), TranslateRoot, RotToZ);
|
||||
|
||||
Cone_X = CreateCone(TEXT("Cone_X"), TranslateRoot, RotToX);
|
||||
Cone_Y = CreateCone(TEXT("Cone_Y"), TranslateRoot, RotToY);
|
||||
Cone_Z = CreateCone(TEXT("Cone_Z"), TranslateRoot, RotToZ);
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Load the gizmo base material.
|
||||
// The user must create: /PS_Editor/M_PS_Editor_Gizmo
|
||||
// (Unlit material with a Vector Parameter named "Color" connected to Emissive Color)
|
||||
UMaterialInterface* GizmoBaseMat = LoadObject<UMaterialInterface>(
|
||||
nullptr, TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
||||
|
||||
if (!GizmoBaseMat)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Material /PS_Editor/M_PS_Editor_Gizmo not found! "
|
||||
"Create an Unlit material with a Vector Parameter 'Color' connected to Emissive Color "
|
||||
"in Plugins/PS_Editor/Content/"));
|
||||
}
|
||||
|
||||
auto MakeColorMat = [&](const FLinearColor& Color) -> UMaterialInstanceDynamic*
|
||||
{
|
||||
if (!GizmoBaseMat) return nullptr;
|
||||
UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(GizmoBaseMat, this);
|
||||
MID->SetVectorParameterValue(TEXT("Color"), Color);
|
||||
return MID;
|
||||
};
|
||||
|
||||
// UE editor gizmo colors
|
||||
Mat_X = MakeColorMat(FLinearColor(0.594f, 0.0197f, 0.0f)); // Dark red
|
||||
Mat_Y = MakeColorMat(FLinearColor(0.1255f, 0.372f, 0.0f)); // Dark green
|
||||
Mat_Z = MakeColorMat(FLinearColor(0.0392f, 0.0784f, 0.594f)); // Dark blue
|
||||
Mat_Highlight = MakeColorMat(FLinearColor::Yellow);
|
||||
|
||||
// Apply materials to shafts and cones
|
||||
auto ApplyToAxis = [](UStaticMeshComponent* Shaft, UStaticMeshComponent* Tip, UMaterialInstanceDynamic* Mat)
|
||||
{
|
||||
if (Mat)
|
||||
{
|
||||
if (Shaft) Shaft->SetMaterial(0, Mat);
|
||||
if (Tip) Tip->SetMaterial(0, Mat);
|
||||
}
|
||||
};
|
||||
|
||||
ApplyToAxis(Arrow_X, Cone_X, Mat_X);
|
||||
ApplyToAxis(Arrow_Y, Cone_Y, Mat_Y);
|
||||
ApplyToAxis(Arrow_Z, Cone_Z, Mat_Z);
|
||||
|
||||
// Start hidden
|
||||
SetGizmoVisible(false);
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
if (bGizmoVisible)
|
||||
{
|
||||
UpdateConstantScreenSize();
|
||||
}
|
||||
}
|
||||
|
||||
static void ConfigureGizmoComponent(UStaticMeshComponent* Comp)
|
||||
{
|
||||
Comp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
Comp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
Comp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||
Comp->SetCollisionObjectType(ECC_WorldDynamic);
|
||||
|
||||
Comp->SetCastShadow(false);
|
||||
Comp->SetRenderCustomDepth(false);
|
||||
|
||||
// Render on top of everything
|
||||
Comp->SetDepthPriorityGroup(SDPG_Foreground);
|
||||
Comp->bUseViewOwnerDepthPriorityGroup = false;
|
||||
Comp->SetTranslucentSortPriority(10000);
|
||||
}
|
||||
|
||||
UStaticMeshComponent* APS_Editor_Gizmo::CreateArrow(const FName& Name, USceneComponent* Parent, const FRotator& Rotation)
|
||||
{
|
||||
UStaticMeshComponent* Comp = CreateDefaultSubobject<UStaticMeshComponent>(Name);
|
||||
Comp->SetupAttachment(Parent);
|
||||
|
||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> CylinderMesh(TEXT("/Engine/BasicShapes/Cylinder"));
|
||||
if (CylinderMesh.Succeeded())
|
||||
{
|
||||
Comp->SetStaticMesh(CylinderMesh.Object);
|
||||
}
|
||||
|
||||
// Shaft: radius ~3 units, length ~60 units
|
||||
Comp->SetRelativeScale3D(FVector(0.06f, 0.06f, 0.6f));
|
||||
Comp->SetRelativeRotation(Rotation);
|
||||
|
||||
const FVector AxisDir = Rotation.RotateVector(FVector::UpVector);
|
||||
Comp->SetRelativeLocation(AxisDir * 30.0f);
|
||||
|
||||
ConfigureGizmoComponent(Comp);
|
||||
return Comp;
|
||||
}
|
||||
|
||||
UStaticMeshComponent* APS_Editor_Gizmo::CreateCone(const FName& Name, USceneComponent* Parent, const FRotator& Rotation)
|
||||
{
|
||||
UStaticMeshComponent* Comp = CreateDefaultSubobject<UStaticMeshComponent>(Name);
|
||||
Comp->SetupAttachment(Parent);
|
||||
|
||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> ConeMesh(TEXT("/Engine/BasicShapes/Cone"));
|
||||
if (ConeMesh.Succeeded())
|
||||
{
|
||||
Comp->SetStaticMesh(ConeMesh.Object);
|
||||
}
|
||||
|
||||
Comp->SetRelativeScale3D(FVector(0.15f, 0.15f, 0.2f));
|
||||
Comp->SetRelativeRotation(Rotation);
|
||||
|
||||
const FVector AxisDir = Rotation.RotateVector(FVector::UpVector);
|
||||
Comp->SetRelativeLocation(AxisDir * 65.0f);
|
||||
|
||||
ConfigureGizmoComponent(Comp);
|
||||
return Comp;
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::SetGizmoActive(bool bActive, FVector WorldLocation)
|
||||
{
|
||||
if (bActive)
|
||||
{
|
||||
SetActorLocation(WorldLocation);
|
||||
SetTransformMode(CurrentMode);
|
||||
}
|
||||
|
||||
SetGizmoVisible(bActive);
|
||||
SetActorEnableCollision(bActive);
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::SetGizmoVisible(bool bVisible)
|
||||
{
|
||||
bGizmoVisible = bVisible;
|
||||
|
||||
// Control visibility per-component instead of SetActorHiddenInGame
|
||||
// so we don't override individual component visibility settings.
|
||||
TranslateRoot->SetVisibility(bVisible && CurrentMode == EPS_Editor_TransformMode::Translate, true);
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::SetTransformMode(EPS_Editor_TransformMode NewMode)
|
||||
{
|
||||
CurrentMode = NewMode;
|
||||
TranslateRoot->SetVisibility(bGizmoVisible && NewMode == EPS_Editor_TransformMode::Translate, true);
|
||||
}
|
||||
|
||||
EPS_Editor_GizmoAxis APS_Editor_Gizmo::GetAxisUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const
|
||||
{
|
||||
if (!bGizmoVisible)
|
||||
{
|
||||
return EPS_Editor_GizmoAxis::None;
|
||||
}
|
||||
|
||||
float BestDistance = TNumericLimits<float>::Max();
|
||||
EPS_Editor_GizmoAxis BestAxis = EPS_Editor_GizmoAxis::None;
|
||||
|
||||
// Use a sphere sweep with a generous radius for easier clicking
|
||||
const float SweepRadius = 8.0f * GetActorScale3D().X;
|
||||
|
||||
auto TestComponent = [&](UStaticMeshComponent* Comp, EPS_Editor_GizmoAxis Axis)
|
||||
{
|
||||
if (!Comp) return;
|
||||
|
||||
FHitResult Hit;
|
||||
const FVector RayEnd = RayOrigin + RayDirection * 100000.0f;
|
||||
const FCollisionShape Sphere = FCollisionShape::MakeSphere(SweepRadius);
|
||||
|
||||
if (Comp->SweepComponent(Hit, RayOrigin, RayEnd, FQuat::Identity, Sphere, false))
|
||||
{
|
||||
if (Hit.Distance < BestDistance)
|
||||
{
|
||||
BestDistance = Hit.Distance;
|
||||
BestAxis = Axis;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TestComponent(Arrow_X, EPS_Editor_GizmoAxis::X);
|
||||
TestComponent(Cone_X, EPS_Editor_GizmoAxis::X);
|
||||
TestComponent(Arrow_Y, EPS_Editor_GizmoAxis::Y);
|
||||
TestComponent(Cone_Y, EPS_Editor_GizmoAxis::Y);
|
||||
TestComponent(Arrow_Z, EPS_Editor_GizmoAxis::Z);
|
||||
TestComponent(Cone_Z, EPS_Editor_GizmoAxis::Z);
|
||||
|
||||
return BestAxis;
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::SetHighlightedAxis(EPS_Editor_GizmoAxis Axis)
|
||||
{
|
||||
if (HighlightedAxis == Axis) return;
|
||||
|
||||
auto SetAxisMat = [this](EPS_Editor_GizmoAxis A, UMaterialInstanceDynamic* Mat)
|
||||
{
|
||||
UStaticMeshComponent* Shaft = GetComponentForAxis(A);
|
||||
UStaticMeshComponent* Tip = nullptr;
|
||||
switch (A)
|
||||
{
|
||||
case EPS_Editor_GizmoAxis::X: Tip = Cone_X; break;
|
||||
case EPS_Editor_GizmoAxis::Y: Tip = Cone_Y; break;
|
||||
case EPS_Editor_GizmoAxis::Z: Tip = Cone_Z; break;
|
||||
default: break;
|
||||
}
|
||||
SetComponentMaterial(Shaft, Mat);
|
||||
SetComponentMaterial(Tip, Mat);
|
||||
};
|
||||
|
||||
// Restore previous
|
||||
if (HighlightedAxis != EPS_Editor_GizmoAxis::None)
|
||||
{
|
||||
UMaterialInstanceDynamic* OrigMat = nullptr;
|
||||
switch (HighlightedAxis)
|
||||
{
|
||||
case EPS_Editor_GizmoAxis::X: OrigMat = Mat_X; break;
|
||||
case EPS_Editor_GizmoAxis::Y: OrigMat = Mat_Y; break;
|
||||
case EPS_Editor_GizmoAxis::Z: OrigMat = Mat_Z; break;
|
||||
default: break;
|
||||
}
|
||||
SetAxisMat(HighlightedAxis, OrigMat);
|
||||
}
|
||||
|
||||
HighlightedAxis = Axis;
|
||||
|
||||
// Highlight new
|
||||
if (Axis != EPS_Editor_GizmoAxis::None)
|
||||
{
|
||||
SetAxisMat(Axis, Mat_Highlight);
|
||||
}
|
||||
}
|
||||
|
||||
FVector APS_Editor_Gizmo::GetAxisDirection(EPS_Editor_GizmoAxis Axis) const
|
||||
{
|
||||
switch (Axis)
|
||||
{
|
||||
case EPS_Editor_GizmoAxis::X: return FVector::ForwardVector;
|
||||
case EPS_Editor_GizmoAxis::Y: return FVector::RightVector;
|
||||
case EPS_Editor_GizmoAxis::Z: return FVector::UpVector;
|
||||
default: return FVector::ZeroVector;
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::UpdateConstantScreenSize()
|
||||
{
|
||||
APlayerController* PC = GetWorld()->GetFirstPlayerController();
|
||||
if (!PC) return;
|
||||
|
||||
FVector CameraLocation;
|
||||
FRotator CameraRotation;
|
||||
PC->GetPlayerViewPoint(CameraLocation, CameraRotation);
|
||||
|
||||
const float Distance = FVector::Dist(CameraLocation, GetActorLocation());
|
||||
if (Distance < 1.0f) return;
|
||||
|
||||
const float Scale = Distance * DesiredScreenSize * 0.01f;
|
||||
SetActorScale3D(FVector(Scale));
|
||||
}
|
||||
|
||||
UStaticMeshComponent* APS_Editor_Gizmo::GetComponentForAxis(EPS_Editor_GizmoAxis Axis) const
|
||||
{
|
||||
switch (Axis)
|
||||
{
|
||||
case EPS_Editor_GizmoAxis::X: return Arrow_X;
|
||||
case EPS_Editor_GizmoAxis::Y: return Arrow_Y;
|
||||
case EPS_Editor_GizmoAxis::Z: return Arrow_Z;
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Gizmo::SetComponentMaterial(UStaticMeshComponent* Comp, UMaterialInstanceDynamic* Mat)
|
||||
{
|
||||
if (Comp && Mat)
|
||||
{
|
||||
Comp->SetMaterial(0, Mat);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "PS_Editor_SelectionTypes.h"
|
||||
#include "PS_Editor_Gizmo.generated.h"
|
||||
|
||||
class UStaticMeshComponent;
|
||||
class UMaterialInstanceDynamic;
|
||||
|
||||
/**
|
||||
* Custom runtime transform gizmo for PS_Editor.
|
||||
* Uses engine BasicShapes (Cone, Cylinder, Cube) to build translate/rotate/scale handles.
|
||||
* Maintains constant screen size by adjusting scale based on camera distance.
|
||||
*
|
||||
* Phase 1: Translate mode only (3 axis arrows + 3 plane handles).
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API APS_Editor_Gizmo : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APS_Editor_Gizmo();
|
||||
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
|
||||
/** Show/hide the gizmo and set its position. */
|
||||
void SetGizmoActive(bool bActive, FVector WorldLocation = FVector::ZeroVector);
|
||||
|
||||
/** Switch between Translate/Rotate/Scale modes. */
|
||||
void SetTransformMode(EPS_Editor_TransformMode NewMode);
|
||||
|
||||
/** Test which gizmo axis is under the given world ray. */
|
||||
EPS_Editor_GizmoAxis GetAxisUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const;
|
||||
|
||||
/** Highlight the hovered axis (change material color). */
|
||||
void SetHighlightedAxis(EPS_Editor_GizmoAxis Axis);
|
||||
|
||||
/** Get the world-space direction for a given axis. */
|
||||
FVector GetAxisDirection(EPS_Editor_GizmoAxis Axis) const;
|
||||
|
||||
EPS_Editor_TransformMode GetTransformMode() const { return CurrentMode; }
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
// ---- Root ----
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<USceneComponent> GizmoRoot;
|
||||
|
||||
// ---- Translate handles ----
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<USceneComponent> TranslateRoot;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> Arrow_X;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> Arrow_Y;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> Arrow_Z;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> Cone_X;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> Cone_Y;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UStaticMeshComponent> Cone_Z;
|
||||
|
||||
// ---- Materials ----
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_X;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Y;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Z;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_Highlight;
|
||||
|
||||
// ---- State ----
|
||||
EPS_Editor_TransformMode CurrentMode = EPS_Editor_TransformMode::Translate;
|
||||
EPS_Editor_GizmoAxis HighlightedAxis = EPS_Editor_GizmoAxis::None;
|
||||
bool bGizmoVisible = false;
|
||||
|
||||
float DesiredScreenSize = 0.25f;
|
||||
|
||||
// ---- Helpers ----
|
||||
UStaticMeshComponent* CreateArrow(const FName& Name, USceneComponent* Parent, const FRotator& Rotation);
|
||||
UStaticMeshComponent* CreateCone(const FName& Name, USceneComponent* Parent, const FRotator& Rotation);
|
||||
void UpdateConstantScreenSize();
|
||||
void SetGizmoVisible(bool bVisible);
|
||||
UStaticMeshComponent* GetComponentForAxis(EPS_Editor_GizmoAxis Axis) const;
|
||||
void SetComponentMaterial(UStaticMeshComponent* Comp, UMaterialInstanceDynamic* Mat);
|
||||
};
|
||||
45
Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs
Normal file
45
Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using UnrealBuildTool;
|
||||
using System.IO;
|
||||
|
||||
public class PS_Editor : ModuleRules
|
||||
{
|
||||
public PS_Editor(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
// Add all subdirectories to include paths so headers can be included by name
|
||||
string SourceDir = Path.Combine(ModuleDirectory);
|
||||
PublicIncludePaths.Add(SourceDir);
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "GameMode"));
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "UI"));
|
||||
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"));
|
||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Serialization"));
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"InputCore",
|
||||
"EnhancedInput",
|
||||
"UMG",
|
||||
"Json",
|
||||
"JsonUtilities"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Slate",
|
||||
"SlateCore"
|
||||
});
|
||||
|
||||
// Editor-only: Factory for SpawnCatalog DataAsset creation
|
||||
if (Target.bBuildEditor)
|
||||
{
|
||||
PrivateDependencyModuleNames.Add("UnrealEd");
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.cpp
Normal file
17
Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "PS_Editor.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FPS_EditorModule"
|
||||
|
||||
void FPS_EditorModule::StartupModule()
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor module started."));
|
||||
}
|
||||
|
||||
void FPS_EditorModule::ShutdownModule()
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor module shut down."));
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FPS_EditorModule, PS_Editor)
|
||||
11
Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.h
Normal file
11
Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FPS_EditorModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
@@ -0,0 +1,301 @@
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "Components/PrimitiveComponent.h"
|
||||
|
||||
void UPS_Editor_SelectionManager::Initialize(APlayerController* InOwnerPC)
|
||||
{
|
||||
OwnerPC = InOwnerPC;
|
||||
|
||||
// Spawn the gizmo actor (kept alive for the whole session, shown/hidden as needed)
|
||||
if (UWorld* World = InOwnerPC->GetWorld())
|
||||
{
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.Owner = InOwnerPC;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
Gizmo = World->SpawnActor<APS_Editor_Gizmo>(APS_Editor_Gizmo::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams);
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::HandleClickAtScreenPosition(FVector2D ScreenPos, bool bAdditive)
|
||||
{
|
||||
APlayerController* PC = OwnerPC.Get();
|
||||
if (!PC)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Deproject screen position to world ray
|
||||
FVector WorldOrigin, WorldDirection;
|
||||
if (!PC->DeprojectScreenPositionToWorld(ScreenPos.X, ScreenPos.Y, WorldOrigin, WorldDirection))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Raycast
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams Params;
|
||||
Params.AddIgnoredActor(PC->GetPawn());
|
||||
if (Gizmo)
|
||||
{
|
||||
Params.AddIgnoredActor(Gizmo);
|
||||
}
|
||||
|
||||
const FVector End = WorldOrigin + WorldDirection * 100000.0f;
|
||||
const bool bHit = PC->GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Visibility, Params);
|
||||
|
||||
if (bHit && Hit.GetActor())
|
||||
{
|
||||
AActor* HitActor = Hit.GetActor();
|
||||
|
||||
// Only allow selection of Movable actors (skip static scene geometry like floors/walls)
|
||||
USceneComponent* RootComp = HitActor->GetRootComponent();
|
||||
const bool bIsSelectable = RootComp && RootComp->Mobility == EComponentMobility::Movable;
|
||||
|
||||
if (bIsSelectable)
|
||||
{
|
||||
if (bAdditive)
|
||||
{
|
||||
ToggleActor(HitActor);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearSelection();
|
||||
SelectActor(HitActor);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hit a non-selectable actor (static geometry): treat as clicking empty space
|
||||
if (!bAdditive)
|
||||
{
|
||||
ClearSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clicked on nothing: deselect all
|
||||
if (!bAdditive)
|
||||
{
|
||||
ClearSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::SelectActor(AActor* Actor)
|
||||
{
|
||||
if (!Actor || IsActorSelected(Actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedActors.Add(Actor);
|
||||
SetActorHighlight(Actor, true);
|
||||
UpdateGizmo();
|
||||
OnSelectionChanged.Broadcast();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selected %s"), *Actor->GetName());
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
||||
{
|
||||
if (!Actor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int32 Removed = SelectedActors.RemoveAll([Actor](const TWeakObjectPtr<AActor>& Weak)
|
||||
{
|
||||
return Weak.Get() == Actor;
|
||||
});
|
||||
|
||||
if (Removed > 0)
|
||||
{
|
||||
SetActorHighlight(Actor, false);
|
||||
UpdateGizmo();
|
||||
OnSelectionChanged.Broadcast();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::ToggleActor(AActor* Actor)
|
||||
{
|
||||
if (IsActorSelected(Actor))
|
||||
{
|
||||
DeselectActor(Actor);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectActor(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::ClearSelection()
|
||||
{
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
SetActorHighlight(Actor, false);
|
||||
}
|
||||
}
|
||||
|
||||
const int32 Count = SelectedActors.Num();
|
||||
SelectedActors.Empty();
|
||||
UpdateGizmo();
|
||||
OnSelectionChanged.Broadcast();
|
||||
|
||||
if (Count > 0)
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Cleared selection (%d actors)"), Count);
|
||||
}
|
||||
}
|
||||
|
||||
bool UPS_Editor_SelectionManager::IsActorSelected(AActor* Actor) const
|
||||
{
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||
{
|
||||
if (Weak.Get() == Actor)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FVector UPS_Editor_SelectionManager::GetSelectionPivot() const
|
||||
{
|
||||
if (SelectedActors.Num() == 0)
|
||||
{
|
||||
return FVector::ZeroVector;
|
||||
}
|
||||
|
||||
FVector Sum = FVector::ZeroVector;
|
||||
int32 Count = 0;
|
||||
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
Sum += Actor->GetActorLocation();
|
||||
Count++;
|
||||
}
|
||||
}
|
||||
|
||||
return Count > 0 ? (Sum / Count) : FVector::ZeroVector;
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::SetTransformMode(EPS_Editor_TransformMode NewMode)
|
||||
{
|
||||
CurrentTransformMode = NewMode;
|
||||
OnSelectionChanged.Broadcast();
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::SetTransformSpace(EPS_Editor_Space NewSpace)
|
||||
{
|
||||
CurrentSpace = NewSpace;
|
||||
OnSelectionChanged.Broadcast();
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::SetActorHighlight(AActor* Actor, bool bHighlight)
|
||||
{
|
||||
if (!Actor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<UPrimitiveComponent*> Primitives;
|
||||
Actor->GetComponents<UPrimitiveComponent>(Primitives);
|
||||
|
||||
for (UPrimitiveComponent* Prim : Primitives)
|
||||
{
|
||||
Prim->SetRenderCustomDepth(bHighlight);
|
||||
Prim->SetCustomDepthStencilValue(bHighlight ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
||||
{
|
||||
APlayerController* PC = OwnerPC.Get();
|
||||
if (!PC) return;
|
||||
|
||||
// Capture transforms BEFORE snap for undo
|
||||
TSharedPtr<FPS_Editor_TransformAction> UndoAction = MakeShared<FPS_Editor_TransformAction>();
|
||||
UndoAction->Description = FString::Printf(TEXT("Snap to ground %d actor(s)"), SelectedActors.Num());
|
||||
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||
{
|
||||
AActor* Actor = Weak.Get();
|
||||
if (!Actor) continue;
|
||||
|
||||
UndoAction->Actors.Add(Actor);
|
||||
UndoAction->OldTransforms.Add(Actor->GetActorTransform());
|
||||
|
||||
FVector Origin, Extent;
|
||||
Actor->GetActorBounds(false, Origin, Extent);
|
||||
const float BottomOffset = Extent.Z;
|
||||
|
||||
const FVector Start = Actor->GetActorLocation();
|
||||
const FVector End = Start - FVector::UpVector * 100000.0f;
|
||||
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams Params;
|
||||
Params.AddIgnoredActor(Actor);
|
||||
Params.AddIgnoredActor(PC->GetPawn());
|
||||
if (Gizmo) Params.AddIgnoredActor(Gizmo);
|
||||
|
||||
if (Actor->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
|
||||
{
|
||||
FVector NewLocation = Hit.ImpactPoint;
|
||||
NewLocation.Z += BottomOffset;
|
||||
Actor->SetActorLocation(NewLocation);
|
||||
}
|
||||
|
||||
UndoAction->NewTransforms.Add(Actor->GetActorTransform());
|
||||
}
|
||||
|
||||
// Record undo action
|
||||
if (UndoAction->Actors.Num() > 0)
|
||||
{
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||
{
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
{
|
||||
UM->RecordAction(UndoAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpdateGizmo();
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::UpdateGizmo()
|
||||
{
|
||||
if (!Gizmo)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsAnythingSelected())
|
||||
{
|
||||
Gizmo->SetGizmoActive(true, GetSelectionPivot());
|
||||
}
|
||||
else
|
||||
{
|
||||
Gizmo->SetGizmoActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_SelectionManager::CleanupStaleReferences()
|
||||
{
|
||||
SelectedActors.RemoveAll([](const TWeakObjectPtr<AActor>& Weak)
|
||||
{
|
||||
return !Weak.IsValid();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/NoExportTypes.h"
|
||||
#include "PS_Editor_SelectionTypes.h"
|
||||
#include "PS_Editor_SelectionManager.generated.h"
|
||||
|
||||
class APlayerController;
|
||||
class APS_Editor_Gizmo;
|
||||
|
||||
DECLARE_MULTICAST_DELEGATE(FPS_Editor_OnSelectionChanged);
|
||||
|
||||
/**
|
||||
* Manages the selection state for the PS_Editor runtime editor.
|
||||
* Owned by APS_Editor_PlayerController.
|
||||
*
|
||||
* Handles: raycast selection, multi-select, highlight toggling (custom depth stencil),
|
||||
* transform mode, and selection-changed notifications.
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API UPS_Editor_SelectionManager : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Initialize with owning player controller. Must be called after construction. */
|
||||
void Initialize(APlayerController* InOwnerPC);
|
||||
|
||||
// ---- Selection Operations ----
|
||||
|
||||
/** Perform a raycast at the given screen position and select/deselect accordingly. */
|
||||
void HandleClickAtScreenPosition(FVector2D ScreenPos, bool bAdditive);
|
||||
|
||||
void SelectActor(AActor* Actor);
|
||||
void DeselectActor(AActor* Actor);
|
||||
void ToggleActor(AActor* Actor);
|
||||
void ClearSelection();
|
||||
|
||||
/** Snap all selected actors to the ground below them. */
|
||||
void SnapSelectionToGround();
|
||||
|
||||
// ---- Queries ----
|
||||
|
||||
const TArray<TWeakObjectPtr<AActor>>& GetSelectedActors() const { return SelectedActors; }
|
||||
bool IsAnythingSelected() const { return SelectedActors.Num() > 0; }
|
||||
bool IsActorSelected(AActor* Actor) const;
|
||||
FVector GetSelectionPivot() const;
|
||||
|
||||
// ---- Transform Mode ----
|
||||
|
||||
EPS_Editor_TransformMode GetTransformMode() const { return CurrentTransformMode; }
|
||||
void SetTransformMode(EPS_Editor_TransformMode NewMode);
|
||||
|
||||
APS_Editor_Gizmo* GetGizmo() const { return Gizmo; }
|
||||
|
||||
EPS_Editor_Space GetTransformSpace() const { return CurrentSpace; }
|
||||
void SetTransformSpace(EPS_Editor_Space NewSpace);
|
||||
|
||||
// ---- Delegates ----
|
||||
|
||||
FPS_Editor_OnSelectionChanged OnSelectionChanged;
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<TWeakObjectPtr<AActor>> SelectedActors;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<APS_Editor_Gizmo> Gizmo;
|
||||
|
||||
EPS_Editor_TransformMode CurrentTransformMode = EPS_Editor_TransformMode::Translate;
|
||||
EPS_Editor_Space CurrentSpace = EPS_Editor_Space::World;
|
||||
|
||||
/** Toggle custom depth stencil rendering on an actor's primitive components. */
|
||||
void SetActorHighlight(AActor* Actor, bool bHighlight);
|
||||
|
||||
/** Clean up any stale weak pointers in the selection array. */
|
||||
void CleanupStaleReferences();
|
||||
|
||||
/** Update gizmo visibility and position based on current selection. */
|
||||
void UpdateGizmo();
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "PS_Editor_SelectionTypes.generated.h"
|
||||
|
||||
/** Transform mode for the gizmo. */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_Editor_TransformMode : uint8
|
||||
{
|
||||
Translate,
|
||||
Rotate,
|
||||
Scale
|
||||
};
|
||||
|
||||
/** Which gizmo axis/plane is being interacted with. */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_Editor_GizmoAxis : uint8
|
||||
{
|
||||
None,
|
||||
X,
|
||||
Y,
|
||||
Z,
|
||||
XY,
|
||||
XZ,
|
||||
YZ,
|
||||
XYZ
|
||||
};
|
||||
|
||||
/** Transform coordinate space. */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_Editor_Space : uint8
|
||||
{
|
||||
World,
|
||||
Local
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
|
||||
void UPS_Editor_UndoManager::RecordAction(TSharedPtr<FPS_Editor_Action> Action)
|
||||
{
|
||||
if (!Action.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Discard any redo history (actions after CurrentIndex)
|
||||
if (CurrentIndex < ActionStack.Num() - 1)
|
||||
{
|
||||
// Cleanup delete actions that will be discarded (truly destroy hidden actors)
|
||||
for (int32 i = CurrentIndex + 1; i < ActionStack.Num(); ++i)
|
||||
{
|
||||
CleanupDeleteAction(ActionStack[i]);
|
||||
}
|
||||
ActionStack.SetNum(CurrentIndex + 1);
|
||||
}
|
||||
|
||||
ActionStack.Add(Action);
|
||||
CurrentIndex = ActionStack.Num() - 1;
|
||||
|
||||
PruneHistory();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Recorded action: %s (stack size: %d)"),
|
||||
*Action->GetDescription(), ActionStack.Num());
|
||||
}
|
||||
|
||||
void UPS_Editor_UndoManager::Undo()
|
||||
{
|
||||
if (!CanUndo())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TSharedPtr<FPS_Editor_Action>& Action = ActionStack[CurrentIndex];
|
||||
Action->Undo();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Undo: %s"), *Action->GetDescription());
|
||||
|
||||
CurrentIndex--;
|
||||
OnUndoRedo.Broadcast();
|
||||
}
|
||||
|
||||
void UPS_Editor_UndoManager::Redo()
|
||||
{
|
||||
if (!CanRedo())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentIndex++;
|
||||
TSharedPtr<FPS_Editor_Action>& Action = ActionStack[CurrentIndex];
|
||||
Action->Execute();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Redo: %s"), *Action->GetDescription());
|
||||
OnUndoRedo.Broadcast();
|
||||
}
|
||||
|
||||
FString UPS_Editor_UndoManager::GetUndoDescription() const
|
||||
{
|
||||
if (!CanUndo())
|
||||
{
|
||||
return TEXT("");
|
||||
}
|
||||
return ActionStack[CurrentIndex]->GetDescription();
|
||||
}
|
||||
|
||||
FString UPS_Editor_UndoManager::GetRedoDescription() const
|
||||
{
|
||||
if (!CanRedo())
|
||||
{
|
||||
return TEXT("");
|
||||
}
|
||||
return ActionStack[CurrentIndex + 1]->GetDescription();
|
||||
}
|
||||
|
||||
void UPS_Editor_UndoManager::PruneHistory()
|
||||
{
|
||||
while (ActionStack.Num() > MaxHistorySize)
|
||||
{
|
||||
// Cleanup the oldest action before removing
|
||||
CleanupDeleteAction(ActionStack[0]);
|
||||
ActionStack.RemoveAt(0);
|
||||
CurrentIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_UndoManager::CleanupDeleteAction(TSharedPtr<FPS_Editor_Action> Action)
|
||||
{
|
||||
if (!Action.IsValid()) return;
|
||||
|
||||
// Truly destroy hidden actors when their action is pruned from history
|
||||
TArray<TWeakObjectPtr<AActor>>* ActorsToClean = nullptr;
|
||||
|
||||
if (Action->ActionType == EPS_Editor_ActionType::Delete)
|
||||
{
|
||||
ActorsToClean = &static_cast<FPS_Editor_DeleteAction*>(Action.Get())->Actors;
|
||||
}
|
||||
else if (Action->ActionType == EPS_Editor_ActionType::Spawn)
|
||||
{
|
||||
ActorsToClean = &static_cast<FPS_Editor_SpawnAction*>(Action.Get())->Actors;
|
||||
}
|
||||
|
||||
if (ActorsToClean)
|
||||
{
|
||||
for (const TWeakObjectPtr<AActor>& Weak : *ActorsToClean)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
if (Actor->IsHidden())
|
||||
{
|
||||
Actor->Destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/NoExportTypes.h"
|
||||
#include "PS_Editor_UndoManager.generated.h"
|
||||
|
||||
enum class EPS_Editor_ActionType : uint8
|
||||
{
|
||||
Transform,
|
||||
Delete,
|
||||
Spawn
|
||||
};
|
||||
|
||||
/**
|
||||
* Base class for undoable editor actions.
|
||||
*/
|
||||
struct FPS_Editor_Action
|
||||
{
|
||||
EPS_Editor_ActionType ActionType;
|
||||
|
||||
explicit FPS_Editor_Action(EPS_Editor_ActionType InType) : ActionType(InType) {}
|
||||
virtual ~FPS_Editor_Action() = default;
|
||||
virtual void Execute() = 0;
|
||||
virtual void Undo() = 0;
|
||||
virtual FString GetDescription() const = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a transform change on one or more actors.
|
||||
* Used for: gizmo translate/rotate/scale, snap to ground, UI transform edit.
|
||||
*/
|
||||
struct FPS_Editor_TransformAction : public FPS_Editor_Action
|
||||
{
|
||||
FPS_Editor_TransformAction() : FPS_Editor_Action(EPS_Editor_ActionType::Transform) {}
|
||||
|
||||
TArray<TWeakObjectPtr<AActor>> Actors;
|
||||
TArray<FTransform> OldTransforms;
|
||||
TArray<FTransform> NewTransforms;
|
||||
FString Description;
|
||||
|
||||
virtual void Execute() override
|
||||
{
|
||||
for (int32 i = 0; i < Actors.Num(); ++i)
|
||||
{
|
||||
if (AActor* Actor = Actors[i].Get())
|
||||
{
|
||||
if (i < NewTransforms.Num())
|
||||
{
|
||||
Actor->SetActorTransform(NewTransforms[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void Undo() override
|
||||
{
|
||||
for (int32 i = 0; i < Actors.Num(); ++i)
|
||||
{
|
||||
if (AActor* Actor = Actors[i].Get())
|
||||
{
|
||||
if (i < OldTransforms.Num())
|
||||
{
|
||||
Actor->SetActorTransform(OldTransforms[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual FString GetDescription() const override { return Description; }
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a soft-delete action (hide + disable collision).
|
||||
* Undo re-shows the actor. Actors are truly destroyed when the action is pruned from the stack.
|
||||
*/
|
||||
struct FPS_Editor_DeleteAction : public FPS_Editor_Action
|
||||
{
|
||||
FPS_Editor_DeleteAction() : FPS_Editor_Action(EPS_Editor_ActionType::Delete) {}
|
||||
|
||||
TArray<TWeakObjectPtr<AActor>> Actors;
|
||||
|
||||
virtual void Execute() override
|
||||
{
|
||||
for (const TWeakObjectPtr<AActor>& Weak : Actors)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
Actor->SetActorHiddenInGame(true);
|
||||
Actor->SetActorEnableCollision(false);
|
||||
Actor->SetActorTickEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void Undo() override
|
||||
{
|
||||
for (const TWeakObjectPtr<AActor>& Weak : Actors)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
Actor->SetActorHiddenInGame(false);
|
||||
Actor->SetActorEnableCollision(true);
|
||||
Actor->SetActorTickEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return FString::Printf(TEXT("Delete %d actor(s)"), Actors.Num());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a spawn action. Undo hides the spawned actor, redo shows it again.
|
||||
*/
|
||||
struct FPS_Editor_SpawnAction : public FPS_Editor_Action
|
||||
{
|
||||
FPS_Editor_SpawnAction() : FPS_Editor_Action(EPS_Editor_ActionType::Spawn) {}
|
||||
|
||||
TArray<TWeakObjectPtr<AActor>> Actors;
|
||||
|
||||
virtual void Execute() override
|
||||
{
|
||||
// Redo: show the spawned actors
|
||||
for (const TWeakObjectPtr<AActor>& Weak : Actors)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
Actor->SetActorHiddenInGame(false);
|
||||
Actor->SetActorEnableCollision(true);
|
||||
Actor->SetActorTickEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void Undo() override
|
||||
{
|
||||
// Undo spawn: hide the actors
|
||||
for (const TWeakObjectPtr<AActor>& Weak : Actors)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
Actor->SetActorHiddenInGame(true);
|
||||
Actor->SetActorEnableCollision(false);
|
||||
Actor->SetActorTickEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return FString::Printf(TEXT("Spawn %d actor(s)"), Actors.Num());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Runtime undo/redo manager for PS_Editor.
|
||||
* Maintains a linear action stack with a cursor.
|
||||
* When a new action is recorded after undoing, all redo history is discarded.
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API UPS_Editor_UndoManager : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Record a new undoable action. Discards any redo history. */
|
||||
void RecordAction(TSharedPtr<FPS_Editor_Action> Action);
|
||||
|
||||
/** Undo the last action. */
|
||||
void Undo();
|
||||
|
||||
/** Redo the next action. */
|
||||
void Redo();
|
||||
|
||||
bool CanUndo() const { return CurrentIndex >= 0; }
|
||||
bool CanRedo() const { return CurrentIndex < ActionStack.Num() - 1; }
|
||||
|
||||
/** Get description of the action that would be undone. */
|
||||
FString GetUndoDescription() const;
|
||||
|
||||
/** Get description of the action that would be redone. */
|
||||
FString GetRedoDescription() const;
|
||||
|
||||
/** Called after any undo/redo to update gizmo, UI, etc. */
|
||||
DECLARE_MULTICAST_DELEGATE(FOnUndoRedo);
|
||||
FOnUndoRedo OnUndoRedo;
|
||||
|
||||
/** Maximum number of actions to keep in history. */
|
||||
int32 MaxHistorySize = 50;
|
||||
|
||||
private:
|
||||
TArray<TSharedPtr<FPS_Editor_Action>> ActionStack;
|
||||
|
||||
/** Points to the last executed action. -1 means no actions executed. */
|
||||
int32 CurrentIndex = -1;
|
||||
|
||||
/** Prune oldest actions if stack exceeds MaxHistorySize. */
|
||||
void PruneHistory();
|
||||
|
||||
/** Destroy actors from pruned delete actions. */
|
||||
void CleanupDeleteAction(TSharedPtr<FPS_Editor_Action> Action);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "PS_Editor_SceneData.generated.h"
|
||||
|
||||
/** Serialized data for a single spawned actor. */
|
||||
USTRUCT()
|
||||
struct FPS_Editor_ActorData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Class path (e.g. "/Game/BP_Cube.BP_Cube_C"). */
|
||||
UPROPERTY()
|
||||
FString ClassPath;
|
||||
|
||||
UPROPERTY()
|
||||
FVector Location = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY()
|
||||
FRotator Rotation = FRotator::ZeroRotator;
|
||||
|
||||
UPROPERTY()
|
||||
FVector Scale = FVector::OneVector;
|
||||
};
|
||||
|
||||
/** Serialized data for a complete scene. */
|
||||
USTRUCT()
|
||||
struct FPS_Editor_SceneData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
int32 Version = 1;
|
||||
|
||||
UPROPERTY()
|
||||
FString SceneName;
|
||||
|
||||
UPROPERTY()
|
||||
FString Timestamp;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FPS_Editor_ActorData> Actors;
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "PS_Editor_SceneSerializer.h"
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "JsonObjectConverter.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "HAL/PlatformFileManager.h"
|
||||
|
||||
bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager)
|
||||
{
|
||||
if (!SpawnManager || SceneName.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build scene data from currently spawned actors
|
||||
FPS_Editor_SceneData SceneData;
|
||||
SceneData.SceneName = SceneName;
|
||||
SceneData.Timestamp = FDateTime::Now().ToString();
|
||||
|
||||
const TArray<TWeakObjectPtr<AActor>>& SpawnedActors = SpawnManager->GetSpawnedActors();
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
|
||||
{
|
||||
AActor* Actor = Weak.Get();
|
||||
if (!Actor || Actor->IsHidden())
|
||||
{
|
||||
continue; // Skip destroyed or soft-deleted actors
|
||||
}
|
||||
|
||||
FPS_Editor_ActorData ActorData;
|
||||
ActorData.ClassPath = Actor->GetClass()->GetPathName();
|
||||
ActorData.Location = Actor->GetActorLocation();
|
||||
ActorData.Rotation = Actor->GetActorRotation();
|
||||
ActorData.Scale = Actor->GetActorScale3D();
|
||||
|
||||
SceneData.Actors.Add(ActorData);
|
||||
}
|
||||
|
||||
// Convert to JSON
|
||||
FString JsonString;
|
||||
if (!FJsonObjectConverter::UStructToJsonObjectString(SceneData, JsonString, 0, 0, 0, nullptr, true))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("PS_Editor: Failed to serialize scene to JSON"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
const FString Directory = GetScenesDirectory();
|
||||
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
|
||||
if (!PlatformFile.DirectoryExists(*Directory))
|
||||
{
|
||||
PlatformFile.CreateDirectoryTree(*Directory);
|
||||
}
|
||||
|
||||
// Write file
|
||||
const FString FilePath = GetSceneFilePath(SceneName);
|
||||
if (!FFileHelper::SaveStringToFile(JsonString, *FilePath))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("PS_Editor: Failed to write scene file: %s"), *FilePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scene saved: %s (%d actors) -> %s"), *SceneName, SceneData.Actors.Num(), *FilePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager)
|
||||
{
|
||||
if (!SpawnManager || SceneName.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read file
|
||||
const FString FilePath = GetSceneFilePath(SceneName);
|
||||
FString JsonString;
|
||||
if (!FFileHelper::LoadFileToString(JsonString, *FilePath))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("PS_Editor: Failed to read scene file: %s"), *FilePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
FPS_Editor_SceneData SceneData;
|
||||
if (!FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &SceneData, 0, 0))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("PS_Editor: Failed to parse scene JSON"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear existing spawned actors
|
||||
ClearScene(SpawnManager);
|
||||
|
||||
// Re-spawn actors from saved data
|
||||
int32 SpawnedCount = 0;
|
||||
for (const FPS_Editor_ActorData& ActorData : SceneData.Actors)
|
||||
{
|
||||
UClass* LoadedClass = LoadObject<UClass>(nullptr, *ActorData.ClassPath);
|
||||
if (!LoadedClass)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load class: %s"), *ActorData.ClassPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
AActor* SpawnedActor = SpawnManager->SpawnActorDirect(LoadedClass, ActorData.Location, ActorData.Rotation, ActorData.Scale);
|
||||
if (SpawnedActor)
|
||||
{
|
||||
SpawnedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scene loaded: %s (%d actors)"), *SceneName, SpawnedCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
TArray<FString> UPS_Editor_SceneSerializer::GetSavedSceneNames() const
|
||||
{
|
||||
TArray<FString> SceneNames;
|
||||
const FString Directory = GetScenesDirectory();
|
||||
|
||||
TArray<FString> Files;
|
||||
IFileManager::Get().FindFiles(Files, *FPaths::Combine(Directory, TEXT("*.json")), true, false);
|
||||
|
||||
for (const FString& File : Files)
|
||||
{
|
||||
SceneNames.Add(FPaths::GetBaseFilename(File));
|
||||
}
|
||||
|
||||
SceneNames.Sort();
|
||||
return SceneNames;
|
||||
}
|
||||
|
||||
bool UPS_Editor_SceneSerializer::DeleteScene(const FString& SceneName)
|
||||
{
|
||||
const FString FilePath = GetSceneFilePath(SceneName);
|
||||
if (IFileManager::Get().Delete(*FilePath))
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scene deleted: %s"), *SceneName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void UPS_Editor_SceneSerializer::ClearScene(UPS_Editor_SpawnManager* SpawnManager)
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = SelectionManagerRef.Get())
|
||||
{
|
||||
SM->ClearSelection();
|
||||
}
|
||||
|
||||
if (SpawnManager)
|
||||
{
|
||||
SpawnManager->DestroyAllSpawnedActors();
|
||||
}
|
||||
}
|
||||
|
||||
FString UPS_Editor_SceneSerializer::GetSceneFilePath(const FString& SceneName) const
|
||||
{
|
||||
return FPaths::Combine(GetScenesDirectory(), SceneName + TEXT(".json"));
|
||||
}
|
||||
|
||||
FString UPS_Editor_SceneSerializer::GetScenesDirectory() const
|
||||
{
|
||||
return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("PS_Editor"), TEXT("Scenes"));
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/NoExportTypes.h"
|
||||
#include "PS_Editor_SceneData.h"
|
||||
#include "PS_Editor_SceneSerializer.generated.h"
|
||||
|
||||
class UPS_Editor_SpawnManager;
|
||||
class UPS_Editor_SelectionManager;
|
||||
|
||||
/**
|
||||
* Handles saving and loading PS_Editor scenes as JSON files.
|
||||
* Scenes are stored in {ProjectDir}/Saved/PS_Editor/Scenes/{SceneName}.json
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API UPS_Editor_SceneSerializer : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Save the current spawned actors to a JSON file. */
|
||||
bool SaveScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager);
|
||||
|
||||
/** Load a scene from a JSON file. Destroys current spawned actors and re-spawns from file. */
|
||||
bool LoadScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager);
|
||||
|
||||
/** Get a list of all saved scene names (without .json extension). */
|
||||
TArray<FString> GetSavedSceneNames() const;
|
||||
|
||||
/** Delete a saved scene file. */
|
||||
bool DeleteScene(const FString& SceneName);
|
||||
|
||||
/** Clear all spawned actors (new scene). Also clears selection and undo history. */
|
||||
void ClearScene(UPS_Editor_SpawnManager* SpawnManager);
|
||||
|
||||
/** Set optional selection manager to clear on load/new. */
|
||||
void SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager) { SelectionManagerRef = InSelectionManager; }
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<UPS_Editor_SelectionManager> SelectionManagerRef;
|
||||
|
||||
/** Get the full file path for a scene name. */
|
||||
FString GetSceneFilePath(const FString& SceneName) const;
|
||||
|
||||
/** Get the directory where scenes are stored. */
|
||||
FString GetScenesDirectory() const;
|
||||
};
|
||||
@@ -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,215 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
|
||||
// Track spawned actor
|
||||
SpawnedActors.Add(SpawnedActor);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
AActor* UPS_Editor_SpawnManager::SpawnActorDirect(UClass* ActorClass, FVector Location, FRotator Rotation, FVector Scale)
|
||||
{
|
||||
APlayerController* PC = OwnerPC.Get();
|
||||
if (!PC || !PC->GetWorld() || !ActorClass)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||
|
||||
AActor* SpawnedActor = PC->GetWorld()->SpawnActor<AActor>(ActorClass, Location, Rotation, SpawnParams);
|
||||
if (!SpawnedActor)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (USceneComponent* Root = SpawnedActor->GetRootComponent())
|
||||
{
|
||||
Root->SetMobility(EComponentMobility::Movable);
|
||||
}
|
||||
|
||||
SpawnedActor->SetActorScale3D(Scale);
|
||||
SpawnedActors.Add(SpawnedActor);
|
||||
|
||||
return SpawnedActor;
|
||||
}
|
||||
|
||||
void UPS_Editor_SpawnManager::DestroyAllSpawnedActors()
|
||||
{
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
Actor->Destroy();
|
||||
}
|
||||
}
|
||||
SpawnedActors.Empty();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: All spawned actors destroyed"));
|
||||
}
|
||||
|
||||
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,82 @@
|
||||
#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);
|
||||
|
||||
/** Spawn an actor directly from a class (used by scene loading). No undo recorded. */
|
||||
AActor* SpawnActorDirect(UClass* ActorClass, FVector Location, FRotator Rotation, FVector Scale);
|
||||
|
||||
/** Destroy all spawned actors (for scene clear/load). */
|
||||
void DestroyAllSpawnedActors();
|
||||
|
||||
/** Get all currently spawned actors. */
|
||||
const TArray<TWeakObjectPtr<AActor>>& GetSpawnedActors() const { return SpawnedActors; }
|
||||
|
||||
/** 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;
|
||||
|
||||
/** All actors spawned by this manager (tracked for save/load). */
|
||||
UPROPERTY()
|
||||
TArray<TWeakObjectPtr<AActor>> SpawnedActors;
|
||||
|
||||
/** 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
|
||||
@@ -0,0 +1,27 @@
|
||||
#include "PS_Editor_HUD.h"
|
||||
#include "PS_Editor_MainWidget.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
|
||||
void APS_Editor_HUD::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (APlayerController* PC = GetOwningPlayerController())
|
||||
{
|
||||
MainWidget = CreateWidget<UPS_Editor_MainWidget>(PC, UPS_Editor_MainWidget::StaticClass());
|
||||
if (MainWidget)
|
||||
{
|
||||
// Wire the SelectionManager to the UI
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||
{
|
||||
MainWidget->SetSelectionManager(EditorPC->GetSelectionManager());
|
||||
MainWidget->SetSpawnManager(EditorPC->GetSpawnManager());
|
||||
MainWidget->SetSceneSerializer(EditorPC->GetSceneSerializer());
|
||||
}
|
||||
|
||||
MainWidget->AddToViewport(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.h
Normal file
29
Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/HUD.h"
|
||||
#include "PS_Editor_HUD.generated.h"
|
||||
|
||||
class UPS_Editor_MainWidget;
|
||||
|
||||
/**
|
||||
* HUD for the PS_Editor runtime editing mode.
|
||||
* Creates and manages the main editor widget.
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API APS_Editor_HUD : public AHUD
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Returns the main editor widget instance. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor|UI")
|
||||
UPS_Editor_MainWidget* GetMainWidget() const { return MainWidget; }
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UPS_Editor_MainWidget> MainWidget;
|
||||
};
|
||||
@@ -0,0 +1,725 @@
|
||||
#include "PS_Editor_MainWidget.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_SceneSerializer.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#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"
|
||||
|
||||
void UPS_Editor_MainWidget::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
|
||||
{
|
||||
SelectionManager = InSelectionManager;
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager)
|
||||
{
|
||||
SpawnManager = InSpawnManager;
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::SetSceneSerializer(UPS_Editor_SceneSerializer* InSerializer)
|
||||
{
|
||||
SceneSerializer = InSerializer;
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||
{
|
||||
return SNew(SOverlay)
|
||||
// Top-left: Title + Toolbar
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(HAlign_Left)
|
||||
.VAlign(VAlign_Top)
|
||||
.Padding(16.0f)
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
// Title bar
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(SBorder)
|
||||
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||
.Padding(FMargin(12.0f, 8.0f))
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("PS Editor")))
|
||||
.ColorAndOpacity(FLinearColor(0.2f, 0.7f, 1.0f, 1.0f))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 14))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
.Padding(12.0f, 0.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SAssignNew(ModeText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("Translate (E)")))
|
||||
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 11))
|
||||
]
|
||||
]
|
||||
]
|
||||
// Toolbar
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 4.0f, 0.0f, 0.0f)
|
||||
[
|
||||
BuildToolbar()
|
||||
]
|
||||
// Scene save/load
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 4.0f, 0.0f, 0.0f)
|
||||
[
|
||||
BuildSceneButtons()
|
||||
]
|
||||
]
|
||||
// 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)
|
||||
.VAlign(VAlign_Top)
|
||||
.Padding(16.0f)
|
||||
[
|
||||
BuildPropertiesPanel()
|
||||
]
|
||||
// Bottom help bar
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(HAlign_Center)
|
||||
.VAlign(VAlign_Bottom)
|
||||
.Padding(16.0f)
|
||||
[
|
||||
SNew(SBorder)
|
||||
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.75f))
|
||||
.Padding(FMargin(16.0f, 6.0f))
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | E/R/T: Translate/Rotate/Scale | End: Snap | Del: Delete")))
|
||||
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
|
||||
{
|
||||
auto MakeButton = [this](const FText& Label, EPS_Editor_TransformMode Mode) -> TSharedRef<SWidget>
|
||||
{
|
||||
return SNew(SButton)
|
||||
.OnClicked_Lambda([this, Mode]() -> FReply
|
||||
{
|
||||
OnModeButtonClicked(Mode);
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(Label)
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
.Justification(ETextJustify::Center)
|
||||
];
|
||||
};
|
||||
|
||||
return SNew(SBorder)
|
||||
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||
.Padding(FMargin(8.0f, 4.0f))
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||
[ MakeButton(FText::FromString(TEXT("Translate (E)")), EPS_Editor_TransformMode::Translate) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||
[ MakeButton(FText::FromString(TEXT("Rotate (R)")), EPS_Editor_TransformMode::Rotate) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||
[ MakeButton(FText::FromString(TEXT("Scale (T)")), EPS_Editor_TransformMode::Scale) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
|
||||
{
|
||||
SM->SnapSelectionToGround();
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Snap Ground")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UPS_Editor_MainWidget::BuildPropertiesPanel()
|
||||
{
|
||||
const FMargin RowPadding(0.0f, 2.0f);
|
||||
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||
const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||
const float LabelWidth = 14.0f;
|
||||
const float FieldWidth = 70.0f;
|
||||
|
||||
auto MakeRow = [&](const FText& Label, TSharedPtr<SEditableTextBox>& X, TSharedPtr<SEditableTextBox>& Y, TSharedPtr<SEditableTextBox>& Z, const FLinearColor& ColorX, const FLinearColor& ColorY, const FLinearColor& ColorZ) -> TSharedRef<SWidget>
|
||||
{
|
||||
auto MakeField = [&](TSharedPtr<SEditableTextBox>& Field, const FLinearColor& Col) -> TSharedRef<SWidget>
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f, 2.0f, 0.0f)
|
||||
[
|
||||
SNew(SBorder)
|
||||
.BorderBackgroundColor(Col)
|
||||
.Padding(FMargin(3.0f, 1.0f))
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Font(LabelFont)
|
||||
.ColorAndOpacity(FLinearColor::White)
|
||||
]
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1.0f)
|
||||
[
|
||||
SAssignNew(Field, SEditableTextBox)
|
||||
.Font(ValueFont)
|
||||
.MinDesiredWidth(FieldWidth)
|
||||
.OnTextCommitted_Lambda([this](const FText&, ETextCommit::Type)
|
||||
{
|
||||
ApplyTransformFromUI();
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(Label)
|
||||
.Font(LabelFont)
|
||||
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||
.MinDesiredWidth(LabelWidth)
|
||||
]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.0f)
|
||||
[ MakeField(X, ColorX) ]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.0f)
|
||||
[ MakeField(Y, ColorY) ]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.0f)
|
||||
[ MakeField(Z, ColorZ) ];
|
||||
};
|
||||
|
||||
const FLinearColor RedX(0.8f, 0.15f, 0.1f);
|
||||
const FLinearColor GreenY(0.15f, 0.6f, 0.1f);
|
||||
const FLinearColor BlueZ(0.1f, 0.3f, 0.8f);
|
||||
|
||||
return SNew(SBorder)
|
||||
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||
.Padding(FMargin(10.0f, 8.0f))
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
+ SVerticalBox::Slot().AutoHeight()
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().FillWidth(1.0f)
|
||||
[
|
||||
SAssignNew(SelectionInfoText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("No selection")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
|
||||
.ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f))
|
||||
]
|
||||
]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Location")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||
]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||
[ MakeRow(FText::GetEmpty(), LocX, LocY, LocZ, RedX, GreenY, BlueZ) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Rotation")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||
]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||
[ MakeRow(FText::GetEmpty(), RotX, RotY, RotZ, RedX, GreenY, BlueZ) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Scale")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||
]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||
[ MakeRow(FText::GetEmpty(), ScaX, ScaY, ScaZ, RedX, GreenY, BlueZ) ]
|
||||
];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
{
|
||||
return SNew(SBorder)
|
||||
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||
.Padding(FMargin(8.0f, 4.0f))
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
// Save row: text field + save button
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 2.0f)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1.0f)
|
||||
.Padding(0.0f, 0.0f, 4.0f, 0.0f)
|
||||
[
|
||||
SAssignNew(SaveNameField, SEditableTextBox)
|
||||
.HintText(FText::FromString(TEXT("Scene name...")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
if (SaveNameField.IsValid() && SceneSerializer.IsValid() && SpawnManager.IsValid())
|
||||
{
|
||||
FString Name = SaveNameField->GetText().ToString();
|
||||
if (!Name.IsEmpty())
|
||||
{
|
||||
SceneSerializer->SaveScene(Name, SpawnManager.Get());
|
||||
// Refresh scene list
|
||||
PopulateSceneList();
|
||||
}
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Save")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
]
|
||||
]
|
||||
// Action buttons: New + Load list
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 2.0f)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0.0f, 0.0f, 4.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
||||
{
|
||||
SceneSerializer->ClearScene(SpawnManager.Get());
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("New Scene")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
]
|
||||
]
|
||||
// Saved scenes list
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 4.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SAssignNew(SceneListContainer, SVerticalBox)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSpawnCatalog()
|
||||
{
|
||||
TSharedRef<SVerticalBox> CatalogList = SNew(SVerticalBox);
|
||||
|
||||
// Header
|
||||
CatalogList->AddSlot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 0.0f, 0.0f, 4.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Spawn Catalogue")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||
.ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f))
|
||||
];
|
||||
|
||||
// Note: entries will be populated in NativeConstruct when SpawnManager is set.
|
||||
// For now, show a placeholder. The actual list is built dynamically.
|
||||
CatalogList->AddSlot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SAssignNew(SpawnListContainer, SVerticalBox)
|
||||
];
|
||||
|
||||
return SNew(SBorder)
|
||||
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||
.Padding(FMargin(10.0f, 8.0f))
|
||||
[
|
||||
SNew(SBox)
|
||||
.MinDesiredWidth(180.0f)
|
||||
.MaxDesiredHeight(400.0f)
|
||||
[
|
||||
SNew(SScrollBox)
|
||||
+ SScrollBox::Slot()
|
||||
[
|
||||
CatalogList
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
PopulateSpawnCatalog();
|
||||
PopulateSceneList();
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::PopulateSceneList()
|
||||
{
|
||||
if (!SceneListContainer.IsValid()) return;
|
||||
SceneListContainer->ClearChildren();
|
||||
|
||||
UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get();
|
||||
if (!Ser) return;
|
||||
|
||||
TArray<FString> Scenes = Ser->GetSavedSceneNames();
|
||||
if (Scenes.Num() == 0)
|
||||
{
|
||||
SceneListContainer->AddSlot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("No saved scenes")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Italic", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
SceneListContainer->AddSlot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Load scene:")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f))
|
||||
];
|
||||
|
||||
for (const FString& SceneName : Scenes)
|
||||
{
|
||||
FString Name = SceneName;
|
||||
SceneListContainer->AddSlot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 1.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this, Name]() -> FReply
|
||||
{
|
||||
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
||||
{
|
||||
SceneSerializer->LoadScene(Name, SpawnManager.Get());
|
||||
if (SaveNameField.IsValid())
|
||||
{
|
||||
SaveNameField->SetText(FText::FromString(Name));
|
||||
}
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(SceneName))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
Super::NativeTick(MyGeometry, InDeltaTime);
|
||||
RefreshPropertiesFromSelection();
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
||||
{
|
||||
bIsRefreshing = true;
|
||||
|
||||
UPS_Editor_SelectionManager* SM = SelectionManager.Get();
|
||||
if (!SM)
|
||||
{
|
||||
bIsRefreshing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Update mode text
|
||||
if (ModeText.IsValid())
|
||||
{
|
||||
FString ModeStr;
|
||||
switch (SM->GetTransformMode())
|
||||
{
|
||||
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (E)"); break;
|
||||
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (R)"); break;
|
||||
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (T)"); break;
|
||||
}
|
||||
ModeText->SetText(FText::FromString(ModeStr));
|
||||
}
|
||||
|
||||
if (!SM->IsAnythingSelected())
|
||||
{
|
||||
if (SelectionInfoText.IsValid())
|
||||
{
|
||||
SelectionInfoText->SetText(FText::FromString(TEXT("No selection")));
|
||||
}
|
||||
auto ClearField = [](TSharedPtr<SEditableTextBox>& Field)
|
||||
{
|
||||
if (Field.IsValid()) Field->SetText(FText::FromString(TEXT("--")));
|
||||
};
|
||||
ClearField(LocX); ClearField(LocY); ClearField(LocZ);
|
||||
ClearField(RotX); ClearField(RotY); ClearField(RotZ);
|
||||
ClearField(ScaX); ClearField(ScaY); ClearField(ScaZ);
|
||||
bIsRefreshing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show first selected actor info
|
||||
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
||||
AActor* Actor = Selected[0].Get();
|
||||
if (!Actor)
|
||||
{
|
||||
bIsRefreshing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectionInfoText.IsValid())
|
||||
{
|
||||
FString Info = FString::Printf(TEXT("%s (%d)"), *Actor->GetActorLabel(), Selected.Num());
|
||||
SelectionInfoText->SetText(FText::FromString(Info));
|
||||
}
|
||||
|
||||
const FVector Loc = Actor->GetActorLocation();
|
||||
const FRotator Rot = Actor->GetActorRotation();
|
||||
const FVector Sca = Actor->GetActorScale3D();
|
||||
|
||||
auto SetField = [this](TSharedPtr<SEditableTextBox>& Field, float Val)
|
||||
{
|
||||
if (Field.IsValid() && !Field->HasKeyboardFocus())
|
||||
{
|
||||
Field->SetText(FText::FromString(FloatToString(Val)));
|
||||
}
|
||||
};
|
||||
|
||||
SetField(LocX, Loc.X); SetField(LocY, Loc.Y); SetField(LocZ, Loc.Z);
|
||||
SetField(RotX, Rot.Roll); SetField(RotY, Rot.Pitch); SetField(RotZ, Rot.Yaw);
|
||||
SetField(ScaX, Sca.X); SetField(ScaY, Sca.Y); SetField(ScaZ, Sca.Z);
|
||||
|
||||
bIsRefreshing = false;
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::ApplyTransformFromUI()
|
||||
{
|
||||
if (bIsRefreshing) return;
|
||||
|
||||
UPS_Editor_SelectionManager* SM = SelectionManager.Get();
|
||||
if (!SM || !SM->IsAnythingSelected()) return;
|
||||
|
||||
AActor* Actor = SM->GetSelectedActors()[0].Get();
|
||||
if (!Actor) return;
|
||||
|
||||
auto GetFloat = [](TSharedPtr<SEditableTextBox>& Field, float Default) -> float
|
||||
{
|
||||
if (!Field.IsValid()) return Default;
|
||||
return FCString::Atof(*Field->GetText().ToString());
|
||||
};
|
||||
|
||||
// Capture old transform for undo
|
||||
const FTransform OldTransform = Actor->GetActorTransform();
|
||||
|
||||
const FVector NewLoc(GetFloat(LocX, 0), GetFloat(LocY, 0), GetFloat(LocZ, 0));
|
||||
const FRotator NewRot(GetFloat(RotY, 0), GetFloat(RotZ, 0), GetFloat(RotX, 0));
|
||||
const FVector NewScale(GetFloat(ScaX, 1), GetFloat(ScaY, 1), GetFloat(ScaZ, 1));
|
||||
|
||||
Actor->SetActorLocation(NewLoc);
|
||||
Actor->SetActorRotation(NewRot);
|
||||
Actor->SetActorScale3D(NewScale);
|
||||
|
||||
// Record undo action
|
||||
if (APlayerController* PC = GetOwningPlayer())
|
||||
{
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||
{
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
{
|
||||
TSharedPtr<FPS_Editor_TransformAction> Action = MakeShared<FPS_Editor_TransformAction>();
|
||||
Action->Actors.Add(Actor);
|
||||
Action->OldTransforms.Add(OldTransform);
|
||||
Action->NewTransforms.Add(Actor->GetActorTransform());
|
||||
Action->Description = TEXT("UI Transform Edit");
|
||||
UM->RecordAction(Action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FString UPS_Editor_MainWidget::FloatToString(float Value) const
|
||||
{
|
||||
return FString::Printf(TEXT("%.2f"), Value);
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::OnModeButtonClicked(EPS_Editor_TransformMode Mode)
|
||||
{
|
||||
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
|
||||
{
|
||||
SM->SetTransformMode(Mode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "PS_Editor_SelectionTypes.h"
|
||||
#include "PS_Editor_MainWidget.generated.h"
|
||||
|
||||
class UPS_Editor_SelectionManager;
|
||||
class UPS_Editor_SpawnManager;
|
||||
class UPS_Editor_SceneSerializer;
|
||||
|
||||
/**
|
||||
* Main editor widget for PS_Editor.
|
||||
* Contains: title bar, transform mode toolbar, properties panel, help bar.
|
||||
* Built entirely in C++ using Slate.
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API UPS_Editor_MainWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Set manager references. Must be called after construction. */
|
||||
void SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager);
|
||||
void SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager);
|
||||
void SetSceneSerializer(UPS_Editor_SceneSerializer* InSerializer);
|
||||
|
||||
protected:
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
virtual void NativeConstruct() override;
|
||||
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
|
||||
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<UPS_Editor_SpawnManager> SpawnManager;
|
||||
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<UPS_Editor_SceneSerializer> SceneSerializer;
|
||||
|
||||
// ---- Slate widget refs for dynamic updates ----
|
||||
TSharedPtr<STextBlock> ModeText;
|
||||
TSharedPtr<STextBlock> SelectionInfoText;
|
||||
|
||||
// Spawn catalog
|
||||
TSharedPtr<SVerticalBox> SpawnListContainer;
|
||||
|
||||
// Scene save/load
|
||||
TSharedPtr<SEditableTextBox> SaveNameField;
|
||||
TSharedPtr<SVerticalBox> SceneListContainer;
|
||||
TArray<TSharedPtr<FSlateBrush>> CachedBrushes;
|
||||
|
||||
// Transform fields
|
||||
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||
TSharedPtr<SEditableTextBox> ScaX, ScaY, ScaZ;
|
||||
|
||||
// ---- Helpers ----
|
||||
TSharedRef<SWidget> BuildToolbar();
|
||||
TSharedRef<SWidget> BuildPropertiesPanel();
|
||||
TSharedRef<SWidget> BuildSpawnCatalog();
|
||||
TSharedRef<SWidget> BuildSceneButtons();
|
||||
void PopulateSceneList();
|
||||
void PopulateSpawnCatalog();
|
||||
void RefreshPropertiesFromSelection();
|
||||
void ApplyTransformFromUI();
|
||||
FString FloatToString(float Value) const;
|
||||
|
||||
void OnModeButtonClicked(EPS_Editor_TransformMode Mode);
|
||||
|
||||
/** Whether we are currently refreshing UI from selection (avoid feedback loop). */
|
||||
bool bIsRefreshing = false;
|
||||
};
|
||||
Reference in New Issue
Block a user