Compare commits

..

4 Commits

Author SHA1 Message Date
6eca4fc94b Add editor-only floor cut to hide primitives above the MandatoryAnchor
Per-component visibility/collision toggle gated on bounds-min Z vs
(AnchorZ + Height). Snapshotted before mutation and restored on
disable, so the feature is non-destructive. Wired into the toolbars of
both Slate widgets and exposed as Blueprint methods on the UMG widget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:07:28 +02:00
4a98c6402b Rename LoadScene/LoadSceneAdditive to LoadScenario/UnloadScenario
The old naming suggested UE level loading; what these calls actually do
is spawn actors described by a .pss scenario file into the already-loaded
UE level. Renamed for clarity:

  SceneLoader::LoadScene         -> LoadScenario
  SceneLoader::LoadSceneFromData -> LoadScenarioFromData
  SceneLoader::LoadSceneAdditive -> dropped (merged into LoadScenario)
  SceneLoader::UnloadSceneAdditive -> UnloadScenario
  SceneSerializer::LoadScene     -> LoadScenario
  AdditiveSpawnedActors          -> TrackedSpawnedActors

LoadScenario now branches on the filter: when Filter == PreloadOnly the
timeline subsystem is skipped entirely (no SetData, no ActorId
registration, no auto-play) so a briefing scene can drop in player
placement markers without disturbing its own timeline. All other filter
values keep the full gameplay behaviour (timeline + AI-readiness poll).

Every load tracks its actors so a follow-up UnloadScenario() can destroy
them — supports the briefing flow where the player toggles between
scenarios at runtime.

Breaking change for external BP callers: any node binding the old
LoadScene / LoadSceneAdditive / UnloadSceneAdditive functions will show
up as missing on next BP compile; replace with LoadScenario /
UnloadScenario (identical or simpler signatures).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:42:57 +02:00
f6efc00c73 Add LoadSceneAdditive / UnloadSceneAdditive for briefing scene preload
Lets a briefing scene materialize only the preload-tagged actors from a
scenario .pss (player placement markers, briefing props) without
clearing the current world, pushing timeline data, registering ActorIds,
or starting any auto-play. The actors spawned by each LoadSceneAdditive
call are tracked in a static TWeakObjectPtr array; UnloadSceneAdditive
destroys every still-valid one and clears the array.

Typical BP usage on scenario change:
  UnloadSceneAdditive(this) -> LoadSceneAdditive(this, NewSceneName)

No BP-side bookkeeping required. Filter defaults to PreloadOnly which
matches the intended briefing use case; the parameter stays exposed for
other niche cases (ExcludePreload, All).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:10:52 +02:00
34280de44a Update memory notes for UE 5.7 host project (was 5.5)
Host project migrated from PROSERVE_UE_5_5 to PROSERVE_UE_5_7 in April
2026. CLAUDE.md, memory index, project notes, and build reference now
point at the new path / target / engine version. The 5.5 host is
flagged deprecated to avoid accidental builds against it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:09:17 +02:00
18 changed files with 698 additions and 62 deletions

View File

@@ -1,4 +1,4 @@
- [PS_Editor plugin project](project_ps_editor.md) — Runtime editor plugin for UE 5.5, Phases 1-4 done, outline material pending
- [PS_Editor plugin project](project_ps_editor.md) — Runtime editor plugin for UE 5.7 (migrated from 5.5 in April 2026), 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)
- [UE5 build process](reference_build.md) — CLI build command for PROSERVE_UE_5_7 (5.5 host deprecated), Build.bat path, target name, junction check
- [Build workflow](feedback_build_workflow.md) — Prefer Live Coding (Ctrl+Alt+F11) when UE is open, CLI build only when UE is closed

View File

@@ -1,10 +1,10 @@
---
name: PS_Editor plugin project
description: Runtime editor plugin for UE 5.5 - object placement, scene editing, JSON save/load in packaged builds
description: Runtime editor plugin for UE 5.7 - 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.
PS_Editor is a runtime editor plugin for Unreal Engine 5.7 for the PROSERVE project. The plugin was started on UE 5.5; the host project migrated to UE 5.7 in April 2026 and the plugin follows. Build details in reference_build.md.
Key decisions:
- UI: UMG driven by C++ (Slate)

View File

