diff --git a/editor/AGENTS.md b/editor/AGENTS.md index 9c0fb01f..59d6f1ae 100644 --- a/editor/AGENTS.md +++ b/editor/AGENTS.md @@ -11,6 +11,7 @@ - 产品装配应以 `app/Core/Product/EditorProductManifest.*` 为单一事实源。正式 panel 集、action route、runtime owner、viewport renderer owner 先在 manifest 中声明,再派生 shell / menu / command / runtime / viewport 注册。 - UI widget / shell / workspace 代码优先保持 model/state/request/frame/result 风格。新增行为要能被 `tests/UI/Editor/unit` 以纯状态方式测试。 - scene/project 的用户操作应通过 runtime 或 command route 进入,不要在 draw/append 阶段直接改 scene 或文件系统。 +- scene 事实所有权走 `app/Core/Scene/EditorSceneBackend.h` 合约。`EditorSceneRuntime` 只消费显式 backend;只有 `app/Services/Scene/EngineEditorSceneBackend.*` 可以直接触碰 engine `SceneManager::Get()` / `ResourceManager::Get()` 这类全局 runtime。 - scene 渲染私有逻辑不要塞进 panel 或 shell。新增渲染能力时先判断它属于 engine `Rendering/RHI`、editor viewport pass bundle,还是 UI overlay。 - 运行时路径以 `app/Core/Environment/EditorRuntimePaths.h` 为显式契约。workspace、executable、resource、project、capture 根路径由 bootstrap 一次性解析并向下传递;不要在下游重新从 repo root、当前工作目录或文档文件推导运行环境。 - 资源路径、图标、shader、截图输出都应走明确服务或 host 接口。不要硬编码从当前工作目录猜路径;手动验证截图不得写回 source tree。 @@ -30,7 +31,7 @@ - `EditorSelectionService` 是 hierarchy/project/inspector/scene viewport 之间的选择同步核心。不要在单个 panel 内维护另一套长期选择真相。 - `EditorProjectRuntime` 包装 `ProjectBrowserModel`,负责 project tree/grid、选择、文件操作、scene asset open request。文件系统改动后要刷新并 revalidate selection。 - `ProjectPanel` 只消费由 `EditorContext` / `EditorPanelServices` 提供的 `EditorProjectRuntime`,不再拥有或初始化自己的 project runtime。测试也应显式创建 `EditorProjectRuntime` 并通过 `SetProjectRuntime()` 注入,不要把 `projectRoot` 传给 panel。 -- `EditorSceneRuntime` 负责 startup scene、editor scene camera、hierarchy selection、component list、transform edit history 和 scene tool state。 +- `EditorSceneBackend` 是 scene document/backend 的显式边界;`EngineEditorSceneBackend` 是当前接入 engine 全局 scene/resource runtime 的唯一生产实现。`EditorSceneRuntime` 负责 startup scene 编排、editor scene camera、hierarchy selection、component list、transform edit history 和 scene tool state,但不直接访问 engine 单例。 当前目录地图: @@ -43,10 +44,12 @@ - `include/XCEditor/Windowing`:多窗口 workspace 状态、同步计划和 presentation policy。 - `src/**`:对应公共头的实现。保持偏纯函数/状态机风格。 - `app/Core`:产品级 contracts、session、command focus、selection、panel services、scene/project/viewport/windowing 接口。 +- `app/Core/Scene`:scene backend 合约和 editor scene runtime 可消费的公共 scene 标识工具。 - `app/Core/Environment`:运行时路径契约。bootstrap 负责解析 `EditorRuntimePaths`,core/composition/features/rendering 只消费显式路径。 - `app/Core/Product`:产品 manifest。这里定义正式 panel 集、route 归属、runtime owner 和 viewport renderer owner。 - `app/Composition`:装配编辑器 shell。`EditorContext` 拥有 session、project runtime、scene runtime、selection、command bridge;`EditorShellRuntime` 驱动 shell interaction、hosted panels 和 viewport runtime。 - `app/Features`:产品面板与场景视图工具。 +- `app/Services`:产品 runtime 的生产服务实现。scene 的 engine 接入集中在 `app/Services/Scene/EngineEditorSceneBackend.*`;project runtime 集中在 `app/Services/Project`。 - `app/Rendering`:编辑器 viewport、icon、object-id picking、grid/outline/helper pass 相关服务。渲染执行仍走 engine `Rendering + RHI`。 - `app/Host`:宿主接口实现。Win32/D3D12 细节只能待在这里或 rendering host 实现里。 - `app/Windowing`:窗口实例、内容控制器、生命周期协调器、workspace 多窗口同步、截图和 frame orchestration。 @@ -77,7 +80,9 @@ wWinMain - `src/Viewport` 和 `include/XCEditor/Viewport`:viewport slot/shell/input bridge,只处理 UI 容器和输入桥。 - `app/Features/Scene/SceneViewportController.*`:场景视图产品交互,处理 Q/W/E/R 工具切换、F 聚焦、鼠标导航、scene icon picking、transform gizmo。 -- `app/Core/Scene/EditorSceneRuntime.*`:scene selection、editor camera、transform undo/redo、component mutation 和 scene render request 的事实来源。 +- `app/Core/Scene/EditorSceneBackend.h`:scene backend contract、startup scene result 和 game object item id helpers。 +- `app/Services/Scene/EditorSceneRuntime.*`:scene selection、editor camera、transform undo/redo、component mutation 和 scene render request 的事实来源;它必须通过注入的 `EditorSceneBackend` 访问真实 scene。 +- `app/Services/Scene/EngineEditorSceneBackend.*`:当前唯一允许直接接入 engine `SceneManager` / `ResourceManager` 的 scene backend 生产实现。 - `app/Rendering/Viewport/ViewportHostService.*`:离屏 viewport 资源管理和 renderer 注册。 - `app/Rendering/Viewport/SceneViewportRenderService.*`:调用 engine `SceneRenderer`,插入 grid、selection outline、selected helpers、object-id 等 editor pass。 - object-id picking 依赖有效 object-id surface 和 frame serial,修改时要覆盖 `test_viewport_object_id_picker.cpp` 及相关 viewport render plan 测试。 @@ -120,9 +125,8 @@ ctest --test-dir build -C Debug -R "editor|xceditor" --output-on-failure - 已新增 manifest validation 测试,确保产品 manifest 能声明 panel runtime owner 和 viewport renderer owner,并覆盖 `game` placeholder 的预期状态。 - 已更新根 `AGENT.md` 和本文件,去掉旧 `--project` 说法,记录当前 XCUI editor、`XCEditorCore` 分层和 product manifest 规则。 - 已移除 `ProjectPanel` 自持 `EditorProjectRuntime` 的 fallback 路径;project runtime 事实源现在只来自 `EditorContext`,面板测试改为显式 runtime 注入。 +- 已把 scene 的 engine 全局访问收敛到 `EngineEditorSceneBackend`,让 `EditorSceneRuntime` 通过 `EditorSceneBackend` 合约初始化、打开 scene 和执行 hierarchy mutation;新增 backend contract 单测覆盖无 backend 失败和 fake backend 注入。 - 本次改动验证过: - `cmake --build build --config Debug --target XCEditor` - `cmake --build build --config Debug --target editor_app_core_tests` - - `.\build\tests\UI\Editor\unit\Debug\editor_app_core_tests.exe --gtest_filter=EditorShellAssetValidationTest.*` - `cmake --build build --config Debug --target editor_app_feature_tests` - - `.\build\tests\UI\Editor\unit\Debug\editor_app_feature_tests.exe` diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 79865dc6..992c9f7a 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -283,7 +283,7 @@ if(XCENGINE_BUILD_XCUI_EDITOR_CORE) app/Services/Scene/EditorSceneRuntime.cpp app/Services/Project/EditorProjectRuntime.cpp app/Services/Project/ProjectBrowserModel.cpp - app/Services/Scene/EditorSceneBridge.cpp + app/Services/Scene/EngineEditorSceneBackend.cpp ) set(XCUI_EDITOR_APP_CORE_SOURCES diff --git a/editor/app/Composition/EditorContext.cpp b/editor/app/Composition/EditorContext.cpp index 1f266a92..4ddc520f 100644 --- a/editor/app/Composition/EditorContext.cpp +++ b/editor/app/Composition/EditorContext.cpp @@ -1,5 +1,6 @@ #include "EditorContext.h" #include "EditorShellAssetBuilder.h" +#include "Scene/EngineEditorSceneBackend.h" #include "Scene/EditorSceneRuntime.h" #include "Panels/EditorPanelIds.h" #include "WorkspaceEventSync.h" @@ -75,7 +76,7 @@ bool EditorContext::Initialize(const EditorRuntimePaths& runtimePaths) { m_projectRuntime.Initialize(runtimePaths.projectRoot); AppendUIEditorRuntimeTrace("startup", "EditorProjectRuntime::Initialize end"); m_projectRuntime.BindSelectionService(&m_selectionService); - m_sceneRuntime.Reset(); + m_sceneRuntime.SetBackend(CreateEngineEditorSceneBackend()); AppendUIEditorRuntimeTrace("startup", "EditorSceneRuntime::Initialize begin"); m_sceneRuntime.Initialize(m_session.projectRoot); AppendUIEditorRuntimeTrace("startup", "EditorSceneRuntime::Initialize end"); diff --git a/editor/app/Core/Scene/EditorSceneBackend.h b/editor/app/Core/Scene/EditorSceneBackend.h new file mode 100644 index 00000000..aea468d9 --- /dev/null +++ b/editor/app/Core/Scene/EditorSceneBackend.h @@ -0,0 +1,83 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace XCEngine::Components { + +class Scene; +class GameObject; + +} // namespace XCEngine::Components + +namespace XCEngine::UI::Editor::App { + +struct EditorStartupSceneResult { + bool ready = false; + bool loadedFromDisk = false; + std::filesystem::path scenePath = {}; + std::string sceneName = {}; +}; + +class EditorSceneBackend { +public: + virtual ~EditorSceneBackend() = default; + + virtual EditorStartupSceneResult EnsureStartupScene( + const std::filesystem::path& projectRoot) = 0; + virtual ::XCEngine::Components::Scene* GetActiveScene() const = 0; + virtual bool OpenSceneAsset(const std::filesystem::path& scenePath) = 0; + virtual ::XCEngine::Components::GameObject* FindGameObject( + std::string_view itemId) const = 0; + virtual bool RenameGameObject( + std::string_view itemId, + std::string_view newName) = 0; + virtual bool DeleteGameObject(std::string_view itemId) = 0; + virtual std::string DuplicateGameObject(std::string_view itemId) = 0; + virtual bool ReparentGameObject( + std::string_view itemId, + std::string_view parentItemId) = 0; + virtual bool MoveGameObjectBefore( + std::string_view itemId, + std::string_view targetItemId) = 0; + virtual bool MoveGameObjectAfter( + std::string_view itemId, + std::string_view targetItemId) = 0; + virtual bool MoveGameObjectToRoot(std::string_view itemId) = 0; +}; + +inline std::string MakeEditorGameObjectItemId( + ::XCEngine::Components::GameObject::ID id) { + return id == ::XCEngine::Components::GameObject::INVALID_ID + ? std::string() + : std::to_string(id); +} + +inline std::optional<::XCEngine::Components::GameObject::ID> +ParseEditorGameObjectItemId(std::string_view itemId) { + if (itemId.empty()) { + return std::nullopt; + } + + ::XCEngine::Components::GameObject::ID parsedId = + ::XCEngine::Components::GameObject::INVALID_ID; + const char* first = itemId.data(); + const char* last = itemId.data() + itemId.size(); + const std::from_chars_result result = + std::from_chars(first, last, parsedId); + if (result.ec != std::errc() || + result.ptr != last || + parsedId == ::XCEngine::Components::GameObject::INVALID_ID) { + return std::nullopt; + } + + return parsedId; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Features/Hierarchy/HierarchyModel.cpp b/editor/app/Features/Hierarchy/HierarchyModel.cpp index c7c5ff70..db5da779 100644 --- a/editor/app/Features/Hierarchy/HierarchyModel.cpp +++ b/editor/app/Features/Hierarchy/HierarchyModel.cpp @@ -1,6 +1,6 @@ #include "HierarchyModel.h" -#include "Scene/EditorSceneBridge.h" +#include "Scene/EditorSceneBackend.h" #include #include diff --git a/editor/app/Services/Scene/EditorSceneBridge.h b/editor/app/Services/Scene/EditorSceneBridge.h deleted file mode 100644 index 1c0f2bb3..00000000 --- a/editor/app/Services/Scene/EditorSceneBridge.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include - -namespace XCEngine::Components { - -class Scene; - -} // namespace XCEngine::Components - -namespace XCEngine::UI::Editor::App { - -struct EditorStartupSceneResult { - bool ready = false; - bool loadedFromDisk = false; - std::filesystem::path scenePath = {}; - std::string sceneName = {}; -}; - -EditorStartupSceneResult EnsureEditorStartupScene( - const std::filesystem::path& projectRoot); - -::XCEngine::Components::Scene* GetActiveEditorScene(); -bool OpenEditorSceneAsset(const std::filesystem::path& scenePath); -std::string MakeEditorGameObjectItemId(::XCEngine::Components::GameObject::ID id); -std::optional<::XCEngine::Components::GameObject::ID> ParseEditorGameObjectItemId( - std::string_view itemId); -::XCEngine::Components::GameObject* FindEditorGameObject(std::string_view itemId); -bool RenameEditorGameObject( - std::string_view itemId, - std::string_view newName); -bool DeleteEditorGameObject(std::string_view itemId); -std::string DuplicateEditorGameObject(std::string_view itemId); -bool ReparentEditorGameObject( - std::string_view itemId, - std::string_view parentItemId); -bool MoveEditorGameObjectBefore( - std::string_view itemId, - std::string_view targetItemId); -bool MoveEditorGameObjectAfter( - std::string_view itemId, - std::string_view targetItemId); -bool MoveEditorGameObjectToRoot(std::string_view itemId); - -} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Services/Scene/EditorSceneRuntime.cpp b/editor/app/Services/Scene/EditorSceneRuntime.cpp index 3ebc3ca3..1bf447f7 100644 --- a/editor/app/Services/Scene/EditorSceneRuntime.cpp +++ b/editor/app/Services/Scene/EditorSceneRuntime.cpp @@ -182,10 +182,18 @@ void EditorSceneRuntime::Reset() { m_inspectorRevision = 0u; } +void EditorSceneRuntime::SetBackend(std::unique_ptr backend) { + m_backend = std::move(backend); + Reset(); +} + bool EditorSceneRuntime::Initialize(const std::filesystem::path& projectRoot) { Reset(); + if (m_backend == nullptr) { + return false; + } m_projectRoot = projectRoot; - m_startupSceneResult = EnsureEditorStartupScene(projectRoot); + m_startupSceneResult = m_backend->EnsureStartupScene(projectRoot); EnsureSceneViewCamera(); RefreshScene(); return m_startupSceneResult.ready; @@ -225,7 +233,7 @@ const EditorStartupSceneResult& EditorSceneRuntime::GetStartupResult() const { } Scene* EditorSceneRuntime::GetActiveScene() const { - return GetActiveEditorScene(); + return m_backend != nullptr ? m_backend->GetActiveScene() : nullptr; } ::XCEngine::Components::CameraComponent* EditorSceneRuntime::GetSceneViewCamera() { @@ -389,7 +397,7 @@ void EditorSceneRuntime::ClearSelection() { } bool EditorSceneRuntime::OpenSceneAsset(const std::filesystem::path& scenePath) { - if (!OpenEditorSceneAsset(scenePath)) { + if (m_backend == nullptr || !m_backend->OpenSceneAsset(scenePath)) { return false; } @@ -413,13 +421,15 @@ bool EditorSceneRuntime::OpenSceneAsset(const std::filesystem::path& scenePath) } GameObject* EditorSceneRuntime::FindGameObject(std::string_view itemId) const { - return FindEditorGameObject(itemId); + return m_backend != nullptr ? m_backend->FindGameObject(itemId) : nullptr; } bool EditorSceneRuntime::RenameGameObject( std::string_view itemId, std::string_view newName) { - const bool renamed = RenameEditorGameObject(itemId, newName); + const bool renamed = + m_backend != nullptr && + m_backend->RenameGameObject(itemId, newName); if (renamed) { IncrementInspectorRevision(); } @@ -429,7 +439,9 @@ bool EditorSceneRuntime::RenameGameObject( bool EditorSceneRuntime::DeleteGameObject(std::string_view itemId) { ResetTransformEditHistory(); - const bool deleted = DeleteEditorGameObject(itemId); + const bool deleted = + m_backend != nullptr && + m_backend->DeleteGameObject(itemId); if (deleted) { IncrementInspectorRevision(); } @@ -440,7 +452,10 @@ bool EditorSceneRuntime::DeleteGameObject(std::string_view itemId) { std::string EditorSceneRuntime::DuplicateGameObject(std::string_view itemId) { ResetTransformEditHistory(); - const std::string duplicatedItemId = DuplicateEditorGameObject(itemId); + const std::string duplicatedItemId = + m_backend != nullptr + ? m_backend->DuplicateGameObject(itemId) + : std::string(); if (!duplicatedItemId.empty()) { IncrementInspectorRevision(); SetSelection(duplicatedItemId); @@ -455,7 +470,8 @@ bool EditorSceneRuntime::ReparentGameObject( std::string_view parentItemId) { ResetTransformEditHistory(); const bool reparented = - ReparentEditorGameObject(itemId, parentItemId); + m_backend != nullptr && + m_backend->ReparentGameObject(itemId, parentItemId); if (reparented) { IncrementInspectorRevision(); } @@ -468,7 +484,8 @@ bool EditorSceneRuntime::MoveGameObjectBefore( std::string_view targetItemId) { ResetTransformEditHistory(); const bool moved = - MoveEditorGameObjectBefore(itemId, targetItemId); + m_backend != nullptr && + m_backend->MoveGameObjectBefore(itemId, targetItemId); if (moved) { IncrementInspectorRevision(); } @@ -481,7 +498,8 @@ bool EditorSceneRuntime::MoveGameObjectAfter( std::string_view targetItemId) { ResetTransformEditHistory(); const bool moved = - MoveEditorGameObjectAfter(itemId, targetItemId); + m_backend != nullptr && + m_backend->MoveGameObjectAfter(itemId, targetItemId); if (moved) { IncrementInspectorRevision(); } @@ -491,7 +509,9 @@ bool EditorSceneRuntime::MoveGameObjectAfter( bool EditorSceneRuntime::MoveGameObjectToRoot(std::string_view itemId) { ResetTransformEditHistory(); - const bool moved = MoveEditorGameObjectToRoot(itemId); + const bool moved = + m_backend != nullptr && + m_backend->MoveGameObjectToRoot(itemId); if (moved) { IncrementInspectorRevision(); } diff --git a/editor/app/Services/Scene/EditorSceneRuntime.h b/editor/app/Services/Scene/EditorSceneRuntime.h index cad32c86..303da994 100644 --- a/editor/app/Services/Scene/EditorSceneRuntime.h +++ b/editor/app/Services/Scene/EditorSceneRuntime.h @@ -1,6 +1,6 @@ #pragma once -#include "Scene/EditorSceneBridge.h" +#include "Scene/EditorSceneBackend.h" #include "Scene/SceneViewportCameraController.h" #include "Scene/SceneToolState.h" #include "State/EditorSelectionService.h" @@ -55,6 +55,7 @@ public: EditorSceneRuntime& operator=(EditorSceneRuntime&&) = delete; void Reset(); + void SetBackend(std::unique_ptr backend); bool Initialize(const std::filesystem::path& projectRoot); void BindSelectionService(EditorSelectionService* selectionService); @@ -175,6 +176,7 @@ private: void IncrementInspectorRevision(); std::filesystem::path m_projectRoot = {}; + std::unique_ptr m_backend = {}; EditorStartupSceneResult m_startupSceneResult = {}; EditorSelectionService m_ownedSelectionService = {}; EditorSelectionService* m_selectionService = &m_ownedSelectionService; diff --git a/editor/app/Services/Scene/EditorSceneBridge.cpp b/editor/app/Services/Scene/EngineEditorSceneBackend.cpp similarity index 53% rename from editor/app/Services/Scene/EditorSceneBridge.cpp rename to editor/app/Services/Scene/EngineEditorSceneBackend.cpp index 409c5852..c754e716 100644 --- a/editor/app/Services/Scene/EditorSceneBridge.cpp +++ b/editor/app/Services/Scene/EngineEditorSceneBackend.cpp @@ -1,4 +1,4 @@ -#include "Scene/EditorSceneBridge.h" +#include "Scene/EngineEditorSceneBackend.h" #include #include @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include #include @@ -169,13 +169,24 @@ std::optional FindSiblingIndex( return static_cast(std::distance(siblings.begin(), it)); } +GameObject* FindGameObjectByItemId(std::string_view itemId) { + Scene* scene = ResolvePrimaryScene(); + const std::optional gameObjectId = + ParseEditorGameObjectItemId(itemId); + if (scene == nullptr || !gameObjectId.has_value()) { + return nullptr; + } + + return scene->FindByID(gameObjectId.value()); +} + bool MoveGameObjectRelativeToTarget( std::string_view itemId, std::string_view targetItemId, bool placeAfterTarget) { Scene* scene = ResolvePrimaryScene(); - GameObject* gameObject = FindEditorGameObject(itemId); - GameObject* target = FindEditorGameObject(targetItemId); + GameObject* gameObject = FindGameObjectByItemId(itemId); + GameObject* target = FindGameObjectByItemId(targetItemId); if (scene == nullptr || gameObject == nullptr || target == nullptr || @@ -218,211 +229,192 @@ bool MoveGameObjectRelativeToTarget( return true; } -} // namespace +class EngineEditorSceneBackend final : public EditorSceneBackend { +public: + EditorStartupSceneResult EnsureStartupScene( + const std::filesystem::path& projectRoot) override { + EditorStartupSceneResult result = {}; + TraceSceneStartup("EnsureStartupScene begin projectRoot=" + projectRoot.string()); + if (projectRoot.empty()) { + return result; + } -EditorStartupSceneResult EnsureEditorStartupScene( - const std::filesystem::path& projectRoot) { - EditorStartupSceneResult result = {}; - TraceSceneStartup("EnsureEditorStartupScene begin projectRoot=" + projectRoot.string()); - if (projectRoot.empty()) { + ResourceManager::Get().SetResourceRoot(projectRoot.string().c_str()); + TraceSceneStartup("ResourceManager::SetResourceRoot complete"); + + if (Scene* activeScene = ResolvePrimaryScene(); + activeScene != nullptr) { + result.ready = true; + result.sceneName = activeScene->GetName(); + TraceSceneStartup("EnsureStartupScene reused active scene=" + result.sceneName); + return result; + } + + const std::filesystem::path startupScenePath = + (projectRoot / "Assets" / "Scenes" / "Main.xc").lexically_normal(); + SceneManager& sceneManager = SceneManager::Get(); + + if (std::filesystem::exists(startupScenePath) && + std::filesystem::is_regular_file(startupScenePath)) { + TraceSceneStartup( + "SceneManager::LoadScene begin path=" + startupScenePath.string()); + { + ResourceManager::ScopedDeferredSceneLoad deferredSceneLoad( + ResourceManager::Get()); + sceneManager.LoadScene(startupScenePath.string()); + } + TraceSceneStartup("SceneManager::LoadScene end"); + Scene* loadedScene = + sceneManager.GetScene(startupScenePath.stem().string()); + if (loadedScene == nullptr) { + loadedScene = ResolvePrimaryScene(); + } else { + sceneManager.SetActiveScene(loadedScene); + } + + if (loadedScene != nullptr) { + result.ready = true; + result.loadedFromDisk = true; + result.scenePath = startupScenePath; + result.sceneName = loadedScene->GetName(); + TraceSceneStartup( + "EnsureStartupScene loaded scene=" + result.sceneName); + return result; + } + } + + if (Scene* scene = sceneManager.CreateScene("Main"); + scene != nullptr) { + sceneManager.SetActiveScene(scene); + result.ready = true; + result.sceneName = scene->GetName(); + TraceSceneStartup( + "EnsureStartupScene created scene=" + result.sceneName); + } + + TraceSceneStartup( + std::string("EnsureStartupScene end ready=") + + (result.ready ? "1" : "0")); return result; } - ResourceManager::Get().SetResourceRoot(projectRoot.string().c_str()); - TraceSceneStartup("ResourceManager::SetResourceRoot complete"); - - if (Scene* activeScene = ResolvePrimaryScene(); - activeScene != nullptr) { - result.ready = true; - result.sceneName = activeScene->GetName(); - TraceSceneStartup("EnsureEditorStartupScene reused active scene=" + result.sceneName); - return result; + Scene* GetActiveScene() const override { + return ResolvePrimaryScene(); } - const std::filesystem::path startupScenePath = - (projectRoot / "Assets" / "Scenes" / "Main.xc").lexically_normal(); - SceneManager& sceneManager = SceneManager::Get(); + bool OpenSceneAsset(const std::filesystem::path& scenePath) override { + if (scenePath.empty()) { + return false; + } - if (std::filesystem::exists(startupScenePath) && - std::filesystem::is_regular_file(startupScenePath)) { - TraceSceneStartup("SceneManager::LoadScene begin path=" + startupScenePath.string()); + std::error_code errorCode = {}; + if (!std::filesystem::exists(scenePath, errorCode) || + errorCode || + !std::filesystem::is_regular_file(scenePath, errorCode)) { + return false; + } + + SceneManager& sceneManager = SceneManager::Get(); { ResourceManager::ScopedDeferredSceneLoad deferredSceneLoad( ResourceManager::Get()); - sceneManager.LoadScene(startupScenePath.string()); + sceneManager.LoadScene(scenePath.string()); } - TraceSceneStartup("SceneManager::LoadScene end"); - Scene* loadedScene = sceneManager.GetScene(startupScenePath.stem().string()); + Scene* loadedScene = sceneManager.GetScene(scenePath.stem().string()); if (loadedScene == nullptr) { loadedScene = ResolvePrimaryScene(); } else { sceneManager.SetActiveScene(loadedScene); } - if (loadedScene != nullptr) { - result.ready = true; - result.loadedFromDisk = true; - result.scenePath = startupScenePath; - result.sceneName = loadedScene->GetName(); - TraceSceneStartup("EnsureEditorStartupScene loaded scene=" + result.sceneName); - return result; + return loadedScene != nullptr; + } + + GameObject* FindGameObject(std::string_view itemId) const override { + return FindGameObjectByItemId(itemId); + } + + bool RenameGameObject( + std::string_view itemId, + std::string_view newName) override { + GameObject* gameObject = FindGameObject(itemId); + if (gameObject == nullptr) { + return false; } + + gameObject->SetName(std::string(newName)); + return true; } - if (Scene* scene = sceneManager.CreateScene("Main"); - scene != nullptr) { - sceneManager.SetActiveScene(scene); - result.ready = true; - result.sceneName = scene->GetName(); - TraceSceneStartup("EnsureEditorStartupScene created scene=" + result.sceneName); + bool DeleteGameObject(std::string_view itemId) override { + Scene* scene = ResolvePrimaryScene(); + GameObject* gameObject = FindGameObject(itemId); + if (scene == nullptr || gameObject == nullptr) { + return false; + } + + scene->DestroyGameObject(gameObject); + return true; } - TraceSceneStartup( - std::string("EnsureEditorStartupScene end ready=") + - (result.ready ? "1" : "0")); - return result; -} + std::string DuplicateGameObject(std::string_view itemId) override { + Scene* scene = ResolvePrimaryScene(); + GameObject* gameObject = FindGameObject(itemId); + if (scene == nullptr || gameObject == nullptr) { + return {}; + } -Scene* GetActiveEditorScene() { - return ResolvePrimaryScene(); -} - -bool OpenEditorSceneAsset(const std::filesystem::path& scenePath) { - if (scenePath.empty()) { - return false; + const ClipboardNode clipboard = CopyGameObjectRecursive(*gameObject); + GameObject* duplicate = + PasteGameObjectRecursive(*scene, clipboard, gameObject->GetParent()); + return duplicate != nullptr + ? MakeEditorGameObjectItemId(duplicate->GetID()) + : std::string(); } - std::error_code errorCode = {}; - if (!std::filesystem::exists(scenePath, errorCode) || - errorCode || - !std::filesystem::is_regular_file(scenePath, errorCode)) { - return false; + bool ReparentGameObject( + std::string_view itemId, + std::string_view parentItemId) override { + GameObject* gameObject = FindGameObject(itemId); + GameObject* newParent = FindGameObject(parentItemId); + if (gameObject == nullptr || newParent == nullptr || + gameObject == newParent || + gameObject->GetParent() == newParent || + WouldCreateCycle(*gameObject, *newParent)) { + return false; + } + + gameObject->SetParent(newParent); + return true; } - SceneManager& sceneManager = SceneManager::Get(); - { - ResourceManager::ScopedDeferredSceneLoad deferredSceneLoad( - ResourceManager::Get()); - sceneManager.LoadScene(scenePath.string()); - } - Scene* loadedScene = sceneManager.GetScene(scenePath.stem().string()); - if (loadedScene == nullptr) { - loadedScene = ResolvePrimaryScene(); - } else { - sceneManager.SetActiveScene(loadedScene); + bool MoveGameObjectBefore( + std::string_view itemId, + std::string_view targetItemId) override { + return MoveGameObjectRelativeToTarget(itemId, targetItemId, false); } - return loadedScene != nullptr; -} - -std::string MakeEditorGameObjectItemId(GameObject::ID id) { - return id == GameObject::INVALID_ID ? std::string() : std::to_string(id); -} - -std::optional ParseEditorGameObjectItemId( - std::string_view itemId) { - if (itemId.empty()) { - return std::nullopt; + bool MoveGameObjectAfter( + std::string_view itemId, + std::string_view targetItemId) override { + return MoveGameObjectRelativeToTarget(itemId, targetItemId, true); } - GameObject::ID parsedId = GameObject::INVALID_ID; - const char* first = itemId.data(); - const char* last = itemId.data() + itemId.size(); - const std::from_chars_result result = - std::from_chars(first, last, parsedId); - if (result.ec != std::errc() || result.ptr != last || - parsedId == GameObject::INVALID_ID) { - return std::nullopt; + bool MoveGameObjectToRoot(std::string_view itemId) override { + GameObject* gameObject = FindGameObject(itemId); + if (gameObject == nullptr || gameObject->GetParent() == nullptr) { + return false; + } + + gameObject->SetParent(nullptr); + return true; } +}; - return parsedId; -} +} // namespace -GameObject* FindEditorGameObject(std::string_view itemId) { - Scene* scene = ResolvePrimaryScene(); - const std::optional gameObjectId = - ParseEditorGameObjectItemId(itemId); - if (scene == nullptr || !gameObjectId.has_value()) { - return nullptr; - } - - return scene->FindByID(gameObjectId.value()); -} - -bool RenameEditorGameObject( - std::string_view itemId, - std::string_view newName) { - GameObject* gameObject = FindEditorGameObject(itemId); - if (gameObject == nullptr) { - return false; - } - - gameObject->SetName(std::string(newName)); - return true; -} - -bool DeleteEditorGameObject(std::string_view itemId) { - Scene* scene = ResolvePrimaryScene(); - GameObject* gameObject = FindEditorGameObject(itemId); - if (scene == nullptr || gameObject == nullptr) { - return false; - } - - scene->DestroyGameObject(gameObject); - return true; -} - -std::string DuplicateEditorGameObject(std::string_view itemId) { - Scene* scene = ResolvePrimaryScene(); - GameObject* gameObject = FindEditorGameObject(itemId); - if (scene == nullptr || gameObject == nullptr) { - return {}; - } - - const ClipboardNode clipboard = CopyGameObjectRecursive(*gameObject); - GameObject* duplicate = - PasteGameObjectRecursive(*scene, clipboard, gameObject->GetParent()); - return duplicate != nullptr - ? MakeEditorGameObjectItemId(duplicate->GetID()) - : std::string(); -} - -bool ReparentEditorGameObject( - std::string_view itemId, - std::string_view parentItemId) { - GameObject* gameObject = FindEditorGameObject(itemId); - GameObject* newParent = FindEditorGameObject(parentItemId); - if (gameObject == nullptr || newParent == nullptr || - gameObject == newParent || - gameObject->GetParent() == newParent || - WouldCreateCycle(*gameObject, *newParent)) { - return false; - } - - gameObject->SetParent(newParent); - return true; -} - -bool MoveEditorGameObjectBefore( - std::string_view itemId, - std::string_view targetItemId) { - return MoveGameObjectRelativeToTarget(itemId, targetItemId, false); -} - -bool MoveEditorGameObjectAfter( - std::string_view itemId, - std::string_view targetItemId) { - return MoveGameObjectRelativeToTarget(itemId, targetItemId, true); -} - -bool MoveEditorGameObjectToRoot(std::string_view itemId) { - GameObject* gameObject = FindEditorGameObject(itemId); - if (gameObject == nullptr || gameObject->GetParent() == nullptr) { - return false; - } - - gameObject->SetParent(nullptr); - return true; +std::unique_ptr CreateEngineEditorSceneBackend() { + return std::make_unique(); } } // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Services/Scene/EngineEditorSceneBackend.h b/editor/app/Services/Scene/EngineEditorSceneBackend.h new file mode 100644 index 00000000..3561705e --- /dev/null +++ b/editor/app/Services/Scene/EngineEditorSceneBackend.h @@ -0,0 +1,11 @@ +#pragma once + +#include "Scene/EditorSceneBackend.h" + +#include + +namespace XCEngine::UI::Editor::App { + +std::unique_ptr CreateEngineEditorSceneBackend(); + +} // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index 44fa9f5e..1f9253de 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -67,6 +67,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES set(EDITOR_APP_CORE_TEST_SOURCES test_editor_host_command_bridge.cpp test_editor_project_runtime.cpp + test_editor_scene_runtime_backend.cpp test_editor_shell_asset_validation.cpp test_project_browser_model.cpp test_hierarchy_scene_binding.cpp diff --git a/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp b/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp new file mode 100644 index 00000000..58c26d5d --- /dev/null +++ b/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp @@ -0,0 +1,124 @@ +#include "Scene/EditorSceneBackend.h" +#include "Scene/EditorSceneRuntime.h" + +#include +#include + +#include + +namespace XCEngine::UI::Editor::App { +namespace { + +using ::XCEngine::Components::GameObject; +using ::XCEngine::Components::Scene; + +class FakeEditorSceneBackend final : public EditorSceneBackend { +public: + EditorStartupSceneResult EnsureStartupScene( + const std::filesystem::path& projectRoot) override { + ++ensureStartupSceneCallCount; + lastProjectRoot = projectRoot; + return startupSceneResult; + } + + Scene* GetActiveScene() const override { + return activeScene; + } + + bool OpenSceneAsset(const std::filesystem::path& scenePath) override { + lastOpenedScenePath = scenePath; + return openSceneResult; + } + + GameObject* FindGameObject(std::string_view itemId) const override { + lastFindItemId = std::string(itemId); + return foundGameObject; + } + + bool RenameGameObject( + std::string_view, + std::string_view) override { + return false; + } + + bool DeleteGameObject(std::string_view) override { + return false; + } + + std::string DuplicateGameObject(std::string_view) override { + return {}; + } + + bool ReparentGameObject( + std::string_view, + std::string_view) override { + return false; + } + + bool MoveGameObjectBefore( + std::string_view, + std::string_view) override { + return false; + } + + bool MoveGameObjectAfter( + std::string_view, + std::string_view) override { + return false; + } + + bool MoveGameObjectToRoot(std::string_view) override { + return false; + } + + EditorStartupSceneResult startupSceneResult = {}; + Scene* activeScene = nullptr; + GameObject* foundGameObject = nullptr; + bool openSceneResult = false; + int ensureStartupSceneCallCount = 0; + std::filesystem::path lastProjectRoot = {}; + std::filesystem::path lastOpenedScenePath = {}; + mutable std::string lastFindItemId = {}; +}; + +TEST(EditorSceneRuntimeBackendTests, InitializeFailsWithoutBoundBackend) { + EditorSceneRuntime runtime = {}; + EXPECT_FALSE(runtime.Initialize("D:/Xuanchi/Main/XCEngine/project")); +} + +TEST(EditorSceneRuntimeBackendTests, InitializeUsesBoundBackend) { + auto backend = std::make_unique(); + backend->startupSceneResult.ready = true; + backend->startupSceneResult.sceneName = "Main"; + FakeEditorSceneBackend* const backendPtr = backend.get(); + + EditorSceneRuntime runtime = {}; + runtime.SetBackend(std::move(backend)); + + EXPECT_TRUE(runtime.Initialize("D:/Xuanchi/Main/XCEngine/project")); + EXPECT_EQ(backendPtr->ensureStartupSceneCallCount, 1); + EXPECT_EQ( + backendPtr->lastProjectRoot, + std::filesystem::path("D:/Xuanchi/Main/XCEngine/project")); + EXPECT_EQ(runtime.GetStartupResult().sceneName, "Main"); +} + +TEST(EditorSceneRuntimeBackendTests, FindGameObjectUsesBoundBackend) { + auto backend = std::make_unique(); + Scene scene("Main"); + GameObject probe("Probe"); + backend->startupSceneResult.ready = true; + backend->activeScene = &scene; + backend->foundGameObject = &probe; + FakeEditorSceneBackend* const backendPtr = backend.get(); + + EditorSceneRuntime runtime = {}; + runtime.SetBackend(std::move(backend)); + ASSERT_TRUE(runtime.Initialize("D:/Xuanchi/Main/XCEngine/project")); + + EXPECT_EQ(runtime.FindGameObject("42"), &probe); + EXPECT_EQ(backendPtr->lastFindItemId, "42"); +} + +} // namespace +} // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp b/tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp index 19085ffa..8a0ef69f 100644 --- a/tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp +++ b/tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp @@ -1,5 +1,5 @@ #include "Hierarchy/HierarchyModel.h" -#include "Scene/EditorSceneBridge.h" +#include "Scene/EngineEditorSceneBackend.h" #include #include @@ -10,6 +10,7 @@ #include #include +#include namespace XCEngine::UI::Editor::App { namespace { @@ -61,6 +62,10 @@ private: std::filesystem::path m_root = {}; }; +std::unique_ptr CreateTestSceneBackend() { + return CreateEngineEditorSceneBackend(); +} + TEST(HierarchySceneBindingTests, BuildFromSceneUsesRealGameObjectIds) { ScopedSceneManagerReset reset = {}; @@ -97,8 +102,9 @@ TEST(HierarchySceneBindingTests, DuplicateGameObjectClonesHierarchyIntoScene) { GameObject* child = scene->CreateGameObject("Child", root); ASSERT_NE(child, nullptr); + const std::unique_ptr backend = CreateTestSceneBackend(); const std::string duplicateId = - DuplicateEditorGameObject(MakeEditorGameObjectItemId(root->GetID())); + backend->DuplicateGameObject(MakeEditorGameObjectItemId(root->GetID())); ASSERT_FALSE(duplicateId.empty()); const HierarchyModel model = HierarchyModel::BuildFromScene(scene); @@ -124,7 +130,8 @@ TEST(HierarchySceneBindingTests, RenameGameObjectUpdatesRealSceneAndProjection) const std::string itemId = MakeEditorGameObjectItemId(gameObject->GetID()); - ASSERT_TRUE(RenameEditorGameObject(itemId, "PlayerCamera")); + const std::unique_ptr backend = CreateTestSceneBackend(); + ASSERT_TRUE(backend->RenameGameObject(itemId, "PlayerCamera")); EXPECT_EQ(gameObject->GetName(), "PlayerCamera"); const HierarchyModel model = HierarchyModel::BuildFromScene(scene); @@ -147,7 +154,8 @@ TEST(HierarchySceneBindingTests, ReparentAndMoveToRootOperateOnRealScene) { GameObject* child = scene->CreateGameObject("Child", parentA); ASSERT_NE(child, nullptr); - ASSERT_TRUE(ReparentEditorGameObject( + const std::unique_ptr backend = CreateTestSceneBackend(); + ASSERT_TRUE(backend->ReparentGameObject( MakeEditorGameObjectItemId(child->GetID()), MakeEditorGameObjectItemId(parentB->GetID()))); EXPECT_EQ(child->GetParent(), parentB); @@ -159,7 +167,7 @@ TEST(HierarchySceneBindingTests, ReparentAndMoveToRootOperateOnRealScene) { ASSERT_EQ(parentBNode->children.size(), 1u); EXPECT_EQ(parentBNode->children.front().label, "Child"); - ASSERT_TRUE(MoveEditorGameObjectToRoot( + ASSERT_TRUE(backend->MoveGameObjectToRoot( MakeEditorGameObjectItemId(child->GetID()))); EXPECT_EQ(child->GetParent(), nullptr); @@ -181,15 +189,16 @@ TEST(HierarchySceneBindingTests, EnsureStartupSceneLoadsMainSceneAndSetsActive) scene.Save(scenePath.string()); } + const std::unique_ptr backend = CreateTestSceneBackend(); const EditorStartupSceneResult result = - EnsureEditorStartupScene(projectRoot.Root()); + backend->EnsureStartupScene(projectRoot.Root()); EXPECT_TRUE(result.ready); EXPECT_TRUE(result.loadedFromDisk); - ASSERT_NE(GetActiveEditorScene(), nullptr); - EXPECT_EQ(GetActiveEditorScene()->GetName(), "Main"); + ASSERT_NE(backend->GetActiveScene(), nullptr); + EXPECT_EQ(backend->GetActiveScene()->GetName(), "Main"); const HierarchyModel model = - HierarchyModel::BuildFromScene(GetActiveEditorScene()); + HierarchyModel::BuildFromScene(backend->GetActiveScene()); EXPECT_FALSE(model.Empty()); SceneManager& sceneManager = SceneManager::Get(); diff --git a/tests/UI/Editor/unit/test_inspector_presentation.cpp b/tests/UI/Editor/unit/test_inspector_presentation.cpp index 17fe82ea..f92e7648 100644 --- a/tests/UI/Editor/unit/test_inspector_presentation.cpp +++ b/tests/UI/Editor/unit/test_inspector_presentation.cpp @@ -3,6 +3,7 @@ #include "Inspector/InspectorPresentationModel.h" #include "Inspector/InspectorSubject.h" #include "Scene/EditorSceneRuntime.h" +#include "Scene/EngineEditorSceneBackend.h" #include #include @@ -87,6 +88,10 @@ void SaveMainScene(const TemporaryProjectRoot& projectRoot) { scene.Save(scenePath.string()); } +void BindEngineSceneBackend(EditorSceneRuntime& runtime) { + runtime.SetBackend(CreateEngineEditorSceneBackend()); +} + const UIEditorPropertyGridSection* FindSection( const InspectorPresentationModel& model, std::string_view title) { @@ -126,6 +131,7 @@ const InspectorPresentationComponentBinding* FindBinding( TEST(InspectorPresentationModelTests, EmptySubjectBuildsDefaultEmptyState) { EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); const InspectorPresentationModel model = BuildInspectorPresentationModel( {}, @@ -150,6 +156,7 @@ TEST(InspectorPresentationModelTests, ProjectAssetSubjectBuildsIdentityAndLocati std::filesystem::path("D:/Xuanchi/Main/XCEngine/project/Assets/Materials/Test.mat"); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); const InspectorPresentationModel model = BuildInspectorPresentationModel( subject, @@ -190,6 +197,7 @@ TEST(InspectorPresentationModelTests, SceneObjectSubjectBuildsRegisteredComponen SaveMainScene(projectRoot); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); Scene* scene = runtime.GetActiveScene(); ASSERT_NE(scene, nullptr); @@ -261,6 +269,7 @@ TEST(InspectorPresentationModelTests, CameraSkyboxMaterialBuildsAssetField) { SaveMainScene(projectRoot); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); Scene* scene = runtime.GetActiveScene(); ASSERT_NE(scene, nullptr); diff --git a/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp b/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp index 4ead961a..c44b197c 100644 --- a/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp +++ b/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp @@ -1,4 +1,5 @@ #include "Scene/EditorSceneRuntime.h" +#include "Scene/EngineEditorSceneBackend.h" #include "Scene/SceneViewportController.h" #include "Inspector/InspectorSubject.h" #include "Viewport/SceneViewportRenderService.h" @@ -106,6 +107,10 @@ void SaveMainScene(const TemporaryProjectRoot& projectRoot, const Math::Vector3& scene.Save(scenePath.string()); } +void BindEngineSceneBackend(EditorSceneRuntime& runtime) { + runtime.SetBackend(CreateEngineEditorSceneBackend()); +} + UIInputEvent MakePointerEvent( UIInputEventType type, float x, @@ -207,6 +212,7 @@ TEST(SceneViewportRuntimeTests, ApplySceneViewportCameraInputUpdatesCameraTransf SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); auto* camera = runtime.GetSceneViewCamera(); @@ -233,6 +239,7 @@ TEST(SceneViewportRuntimeTests, FocusSceneSelectionRepositionsCameraAroundSelect SaveMainScene(projectRoot, Math::Vector3(12.0f, 3.0f, -8.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); Scene* scene = runtime.GetActiveScene(); ASSERT_NE(scene, nullptr); @@ -261,6 +268,7 @@ TEST(SceneViewportRuntimeTests, BuildSceneViewportRenderRequestIncludesSelectedO SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); Scene* scene = runtime.GetActiveScene(); ASSERT_NE(scene, nullptr); @@ -284,6 +292,7 @@ TEST(SceneViewportRuntimeTests, SelectedComponentsExposeTransformAndAttachedCame SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); Scene* scene = runtime.GetActiveScene(); ASSERT_NE(scene, nullptr); @@ -314,6 +323,7 @@ TEST(SceneViewportRuntimeTests, RemoveSelectedComponentDropsRemovableDescriptorB SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); Scene* scene = runtime.GetActiveScene(); ASSERT_NE(scene, nullptr); @@ -340,6 +350,7 @@ TEST(SceneViewportRuntimeTests, TransformSetterApisWriteLocalValuesOnSelectedTra SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); runtime.EnsureSceneSelection(); @@ -380,6 +391,7 @@ TEST(SceneViewportRuntimeTests, SelectionStampAdvancesOnSceneSelectionChanges) { SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); runtime.EnsureSceneSelection(); @@ -409,6 +421,7 @@ TEST(SceneViewportRuntimeTests, InspectorSelectionResolverFollowsUnifiedSelectio EditorSelectionService selectionService = {}; EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); runtime.BindSelectionService(&selectionService); runtime.EnsureSceneSelection(); @@ -476,6 +489,7 @@ TEST(SceneViewportRuntimeTests, RightMouseDragRotatesSceneCameraThroughViewportC SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); auto* camera = runtime.GetSceneViewCamera(); @@ -536,6 +550,7 @@ TEST(SceneViewportRuntimeTests, MoveRightInputMovesSceneCameraTowardPositiveCame SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); auto* camera = runtime.GetSceneViewCamera(); @@ -560,6 +575,7 @@ TEST(SceneViewportRuntimeTests, MiddleMouseDragPansSceneCameraWithGrabSemantics) SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); auto* camera = runtime.GetSceneViewCamera(); @@ -618,6 +634,7 @@ TEST(SceneViewportRuntimeTests, ViewToolLeftMouseDragPansSceneCameraWithGrabSema SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); runtime.SetToolMode(SceneToolMode::View); @@ -677,6 +694,7 @@ TEST(SceneViewportRuntimeTests, MouseWheelUsesSingleNotchNormalizationForSceneZo SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); const float beforeDistance = runtime.BuildSceneViewportRenderRequest().orbitDistance; @@ -723,6 +741,7 @@ TEST(SceneViewportRuntimeTests, ToolShortcutSwitchesFocusedSceneViewportIntoTran SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); runtime.SetToolMode(SceneToolMode::View); EXPECT_EQ(runtime.GetToolMode(), SceneToolMode::View); @@ -781,6 +800,7 @@ TEST(SceneViewportRuntimeTests, SceneToolOverlayClickSwitchesModeOnPointerDown) SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); runtime.SetToolMode(SceneToolMode::Translate); @@ -819,6 +839,7 @@ TEST(SceneViewportRuntimeTests, SceneToolOverlayIncludesTransformButtonAndSwitch SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); runtime.SetToolMode(SceneToolMode::Translate); @@ -860,6 +881,7 @@ TEST(SceneViewportRuntimeTests, SceneToolOverlayHandlesCoalescedClickInSingleFra SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); runtime.SetToolMode(SceneToolMode::Translate);