From b67af931de20225e8901cc5c9117847a78d9bde2 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 28 Apr 2026 17:53:36 +0800 Subject: [PATCH] Tighten editor scene mutation boundary --- editor/AGENTS.md | 8 ++- editor/app/Core/Scene/EditorSceneBackend.h | 37 +++++++--- .../app/Features/Hierarchy/HierarchyModel.cpp | 28 ++++++++ .../app/Features/Hierarchy/HierarchyModel.h | 5 +- .../app/Features/Hierarchy/HierarchyPanel.cpp | 8 +-- .../Scene/SceneViewportTransformGizmo.cpp | 26 ++++++- .../SceneViewportTransformGizmoSupport.cpp | 53 +++++++------- .../SceneViewportTransformGizmoSupport.h | 13 +++- .../Services/Engine/EngineEditorServices.cpp | 61 +++++++++++++++- .../app/Services/Scene/EditorSceneRuntime.cpp | 69 ++++++++++++++----- .../app/Services/Scene/EditorSceneRuntime.h | 9 +++ .../test_editor_scene_runtime_backend.cpp | 35 ++++++++++ 12 files changed, 290 insertions(+), 62 deletions(-) diff --git a/editor/AGENTS.md b/editor/AGENTS.md index b77eb90a..83862a16 100644 --- a/editor/AGENTS.md +++ b/editor/AGENTS.md @@ -10,9 +10,9 @@ - `XCEditor` 是可选 Win32 + D3D12 应用壳。代码在 `app/Bootstrap`、`app/Host/Win32`、`app/Host/D3D12`,负责窗口、DPI、消息分发、swapchain、UI 纹理和截图。 - 产品装配应以 `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/project 的用户操作应通过 runtime 或 command route 进入,不要在 draw/append 阶段直接改 scene 或文件系统。scene feature / panel 层不得直接 `SetPosition()`、`SetRotation()`、`SetLocalScale()`、`CreateComponent()` 或类似写入 engine object graph;交互预览也必须回到 `EditorSceneRuntime`,再由 backend/adapter 执行。 - engine 全局 runtime 访问权统一收口到 `app/Core/Engine/EditorEngineServices.h` 合约和 `app/Services/Engine/EngineEditorServices.*` 生产实现。`SceneManager::Get()`、`ResourceManager::Get()`、`RenderObjectIdRegistry::Get()` 这类入口只能在该生产 adapter 内出现。 -- scene 事实所有权走 `app/Core/Scene/EditorSceneBackend.h` 合约。`EditorSceneRuntime` 只消费显式 backend;`EngineEditorSceneBackend` 必须吃显式注入的 engine manager / resource manager,不要在 scene runtime、panel、composition 或 viewport pass 中重新触碰 engine 全局单例。 +- scene 事实所有权走 `app/Core/Scene/EditorSceneBackend.h` 合约。`EditorSceneRuntime` 只消费显式 backend;`EngineEditorSceneBackend` 必须吃显式注入的 engine manager / resource manager,不要在 scene runtime、panel、composition 或 viewport pass 中重新触碰 engine 全局单例。读模型优先经由 runtime/backend 快照派生,例如 hierarchy 使用 `EditorSceneHierarchySnapshot`,不要让面板从 `Scene*` 现场重建业务模型。 - 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。 @@ -34,6 +34,8 @@ - `ProjectPanel` 只消费由 `EditorContext` / `EditorPanelServices` 提供的 `EditorProjectRuntime`,不再拥有或初始化自己的 project runtime。测试也应显式创建 `EditorProjectRuntime` 并通过 `SetProjectRuntime()` 注入,不要把 `projectRoot` 传给 panel。 - `EditorEngineServices` 是 editor 接入 engine 全局 runtime 的唯一生产 adapter。`Application` 创建它并向 `EditorContext`、window shell runtime、viewport runtime 传递;下游只消费显式服务。 - `EditorSceneBackend` 是 scene document/backend 的显式边界;`EngineEditorSceneBackend` 由 `EditorEngineServices` 创建,并通过显式 `SceneManager&` / `ResourceManager&` 接入真实 engine runtime。`EditorSceneRuntime` 负责 startup scene 编排、editor scene camera、hierarchy selection、component list、transform edit history 和 scene tool state,但不直接访问 engine 单例。 +- `EditorSceneRuntime::BuildHierarchySnapshot()` 是 hierarchy 面板的 scene 读入口;`HierarchyModel` 从该快照构建 UI tree。不要重新把 hierarchy 绑定回 `Scene*`。 +- transform gizmo support 只负责几何计算和 hit/drag 状态;预览写入通过 `SceneGizmoUndoBridge` 调回 `EditorSceneRuntime::ApplyTransformTool*Preview()`。不要在 `SceneViewportTransformGizmoSupport.*` 里直接写 `GameObject` transform。 当前目录地图: @@ -129,6 +131,8 @@ ctest --test-dir build -C Debug -R "editor|xceditor" --output-on-failure - 已移除 `ProjectPanel` 自持 `EditorProjectRuntime` 的 fallback 路径;project runtime 事实源现在只来自 `EditorContext`,面板测试改为显式 runtime 注入。 - 已把 scene 的 engine 全局访问收敛到 `EngineEditorSceneBackend`,让 `EditorSceneRuntime` 通过 `EditorSceneBackend` 合约初始化、打开 scene 和执行 hierarchy mutation;新增 backend contract 单测覆盖无 backend 失败和 fake backend 注入。 - 已新增 `EditorEngineServices` 边界,把 `SceneManager::Get()`、`ResourceManager::Get()`、`RenderObjectIdRegistry::Get()` 从 `Application`、`EditorContext` 和 viewport shader/object-id 路径收口到 `app/Services/Engine/EngineEditorServices.*`;`EditorContext` 现在通过显式 engine service 创建 scene backend,并会在 scene runtime 初始化失败时返回失败。 +- 已把 hierarchy 面板的 scene 读取改为 `EditorSceneHierarchySnapshot` 快照路径,`HierarchyModel` 不再由 panel 直接拿 `Scene*` 构建;已把 inspector 添加组件动作收口到 `EditorSceneBackend::AddComponent()`,让 `ComponentFactoryRegistry::Get()` 留在 engine adapter 内。 +- 已把 scene transform gizmo 的拖拽预览写入从 feature support 层移到 `EditorSceneRuntime`,support 层不再直接 `SetPosition()` / `SetRotation()` / `SetLocalScale()`。 - 本次改动验证过: - `cmake --build build --config Debug --target XCEditor` - `cmake --build build --config Debug --target editor_app_core_tests` diff --git a/editor/app/Core/Scene/EditorSceneBackend.h b/editor/app/Core/Scene/EditorSceneBackend.h index aea468d9..2c15bd29 100644 --- a/editor/app/Core/Scene/EditorSceneBackend.h +++ b/editor/app/Core/Scene/EditorSceneBackend.h @@ -1,13 +1,13 @@ #pragma once -#include - #include +#include #include #include #include #include #include +#include namespace XCEngine::Components { @@ -25,6 +25,24 @@ struct EditorStartupSceneResult { std::string sceneName = {}; }; +using EditorSceneObjectId = std::uint64_t; + +inline constexpr EditorSceneObjectId kInvalidEditorSceneObjectId = 0u; + +struct EditorSceneHierarchyNode { + std::string itemId = {}; + std::string displayName = {}; + std::vector children = {}; +}; + +struct EditorSceneHierarchySnapshot { + std::vector roots = {}; + + [[nodiscard]] bool Empty() const { + return roots.empty(); + } +}; + class EditorSceneBackend { public: virtual ~EditorSceneBackend() = default; @@ -32,9 +50,13 @@ public: virtual EditorStartupSceneResult EnsureStartupScene( const std::filesystem::path& projectRoot) = 0; virtual ::XCEngine::Components::Scene* GetActiveScene() const = 0; + virtual EditorSceneHierarchySnapshot BuildHierarchySnapshot() const = 0; virtual bool OpenSceneAsset(const std::filesystem::path& scenePath) = 0; virtual ::XCEngine::Components::GameObject* FindGameObject( std::string_view itemId) const = 0; + virtual bool AddComponent( + std::string_view itemId, + std::string_view componentTypeName) = 0; virtual bool RenameGameObject( std::string_view itemId, std::string_view newName) = 0; @@ -53,27 +75,26 @@ public: }; inline std::string MakeEditorGameObjectItemId( - ::XCEngine::Components::GameObject::ID id) { - return id == ::XCEngine::Components::GameObject::INVALID_ID + EditorSceneObjectId id) { + return id == kInvalidEditorSceneObjectId ? std::string() : std::to_string(id); } -inline std::optional<::XCEngine::Components::GameObject::ID> +inline std::optional ParseEditorGameObjectItemId(std::string_view itemId) { if (itemId.empty()) { return std::nullopt; } - ::XCEngine::Components::GameObject::ID parsedId = - ::XCEngine::Components::GameObject::INVALID_ID; + EditorSceneObjectId parsedId = kInvalidEditorSceneObjectId; 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) { + parsedId == kInvalidEditorSceneObjectId) { return std::nullopt; } diff --git a/editor/app/Features/Hierarchy/HierarchyModel.cpp b/editor/app/Features/Hierarchy/HierarchyModel.cpp index db5da779..b93a3717 100644 --- a/editor/app/Features/Hierarchy/HierarchyModel.cpp +++ b/editor/app/Features/Hierarchy/HierarchyModel.cpp @@ -163,6 +163,20 @@ HierarchyNode BuildSceneNodeRecursive( return node; } +HierarchyNode BuildSnapshotNodeRecursive( + const EditorSceneHierarchyNode& snapshotNode) { + HierarchyNode node = {}; + node.nodeId = snapshotNode.itemId; + node.label = snapshotNode.displayName.empty() + ? std::string("GameObject") + : snapshotNode.displayName; + node.children.reserve(snapshotNode.children.size()); + for (const EditorSceneHierarchyNode& child : snapshotNode.children) { + node.children.push_back(BuildSnapshotNodeRecursive(child)); + } + return node; +} + } // namespace HierarchyModel HierarchyModel::BuildFromScene( @@ -184,6 +198,20 @@ HierarchyModel HierarchyModel::BuildFromScene( return model; } +HierarchyModel HierarchyModel::BuildFromSnapshot( + const EditorSceneHierarchySnapshot& snapshot) { + HierarchyModel model = {}; + model.m_roots.reserve(snapshot.roots.size()); + for (const EditorSceneHierarchyNode& root : snapshot.roots) { + if (root.itemId.empty()) { + continue; + } + + model.m_roots.push_back(BuildSnapshotNodeRecursive(root)); + } + return model; +} + bool HierarchyModel::Empty() const { return m_roots.empty(); } diff --git a/editor/app/Features/Hierarchy/HierarchyModel.h b/editor/app/Features/Hierarchy/HierarchyModel.h index d8b6afa5..f2e366cb 100644 --- a/editor/app/Features/Hierarchy/HierarchyModel.h +++ b/editor/app/Features/Hierarchy/HierarchyModel.h @@ -1,7 +1,8 @@ #pragma once -#include +#include "Scene/EditorSceneBackend.h" +#include #include #include @@ -29,6 +30,8 @@ struct HierarchyNode { class HierarchyModel { public: static HierarchyModel BuildFromScene(const ::XCEngine::Components::Scene* scene); + static HierarchyModel BuildFromSnapshot( + const EditorSceneHierarchySnapshot& snapshot); bool Empty() const; bool HasSameTree(const HierarchyModel& other) const; diff --git a/editor/app/Features/Hierarchy/HierarchyPanel.cpp b/editor/app/Features/Hierarchy/HierarchyPanel.cpp index 2bb80b36..a7c91d5f 100644 --- a/editor/app/Features/Hierarchy/HierarchyPanel.cpp +++ b/editor/app/Features/Hierarchy/HierarchyPanel.cpp @@ -111,10 +111,10 @@ void HierarchyPanel::SyncModelFromScene() { } const HierarchyModel sceneModel = - HierarchyModel::BuildFromScene( - m_sceneRuntime != nullptr - ? m_sceneRuntime->GetActiveScene() - : nullptr); + m_sceneRuntime != nullptr + ? HierarchyModel::BuildFromSnapshot( + m_sceneRuntime->BuildHierarchySnapshot()) + : HierarchyModel{}; if (!m_model.HasSameTree(sceneModel) || m_treeItems.empty()) { m_model = sceneModel; RebuildItems(); diff --git a/editor/app/Features/Scene/SceneViewportTransformGizmo.cpp b/editor/app/Features/Scene/SceneViewportTransformGizmo.cpp index bd811d44..64e42c18 100644 --- a/editor/app/Features/Scene/SceneViewportTransformGizmo.cpp +++ b/editor/app/Features/Scene/SceneViewportTransformGizmo.cpp @@ -282,6 +282,26 @@ public: return m_beforeSnapshot.IsValid(); } + bool ApplyWorldTransformPreview( + std::uint64_t entityId, + const Vector3& position, + const Quaternion& rotation) override { + return m_sceneRuntime != nullptr && + m_sceneRuntime->ApplyTransformToolWorldPreview( + entityId, + position, + rotation); + } + + bool ApplyLocalScalePreview( + std::uint64_t entityId, + const Vector3& localScale) override { + return m_sceneRuntime != nullptr && + m_sceneRuntime->ApplyTransformToolLocalScalePreview( + entityId, + localScale); + } + void FinalizeInteractiveChange() override { if (m_sceneRuntime == nullptr || !HasPendingInteractiveChange()) { return; @@ -536,13 +556,13 @@ bool SceneViewportTransformGizmo::UpdateDrag(EditorSceneRuntime& sceneRuntime) { state.rotateGizmo, state.scaleGizmo)) { case ActiveTransformGizmoKind::Move: - state.moveGizmo.UpdateDrag(state.moveContext); + state.moveGizmo.UpdateDrag(state.moveContext, state.undoBridge); return true; case ActiveTransformGizmoKind::Rotate: - state.rotateGizmo.UpdateDrag(state.rotateContext); + state.rotateGizmo.UpdateDrag(state.rotateContext, state.undoBridge); return true; case ActiveTransformGizmoKind::Scale: - state.scaleGizmo.UpdateDrag(state.scaleContext); + state.scaleGizmo.UpdateDrag(state.scaleContext, state.undoBridge); return true; case ActiveTransformGizmoKind::None: default: diff --git a/editor/app/Features/Scene/SceneViewportTransformGizmoSupport.cpp b/editor/app/Features/Scene/SceneViewportTransformGizmoSupport.cpp index 358feb14..4c007415 100644 --- a/editor/app/Features/Scene/SceneViewportTransformGizmoSupport.cpp +++ b/editor/app/Features/Scene/SceneViewportTransformGizmoSupport.cpp @@ -1126,7 +1126,9 @@ bool SceneViewportMoveGizmo::TryBeginDrag( return true; } -void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& context) { +void SceneViewportMoveGizmo::UpdateDrag( + const SceneViewportMoveGizmoContext& context, + IUndoManager& undoManager) { if (m_dragMode == DragMode::None || context.selectedObject == nullptr || context.selectedObject->GetID() != m_activeEntityId || @@ -1160,8 +1162,10 @@ void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& con m_dragObjects[index]->GetTransform() == nullptr) { continue; } - m_dragObjects[index]->GetTransform()->SetPosition( - m_dragStartObjectWorldPositions[index] + worldDelta); + undoManager.ApplyWorldTransformPreview( + m_dragObjects[index]->GetID(), + m_dragStartObjectWorldPositions[index] + worldDelta, + m_dragObjects[index]->GetTransform()->GetRotation()); } return; } @@ -1178,8 +1182,10 @@ void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& con m_dragObjects[index]->GetTransform() == nullptr) { continue; } - m_dragObjects[index]->GetTransform()->SetPosition( - m_dragStartObjectWorldPositions[index] + worldDelta); + undoManager.ApplyWorldTransformPreview( + m_dragObjects[index]->GetID(), + m_dragStartObjectWorldPositions[index] + worldDelta, + m_dragObjects[index]->GetTransform()->GetRotation()); } } @@ -1557,7 +1563,8 @@ bool SceneViewportRotateGizmo::TryBeginDrag( } void SceneViewportRotateGizmo::UpdateDrag( - const SceneViewportRotateGizmoContext& context) { + const SceneViewportRotateGizmoContext& context, + IUndoManager& undoManager) { if (m_activeAxis == SceneViewportRotateGizmoAxis::None || context.selectedObject == nullptr || context.selectedObject->GetID() != m_activeEntityId || @@ -1622,22 +1629,21 @@ void SceneViewportRotateGizmo::UpdateDrag( continue; } - if (m_rotateAroundSharedPivot) { - gameObject->GetTransform()->SetPosition( - m_dragStartPivotWorldPosition + - worldDeltaRotation * - (m_dragStartWorldPositions[index] - m_dragStartPivotWorldPosition)); - } else { - gameObject->GetTransform()->SetPosition(m_dragStartWorldPositions[index]); - } + const Vector3 previewPosition = + m_rotateAroundSharedPivot + ? m_dragStartPivotWorldPosition + + worldDeltaRotation * + (m_dragStartWorldPositions[index] - m_dragStartPivotWorldPosition) + : m_dragStartWorldPositions[index]; - if (m_localSpace && m_activeAxis != SceneViewportRotateGizmoAxis::View) { - gameObject->GetTransform()->SetRotation( - m_dragStartWorldRotations[index] * localDeltaRotation); - } else { - gameObject->GetTransform()->SetRotation( - worldDeltaRotation * m_dragStartWorldRotations[index]); - } + const Quaternion previewRotation = + m_localSpace && m_activeAxis != SceneViewportRotateGizmoAxis::View + ? m_dragStartWorldRotations[index] * localDeltaRotation + : worldDeltaRotation * m_dragStartWorldRotations[index]; + undoManager.ApplyWorldTransformPreview( + gameObject->GetID(), + previewPosition, + previewRotation); } SceneViewportRotateGizmoContext drawContext = context; @@ -2091,7 +2097,8 @@ bool SceneViewportScaleGizmo::TryBeginDrag( } void SceneViewportScaleGizmo::UpdateDrag( - const SceneViewportScaleGizmoContext& context) { + const SceneViewportScaleGizmoContext& context, + IUndoManager& undoManager) { if (m_activeHandle == SceneViewportScaleGizmoHandle::None || context.selectedObject == nullptr || context.selectedObject->GetTransform() == nullptr || @@ -2137,7 +2144,7 @@ void SceneViewportScaleGizmo::UpdateDrag( } } - context.selectedObject->GetTransform()->SetLocalScale(localScale); + undoManager.ApplyLocalScalePreview(context.selectedObject->GetID(), localScale); switch (m_activeHandle) { case SceneViewportScaleGizmoHandle::X: m_dragCurrentVisualScale = Vector3( diff --git a/editor/app/Features/Scene/SceneViewportTransformGizmoSupport.h b/editor/app/Features/Scene/SceneViewportTransformGizmoSupport.h index 7c887e15..1304edee 100644 --- a/editor/app/Features/Scene/SceneViewportTransformGizmoSupport.h +++ b/editor/app/Features/Scene/SceneViewportTransformGizmoSupport.h @@ -28,6 +28,13 @@ public: virtual void BeginInteractiveChange(const std::string& label) = 0; virtual bool HasPendingInteractiveChange() const = 0; + virtual bool ApplyWorldTransformPreview( + std::uint64_t entityId, + const Math::Vector3& position, + const Math::Quaternion& rotation) = 0; + virtual bool ApplyLocalScalePreview( + std::uint64_t entityId, + const Math::Vector3& localScale) = 0; virtual void FinalizeInteractiveChange() = 0; virtual void CancelInteractiveChange() = 0; }; @@ -181,7 +188,7 @@ class SceneViewportMoveGizmo { public: void Update(const SceneViewportMoveGizmoContext& context); bool TryBeginDrag(const SceneViewportMoveGizmoContext& context, IUndoManager& undoManager); - void UpdateDrag(const SceneViewportMoveGizmoContext& context); + void UpdateDrag(const SceneViewportMoveGizmoContext& context, IUndoManager& undoManager); void EndDrag(IUndoManager& undoManager); void CancelDrag(IUndoManager* undoManager = nullptr); @@ -292,7 +299,7 @@ class SceneViewportRotateGizmo { public: void Update(const SceneViewportRotateGizmoContext& context); bool TryBeginDrag(const SceneViewportRotateGizmoContext& context, IUndoManager& undoManager); - void UpdateDrag(const SceneViewportRotateGizmoContext& context); + void UpdateDrag(const SceneViewportRotateGizmoContext& context, IUndoManager& undoManager); void EndDrag(IUndoManager& undoManager); void CancelDrag(IUndoManager* undoManager = nullptr); @@ -388,7 +395,7 @@ class SceneViewportScaleGizmo { public: void Update(const SceneViewportScaleGizmoContext& context); bool TryBeginDrag(const SceneViewportScaleGizmoContext& context, IUndoManager& undoManager); - void UpdateDrag(const SceneViewportScaleGizmoContext& context); + void UpdateDrag(const SceneViewportScaleGizmoContext& context, IUndoManager& undoManager); void EndDrag(IUndoManager& undoManager); void CancelDrag(IUndoManager* undoManager = nullptr); diff --git a/editor/app/Services/Engine/EngineEditorServices.cpp b/editor/app/Services/Engine/EngineEditorServices.cpp index 140eab87..57df6df2 100644 --- a/editor/app/Services/Engine/EngineEditorServices.cpp +++ b/editor/app/Services/Engine/EngineEditorServices.cpp @@ -177,7 +177,7 @@ GameObject* FindGameObjectByItemId( SceneManager& sceneManager, std::string_view itemId) { Scene* scene = ResolvePrimaryScene(sceneManager); - const std::optional gameObjectId = + const std::optional gameObjectId = ParseEditorGameObjectItemId(itemId); if (scene == nullptr || !gameObjectId.has_value()) { return nullptr; @@ -186,6 +186,27 @@ GameObject* FindGameObjectByItemId( return scene->FindByID(gameObjectId.value()); } +EditorSceneHierarchyNode BuildHierarchySnapshotNodeRecursive( + const GameObject& gameObject) { + EditorSceneHierarchyNode node = {}; + node.itemId = MakeEditorGameObjectItemId(gameObject.GetID()); + node.displayName = gameObject.GetName().empty() + ? std::string("GameObject") + : gameObject.GetName(); + node.children.reserve(gameObject.GetChildCount()); + for (std::size_t childIndex = 0u; + childIndex < gameObject.GetChildCount(); + ++childIndex) { + const GameObject* child = gameObject.GetChild(childIndex); + if (child == nullptr) { + continue; + } + + node.children.push_back(BuildHierarchySnapshotNodeRecursive(*child)); + } + return node; +} + bool MoveGameObjectRelativeToTarget( SceneManager& sceneManager, std::string_view itemId, @@ -313,6 +334,25 @@ public: return ResolvePrimaryScene(m_sceneManager); } + EditorSceneHierarchySnapshot BuildHierarchySnapshot() const override { + EditorSceneHierarchySnapshot snapshot = {}; + Scene* scene = ResolvePrimaryScene(m_sceneManager); + if (scene == nullptr) { + return snapshot; + } + + const std::vector roots = scene->GetRootGameObjects(); + snapshot.roots.reserve(roots.size()); + for (const GameObject* root : roots) { + if (root == nullptr) { + continue; + } + + snapshot.roots.push_back(BuildHierarchySnapshotNodeRecursive(*root)); + } + return snapshot; + } + bool OpenSceneAsset(const std::filesystem::path& scenePath) override { if (scenePath.empty()) { return false; @@ -344,6 +384,25 @@ public: return FindGameObjectByItemId(m_sceneManager, itemId); } + bool AddComponent( + std::string_view itemId, + std::string_view componentTypeName) override { + if (componentTypeName.empty()) { + return false; + } + + GameObject* gameObject = FindGameObject(itemId); + if (gameObject == nullptr) { + return false; + } + + Component* addedComponent = + ComponentFactoryRegistry::Get().CreateComponent( + gameObject, + std::string(componentTypeName)); + return addedComponent != nullptr; + } + bool RenameGameObject( std::string_view itemId, std::string_view newName) override { diff --git a/editor/app/Services/Scene/EditorSceneRuntime.cpp b/editor/app/Services/Scene/EditorSceneRuntime.cpp index 1bf447f7..bcd10ff4 100644 --- a/editor/app/Services/Scene/EditorSceneRuntime.cpp +++ b/editor/app/Services/Scene/EditorSceneRuntime.cpp @@ -1,6 +1,5 @@ #include "Scene/EditorSceneRuntime.h" -#include #include #include #include @@ -236,6 +235,12 @@ Scene* EditorSceneRuntime::GetActiveScene() const { return m_backend != nullptr ? m_backend->GetActiveScene() : nullptr; } +EditorSceneHierarchySnapshot EditorSceneRuntime::BuildHierarchySnapshot() const { + return m_backend != nullptr + ? m_backend->BuildHierarchySnapshot() + : EditorSceneHierarchySnapshot{}; +} + ::XCEngine::Components::CameraComponent* EditorSceneRuntime::GetSceneViewCamera() { return EnsureSceneViewCamera() ? m_sceneViewCamera.camera : nullptr; } @@ -525,22 +530,10 @@ bool EditorSceneRuntime::AddComponentToSelectedGameObject( return false; } - const std::optional selectedId = GetSelectedGameObjectId(); - Scene* scene = GetActiveScene(); - if (!selectedId.has_value() || scene == nullptr) { - return false; - } - - GameObject* gameObject = scene->FindByID(selectedId.value()); - if (gameObject == nullptr) { - return false; - } - - Component* addedComponent = - ::XCEngine::Components::ComponentFactoryRegistry::Get().CreateComponent( - gameObject, - std::string(componentTypeName)); - if (addedComponent == nullptr) { + const std::string selectedItemId = GetSelectedItemId(); + if (selectedItemId.empty() || + m_backend == nullptr || + !m_backend->AddComponent(selectedItemId, componentTypeName)) { return false; } @@ -866,6 +859,48 @@ bool EditorSceneRuntime::ApplyTransformToolPreview( return ApplyTransformSnapshot(snapshot); } +bool EditorSceneRuntime::ApplyTransformToolWorldPreview( + EditorSceneObjectId targetId, + const Vector3& position, + const Quaternion& rotation) { + if (!m_toolState.dragState.active || + targetId == kInvalidEditorSceneObjectId || + targetId != m_toolState.dragState.initialTransform.targetId) { + return false; + } + + SceneTransformSnapshot snapshot = m_toolState.dragState.initialTransform; + snapshot.targetId = targetId; + snapshot.position = position; + snapshot.rotation = rotation; + snapshot.valid = true; + return ApplyTransformSnapshot(snapshot); +} + +bool EditorSceneRuntime::ApplyTransformToolLocalScalePreview( + EditorSceneObjectId targetId, + const Vector3& localScale) { + if (!m_toolState.dragState.active || + targetId == kInvalidEditorSceneObjectId || + targetId != m_toolState.dragState.initialTransform.targetId) { + return false; + } + + Scene* scene = GetActiveScene(); + if (scene == nullptr) { + return false; + } + + GameObject* gameObject = scene->FindByID(targetId); + if (gameObject == nullptr || gameObject->GetTransform() == nullptr) { + return false; + } + + gameObject->GetTransform()->SetLocalScale(localScale); + IncrementInspectorRevision(); + return true; +} + bool EditorSceneRuntime::CommitTransformToolDrag() { if (!m_toolState.dragState.active) { return false; diff --git a/editor/app/Services/Scene/EditorSceneRuntime.h b/editor/app/Services/Scene/EditorSceneRuntime.h index 303da994..a4fe2474 100644 --- a/editor/app/Services/Scene/EditorSceneRuntime.h +++ b/editor/app/Services/Scene/EditorSceneRuntime.h @@ -27,6 +27,7 @@ class Scene; namespace XCEngine::Math { +struct Quaternion; struct Vector3; } // namespace XCEngine::Math @@ -64,6 +65,7 @@ public: const EditorStartupSceneResult& GetStartupResult() const; ::XCEngine::Components::Scene* GetActiveScene() const; + EditorSceneHierarchySnapshot BuildHierarchySnapshot() const; ::XCEngine::Components::CameraComponent* GetSceneViewCamera(); const ::XCEngine::Components::CameraComponent* GetSceneViewCamera() const; float GetSceneViewOrbitDistance() const; @@ -140,6 +142,13 @@ public: bool HasActiveTransformToolDrag() const; const SceneToolDragState* GetActiveTransformToolDrag() const; bool ApplyTransformToolPreview(const SceneTransformSnapshot& snapshot); + bool ApplyTransformToolWorldPreview( + EditorSceneObjectId targetId, + const ::XCEngine::Math::Vector3& position, + const ::XCEngine::Math::Quaternion& rotation); + bool ApplyTransformToolLocalScalePreview( + EditorSceneObjectId targetId, + const ::XCEngine::Math::Vector3& localScale); bool CommitTransformToolDrag(); void CancelTransformToolDrag(); 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 58c26d5d..8cf7362e 100644 --- a/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp +++ b/tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp @@ -25,6 +25,10 @@ public: return activeScene; } + EditorSceneHierarchySnapshot BuildHierarchySnapshot() const override { + return hierarchySnapshot; + } + bool OpenSceneAsset(const std::filesystem::path& scenePath) override { lastOpenedScenePath = scenePath; return openSceneResult; @@ -35,6 +39,14 @@ public: return foundGameObject; } + bool AddComponent( + std::string_view itemId, + std::string_view componentTypeName) override { + lastAddComponentItemId = std::string(itemId); + lastAddComponentTypeName = std::string(componentTypeName); + return addComponentResult; + } + bool RenameGameObject( std::string_view, std::string_view) override { @@ -72,13 +84,17 @@ public: } EditorStartupSceneResult startupSceneResult = {}; + EditorSceneHierarchySnapshot hierarchySnapshot = {}; Scene* activeScene = nullptr; GameObject* foundGameObject = nullptr; bool openSceneResult = false; + bool addComponentResult = false; int ensureStartupSceneCallCount = 0; std::filesystem::path lastProjectRoot = {}; std::filesystem::path lastOpenedScenePath = {}; mutable std::string lastFindItemId = {}; + std::string lastAddComponentItemId = {}; + std::string lastAddComponentTypeName = {}; }; TEST(EditorSceneRuntimeBackendTests, InitializeFailsWithoutBoundBackend) { @@ -120,5 +136,24 @@ TEST(EditorSceneRuntimeBackendTests, FindGameObjectUsesBoundBackend) { EXPECT_EQ(backendPtr->lastFindItemId, "42"); } +TEST(EditorSceneRuntimeBackendTests, BuildHierarchySnapshotUsesBoundBackend) { + auto backend = std::make_unique(); + backend->startupSceneResult.ready = true; + backend->hierarchySnapshot.roots.push_back(EditorSceneHierarchyNode{ + .itemId = "42", + .displayName = "Probe", + .children = {} + }); + + EditorSceneRuntime runtime = {}; + runtime.SetBackend(std::move(backend)); + ASSERT_TRUE(runtime.Initialize("D:/Xuanchi/Main/XCEngine/project")); + + const EditorSceneHierarchySnapshot snapshot = runtime.BuildHierarchySnapshot(); + ASSERT_EQ(snapshot.roots.size(), 1u); + EXPECT_EQ(snapshot.roots[0].itemId, "42"); + EXPECT_EQ(snapshot.roots[0].displayName, "Probe"); +} + } // namespace } // namespace XCEngine::UI::Editor::App