@@ -1,18 +1,30 @@
---
name: UE5 build process
description: How to build the PS_ProserveEditor UE 5.5 project from command line on Windows
description: How to build the PROSERVE host project (currently UE 5.7) 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`).
The PS_Editor plugin is developed in `E:\ASTERION\GIT\PS_Editor\Unreal\Plugins\PS_Editor` and consumed by the PROSERVE host project via an NTFS junction. Always build the host project, not the plugin in isolation.
**Current host project (April 2026):** `E:\ASTERION\SVN\DEV\PROSERVE_UE_5_7\PROSERVE_UE_5_7.uproject` (Unreal Engine 5.7)
The previous 5.5 host (`PROSERVE_UE_5_5`) is deprecated — do not use unless explicitly asked.
UE 5.7 is installed at `C:\Program Files\Epic Games\UE_5.7` (found via registry key `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.7`).
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
"C:/Program Files/Epic Games/UE_5.7/Engine/Build/BatchFiles/Build.bat" PROSERVE_UE_5_7Editor Win64 Development -Project="E:/ASTERION/SVN/DEV/PROSERVE_UE_5_7/PROSERVE_UE_5_7.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.
**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 `PROSERVE_UE_5_7Editor` (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
**Plugin junction verification:**
```
ls -la E:/ASTERION/SVN/DEV/PROSERVE_UE_5_7/Plugins/ | grep PS_Editor
```
Should show a symlink pointing to `/e/ASTERION/GIT/PS_Editor/Unreal/Plugins/PS_Editor`. If broken, the host project will build against a stale or missing plugin copy.

View File

@@ -1,7 +1,7 @@
# 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.
Unreal Engine 5.7 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. The plugin was started on UE 5.5; the host project migrated to UE 5.7 in April 2026.
## Architecture
- **Plugin**: PS_Editor (runtime plugin, must work in packaged builds)
@@ -118,14 +118,16 @@ Symptom that the wrong widget was edited: logs you added don't appear in the Out
and wait. Don't pre-emptively write + apply the code change in the same turn.
## Build
- Engine: Unreal Engine 5.5
- Plugin location: `Unreal/Plugins/PS_Editor/`
- Game module: `PS_ProserveEditor` (in `Unreal/Source/PS_ProserveEditor/`)
- Engine: Unreal Engine 5.7 (host project migrated from 5.5 in April 2026)
- Plugin location: `Unreal/Plugins/PS_Editor/` (consumed via NTFS junction from the PROSERVE host project's `Plugins/` directory)
- Host project: `E:\ASTERION\SVN\DEV\PROSERVE_UE_5_7\PROSERVE_UE_5_7.uproject` — always build this, not the plugin in isolation
- Game module: `PROSERVE_UE_5_7` (in the host project's `Source/`)
- CLI build command (Editor, Development):
```
"<UE_INSTALL>/Engine/Build/BatchFiles/Build.bat" PS_ProserveEditorEditor Win64 Development -Project="<REPO>/Unreal/PS_ProserveEditor.uproject" -WaitMutex -FromMsBuild
"C:/Program Files/Epic Games/UE_5.7/Engine/Build/BatchFiles/Build.bat" PROSERVE_UE_5_7Editor Win64 Development -Project="E:/ASTERION/SVN/DEV/PROSERVE_UE_5_7/PROSERVE_UE_5_7.uproject" -WaitMutex -FromMsBuild
```
UE install path can be found via registry: `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.5` > `InstalledDirectory`
UE install path can be found via registry: `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.7` > `InstalledDirectory`
- The 5.5 host (`PROSERVE_UE_5_5`) is deprecated — do not build against it unless explicitly asked.
## 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:

View File

@@ -138,8 +138,8 @@ void APS_Editor_PlayerController::BeginPlay()
if (!Sub->PendingScenarioName.IsEmpty() && SpawnManager && SceneSerializer)
{
bool bLoaded = SceneSerializer->LoadScene(Sub->PendingScenarioName, SpawnManager);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: LoadScene('%s') = %s"),
bool bLoaded = SceneSerializer->LoadScenario(Sub->PendingScenarioName, SpawnManager);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: LoadScenario('%s') = %s"),
*Sub->PendingScenarioName, bLoaded ? TEXT("OK") : TEXT("FAILED"));
}
Sub->PendingScenarioName.Empty();

View File

@@ -49,11 +49,13 @@ namespace
}
}
bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray<AActor*>& OutActors, bool bStripEditorComponents, EPS_Editor_SceneLoadFilter Filter)
TArray<TWeakObjectPtr<AActor>> UPS_Editor_SceneLoader::TrackedSpawnedActors;
bool UPS_Editor_SceneLoader::LoadScenario(const UObject* WorldContextObject, const FString& ScenarioName, TArray<AActor*>& OutActors, bool bStripEditorComponents, EPS_Editor_SceneLoadFilter Filter)
{
OutActors.Empty();
if (!WorldContextObject || SceneName.IsEmpty())
if (!WorldContextObject || ScenarioName.IsEmpty())
{
return false;
}
@@ -65,7 +67,7 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
}
// Read JSON file
const FString FilePath = GetSceneFilePath(SceneName);
const FString FilePath = GetSceneFilePath(ScenarioName);
FString JsonString;
if (!FFileHelper::LoadFileToString(JsonString, *FilePath))
{
@@ -81,11 +83,22 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
return false;
}
if (!LoadSceneFromData(World, SceneData, OutActors, Filter))
if (!LoadScenarioFromData(World, SceneData, OutActors, Filter))
{
return false;
}
// Track every spawned actor so UnloadScenario() can dispose of them later — this is the
// briefing-flow hook (load preload markers, then unload on scenario change). Gameplay
// callers that never unload don't pay a price beyond a small TWeakObjectPtr array.
for (AActor* Actor : OutActors)
{
if (Actor)
{
TrackedSpawnedActors.Add(Actor);
}
}
// Strip editor-only components for clean gameplay
if (bStripEditorComponents)
{
@@ -94,11 +107,6 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
if (!Actor) continue;
TArray<UActorComponent*> ToRemove;
for (UActorComponent* Comp : Actor->GetComponentsByInterface(UPS_Editor_PointPlaceable::StaticClass()))
{
// Don't remove — PointPlaceable is on the actor itself, not a component
}
if (UActorComponent* Spawnable = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
{
ToRemove.Add(Spawnable);
@@ -124,7 +132,28 @@ bool UPS_Editor_SceneLoader::LoadScene(const UObject* WorldContextObject, const
return true;
}
bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, EPS_Editor_SceneLoadFilter Filter)
int32 UPS_Editor_SceneLoader::UnloadScenario(const UObject* WorldContextObject)
{
int32 Destroyed = 0;
for (TWeakObjectPtr<AActor>& Weak : TrackedSpawnedActors)
{
if (AActor* Actor = Weak.Get())
{
Actor->Destroy();
++Destroyed;
}
// else: already gone (GC, manual destroy, level reload) — skip silently
}
TrackedSpawnedActors.Reset();
if (Destroyed > 0)
{
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader: UnloadScenario destroyed %d actor(s)"), Destroyed);
}
return Destroyed;
}
bool UPS_Editor_SceneLoader::LoadScenarioFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors, EPS_Editor_SceneLoadFilter Filter)
{
if (!World)
{
@@ -144,7 +173,13 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S
// Static nav is preserved on disk for scenarios that don't need this.
PS_Editor_NavUtils::SetDynamicNavigationEnabled(World, SceneData.bUseDynamicNavigation);
UPS_Editor_TimelineSubsystem* TimelineSS = World->GetSubsystem<UPS_Editor_TimelineSubsystem>();
// PreloadOnly is the briefing path: only player-placement markers / briefing props
// should spawn, and the briefing scene runs its OWN timeline (don't push the scenario's
// timeline data or auto-play it). Skip the timeline subsystem entirely in that case.
const bool bSkipTimeline = (Filter == EPS_Editor_SceneLoadFilter::PreloadOnly);
UPS_Editor_TimelineSubsystem* TimelineSS = bSkipTimeline
? nullptr
: World->GetSubsystem<UPS_Editor_TimelineSubsystem>();
int32 SpawnedCount = 0;
@@ -154,7 +189,8 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S
AActor* SpawnedActor = SpawnActorFromData(World, ActorData, Filter);
if (SpawnedActor)
{
// Register the stable ID so timeline tracks can find this actor
// Register the stable ID so timeline tracks can find this actor.
// Only when timeline is active for this load (PreloadOnly skips the subsystem).
if (TimelineSS && ActorData.ActorId.IsValid())
{
TimelineSS->RegisterActorId(SpawnedActor, ActorData.ActorId);
@@ -220,8 +256,8 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S
}
}
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader: Loaded %d actors from scene '%s'"),
SpawnedCount, *SceneData.SceneName);
UE_LOG(LogTemp, Log, TEXT("PS_Editor SceneLoader: Loaded %d actor(s) from scenario '%s' [filter=%d]"),
SpawnedCount, *SceneData.SceneName, (int32)Filter);
return SpawnedCount > 0;
}

View File

@@ -5,7 +5,7 @@
#include "Kismet/BlueprintFunctionLibrary.h"
#include "PS_Editor_SceneLoader.generated.h"
/** Filter for selective scene loading. */
/** Filter for selective scenario loading. */
UENUM(BlueprintType)
enum class EPS_Editor_SceneLoadFilter : uint8
{
@@ -15,20 +15,27 @@ enum class EPS_Editor_SceneLoadFilter : uint8
};
/**
* Standalone scene loader for PS_Editor.
* Loads a JSON scene file and spawns all actors with their properties and spline data.
* Standalone scenario loader for PS_Editor.
* A "scenario" is a .pss file describing actors + properties + timeline saved by the runtime
* editor. This loader spawns those actors into an already-loaded UE level — it does NOT
* change the current UE level. Spawning is additive: existing world content is preserved.
*
* This is the entry point for GAME MODE usage — it does NOT require
* PS_Editor's PlayerController, SelectionManager, Gizmo, or UI.
* Just call LoadScene() from your GameMode's BeginPlay.
* Just call LoadScenario() from your GameMode's BeginPlay.
*
* Usage:
* void AMyGameMode::BeginPlay()
* {
* Super::BeginPlay();
* TArray<AActor*> SpawnedActors;
* UPS_Editor_SceneLoader::LoadScene(GetWorld(), TEXT("MyScene"), SpawnedActors);
* UPS_Editor_SceneLoader::LoadScenario(GetWorld(), TEXT("MyScenario"), SpawnedActors);
* }
*
* Briefing usage (player placement markers only, no timeline animation):
* UPS_Editor_SceneLoader::UnloadScenario(this);
* UPS_Editor_SceneLoader::LoadScenario(this, TEXT("MyScenario"), Out, true,
* EPS_Editor_SceneLoadFilter::PreloadOnly);
*/
UCLASS()
class PS_EDITOR_API UPS_Editor_SceneLoader : public UBlueprintFunctionLibrary
@@ -37,56 +44,71 @@ class PS_EDITOR_API UPS_Editor_SceneLoader : public UBlueprintFunctionLibrary
public:
/**
* Load a scene from a JSON file and spawn all actors.
* Load a scenario from a .pss file and spawn its actors into the current world.
*
* @param World The world to spawn actors into.
* @param SceneName Scene name (without .json extension).
* @param OutActors Filled with all spawned actors.
* The call is additive — existing world content is preserved. Spawned actors are tracked
* internally so UnloadScenario() can clean them up; this lets you switch scenarios at
* runtime (typical briefing flow). When Filter == PreloadOnly, the timeline subsystem is
* NOT touched (no SetData, no auto-play, no ActorId registration) so a briefing scene's
* own timeline keeps running untouched.
*
* @param ScenarioName Scenario name (without .pss extension).
* @param OutActors Filled with all spawned actors (also tracked internally).
* @param bStripEditorComponents If true, removes SpawnableComponent and EditableComponent after spawn (cleaner for gameplay).
* @return True if the scene was loaded successfully.
* @param Filter Which actors to spawn. PreloadOnly = briefing-only, skips timeline setup.
* @return True if the scenario was loaded successfully.
*/
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
static bool LoadScene(const UObject* WorldContextObject, const FString& SceneName, TArray<AActor*>& OutActors,
static bool LoadScenario(const UObject* WorldContextObject, const FString& ScenarioName, TArray<AActor*>& OutActors,
bool bStripEditorComponents = true, EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
/**
* Load scene from an already-parsed SceneData struct.
* Useful if you want to parse the JSON yourself or modify data before spawning.
* Destroy every actor previously spawned via LoadScenario(). No-op if nothing was loaded
* or all tracked actors have already been GC'd / manually destroyed.
* Call this before LoadScenario() when switching scenarios.
* @return Number of actors destroyed.
*/
static bool LoadSceneFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors,
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
static int32 UnloadScenario(const UObject* WorldContextObject);
/**
* Load scenario actors from an already-parsed SceneData struct.
* Useful if you want to parse the .pss JSON yourself or modify data before spawning.
*/
static bool LoadScenarioFromData(UWorld* World, const FPS_Editor_SceneData& SceneData, TArray<AActor*>& OutActors,
EPS_Editor_SceneLoadFilter Filter = EPS_Editor_SceneLoadFilter::All);
/**
* Get the directory where scenes are stored.
* Get the directory where scenarios are stored.
*/
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
static FString GetScenesDirectory();
/**
* Get the full file path for a scene name.
* Get the full file path for a scenario name.
*/
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
static FString GetSceneFilePath(const FString& SceneName);
/** Get the full file path for a scene name and its base level. Uses level-specific subdirectory. */
/** Get the full file path for a scenario name and its base level. Uses level-specific subdirectory. */
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
static FString GetSceneFilePathForLevel(const FString& SceneName, const FString& LevelName);
/**
* Get all saved scene names. Optionally filter by level name.
* @param LevelFilter If not empty, only return scenes associated with this level.
* Get all saved scenario names. Optionally filter by level name.
* @param LevelFilter If not empty, only return scenarios associated with this level.
*/
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
static TArray<FString> GetSavedSceneNames(const FString& LevelFilter = TEXT(""));
/**
* Get the level name associated with a saved scene (reads the JSON header without spawning).
* Get the level name associated with a saved scenario (reads the JSON header without spawning).
*/
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
static FString GetSceneLevelName(const FString& SceneName);
/**
* Returns true while a scene is being loaded (actors are being spawned from JSON).
* Returns true while a scenario is being loaded (actors are being spawned from JSON).
* Use in BeginPlay to skip default initialization — wait for OnPropertiesLoaded instead.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS_Editor")
@@ -103,7 +125,7 @@ public:
static bool IsInEditorMode() { return bIsEditorModeActive; }
/**
* Get all saved scenes for the current level.
* Get all saved scenarios for the current level.
*/
UFUNCTION(BlueprintCallable, Category = "PS_Editor", meta = (WorldContext = "WorldContextObject"))
static TArray<FString> GetSavedSceneNamesForCurrentLevel(const UObject* WorldContextObject);
@@ -114,4 +136,8 @@ private:
/** Apply custom editable properties to a spawned actor. */
static void ApplyCustomProperties(AActor* Actor, const TMap<FString, FString>& CustomProperties);
/** Actors spawned via LoadScenario(), tracked so UnloadScenario() can find them.
* TWeakObjectPtr handles GC / manual destruction gracefully (skipped at unload time). */
static TArray<TWeakObjectPtr<AActor>> TrackedSpawnedActors;
};

View File

@@ -202,7 +202,7 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
return true;
}
bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager)
bool UPS_Editor_SceneSerializer::LoadScenario(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager)
{
if (!SpawnManager || SceneName.IsEmpty())
{
@@ -523,9 +523,9 @@ void UPS_Editor_SceneSerializer::ClearScene(UPS_Editor_SpawnManager* SpawnManage
// NOTE: Mandatory-actor auto-spawn is intentionally NOT done here. ClearScene is called
// from three contexts: (1) "New Scenario" button (we want mandatory spawn — handled in the
// widget after ClearScene returns), (2) LoadScene before re-spawning saved actors (we
// widget after ClearScene returns), (2) LoadScenario before re-spawning saved actors (we
// don't want it — the JSON itself contains the mandatory actor, and there's a final
// EnsureMandatoryActorsPresent at the end of LoadScene that handles legacy saves), and
// EnsureMandatoryActorsPresent at the end of LoadScenario that handles legacy saves), and
// (3) BaseLevel switch (we don't want it — the new baseLevel isn't loaded yet, the
// MandatoryAnchor isn't in the world; the post-OnLevelShown hook handles that).
}

View File

@@ -21,8 +21,8 @@ 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);
/** Load a scenario from a .pss file. Destroys current spawned actors and re-spawns from file. */
bool LoadScenario(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager);
/** Get a list of all saved scene names (without .json extension). */
TArray<FString> GetSavedSceneNames() const;

View File

@@ -0,0 +1,230 @@
#include "PS_Editor_FloorCutManager.h"
#include "PS_Editor_MandatoryAnchor.h"
#include "PS_Editor_SceneLoader.h"
#include "Components/PrimitiveComponent.h"
#include "EngineUtils.h"
#include "Engine/World.h"
bool UPS_Editor_FloorCutManager::ShouldCreateSubsystem(UObject* Outer) const
{
// Only create on real game worlds — skip preview / inactive worlds.
if (const UWorld* World = Cast<UWorld>(Outer))
{
const EWorldType::Type WT = World->WorldType;
return WT == EWorldType::Game || WT == EWorldType::PIE || WT == EWorldType::Editor;
}
return false;
}
void UPS_Editor_FloorCutManager::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
bEnabled = false;
HeightAboveAnchor = 200.f;
bAnchorZCached = false;
CachedAnchorZ = 0.f;
}
void UPS_Editor_FloorCutManager::Deinitialize()
{
// Restore everything we touched, so a world tear-down leaves a clean state.
RestoreAll();
Snapshots.Empty();
Super::Deinitialize();
}
void UPS_Editor_FloorCutManager::SetEnabled(bool bInEnabled)
{
// Editor-only feature: silently ignore in gameplay mode. The widgets gate calls already,
// but this is a belt-and-braces safety so nothing leaks into a SceneLoader-driven runtime.
if (!UPS_Editor_SceneLoader::IsInEditorMode())
{
if (bEnabled)
{
RestoreAll();
bEnabled = false;
}
return;
}
if (bEnabled == bInEnabled) return;
bEnabled = bInEnabled;
if (bEnabled)
{
ApplyCutToWorld();
}
else
{
RestoreAll();
}
}
void UPS_Editor_FloorCutManager::SetHeightAboveAnchor(float HeightCm)
{
// Clamp to a sane range matching the UI slider (01000 cm).
HeightCm = FMath::Clamp(HeightCm, 0.f, 10000.f);
if (FMath::IsNearlyEqual(HeightAboveAnchor, HeightCm)) return;
HeightAboveAnchor = HeightCm;
if (bEnabled)
{
ApplyCutToWorld();
}
}
float UPS_Editor_FloorCutManager::GetCutWorldZ() const
{
if (!bAnchorZCached)
{
// Const-cast acceptable: lazy cache only, no observable mutation of "logical" state.
const_cast<UPS_Editor_FloorCutManager*>(this)->ResolveAnchorZ();
}
return CachedAnchorZ + HeightAboveAnchor;
}
void UPS_Editor_FloorCutManager::ReapplyCut()
{
if (!bEnabled) return;
ApplyCutToWorld();
}
void UPS_Editor_FloorCutManager::OnActorSpawned(AActor* Actor)
{
if (!bEnabled || !Actor) return;
ResolveAnchorZ();
const float CutZ = CachedAnchorZ + HeightAboveAnchor;
TArray<UPrimitiveComponent*> Prims;
Actor->GetComponents<UPrimitiveComponent>(Prims);
for (UPrimitiveComponent* P : Prims)
{
ApplyCutToComponent(P, CutZ);
}
}
void UPS_Editor_FloorCutManager::InvalidateAnchorCache()
{
bAnchorZCached = false;
}
float UPS_Editor_FloorCutManager::ResolveAnchorZ()
{
bAnchorZCached = true;
CachedAnchorZ = 0.f;
UWorld* World = GetWorld();
if (!World) return CachedAnchorZ;
for (TActorIterator<APS_Editor_MandatoryAnchor> It(World); It; ++It)
{
if (APS_Editor_MandatoryAnchor* Anchor = *It)
{
CachedAnchorZ = Anchor->GetActorLocation().Z;
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: FloorCut anchor Z = %f"), CachedAnchorZ);
return CachedAnchorZ;
}
}
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: FloorCut — no APS_Editor_MandatoryAnchor found, using world Z=0 as reference"));
return CachedAnchorZ;
}
void UPS_Editor_FloorCutManager::ApplyCutToWorld()
{
UWorld* World = GetWorld();
if (!World) return;
ResolveAnchorZ();
const float CutZ = CachedAnchorZ + HeightAboveAnchor;
int32 Hidden = 0;
int32 Visible = 0;
for (TActorIterator<AActor> ActorIt(World); ActorIt; ++ActorIt)
{
AActor* Actor = *ActorIt;
if (!Actor) continue;
// Never hide the anchor itself (it's hidden in game anyway, but defensive).
if (Actor->IsA<APS_Editor_MandatoryAnchor>()) continue;
TArray<UPrimitiveComponent*> Prims;
Actor->GetComponents<UPrimitiveComponent>(Prims);
for (UPrimitiveComponent* P : Prims)
{
if (!P) continue;
const FBoxSphereBounds B = P->Bounds;
const float MinZ = B.Origin.Z - B.BoxExtent.Z;
if (MinZ > CutZ)
{
ApplyCutToComponent(P, CutZ);
++Hidden;
}
else
{
// Component sits at or straddles the plane → must be in its restored state.
RestoreComponent(P);
++Visible;
}
}
}
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: FloorCut applied — CutZ=%.1f, hidden=%d, kept=%d, anchor=%.1f, height=%.1f"),
CutZ, Hidden, Visible, CachedAnchorZ, HeightAboveAnchor);
}
void UPS_Editor_FloorCutManager::ApplyCutToComponent(UPrimitiveComponent* Comp, float CutZ)
{
if (!Comp) return;
const FBoxSphereBounds B = Comp->Bounds;
const float MinZ = B.Origin.Z - B.BoxExtent.Z;
if (MinZ <= CutZ)
{
// Below the plane — make sure it is in its restored state (caller may not have).
RestoreComponent(Comp);
return;
}
// Snapshot before first mutation, so we can restore the exact original state.
TWeakObjectPtr<UPrimitiveComponent> Key(Comp);
if (!Snapshots.Contains(Key))
{
FCutSnapshot Snap;
Snap.bWasVisible = Comp->IsVisible();
Snap.CollisionEnabled = Comp->GetCollisionEnabled();
Snapshots.Add(Key, Snap);
}
Comp->SetVisibility(false, /*bPropagateToChildren=*/false);
Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
void UPS_Editor_FloorCutManager::RestoreComponent(UPrimitiveComponent* Comp)
{
if (!Comp) return;
TWeakObjectPtr<UPrimitiveComponent> Key(Comp);
const FCutSnapshot* Snap = Snapshots.Find(Key);
if (!Snap) return;
Comp->SetVisibility(Snap->bWasVisible, /*bPropagateToChildren=*/false);
Comp->SetCollisionEnabled(Snap->CollisionEnabled);
Snapshots.Remove(Key);
}
void UPS_Editor_FloorCutManager::RestoreAll()
{
for (auto It = Snapshots.CreateIterator(); It; ++It)
{
UPrimitiveComponent* Comp = It.Key().Get();
if (Comp)
{
Comp->SetVisibility(It.Value().bWasVisible, /*bPropagateToChildren=*/false);
Comp->SetCollisionEnabled(It.Value().CollisionEnabled);
}
}
Snapshots.Empty();
}

View File

@@ -0,0 +1,101 @@
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "Engine/EngineTypes.h"
#include "UObject/WeakObjectPtr.h"
#include "PS_Editor_FloorCutManager.generated.h"
class UPrimitiveComponent;
class APS_Editor_MandatoryAnchor;
/**
* Editor-only horizontal "floor cut" plane.
*
* Hides every UPrimitiveComponent whose world-space bounds-min Z is strictly above
* AnchorZ + HeightAboveAnchor, by toggling per-component visibility and collision.
* The original state is snapshotted before any modification and restored on disable
* (or on plugin exit), so the feature is non-destructive.
*
* Editor-only by design — the gameplay runtime loaded via SceneLoader::LoadScene never
* touches this subsystem. UI in the editor widgets is the only consumer.
*
* Limitations:
* - Components straddling the cut plane (minZ <= cut <= maxZ) stay fully visible.
* Per-pixel cuts would require modifying master materials, which we explicitly
* don't want.
* - Anchor Z is sampled once and cached. If the user moves the APS_Editor_MandatoryAnchor
* at runtime, call InvalidateAnchorCache() to re-sample.
*
* Usage:
* - Get via UWorld::GetSubsystem<UPS_Editor_FloorCutManager>(World)
* - SetEnabled(true), SetHeightAboveAnchor(200.f)
* - OnActorSpawned(NewActor) when SpawnManager spawns a new actor while active
*/
UCLASS()
class PS_EDITOR_API UPS_Editor_FloorCutManager : public UWorldSubsystem
{
GENERATED_BODY()
public:
// UWorldSubsystem
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
/** Enable / disable the cut. On disable, all snapshotted components are restored. */
void SetEnabled(bool bInEnabled);
bool IsEnabled() const { return bEnabled; }
/** Set the cut height in centimeters above the MandatoryAnchor. Re-applies immediately
* if the cut is enabled. */
void SetHeightAboveAnchor(float HeightCm);
float GetHeightAboveAnchor() const { return HeightAboveAnchor; }
/** Computed world Z of the cut plane (AnchorZ + HeightAboveAnchor). Returns
* TNumericLimits<float>::Max() if no anchor is found. */
float GetCutWorldZ() const;
/** Force a re-application of the current cut on all current components.
* Cheap (no allocation if components haven't moved). */
void ReapplyCut();
/** Called by the SpawnManager whenever it spawns a new actor while the cut is
* active, so the new actor's primitives are evaluated against the cut. */
void OnActorSpawned(AActor* Actor);
/** Drop the cached anchor Z (re-sampled on next ReapplyCut). */
void InvalidateAnchorCache();
private:
/** Per-component snapshot of the state we tweak, so we can restore exactly. */
struct FCutSnapshot
{
bool bWasVisible = true;
TEnumAsByte<ECollisionEnabled::Type> CollisionEnabled = ECollisionEnabled::QueryAndPhysics;
};
/** Apply the cut to a single primitive (snapshot if needed, then hide). */
void ApplyCutToComponent(UPrimitiveComponent* Comp, float CutZ);
/** Restore a single primitive from its snapshot, if any. */
void RestoreComponent(UPrimitiveComponent* Comp);
/** Walk the world and apply the cut to every primitive. */
void ApplyCutToWorld();
/** Restore every snapshotted primitive. */
void RestoreAll();
/** Find the world Z of the first MandatoryAnchor in the world, or fall back to 0.
* Result is cached in CachedAnchorZ. */
float ResolveAnchorZ();
bool bEnabled = false;
float HeightAboveAnchor = 200.f; // cm — default 2m as agreed with user
bool bAnchorZCached = false;
float CachedAnchorZ = 0.f;
/** Snapshots of components we have modified, keyed by weak pointer so destroyed
* primitives are simply ignored on restore. */
TMap<TWeakObjectPtr<UPrimitiveComponent>, FCutSnapshot> Snapshots;
};

View File

@@ -8,6 +8,7 @@
#include "PS_Editor_GroundSnap.h"
#include "PS_Editor_NavUtils.h"
#include "PS_Editor_MandatoryAnchor.h"
#include "PS_Editor_FloorCutManager.h"
#include "AIController.h"
#include "BrainComponent.h"
#include "EngineUtils.h"
@@ -310,6 +311,19 @@ AActor* UPS_Editor_SpawnManager::SpawnActorDirect(UClass* ActorClass, FVector Lo
SpawnedActors.Add(SpawnedActor);
RegisterSpawnedActorId(PC->GetWorld(), SpawnedActor);
// If the editor's floor-cut is active, evaluate the new actor against it so it doesn't
// appear floating above a "hidden" floor.
if (UWorld* W = PC->GetWorld())
{
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
{
if (FC->IsEnabled())
{
FC->OnActorSpawned(SpawnedActor);
}
}
}
return SpawnedActor;
}
@@ -335,6 +349,17 @@ void UPS_Editor_SpawnManager::TrackSpawnedActor(AActor* Actor)
if (APlayerController* PC = OwnerPC.Get())
{
RegisterSpawnedActorId(PC->GetWorld(), Actor);
// Re-evaluate against the floor cut (duplicate / paste paths land here).
if (UWorld* W = PC->GetWorld())
{
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
{
if (FC->IsEnabled())
{
FC->OnActorSpawned(Actor);
}
}
}
}
}
}

View File

@@ -32,6 +32,7 @@
#include "DesktopPlatformModule.h"
#include "PS_Editor_GameInstanceSubsystem.h"
#include "PS_Editor_TimelineSubsystem.h"
#include "PS_Editor_FloorCutManager.h"
#include "Engine/World.h"
#include "PS_Editor_UIStyleAsset.h"
#include "PS_Editor_UIStyle.h"
@@ -892,6 +893,70 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
})
]
]
+ SHorizontalBox::Slot().AutoWidth() [ MakeSeparator() ]
// ---- Floor cut: editor-only horizontal plane to hide upper floors ----
+ SHorizontalBox::Slot().AutoWidth()
[
MakeToggleButton(EPS_Editor_Icon::Grid, EPS_Editor_Icon::Grid,
TAttribute<FString>::CreateLambda([this]() -> FString
{
UWorld* W = GetWorld();
UPS_Editor_FloorCutManager* FC = W ? W->GetSubsystem<UPS_Editor_FloorCutManager>() : nullptr;
return (FC && FC->IsEnabled()) ? TEXT("Floor cut On") : TEXT("Floor cut");
}),
[this]() -> bool
{
UWorld* W = GetWorld();
UPS_Editor_FloorCutManager* FC = W ? W->GetSubsystem<UPS_Editor_FloorCutManager>() : nullptr;
return FC && FC->IsEnabled();
},
[this]()
{
if (UWorld* W = GetWorld())
{
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
{
FC->SetEnabled(!FC->IsEnabled());
}
}
})
]
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f)
[
SNew(SBox).MinDesiredWidth(120.0f)
[
SAssignNew(FloorCutSlider, SSlider)
.MinValue(0.0f)
.MaxValue(1000.0f)
.StepSize(10.0f)
.MouseUsesStep(true)
.Value(200.0f)
.ToolTipText(FText::FromString(TEXT("Cut height above MandatoryAnchor (cm) — range 0..1000, step 10")))
.OnValueChanged_Lambda([this](float NewValue)
{
const float Snapped = FMath::RoundToFloat(NewValue / 10.f) * 10.f;
if (UWorld* W = GetWorld())
{
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
{
FC->SetHeightAboveAnchor(Snapped);
}
}
if (FloorCutValueText.IsValid())
{
FloorCutValueText->SetText(FText::FromString(FString::Printf(TEXT("%.0f cm"), Snapped)));
}
})
]
]
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f)
[
SAssignNew(FloorCutValueText, STextBlock)
.Text(FText::FromString(TEXT("200 cm")))
.Font(FPS_Editor_UIStyle::SmallFont(S))
.ColorAndOpacity(FPS_Editor_UIStyle::TextMuted(S))
.MinDesiredWidth(48.0f)
]
];
}
@@ -1352,7 +1417,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
}
}
SceneSerializer->LoadScene(FileName, SpawnManager.Get());
SceneSerializer->LoadScenario(FileName, SpawnManager.Get());
bSceneDirty = false;
if (SaveNameField.IsValid())
{
@@ -1653,7 +1718,7 @@ void UPS_Editor_MainWidget::PopulateSceneList()
// current sublevel as-is — don't unload it (avoids empty scenes).
}
SceneSerializer->LoadScene(Name, SpawnManager.Get());
SceneSerializer->LoadScenario(Name, SpawnManager.Get());
bSceneDirty = false;
if (SaveNameField.IsValid())
{

View File

@@ -107,6 +107,10 @@ private:
TSharedPtr<STextBlock> SnapText;
TSharedPtr<SEditableTextBox> SnapSizeField;
// Floor-cut UI (editor-only horizontal plane that hides primitives above AnchorZ+H)
TSharedPtr<STextBlock> FloorCutValueText;
TSharedPtr<class SSlider> FloorCutSlider;
// Spawn catalog
TSharedPtr<SVerticalBox> SpawnListContainer;

View File

@@ -32,6 +32,7 @@
#include "DesktopPlatformModule.h"
#include "PS_Editor_GameInstanceSubsystem.h"
#include "PS_Editor_TimelineSubsystem.h"
#include "PS_Editor_FloorCutManager.h"
#include "Engine/World.h"
// Forward declaration — the real definition lives further down with the timeline UI block,
@@ -740,6 +741,69 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildToolbar()
})
]
]
// ---- Floor cut: editor-only horizontal plane to hide upper floors ----
+ SHorizontalBox::Slot().AutoWidth().Padding(16.0f, 0.0f, 2.0f, 0.0f).VAlign(VAlign_Center)
[
SNew(SButton)
.ToolTipText(FText::FromString(TEXT("Toggle floor cut. Hides primitives whose bounds-min Z is above (Anchor.Z + Height)")))
.OnClicked_Lambda([this]() -> FReply
{
if (UWorld* W = GetWorld())
{
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
{
FC->SetEnabled(!FC->IsEnabled());
if (FloorCutToggleText.IsValid())
{
FloorCutToggleText->SetText(FText::FromString(FC->IsEnabled() ? TEXT("Floor cut: On") : TEXT("Floor cut: Off")));
}
}
}
return FReply::Handled();
})
[
SAssignNew(FloorCutToggleText, STextBlock)
.Text(FText::FromString(TEXT("Floor cut: Off")))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
]
]
+ SHorizontalBox::Slot().AutoWidth().Padding(4.0f, 0.0f).VAlign(VAlign_Center)
[
SNew(SBox).MinDesiredWidth(120.0f)
[
SAssignNew(FloorCutSlider, SSlider)
.MinValue(0.0f)
.MaxValue(1000.0f)
.StepSize(10.0f)
.MouseUsesStep(true)
.Value(200.0f)
.ToolTipText(FText::FromString(TEXT("Cut height above MandatoryAnchor (cm) — range 0..1000, step 10")))
.OnValueChanged_Lambda([this](float NewValue)
{
// Snap to 10 cm — SSlider's MouseUsesStep already does this for clicks,
// but a drag still emits raw floats, so we re-snap here.
const float Snapped = FMath::RoundToFloat(NewValue / 10.f) * 10.f;
if (UWorld* W = GetWorld())
{
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
{
FC->SetHeightAboveAnchor(Snapped);
}
}
if (FloorCutValueText.IsValid())
{
FloorCutValueText->SetText(FText::FromString(FString::Printf(TEXT("%.0f cm"), Snapped)));
}
})
]
]
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f).VAlign(VAlign_Center)
[
SAssignNew(FloorCutValueText, STextBlock)
.Text(FText::FromString(TEXT("200 cm")))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
.MinDesiredWidth(48.0f)
]
];
}
@@ -1192,7 +1256,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildSceneButtons()
}
}
SceneSerializer->LoadScene(FileName, SpawnManager.Get());
SceneSerializer->LoadScenario(FileName, SpawnManager.Get());
bSceneDirty = false;
if (SaveNameField.IsValid())
{
@@ -1487,7 +1551,7 @@ void UPS_Editor_MainWidget_Legacy::PopulateSceneList()
// current sublevel as-is — don't unload it (avoids empty scenes).
}
SceneSerializer->LoadScene(Name, SpawnManager.Get());
SceneSerializer->LoadScenario(Name, SpawnManager.Get());
bSceneDirty = false;
if (SaveNameField.IsValid())
{

View File

@@ -80,6 +80,11 @@ private:
TSharedPtr<STextBlock> SnapText;
TSharedPtr<SEditableTextBox> SnapSizeField;
// Floor-cut UI (editor-only horizontal plane that hides primitives above AnchorZ+H)
TSharedPtr<STextBlock> FloorCutToggleText;
TSharedPtr<class SSlider> FloorCutSlider;
TSharedPtr<STextBlock> FloorCutValueText;
// Spawn catalog
TSharedPtr<SVerticalBox> SpawnListContainer;

View File

@@ -4,7 +4,9 @@
#include "PS_Editor_SpawnManager.h"
#include "PS_Editor_SceneSerializer.h"
#include "PS_Editor_PlayerController.h"
#include "PS_Editor_FloorCutManager.h"
#include "GameFramework/PlayerController.h"
#include "Engine/World.h"
void UPS_Editor_MainWidget_UMG::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
{
@@ -130,7 +132,7 @@ bool UPS_Editor_MainWidget_UMG::LoadScenario(const FString& Name)
}
if (!SM) SM = SpawnManager.Get();
if (!Ser || !SM || Name.IsEmpty()) return false;
Ser->LoadScene(Name, SM);
Ser->LoadScenario(Name, SM);
OnSceneDirty.Broadcast();
return true;
}
@@ -161,6 +163,54 @@ void UPS_Editor_MainWidget_UMG::ClearSelection()
}
}
// -------------------- Floor cut --------------------
void UPS_Editor_MainWidget_UMG::SetFloorCutEnabled(bool bEnabled)
{
if (UWorld* W = GetWorld())
{
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
{
FC->SetEnabled(bEnabled);
}
}
}
void UPS_Editor_MainWidget_UMG::SetFloorCutHeightCm(float HeightCm)
{
if (UWorld* W = GetWorld())
{
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
{
FC->SetHeightAboveAnchor(HeightCm);
}
}
}
bool UPS_Editor_MainWidget_UMG::IsFloorCutEnabled() const
{
if (UWorld* W = GetWorld())
{
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
{
return FC->IsEnabled();
}
}
return false;
}
float UPS_Editor_MainWidget_UMG::GetFloorCutHeightCm() const
{
if (UWorld* W = GetWorld())
{
if (UPS_Editor_FloorCutManager* FC = W->GetSubsystem<UPS_Editor_FloorCutManager>())
{
return FC->GetHeightAboveAnchor();
}
}
return 200.f;
}
// -------------------- Getters --------------------
EPS_Editor_TransformMode UPS_Editor_MainWidget_UMG::GetCurrentTransformMode() const

View File

@@ -70,6 +70,22 @@ public:
UFUNCTION(BlueprintCallable, Category = "PS Editor|Selection")
void ClearSelection();
// ---- Floor cut (editor-only horizontal plane that hides upper floors) ----
/** Enable/disable the floor cut. Slider value drives the cut height in cm above the
* MandatoryAnchor. */
UFUNCTION(BlueprintCallable, Category = "PS Editor|FloorCut")
void SetFloorCutEnabled(bool bEnabled);
UFUNCTION(BlueprintCallable, Category = "PS Editor|FloorCut")
void SetFloorCutHeightCm(float HeightCm);
UFUNCTION(BlueprintPure, Category = "PS Editor|FloorCut")
bool IsFloorCutEnabled() const;
UFUNCTION(BlueprintPure, Category = "PS Editor|FloorCut")
float GetFloorCutHeightCm() const;
// ==================== BP-PURE GETTERS ====================
UFUNCTION(BlueprintPure, Category = "PS Editor|Tools")
EPS_Editor_TransformMode GetCurrentTransformMode() const;