diff --git a/docs/api/XCEngine/Editor/Core/Core.md b/docs/api/XCEngine/Editor/Core/Core.md index 1a3f7c4f..5df3c2be 100644 --- a/docs/api/XCEngine/Editor/Core/Core.md +++ b/docs/api/XCEngine/Editor/Core/Core.md @@ -19,12 +19,15 @@ - [EditorActionRoute](EditorActionRoute/EditorActionRoute.md) - [EditorConsoleSink](EditorConsoleSink/EditorConsoleSink.md) - [EditorLoggingSetup](EditorLoggingSetup/EditorLoggingSetup.md) +- [EditorRuntimeMode](EditorRuntimeMode/EditorRuntimeMode.md) - [EditorWindowTitle](EditorWindowTitle/EditorWindowTitle.md) - [EditorWorkspace](EditorWorkspace/EditorWorkspace.md) - [IProjectManager](IProjectManager/IProjectManager.md) - [ISceneManager](ISceneManager/ISceneManager.md) - [ISelectionManager](ISelectionManager/ISelectionManager.md) - [IUndoManager](IUndoManager/IUndoManager.md) +- [PlaySessionController](PlaySessionController/PlaySessionController.md) +- [ProjectRootResolver](ProjectRootResolver/ProjectRootResolver.md) - [SceneSnapshot](SceneSnapshot/SceneSnapshot.md) - [SelectionManager](SelectionManager/SelectionManager.md) - [UndoManager](UndoManager/UndoManager.md) @@ -41,9 +44,10 @@ 当前还可以看到一个很清晰的工程化方向: - `EditorActionRoute` 负责动作路由语义 +- `EditorRuntimeMode + PlaySessionController` 负责编辑态 / 运行态切换和 play mode 主循环 - `SceneSnapshot + UndoManager` 负责状态回滚 - `EditorConsoleSink + EditorLoggingSetup` 负责把引擎日志接入编辑器壳层 -- `EditorWindowTitle` 负责把底层状态翻译成可见的主窗口反馈 +- `EditorWindowTitle + ProjectRootResolver` 负责把启动路径和底层状态翻译成可见的宿主行为 ## 相关文档 diff --git a/docs/api/XCEngine/Editor/Core/EditorActionRoute/EditorActionRoute.md b/docs/api/XCEngine/Editor/Core/EditorActionRoute/EditorActionRoute.md index 433a0422..a72330a8 100644 --- a/docs/api/XCEngine/Editor/Core/EditorActionRoute/EditorActionRoute.md +++ b/docs/api/XCEngine/Editor/Core/EditorActionRoute/EditorActionRoute.md @@ -10,30 +10,75 @@ ## 概述 -`EditorActionRoute` 是 `ActionRouting` 和 `EditActionRouter` 之间的最小契约。 +`EditorActionRoute` 是 [ActionRouting](../../Actions/ActionRouting/ActionRouting.md) 和 `EditActionRouter` 之间的最小契约。 + 当前枚举值只有三个: - `None` - `Hierarchy` - `Project` -它本质上回答的是一个问题:当用户按下 `Delete`、`Ctrl+C` 或 `Ctrl+V` 时,编辑器到底应该操作实体树,还是资产浏览器,还是暂时不响应。 +它回答的问题很直接: + +- 当前 `Delete`、`Ctrl+C`、`Ctrl+V` 应该作用到实体树,还是资源浏览器,还是暂时不响应? + +## 当前语义 + +### `None` + +表示当前没有显式编辑目标。 + +典型情况包括: + +- 某个不接管全局编辑动作的面板获得焦点 +- `InspectorPanel` 或 `ConsolePanel` 调用 `ObserveInactiveActionRoute(...)` + +### `Hierarchy` + +表示全局编辑动作当前应当解释为“面向场景实体”的动作。 + +典型行为包括: + +- 删除实体 +- 复制 / 粘贴实体 +- 重命名实体 + +### `Project` + +表示全局编辑动作当前应当解释为“面向 Project Browser 当前选中资源”的动作。 + +当前它至少已经参与: + +- 资源删除 +- 资源选择驱动的 Inspector subject 切换 +- Project 面板焦点语义 + +这里需要特别强调一点: + +- 虽然没有单独的 `Inspector` 或 `MaterialAsset` route, +- 但 Project route 已经会间接驱动资源检查器路径。 + +所以旧文档里“资源检查器还没有路由值”这种说法,如果被理解成“Project route 无法覆盖资源检查流程”,就不准确了。 ## 设计说明 -把路由显式枚举出来,而不是靠每个面板自己判断快捷键,有几个直接收益: +把编辑目标显式枚举出来,而不是让每个面板自己判断快捷键,有几个直接收益: -- 全局 Edit 菜单可以统一生成启用状态 -- 快捷键处理逻辑可以收口到 `EditActionRouter` -- 不同面板之间的编辑语义冲突会更容易排查 +- 全局 Edit 菜单可以统一计算启用状态 +- 快捷键处理可以收口到 `EditActionRouter` +- 不同面板之间的语义冲突更容易排查 + +这也是当前 editor 能同时支持 Hierarchy 和 Project 两套上下文敏感编辑动作的基础。 ## 当前限制 -- 当前只覆盖 `Hierarchy` 和 `Project` -- `Inspector`、`Console`、未来的材质编辑器或资源检查器都还没有自己的路由值 +- 当前只定义了 `Hierarchy`、`Project` 和 `None`。 +- `Inspector` 本身不拥有独立 route,而是更多作为当前 route 的观察者和结果呈现者。 +- 没有更细粒度的资源子类型 route,也没有多窗口冲突仲裁。 ## 相关文档 - [Core](../Core.md) - [ActionRouting](../../Actions/ActionRouting/ActionRouting.md) - [EditActionRouter](../../Actions/EditActionRouter/EditActionRouter.md) +- [InspectorPanel](../../panels/InspectorPanel/InspectorPanel.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorContext/Constructor-And-Lifetime.md b/docs/api/XCEngine/Editor/Core/EditorContext/Constructor-And-Lifetime.md new file mode 100644 index 00000000..119e38d9 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorContext/Constructor-And-Lifetime.md @@ -0,0 +1,55 @@ +# EditorContext Constructor And Lifetime + +**命名空间**: `XCEngine::Editor` + +**类型**: `constructor and destructor` + +**源文件**: `editor/src/Core/EditorContext.h` + +## 签名 + +```cpp +EditorContext(); +~EditorContext(); +``` + +## 作用 + +构造并装配当前默认的 Editor 服务集合,同时在销毁时回收内部事件订阅。 + +## 当前实现行为 + +### 构造函数 + +- 会按固定顺序创建: + - `EventBus` + - `SelectionManager` + - `SceneManager` + - `UndoManager` + - `ProjectManager` +- `UndoManager` 当前依赖 `SceneManager` 与 `SelectionManager`。 +- 构造完成后,会订阅 `EntityDeletedEvent`。 +- 当前订阅逻辑是: + - 如果被删实体刚好等于当前主选中实体 + - 就调用 `m_selectionManager->ClearSelection()` + +### 析构函数 + +- 会按保存下来的 `m_entityDeletedHandlerId` 调用: + +```cpp +m_eventBus->Unsubscribe(m_entityDeletedHandlerId); +``` + +- 其余服务对象由 `std::unique_ptr` 自动释放。 + +## 当前实现边界 + +- 当前所有核心服务都在构造时立即实例化,不做懒加载。 +- “删除实体时自动清理选择”这条规则直接写在构造函数里,不是独立策略对象。 + +## 相关文档 + +- [EditorContext](EditorContext.md) +- [Service Accessors](Service-Accessors.md) +- [Runtime Mode And Project Path](Runtime-Mode-And-Project-Path.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorContext/EditorContext.md b/docs/api/XCEngine/Editor/Core/EditorContext/EditorContext.md index dcc2de10..5f0ababb 100644 --- a/docs/api/XCEngine/Editor/Core/EditorContext/EditorContext.md +++ b/docs/api/XCEngine/Editor/Core/EditorContext/EditorContext.md @@ -29,6 +29,24 @@ - 通过 `EditorActionRoute` 记录当前激活的动作路由。 - 同时保存项目路径字符串。 +## 公开 API + +### 构造与生命周期 + +- [Constructor And Lifetime](Constructor-And-Lifetime.md) + +### 核心服务访问 + +- [Service Accessors](Service-Accessors.md) + +### 视口与动作路由 + +- [Viewport And Action Route](Viewport-And-Action-Route.md) + +### 运行模式与项目路径 + +- [Runtime Mode And Project Path](Runtime-Mode-And-Project-Path.md) + ## 当前实现边界 - 当前所有服务都在构造时固定实例化,不是懒加载。 diff --git a/docs/api/XCEngine/Editor/Core/EditorContext/Runtime-Mode-And-Project-Path.md b/docs/api/XCEngine/Editor/Core/EditorContext/Runtime-Mode-And-Project-Path.md new file mode 100644 index 00000000..f4348264 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorContext/Runtime-Mode-And-Project-Path.md @@ -0,0 +1,53 @@ +# EditorContext Runtime Mode And Project Path + +**命名空间**: `XCEngine::Editor` + +**类型**: `method group` + +**源文件**: `editor/src/Core/EditorContext.h` + +## 签名 + +```cpp +void SetRuntimeMode(EditorRuntimeMode mode) override; +EditorRuntimeMode GetRuntimeMode() const override; +void SetProjectPath(const std::string& path) override; +const std::string& GetProjectPath() const override; +``` + +## 作用 + +维护当前 Editor 的运行模式和项目根路径元数据。 + +## 当前实现行为 + +### `SetRuntimeMode(mode)` + +- 若传入模式与当前 `m_runtimeMode` 相同,直接返回。 +- 否则会: + - 先保存旧模式 + - 更新 `m_runtimeMode` + - 通过 `EventBus` 发布 `EditorModeChangedEvent{ oldMode, newMode }` + +### `GetRuntimeMode()` + +- 返回当前缓存的运行模式,默认初值为 `EditorRuntimeMode::Edit`。 + +### `SetProjectPath(path)` + +- 直接用传入字符串覆盖 `m_projectPath`。 + +### `GetProjectPath()` + +- 返回当前缓存的项目路径字符串引用。 + +## 设计含义 + +- 运行模式切换是当前 `EditorContext` 少数带副作用的 setter 之一,因为它会广播 `EditorModeChangedEvent`。 +- 项目路径当前只是简单元数据,不会在设置时自动触发项目初始化。 + +## 相关文档 + +- [EditorContext](EditorContext.md) +- [Constructor And Lifetime](Constructor-And-Lifetime.md) +- [EditorRuntimeMode](../EditorRuntimeMode/EditorRuntimeMode.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorContext/Service-Accessors.md b/docs/api/XCEngine/Editor/Core/EditorContext/Service-Accessors.md new file mode 100644 index 00000000..bf77574a --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorContext/Service-Accessors.md @@ -0,0 +1,42 @@ +# EditorContext Service Accessors + +**命名空间**: `XCEngine::Editor` + +**类型**: `method group` + +**源文件**: `editor/src/Core/EditorContext.h` + +## 签名 + +```cpp +EventBus& GetEventBus() override; +ISelectionManager& GetSelectionManager() override; +ISceneManager& GetSceneManager() override; +IProjectManager& GetProjectManager() override; +IUndoManager& GetUndoManager() override; +``` + +## 作用 + +暴露 `EditorContext` 当前持有的核心服务对象。 + +## 当前实现行为 + +- 每个 getter 都直接返回内部 `std::unique_ptr` 指向的具体服务实例引用。 +- 当前默认绑定关系是: + - `GetEventBus()` -> `EventBus` + - `GetSelectionManager()` -> `SelectionManager` + - `GetSceneManager()` -> `SceneManager` + - `GetProjectManager()` -> `ProjectManager` + - `GetUndoManager()` -> `UndoManager` + +## 设计含义 + +- `EditorContext` 当前就是一个轻量 service hub。 +- 面板、命令和 action router 通常通过这些 getter 获取协作服务,而不是直接保存一组分散指针。 + +## 相关文档 + +- [EditorContext](EditorContext.md) +- [Constructor And Lifetime](Constructor-And-Lifetime.md) +- [EventBus](../EventBus/EventBus.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorContext/Viewport-And-Action-Route.md b/docs/api/XCEngine/Editor/Core/EditorContext/Viewport-And-Action-Route.md new file mode 100644 index 00000000..dc16ff4d --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorContext/Viewport-And-Action-Route.md @@ -0,0 +1,44 @@ +# EditorContext Viewport And Action Route + +**命名空间**: `XCEngine::Editor` + +**类型**: `method group` + +**源文件**: `editor/src/Core/EditorContext.h` + +## 签名 + +```cpp +IViewportHostService* GetViewportHostService() override; +void SetViewportHostService(IViewportHostService* viewportHostService); +void SetActiveActionRoute(EditorActionRoute route) override; +EditorActionRoute GetActiveActionRoute() const override; +``` + +## 作用 + +保存当前工作区绑定的视口宿主服务,并记录当前激活的动作路由。 + +## 当前实现行为 + +### 视口宿主服务 + +- `GetViewportHostService()` 直接返回当前缓存的裸指针。 +- `SetViewportHostService(...)` 只是简单覆盖 `m_viewportHostService`。 +- 当前 `EditorContext` 不拥有这个对象的生命周期。 + +### 动作路由 + +- `SetActiveActionRoute(route)` 直接把当前活动路由写入 `m_activeActionRoute`。 +- `GetActiveActionRoute()` 返回当前活动路由枚举。 + +## 当前实现边界 + +- 当前没有对 `viewportHostService` 的空指针保护策略,调用方需要自己判断是否已绑定。 +- 当前设置 action route 时不会额外广播事件。 + +## 相关文档 + +- [EditorContext](EditorContext.md) +- [Service Accessors](Service-Accessors.md) +- [EditorActionRoute](../EditorActionRoute/EditorActionRoute.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorEvents/EditorEvents.md b/docs/api/XCEngine/Editor/Core/EditorEvents/EditorEvents.md index 5f43c2f8..c5621d3e 100644 --- a/docs/api/XCEngine/Editor/Core/EditorEvents/EditorEvents.md +++ b/docs/api/XCEngine/Editor/Core/EditorEvents/EditorEvents.md @@ -6,7 +6,7 @@ **源文件**: `editor/src/Core/EditorEvents.h` -**描述**: 定义编辑器内部事件类型,包括选择变化、实体生命周期、场景变化、播放模式变化、Dock 布局重置和退出请求等事件。 +**描述**: 定义编辑器内部事件类型,包括选择变化、实体生命周期、场景变化、播放模式变化、Game View 输入快照、Dock 布局重置和退出请求等事件。 ## 概述 @@ -16,29 +16,72 @@ ## 当前事件 -| 事件 | 作用 | -|------|------| -| `SelectionChangedEvent` | 选择集合变化。 | -| `EntityCreatedEvent` | 新建实体。 | -| `EntityDeletedEvent` | 删除实体。 | -| `EntityChangedEvent` | 实体内容变化。 | -| `EntityRenameRequestedEvent` | 请求重命名实体。 | -| `EntityParentChangedEvent` | 实体父子关系变化。 | -| `SceneChangedEvent` | 当前场景发生变化。 | -| `PlayModeStartedEvent` / `Stopped` / `Paused` | 编辑器播放模式状态变化。 | -| `EditorModeChangedEvent` | 编辑器模式切换。 | -| `DockLayoutResetRequestedEvent` | 请求重置 dock 布局。 | -| `EditorExitRequestedEvent` | 请求关闭编辑器。 | +### 选择与实体事件 + +- [Selection And Entity Events](Selection-And-Entity-Events.md) + +### 场景与 play mode 事件 + +- [Scene And PlayMode Events](Scene-And-PlayMode-Events.md) + +### Game View 输入快照 + +- [GameViewInputFrameEvent](GameViewInputFrameEvent.md) + +### 宿主与模式事件 + +- [Host And Mode Events](Host-And-Mode-Events.md) + +## `GameViewInputFrameEvent` + +`GameViewInputFrameEvent` 是当前 Editor 运行时输入桥接链路里最关键的新事件之一,具体可参考 [GameViewInputFrameEvent](GameViewInputFrameEvent.md)。 + +它不是离散的 down/up 事件,而是逐帧输入快照,字段包括: + +- `hovered` +- `focused` +- `mousePosition` +- `mouseDelta` +- `mouseWheel` +- `keyDown[KeyStateCount]` +- `mouseButtonDown[MouseButtonStateCount]` + +当前主要发布方是 [GameViewPanel](../../panels/GameViewPanel/GameViewPanel.md),主要消费方是 [PlaySessionController](../PlaySessionController/PlaySessionController.md)。 + +当前这两个容量常量按源码固定为: + +- `KeyStateCount = 256` +- `MouseButtonStateCount = 5` + +但要注意,当前生产方 `GameViewPanel` 并不会填满所有槽位,而只会写入: + +- 映射表覆盖到的 `ImGuiKey -> KeyCode` +- Left / Right / Middle 三个鼠标按钮 + +这条链路的意义是: + +- `GameViewPanel` 只负责采集 ImGui 当前可见输入状态。 +- `PlaySessionController` 再决定这些状态是否应桥接到运行时 [InputManager](../../../Input/InputManager/InputManager.md)。 + +因此 Game View 输入不是直接打到运行时,而是先经过 editor 事件总线。 + +如果需要按完整顺序理解这条桥接链路,可以继续看: + +- [Game View Runtime Input Bridge](../../../../_guides/Editor/Game-View-Runtime-Input-Bridge.md) ## 特别说明 - `GameObjectID` 当前被定义为 `uint64_t` 别名。 - `SelectionChangedEvent` 同时包含完整选择列表和主选择。 -- `EditorModeChangedEvent` 当前使用整数记录旧模式/新模式,而不是更强类型的枚举。 +- `GameViewInputFrameEvent` 里的 `mousePosition` 是相对 Game View 视口左上角的局部坐标。 +- `EditorModeChangedEvent` 当前使用强类型的 `EditorRuntimeMode`,不是裸整数。 ## 相关文档 - [Core](../Core.md) - [EventBus](../EventBus/EventBus.md) +- [GameViewPanel](../../panels/GameViewPanel/GameViewPanel.md) +- [PlaySessionController](../PlaySessionController/PlaySessionController.md) +- [Game View Runtime Input Bridge](../../../../_guides/Editor/Game-View-Runtime-Input-Bridge.md) - [SelectionManager](../SelectionManager/SelectionManager.md) - [SceneManager](../../Managers/SceneManager/SceneManager.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorEvents/GameViewInputFrameEvent.md b/docs/api/XCEngine/Editor/Core/EditorEvents/GameViewInputFrameEvent.md new file mode 100644 index 00000000..6942890f --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorEvents/GameViewInputFrameEvent.md @@ -0,0 +1,56 @@ +# GameViewInputFrameEvent + +**命名空间**: `XCEngine::Editor` + +**类型**: `event struct` + +**源文件**: `editor/src/Core/EditorEvents.h` + +## 签名 + +```cpp +struct GameViewInputFrameEvent { + static constexpr size_t KeyStateCount = 256u; + static constexpr size_t MouseButtonStateCount = 5u; + + bool hovered = false; + bool focused = false; + Math::Vector2 mousePosition = Math::Vector2::Zero(); + Math::Vector2 mouseDelta = Math::Vector2::Zero(); + float mouseWheel = 0.0f; + std::array keyDown = {}; + std::array mouseButtonDown = {}; +}; +``` + +## 作用 + +表示 Game View 在单帧内采集到的键鼠输入快照,是当前 editor 到运行时输入桥接链路的核心事件。 + +## 当前实现行为 + +- 这是逐帧快照事件,不是离散 down/up 事件。 +- 关键字段包括: + - `hovered` + - `focused` + - `mousePosition` + - `mouseDelta` + - `mouseWheel` + - `keyDown[256]` + - `mouseButtonDown[5]` +- 当前主要发布方是 [GameViewPanel](../../panels/GameViewPanel/GameViewPanel.md)。 +- 当前主要消费方是 [PlaySessionController](../PlaySessionController/PlaySessionController.md)。 + +## 当前语义细节 + +- `mousePosition` 当前是相对 Game View 左上角的局部坐标。 +- 当前生产方并不会填满所有键鼠槽位,而是只覆盖当前映射表和常用鼠标按钮。 +- 这条链路的意义是: + - `GameViewPanel` 只负责采集 editor 侧可见输入状态 + - `PlaySessionController` 再决定是否桥接到运行时 [InputManager](../../../Input/InputManager/InputManager.md) + +## 相关文档 + +- [EditorEvents](EditorEvents.md) +- [PlaySessionController](../PlaySessionController/PlaySessionController.md) +- [Game View Runtime Input Bridge](../../../../_guides/Editor/Game-View-Runtime-Input-Bridge.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorEvents/Host-And-Mode-Events.md b/docs/api/XCEngine/Editor/Core/EditorEvents/Host-And-Mode-Events.md new file mode 100644 index 00000000..a33ba20e --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorEvents/Host-And-Mode-Events.md @@ -0,0 +1,54 @@ +# EditorEvents Host And Mode Events + +**命名空间**: `XCEngine::Editor` + +**类型**: `event group` + +**源文件**: `editor/src/Core/EditorEvents.h` + +## 作用 + +定义编辑器宿主层、模式切换和布局收口相关事件。 + +## 当前事件 + +### `EditorModeChangedEvent` + +```cpp +struct EditorModeChangedEvent { + EditorRuntimeMode oldMode = EditorRuntimeMode::Edit; + EditorRuntimeMode newMode = EditorRuntimeMode::Edit; +}; +``` + +- 用于广播当前编辑器运行模式变化。 +- 当前典型发布方是 [EditorContext](../EditorContext/EditorContext.md)。 + +### `DockLayoutResetRequestedEvent` + +```cpp +struct DockLayoutResetRequestedEvent {}; +``` + +- 用于请求重置当前 dock 布局。 +- 当前适合项目切换或缺少保存布局时使用。 + +### `EditorExitRequestedEvent` + +```cpp +struct EditorExitRequestedEvent {}; +``` + +- 用于请求关闭编辑器。 +- 当前 `Application` 会把它转成真正的 `WM_CLOSE` 路径。 + +## 设计含义 + +- 这类事件说明当前宿主层动作并不是直接由菜单栏或面板调用 Win32 API 完成。 +- 它们先以事件形式表达“请求”,再由宿主壳决定如何执行。 + +## 相关文档 + +- [EditorEvents](EditorEvents.md) +- [EditorContext](../EditorContext/EditorContext.md) +- [Application](../../Application/Application.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorEvents/Scene-And-PlayMode-Events.md b/docs/api/XCEngine/Editor/Core/EditorEvents/Scene-And-PlayMode-Events.md new file mode 100644 index 00000000..db7283c0 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorEvents/Scene-And-PlayMode-Events.md @@ -0,0 +1,60 @@ +# EditorEvents Scene And PlayMode Events + +**命名空间**: `XCEngine::Editor` + +**类型**: `event group` + +**源文件**: `editor/src/Core/EditorEvents.h` + +## 作用 + +定义场景变化通知以及 play mode 请求/状态变化事件。 + +## 当前事件 + +### `SceneChangedEvent` + +```cpp +struct SceneChangedEvent {}; +``` + +- 用于广播当前场景结构或内容已经发生变化。 +- 当前是一个无载荷事件,语义重点在“场景整体应被视为已变化”。 + +### Play mode 请求事件 + +```cpp +struct PlayModeStartRequestedEvent {}; +struct PlayModeStopRequestedEvent {}; +struct PlayModePauseRequestedEvent {}; +struct PlayModeResumeRequestedEvent {}; +struct PlayModeStepRequestedEvent {}; +``` + +- 表示 UI 或动作系统请求发生一次 play mode 状态切换。 +- 这些事件本身不表示状态已经真正切换完成。 + +### Play mode 状态已变更事件 + +```cpp +struct PlayModeStartedEvent {}; +struct PlayModeStoppedEvent {}; +struct PlayModePausedEvent {}; +struct PlayModeResumedEvent {}; +``` + +- 表示 play mode 状态切换已经完成。 +- 与“请求事件”分离后,UI 发起者和真正的状态执行者可以解耦。 + +## 设计含义 + +- 当前 play mode 事件集明显分成两层: + - requested + - already happened +- 这是编辑器里很常见的模式,因为工具栏按钮和真正的运行态切换并不是同一个责任点。 + +## 相关文档 + +- [EditorEvents](EditorEvents.md) +- [EventBus](../EventBus/EventBus.md) +- [PlaySessionController](../PlaySessionController/PlaySessionController.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorEvents/Selection-And-Entity-Events.md b/docs/api/XCEngine/Editor/Core/EditorEvents/Selection-And-Entity-Events.md new file mode 100644 index 00000000..de8ea3ee --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorEvents/Selection-And-Entity-Events.md @@ -0,0 +1,63 @@ +# EditorEvents Selection And Entity Events + +**命名空间**: `XCEngine::Editor` + +**类型**: `event group` + +**源文件**: `editor/src/Core/EditorEvents.h` + +## 作用 + +定义与当前选择集合和实体生命周期相关的编辑器事件类型。 + +## 当前事件 + +### `SelectionChangedEvent` + +```cpp +struct SelectionChangedEvent { + std::vector selectedObjects; + GameObjectID primarySelection; +}; +``` + +- 用于广播当前完整选择集合和主选择对象。 +- 当前主要由 [SelectionManager](../SelectionManager/SelectionManager.md) 发布。 + +### 实体生命周期事件 + +```cpp +struct EntityCreatedEvent { GameObjectID entityId; }; +struct EntityDeletedEvent { GameObjectID entityId; }; +struct EntityChangedEvent { GameObjectID entityId; }; +struct EntityRenameRequestedEvent { GameObjectID entityId; }; +``` + +- `EntityCreatedEvent` + - 表示实体已创建 +- `EntityDeletedEvent` + - 表示实体已删除 +- `EntityChangedEvent` + - 表示实体内容或状态发生变化 +- `EntityRenameRequestedEvent` + - 表示 UI 或 action router 请求进入重命名流程 + +### `EntityParentChangedEvent` + +```cpp +struct EntityParentChangedEvent { + GameObjectID entityId; + GameObjectID oldParentId; + GameObjectID newParentId; +}; +``` + +- 用于表达层级关系变化,而不仅仅是“对象变了”。 +- 当前适合 Hierarchy、SceneManager 和相关 action router 协作使用。 + +## 相关文档 + +- [EditorEvents](EditorEvents.md) +- [EventBus](../EventBus/EventBus.md) +- [SelectionManager](../SelectionManager/SelectionManager.md) +- [SceneManager](../../Managers/SceneManager/SceneManager.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorRuntimeMode/EditorRuntimeMode.md b/docs/api/XCEngine/Editor/Core/EditorRuntimeMode/EditorRuntimeMode.md new file mode 100644 index 00000000..585e2b39 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorRuntimeMode/EditorRuntimeMode.md @@ -0,0 +1,70 @@ +# EditorRuntimeMode + +**命名空间**: `XCEngine::Editor` + +**类型**: `enum-header` + +**源文件**: `editor/src/Core/EditorRuntimeMode.h` + +**描述**: 定义编辑器当前处于编辑、播放、暂停还是模拟态,并提供一组针对编辑权限的轻量判断 helper。 + +## 概述 + +`EditorRuntimeMode` 是 Editor 内部“当前工作态”最基础的状态枚举。它会被: + +- `IEditorContext` 持有 +- `PlaySessionController` 驱动切换 +- `MainMenuActionRouter`、`MenuBar` 和 `EditorActions` 用来决定哪些操作可用 +- `EditorWindowTitle` 和相关事件用来生成用户可见状态反馈 + +## 当前枚举值 + +| 枚举值 | 说明 | +|------|------| +| `Edit` | 普通编辑态。 | +| `Play` | Play mode 正在运行。 | +| `Paused` | Play mode 暂停。 | +| `Simulate` | 预留的模拟态。 | + +## helper 语义 + +### `IsEditorRuntimeActive()` + +只要不是 `Edit` 就返回 `true`。当前会把 `Play`、`Paused` 和 `Simulate` 都视为运行态。 + +### `IsEditorDocumentEditingAllowed()` + +当前只有 `Edit` 返回 `true`。这更接近“是否允许把当前场景文档当成普通可编辑文档”的判断。 + +### `IsEditorSceneObjectEditingAllowed()` + +当前实现对四种枚举值全部返回 `true`。这意味着编辑器并没有在 play mode 下彻底禁止场景对象级交互。 + +### `IsEditorSceneUndoRedoAllowed()` + +当前直接复用 `IsEditorSceneObjectEditingAllowed()`,因此也对现有四种状态全部放行。 + +## 当前设计取向 + +这组 helper 反映的是当前 Editor 的真实策略: + +- 文档级保存/切换更偏向只在 `Edit` 态进行 +- 场景对象级交互并没有在 `Play` / `Paused` 下被完全锁死 + +这比很多“play mode 一进入就把编辑操作全禁用”的编辑器更宽松,也更依赖调用方自己判断哪些动作应该在当前态开放。 + +## 真实使用位置 + +- `editor/src/Core/IEditorContext.h` +- `editor/src/Core/PlaySessionController.cpp` +- `editor/src/Actions/EditorActions.h` +- `editor/src/Actions/MainMenuActionRouter.h` +- `editor/src/panels/MenuBar.cpp` +- `tests/editor/test_action_routing.cpp` +- `tests/editor/test_play_session_controller.cpp` + +## 相关文档 + +- [Core](../Core.md) +- [PlaySessionController](../PlaySessionController/PlaySessionController.md) +- [EditorEvents](../EditorEvents/EditorEvents.md) diff --git a/docs/api/XCEngine/Editor/Core/EventBus/EventBus.md b/docs/api/XCEngine/Editor/Core/EventBus/EventBus.md index 38a26eca..3e83d6c5 100644 --- a/docs/api/XCEngine/Editor/Core/EventBus/EventBus.md +++ b/docs/api/XCEngine/Editor/Core/EventBus/EventBus.md @@ -6,38 +6,125 @@ **源文件**: `editor/src/Core/EventBus.h` -**描述**: 提供编辑器内部的轻量级泛型事件总线,支持按事件类型订阅、取消订阅、发布和清空。 +**描述**: 编辑器内部的同步类型事件总线,提供基于模板事件类型的订阅、取消订阅、发布与清空能力。 ## 概述 -`EventBus` 是当前编辑器内部通信的基础设施。 +`EventBus` 是当前 Editor 内部最基础的解耦设施之一。 -它的核心机制是: +它的角色很明确: -- 每个事件类型通过 `EventTypeId` 获得一个静态类型 ID。 -- `Subscribe()` 返回 handler ID。 -- `Publish()` 根据类型 ID 找到对应处理器并逐个调用。 +- 让面板、宿主层和 manager 可以通过事件协作。 +- 避免 `MainMenu`、`Application`、`HierarchyPanel`、`InspectorPanel` 之间直接互相依赖。 +- 保持事件类型是静态类型,而不是字符串 topic。 -同时这个头里还定义了: +例如当前实际用到的事件就包括: -- `EventTypeRegistry` -- `EventTypeId` +- [SelectionChangedEvent](../EditorEvents/EditorEvents.md) +- `EntityRenameRequestedEvent` +- `DockLayoutResetRequestedEvent` +- `EditorExitRequestedEvent` -## 当前实现说明 +这是一种典型的编辑器架构做法。对工具软件来说,很多行为天然更像“广播一个状态变化”而不是“直接调用某个固定对象的方法”。 -- 订阅表使用 `std::unordered_map>`。 -- `Subscribe` / `Unsubscribe` 使用 `std::lock_guard`。 -- `Publish` 使用 `std::shared_lock`。 -- `Clear()` 会清空全部 handler。 +## 公开接口 -## 当前实现风险与边界 +### 类型到运行时 id -- `Publish()` 在持有共享锁时直接执行 handler。 -- 如果某个 handler 在回调内部再次对同一个 `EventBus` 做需要独占锁的订阅/取消订阅操作,重入语义需要格外小心。 -- 当前没有事件排队、延迟分发或线程切换机制。 +- [EventTypeRegistry / EventTypeId](EventTypeRegistry-And-EventTypeId.md) + +### 订阅与退订 + +- [Subscribe / Unsubscribe](Subscribe-And-Unsubscribe.md) + +### 发布与清空 + +- [Publish / Clear](Publish-And-Clear.md) + +## 当前实现行为 + +### 类型到运行时 id 的映射 + +`EventTypeId::Get()` 会通过函数内静态值,为每个事件类型 `T` 只生成一次 `uint32_t` 类型 id。 +这个 id 来自 `EventTypeRegistry::NextId()`,后者使用原子计数器递增。 + +这意味着: + +- 事件类型不需要手工注册。 +- 同一进程内,相同模板类型总能映射到同一个 id。 +- 不同事件类型不会共享 handler 桶。 + +### handler 存储 + +当前内部存储结构是: + +- `std::unordered_map> m_handlers` + +其中 `HandlerEntry` 保存: + +- `uint64_t id` +- `std::function handler` + +`Subscribe()` 会把用户传入的 `std::function` 包装成 `void*` 擦除后的回调,并存入对应 `typeId` 的数组。 + +### 锁与发布语义 + +当前实现最容易被旧文档写错的点就在这里: + +- `Subscribe()` 和 `Unsubscribe()` 在持有独占锁时修改订阅表。 +- `Publish()` 在共享锁下读取当前 handler 列表,但不会在持锁期间执行回调。 + +`Publish()` 的真实流程是: + +1. 取 `typeId`。 +2. 在共享锁下把该类型当前的 `HandlerEntry` 数组复制到局部变量。 +3. 释放锁。 +4. 逐个执行局部快照中的 handler。 + +所以当前语义不是“持锁回调”,而是“快照式同步发布”。 + +## 线程与重入语义 + +当前实现可以视为“结构线程安全,但不负责线程切换”的同步事件总线。 + +这具体意味着: + +- 多线程同时订阅 / 取消订阅 / 发布,不会直接破坏内部容器。 +- 但事件 handler 在哪个线程里执行,完全取决于谁调用了 `Publish()`。 +- 如果在 handler 内部再次调用 `Subscribe()` 或 `Unsubscribe()`,不会与当前 `Publish()` 形成死锁,因为回调执行时已经离开锁保护区。 + +同时也要注意: + +- 当前这次 `Publish()` 使用的是发布前复制的 handler 快照。 +- 因此在回调过程中新增或移除的 handler,只会影响下一次发布,不会影响这一次。 + +这对编辑器工具链来说通常是更稳妥的选择。 +因为 UI 事件、选择事件、菜单动作请求通常都希望“这次广播的收件人集合是稳定的”。 + +## 设计说明 + +当前 `EventBus` 的设计明显偏向编辑器工具场景,而不是高吞吐异步消息系统: + +- 事件是强类型模板,不是字符串 topic。 +- 分发是立即同步的,不做排队。 +- 不尝试做线程切换、延迟执行或优先级调度。 + +这种设计的好处是: + +- 调用栈清晰,调试容易。 +- 面板与宿主层之间解耦,但仍然保留 C++ 静态类型安全。 +- 对当前这种 UI 主线程驱动的 editor 足够简单可靠。 + +## 当前限制 + +- 当前没有“只触发一次”的订阅模式。 +- 当前没有 handler 优先级、事件冒泡或取消传播机制。 +- 当前没有主线程投递队列;若从后台线程发布,handler 也会在后台线程执行。 +- 若 handler 抛出异常,`EventBus` 本身不做异常屏障。 ## 相关文档 -- [Core](../Core.md) - [EditorEvents](../EditorEvents/EditorEvents.md) - [EditorContext](../EditorContext/EditorContext.md) +- [IEditorContext](../IEditorContext/IEditorContext.md) +- [Application](../../Application/Application.md) diff --git a/docs/api/XCEngine/Editor/Core/EventBus/EventTypeRegistry-And-EventTypeId.md b/docs/api/XCEngine/Editor/Core/EventBus/EventTypeRegistry-And-EventTypeId.md new file mode 100644 index 00000000..6a3c2a99 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EventBus/EventTypeRegistry-And-EventTypeId.md @@ -0,0 +1,49 @@ +# EventTypeRegistry / EventTypeId + +**命名空间**: `XCEngine::Editor` + +**类型**: `helper types` + +**源文件**: `editor/src/Core/EventBus.h` + +## 签名 + +```cpp +class EventTypeRegistry { +public: + static uint32_t NextId(); +}; + +template +struct EventTypeId { + static uint32_t Get(); +}; +``` + +## 作用 + +为 `EventBus` 的模板事件类型分配稳定的运行时类型 id。 + +## 当前实现行为 + +### `EventTypeRegistry::NextId()` + +- 使用一个进程内静态 `std::atomic` 计数器。 +- 每次调用都会通过 `fetch_add(1, std::memory_order_relaxed)` 生成新的类型 id。 + +### `EventTypeId::Get()` + +- 对每个模板类型 `T`,只在第一次调用时向 `EventTypeRegistry::NextId()` 申请一次 id。 +- 之后会复用函数内静态 `id`。 + +## 设计含义 + +- 事件类型不需要手工注册字符串 topic。 +- 同一进程内,相同模板类型始终映射到同一个 `uint32_t` id。 +- 不同事件类型会落到不同的 handler 桶里。 + +## 相关文档 + +- [EventBus](EventBus.md) +- [Subscribe / Unsubscribe](Subscribe-And-Unsubscribe.md) +- [Publish / Clear](Publish-And-Clear.md) diff --git a/docs/api/XCEngine/Editor/Core/EventBus/Publish-And-Clear.md b/docs/api/XCEngine/Editor/Core/EventBus/Publish-And-Clear.md new file mode 100644 index 00000000..299c22a7 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EventBus/Publish-And-Clear.md @@ -0,0 +1,52 @@ +# EventBus::Publish / Clear + +**命名空间**: `XCEngine::Editor` + +**类型**: `template method group` + +**源文件**: `editor/src/Core/EventBus.h` + +## 签名 + +```cpp +template +void Publish(const T& event); + +void Clear(); +``` + +## 作用 + +同步发布一个事件实例,或清空总线中的全部 handler。 + +## 当前实现行为 + +### `Publish(event)` + +- 会先取出 `EventTypeId::Get()`。 +- 在共享锁下查找当前事件类型的 handler 数组。 +- 如果不存在订阅者,直接返回。 +- 如果存在,会先把该数组复制到局部变量 `handlers`。 +- 释放锁后,再逐个执行局部快照里的回调。 + +## 关键语义 + +- 当前不是“持锁执行回调”,而是“快照式同步发布”。 +- 回调发生在哪个线程,完全取决于谁调用了 `Publish()`。 +- 在回调里新增或移除 handler,只会影响下一次发布,不会影响这一次。 + +### `Clear()` + +- 在独占锁下直接清空 `m_handlers`。 +- 当前不会重置 `m_nextHandlerId`。 + +## 设计含义 + +- `Publish()` 的快照式实现避免了持锁回调导致的死锁或长时间锁占用。 +- `Clear()` 更像生命周期收口操作,适合在整个编辑器上下文销毁时使用。 + +## 相关文档 + +- [EventBus](EventBus.md) +- [Subscribe / Unsubscribe](Subscribe-And-Unsubscribe.md) +- [EditorContext](../EditorContext/EditorContext.md) diff --git a/docs/api/XCEngine/Editor/Core/EventBus/Subscribe-And-Unsubscribe.md b/docs/api/XCEngine/Editor/Core/EventBus/Subscribe-And-Unsubscribe.md new file mode 100644 index 00000000..7c703d96 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EventBus/Subscribe-And-Unsubscribe.md @@ -0,0 +1,50 @@ +# EventBus::Subscribe / Unsubscribe + +**命名空间**: `XCEngine::Editor` + +**类型**: `template method group` + +**源文件**: `editor/src/Core/EventBus.h` + +## 签名 + +```cpp +template +uint64_t Subscribe(std::function handler); + +template +void Unsubscribe(uint64_t handlerId); +``` + +## 作用 + +按事件类型注册或移除同步 handler。 + +## 当前实现行为 + +### `Subscribe(handler)` + +- 会先取出 `EventTypeId::Get()` 作为当前事件类型桶 id。 +- 在独占锁保护下: + - 生成新的 `handlerId` + - 确保该 `typeId` 对应的 `std::vector` 已存在 + - 把 `std::function` 包装成擦除后的 `std::function` + - 追加到对应 handler 数组 +- 最终返回这个 `handlerId` + +### `Unsubscribe(handlerId)` + +- 同样先解析 `typeId` +- 在独占锁下找到对应 handler 数组 +- 通过 `std::remove_if(...)` 删除指定 `handlerId` + +## 线程语义 + +- 订阅和退订都会修改内部 handler 表,因此当前都持有独占锁。 +- 它们不会直接执行任何事件回调。 + +## 相关文档 + +- [EventBus](EventBus.md) +- [EventTypeRegistry / EventTypeId](EventTypeRegistry-And-EventTypeId.md) +- [Publish / Clear](Publish-And-Clear.md) diff --git a/docs/api/XCEngine/Editor/Core/IEditorContext/IEditorContext.md b/docs/api/XCEngine/Editor/Core/IEditorContext/IEditorContext.md index d4e54de9..7ef15d34 100644 --- a/docs/api/XCEngine/Editor/Core/IEditorContext/IEditorContext.md +++ b/docs/api/XCEngine/Editor/Core/IEditorContext/IEditorContext.md @@ -24,15 +24,17 @@ ## 核心接口 -| 方法 | 作用 | -|------|------| -| `GetEventBus()` | 获取事件总线。 | -| `GetSelectionManager()` | 获取选择管理服务。 | -| `GetSceneManager()` | 获取场景管理服务。 | -| `GetProjectManager()` | 获取项目管理服务。 | -| `GetUndoManager()` | 获取撤销管理服务。 | -| `SetActiveActionRoute()` / `GetActiveActionRoute()` | 管理当前动作路由。 | -| `SetProjectPath()` / `GetProjectPath()` | 管理当前项目根路径。 | +### 核心服务访问 + +- [Service Accessors](Service-Accessors.md) + +### 视口、动作路由与运行模式 + +- [Viewport, Action Route And Runtime Mode](Viewport-Action-Route-And-Runtime-Mode.md) + +### 项目路径 + +- [Project Path](Project-Path.md) ## 设计说明 diff --git a/docs/api/XCEngine/Editor/Core/IEditorContext/Project-Path.md b/docs/api/XCEngine/Editor/Core/IEditorContext/Project-Path.md new file mode 100644 index 00000000..4fb20de6 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/IEditorContext/Project-Path.md @@ -0,0 +1,39 @@ +# IEditorContext Project Path + +**命名空间**: `XCEngine::Editor` + +**类型**: `interface methods` + +**源文件**: `editor/src/Core/IEditorContext.h` + +## 签名 + +```cpp +virtual void SetProjectPath(const std::string& path) = 0; +virtual const std::string& GetProjectPath() const = 0; +``` + +## 作用 + +定义当前打开项目根路径的读写契约。 + +## 契约语义 + +- `SetProjectPath(path)` + - 写入当前上下文的项目根路径 +- `GetProjectPath()` + - 返回当前项目路径字符串 + +## 设计含义 + +- 项目路径是当前 Editor 很多服务的共同输入,例如: + - `ProjectManager` + - `ProjectPanel` + - 启动场景加载 + - dock / workspace 初始化 + +## 相关文档 + +- [IEditorContext](IEditorContext.md) +- [Service Accessors](Service-Accessors.md) +- [EditorWorkspace](../EditorWorkspace/EditorWorkspace.md) diff --git a/docs/api/XCEngine/Editor/Core/IEditorContext/Service-Accessors.md b/docs/api/XCEngine/Editor/Core/IEditorContext/Service-Accessors.md new file mode 100644 index 00000000..52ce18db --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/IEditorContext/Service-Accessors.md @@ -0,0 +1,45 @@ +# IEditorContext Service Accessors + +**命名空间**: `XCEngine::Editor` + +**类型**: `interface methods` + +**源文件**: `editor/src/Core/IEditorContext.h` + +## 签名 + +```cpp +virtual EventBus& GetEventBus() = 0; +virtual ISelectionManager& GetSelectionManager() = 0; +virtual ISceneManager& GetSceneManager() = 0; +virtual IProjectManager& GetProjectManager() = 0; +virtual IUndoManager& GetUndoManager() = 0; +``` + +## 作用 + +定义 Editor 上下文对核心服务定位入口的最小契约。 + +## 契约语义 + +- `GetEventBus()` + - 返回当前编辑器事件总线 +- `GetSelectionManager()` + - 返回选择管理服务 +- `GetSceneManager()` + - 返回场景管理服务 +- `GetProjectManager()` + - 返回项目管理服务 +- `GetUndoManager()` + - 返回撤销管理服务 + +## 设计含义 + +- `IEditorContext` 当前更像一个 service hub 接口,而不是业务对象。 +- 面板、命令和 action router 通过它统一访问核心服务,避免各处自己持有一组分散依赖。 + +## 相关文档 + +- [IEditorContext](IEditorContext.md) +- [Viewport, Action Route And Runtime Mode](Viewport-Action-Route-And-Runtime-Mode.md) +- [EditorContext](../EditorContext/EditorContext.md) diff --git a/docs/api/XCEngine/Editor/Core/IEditorContext/Viewport-Action-Route-And-Runtime-Mode.md b/docs/api/XCEngine/Editor/Core/IEditorContext/Viewport-Action-Route-And-Runtime-Mode.md new file mode 100644 index 00000000..1d485e53 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/IEditorContext/Viewport-Action-Route-And-Runtime-Mode.md @@ -0,0 +1,41 @@ +# IEditorContext Viewport, Action Route And Runtime Mode + +**命名空间**: `XCEngine::Editor` + +**类型**: `interface methods` + +**源文件**: `editor/src/Core/IEditorContext.h` + +## 签名 + +```cpp +virtual IViewportHostService* GetViewportHostService() = 0; +virtual void SetActiveActionRoute(EditorActionRoute route) = 0; +virtual EditorActionRoute GetActiveActionRoute() const = 0; +virtual void SetRuntimeMode(EditorRuntimeMode mode) = 0; +virtual EditorRuntimeMode GetRuntimeMode() const = 0; +``` + +## 作用 + +定义编辑器当前视口宿主绑定、动作路由状态和运行模式状态的访问契约。 + +## 契约语义 + +- `GetViewportHostService()` + - 返回当前工作区绑定的视口宿主服务 +- `SetActiveActionRoute(...)` / `GetActiveActionRoute()` + - 读写当前活动动作路由 +- `SetRuntimeMode(...)` / `GetRuntimeMode()` + - 读写当前运行模式,例如 `Edit / Play` + +## 设计含义 + +- `IEditorContext` 不只暴露纯数据服务,也暴露当前 Editor 运行态。 +- 这让 `Application`、面板和 play session 控制器都可以围绕同一份上下文协作。 + +## 相关文档 + +- [IEditorContext](IEditorContext.md) +- [Service Accessors](Service-Accessors.md) +- [Project Path](Project-Path.md) diff --git a/docs/api/XCEngine/Editor/Core/PlaySessionController/Attach.md b/docs/api/XCEngine/Editor/Core/PlaySessionController/Attach.md new file mode 100644 index 00000000..5d2fbccd --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/PlaySessionController/Attach.md @@ -0,0 +1,41 @@ +# PlaySessionController::Attach + +**命名空间**: `XCEngine::Editor` + +**类型**: `method` + +**源文件**: `editor/src/Core/PlaySessionController.h` + +## 签名 + +```cpp +void Attach(IEditorContext& context); +``` + +## 作用 + +把 `PlaySessionController` 接入 editor 事件总线,开始监听 play mode 请求和 Game View 输入帧。 + +## 当前实现行为 + +- 对每类事件都只在对应 handler id 为 `0` 时订阅,避免重复 attach 时二次注册。 +- 当前会订阅: + - `PlayModeStartRequestedEvent` + - `PlayModeStopRequestedEvent` + - `PlayModePauseRequestedEvent` + - `PlayModeResumeRequestedEvent` + - `PlayModeStepRequestedEvent` + - `GameViewInputFrameEvent` +- play mode 请求事件最终分别转发到: + - [StartPlay](StartPlay.md) + - [StopPlay](StopPlay.md) + - [PausePlay](PausePlay.md) + - [ResumePlay](ResumePlay.md) + - [StepPlay](StepPlay.md) +- `GameViewInputFrameEvent` 不会立即驱动 `InputManager`;它只更新 `m_pendingGameViewInput`,真正应用发生在 [Update](Update.md)。 + +## 相关文档 + +- [PlaySessionController](PlaySessionController.md) +- [Detach](Detach.md) +- [Update](Update.md) diff --git a/docs/api/XCEngine/Editor/Core/PlaySessionController/Detach.md b/docs/api/XCEngine/Editor/Core/PlaySessionController/Detach.md new file mode 100644 index 00000000..779d3ff1 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/PlaySessionController/Detach.md @@ -0,0 +1,32 @@ +# PlaySessionController::Detach + +**命名空间**: `XCEngine::Editor` + +**类型**: `method` + +**源文件**: `editor/src/Core/PlaySessionController.h` + +## 签名 + +```cpp +void Detach(IEditorContext& context); +``` + +## 作用 + +从 editor 生命周期中移除 `PlaySessionController`,并清理运行态订阅与输入桥接状态。 + +## 当前实现行为 + +- 一进入函数就调用 [StopPlay](StopPlay.md);如果当前仍处于 play 或 paused,会尝试恢复编辑态场景。 +- 之后逐项注销在 [Attach](Attach.md) 中注册的全部事件订阅,并把 handler id 清零。 +- 最后调用私有 helper `ResetRuntimeInputBridge()`,清空: + - `m_pendingGameViewInput` + - `m_appliedGameViewInput` + - `m_hasPendingGameViewInput` + +## 相关文档 + +- [PlaySessionController](PlaySessionController.md) +- [Attach](Attach.md) +- [StopPlay](StopPlay.md) diff --git a/docs/api/XCEngine/Editor/Core/PlaySessionController/PausePlay.md b/docs/api/XCEngine/Editor/Core/PlaySessionController/PausePlay.md new file mode 100644 index 00000000..c9892b41 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/PlaySessionController/PausePlay.md @@ -0,0 +1,31 @@ +# PlaySessionController::PausePlay + +**命名空间**: `XCEngine::Editor` + +**类型**: `method` + +**源文件**: `editor/src/Core/PlaySessionController.h` + +## 签名 + +```cpp +bool PausePlay(IEditorContext& context); +``` + +## 作用 + +把当前运行态从 `Play` 切到 `Paused`。 + +## 当前实现行为 + +- 只有在 `context.GetRuntimeMode() == EditorRuntimeMode::Play` 且 `m_runtimeLoop.IsRunning()` 时才成功。 +- 成功路径会: + - 调用 `m_runtimeLoop.Pause()` + - 把 `runtimeMode` 切到 `Paused` + - 发布 `PlayModePausedEvent` + +## 相关文档 + +- [PlaySessionController](PlaySessionController.md) +- [ResumePlay](ResumePlay.md) +- [StepPlay](StepPlay.md) diff --git a/docs/api/XCEngine/Editor/Core/PlaySessionController/PlaySessionController.md b/docs/api/XCEngine/Editor/Core/PlaySessionController/PlaySessionController.md new file mode 100644 index 00000000..1811124c --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/PlaySessionController/PlaySessionController.md @@ -0,0 +1,224 @@ +# PlaySessionController + +**命名空间**: `XCEngine::Editor` + +**类型**: `class` + +**源文件**: `editor/src/Core/PlaySessionController.h` + +**描述**: 管理 Editor play mode 状态机、场景快照切换、运行时主循环推进,以及 `GameViewInputFrameEvent` 到运行时 `InputManager` 的输入桥接。 + +## 概述 + +`PlaySessionController` 是当前 Editor 里把“编辑态场景”切换到“运行态场景”的核心胶水层。 + +它串起了四条关键链路: + +1. `EventBus` 的 play / pause / resume / step 请求事件。 +2. `SceneSnapshot` 的编辑态场景备份与恢复。 +3. `RuntimeLoop` 驱动的场景逐帧运行。 +4. [GameViewInputFrameEvent](../EditorEvents/EditorEvents.md) 到运行时 [InputManager](../../../Input/InputManager/InputManager.md) 的输入桥接。 + +如果你想按一条连续链路理解这部分行为,可以继续看: + +- [Game View Runtime Input Bridge](../../../../_guides/Editor/Game-View-Runtime-Input-Bridge.md) + +## 生命周期 + +### `Attach()` + +订阅以下事件: + +- `PlayModeStartRequestedEvent` +- `PlayModeStopRequestedEvent` +- `PlayModePauseRequestedEvent` +- `PlayModeResumeRequestedEvent` +- `PlayModeStepRequestedEvent` +- `GameViewInputFrameEvent` + +其中 `GameViewInputFrameEvent` 的处理方式不是立即改动运行时输入,而是: + +- 把事件保存到 `m_pendingGameViewInput` +- 置 `m_hasPendingGameViewInput = true` + +真正的输入应用发生在之后的 `Update()`。 + +### `Detach()` + +- 先调用 `StopPlay()` +- 再取消所有事件订阅 +- 最后通过 `ResetRuntimeInputBridge()` 清空 pending / applied 输入桥接状态 + +## Play mode 状态机 + +### `StartPlay()` + +当前流程是: + +1. 只有在 `EditorRuntimeMode::Edit` 下才允许进入。 +2. 要求存在活动场景。 +3. 捕获当前编辑态 `SceneSnapshot`。 +4. 立刻用这份快照恢复一次场景,得到干净的运行态副本。 +5. 关闭 scene dirty tracking。 +6. 把 `RuntimeLoop` 的 fixed delta 写回 `ScriptEngine`。 +7. `ResetRuntimeInputBridge()`。 +8. 重新 `Shutdown()` / `Initialize()` 运行时 `InputManager`。 +9. 启动内部 `RuntimeLoop`。 +10. 清空 undo 历史。 +11. 把 `runtimeMode` 切到 `Play` 并发布 `PlayModeStartedEvent`。 + +这里第 `7-8` 步很关键。当前实现会在进入 play mode 时重建一套干净的运行时输入状态,而不是沿用编辑态里已有的 `InputManager` 状态。 + +### `StopPlay()` + +当前流程是: + +1. 只要当前仍处于运行态就允许退出。 +2. 停止 `RuntimeLoop`。 +3. `ResetRuntimeInputBridge()`。 +4. `InputManager::Shutdown()`。 +5. 重新开启 scene dirty tracking。 +6. 用编辑态快照恢复场景。 +7. 清空 undo 历史。 +8. 把 `runtimeMode` 切回 `Edit`。 +9. 发布 `PlayModeStoppedEvent` 并清空内部快照。 + +因此退出 play mode 时,运行时输入状态不会残留到编辑态。 + +### `PausePlay()` / `ResumePlay()` / `StepPlay()` + +- `PausePlay()` 只接受 `Play -> Paused` +- `ResumePlay()` 只接受 `Paused -> Play` +- `StepPlay()` 只接受 `Paused`,并只请求单帧推进,不会自动恢复到 `Play` + +## `Update()` 与输入桥接 + +`Update(context, deltaTime)` 当前的第一道门槛是: + +- 只有 `m_runtimeLoop.IsRunning() == true` 时才继续 + +也就是说: + +- 即使 `GameViewPanel` 每帧都在发布 [GameViewInputFrameEvent](../EditorEvents/EditorEvents.md) +- 只要当前不在 Play / Paused 运行态 + +这些事件都不会驱动运行时 `InputManager` + +这是 `tests/editor/test_play_session_controller.cpp` 明确覆盖的当前规则。 + +通过这道门槛后,`Update()` 才会按顺序执行: + +1. `ApplyGameViewInputFrame(deltaTime)` +2. `m_runtimeLoop.Tick(deltaTime)` + +## `ApplyGameViewInputFrame()` 的真实行为 + +这条方法是当前 Game View 输入桥的核心。 + +### 1. 先推进运行时输入帧边界 + +一进函数先调用: + +```cpp +InputManager::Get().Update(deltaTime); +``` + +所以运行时输入的“本帧按下 / 本帧抬起 / 本帧滚轮 / 本帧鼠标位移”都是在这里按 play mode 帧边界被清空的。 + +### 2. 取本帧待应用快照 + +如果本帧收到过新的 `GameViewInputFrameEvent`,就用 `m_pendingGameViewInput`;否则回退到空事件。 + +这意味着: + +- `GameViewPanel` 如果没有继续发布输入 +- 或者面板关闭后发布了空事件 + +桥接层就会把它视为“这一帧没有 Game View 输入”。 + +### 3. 计算 inputActive + +当前激活规则是: + +```cpp +input.hovered || input.focused +``` + +因此只有当 Game View 被悬停或聚焦时,键鼠状态才会真正送入运行时。 + +### 4. 对比 pending 与 applied,补发 down/up + +当前实现不会把整包快照粗暴覆盖进 `InputManager`,而是逐项比较: + +- `m_appliedGameViewInput.keyDown[index]` +- `input.keyDown[index]` + +以及: + +- `m_appliedGameViewInput.mouseButtonDown[index]` +- `input.mouseButtonDown[index]` + +然后按差异补发: + +- `ProcessKeyDown(...)` +- `ProcessKeyUp(...)` +- `ProcessMouseButton(...)` + +因此运行时 `InputManager` 里看到的仍然是标准 down/up 事件流,而不是一份 editor 专用快照结构。 + +### 5. 鼠标移动和滚轮 + +当 `inputActive == true` 时: + +- 只要鼠标位置变化,或 `mouseDelta != Zero`,就调用 `ProcessMouseMove(...)` +- 只要 `mouseWheel != 0.0f`,就调用 `ProcessMouseWheel(...)` + +### 6. 更新 `m_appliedGameViewInput` + +函数末尾会先把 `m_appliedGameViewInput` 清空;只有当 `inputActive == true` 时,才把它设成当前输入快照。 + +这条规则带来的结果是: + +- 一旦 Game View 失焦或关闭 +- 下一帧比较时就会把上一帧保持的键和鼠标按钮全部补发为 up + +这正是“视口失活时自动释放运行时输入”的来源。 + +## `hovered` / `focused` 的产品语义 + +当前桥接规则不是只认 `hovered`,而是 `hovered || focused`。 + +这意味着: + +- 鼠标悬停在 Game View 上时,输入肯定生效。 +- 即使鼠标暂时移出,只要 Game View 仍保有焦点,键盘状态仍可继续驱动 play mode。 +- 一旦两者都失去,桥接层会在下一帧释放之前按下的运行时输入。 + +这和纯粹的“鼠标必须在视口上”相比,更接近实际游戏调试体验。 + +## 测试覆盖 + +当前已有两组直接测试: + +- `tests/editor/test_play_session_controller.cpp` + - 覆盖快照恢复、事件总线路由、暂停/恢复/步进,以及“非 Play mode 不驱动 InputManager”和“Play mode 下输入被桥接并在空事件后释放” +- `tests/editor/test_play_session_controller_scripting.cpp` + - 覆盖 play mode 下脚本生命周期、fixed delta 传递、managed 输入桥接和停止后恢复编辑态场景 + +## 当前实现边界 + +- 当前只管理单场景 play mode,不处理多场景并行运行。 +- 运行时输入完全来自 `GameViewInputFrameEvent`,不是全局窗口输入直通。 +- `Simulate` 枚举值当前还没有单独的 `PlaySessionController` 分支逻辑。 + +## 相关文档 + +- [Core](../Core.md) +- [EditorRuntimeMode](../EditorRuntimeMode/EditorRuntimeMode.md) +- [EditorEvents](../EditorEvents/EditorEvents.md) +- [EventBus](../EventBus/EventBus.md) +- [GameViewPanel](../../panels/GameViewPanel/GameViewPanel.md) +- [Game View Runtime Input Bridge](../../../../_guides/Editor/Game-View-Runtime-Input-Bridge.md) +- [SceneSnapshot](../SceneSnapshot/SceneSnapshot.md) +- [RuntimeLoop](../../../Scene/RuntimeLoop/RuntimeLoop.md) +- [InputManager](../../../Input/InputManager/InputManager.md) diff --git a/docs/api/XCEngine/Editor/Core/PlaySessionController/ResumePlay.md b/docs/api/XCEngine/Editor/Core/PlaySessionController/ResumePlay.md new file mode 100644 index 00000000..9660549e --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/PlaySessionController/ResumePlay.md @@ -0,0 +1,30 @@ +# PlaySessionController::ResumePlay + +**命名空间**: `XCEngine::Editor` + +**类型**: `method` + +**源文件**: `editor/src/Core/PlaySessionController.h` + +## 签名 + +```cpp +bool ResumePlay(IEditorContext& context); +``` + +## 作用 + +把暂停态恢复为正常运行态。 + +## 当前实现行为 + +- 只有在 `context.GetRuntimeMode() == EditorRuntimeMode::Paused` 且 `m_runtimeLoop.IsRunning()` 时才成功。 +- 成功路径会: + - 调用 `m_runtimeLoop.Resume()` + - 把 `runtimeMode` 切到 `Play` + - 发布 `PlayModeResumedEvent` + +## 相关文档 + +- [PlaySessionController](PlaySessionController.md) +- [PausePlay](PausePlay.md) diff --git a/docs/api/XCEngine/Editor/Core/PlaySessionController/StartPlay.md b/docs/api/XCEngine/Editor/Core/PlaySessionController/StartPlay.md new file mode 100644 index 00000000..add4191e --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/PlaySessionController/StartPlay.md @@ -0,0 +1,39 @@ +# PlaySessionController::StartPlay + +**命名空间**: `XCEngine::Editor` + +**类型**: `method` + +**源文件**: `editor/src/Core/PlaySessionController.h` + +## 签名 + +```cpp +bool StartPlay(IEditorContext& context); +``` + +## 作用 + +从编辑态切入 play mode,启动运行时主循环并冻结当前编辑态场景快照。 + +## 当前实现行为 + +- 只有当前 `runtimeMode` 为 `EditorRuntimeMode::Edit` 时才可能成功。 +- 要求 `ISceneManager` 存在活动场景,并且 `CaptureSceneSnapshot()` 得到的 `SceneSnapshot` 标记为 `hasScene`。 +- 捕获到编辑态快照后,会立刻执行一次 `RestoreSceneSnapshot(m_editorSnapshot)`,生成干净的运行态副本。 +- 之后执行以下准备动作: + - 关闭 scene dirty tracking + - 把 `RuntimeLoop` 的 `fixedDeltaTime` 写入 `ScriptEngine` + - 重置输入桥接状态 + - 重新初始化运行时 `InputManager` + - 启动 `m_runtimeLoop` + - 清空 undo 历史 + - 把 `runtimeMode` 切到 `Play` + - 发布 `PlayModeStartedEvent` +- 任一前置条件失败时都会返回 `false`。 + +## 相关文档 + +- [PlaySessionController](PlaySessionController.md) +- [StopPlay](StopPlay.md) +- [PausePlay](PausePlay.md) diff --git a/docs/api/XCEngine/Editor/Core/PlaySessionController/StepPlay.md b/docs/api/XCEngine/Editor/Core/PlaySessionController/StepPlay.md new file mode 100644 index 00000000..6d1b9fd3 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/PlaySessionController/StepPlay.md @@ -0,0 +1,28 @@ +# PlaySessionController::StepPlay + +**命名空间**: `XCEngine::Editor` + +**类型**: `method` + +**源文件**: `editor/src/Core/PlaySessionController.h` + +## 签名 + +```cpp +bool StepPlay(IEditorContext& context); +``` + +## 作用 + +在暂停态下请求 `RuntimeLoop` 仅推进一帧。 + +## 当前实现行为 + +- 只有在 `context.GetRuntimeMode() == EditorRuntimeMode::Paused` 且 `m_runtimeLoop.IsRunning()` 时才成功。 +- 成功时调用 `m_runtimeLoop.StepFrame()`,但不会自动把 `runtimeMode` 切回 `Play`。 + +## 相关文档 + +- [PlaySessionController](PlaySessionController.md) +- [PausePlay](PausePlay.md) +- [ResumePlay](ResumePlay.md) diff --git a/docs/api/XCEngine/Editor/Core/PlaySessionController/StopPlay.md b/docs/api/XCEngine/Editor/Core/PlaySessionController/StopPlay.md new file mode 100644 index 00000000..8ed0b4f3 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/PlaySessionController/StopPlay.md @@ -0,0 +1,35 @@ +# PlaySessionController::StopPlay + +**命名空间**: `XCEngine::Editor` + +**类型**: `method` + +**源文件**: `editor/src/Core/PlaySessionController.h` + +## 签名 + +```cpp +bool StopPlay(IEditorContext& context); +``` + +## 作用 + +停止运行态并恢复进入 play mode 前保存的编辑态场景。 + +## 当前实现行为 + +- 只有当前 `runtimeMode` 属于 editor runtime active 状态时才继续执行;否则返回 `false`。 +- 首先停止 `m_runtimeLoop`,然后重置输入桥接并关闭 `InputManager`。 +- 重新启用 scene dirty tracking。 +- 接着调用 `RestoreSceneSnapshot(m_editorSnapshot)` 恢复编辑态场景;如果恢复失败,函数会直接返回 `false`。 +- 恢复成功后: + - 清空 undo 历史 + - 把 `runtimeMode` 切回 `Edit` + - 发布 `PlayModeStoppedEvent` + - 清空 `m_editorSnapshot` + +## 相关文档 + +- [PlaySessionController](PlaySessionController.md) +- [StartPlay](StartPlay.md) +- [Detach](Detach.md) diff --git a/docs/api/XCEngine/Editor/Core/PlaySessionController/Update.md b/docs/api/XCEngine/Editor/Core/PlaySessionController/Update.md new file mode 100644 index 00000000..79e8c87a --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/PlaySessionController/Update.md @@ -0,0 +1,31 @@ +# PlaySessionController::Update + +**命名空间**: `XCEngine::Editor` + +**类型**: `method` + +**源文件**: `editor/src/Core/PlaySessionController.h` + +## 签名 + +```cpp +void Update(IEditorContext& context, float deltaTime); +``` + +## 作用 + +在 play session 运行期间逐帧推进输入桥接和 `RuntimeLoop`。 + +## 当前实现行为 + +- 当前实现不直接使用传入的 `context`。 +- 若 `m_runtimeLoop.IsRunning()` 为假,则立即返回。 +- 否则固定按下面顺序执行: + 1. 调用私有 `ApplyGameViewInputFrame(deltaTime)`,把缓存的 `GameViewInputFrameEvent` 映射到运行时 `InputManager` + 2. 调用 `m_runtimeLoop.Tick(deltaTime)` 推进一帧 + +## 相关文档 + +- [PlaySessionController](PlaySessionController.md) +- [Attach](Attach.md) +- [StartPlay](StartPlay.md) diff --git a/docs/api/XCEngine/Editor/panels/GameViewPanel/Constructor.md b/docs/api/XCEngine/Editor/panels/GameViewPanel/Constructor.md new file mode 100644 index 00000000..134e8437 --- /dev/null +++ b/docs/api/XCEngine/Editor/panels/GameViewPanel/Constructor.md @@ -0,0 +1,40 @@ +# GameViewPanel::GameViewPanel + +**命名空间**: `XCEngine::Editor` + +**类型**: `constructor` + +**源文件**: `editor/src/panels/GameViewPanel.h` + +## 签名 + +```cpp +GameViewPanel(); +``` + +## 作用 + +创建标题固定为 `"Game"` 的 Game 视图面板。 + +## 当前实现行为 + +当前构造函数非常薄: + +```cpp +GameViewPanel::GameViewPanel() : Panel("Game") {} +``` + +它只完成一件事: + +- 通过基类 `Panel` 把面板标题初始化为 `"Game"` + +也就是说,`GameViewPanel` 的真实行为几乎都不在 constructor,而集中在 [Render](Render.md) 里: + +- 请求 `Game` 视口内容 +- 采集 ImGui 键鼠状态 +- 发布 `GameViewInputFrameEvent` + +## 相关文档 + +- [GameViewPanel](GameViewPanel.md) +- [Render](Render.md) diff --git a/docs/api/XCEngine/Editor/panels/GameViewPanel/GameViewPanel.md b/docs/api/XCEngine/Editor/panels/GameViewPanel/GameViewPanel.md index 87194de7..44d93f53 100644 --- a/docs/api/XCEngine/Editor/panels/GameViewPanel/GameViewPanel.md +++ b/docs/api/XCEngine/Editor/panels/GameViewPanel/GameViewPanel.md @@ -6,23 +6,151 @@ **源文件**: `editor/src/panels/GameViewPanel.h` -**描述**: Game 视图面板,占位承载 Game 窗口并标记动作路由焦点。 +**描述**: Game 视图面板,负责在 editor 窗口中承载 `EditorViewportKind::Game` 视口内容,并把 ImGui 键鼠状态逐帧发布为 `GameViewInputFrameEvent`。 ## 概述 -当前 `GameViewPanel` 和 [SceneViewPanel](../SceneViewPanel/SceneViewPanel.md) 一样,都属于轻量占位面板。 +`GameViewPanel` 当前已经不是单纯的“薄视口壳层”。按 `GameViewPanel.cpp` 的真实实现,它承担两条并行职责: -它目前主要做两件事: +1. 调用 [ViewportPanelContent](../ViewportPanelContent/ViewportPanelContent.md) 承载 `EditorViewportKind::Game` 对应的视口纹理。 +2. 从 ImGui 读取当前 Game View 的键鼠状态,打包成 [GameViewInputFrameEvent](../../Core/EditorEvents/EditorEvents.md),经 [EventBus](../../Core/EventBus/EventBus.md) 逐帧发布给 [PlaySessionController](../../Core/PlaySessionController/PlaySessionController.md)。 -- 以 `"Game"` 为名字打开一个面板窗口 -- 在该窗口激活时通知 `Actions` 层观察焦点路由 +因此它已经是当前 Editor 里“运行时输入桥”的起点,而不是只负责显示画面。 + +如果你想顺着一条连续链路理解“Game View 采样到的输入最终怎样进入运行时 `InputManager`”,可以继续看: + +- [Game View Runtime Input Bridge](../../../../_guides/Editor/Game-View-Runtime-Input-Bridge.md) + +## 生命周期与公开入口 + +- [Constructor](Constructor.md) + 创建标题固定为 `"Game"` 的 Game 视图面板。 +- [Render](Render.md) + 渲染 Game 视口,并在每帧末尾发布 `GameViewInputFrameEvent`。 + +## 当前执行链路 + +`Render()` 当前的真实顺序是: + +1. 把窗口边框宽度压到 `0.0f`。 +2. 打开 `PanelWindowScope("Game")`。 +3. 如果面板未打开,立即发布一个空的 `GameViewInputFrameEvent{}`,然后返回。 +4. 如果面板打开,调用: + +```cpp +RenderViewportPanelContent(*m_context, EditorViewportKind::Game); +``` + +5. 基于返回的 `ViewportPanelContentResult` 构建 `GameViewInputFrameEvent`。 +6. 通过 `context->GetEventBus().Publish(event)` 发布这一帧输入快照。 +7. 调用 `Actions::ObserveInactiveActionRoute(*m_context)`。 + +这意味着 `GameViewPanel` 每一帧都会发事件,不是“只有输入变化时才发”。关闭面板时发布空事件,也是一种有意设计,用来通知下游桥接层释放之前维持的运行时输入状态。 + +## `GameViewInputFrameEvent` 是什么 + +`GameViewPanel` 发布的不是离散键盘事件,而是逐帧快照。当前快照包含: + +- `hovered` +- `focused` +- `mousePosition` +- `mouseDelta` +- `mouseWheel` +- `keyDown[256]` +- `mouseButtonDown[5]` + +它的含义是: + +- `mousePosition` + - 以 Game View 视口左上角为原点的局部坐标。 +- `mouseDelta` + - 当前帧 ImGui 观察到的鼠标位移。 +- `mouseWheel` + - 只有在 `content.hovered == true` 时才会写入 `io.MouseWheel`,否则强制为 `0.0f`。 +- `keyDown` / `mouseButtonDown` + - 表示“这一帧视图认为当前哪些键 / 鼠标按钮处于按住态”。 + +它是状态快照,不是 down/up 事件流。 + +## 键鼠采集规则 + +### 键盘 + +`FillGameViewKeyboardState(...)` 当前维护一张固定映射表,把 `ImGuiKey` 映射到 `XCEngine::Input::KeyCode`,包括: + +- `A-Z` +- `0-9` +- `Space` / `Tab` / `Enter` / `Escape` +- 左右 `Shift` / `Ctrl` / `Alt` +- 方向键、`Home/End/PageUp/PageDown` +- `Delete` / `Backspace` +- `F1-F12` +- 若干标点键 + +所以 `GameViewPanel` 当前不是把所有 ImGui 按键原样透传,而是只透传这张映射表覆盖到的子集。 + +### 鼠标 + +`FillGameViewMouseState(...)` 当前只采集: + +- Left +- Right +- Middle + +并把它们写进 `mouseButtonDown` 数组。 + +### hovered / focused 门控 + +只有在: + +- `event.hovered == true` + - 或 +- `event.focused == true` + +时,`GameViewPanel` 才会填充键盘和鼠标按钮状态。 + +这条规则很关键,因为它决定了: + +- 视口失焦时不会继续把旧键盘状态送进运行时 +- 只要视口仍有焦点,即使鼠标暂时不在上面,键盘状态仍然可以继续驱动 play mode + +## 和 `PlaySessionController` 的关系 + +`GameViewPanel` 自己不会直接调用运行时 [InputManager](../../../Input/InputManager/InputManager.md)。它只负责: + +- 采集 ImGui 输入快照 +- 经 `EventBus` 发布 `GameViewInputFrameEvent` + +真正的桥接发生在 [PlaySessionController](../../Core/PlaySessionController/PlaySessionController.md): + +`GameViewPanel` -> `EventBus.Publish(GameViewInputFrameEvent)` -> `PlaySessionController::ApplyGameViewInputFrame()` -> `InputManager::Process*` + +这条拆分让 `GameViewPanel` 仍然保持在面板层,而不会直接知道 play mode 状态机和运行时输入细节。 + +## 与 SceneView 的关系 + +当前 `GameViewPanel` 与 [SceneViewPanel](../SceneViewPanel/SceneViewPanel.md) 的关系是: + +- 两者都复用 [ViewportPanelContent](../ViewportPanelContent/ViewportPanelContent.md) 作为视口承载 helper。 +- `SceneViewPanel` 把输入主要送给 editor camera / gizmo 交互。 +- `GameViewPanel` 把输入主要送进 `GameViewInputFrameEvent`,再桥接到运行时 `InputManager`。 + +所以两者虽然共用 viewport 承载层,但输入去向完全不同。 ## 当前实现边界 -- 当前没有真正的 runtime frame 嵌入或 play mode 画面输出。 -- 它现在更像未来 Game View 能力的承载容器。 +- 当前页本身不提供 play / pause / step 控件;这些仍属于菜单栏、动作路由和 `PlaySessionController`。 +- 当前没有额外的 Game View toolbar、缩放菜单或 aspect 预设逻辑。 +- 当前发布的是状态快照,不是字符输入或完整原始平台消息。 ## 相关文档 - [panels](../panels.md) -- [Actions](../../Actions/Actions.md) +- [Constructor](Constructor.md) +- [Render](Render.md) +- [ViewportPanelContent](../ViewportPanelContent/ViewportPanelContent.md) +- [EditorEvents](../../Core/EditorEvents/EditorEvents.md) +- [EventBus](../../Core/EventBus/EventBus.md) +- [PlaySessionController](../../Core/PlaySessionController/PlaySessionController.md) +- [Game View Runtime Input Bridge](../../../../_guides/Editor/Game-View-Runtime-Input-Bridge.md) +- [SceneViewPanel](../SceneViewPanel/SceneViewPanel.md) diff --git a/docs/api/XCEngine/Editor/panels/GameViewPanel/Render.md b/docs/api/XCEngine/Editor/panels/GameViewPanel/Render.md new file mode 100644 index 00000000..bb3c4501 --- /dev/null +++ b/docs/api/XCEngine/Editor/panels/GameViewPanel/Render.md @@ -0,0 +1,93 @@ +# GameViewPanel::Render + +**命名空间**: `XCEngine::Editor` + +**类型**: `method` + +**源文件**: `editor/src/panels/GameViewPanel.h` + +## 签名 + +```cpp +void Render() override; +``` + +## 作用 + +绘制 `Game` 视口面板,并把当前帧 Game View 可见输入状态发布为 `GameViewInputFrameEvent`。 + +## 当前实现行为 + +### 1. 建立面板外壳 + +- 先把 `ImGuiStyleVar_WindowBorderSize` 压成 `0.0f` +- 打开 `UI::PanelWindowScope("Game")` + +这一步只负责建立 Game 面板窗口本身,不包含工具栏或额外控制区。 + +### 2. 面板关闭时主动发布空事件 + +如果 `panel.IsOpen() == false`,当前实现不会静默返回,而是先执行: + +```cpp +PublishGameViewInputFrame(m_context, GameViewInputFrameEvent{}); +``` + +然后再返回。 + +这条路径的意义是: + +- 明确告诉下游“这一帧没有有效 Game View 输入” +- 让 [PlaySessionController](../../Core/PlaySessionController/PlaySessionController.md) 在下一帧桥接时释放之前保持的运行时按键和鼠标按钮状态 + +### 3. 请求 Game 视口内容 + +当面板打开时,会调用: + +```cpp +RenderViewportPanelContent(*m_context, EditorViewportKind::Game); +``` + +得到 `ViewportPanelContentResult`,其中包含: + +- 视口区域是否存在 +- `hovered` +- `focused` +- 视口内容矩形范围 + +这些信息随后会被用来构造 `GameViewInputFrameEvent`。 + +### 4. 构造输入快照 + +`BuildGameViewInputFrame(content)` 当前会按真实源码执行以下规则: + +- `content.hasViewportArea == false` 时,直接返回空事件 +- `mousePosition` 使用 `io.MousePos - content.itemMin`,也就是 Game View 局部坐标 +- `mouseDelta` 直接取 `io.MouseDelta` +- `mouseWheel` 只有在 `content.hovered == true` 时才保留,否则强制写 `0.0f` +- 只有在 `hovered || focused` 时,才填充: + - 键盘映射表覆盖到的 `keyDown` + - Left / Right / Middle 三个鼠标按钮的 `mouseButtonDown` + +### 5. 发布事件并继续动作观察 + +构造完成后会立即: + +1. `context->GetEventBus().Publish(event)` +2. `Actions::ObserveInactiveActionRoute(*m_context)` + +这说明 `Render()` 不是只负责画图,它同时还是当前 Game View 输入桥的事件源头。 + +## 当前实现边界 + +- 当前不绘制 play / pause / step 工具条。 +- 当前发布的是状态快照,不是字符输入或原始平台消息。 +- 当前键盘采样只覆盖 `GameViewPanel.cpp` 里维护的那张固定映射表,不是任意 `ImGuiKey` 都会桥接。 + +## 相关文档 + +- [GameViewPanel](GameViewPanel.md) +- [Constructor](Constructor.md) +- [EditorEvents](../../Core/EditorEvents/EditorEvents.md) +- [PlaySessionController](../../Core/PlaySessionController/PlaySessionController.md) +- [Game View Runtime Input Bridge](../../../../_guides/Editor/Game-View-Runtime-Input-Bridge.md) diff --git a/docs/api/_guides/Editor/Game-View-Runtime-Input-Bridge.md b/docs/api/_guides/Editor/Game-View-Runtime-Input-Bridge.md new file mode 100644 index 00000000..32669903 --- /dev/null +++ b/docs/api/_guides/Editor/Game-View-Runtime-Input-Bridge.md @@ -0,0 +1,195 @@ +# Game View Runtime Input Bridge + +这篇 guide 说明当前 Editor 里 `GameViewPanel -> EventBus -> PlaySessionController -> InputManager` 这条运行时输入桥接链路。 + +它不是抽象设计图,而是基于当前 `GameViewPanel.cpp`、`PlaySessionController.cpp` 和 `tests/editor/test_play_session_controller.cpp` 的真实实现整理出来的工作流说明。 + +## 参与者 + +- [GameViewPanel](../../XCEngine/Editor/panels/GameViewPanel/GameViewPanel.md) +- [EditorEvents / GameViewInputFrameEvent](../../XCEngine/Editor/Core/EditorEvents/EditorEvents.md) +- [EventBus](../../XCEngine/Editor/Core/EventBus/EventBus.md) +- [PlaySessionController](../../XCEngine/Editor/Core/PlaySessionController/PlaySessionController.md) +- [InputManager](../../XCEngine/Input/InputManager/InputManager.md) + +## 链路总览 + +当前链路固定按下面的顺序工作: + +1. `GameViewPanel::Render()` 采集当前帧 ImGui 键鼠状态。 +2. 面板把这些状态封装成 `GameViewInputFrameEvent`。 +3. 事件通过 `EventBus` 发布。 +4. `PlaySessionController::Attach()` 期间注册的订阅回调把它缓存到 `m_pendingGameViewInput`。 +5. 只有 play session 正在运行时,`PlaySessionController::Update()` 才会调用 `ApplyGameViewInputFrame(deltaTime)`。 +6. `ApplyGameViewInputFrame(...)` 先推进 `InputManager` 的帧边界,再把快照差异翻译成标准 `ProcessKey* / ProcessMouse*` 调用。 + +所以运行时输入不是 UI 层直接写进 `InputManager`,而是经过一层显式桥接。 + +## 第一步:GameViewPanel 采样输入 + +`GameViewPanel` 每一帧都会发布一次事件,不依赖“本帧有没有输入变化”。 + +### 面板关闭 + +如果面板当前没打开,`Render()` 会立即发布一个空的 `GameViewInputFrameEvent{}`。 +这不是多余操作,而是为了通知桥接层: + +- 这一帧已经没有有效 Game View 输入 +- 之前保持的运行时按键状态应该在下一帧被释放 + +### 视口打开 + +视口打开时,`BuildGameViewInputFrame(...)` 会使用 `ViewportPanelContentResult` 构建快照: + +- `hovered` +- `focused` +- `mousePosition` +- `mouseDelta` +- `mouseWheel` +- `keyDown` +- `mouseButtonDown` + +这里有三个实现细节需要特别注意: + +1. `mousePosition` 是相对 Game View 左上角的局部坐标,不是整窗口坐标。 +2. `mouseWheel` 只有在 `hovered == true` 时才保留。 +3. 只有 `hovered || focused` 时,面板才会填充键盘和鼠标按钮按住态。 + +## 第二步:EventBus 只传快照,不做运行时逻辑 + +`GameViewInputFrameEvent` 是一个轻量状态结构,不带成员函数,也不在事件总线层做任何特殊处理。 +`EventBus` 当前只负责把这份快照广播给订阅者。 + +这意味着: + +- `GameViewPanel` 不需要知道谁在消费事件 +- `PlaySessionController` 也不需要依赖面板实例 + +## 第三步:PlaySessionController 缓存 pending 输入 + +`PlaySessionController::Attach()` 订阅 `GameViewInputFrameEvent` 之后,当前回调只做两件事: + +- `m_pendingGameViewInput = event` +- `m_hasPendingGameViewInput = true` + +它不会在事件回调里立刻调用 `InputManager`。 +真正的桥接被推迟到 `Update()`,这样运行时输入的帧边界就能和 `RuntimeLoop` 保持一致。 + +## 第四步:只有 play session 运行时才会真正驱动 InputManager + +`PlaySessionController::Update()` 的第一道判断是: + +```cpp +if (!m_runtimeLoop.IsRunning()) { + return; +} +``` + +所以即使 `GameViewPanel` 继续每帧发事件: + +- 只要当前不在 Play / Paused 运行态 +- 这些事件都不会影响运行时 `InputManager` + +这也是为什么测试里要单独覆盖“非 Play mode 不驱动输入”。 + +## 第五步:ApplyGameViewInputFrame 如何把快照变成标准输入流 + +### 1. 先推进输入帧边界 + +函数一进入就先调用: + +```cpp +InputManager::Get().Update(deltaTime); +``` + +这一步会清理上一帧的 pressed / released / scroll / delta 这类瞬时状态。 + +### 2. 选择本帧输入来源 + +- 如果本帧收到了新的 `GameViewInputFrameEvent`,就使用 `m_pendingGameViewInput` +- 否则回退到空事件 + +随后会把 `m_hasPendingGameViewInput` 重新清成 `false`。 + +### 3. 计算输入是否激活 + +当前规则是: + +```cpp +input.hovered || input.focused +``` + +也就是说: + +- 鼠标悬停时,输入激活 +- 鼠标暂时移出但视口仍持有焦点时,输入仍激活 +- 两者都失去时,桥接层会把输入视为失活 + +### 4. 用 diff 方式补发 down / up + +桥接层不会把整份快照“整体覆盖”进 `InputManager`。 +它会拿本帧 `input` 和上一帧 `m_appliedGameViewInput` 逐项比较: + +- 键盘:`ProcessKeyDown(...)` / `ProcessKeyUp(...)` +- 鼠标按钮:`ProcessMouseButton(...)` +- 鼠标移动:`ProcessMouseMove(...)` +- 滚轮:`ProcessMouseWheel(...)` + +这样运行时侧看到的仍是标准事件流,所以: + +- `IsKeyPressed()` +- `IsKeyReleased()` +- `IsMouseButtonClicked()` + +这些查询都还能按 `InputManager` 的既有语义工作。 + +### 5. 失活时自动释放 + +函数末尾会先把 `m_appliedGameViewInput` 清空;只有当 `inputActive == true` 时,才把它改成当前快照。 +因此一旦: + +- 面板关闭 +- 视口没有被 hovered,也没有被 focused +- 或者本帧没有新的有效事件 + +下一帧比较时,桥接层就会把上一帧保持的 down 状态补发成 up。 + +这就是“Game View 失活时自动释放运行时输入”的真正来源。 + +## 进入和退出 Play Mode 时为什么要重建输入桥 + +`PlaySessionController::StartPlay()` 会先: + +- `ResetRuntimeInputBridge()` +- `InputManager::Shutdown()` +- `InputManager::Initialize(nullptr)` + +`StopPlay()` 则会: + +- `ResetRuntimeInputBridge()` +- `InputManager::Shutdown()` + +这保证了两件事: + +1. 编辑态不会把旧的运行时输入状态带进 play mode。 +2. 退出 play mode 后,运行时输入也不会残留回编辑态。 + +## 当前测试覆盖 + +[tests/editor/test_play_session_controller.cpp](D:/Xuanchi/Main/XCEngine/tests/editor/test_play_session_controller.cpp) 当前直接覆盖了两条关键规则: + +- 非 Play mode 下,`GameViewInputFrameEvent` 不会驱动 `InputManager` +- Play mode 下,输入会被桥接,并在空事件后释放 + +## 当前实现边界 + +- 当前桥接只处理 `GameViewInputFrameEvent`,不是平台原始输入直通。 +- 当前键盘映射只覆盖 `GameViewPanel.cpp` 明确列出的 `ImGuiKey -> KeyCode` 子集。 +- 当前鼠标按钮只采集 Left / Right / Middle。 + +## 相关文档 + +- [GameViewPanel](../../XCEngine/Editor/panels/GameViewPanel/GameViewPanel.md) +- [EditorEvents](../../XCEngine/Editor/Core/EditorEvents/EditorEvents.md) +- [PlaySessionController](../../XCEngine/Editor/Core/PlaySessionController/PlaySessionController.md) +- [Input Flow and Frame Semantics](../Input/Input-Flow-and-Frame-Semantics.md) diff --git a/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md b/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md index fbb22c50..87c62efb 100644 --- a/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md +++ b/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md @@ -1,5 +1,22 @@ # SceneViewport Overlay/Gizmo Rework Checkpoint +## Update 2026-04-04 Phase 5C + +### Transform Gizmo Coordinator Completed + +- Added `SceneViewportTransformGizmoCoordinator.{h,cpp}` to formalize transform gizmo overlay submission and drag lifecycle dispatch. +- `SceneViewPanel` no longer assembles transform gizmo overlay state inline or manually switches between `TryBeginDrag(...)`, `UpdateDrag(...)`, and `EndDrag(...)`. +- Overlay submission now flows through `BuildSceneViewportTransformGizmoOverlaySubmission(...)` and `SubmitSceneViewportTransformGizmoOverlaySubmission(...)`. +- Drag lifecycle intent now flows through explicit lifecycle commands, keeping the existing interaction timing unchanged while shrinking panel-owned orchestration. + +### Verification + +- `cmake --build build --config Debug --target editor_tests -- /p:BuildProjectReferences=false` +- `build/tests/Editor/Debug/editor_tests.exe --gtest_filter=SceneViewportInteractionActionsTest.*:SceneViewportInteractionResolverTest.*:SceneViewportTransformGizmoCoordinatorTest.*:SceneViewportOverlayRenderer_Test.*:SceneViewportOverlayProviderRegistryTest.*:ViewportRenderFlowUtilsTest.*` +- `cmake --build build --config Debug --target XCEditor` + +All commands completed successfully in `Debug`. + ## Update 2026-04-04 Phase 5B ### Interaction Actions Completed diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 2dd71a25..c02bfbbb 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -85,6 +85,7 @@ add_executable(${PROJECT_NAME} WIN32 src/Viewport/SceneViewportHudOverlay.cpp src/Viewport/SceneViewportInteractionActions.cpp src/Viewport/SceneViewportInteractionResolver.cpp + src/Viewport/SceneViewportTransformGizmoCoordinator.cpp src/Viewport/SceneViewportOrientationGizmo.cpp src/Viewport/SceneViewportOverlayBuilder.cpp src/Viewport/SceneViewportOverlayProviders.cpp diff --git a/editor/src/Viewport/SceneViewportTransformGizmoCoordinator.cpp b/editor/src/Viewport/SceneViewportTransformGizmoCoordinator.cpp new file mode 100644 index 00000000..6007bc9e --- /dev/null +++ b/editor/src/Viewport/SceneViewportTransformGizmoCoordinator.cpp @@ -0,0 +1,156 @@ +#include "SceneViewportTransformGizmoCoordinator.h" + +#include "Core/IUndoManager.h" + +namespace XCEngine { +namespace Editor { + +SceneViewportTransformGizmoOverlaySubmission BuildSceneViewportTransformGizmoOverlaySubmission( + const SceneViewportTransformGizmoFrameState& frameState, + bool showingMoveGizmo, + const SceneViewportMoveGizmo& moveGizmo, + bool showingRotateGizmo, + const SceneViewportRotateGizmo& rotateGizmo, + bool showingScaleGizmo, + const SceneViewportScaleGizmo& scaleGizmo) { + SceneViewportTransformGizmoOverlaySubmission submission = {}; + submission.overlayState = BuildSceneViewportTransformGizmoOverlayState( + BuildSceneViewportTransformGizmoHandleBuildInputs( + showingMoveGizmo, + moveGizmo, + frameState.moveContext, + showingRotateGizmo, + rotateGizmo, + frameState.rotateContext, + showingScaleGizmo, + scaleGizmo, + frameState.scaleContext)); + submission.activeGizmoKind = GetActiveSceneViewportGizmoKind(moveGizmo, rotateGizmo, scaleGizmo); + return submission; +} + +void SubmitSceneViewportTransformGizmoOverlaySubmission( + IViewportHostService& viewportHostService, + const SceneViewportTransformGizmoOverlaySubmission& submission) { + viewportHostService.SetSceneViewTransformGizmoOverlayState(submission.overlayState); +} + +SceneViewportTransformGizmoLifecycleCommand BuildBeginSceneViewportTransformGizmoLifecycleCommand( + const SceneViewportInteractionActions& actions) { + if (!actions.beginTransformGizmo) { + return {}; + } + + SceneViewportTransformGizmoLifecycleCommand command = {}; + command.stage = SceneViewportTransformGizmoLifecycleStage::Begin; + command.gizmoKind = actions.hoveredGizmoKind; + return command; +} + +SceneViewportTransformGizmoLifecycleCommand BuildFrameSceneViewportTransformGizmoLifecycleCommand( + SceneViewportActiveGizmoKind activeGizmoKind, + bool leftMouseDown) { + if (activeGizmoKind == SceneViewportActiveGizmoKind::None) { + return {}; + } + + SceneViewportTransformGizmoLifecycleCommand command = {}; + command.stage = leftMouseDown + ? SceneViewportTransformGizmoLifecycleStage::Update + : SceneViewportTransformGizmoLifecycleStage::End; + command.gizmoKind = activeGizmoKind; + return command; +} + +namespace { + +void ExecuteMoveGizmoLifecycleCommand( + SceneViewportTransformGizmoLifecycleStage stage, + IUndoManager& undoManager, + const SceneViewportMoveGizmoContext& context, + SceneViewportMoveGizmo& moveGizmo) { + switch (stage) { + case SceneViewportTransformGizmoLifecycleStage::Begin: + moveGizmo.TryBeginDrag(context, undoManager); + return; + case SceneViewportTransformGizmoLifecycleStage::Update: + moveGizmo.UpdateDrag(context); + return; + case SceneViewportTransformGizmoLifecycleStage::End: + moveGizmo.EndDrag(undoManager); + return; + case SceneViewportTransformGizmoLifecycleStage::None: + default: + return; + } +} + +void ExecuteRotateGizmoLifecycleCommand( + SceneViewportTransformGizmoLifecycleStage stage, + IUndoManager& undoManager, + const SceneViewportRotateGizmoContext& context, + SceneViewportRotateGizmo& rotateGizmo) { + switch (stage) { + case SceneViewportTransformGizmoLifecycleStage::Begin: + rotateGizmo.TryBeginDrag(context, undoManager); + return; + case SceneViewportTransformGizmoLifecycleStage::Update: + rotateGizmo.UpdateDrag(context); + return; + case SceneViewportTransformGizmoLifecycleStage::End: + rotateGizmo.EndDrag(undoManager); + return; + case SceneViewportTransformGizmoLifecycleStage::None: + default: + return; + } +} + +void ExecuteScaleGizmoLifecycleCommand( + SceneViewportTransformGizmoLifecycleStage stage, + IUndoManager& undoManager, + const SceneViewportScaleGizmoContext& context, + SceneViewportScaleGizmo& scaleGizmo) { + switch (stage) { + case SceneViewportTransformGizmoLifecycleStage::Begin: + scaleGizmo.TryBeginDrag(context, undoManager); + return; + case SceneViewportTransformGizmoLifecycleStage::Update: + scaleGizmo.UpdateDrag(context); + return; + case SceneViewportTransformGizmoLifecycleStage::End: + scaleGizmo.EndDrag(undoManager); + return; + case SceneViewportTransformGizmoLifecycleStage::None: + default: + return; + } +} + +} // namespace + +void ExecuteSceneViewportTransformGizmoLifecycleCommand( + const SceneViewportTransformGizmoLifecycleCommand& command, + IUndoManager& undoManager, + const SceneViewportTransformGizmoFrameState& frameState, + SceneViewportMoveGizmo& moveGizmo, + SceneViewportRotateGizmo& rotateGizmo, + SceneViewportScaleGizmo& scaleGizmo) { + switch (command.gizmoKind) { + case SceneViewportActiveGizmoKind::Move: + ExecuteMoveGizmoLifecycleCommand(command.stage, undoManager, frameState.moveContext, moveGizmo); + return; + case SceneViewportActiveGizmoKind::Rotate: + ExecuteRotateGizmoLifecycleCommand(command.stage, undoManager, frameState.rotateContext, rotateGizmo); + return; + case SceneViewportActiveGizmoKind::Scale: + ExecuteScaleGizmoLifecycleCommand(command.stage, undoManager, frameState.scaleContext, scaleGizmo); + return; + case SceneViewportActiveGizmoKind::None: + default: + return; + } +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportTransformGizmoCoordinator.h b/editor/src/Viewport/SceneViewportTransformGizmoCoordinator.h new file mode 100644 index 00000000..5a58db58 --- /dev/null +++ b/editor/src/Viewport/SceneViewportTransformGizmoCoordinator.h @@ -0,0 +1,68 @@ +#pragma once + +#include "SceneViewportInteractionActions.h" +#include "SceneViewportOverlayHandleBuilder.h" + +#include + +namespace XCEngine { +namespace Editor { + +class IUndoManager; + +struct SceneViewportTransformGizmoOverlaySubmission { + SceneViewportTransformGizmoOverlayState overlayState = {}; + SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None; + + bool GizmoActive() const { + return activeGizmoKind != SceneViewportActiveGizmoKind::None; + } +}; + +enum class SceneViewportTransformGizmoLifecycleStage : uint8_t { + None = 0, + Begin, + Update, + End +}; + +struct SceneViewportTransformGizmoLifecycleCommand { + SceneViewportTransformGizmoLifecycleStage stage = SceneViewportTransformGizmoLifecycleStage::None; + SceneViewportActiveGizmoKind gizmoKind = SceneViewportActiveGizmoKind::None; + + bool HasWork() const { + return stage != SceneViewportTransformGizmoLifecycleStage::None && + gizmoKind != SceneViewportActiveGizmoKind::None; + } +}; + +SceneViewportTransformGizmoOverlaySubmission BuildSceneViewportTransformGizmoOverlaySubmission( + const SceneViewportTransformGizmoFrameState& frameState, + bool showingMoveGizmo, + const SceneViewportMoveGizmo& moveGizmo, + bool showingRotateGizmo, + const SceneViewportRotateGizmo& rotateGizmo, + bool showingScaleGizmo, + const SceneViewportScaleGizmo& scaleGizmo); + +void SubmitSceneViewportTransformGizmoOverlaySubmission( + IViewportHostService& viewportHostService, + const SceneViewportTransformGizmoOverlaySubmission& submission); + +SceneViewportTransformGizmoLifecycleCommand BuildBeginSceneViewportTransformGizmoLifecycleCommand( + const SceneViewportInteractionActions& actions); + +SceneViewportTransformGizmoLifecycleCommand BuildFrameSceneViewportTransformGizmoLifecycleCommand( + SceneViewportActiveGizmoKind activeGizmoKind, + bool leftMouseDown); + +void ExecuteSceneViewportTransformGizmoLifecycleCommand( + const SceneViewportTransformGizmoLifecycleCommand& command, + IUndoManager& undoManager, + const SceneViewportTransformGizmoFrameState& frameState, + SceneViewportMoveGizmo& moveGizmo, + SceneViewportRotateGizmo& rotateGizmo, + SceneViewportScaleGizmo& scaleGizmo); + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index 9fe691bb..387d3387 100644 --- a/editor/src/panels/SceneViewPanel.cpp +++ b/editor/src/panels/SceneViewPanel.cpp @@ -7,8 +7,8 @@ #include "Viewport/SceneViewportHudOverlay.h" #include "Viewport/SceneViewportInteractionActions.h" #include "Viewport/SceneViewportInteractionResolver.h" -#include "Viewport/SceneViewportOverlayHandleBuilder.h" #include "Viewport/SceneViewportMath.h" +#include "Viewport/SceneViewportTransformGizmoCoordinator.h" #include "Viewport/SceneViewportTransformGizmoFrameBuilder.h" #include "ViewportPanelContent.h" #include "Platform/Win32Utf8.h" @@ -346,7 +346,6 @@ void SceneViewPanel::Render() { SceneViewportOverlayData overlay = {}; SceneViewportTransformGizmoFrameState gizmoFrameState = {}; SceneViewportOverlayFrameData emptySceneOverlayFrameData = {}; - SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None; if (hasInteractiveViewport) { overlay = viewportHostService->GetSceneViewOverlayData(); @@ -364,44 +363,28 @@ void SceneViewPanel::Render() { m_rotateGizmo, showingScaleGizmo, m_scaleGizmo); - activeGizmoKind = gizmoFrameState.activeGizmoKind; } else { CancelSceneViewportTransformGizmoDrags(*m_context, m_moveGizmo, m_rotateGizmo, m_scaleGizmo); } - const SceneViewportTransformGizmoHandleBuildInputs interactionGizmoInputs = + const SceneViewportTransformGizmoOverlaySubmission interactionGizmoSubmission = hasInteractiveViewport - ? BuildSceneViewportTransformGizmoHandleBuildInputs( + ? BuildSceneViewportTransformGizmoOverlaySubmission( + gizmoFrameState, showingMoveGizmo, m_moveGizmo, - gizmoFrameState.moveContext, showingRotateGizmo, m_rotateGizmo, - gizmoFrameState.rotateContext, showingScaleGizmo, - m_scaleGizmo, - gizmoFrameState.scaleContext) - : SceneViewportTransformGizmoHandleBuildInputs{}; - const SceneViewportTransformGizmoOverlayState interactionGizmoOverlayState = - BuildSceneViewportTransformGizmoOverlayState(interactionGizmoInputs); - viewportHostService->SetSceneViewTransformGizmoOverlayState(interactionGizmoOverlayState); + m_scaleGizmo) + : SceneViewportTransformGizmoOverlaySubmission{}; + SubmitSceneViewportTransformGizmoOverlaySubmission(*viewportHostService, interactionGizmoSubmission); const SceneViewportOverlayFrameData& interactionOverlayFrameData = hasInteractiveViewport ? viewportHostService->GetSceneViewEditorOverlayFrameData(*m_context) : emptySceneOverlayFrameData; - const bool moveGizmoActive = showingMoveGizmo && m_moveGizmo.IsActive(); - const bool rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive(); - const bool scaleGizmoActive = showingScaleGizmo && m_scaleGizmo.IsActive(); - if (moveGizmoActive) { - activeGizmoKind = SceneViewportActiveGizmoKind::Move; - } else if (rotateGizmoActive) { - activeGizmoKind = SceneViewportActiveGizmoKind::Rotate; - } else if (scaleGizmoActive) { - activeGizmoKind = SceneViewportActiveGizmoKind::Scale; - } else { - activeGizmoKind = SceneViewportActiveGizmoKind::None; - } - const bool gizmoActive = activeGizmoKind != SceneViewportActiveGizmoKind::None; + const SceneViewportActiveGizmoKind activeGizmoKind = interactionGizmoSubmission.activeGizmoKind; + const bool gizmoActive = interactionGizmoSubmission.GizmoActive(); const SceneViewportHudOverlayData interactionHudOverlay = BuildSceneViewportHudOverlayData(overlay); SceneViewportInteractionResult hoveredInteraction = {}; @@ -471,15 +454,13 @@ void SceneViewPanel::Render() { ImGui::SetWindowFocus(); } - if (interactionActions.beginTransformGizmo) { - if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) { - m_scaleGizmo.TryBeginDrag(gizmoFrameState.scaleContext, m_context->GetUndoManager()); - } else if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) { - m_moveGizmo.TryBeginDrag(gizmoFrameState.moveContext, m_context->GetUndoManager()); - } else if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) { - m_rotateGizmo.TryBeginDrag(gizmoFrameState.rotateContext, m_context->GetUndoManager()); - } - } + ExecuteSceneViewportTransformGizmoLifecycleCommand( + BuildBeginSceneViewportTransformGizmoLifecycleCommand(interactionActions), + m_context->GetUndoManager(), + gizmoFrameState, + m_moveGizmo, + m_rotateGizmo, + m_scaleGizmo); DispatchSceneViewportInteractionActions( interactionActions, @@ -488,25 +469,15 @@ void SceneViewPanel::Render() { content.availableSize, localMousePosition); - if (gizmoActive) { - if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { - if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) { - m_moveGizmo.UpdateDrag(gizmoFrameState.moveContext); - } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) { - m_rotateGizmo.UpdateDrag(gizmoFrameState.rotateContext); - } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) { - m_scaleGizmo.UpdateDrag(gizmoFrameState.scaleContext); - } - } else { - if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) { - m_moveGizmo.EndDrag(m_context->GetUndoManager()); - } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) { - m_rotateGizmo.EndDrag(m_context->GetUndoManager()); - } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) { - m_scaleGizmo.EndDrag(m_context->GetUndoManager()); - } - } - } + ExecuteSceneViewportTransformGizmoLifecycleCommand( + BuildFrameSceneViewportTransformGizmoLifecycleCommand( + activeGizmoKind, + ImGui::IsMouseDown(ImGuiMouseButton_Left)), + m_context->GetUndoManager(), + gizmoFrameState, + m_moveGizmo, + m_rotateGizmo, + m_scaleGizmo); if (beginLookDrag) { m_lookDragging = true; @@ -604,18 +575,16 @@ void SceneViewPanel::Render() { showingScaleGizmo, m_scaleGizmo); - viewportHostService->SetSceneViewTransformGizmoOverlayState( - BuildSceneViewportTransformGizmoOverlayState( - BuildSceneViewportTransformGizmoHandleBuildInputs( - showingMoveGizmo, - m_moveGizmo, - drawGizmoFrameState.moveContext, - showingRotateGizmo, - m_rotateGizmo, - drawGizmoFrameState.rotateContext, - showingScaleGizmo, - m_scaleGizmo, - drawGizmoFrameState.scaleContext))); + SubmitSceneViewportTransformGizmoOverlaySubmission( + *viewportHostService, + BuildSceneViewportTransformGizmoOverlaySubmission( + drawGizmoFrameState, + showingMoveGizmo, + m_moveGizmo, + showingRotateGizmo, + m_rotateGizmo, + showingScaleGizmo, + m_scaleGizmo)); DrawSceneViewportHudOverlay( ImGui::GetWindowDrawList(), diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index 2a177aff..44f4a4ba 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -13,6 +13,7 @@ set(EDITOR_TEST_SOURCES test_scene_viewport_picker.cpp test_scene_viewport_interaction_actions.cpp test_scene_viewport_interaction_resolver.cpp + test_scene_viewport_transform_gizmo_coordinator.cpp test_scene_viewport_shader_paths.cpp test_scene_viewport_overlay_renderer.cpp test_scene_viewport_overlay_providers.cpp @@ -42,6 +43,7 @@ set(EDITOR_TEST_SOURCES ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportHudOverlay.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportInteractionActions.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportInteractionResolver.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportTransformGizmoCoordinator.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOrientationGizmo.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayBuilder.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayProviders.cpp diff --git a/tests/editor/test_scene_viewport_transform_gizmo_coordinator.cpp b/tests/editor/test_scene_viewport_transform_gizmo_coordinator.cpp new file mode 100644 index 00000000..92afae77 --- /dev/null +++ b/tests/editor/test_scene_viewport_transform_gizmo_coordinator.cpp @@ -0,0 +1,109 @@ +#include + +#include "Core/IUndoManager.h" +#include "Viewport/IViewportHostService.h" +#include "Viewport/SceneViewportTransformGizmoCoordinator.h" + +#include + +namespace { + +using XCEngine::Editor::BuildBeginSceneViewportTransformGizmoLifecycleCommand; +using XCEngine::Editor::BuildFrameSceneViewportTransformGizmoLifecycleCommand; +using XCEngine::Editor::BuildSceneViewportTransformGizmoOverlaySubmission; +using XCEngine::Editor::EditorViewportFrame; +using XCEngine::Editor::EditorViewportKind; +using XCEngine::Editor::IEditorContext; +using XCEngine::Editor::IViewportHostService; +using XCEngine::Editor::SceneViewportActiveGizmoKind; +using XCEngine::Editor::SceneViewportInput; +using XCEngine::Editor::SceneViewportInteractionActions; +using XCEngine::Editor::SceneViewportOrientationAxis; +using XCEngine::Editor::SceneViewportOverlayData; +using XCEngine::Editor::SceneViewportOverlayFrameData; +using XCEngine::Editor::SceneViewportTransformGizmoFrameState; +using XCEngine::Editor::SceneViewportTransformGizmoLifecycleStage; +using XCEngine::Editor::SceneViewportTransformGizmoOverlayState; +using XCEngine::Editor::SubmitSceneViewportTransformGizmoOverlaySubmission; +using XCEngine::Rendering::RenderContext; + +class StubViewportHostService : public IViewportHostService { +public: + void BeginFrame() override {} + EditorViewportFrame RequestViewport(EditorViewportKind, const ImVec2&) override { return {}; } + void UpdateSceneViewInput(IEditorContext&, const SceneViewportInput&) override {} + uint64_t PickSceneViewEntity(IEditorContext&, const ImVec2&, const ImVec2&) override { return 0; } + void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis) override {} + SceneViewportOverlayData GetSceneViewOverlayData() const override { return {}; } + const SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext&) override { + return overlayFrameData; + } + void SetSceneViewTransformGizmoOverlayState(const SceneViewportTransformGizmoOverlayState& state) override { + lastSubmission = state; + } + void RenderRequestedViewports(IEditorContext&, const RenderContext&) override {} + + SceneViewportTransformGizmoOverlayState lastSubmission = {}; + +private: + SceneViewportOverlayFrameData overlayFrameData = {}; +}; + +} // namespace + +TEST(SceneViewportTransformGizmoCoordinatorTest, BuildBeginCommandUsesHoveredGizmoKind) { + SceneViewportInteractionActions actions = {}; + actions.beginTransformGizmo = true; + actions.hoveredGizmoKind = SceneViewportActiveGizmoKind::Rotate; + + const auto command = BuildBeginSceneViewportTransformGizmoLifecycleCommand(actions); + + EXPECT_EQ(command.stage, SceneViewportTransformGizmoLifecycleStage::Begin); + EXPECT_EQ(command.gizmoKind, SceneViewportActiveGizmoKind::Rotate); + EXPECT_TRUE(command.HasWork()); +} + +TEST(SceneViewportTransformGizmoCoordinatorTest, BuildFrameCommandMapsUpdateAndEnd) { + const auto updateCommand = BuildFrameSceneViewportTransformGizmoLifecycleCommand( + SceneViewportActiveGizmoKind::Move, + true); + const auto endCommand = BuildFrameSceneViewportTransformGizmoLifecycleCommand( + SceneViewportActiveGizmoKind::Scale, + false); + + EXPECT_EQ(updateCommand.stage, SceneViewportTransformGizmoLifecycleStage::Update); + EXPECT_EQ(updateCommand.gizmoKind, SceneViewportActiveGizmoKind::Move); + EXPECT_EQ(endCommand.stage, SceneViewportTransformGizmoLifecycleStage::End); + EXPECT_EQ(endCommand.gizmoKind, SceneViewportActiveGizmoKind::Scale); +} + +TEST(SceneViewportTransformGizmoCoordinatorTest, OverlaySubmissionBuildsAndSubmitsState) { + XCEngine::Components::GameObject gameObject("CoordinatorTestObject"); + XCEngine::Editor::SceneViewportMoveGizmo moveGizmo = {}; + XCEngine::Editor::SceneViewportRotateGizmo rotateGizmo = {}; + XCEngine::Editor::SceneViewportScaleGizmo scaleGizmo = {}; + SceneViewportTransformGizmoFrameState frameState = {}; + frameState.moveContext.selectedObject = &gameObject; + + const auto submission = BuildSceneViewportTransformGizmoOverlaySubmission( + frameState, + true, + moveGizmo, + false, + rotateGizmo, + false, + scaleGizmo); + + EXPECT_TRUE(submission.overlayState.hasMoveGizmo); + EXPECT_EQ(submission.overlayState.moveEntityId, gameObject.GetID()); + EXPECT_FALSE(submission.overlayState.hasRotateGizmo); + EXPECT_FALSE(submission.overlayState.hasScaleGizmo); + EXPECT_EQ(submission.activeGizmoKind, SceneViewportActiveGizmoKind::None); + EXPECT_FALSE(submission.GizmoActive()); + + StubViewportHostService viewportHostService = {}; + SubmitSceneViewportTransformGizmoOverlaySubmission(viewportHostService, submission); + + EXPECT_TRUE(viewportHostService.lastSubmission.hasMoveGizmo); + EXPECT_EQ(viewportHostService.lastSubmission.moveEntityId, gameObject.GetID()); +}