From 2a53d12e68587741472731f6d6768813d77a9d87 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Thu, 30 Apr 2026 10:40:18 +0200 Subject: [PATCH] Per-scenario dynamic nav opt-in + Settings popup + timeline restore fixes Dynamic navigation toggle (per scenario): - New PS_Editor_NavUtils helpers wrap UNavigationSystemV1 / RecastNavMesh protected APIs (RuntimeGeneration field + bEnableDrawing) via the C++ using-trick so we can switch the level's nav mode and toggle visualization without engine modifications. - FPS_Editor_SceneData gets bUseDynamicNavigation persisted in JSON. - SpawnManager carries the live state plus a session-only bShowNavMesh preference (never saved). - SceneSerializer reads/writes the flag and applies it on load (editor mode). - SceneLoader applies it on the gameplay path before spawning actors so scenarios that need runtime nav rebuild get it both in editor and at runtime via the project's GameMode + SceneLoader::LoadScene flow. UI: - New "Settings" toolbar button in both Legacy and UMG widgets opens a popup with two checkboxes: * Use Dynamic Navigation - saved per scenario, live toggle * Show NavMesh (P) - session only, mimics editor's P key (green nav visualization) - Pawn binds P to IA_ToggleNavMesh -> SpawnManager.bShowNavMesh, in sync with the popup checkbox. Timeline restore correctness: - AddOrUpdateKeyAtCurrentTime, RemoveKey, SetKeyTime, SetKeyInterpolate now invalidate LastAppliedValues[TrackIndex] so a key edit isn't masked by a stale per-track cache entry from a previous apply. - RestoreBaseline now prefers a key at t=0 (within epsilon) over the captured pre-animation baseline. Updating a t=0 key + Stop now restores the new value instead of snapping back to the original. Tracks without a t=0 key still fall back to the pre-animation baseline. Restart simulation rewind: - Restart() used to call Stop() + Play() but Play immediately cancelled the pending RequestStopSimulation timer set by Stop, so transforms were never restored - characters stayed where they were. - RequestStopSimulation now takes bImmediate; when true, OnEditorBeforeSimulateStop still fires (so BP behaviour-reset hooks run) but StopSimulation runs synchronously, restoring transforms before Play() recaptures them. - Restart inlines the reset (RestoreBaseline + immediate stop + Play) so rewinding actually rewinds. Build: - Re-add NavigationSystem to PS_Editor.Build.cs (needed for ARecastNavMesh + ANavigationData access in NavUtils). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../PS_Editor/GameMode/PS_Editor_Pawn.cpp | 22 +++ .../PS_Editor/GameMode/PS_Editor_Pawn.h | 4 + .../GameMode/PS_Editor_PlayerController.cpp | 28 ++- .../GameMode/PS_Editor_PlayerController.h | 9 +- .../Source/PS_Editor/PS_Editor.Build.cs | 3 +- .../Serialization/PS_Editor_SceneData.h | 7 + .../Serialization/PS_Editor_SceneLoader.cpp | 7 + .../PS_Editor_SceneSerializer.cpp | 18 ++ .../PS_Editor/Spawn/PS_Editor_NavUtils.cpp | 61 ++++++ .../PS_Editor/Spawn/PS_Editor_NavUtils.h | 37 ++++ .../PS_Editor/Spawn/PS_Editor_SpawnManager.h | 13 ++ .../Timeline/PS_Editor_TimelineSubsystem.cpp | 97 +++++++-- .../UI/Widgets/PS_Editor_MainWidget.cpp | 162 +++++++++++++++ .../UI/Widgets/PS_Editor_MainWidget.h | 7 + .../Widgets/PS_Editor_MainWidget_Legacy.cpp | 187 ++++++++++++++++++ .../UI/Widgets/PS_Editor_MainWidget_Legacy.h | 9 + 16 files changed, 653 insertions(+), 18 deletions(-) create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.cpp create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.h diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp index 6f7ba4f..d8f4849 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp @@ -13,6 +13,8 @@ #include "GameFramework/PlayerController.h" #include "PS_Editor_SelectionManager.h" #include "PS_Editor_GroundSnap.h" +#include "PS_Editor_NavUtils.h" +#include "PS_Editor_SpawnManager.h" #include "PS_Editor_PlayerController.h" #include "PS_Editor_Gizmo.h" #include "PS_Editor_UndoManager.h" @@ -254,6 +256,11 @@ void APS_Editor_Pawn::CreateInputActions() IA_Focus = NewObject(this, TEXT("IA_Focus")); IA_Focus->ValueType = EInputActionValueType::Boolean; EditorMappingContext->MapKey(IA_Focus, EKeys::F); + + // Toggle navmesh visualization (mimics UE editor's P key) + IA_ToggleNavMesh = NewObject(this, TEXT("IA_ToggleNavMesh")); + IA_ToggleNavMesh->ValueType = EInputActionValueType::Boolean; + EditorMappingContext->MapKey(IA_ToggleNavMesh, EKeys::P); } void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) @@ -301,6 +308,7 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp EIC->BindAction(IA_Confirm, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleConfirm); EIC->BindAction(IA_Cancel, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCancel); EIC->BindAction(IA_Focus, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleFocus); + EIC->BindAction(IA_ToggleNavMesh, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleToggleNavMesh); } // ---- Input Handlers ---- @@ -1460,6 +1468,20 @@ void APS_Editor_Pawn::HandleFocus(const FInputActionValue& Value) OrbitDistance = TargetDistance; } +void APS_Editor_Pawn::HandleToggleNavMesh(const FInputActionValue& Value) +{ + // Mimics UE editor's P key — toggles the runtime navmesh visualization. We route + // through SpawnManager's session-only bShowNavMesh flag so the Settings popup + // checkbox stays in sync. + APS_Editor_PlayerController* EditorPC = Cast(GetController()); + if (!EditorPC) return; + UPS_Editor_SpawnManager* SM = EditorPC->GetSpawnManager(); + if (!SM) return; + + SM->bShowNavMesh = !SM->bShowNavMesh; + PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), SM->bShowNavMesh); +} + // ---- Helpers ---- void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h index fb7065d..692944b 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h @@ -212,6 +212,9 @@ private: UPROPERTY() TObjectPtr IA_Focus; + UPROPERTY() + TObjectPtr IA_ToggleNavMesh; + // ---- Spline point / ChildMovable drag state ---- TWeakObjectPtr DraggedSplineActor; TArray SplinePointDragInitialState; @@ -260,6 +263,7 @@ private: void HandleConfirm(const FInputActionValue& Value); void HandleCancel(const FInputActionValue& Value); void HandleFocus(const FInputActionValue& Value); + void HandleToggleNavMesh(const FInputActionValue& Value); // ---- Focus animation state (F key, frame selection) ---- bool bIsFocusing = false; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp index f32b1d8..8f78735 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp @@ -389,18 +389,23 @@ void APS_Editor_PlayerController::SetSimulationPaused(bool bPaused) } } -void APS_Editor_PlayerController::RequestStopSimulation() +void APS_Editor_PlayerController::RequestStopSimulation(bool bImmediate) { if (!bSimulating) return; // If a request is already scheduled, do nothing (debounce double-clicks on Stop). - if (UWorld* World = GetWorld()) + // Skip the debounce when bImmediate (Restart wants to flush right now). + if (!bImmediate) { - if (PendingStopSimulationHandle.IsValid()) return; + if (UWorld* World = GetWorld()) + { + if (PendingStopSimulationHandle.IsValid()) return; + } } // Fire OnEditorBeforeSimulateStop immediately on every actor so BP handlers can react - // while the BT is still live. Then schedule the real cut for ~500ms later. + // while the BT is still live. Then schedule the real cut (deferred) or run it now + // (immediate, used by timeline Restart). if (SpawnManager) { for (const TWeakObjectPtr& Weak : SpawnManager->GetSpawnedActors()) @@ -413,6 +418,21 @@ void APS_Editor_PlayerController::RequestStopSimulation() } } + if (bImmediate) + { + // Cancel any pending deferred stop so it doesn't fire again later. + if (UWorld* World = GetWorld()) + { + if (PendingStopSimulationHandle.IsValid()) + { + World->GetTimerManager().ClearTimer(PendingStopSimulationHandle); + PendingStopSimulationHandle.Invalidate(); + } + } + StopSimulation(); + return; + } + if (UWorld* World = GetWorld()) { TWeakObjectPtr WeakThis(this); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h index a309580..b0ed328 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h @@ -76,9 +76,14 @@ public: /** Request a deferred stop: fires OnEditorBeforeSimulateStop immediately, then schedules * the real StopSimulation after a grace period (500ms by default) so BT has time to * react to whatever the BP handler does in OnEditorBeforeSimulateStop. - * Timeline Stop and the Simulate toolbar button both go through this. */ + * Timeline Stop and the Simulate toolbar button both go through this. + * + * @param bImmediate If true, fire OnEditorBeforeSimulateStop and run StopSimulation + * synchronously in the same call (no grace period). Used by + * timeline Restart, where we want transforms restored before the + * subsequent Play(). Defaults to false (deferred). */ UFUNCTION(BlueprintCallable, Category = "PS_Editor") - void RequestStopSimulation(); + void RequestStopSimulation(bool bImmediate = false); /** True while simulation is running. */ UFUNCTION(BlueprintCallable, Category = "PS_Editor") diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs index 6cf9c9e..6f1ced4 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs @@ -33,7 +33,8 @@ public class PS_Editor : ModuleRules "JsonUtilities", "ProceduralMeshComponent", "DesktopPlatform", - "AIModule" + "AIModule", + "NavigationSystem" }); PrivateDependencyModuleNames.AddRange(new string[] diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h index 8a1b9a6..6e8d3df 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h @@ -61,4 +61,11 @@ struct FPS_Editor_SceneData /** Global timeline data (animation tracks + duration). Empty on legacy scenes. */ UPROPERTY() FPS_Editor_TimelineData Timeline; + + /** If true, the level's nav mesh is switched to Dynamic runtime generation when this + * scenario loads — both in the runtime editor (so spawn / move actors update nav live) + * and in gameplay (for scenarios that need runtime-built navigation). False keeps the + * cooked Static nav, which has zero runtime cost. Default: false. */ + UPROPERTY() + bool bUseDynamicNavigation = false; }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp index cbbd49a..ac994ae 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneLoader.cpp @@ -10,6 +10,7 @@ #include "PS_Editor_TimelineSubsystem.h" #include "PS_Editor_SpawnManager.h" #include "PS_Editor_PlayerController.h" +#include "PS_Editor_NavUtils.h" #include "AIController.h" #include "BrainComponent.h" #include "JsonObjectConverter.h" @@ -130,6 +131,12 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S return false; } + // Apply the scenario's dynamic-navigation flag BEFORE spawning actors. If the scenario + // needs runtime nav generation, switching the level's RecastNavMesh to Dynamic now + // means the actors we're about to spawn will dirty + rebuild tiles around them. Cooked + // 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(); int32 SpawnedCount = 0; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp index 9a25ed6..2550d88 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp @@ -14,6 +14,7 @@ #include "GameFramework/CharacterMovementComponent.h" #include "PS_Editor_SplineEditable.h" #include "PS_Editor_TimelineSubsystem.h" +#include "PS_Editor_NavUtils.h" #include "JsonObjectConverter.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" @@ -166,6 +167,12 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_ SceneData.Timeline = TimelineSS->GetData(); } + // Per-scenario settings — read from the SpawnManager's session state. + if (SpawnManager) + { + SceneData.bUseDynamicNavigation = SpawnManager->bUseDynamicNavigation; + } + // Convert to JSON FString JsonString; if (!FJsonObjectConverter::UStructToJsonObjectString(SceneData, JsonString, 0, 0, 0, nullptr, true)) @@ -438,6 +445,17 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_ } } + // Per-scenario settings — push into the SpawnManager's session state and apply + // the dynamic-navigation switch to the level's RecastNavMesh actors right away. + if (SpawnManager) + { + SpawnManager->bUseDynamicNavigation = SceneData.bUseDynamicNavigation; + if (APS_Editor_PlayerController* EditorPC = Cast(GetOuter())) + { + PS_Editor_NavUtils::SetDynamicNavigationEnabled(EditorPC->GetWorld(), SpawnManager->bUseDynamicNavigation); + } + } + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scene loaded: %s (%d actors)"), *SceneName, SpawnedCount); return true; } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.cpp new file mode 100644 index 0000000..90203af --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.cpp @@ -0,0 +1,61 @@ +#include "PS_Editor_NavUtils.h" +#include "NavigationSystem.h" +#include "NavigationData.h" +#include "NavMesh/RecastNavMesh.h" +#include "Engine/World.h" +#include "Kismet/GameplayStatics.h" + +namespace PS_Editor_NavUtils +{ + // ANavigationData::RuntimeGeneration and bEnableDrawing are both protected. Local + // using-trick subclass to re-publish them — the C++ access check uses the static + // type, so a static_cast through this exposing class lets us read/write them + // without modifying the engine. + struct FNavDataAccessor : public ANavigationData + { + using ANavigationData::RuntimeGeneration; + using ANavigationData::bEnableDrawing; + }; + + void SetRuntimeGenerationMode(UWorld* World, ERuntimeGenerationType Mode) + { + if (!World) return; + + TArray NavMeshActors; + UGameplayStatics::GetAllActorsOfClass(World, ANavigationData::StaticClass(), NavMeshActors); + for (AActor* A : NavMeshActors) + { + if (ANavigationData* NavData = Cast(A)) + { + FNavDataAccessor* Accessor = static_cast(NavData); + if (Accessor->RuntimeGeneration != Mode) + { + Accessor->RuntimeGeneration = Mode; + } + } + } + } + + void SetDynamicNavigationEnabled(UWorld* World, bool bDynamic) + { + SetRuntimeGenerationMode(World, + bDynamic ? ERuntimeGenerationType::Dynamic : ERuntimeGenerationType::Static); + } + + void SetNavMeshVisible(UWorld* World, bool bVisible) + { + if (!World) return; + + TArray NavMeshActors; + UGameplayStatics::GetAllActorsOfClass(World, ANavigationData::StaticClass(), NavMeshActors); + for (AActor* A : NavMeshActors) + { + if (ANavigationData* NavData = Cast(A)) + { + FNavDataAccessor* Accessor = static_cast(NavData); + Accessor->bEnableDrawing = bVisible; + NavData->MarkComponentsRenderStateDirty(); + } + } + } +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.h new file mode 100644 index 0000000..e88b7d8 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.h @@ -0,0 +1,37 @@ +#pragma once + +#include "CoreMinimal.h" + +class UWorld; +enum class ERuntimeGenerationType : uint8; + +/** + * Centralised helpers for switching the level's RecastNavMesh runtime-generation mode and + * toggling the runtime navmesh visualization. Used by: + * - The runtime editor "Settings" popup (live toggle of dynamic nav + show navmesh) + * - SceneLoader (gameplay path: applies the scenario's bUseDynamicNavigation flag) + * - Scenario save/load (persists bUseDynamicNavigation in JSON) + */ +namespace PS_Editor_NavUtils +{ + /** + * Switch every RecastNavMesh in the world to the requested runtime-generation mode. + * Static = baked nav only, no runtime updates (fastest gameplay). + * DynamicModifiersOnly = only NavModifierVolumes affect nav at runtime. + * Dynamic = all collision-affecting actors update the nav mesh on spawn / move. + * + * Idempotent — calling with the same mode the navmesh already has is a no-op. + */ + PS_EDITOR_API void SetRuntimeGenerationMode(UWorld* World, ERuntimeGenerationType Mode); + + /** Convenience: switch to Dynamic if bDynamic, Static otherwise. */ + PS_EDITOR_API void SetDynamicNavigationEnabled(UWorld* World, bool bDynamic); + + /** + * Toggle the runtime navmesh visualization (mimics the editor's P key — green nav lines + * + tile bounds rendered on top of the world). Affects all RecastNavMesh actors in the + * world. Free in editor PIE/Standalone; in shipping packaged builds, debug rendering may + * be stripped depending on build flags. + */ + PS_EDITOR_API void SetNavMeshVisible(UWorld* World, bool bVisible); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h index 29929eb..efcf7fd 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h @@ -75,6 +75,19 @@ public: /** Notify all spawned actors that implement IPS_Editor_EditableInterface of editor mode change. */ void NotifyEditorModeChanged(bool bIsEditing); + // ---- Per-scenario settings (persisted in JSON) ---- + + /** If true, switch the level's RecastNavMesh to Dynamic runtime generation when this + * scenario is loaded — both in editor and gameplay. False = keep cooked Static nav. + * Persisted in the scenario JSON; default false. */ + bool bUseDynamicNavigation = false; + + // ---- Session-only UI preferences (NOT persisted) ---- + + /** If true, the navmesh is rendered at runtime (mimics the editor's P key). Personal + * visualization preference, never saved with the scenario. */ + bool bShowNavMesh = false; + /** Tick all spawned actors that implement IPS_Editor_EditableInterface. */ void TickEditorMode(float DeltaTime); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.cpp index 445d61d..3e22b81 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.cpp @@ -194,20 +194,16 @@ void UPS_Editor_TimelineSubsystem::CaptureBaselinesForAllMissingTracks() void UPS_Editor_TimelineSubsystem::RestoreBaseline() { - if (PrePlayBaseline.Num() == 0) return; - // Server-only write path (same guard as ApplyValuesAtTime). if (UWorld* World = GetWorld()) { if (World->GetNetMode() == NM_Client) return; } + const float ZeroEpsilon = 0.0001f; + for (const FPS_Editor_TimelineTrack& Track : Data.Tracks) { - const TPair Key(Track.ActorId, Track.PropertyPath); - const FString* BaselineValue = PrePlayBaseline.Find(Key); - if (!BaselineValue) continue; - AActor* Actor = FindActorById(Track.ActorId); if (!Actor) continue; UObject* Target = nullptr; @@ -215,7 +211,39 @@ void UPS_Editor_TimelineSubsystem::RestoreBaseline() ResolveTarget(Actor, Track.PropertyPath, Target, Prop); if (!Target || !Prop) continue; - Prop->ImportText_InContainer(**BaselineValue, Target, Target, PPF_None); + // Pick the value to restore: + // - If the user placed a key at (or before) t=0 → use that key's value. This + // matches the natural mental model where a key at t=0 sets the timeline's + // starting state — and lets the user adjust it just by re-keying at t=0 + // (otherwise Stop would keep snapping back to the captured pre-animation + // baseline forever, ignoring their edits). + // - Otherwise fall back to the captured pre-animation baseline so Stop still + // undoes whatever the timeline pushed (relevant for tracks whose first key + // is past t=0, e.g. a property that only changes mid-timeline). + const FString* RestoreValue = nullptr; + FString KeyZeroValue; + for (const FPS_Editor_TimelineKey& K : Track.Keys) + { + if (K.Time <= ZeroEpsilon) + { + KeyZeroValue = K.Value; + RestoreValue = &KeyZeroValue; + // Continue scanning in case there are several keys at t≈0 — the LAST one wins + // (matches sort order: later same-time entries override earlier ones). + } + else + { + break; // keys are sorted by time; no need to look past t>0 + } + } + if (!RestoreValue) + { + const TPair BaselineKey(Track.ActorId, Track.PropertyPath); + RestoreValue = PrePlayBaseline.Find(BaselineKey); + } + if (!RestoreValue) continue; + + Prop->ImportText_InContainer(**RestoreValue, Target, Target, PPF_None); if (UActorComponent* AsComp = Cast(Target)) { AsComp->MarkRenderStateDirty(); @@ -234,15 +262,40 @@ void UPS_Editor_TimelineSubsystem::RestoreBaseline() } } - // Don't clear the map — a subsequent Stop (or Restart) must still restore the same baseline. + // Don't clear PrePlayBaseline — subsequent Stops without a t=0 key still need it. // SetData/Clear are the only resetters. } void UPS_Editor_TimelineSubsystem::Restart() { - // Rewind + replay from the start. Done in two steps so simulation transforms are restored - // by Stop() before Play() re-enables AI, giving a clean "from the beginning" rerun. - Stop(); + // Rewind + replay from the start. + // + // We can't just call Stop() then Play() because Stop() goes through + // PlayerController::RequestStopSimulation which DEFERS the actual StopSimulation by + // 500ms (BT grace period). The Play() call right after cancels that pending timer + // AND re-enters StartSimulation early (bSimulating still true), so transforms are + // never restored — characters stay at their current position instead of rewinding. + // + // Fix: do the full reset synchronously here. Fire OnEditorBeforeSimulateStop (via + // RequestStopSimulation(bImmediate=true) so BP behaviour-reset hooks still run), + // then immediately StopSimulation (which restores transforms + fires + // OnEditorSimulateStop), then Play() to capture the rewound transforms and restart + // AI from the clean state. + bIsPlaying = false; + CurrentTime = 0.f; + LastAppliedValues.Empty(); + LastAppliedValues.SetNum(Data.Tracks.Num()); + + RestoreBaseline(); + + if (APS_Editor_PlayerController* EditorPC = FindEditorPC(GetWorld())) + { + if (EditorPC->IsSimulating()) + { + EditorPC->RequestStopSimulation(/*bImmediate=*/ true); + } + } + Play(); } @@ -364,6 +417,14 @@ bool UPS_Editor_TimelineSubsystem::AddOrUpdateKeyAtCurrentTime(AActor* Actor, co if (FMath::IsNearlyEqual(Track.Keys[InsertAt].Time, CurrentTime, Epsilon)) { Track.Keys[InsertAt].Value = CurrentValue; + // Invalidate the per-track applied-value cache so the next ApplyTrackAtTime + // re-writes the (possibly different) value instead of skipping based on the + // stale cache entry. Otherwise, updating a key with a value the cache happened + // to hold from a previous apply would silently revert the property to it. + if (LastAppliedValues.IsValidIndex(TrackIndex)) + { + LastAppliedValues[TrackIndex].Empty(); + } OnTimelineChanged.Broadcast(); return true; } @@ -404,6 +465,12 @@ void UPS_Editor_TimelineSubsystem::RemoveKey(int32 TrackIndex, int32 KeyIndex) Data.Tracks.RemoveAt(TrackIndex); LastAppliedValues.SetNum(Data.Tracks.Num()); } + else if (LastAppliedValues.IsValidIndex(TrackIndex)) + { + // Track still has keys but its surrounding keys for any time may have changed — + // invalidate the cache so the next apply re-writes the correct value. + LastAppliedValues[TrackIndex].Empty(); + } OnTimelineChanged.Broadcast(); } @@ -413,6 +480,10 @@ void UPS_Editor_TimelineSubsystem::SetKeyInterpolate(int32 TrackIndex, int32 Key FPS_Editor_TimelineTrack& Track = Data.Tracks[TrackIndex]; if (!Track.Keys.IsValidIndex(KeyIndex)) return; Track.Keys[KeyIndex].bInterpolate = bInterpolate; + if (LastAppliedValues.IsValidIndex(TrackIndex)) + { + LastAppliedValues[TrackIndex].Empty(); + } OnTimelineChanged.Broadcast(); } @@ -428,6 +499,10 @@ void UPS_Editor_TimelineSubsystem::SetKeyTime(int32 TrackIndex, int32 KeyIndex, { return A.Time < B.Time; }); + if (LastAppliedValues.IsValidIndex(TrackIndex)) + { + LastAppliedValues[TrackIndex].Empty(); + } OnTimelineChanged.Broadcast(); } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index fb015a0..b494856 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -1,6 +1,7 @@ #include "PS_Editor_MainWidget.h" #include "PS_Editor_SelectionManager.h" #include "PS_Editor_SpawnManager.h" +#include "PS_Editor_NavUtils.h" #include "PS_Editor_SceneSerializer.h" #include "PS_Editor_SceneLoader.h" #include "PS_Editor_UndoManager.h" @@ -500,6 +501,110 @@ TSharedRef UPS_Editor_MainWidget::RebuildWidget() ] ] ] + // Settings popup (per-scenario nav option + session navmesh visu) + + SOverlay::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SAssignNew(SettingsPopup, SBorder) + .Visibility(EVisibility::Collapsed) + .BorderImage(BrushGlassStrong.Get()) + .Padding(FMargin(28.0f, 20.0f)) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 0.0f, 0.0f, 16.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Settings"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 14)) + .ColorAndOpacity(FLinearColor::White) + ] + + SVerticalBox::Slot().AutoHeight().Padding(0.0f, 4.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState + { + if (UPS_Editor_SpawnManager* SM = SpawnManager.Get()) + { + return SM->bUseDynamicNavigation ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + } + return ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState State) { OnUseDynamicNavigationToggled(State); }) + ] + + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Use Dynamic Navigation"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) + .ColorAndOpacity(FLinearColor::White) + ] + + SVerticalBox::Slot().AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Rebuild navmesh at runtime when actors are spawned/moved (saved with scenario)."))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f)) + .AutoWrapText(true) + ] + ] + ] + + SVerticalBox::Slot().AutoHeight().Padding(0.0f, 8.0f, 0.0f, 4.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState + { + if (UPS_Editor_SpawnManager* SM = SpawnManager.Get()) + { + return SM->bShowNavMesh ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + } + return ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState State) { OnShowNavMeshToggled(State); }) + ] + + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Show NavMesh (P)"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) + .ColorAndOpacity(FLinearColor::White) + ] + + SVerticalBox::Slot().AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Render the navmesh in green (mimics editor's P key). Personal preference, never saved."))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f)) + .AutoWrapText(true) + ] + ] + ] + + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 16.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .OnClicked_Lambda([this]() -> FReply { CloseSettingsPopup(); return FReply::Handled(); }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Close"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) + .ColorAndOpacity(FLinearColor::White) + ] + ] + ] + ] // ============= COMMAND PALETTE (modal, Ctrl+K) ============= + SOverlay::Slot() @@ -987,6 +1092,26 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) ] ] + // Settings button + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(4.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ToolTipText(FText::FromString(TEXT("Open scenario / session settings (dynamic nav, navmesh visu)."))) + .OnClicked_Lambda([this]() -> FReply + { + OpenSettingsPopup(); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Settings"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + .ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f)) + ] + ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) @@ -3207,6 +3332,43 @@ void UPS_Editor_MainWidget::CancelBaseLevelPopup() PendingBaseLevelSelection.Empty(); } +void UPS_Editor_MainWidget::OpenSettingsPopup() +{ + if (SettingsPopup.IsValid()) + { + SettingsPopup->SetVisibility(EVisibility::Visible); + } +} + +void UPS_Editor_MainWidget::CloseSettingsPopup() +{ + if (SettingsPopup.IsValid()) + { + SettingsPopup->SetVisibility(EVisibility::Collapsed); + } +} + +void UPS_Editor_MainWidget::OnUseDynamicNavigationToggled(ECheckBoxState State) +{ + UPS_Editor_SpawnManager* SM = SpawnManager.Get(); + if (!SM) return; + + const bool bEnabled = (State == ECheckBoxState::Checked); + SM->bUseDynamicNavigation = bEnabled; + bSceneDirty = true; + PS_Editor_NavUtils::SetDynamicNavigationEnabled(GetWorld(), bEnabled); +} + +void UPS_Editor_MainWidget::OnShowNavMeshToggled(ECheckBoxState State) +{ + UPS_Editor_SpawnManager* SM = SpawnManager.Get(); + if (!SM) return; + + const bool bVisible = (State == ECheckBoxState::Checked); + SM->bShowNavMesh = bVisible; + PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), bVisible); +} + void UPS_Editor_MainWidget::OnCloseClicked() { if (CloseConfirmOverlay.IsValid()) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h index f9c255a..4e0aba8 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h @@ -134,6 +134,13 @@ private: void ConfirmBaseLevelSelection(); void CancelBaseLevelPopup(); + // Global Settings popup (per-scenario nav option + session-only navmesh visu). + TSharedPtr SettingsPopup; + void OpenSettingsPopup(); + void CloseSettingsPopup(); + void OnUseDynamicNavigationToggled(ECheckBoxState State); + void OnShowNavMeshToggled(ECheckBoxState State); + // Transform fields TSharedPtr LocX, LocY, LocZ; TSharedPtr RotX, RotY, RotZ; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.cpp index 583e5ca..bb49af1 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.cpp @@ -1,6 +1,7 @@ #include "PS_Editor_MainWidget_Legacy.h" #include "PS_Editor_SelectionManager.h" #include "PS_Editor_SpawnManager.h" +#include "PS_Editor_NavUtils.h" #include "PS_Editor_SceneSerializer.h" #include "PS_Editor_SceneLoader.h" #include "PS_Editor_UndoManager.h" @@ -448,6 +449,124 @@ TSharedRef UPS_Editor_MainWidget_Legacy::RebuildWidget() ] ] ] + ] + // Settings popup (per-scenario nav option + session navmesh visu) + + SOverlay::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SAssignNew(SettingsPopup, SBorder) + .Visibility(EVisibility::Collapsed) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.08f, 0.08f, 0.08f, 0.97f)) + .Padding(FMargin(28.0f, 20.0f)) + [ + SNew(SVerticalBox) + // Header + + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 0.0f, 0.0f, 16.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Settings"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 14)) + .ColorAndOpacity(FLinearColor::White) + ] + // Use Dynamic Navigation (saved per-scenario) + + SVerticalBox::Slot().AutoHeight().Padding(0.0f, 4.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState + { + if (UPS_Editor_SpawnManager* SM = SpawnManager.Get()) + { + return SM->bUseDynamicNavigation ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + } + return ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState State) + { + OnUseDynamicNavigationToggled(State); + }) + ] + + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Use Dynamic Navigation"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) + .ColorAndOpacity(FLinearColor::White) + ] + + SVerticalBox::Slot().AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Rebuild navmesh at runtime when actors are spawned/moved (saved with scenario)."))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f)) + .AutoWrapText(true) + ] + ] + ] + // Show NavMesh (session-only) + + SVerticalBox::Slot().AutoHeight().Padding(0.0f, 8.0f, 0.0f, 4.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState + { + if (UPS_Editor_SpawnManager* SM = SpawnManager.Get()) + { + return SM->bShowNavMesh ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + } + return ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState State) + { + OnShowNavMeshToggled(State); + }) + ] + + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Show NavMesh (P)"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) + .ColorAndOpacity(FLinearColor::White) + ] + + SVerticalBox::Slot().AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Render the navmesh in green (mimics editor's P key). Personal preference, never saved."))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f)) + .AutoWrapText(true) + ] + ] + ] + // Close button + + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 16.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + CloseSettingsPopup(); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Close"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) + .ColorAndOpacity(FLinearColor::White) + ] + ] + ] ]; } @@ -816,6 +935,25 @@ TSharedRef UPS_Editor_MainWidget_Legacy::BuildSceneButtons() .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) ] ] + // Settings button — opens the global Settings popup + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(4.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .ToolTipText(FText::FromString(TEXT("Open scenario / session settings (dynamic nav, navmesh visu)."))) + .OnClicked_Lambda([this]() -> FReply + { + OpenSettingsPopup(); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Settings"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + .ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f)) + ] + ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) @@ -2868,6 +3006,55 @@ void UPS_Editor_MainWidget_Legacy::CancelBaseLevelPopup() PendingBaseLevelSelection.Empty(); } +void UPS_Editor_MainWidget_Legacy::OpenSettingsPopup() +{ + if (SettingsPopup.IsValid()) + { + SettingsPopup->SetVisibility(EVisibility::Visible); + } +} + +void UPS_Editor_MainWidget_Legacy::CloseSettingsPopup() +{ + if (SettingsPopup.IsValid()) + { + SettingsPopup->SetVisibility(EVisibility::Collapsed); + } +} + +void UPS_Editor_MainWidget_Legacy::OnUseDynamicNavigationToggled(ECheckBoxState State) +{ + UPS_Editor_SpawnManager* SM = SpawnManager.Get(); + if (!SM) return; + + const bool bEnabled = (State == ECheckBoxState::Checked); + SM->bUseDynamicNavigation = bEnabled; + bSceneDirty = true; // user changed a per-scenario setting + + // Apply live to the level's RecastNavMesh actors. + PS_Editor_NavUtils::SetDynamicNavigationEnabled(GetWorld(), bEnabled); +} + +void UPS_Editor_MainWidget_Legacy::OnShowNavMeshToggled(ECheckBoxState State) +{ + UPS_Editor_SpawnManager* SM = SpawnManager.Get(); + if (!SM) return; + + const bool bVisible = (State == ECheckBoxState::Checked); + SM->bShowNavMesh = bVisible; + + PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), bVisible); +} + +void UPS_Editor_MainWidget_Legacy::ToggleShowNavMesh() +{ + UPS_Editor_SpawnManager* SM = SpawnManager.Get(); + if (!SM) return; + + SM->bShowNavMesh = !SM->bShowNavMesh; + PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), SM->bShowNavMesh); +} + void UPS_Editor_MainWidget_Legacy::OnCloseClicked() { if (CloseConfirmOverlay.IsValid()) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.h index 417164c..98bf7db 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.h @@ -107,6 +107,15 @@ private: void ConfirmBaseLevelSelection(); void CancelBaseLevelPopup(); + // Global Settings popup (per-scenario nav option + session-only navmesh visu). + TSharedPtr SettingsPopup; + void OpenSettingsPopup(); + void CloseSettingsPopup(); + void OnUseDynamicNavigationToggled(ECheckBoxState State); + void OnShowNavMeshToggled(ECheckBoxState State); + /** Toggle the navmesh visualization (P key handler). */ + void ToggleShowNavMesh(); + // Transform fields TSharedPtr LocX, LocY, LocZ; TSharedPtr RotX, RotY, RotZ;