diff --git a/editor/AGENTS.md b/editor/AGENTS.md index bdee0405..5e65c58c 100644 --- a/editor/AGENTS.md +++ b/editor/AGENTS.md @@ -10,11 +10,10 @@ The editor is not in a "split more layers for their own sake" phase. The primary product line is still runtime/product loop closure: -- bind real owners for `run.*` -- bind real owners for `scripts.*` -- bind real owners for scene document commands such as - `file.new_scene`, `file.open_scene`, `file.save_scene`, and - `file.save_scene_as` +- extend the bound `EditorRuntimeCoordinator` instead of adding panel-local + shortcuts for `run.*`, scene document commands, or project scene opens +- bind a real script assembly builder behind `scripts.*`; until then the + coordinator must keep the command honestly disabled - keep `Game`, `Scene`, `Inspector`, `Selection`, and `Console` coherent across runtime transitions @@ -51,21 +50,28 @@ Rules: ## Product Truths - `GameViewportFeature`, `GameViewportRenderService`, and related tests exist. - The game viewport path is real, but that does not mean play-mode ownership is - complete. -- `EditorHostCommandBridge` still exposes honest failure messages for commands - without a bound owner. Do not replace these with fake success. -- `EditorSceneRuntime` already owns real scene editing behavior. Missing work is - usually at the app/document/runtime owner layer, not in the raw scene backend. -- `ProjectPanel` should keep using explicit runtime ownership and honest blocked - behavior for unsupported asset commands. + `EditorRuntimeCoordinator` is now the app-level owner for play-mode command + routing and uses `RuntimeLoop` for the active scene. +- `EditorHostCommandBridge` delegates `file.*`, `run.*`, and `scripts.*` to a + bound runtime owner. If no owner is bound, it must continue exposing honest + disabled messages. +- `EditorSceneRuntime` owns raw scene editing behavior. `EditorRuntimeCoordinator` + owns scene document state: current path/name, dirty flag, new/open/save + routing, and runtime-mode transitions. +- `ProjectPanel` may identify an openable scene asset, but scene document loading + must go through `EditorPanelServices::RequestOpenSceneAsset` and the + coordinator. Do not reintroduce `ProjectRuntime` pending-open queues. +- `scripts.rebuild` has a bound coordinator owner, but no in-process script + assembly builder is currently wired. Keep it disabled and explicit until that + builder exists. ## Working Rules - prefer explicit owner/coordinator/runtime-service seams over direct panel to engine hookups - when changing runtime or document flow, inspect `EditorSession`, - `EditorHostCommandBridge`, the relevant feature, and the matching tests + `EditorRuntimeCoordinator`, `EditorHostCommandBridge`, the relevant feature, + and the matching tests - if a change affects ownership, state flow, or command routing, add or update tests in `tests/UI/Editor/unit` - keep multi-window work behind runtime/document correctness; do not move it @@ -77,12 +83,15 @@ Rules: - no new low-level render pass dependency on scene backend creation or lifecycle - no silent fallback where a command should fail honestly - no panel-local shortcut that bypasses the intended runtime owner +- no scene-open side channel in `EditorProjectRuntime`; project UI must call the + panel service request hook ## Good Entry Points - `editor/app/Bootstrap/Application.cpp` - `editor/app/Composition/EditorContext.cpp` - `editor/app/Composition/EditorShellRuntime.cpp` +- `editor/app/Services/Runtime/EditorRuntimeCoordinator.cpp` - `editor/app/Rendering/Viewport/ViewportHostService.cpp` - `editor/app/Services/Engine/EngineEditorServices.h` - `editor/app/Services/Scene/EditorSceneRuntime.cpp` diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 335d41d8..677fca60 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -288,6 +288,7 @@ if(XCENGINE_BUILD_XCUI_EDITOR_CORE) app/Services/Engine/EngineSceneViewportBridge.cpp app/Services/Scene/EngineEditorSceneBackend.cpp app/Services/Scene/EditorSceneRuntime.cpp + app/Services/Runtime/EditorRuntimeCoordinator.cpp app/Services/Project/EditorProjectRuntime.cpp app/Services/Project/ProjectBrowserModel.cpp ) diff --git a/editor/app/Composition/EditorContext.cpp b/editor/app/Composition/EditorContext.cpp index 5ca7e186..cfc64cb5 100644 --- a/editor/app/Composition/EditorContext.cpp +++ b/editor/app/Composition/EditorContext.cpp @@ -26,6 +26,13 @@ void RequestEditorContextUtilityWindow( } } +bool RequestEditorContextOpenSceneAsset( + void* requester, + const std::filesystem::path& scenePath) { + auto* context = static_cast(requester); + return context != nullptr && context->RequestOpenSceneAsset(scenePath); +} + std::string ComposeStatusText( std::string_view status, std::string_view message) { @@ -91,11 +98,17 @@ bool EditorContext::Initialize( } AppendUIEditorRuntimeTrace("startup", "EditorSceneRuntime::Initialize end"); m_sceneRuntime.BindSelectionService(&m_selectionService); + m_runtimeCoordinator.Initialize( + m_session, + m_sceneRuntime, + m_projectRuntime, + runtimePaths); ResetEditorColorPickerToolState(m_colorPickerToolState); ResetEditorUtilityWindowRequestState(m_utilityWindowRequestState); SyncSessionFromSelectionService(); m_hostCommandBridge.BindSession(m_session); m_hostCommandBridge.BindCommandFocusService(m_commandFocusService); + m_hostCommandBridge.BindRuntimeCommandOwner(&m_runtimeCoordinator); SyncSessionFromCommandFocusService(); m_shortcutManager = BuildEditorShellShortcutManager(m_shellAsset); m_shortcutManager.SetHostCommandHandler(&m_hostCommandBridge); @@ -141,6 +154,10 @@ void EditorContext::SyncSessionFromWorkspace( SyncSessionFromCommandFocusService(); } +void EditorContext::TickEditorRuntime() { + m_runtimeCoordinator.TickFrame(); +} + bool EditorContext::IsValid() const { return m_valid; } @@ -181,6 +198,14 @@ const EditorSceneRuntime& EditorContext::GetSceneRuntime() const { return m_sceneRuntime; } +EditorRuntimeCoordinator& EditorContext::GetRuntimeCoordinator() { + return m_runtimeCoordinator; +} + +const EditorRuntimeCoordinator& EditorContext::GetRuntimeCoordinator() const { + return m_runtimeCoordinator; +} + EditorColorPickerToolState& EditorContext::GetColorPickerToolState() { return m_colorPickerToolState; } @@ -246,9 +271,24 @@ EditorPanelServices EditorContext::BuildPanelServices() { .textMeasurer = m_shellServices.textMeasurer, .utilityWindowRequester = this, .requestUtilityWindow = RequestEditorContextUtilityWindow, + .sceneAssetOpenRequester = this, + .requestOpenSceneAsset = RequestEditorContextOpenSceneAsset, }; } +bool EditorContext::RequestOpenSceneAsset(const std::filesystem::path& scenePath) { + const bool opened = m_runtimeCoordinator.RequestOpenSceneAsset(scenePath); + SetStatus( + "Scene", + opened + ? m_runtimeCoordinator.GetLastMessage() + : m_runtimeCoordinator.GetLastMessage().empty() + ? std::string("Failed to open scene asset.") + : m_runtimeCoordinator.GetLastMessage()); + SyncSessionFromSelectionService(); + return opened; +} + UIEditorShellInteractionDefinition EditorContext::BuildShellDefinition( const UIEditorWorkspaceController& workspaceController, std::string_view captureText, diff --git a/editor/app/Composition/EditorContext.h b/editor/app/Composition/EditorContext.h index 51ea27ab..3f793eee 100644 --- a/editor/app/Composition/EditorContext.h +++ b/editor/app/Composition/EditorContext.h @@ -5,6 +5,7 @@ #include "Windowing/EditorFrameServices.h" #include "Scene/EditorSceneRuntime.h" #include "Project/EditorProjectRuntime.h" +#include "Runtime/EditorRuntimeCoordinator.h" #include "UtilityWindows/EditorUtilityWindowRuntime.h" #include "Commands/EditorHostCommandBridge.h" @@ -48,6 +49,7 @@ public: void SetExitRequestHandler(std::function handler); void SyncSessionFromWorkspace( const UIEditorWorkspaceController& workspaceController) override; + void TickEditorRuntime() override; bool IsValid() const override; const std::string& GetValidationMessage() const override; @@ -59,6 +61,8 @@ public: const EditorProjectRuntime& GetProjectRuntime() const; EditorSceneRuntime& GetSceneRuntime(); const EditorSceneRuntime& GetSceneRuntime() const; + EditorRuntimeCoordinator& GetRuntimeCoordinator(); + const EditorRuntimeCoordinator& GetRuntimeCoordinator() const; EditorColorPickerToolState& GetColorPickerToolState(); const EditorColorPickerToolState& GetColorPickerToolState() const; void RequestOpenUtilityWindow(EditorUtilityWindowKind kind); @@ -73,6 +77,7 @@ public: UIEditorWorkspaceController BuildWorkspaceController() const; const UIEditorShellInteractionServices& GetShellServices() const override; EditorPanelServices BuildPanelServices() override; + bool RequestOpenSceneAsset(const std::filesystem::path& scenePath); UIEditorShellInteractionDefinition BuildShellDefinition( const UIEditorWorkspaceController& workspaceController, @@ -104,6 +109,7 @@ private: EditorSelectionService m_selectionService = {}; EditorProjectRuntime m_projectRuntime = {}; EditorSceneRuntime m_sceneRuntime = {}; + EditorRuntimeCoordinator m_runtimeCoordinator = {}; EditorColorPickerToolState m_colorPickerToolState = {}; EditorUtilityWindowRequestState m_utilityWindowRequestState = {}; EditorHostCommandBridge m_hostCommandBridge = {}; diff --git a/editor/app/Composition/EditorShellRuntime.cpp b/editor/app/Composition/EditorShellRuntime.cpp index 11c9c959..5414c6cf 100644 --- a/editor/app/Composition/EditorShellRuntime.cpp +++ b/editor/app/Composition/EditorShellRuntime.cpp @@ -177,8 +177,11 @@ std::unique_ptr CreateEditorWorkspaceShellRuntime( namespace XCEngine::UI::Editor::App { void EditorShellRuntime::RenderRequestedViewports( + EditorFrameServices& frameServices, const ::XCEngine::Rendering::RenderContext& renderContext) { if (m_viewportRuntimeServices != nullptr) { + frameServices.SyncSceneViewportRenderRequest( + m_viewportRuntimeServices->GetSceneViewportRuntime()); m_viewportRuntimeServices->RenderRequestedViewports(renderContext); } } @@ -255,8 +258,7 @@ void EditorShellRuntime::Update( }); m_traceEntries = frameServices.SyncWorkspacePanelFrameEvents( m_workspacePanels.CollectFrameEvents()); - frameServices.SyncSceneViewportRenderRequest( - m_viewportRuntimeServices->GetSceneViewportRuntime()); + frameServices.TickEditorRuntime(); } } // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Composition/EditorShellRuntime.h b/editor/app/Composition/EditorShellRuntime.h index cc85936b..d75cf726 100644 --- a/editor/app/Composition/EditorShellRuntime.h +++ b/editor/app/Composition/EditorShellRuntime.h @@ -75,6 +75,7 @@ public: float detachedTitleBarTabHeight = 0.0f, float detachedWindowChromeHeight = 0.0f) override; void RenderRequestedViewports( + EditorFrameServices& frameServices, const ::XCEngine::Rendering::RenderContext& renderContext) override; void Append(::XCEngine::UI::UIDrawData& drawData) const override; diff --git a/editor/app/Composition/WorkspaceEventSync.cpp b/editor/app/Composition/WorkspaceEventSync.cpp index 34c37e3d..72e19852 100644 --- a/editor/app/Composition/WorkspaceEventSync.cpp +++ b/editor/app/Composition/WorkspaceEventSync.cpp @@ -2,8 +2,6 @@ #include "EditorContext.h" -#include - namespace XCEngine::UI::Editor::App { std::vector SyncWorkspaceEvents( @@ -11,12 +9,6 @@ std::vector SyncWorkspaceEvents( const std::vector& panelEvents) { std::vector entries = {}; context.SyncSessionFromSelectionService(); - if (const std::optional scenePath = - context.GetProjectRuntime().ConsumePendingSceneOpenPath(); - scenePath.has_value()) { - context.GetSceneRuntime().OpenSceneAsset(scenePath.value()); - context.SyncSessionFromSelectionService(); - } for (const EditorWorkspacePanelFrameEvent& event : panelEvents) { context.SetStatus(event.status, event.message); diff --git a/editor/app/Core/Commands/EditorHostCommandBridge.cpp b/editor/app/Core/Commands/EditorHostCommandBridge.cpp index 30c07a1a..a794b67f 100644 --- a/editor/app/Core/Commands/EditorHostCommandBridge.cpp +++ b/editor/app/Core/Commands/EditorHostCommandBridge.cpp @@ -14,6 +14,11 @@ void EditorHostCommandBridge::BindCommandFocusService( m_commandFocusService = &commandFocusService; } +void EditorHostCommandBridge::BindRuntimeCommandOwner( + RuntimeCommandOwner* runtimeCommandOwner) { + m_runtimeCommandOwner = runtimeCommandOwner; +} + void EditorHostCommandBridge::BindEditCommandRoutes( EditorEditCommandRoute* hierarchyRoute, EditorEditCommandRoute* projectRoute, @@ -39,6 +44,18 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateHostCommand return BuildDisabledResult("About dialog is unavailable in the current shell."); } + if (commandId.rfind("file.", 0u) == 0u) { + return EvaluateFileCommand(commandId); + } + + if (commandId.rfind("run.", 0u) == 0u) { + return EvaluateRunCommand(commandId); + } + + if (commandId.rfind("scripts.", 0u) == 0u) { + return EvaluateScriptCommand(commandId); + } + if (commandId.rfind("edit.", 0u) == 0u) { return EvaluateEditCommand(commandId); } @@ -68,6 +85,30 @@ UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchHostCommand( return result; } + if (commandId.rfind("file.", 0u) == 0u) { + if (m_runtimeCommandOwner != nullptr) { + return m_runtimeCommandOwner->DispatchFileCommand(commandId); + } + result.message = EvaluateHostCommand(commandId).message; + return result; + } + + if (commandId.rfind("run.", 0u) == 0u) { + if (m_runtimeCommandOwner != nullptr) { + return m_runtimeCommandOwner->DispatchRunCommand(commandId); + } + result.message = EvaluateHostCommand(commandId).message; + return result; + } + + if (commandId.rfind("scripts.", 0u) == 0u) { + if (m_runtimeCommandOwner != nullptr) { + return m_runtimeCommandOwner->DispatchScriptCommand(commandId); + } + result.message = EvaluateHostCommand(commandId).message; + return result; + } + if (commandId.rfind("edit.", 0u) == 0u) { return DispatchEditCommand(commandId); } @@ -102,8 +143,12 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateFileCommand return BuildExecutableResult("Exit editor."); } + if (m_runtimeCommandOwner != nullptr) { + return m_runtimeCommandOwner->EvaluateFileCommand(commandId); + } + return BuildDisabledResult( - "Only file.exit has a bound host owner in the current shell."); + "File commands have no bound document owner in the current shell."); } UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateAssetCommand( @@ -117,14 +162,20 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateAssetComman UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateRunCommand( std::string_view commandId) const { - (void)commandId; + if (m_runtimeCommandOwner != nullptr) { + return m_runtimeCommandOwner->EvaluateRunCommand(commandId); + } + return BuildDisabledResult( "Run commands have no bound play-mode owner in the current shell."); } UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateScriptCommand( std::string_view commandId) const { - (void)commandId; + if (m_runtimeCommandOwner != nullptr) { + return m_runtimeCommandOwner->EvaluateScriptCommand(commandId); + } + return BuildDisabledResult( "Script commands have no bound script-pipeline owner in the current shell."); } diff --git a/editor/app/Core/Commands/EditorHostCommandBridge.h b/editor/app/Core/Commands/EditorHostCommandBridge.h index 6c515b15..7bbc0801 100644 --- a/editor/app/Core/Commands/EditorHostCommandBridge.h +++ b/editor/app/Core/Commands/EditorHostCommandBridge.h @@ -13,8 +13,27 @@ namespace XCEngine::UI::Editor::App { class EditorHostCommandBridge : public UIEditorHostCommandHandler { public: + class RuntimeCommandOwner { + public: + virtual ~RuntimeCommandOwner() = default; + + virtual UIEditorHostCommandEvaluationResult EvaluateFileCommand( + std::string_view commandId) const = 0; + virtual UIEditorHostCommandDispatchResult DispatchFileCommand( + std::string_view commandId) = 0; + virtual UIEditorHostCommandEvaluationResult EvaluateRunCommand( + std::string_view commandId) const = 0; + virtual UIEditorHostCommandDispatchResult DispatchRunCommand( + std::string_view commandId) = 0; + virtual UIEditorHostCommandEvaluationResult EvaluateScriptCommand( + std::string_view commandId) const = 0; + virtual UIEditorHostCommandDispatchResult DispatchScriptCommand( + std::string_view commandId) = 0; + }; + void BindSession(EditorSession& session); void BindCommandFocusService(const EditorCommandFocusService& commandFocusService); + void BindRuntimeCommandOwner(RuntimeCommandOwner* runtimeCommandOwner); void BindEditCommandRoutes( EditorEditCommandRoute* hierarchyRoute, EditorEditCommandRoute* projectRoute, @@ -57,6 +76,7 @@ private: EditorEditCommandRoute* m_projectRoute = nullptr; EditorEditCommandRoute* m_sceneRoute = nullptr; EditorEditCommandRoute* m_inspectorRoute = nullptr; + RuntimeCommandOwner* m_runtimeCommandOwner = nullptr; std::function m_requestExit = {}; }; diff --git a/editor/app/Core/Panels/EditorPanelServices.h b/editor/app/Core/Panels/EditorPanelServices.h index 2f75bc12..f3287875 100644 --- a/editor/app/Core/Panels/EditorPanelServices.h +++ b/editor/app/Core/Panels/EditorPanelServices.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace XCEngine::UI::Editor { @@ -25,6 +26,9 @@ struct EditorSession; struct EditorPanelServices { using UtilityWindowRequestFn = void (*)(void*, EditorUtilityWindowKind); + using SceneAssetOpenRequestFn = bool (*)( + void*, + const std::filesystem::path&); EditorSession& session; EditorProjectRuntime& projectRuntime; @@ -35,12 +39,19 @@ struct EditorPanelServices { const UIEditorTextMeasurer* textMeasurer = nullptr; void* utilityWindowRequester = nullptr; UtilityWindowRequestFn requestUtilityWindow = nullptr; + void* sceneAssetOpenRequester = nullptr; + SceneAssetOpenRequestFn requestOpenSceneAsset = nullptr; void RequestOpenUtilityWindow(EditorUtilityWindowKind kind) const { if (requestUtilityWindow != nullptr) { requestUtilityWindow(utilityWindowRequester, kind); } } + + bool RequestOpenSceneAsset(const std::filesystem::path& scenePath) const { + return requestOpenSceneAsset != nullptr && + requestOpenSceneAsset(sceneAssetOpenRequester, scenePath); + } }; } // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Core/Scene/EditorSceneBackend.h b/editor/app/Core/Scene/EditorSceneBackend.h index 8655bbf5..9b935129 100644 --- a/editor/app/Core/Scene/EditorSceneBackend.h +++ b/editor/app/Core/Scene/EditorSceneBackend.h @@ -15,6 +15,10 @@ #include #include +namespace XCEngine::Components { +class Scene; +} + namespace XCEngine::UI::Editor::App { struct EditorStartupSceneResult { @@ -384,7 +388,10 @@ public: virtual EditorStartupSceneResult EnsureStartupScene( const std::filesystem::path& projectRoot) = 0; virtual EditorSceneHierarchySnapshot BuildHierarchySnapshot() const = 0; + virtual bool NewScene(std::string_view sceneName) = 0; virtual bool OpenSceneAsset(const std::filesystem::path& scenePath) = 0; + virtual bool SaveActiveScene(const std::filesystem::path& scenePath) = 0; + virtual ::XCEngine::Components::Scene* GetActiveScene() const = 0; virtual std::optional GetObjectSnapshot( std::string_view itemId) const = 0; virtual bool AddComponent( diff --git a/editor/app/Core/State/EditorSession.h b/editor/app/Core/State/EditorSession.h index e1ca2003..c4e579da 100644 --- a/editor/app/Core/State/EditorSession.h +++ b/editor/app/Core/State/EditorSession.h @@ -53,11 +53,14 @@ struct EditorConsoleEntry { struct EditorSession { std::filesystem::path workspaceRoot = {}; std::filesystem::path projectRoot = {}; + std::filesystem::path currentScenePath = {}; + std::string currentSceneName = {}; std::string activePanelId = {}; EditorRuntimeMode runtimeMode = EditorRuntimeMode::Edit; EditorActionRoute activeRoute = EditorActionRoute::None; EditorSelectionState selection = {}; std::vector consoleEntries = {}; + bool sceneDocumentDirty = false; }; std::string_view GetEditorRuntimeModeName(EditorRuntimeMode mode); diff --git a/editor/app/Core/Windowing/EditorFrameServices.h b/editor/app/Core/Windowing/EditorFrameServices.h index 80d590a4..1473e325 100644 --- a/editor/app/Core/Windowing/EditorFrameServices.h +++ b/editor/app/Core/Windowing/EditorFrameServices.h @@ -40,6 +40,7 @@ public: virtual void SyncSessionFromWorkspace( const UIEditorWorkspaceController& workspaceController) = 0; virtual void SyncSessionFromCommandFocusService() = 0; + virtual void TickEditorRuntime() = 0; virtual const UIEditorShellInteractionServices& GetShellServices() const = 0; virtual EditorPanelServices BuildPanelServices() = 0; diff --git a/editor/app/Core/Windowing/EditorWorkspaceShellRuntime.h b/editor/app/Core/Windowing/EditorWorkspaceShellRuntime.h index 68d7db43..7ef3c107 100644 --- a/editor/app/Core/Windowing/EditorWorkspaceShellRuntime.h +++ b/editor/app/Core/Windowing/EditorWorkspaceShellRuntime.h @@ -80,6 +80,7 @@ public: float detachedTitleBarTabHeight, float detachedWindowChromeHeight) = 0; virtual void RenderRequestedViewports( + EditorFrameServices& frameServices, const ::XCEngine::Rendering::RenderContext& renderContext) = 0; virtual void Append(::XCEngine::UI::UIDrawData& drawData) const = 0; diff --git a/editor/app/Features/EditorWorkspacePanelRegistry.cpp b/editor/app/Features/EditorWorkspacePanelRegistry.cpp index 710cc2e5..6894a5ad 100644 --- a/editor/app/Features/EditorWorkspacePanelRegistry.cpp +++ b/editor/app/Features/EditorWorkspacePanelRegistry.cpp @@ -343,6 +343,9 @@ public: m_panel.SetProjectRuntime(&context.services.projectRuntime); m_panel.SetCommandFocusService(&context.services.commandFocusService); m_panel.SetSystemInteractionHost(context.services.systemInteractionHost); + m_panel.SetSceneAssetOpenRequestHandler( + context.services.sceneAssetOpenRequester, + context.services.requestOpenSceneAsset); m_panel.Update( ResolveHostedPanelDispatchEntry( context.shellFrame.hostedPanelDispatchFrame, diff --git a/editor/app/Features/Project/ProjectPanel.cpp b/editor/app/Features/Project/ProjectPanel.cpp index f5f11004..8d445110 100644 --- a/editor/app/Features/Project/ProjectPanel.cpp +++ b/editor/app/Features/Project/ProjectPanel.cpp @@ -406,6 +406,13 @@ void ProjectPanel::SetSystemInteractionHost( m_systemInteractionHost = systemInteractionHost; } +void ProjectPanel::SetSceneAssetOpenRequestHandler( + void* requester, + SceneAssetOpenRequestFn requestOpenSceneAsset) { + m_sceneAssetOpenRequester = requester; + m_requestOpenSceneAsset = requestOpenSceneAsset; +} + void ProjectPanel::SetIconService(EditorIconService* icons) { m_icons = icons; RebuildWindowTreeItems(); @@ -853,6 +860,11 @@ bool ProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) return true; } +bool ProjectPanel::RequestOpenSceneAsset(const AssetEntry& asset) { + return m_requestOpenSceneAsset != nullptr && + m_requestOpenSceneAsset(m_sceneAssetOpenRequester, asset.absolutePath); +} + bool ProjectPanel::OpenProjectItem(std::string_view itemId, EventSource source) { const AssetEntry* asset = FindAssetEntry(itemId); if (asset == nullptr) { @@ -882,6 +894,11 @@ bool ProjectPanel::OpenProjectItem(std::string_view itemId, EventSource source) return false; } + if (asset->kind == ProjectBrowserModel::ItemKind::Scene && + !RequestOpenSceneAsset(*asset)) { + return false; + } + EmitEvent(EventKind::AssetOpened, source, asset); return true; } diff --git a/editor/app/Features/Project/ProjectPanel.h b/editor/app/Features/Project/ProjectPanel.h index 32d173b9..1a1300b6 100644 --- a/editor/app/Features/Project/ProjectPanel.h +++ b/editor/app/Features/Project/ProjectPanel.h @@ -81,9 +81,16 @@ public: bool directory = false; }; + using SceneAssetOpenRequestFn = bool (*)( + void*, + const std::filesystem::path&); + void SetProjectRuntime(EditorProjectRuntime* projectRuntime); void SetCommandFocusService(EditorCommandFocusService* commandFocusService); void SetSystemInteractionHost(System::SystemInteractionService* systemInteractionHost); + void SetSceneAssetOpenRequestHandler( + void* requester, + SceneAssetOpenRequestFn requestOpenSceneAsset); void SetIconService(EditorIconService* icons); void SetTextMeasurer(const ::XCEngine::UI::Editor::UIEditorTextMeasurer* textMeasurer); void ResetInteractionState(); @@ -191,6 +198,7 @@ private: DropTargetSurface* surface = nullptr) const; void SyncCurrentFolderSelection(); bool NavigateToFolder(std::string_view itemId, EventSource source = EventSource::None); + bool RequestOpenSceneAsset(const AssetEntry& asset); float MeasureBrowserContentHeight( const ::XCEngine::UI::UIRect& browserContentRect) const; void RebuildBrowserScrollLayout(); @@ -249,6 +257,8 @@ private: EditorProjectRuntime* m_projectRuntime = nullptr; EditorCommandFocusService* m_commandFocusService = nullptr; System::SystemInteractionService* m_systemInteractionHost = nullptr; + void* m_sceneAssetOpenRequester = nullptr; + SceneAssetOpenRequestFn m_requestOpenSceneAsset = nullptr; EditorIconService* m_icons = nullptr; const ::XCEngine::UI::Editor::UIEditorTextMeasurer* m_textMeasurer = nullptr; std::vector m_windowTreeItems = {}; diff --git a/editor/app/Services/Project/EditorProjectRuntime.cpp b/editor/app/Services/Project/EditorProjectRuntime.cpp index a37336b4..c1070064 100644 --- a/editor/app/Services/Project/EditorProjectRuntime.cpp +++ b/editor/app/Services/Project/EditorProjectRuntime.cpp @@ -81,7 +81,6 @@ void EditorProjectRuntime::Reset() { m_browserModel.Reset(); m_ownedSelectionService.ClearSelection(); m_selectionService = &m_ownedSelectionService; - m_pendingSceneOpenPath.reset(); } bool EditorProjectRuntime::Initialize(const std::filesystem::path& projectRoot) { @@ -166,23 +165,12 @@ bool EditorProjectRuntime::OpenItem(std::string_view itemId) { if (asset->kind == ProjectBrowserModel::ItemKind::Scene && !asset->absolutePath.empty()) { - m_pendingSceneOpenPath = asset->absolutePath; return true; } return false; } -std::optional EditorProjectRuntime::ConsumePendingSceneOpenPath() { - if (!m_pendingSceneOpenPath.has_value()) { - return std::nullopt; - } - - std::optional result = m_pendingSceneOpenPath; - m_pendingSceneOpenPath.reset(); - return result; -} - const ProjectBrowserModel::FolderEntry* EditorProjectRuntime::FindFolderEntry( std::string_view itemId) const { return m_browserModel.FindFolderEntry(itemId); diff --git a/editor/app/Services/Project/EditorProjectRuntime.h b/editor/app/Services/Project/EditorProjectRuntime.h index ed2cc192..be3e2a6e 100644 --- a/editor/app/Services/Project/EditorProjectRuntime.h +++ b/editor/app/Services/Project/EditorProjectRuntime.h @@ -60,7 +60,6 @@ public: bool NavigateToFolder(std::string_view itemId); bool OpenItem(std::string_view itemId); - std::optional ConsumePendingSceneOpenPath(); const ProjectBrowserModel::FolderEntry* FindFolderEntry( std::string_view itemId) const; @@ -115,7 +114,6 @@ private: ProjectBrowserModel m_browserModel = {}; EditorSelectionService m_ownedSelectionService = {}; EditorSelectionService* m_selectionService = &m_ownedSelectionService; - std::optional m_pendingSceneOpenPath = std::nullopt; }; } // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Services/Runtime/EditorRuntimeCoordinator.cpp b/editor/app/Services/Runtime/EditorRuntimeCoordinator.cpp new file mode 100644 index 00000000..91d1adbe --- /dev/null +++ b/editor/app/Services/Runtime/EditorRuntimeCoordinator.cpp @@ -0,0 +1,470 @@ +#include "Runtime/EditorRuntimeCoordinator.h" + +#include "Project/EditorProjectRuntime.h" +#include "Scene/EditorSceneRuntime.h" +#include "State/EditorSession.h" + +#include + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +UIEditorHostCommandEvaluationResult BuildDisabledResult( + std::string_view message) { + UIEditorHostCommandEvaluationResult result = {}; + result.executable = false; + result.message = std::string(message); + return result; +} + +UIEditorHostCommandEvaluationResult BuildExecutableResult( + std::string_view message) { + UIEditorHostCommandEvaluationResult result = {}; + result.executable = true; + result.message = std::string(message); + return result; +} + +UIEditorHostCommandDispatchResult BuildRejectedDispatch( + std::string_view message) { + UIEditorHostCommandDispatchResult result = {}; + result.commandExecuted = false; + result.message = std::string(message); + return result; +} + +UIEditorHostCommandDispatchResult BuildExecutedDispatch( + std::string_view message) { + UIEditorHostCommandDispatchResult result = {}; + result.commandExecuted = true; + result.message = std::string(message); + return result; +} + +std::string ResolveSceneDisplayName(const std::filesystem::path& scenePath) { + return scenePath.empty() + ? std::string("Untitled") + : scenePath.stem().string(); +} + +} // namespace + +void EditorRuntimeCoordinator::Initialize( + EditorSession& session, + EditorSceneRuntime& sceneRuntime, + EditorProjectRuntime& projectRuntime, + const EditorRuntimePaths& runtimePaths) { + Shutdown(); + m_session = &session; + m_sceneRuntime = &sceneRuntime; + m_projectRuntime = &projectRuntime; + m_runtimePaths = runtimePaths; + + const EditorStartupSceneResult& startupScene = + m_sceneRuntime->GetStartupResult(); + m_session->currentScenePath = startupScene.scenePath; + m_session->currentSceneName = startupScene.sceneName.empty() + ? ResolveSceneDisplayName(startupScene.scenePath) + : startupScene.sceneName; + m_session->sceneDocumentDirty = false; + m_session->runtimeMode = EditorRuntimeMode::Edit; + CaptureCleanSceneRevision(); +} + +void EditorRuntimeCoordinator::Shutdown() { + m_runtimeLoop.Stop(); + if (m_session != nullptr) { + m_session->runtimeMode = EditorRuntimeMode::Edit; + } + m_session = nullptr; + m_sceneRuntime = nullptr; + m_projectRuntime = nullptr; + m_runtimePaths = EditorRuntimePaths{}; + m_lastFrameTickTime = {}; + m_lastCleanSceneContentRevision = 0u; + m_lastObservedSceneContentRevision = 0u; + m_lastMessage.clear(); +} + +void EditorRuntimeCoordinator::TickFrame() { + const auto now = std::chrono::steady_clock::now(); + if (m_lastFrameTickTime == std::chrono::steady_clock::time_point{}) { + m_lastFrameTickTime = now; + SyncSceneDocumentDirtyFromRevision(); + return; + } + + const float deltaSeconds = std::chrono::duration( + now - m_lastFrameTickTime).count(); + m_lastFrameTickTime = now; + Tick(deltaSeconds); +} + +void EditorRuntimeCoordinator::Tick(float deltaSeconds) { + if (!IsReady()) { + return; + } + + SyncSceneDocumentDirtyFromRevision(); + if (!m_runtimeLoop.IsRunning()) { + if (m_session->runtimeMode != EditorRuntimeMode::Edit) { + m_session->runtimeMode = EditorRuntimeMode::Edit; + } + return; + } + + m_runtimeLoop.Tick(deltaSeconds); + if (m_runtimeLoop.IsPaused()) { + m_session->runtimeMode = EditorRuntimeMode::Paused; + } else { + m_session->runtimeMode = EditorRuntimeMode::Play; + } +} + +bool EditorRuntimeCoordinator::RequestOpenSceneAsset( + const std::filesystem::path& scenePath) { + if (!IsReady()) { + SetLastMessage("Runtime coordinator is unavailable."); + return false; + } + + if (scenePath.empty()) { + SetLastMessage("Scene asset path is empty."); + return false; + } + + StopPlayMode(); + if (!m_sceneRuntime->OpenSceneAsset(scenePath)) { + SetLastMessage("Failed to open scene asset: " + scenePath.string()); + return false; + } + + m_session->currentScenePath = scenePath; + m_session->currentSceneName = ResolveSceneDisplayName(scenePath); + m_session->sceneDocumentDirty = false; + CaptureCleanSceneRevision(); + SetLastMessage("Opened scene: " + scenePath.string()); + return true; +} + +const std::string& EditorRuntimeCoordinator::GetLastMessage() const { + return m_lastMessage; +} + +UIEditorHostCommandEvaluationResult +EditorRuntimeCoordinator::EvaluateFileCommand( + std::string_view commandId) const { + if (!IsReady()) { + return BuildDisabledResult("Runtime coordinator is unavailable."); + } + + if (commandId == "file.new_scene") { + return BuildExecutableResult("Create a new unsaved scene document."); + } + + if (commandId == "file.open_scene") { + return BuildDisabledResult( + "Open Scene requires a project scene asset selection in the current shell."); + } + + if (commandId == "file.save_scene") { + if (IsPlayModeActive()) { + return BuildDisabledResult("Stop play mode before saving the scene."); + } + if (!HasActiveScene()) { + return BuildDisabledResult("No active scene to save."); + } + if (m_session->currentScenePath.empty()) { + return BuildDisabledResult( + "Save Scene As is required before saving this scene."); + } + return BuildExecutableResult("Save the active scene document."); + } + + if (commandId == "file.save_scene_as") { + return BuildDisabledResult( + "Save Scene As requires a file dialog host; the current shell does not provide one."); + } + + return BuildDisabledResult( + "File command is not owned by the runtime document coordinator."); +} + +UIEditorHostCommandDispatchResult +EditorRuntimeCoordinator::DispatchFileCommand( + std::string_view commandId) { + const UIEditorHostCommandEvaluationResult evaluation = + EvaluateFileCommand(commandId); + if (!evaluation.executable) { + SetLastMessage(evaluation.message); + return BuildRejectedDispatch(evaluation.message); + } + + if (commandId == "file.new_scene") { + StopPlayMode(); + if (!m_sceneRuntime->NewScene("Untitled")) { + SetLastMessage("Failed to create a new scene."); + return BuildRejectedDispatch(m_lastMessage); + } + + const EditorStartupSceneResult& startupScene = + m_sceneRuntime->GetStartupResult(); + m_session->currentScenePath = std::filesystem::path(); + m_session->currentSceneName = startupScene.sceneName.empty() + ? std::string("Untitled") + : startupScene.sceneName; + m_session->sceneDocumentDirty = true; + CaptureCleanSceneRevision(); + SetLastMessage("New scene created."); + return BuildExecutedDispatch(m_lastMessage); + } + + if (commandId == "file.save_scene") { + const std::filesystem::path scenePath = m_session->currentScenePath; + if (!m_sceneRuntime->SaveScene(scenePath)) { + SetLastMessage("Failed to save scene: " + scenePath.string()); + return BuildRejectedDispatch(m_lastMessage); + } + + m_session->sceneDocumentDirty = false; + m_session->currentSceneName = ResolveSceneDisplayName(scenePath); + CaptureCleanSceneRevision(); + SetLastMessage("Scene saved: " + scenePath.string()); + return BuildExecutedDispatch(m_lastMessage); + } + + SetLastMessage(evaluation.message); + return BuildRejectedDispatch(evaluation.message); +} + +UIEditorHostCommandEvaluationResult +EditorRuntimeCoordinator::EvaluateRunCommand( + std::string_view commandId) const { + if (!IsReady()) { + return BuildDisabledResult("Runtime coordinator is unavailable."); + } + + if (commandId == "run.play") { + if (!HasActiveScene()) { + return BuildDisabledResult("No active scene is available for play mode."); + } + return IsPlayModeActive() + ? BuildExecutableResult("Stop play mode.") + : BuildExecutableResult("Start play mode."); + } + + if (commandId == "run.pause") { + if (!IsPlayModeActive()) { + return BuildDisabledResult("Play mode is not running."); + } + return m_session->runtimeMode == EditorRuntimeMode::Paused + ? BuildExecutableResult("Resume play mode.") + : BuildExecutableResult("Pause play mode."); + } + + if (commandId == "run.step") { + if (!HasActiveScene()) { + return BuildDisabledResult("No active scene is available for play mode."); + } + return BuildExecutableResult("Step play mode by one frame."); + } + + if (commandId == "run.stop") { + return IsPlayModeActive() + ? BuildExecutableResult("Stop play mode.") + : BuildDisabledResult("Play mode is not running."); + } + + return BuildDisabledResult( + "Run command is not owned by the runtime coordinator."); +} + +UIEditorHostCommandDispatchResult +EditorRuntimeCoordinator::DispatchRunCommand( + std::string_view commandId) { + const UIEditorHostCommandEvaluationResult evaluation = + EvaluateRunCommand(commandId); + if (!evaluation.executable) { + SetLastMessage(evaluation.message); + return BuildRejectedDispatch(evaluation.message); + } + + if (commandId == "run.play") { + const bool executed = IsPlayModeActive() ? StopPlayMode() : StartPlayMode(); + SetLastMessage(executed + ? (m_session->runtimeMode == EditorRuntimeMode::Edit + ? std::string("Play mode stopped.") + : std::string("Play mode started.")) + : std::string("Failed to toggle play mode.")); + return executed + ? BuildExecutedDispatch(m_lastMessage) + : BuildRejectedDispatch(m_lastMessage); + } + + if (commandId == "run.pause") { + const bool executed = m_session->runtimeMode == EditorRuntimeMode::Paused + ? ResumePlayMode() + : PausePlayMode(); + SetLastMessage(executed + ? (m_session->runtimeMode == EditorRuntimeMode::Paused + ? std::string("Play mode paused.") + : std::string("Play mode resumed.")) + : std::string("Failed to toggle play mode pause.")); + return executed + ? BuildExecutedDispatch(m_lastMessage) + : BuildRejectedDispatch(m_lastMessage); + } + + if (commandId == "run.step") { + if (!StepPlayMode()) { + SetLastMessage("Failed to step play mode."); + return BuildRejectedDispatch(m_lastMessage); + } + + SetLastMessage("Play mode stepped one frame."); + return BuildExecutedDispatch(m_lastMessage); + } + + if (commandId == "run.stop") { + if (!StopPlayMode()) { + SetLastMessage("Failed to stop play mode."); + return BuildRejectedDispatch(m_lastMessage); + } + + SetLastMessage("Play mode stopped."); + return BuildExecutedDispatch(m_lastMessage); + } + + SetLastMessage(evaluation.message); + return BuildRejectedDispatch(evaluation.message); +} + +UIEditorHostCommandEvaluationResult +EditorRuntimeCoordinator::EvaluateScriptCommand( + std::string_view commandId) const { + if (!IsReady()) { + return BuildDisabledResult("Runtime coordinator is unavailable."); + } + + if (commandId == "scripts.rebuild") { + return BuildDisabledResult( + "Script rebuild is owned by the runtime coordinator, but no in-process script assembly builder is bound."); + } + + return BuildDisabledResult( + "Script command is not owned by the runtime coordinator."); +} + +UIEditorHostCommandDispatchResult +EditorRuntimeCoordinator::DispatchScriptCommand( + std::string_view commandId) { + const UIEditorHostCommandEvaluationResult evaluation = + EvaluateScriptCommand(commandId); + SetLastMessage(evaluation.message); + return BuildRejectedDispatch(evaluation.message); +} + +bool EditorRuntimeCoordinator::IsReady() const { + return m_session != nullptr && m_sceneRuntime != nullptr; +} + +bool EditorRuntimeCoordinator::HasActiveScene() const { + return IsReady() && m_sceneRuntime->GetActiveScene() != nullptr; +} + +bool EditorRuntimeCoordinator::IsPlayModeActive() const { + return IsReady() && m_session->runtimeMode != EditorRuntimeMode::Edit; +} + +bool EditorRuntimeCoordinator::StartPlayMode() { + if (!HasActiveScene()) { + return false; + } + + m_runtimeLoop.Start(m_sceneRuntime->GetActiveScene()); + m_session->runtimeMode = EditorRuntimeMode::Play; + m_lastFrameTickTime = std::chrono::steady_clock::now(); + return m_runtimeLoop.IsRunning(); +} + +bool EditorRuntimeCoordinator::StopPlayMode() { + if (!IsReady()) { + return false; + } + + m_runtimeLoop.Stop(); + m_session->runtimeMode = EditorRuntimeMode::Edit; + return true; +} + +bool EditorRuntimeCoordinator::PausePlayMode() { + if (!m_runtimeLoop.IsRunning()) { + return false; + } + + m_runtimeLoop.Pause(); + m_session->runtimeMode = EditorRuntimeMode::Paused; + return true; +} + +bool EditorRuntimeCoordinator::ResumePlayMode() { + if (!m_runtimeLoop.IsRunning()) { + return false; + } + + m_runtimeLoop.Resume(); + m_session->runtimeMode = EditorRuntimeMode::Play; + m_lastFrameTickTime = std::chrono::steady_clock::now(); + return true; +} + +bool EditorRuntimeCoordinator::StepPlayMode() { + if (!HasActiveScene()) { + return false; + } + + if (!m_runtimeLoop.IsRunning()) { + m_runtimeLoop.Start(m_sceneRuntime->GetActiveScene()); + } + + m_runtimeLoop.StepFrame(); + m_runtimeLoop.Tick(m_runtimeLoop.GetSettings().fixedDeltaTime); + m_session->runtimeMode = EditorRuntimeMode::Paused; + m_lastFrameTickTime = std::chrono::steady_clock::now(); + return true; +} + +void EditorRuntimeCoordinator::SyncSceneDocumentDirtyFromRevision() { + if (!IsReady()) { + return; + } + + const std::uint64_t revision = m_sceneRuntime->GetSceneContentRevision(); + if (revision != m_lastObservedSceneContentRevision) { + m_lastObservedSceneContentRevision = revision; + } + if (revision != m_lastCleanSceneContentRevision) { + m_session->sceneDocumentDirty = true; + } +} + +void EditorRuntimeCoordinator::CaptureCleanSceneRevision() { + if (!IsReady()) { + m_lastCleanSceneContentRevision = 0u; + m_lastObservedSceneContentRevision = 0u; + return; + } + + const std::uint64_t revision = m_sceneRuntime->GetSceneContentRevision(); + m_lastCleanSceneContentRevision = revision; + m_lastObservedSceneContentRevision = revision; +} + +void EditorRuntimeCoordinator::SetLastMessage(std::string message) { + m_lastMessage = std::move(message); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Services/Runtime/EditorRuntimeCoordinator.h b/editor/app/Services/Runtime/EditorRuntimeCoordinator.h new file mode 100644 index 00000000..5f8d3597 --- /dev/null +++ b/editor/app/Services/Runtime/EditorRuntimeCoordinator.h @@ -0,0 +1,78 @@ +#pragma once + +#include "Commands/EditorHostCommandBridge.h" +#include "Environment/EditorRuntimePaths.h" + +#include + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +class EditorProjectRuntime; +class EditorSceneRuntime; +struct EditorSession; + +class EditorRuntimeCoordinator final + : public EditorHostCommandBridge::RuntimeCommandOwner { +public: + EditorRuntimeCoordinator() = default; + EditorRuntimeCoordinator(const EditorRuntimeCoordinator&) = delete; + EditorRuntimeCoordinator& operator=(const EditorRuntimeCoordinator&) = delete; + + void Initialize( + EditorSession& session, + EditorSceneRuntime& sceneRuntime, + EditorProjectRuntime& projectRuntime, + const EditorRuntimePaths& runtimePaths); + void Shutdown(); + + void TickFrame(); + void Tick(float deltaSeconds); + + bool RequestOpenSceneAsset(const std::filesystem::path& scenePath); + const std::string& GetLastMessage() const; + + UIEditorHostCommandEvaluationResult EvaluateFileCommand( + std::string_view commandId) const override; + UIEditorHostCommandDispatchResult DispatchFileCommand( + std::string_view commandId) override; + UIEditorHostCommandEvaluationResult EvaluateRunCommand( + std::string_view commandId) const override; + UIEditorHostCommandDispatchResult DispatchRunCommand( + std::string_view commandId) override; + UIEditorHostCommandEvaluationResult EvaluateScriptCommand( + std::string_view commandId) const override; + UIEditorHostCommandDispatchResult DispatchScriptCommand( + std::string_view commandId) override; + +private: + bool IsReady() const; + bool HasActiveScene() const; + bool IsPlayModeActive() const; + bool StartPlayMode(); + bool StopPlayMode(); + bool PausePlayMode(); + bool ResumePlayMode(); + bool StepPlayMode(); + void SyncSceneDocumentDirtyFromRevision(); + void CaptureCleanSceneRevision(); + void SetLastMessage(std::string message); + + EditorSession* m_session = nullptr; + EditorSceneRuntime* m_sceneRuntime = nullptr; + EditorProjectRuntime* m_projectRuntime = nullptr; + EditorRuntimePaths m_runtimePaths = {}; + ::XCEngine::Components::RuntimeLoop m_runtimeLoop{ + ::XCEngine::Components::RuntimeLoop::Settings{} }; + std::chrono::steady_clock::time_point m_lastFrameTickTime = {}; + std::uint64_t m_lastCleanSceneContentRevision = 0u; + std::uint64_t m_lastObservedSceneContentRevision = 0u; + std::string m_lastMessage = {}; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Services/Scene/EditorSceneRuntime.cpp b/editor/app/Services/Scene/EditorSceneRuntime.cpp index 12d7fc2b..46101394 100644 --- a/editor/app/Services/Scene/EditorSceneRuntime.cpp +++ b/editor/app/Services/Scene/EditorSceneRuntime.cpp @@ -1,5 +1,7 @@ #include "Scene/EditorSceneRuntime.h" +#include + #include namespace XCEngine::UI::Editor::App { @@ -294,6 +296,10 @@ std::uint64_t EditorSceneRuntime::GetInspectorRevision() const { return m_inspectorRevision; } +std::uint64_t EditorSceneRuntime::GetSceneContentRevision() const { + return m_sceneContentRevision; +} + bool EditorSceneRuntime::SetSelection(std::string_view itemId) { const std::optional gameObjectId = ParseEditorGameObjectItemId(itemId); @@ -335,6 +341,33 @@ void EditorSceneRuntime::ClearSelection() { SelectionService().ClearSelection(); } +bool EditorSceneRuntime::NewScene(std::string_view sceneName) { + if (m_backend == nullptr || !m_backend->NewScene(sceneName)) { + return false; + } + + m_startupSceneResult.ready = true; + m_startupSceneResult.loadedFromDisk = false; + m_startupSceneResult.scenePath = std::filesystem::path(); + if (::XCEngine::Components::Scene* activeScene = m_backend->GetActiveScene(); + activeScene != nullptr) { + m_startupSceneResult.sceneName = activeScene->GetName(); + } else { + m_startupSceneResult.sceneName = sceneName.empty() + ? std::string("Untitled") + : std::string(sceneName); + } + + ResetTransformEditHistory(); + ResetToolInteractionState(); + SelectionService().ClearSelection(); + IncrementInspectorRevision(); + IncrementSceneContentRevision(); + RefreshScene(); + EnsureSceneSelection(); + return true; +} + bool EditorSceneRuntime::OpenSceneAsset(const std::filesystem::path& scenePath) { if (m_backend == nullptr || !m_backend->OpenSceneAsset(scenePath)) { return false; @@ -355,6 +388,14 @@ bool EditorSceneRuntime::OpenSceneAsset(const std::filesystem::path& scenePath) return true; } +bool EditorSceneRuntime::SaveScene(const std::filesystem::path& scenePath) { + return m_backend != nullptr && m_backend->SaveActiveScene(scenePath); +} + +::XCEngine::Components::Scene* EditorSceneRuntime::GetActiveScene() const { + return m_backend != nullptr ? m_backend->GetActiveScene() : nullptr; +} + bool EditorSceneRuntime::RenameGameObject( std::string_view itemId, std::string_view newName) { diff --git a/editor/app/Services/Scene/EditorSceneRuntime.h b/editor/app/Services/Scene/EditorSceneRuntime.h index 8ab78280..28c10fd2 100644 --- a/editor/app/Services/Scene/EditorSceneRuntime.h +++ b/editor/app/Services/Scene/EditorSceneRuntime.h @@ -22,6 +22,12 @@ struct Vector3; } // namespace XCEngine::Math +namespace XCEngine::Components { + +class Scene; + +} // namespace XCEngine::Components + namespace XCEngine::UI::Editor::App { class EditorSceneRuntime { @@ -58,11 +64,15 @@ public: std::vector GetSelectedComponents() const; std::uint64_t GetSelectionStamp() const; std::uint64_t GetInspectorRevision() const; + std::uint64_t GetSceneContentRevision() const; bool SetSelection(std::string_view itemId); bool SetSelection(EditorSceneObjectId id); void ClearSelection(); + bool NewScene(std::string_view sceneName); bool OpenSceneAsset(const std::filesystem::path& scenePath); + bool SaveScene(const std::filesystem::path& scenePath); + ::XCEngine::Components::Scene* GetActiveScene() const; bool RenameGameObject( std::string_view itemId, diff --git a/editor/app/Services/Scene/EngineEditorSceneBackend.cpp b/editor/app/Services/Scene/EngineEditorSceneBackend.cpp index f8f2aaa0..31d9098a 100644 --- a/editor/app/Services/Scene/EngineEditorSceneBackend.cpp +++ b/editor/app/Services/Scene/EngineEditorSceneBackend.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -74,6 +75,27 @@ Scene* ResolvePrimaryScene(SceneManager& sceneManager) { return nullptr; } +std::string ResolveUniqueSceneName( + const SceneManager& sceneManager, + std::string_view requestedName) { + const std::string baseName = + requestedName.empty() ? std::string("Untitled") : std::string(requestedName); + if (sceneManager.GetScene(baseName) == nullptr) { + return baseName; + } + + for (std::uint32_t suffix = 2u; suffix < 10000u; ++suffix) { + const std::string candidate = + baseName + " " + std::to_string(suffix); + if (sceneManager.GetScene(candidate) == nullptr) { + return candidate; + } + } + + return baseName + " " + std::to_string( + static_cast(sceneManager.GetAllScenes().size() + 1u)); +} + std::pair SerializeComponent( const Component* component) { std::ostringstream payload = {}; @@ -1759,6 +1781,49 @@ public: return loadedScene != nullptr; } + bool NewScene(std::string_view sceneName) override { + const std::string resolvedSceneName = + ResolveUniqueSceneName(m_sceneManager, sceneName); + Scene* scene = m_sceneManager.CreateScene(resolvedSceneName); + if (scene == nullptr) { + return false; + } + + m_sceneManager.SetActiveScene(scene); + return true; + } + + bool SaveActiveScene(const std::filesystem::path& scenePath) override { + if (scenePath.empty()) { + return false; + } + + Scene* scene = ResolvePrimaryScene(m_sceneManager); + if (scene == nullptr) { + return false; + } + + const std::filesystem::path parentPath = scenePath.parent_path(); + if (!parentPath.empty()) { + std::error_code errorCode = {}; + std::filesystem::create_directories(parentPath, errorCode); + if (errorCode) { + return false; + } + } + + scene->Save(scenePath.string()); + std::error_code errorCode = {}; + return std::filesystem::exists(scenePath, errorCode) && + !errorCode && + std::filesystem::is_regular_file(scenePath, errorCode) && + !errorCode; + } + + Scene* GetActiveScene() const override { + return ResolvePrimaryScene(m_sceneManager); + } + std::optional GetObjectSnapshot( std::string_view itemId) const override { const GameObject* gameObject = FindGameObject(itemId); diff --git a/editor/app/Windowing/Content/EditorWindowContentController.h b/editor/app/Windowing/Content/EditorWindowContentController.h index 68798a4d..67eda14b 100644 --- a/editor/app/Windowing/Content/EditorWindowContentController.h +++ b/editor/app/Windowing/Content/EditorWindowContentController.h @@ -130,7 +130,9 @@ public: virtual EditorWindowFrameTransferRequests UpdateAndAppend( const EditorWindowContentFrameContext& context, ::XCEngine::UI::UIDrawData& drawData) = 0; - virtual void RenderRequestedViewports(const ::XCEngine::Rendering::RenderContext&) {} + virtual void RenderRequestedViewports( + EditorFrameServices&, + const ::XCEngine::Rendering::RenderContext&) {} virtual const UIEditorShellInteractionFrame& GetShellFrame() const = 0; virtual const UIEditorShellInteractionState& GetShellInteractionState() const = 0; diff --git a/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp b/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp index dd3521e8..13fb6d6b 100644 --- a/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp +++ b/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp @@ -174,9 +174,10 @@ EditorWindowFrameTransferRequests EditorWorkspaceWindowContentController::Update } void EditorWorkspaceWindowContentController::RenderRequestedViewports( + EditorFrameServices& frameServices, const ::XCEngine::Rendering::RenderContext& renderContext) { if (m_shellRuntime != nullptr) { - m_shellRuntime->RenderRequestedViewports(renderContext); + m_shellRuntime->RenderRequestedViewports(frameServices, renderContext); } } diff --git a/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.h b/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.h index 99123045..41470f33 100644 --- a/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.h +++ b/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.h @@ -47,6 +47,7 @@ public: const EditorWindowContentFrameContext& context, ::XCEngine::UI::UIDrawData& drawData) override; void RenderRequestedViewports( + EditorFrameServices& frameServices, const ::XCEngine::Rendering::RenderContext& renderContext) override; const UIEditorShellInteractionFrame& GetShellFrame() const override; diff --git a/editor/app/Windowing/Runtime/EditorWindowRuntimeController.cpp b/editor/app/Windowing/Runtime/EditorWindowRuntimeController.cpp index d8d565cb..8fd19e86 100644 --- a/editor/app/Windowing/Runtime/EditorWindowRuntimeController.cpp +++ b/editor/app/Windowing/Runtime/EditorWindowRuntimeController.cpp @@ -360,7 +360,9 @@ EditorWindowFrameTransferRequests EditorWindowRuntimeController::UpdateAndAppend void EditorWindowRuntimeController::RenderRequestedViewports( const ::XCEngine::Rendering::RenderContext& renderContext) { if (m_contentController != nullptr) { - m_contentController->RenderRequestedViewports(renderContext); + m_contentController->RenderRequestedViewports( + m_frameServices, + renderContext); } } diff --git a/engine/include/XCEngine/Rendering/AGENTS.md b/engine/include/XCEngine/Rendering/AGENTS.md index 667d5d65..1377525b 100644 --- a/engine/include/XCEngine/Rendering/AGENTS.md +++ b/engine/include/XCEngine/Rendering/AGENTS.md @@ -1,6 +1,6 @@ # XCEngine Rendering Agent Guide -Editor viewport 边界补充:SceneViewport 的 render request 是 editor 每帧快照,不属于 SRP fallback 兜底范围。workspace/panel frame events 若会打开或切换场景,必须在事件消费完成后、`RenderRequestedViewports()` 前重新同步 request;不要通过放宽 `ScriptableRenderPipelineHost` authoritative stage recorder 或恢复 backend fallback 来掩盖过期请求。 +Editor viewport 边界补充:SceneViewport 的 render request 是 editor 每帧快照,不属于 SRP fallback 兜底范围。workspace/panel frame events 若会打开或切换场景,必须在事件消费和 editor runtime tick 完成后,在 `RenderRequestedViewports(...)` 入口通过 `EditorFrameServices::SyncSceneViewportRenderRequest(...)` 重新同步 request;不要依赖 panel update 早期快照,也不要通过放宽 `ScriptableRenderPipelineHost` authoritative stage recorder 或恢复 backend fallback 来掩盖过期请求。 本文面向以后在 `engine/include/XCEngine/Rendering/**`、`engine/src/Rendering/**` 以及对应 managed SRP/URP 绑定上工作的 agent / 开发者。这里的目标不是写愿景,而是固定当前 checkout 的真实结构、Unity SRP/URP 对齐方向、职责边界和验证入口。 @@ -15,6 +15,7 @@ Editor viewport 边界补充:SceneViewport 的 render request 是 editor 每 - `BuiltinForwardPipeline` 现在只应被理解为 native scene draw backend / 默认 forward backend,不再是顶层 `RenderPipeline`,不应继续承载越来越多 URP 上层策略。 - `RenderGraph` 是 native 核心。managed 侧通过 `ScriptableRenderContext` / `RenderGraph` wrapper 参与录图和提交命令,不直接拥有 RHI resource / barrier / view lifetime。 - editor object-id、grid、outline、overlay 这类工具输出要接入 request / pass / viewport render plan,不要把 engine rendering 逻辑写回 editor panel 或 ImGui overlay。 +- editor SceneViewport request 的唯一最终刷新点是 workspace window 的 `RenderRequestedViewports(...)` 调用链。Panel 可以提前写快照供 UI 状态使用,但渲染前必须在同一帧重新取 `EditorSceneRuntime::BuildSceneViewportRenderRequest()`,保证 scene open/new、play-mode tick、selection/camera 改动不会用旧 camera/request 进入 SRP。 ## 1. Unity 对齐基准 diff --git a/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp b/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp index f7cc5c86..b3867564 100644 --- a/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp +++ b/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp @@ -52,6 +52,59 @@ public: UIEditorHostCommandDispatchResult dispatchResult = {}; }; +class StubRuntimeCommandOwner final + : public EditorHostCommandBridge::RuntimeCommandOwner { +public: + UIEditorHostCommandEvaluationResult EvaluateFileCommand( + std::string_view commandId) const override { + lastEvaluatedFileCommandId = std::string(commandId); + return fileEvaluationResult; + } + + UIEditorHostCommandDispatchResult DispatchFileCommand( + std::string_view commandId) override { + lastDispatchedFileCommandId = std::string(commandId); + return fileDispatchResult; + } + + UIEditorHostCommandEvaluationResult EvaluateRunCommand( + std::string_view commandId) const override { + lastEvaluatedRunCommandId = std::string(commandId); + return runEvaluationResult; + } + + UIEditorHostCommandDispatchResult DispatchRunCommand( + std::string_view commandId) override { + lastDispatchedRunCommandId = std::string(commandId); + return runDispatchResult; + } + + UIEditorHostCommandEvaluationResult EvaluateScriptCommand( + std::string_view commandId) const override { + lastEvaluatedScriptCommandId = std::string(commandId); + return scriptEvaluationResult; + } + + UIEditorHostCommandDispatchResult DispatchScriptCommand( + std::string_view commandId) override { + lastDispatchedScriptCommandId = std::string(commandId); + return scriptDispatchResult; + } + + mutable std::string lastEvaluatedFileCommandId = {}; + std::string lastDispatchedFileCommandId = {}; + mutable std::string lastEvaluatedRunCommandId = {}; + std::string lastDispatchedRunCommandId = {}; + mutable std::string lastEvaluatedScriptCommandId = {}; + std::string lastDispatchedScriptCommandId = {}; + UIEditorHostCommandEvaluationResult fileEvaluationResult = {}; + UIEditorHostCommandDispatchResult fileDispatchResult = {}; + UIEditorHostCommandEvaluationResult runEvaluationResult = {}; + UIEditorHostCommandDispatchResult runDispatchResult = {}; + UIEditorHostCommandEvaluationResult scriptEvaluationResult = {}; + UIEditorHostCommandDispatchResult scriptDispatchResult = {}; +}; + TEST(EditorHostCommandBridgeTest, HierarchyEditCommandsDelegateToBoundRoute) { EditorSession session = {}; EditorCommandFocusService commandFocus = {}; @@ -99,7 +152,53 @@ TEST(EditorHostCommandBridgeTest, UnsupportedHostCommandsUseHonestMessages) { EXPECT_FALSE(fileEvaluation.executable); EXPECT_EQ( fileEvaluation.message, - "Only file.exit has a bound host owner in the current shell."); + "File commands have no bound document owner in the current shell."); +} + +TEST(EditorHostCommandBridgeTest, RuntimeCommandsDelegateToBoundRuntimeOwner) { + StubRuntimeCommandOwner runtimeOwner = {}; + runtimeOwner.fileEvaluationResult.executable = true; + runtimeOwner.fileEvaluationResult.message = "Runtime owner can save."; + runtimeOwner.fileDispatchResult.commandExecuted = true; + runtimeOwner.fileDispatchResult.message = "Runtime owner saved."; + runtimeOwner.runEvaluationResult.executable = true; + runtimeOwner.runEvaluationResult.message = "Runtime owner can play."; + runtimeOwner.runDispatchResult.commandExecuted = true; + runtimeOwner.runDispatchResult.message = "Runtime owner played."; + runtimeOwner.scriptEvaluationResult.message = "Runtime owns scripts honestly."; + + EditorHostCommandBridge bridge = {}; + bridge.BindRuntimeCommandOwner(&runtimeOwner); + + const UIEditorHostCommandEvaluationResult fileEvaluation = + bridge.EvaluateHostCommand("file.save_scene"); + EXPECT_TRUE(fileEvaluation.executable); + EXPECT_EQ(fileEvaluation.message, "Runtime owner can save."); + EXPECT_EQ(runtimeOwner.lastEvaluatedFileCommandId, "file.save_scene"); + + const UIEditorHostCommandDispatchResult fileDispatch = + bridge.DispatchHostCommand("file.save_scene"); + EXPECT_TRUE(fileDispatch.commandExecuted); + EXPECT_EQ(fileDispatch.message, "Runtime owner saved."); + EXPECT_EQ(runtimeOwner.lastDispatchedFileCommandId, "file.save_scene"); + + const UIEditorHostCommandEvaluationResult runEvaluation = + bridge.EvaluateHostCommand("run.play"); + EXPECT_TRUE(runEvaluation.executable); + EXPECT_EQ(runEvaluation.message, "Runtime owner can play."); + EXPECT_EQ(runtimeOwner.lastEvaluatedRunCommandId, "run.play"); + + const UIEditorHostCommandDispatchResult runDispatch = + bridge.DispatchHostCommand("run.play"); + EXPECT_TRUE(runDispatch.commandExecuted); + EXPECT_EQ(runDispatch.message, "Runtime owner played."); + EXPECT_EQ(runtimeOwner.lastDispatchedRunCommandId, "run.play"); + + const UIEditorHostCommandEvaluationResult scriptEvaluation = + bridge.EvaluateHostCommand("scripts.rebuild"); + EXPECT_FALSE(scriptEvaluation.executable); + EXPECT_EQ(scriptEvaluation.message, "Runtime owns scripts honestly."); + EXPECT_EQ(runtimeOwner.lastEvaluatedScriptCommandId, "scripts.rebuild"); } TEST(EditorHostCommandBridgeTest, AssetCommandsDelegateToProjectRoute) { diff --git a/tests/UI/Editor/unit/test_editor_project_runtime.cpp b/tests/UI/Editor/unit/test_editor_project_runtime.cpp index 77a59b26..7f6c8524 100644 --- a/tests/UI/Editor/unit/test_editor_project_runtime.cpp +++ b/tests/UI/Editor/unit/test_editor_project_runtime.cpp @@ -74,7 +74,7 @@ TEST(EditorProjectRuntimeTests, NavigateToFolderClearsCurrentProjectSelection) { EXPECT_EQ(runtime.GetSelection().kind, EditorSelectionKind::None); } -TEST(EditorProjectRuntimeTests, OpenSceneItemQueuesSceneOpenRequest) { +TEST(EditorProjectRuntimeTests, OpenSceneItemReportsOpenableSceneWithoutOwningDocumentLoad) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc")); @@ -84,11 +84,6 @@ TEST(EditorProjectRuntimeTests, OpenSceneItemQueuesSceneOpenRequest) { ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes")); ASSERT_TRUE(runtime.OpenItem("Assets/Scenes/Main.xc")); - const std::optional openedScene = - runtime.ConsumePendingSceneOpenPath(); - ASSERT_TRUE(openedScene.has_value()); - EXPECT_EQ(openedScene.value(), (repo.Root() / "project/Assets/Scenes/Main.xc")); - EXPECT_FALSE(runtime.ConsumePendingSceneOpenPath().has_value()); } TEST(EditorProjectRuntimeTests, ResolveCommandTargetsFollowRuntimeSelectionAndCurrentFolder) { diff --git a/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp b/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp index 60de53d1..7521d353 100644 --- a/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp +++ b/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp @@ -19,11 +19,25 @@ public: return hierarchySnapshot; } + bool NewScene(std::string_view sceneName) override { + lastNewSceneName = std::string(sceneName); + return newSceneResult; + } + bool OpenSceneAsset(const std::filesystem::path& scenePath) override { lastOpenedScenePath = scenePath; return openSceneResult; } + bool SaveActiveScene(const std::filesystem::path& scenePath) override { + lastSavedScenePath = scenePath; + return saveSceneResult; + } + + ::XCEngine::Components::Scene* GetActiveScene() const override { + return nullptr; + } + std::optional GetObjectSnapshot( std::string_view itemId) const override { lastGetObjectSnapshotItemId = std::string(itemId); @@ -82,11 +96,15 @@ public: EditorStartupSceneResult startupSceneResult = {}; EditorSceneHierarchySnapshot hierarchySnapshot = {}; std::optional objectSnapshot = std::nullopt; + bool newSceneResult = false; bool openSceneResult = false; + bool saveSceneResult = false; bool addComponentResult = false; int ensureStartupSceneCallCount = 0; std::filesystem::path lastProjectRoot = {}; + std::string lastNewSceneName = {}; std::filesystem::path lastOpenedScenePath = {}; + std::filesystem::path lastSavedScenePath = {}; mutable std::string lastGetObjectSnapshotItemId = {}; std::string lastAddComponentItemId = {}; std::string lastAddComponentTypeName = {};