docs: add editor runtime flow docs

This commit is contained in:
2026-04-04 00:45:13 +08:00
parent 8abca3dec5
commit 2620b5914b
41 changed files with 2367 additions and 126 deletions

View File

@@ -19,12 +19,15 @@
- [EditorActionRoute](EditorActionRoute/EditorActionRoute.md) - [EditorActionRoute](EditorActionRoute/EditorActionRoute.md)
- [EditorConsoleSink](EditorConsoleSink/EditorConsoleSink.md) - [EditorConsoleSink](EditorConsoleSink/EditorConsoleSink.md)
- [EditorLoggingSetup](EditorLoggingSetup/EditorLoggingSetup.md) - [EditorLoggingSetup](EditorLoggingSetup/EditorLoggingSetup.md)
- [EditorRuntimeMode](EditorRuntimeMode/EditorRuntimeMode.md)
- [EditorWindowTitle](EditorWindowTitle/EditorWindowTitle.md) - [EditorWindowTitle](EditorWindowTitle/EditorWindowTitle.md)
- [EditorWorkspace](EditorWorkspace/EditorWorkspace.md) - [EditorWorkspace](EditorWorkspace/EditorWorkspace.md)
- [IProjectManager](IProjectManager/IProjectManager.md) - [IProjectManager](IProjectManager/IProjectManager.md)
- [ISceneManager](ISceneManager/ISceneManager.md) - [ISceneManager](ISceneManager/ISceneManager.md)
- [ISelectionManager](ISelectionManager/ISelectionManager.md) - [ISelectionManager](ISelectionManager/ISelectionManager.md)
- [IUndoManager](IUndoManager/IUndoManager.md) - [IUndoManager](IUndoManager/IUndoManager.md)
- [PlaySessionController](PlaySessionController/PlaySessionController.md)
- [ProjectRootResolver](ProjectRootResolver/ProjectRootResolver.md)
- [SceneSnapshot](SceneSnapshot/SceneSnapshot.md) - [SceneSnapshot](SceneSnapshot/SceneSnapshot.md)
- [SelectionManager](SelectionManager/SelectionManager.md) - [SelectionManager](SelectionManager/SelectionManager.md)
- [UndoManager](UndoManager/UndoManager.md) - [UndoManager](UndoManager/UndoManager.md)
@@ -41,9 +44,10 @@
当前还可以看到一个很清晰的工程化方向: 当前还可以看到一个很清晰的工程化方向:
- `EditorActionRoute` 负责动作路由语义 - `EditorActionRoute` 负责动作路由语义
- `EditorRuntimeMode + PlaySessionController` 负责编辑态 / 运行态切换和 play mode 主循环
- `SceneSnapshot + UndoManager` 负责状态回滚 - `SceneSnapshot + UndoManager` 负责状态回滚
- `EditorConsoleSink + EditorLoggingSetup` 负责把引擎日志接入编辑器壳层 - `EditorConsoleSink + EditorLoggingSetup` 负责把引擎日志接入编辑器壳层
- `EditorWindowTitle` 负责把底层状态翻译成可见的主窗口反馈 - `EditorWindowTitle + ProjectRootResolver` 负责把启动路径和底层状态翻译成可见的宿主行为
## 相关文档 ## 相关文档

View File

@@ -10,30 +10,75 @@
## 概述 ## 概述
`EditorActionRoute``ActionRouting``EditActionRouter` 之间的最小契约。 `EditorActionRoute`[ActionRouting](../../Actions/ActionRouting/ActionRouting.md)`EditActionRouter` 之间的最小契约。
当前枚举值只有三个: 当前枚举值只有三个:
- `None` - `None`
- `Hierarchy` - `Hierarchy`
- `Project` - `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 菜单可以统一生成启用状态 - 全局 Edit 菜单可以统一计算启用状态
- 快捷键处理逻辑可以收口到 `EditActionRouter` - 快捷键处理可以收口到 `EditActionRouter`
- 不同面板之间的编辑语义冲突更容易排查 - 不同面板之间的语义冲突更容易排查
这也是当前 editor 能同时支持 Hierarchy 和 Project 两套上下文敏感编辑动作的基础。
## 当前限制 ## 当前限制
- 当前只覆盖 `Hierarchy``Project` - 当前只定义了 `Hierarchy``Project``None`
- `Inspector``Console`、未来的材质编辑器或资源检查器都还没有自己的路由值 - `Inspector` 本身不拥有独立 route而是更多作为当前 route 的观察者和结果呈现者。
- 没有更细粒度的资源子类型 route也没有多窗口冲突仲裁。
## 相关文档 ## 相关文档
- [Core](../Core.md) - [Core](../Core.md)
- [ActionRouting](../../Actions/ActionRouting/ActionRouting.md) - [ActionRouting](../../Actions/ActionRouting/ActionRouting.md)
- [EditActionRouter](../../Actions/EditActionRouter/EditActionRouter.md) - [EditActionRouter](../../Actions/EditActionRouter/EditActionRouter.md)
- [InspectorPanel](../../panels/InspectorPanel/InspectorPanel.md)

View File

@@ -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<EntityDeletedEvent>(m_entityDeletedHandlerId);
```
- 其余服务对象由 `std::unique_ptr` 自动释放。
## 当前实现边界
- 当前所有核心服务都在构造时立即实例化,不做懒加载。
- “删除实体时自动清理选择”这条规则直接写在构造函数里,不是独立策略对象。
## 相关文档
- [EditorContext](EditorContext.md)
- [Service Accessors](Service-Accessors.md)
- [Runtime Mode And Project Path](Runtime-Mode-And-Project-Path.md)

View File

@@ -29,6 +29,24 @@
- 通过 `EditorActionRoute` 记录当前激活的动作路由。 - 通过 `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)
## 当前实现边界 ## 当前实现边界
- 当前所有服务都在构造时固定实例化,不是懒加载。 - 当前所有服务都在构造时固定实例化,不是懒加载。

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -6,7 +6,7 @@
**源文件**: `editor/src/Core/EditorEvents.h` **源文件**: `editor/src/Core/EditorEvents.h`
**描述**: 定义编辑器内部事件类型包括选择变化、实体生命周期、场景变化、播放模式变化、Dock 布局重置和退出请求等事件。 **描述**: 定义编辑器内部事件类型,包括选择变化、实体生命周期、场景变化、播放模式变化、Game View 输入快照、Dock 布局重置和退出请求等事件。
## 概述 ## 概述
@@ -16,29 +16,72 @@
## 当前事件 ## 当前事件
| 事件 | 作用 | ### 选择与实体事件
|------|------|
| `SelectionChangedEvent` | 选择集合变化。 | - [Selection And Entity Events](Selection-And-Entity-Events.md)
| `EntityCreatedEvent` | 新建实体。 |
| `EntityDeletedEvent` | 删除实体。 | ### 场景与 play mode 事件
| `EntityChangedEvent` | 实体内容变化。 |
| `EntityRenameRequestedEvent` | 请求重命名实体。 | - [Scene And PlayMode Events](Scene-And-PlayMode-Events.md)
| `EntityParentChangedEvent` | 实体父子关系变化。 |
| `SceneChangedEvent` | 当前场景发生变化。 | ### Game View 输入快照
| `PlayModeStartedEvent` / `Stopped` / `Paused` | 编辑器播放模式状态变化。 |
| `EditorModeChangedEvent` | 编辑器模式切换。 | - [GameViewInputFrameEvent](GameViewInputFrameEvent.md)
| `DockLayoutResetRequestedEvent` | 请求重置 dock 布局。 |
| `EditorExitRequestedEvent` | 请求关闭编辑器。 | ### 宿主与模式事件
- [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` 别名。 - `GameObjectID` 当前被定义为 `uint64_t` 别名。
- `SelectionChangedEvent` 同时包含完整选择列表和主选择。 - `SelectionChangedEvent` 同时包含完整选择列表和主选择。
- `EditorModeChangedEvent` 当前使用整数记录旧模式/新模式,而不是更强类型的枚举 - `GameViewInputFrameEvent` 里的 `mousePosition` 是相对 Game View 视口左上角的局部坐标
- `EditorModeChangedEvent` 当前使用强类型的 `EditorRuntimeMode`,不是裸整数。
## 相关文档 ## 相关文档
- [Core](../Core.md) - [Core](../Core.md)
- [EventBus](../EventBus/EventBus.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) - [SelectionManager](../SelectionManager/SelectionManager.md)
- [SceneManager](../../Managers/SceneManager/SceneManager.md) - [SceneManager](../../Managers/SceneManager/SceneManager.md)

View File

@@ -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<bool, KeyStateCount> keyDown = {};
std::array<bool, MouseButtonStateCount> 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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,63 @@
# EditorEvents Selection And Entity Events
**命名空间**: `XCEngine::Editor`
**类型**: `event group`
**源文件**: `editor/src/Core/EditorEvents.h`
## 作用
定义与当前选择集合和实体生命周期相关的编辑器事件类型。
## 当前事件
### `SelectionChangedEvent`
```cpp
struct SelectionChangedEvent {
std::vector<GameObjectID> 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)

View File

@@ -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)

View File

@@ -6,38 +6,125 @@
**源文件**: `editor/src/Core/EventBus.h` **源文件**: `editor/src/Core/EventBus.h`
**描述**: 提供编辑器内部的轻量级泛型事件总线,支持按事件类型订阅、取消订阅、发布清空。 **描述**: 编辑器内部的同步类型事件总线,提供基于模板事件类型订阅、取消订阅、发布清空能力
## 概述 ## 概述
`EventBus` 是当前编辑器内部通信的基础设施 `EventBus` 是当前 Editor 内部最基础的解耦设施之一
它的核心机制是 它的角色很明确
- 每个事件类型通过 `EventTypeId<T>` 获得一个静态类型 ID - 让面板、宿主层和 manager 可以通过事件协作
- `Subscribe<T>()` 返回 handler ID - 避免 `MainMenu``Application``HierarchyPanel``InspectorPanel` 之间直接互相依赖
- `Publish<T>()` 根据类型 ID 找到对应处理器并逐个调用 - 保持事件类型是静态类型,而不是字符串 topic
同时这个头里还定义了 例如当前实际用到的事件就包括
- `EventTypeRegistry` - [SelectionChangedEvent](../EditorEvents/EditorEvents.md)
- `EventTypeId<T>` - `EntityRenameRequestedEvent`
- `DockLayoutResetRequestedEvent`
- `EditorExitRequestedEvent`
## 当前实现说明 这是一种典型的编辑器架构做法。对工具软件来说,很多行为天然更像“广播一个状态变化”而不是“直接调用某个固定对象的方法”。
- 订阅表使用 `std::unordered_map<uint32_t, std::vector<HandlerEntry>>` ## 公开接口
- `Subscribe` / `Unsubscribe` 使用 `std::lock_guard<std::shared_mutex>`
- `Publish` 使用 `std::shared_lock<std::shared_mutex>`
- `Clear()` 会清空全部 handler。
## 当前实现风险与边界 ### 类型到运行时 id
- `Publish()` 在持有共享锁时直接执行 handler。 - [EventTypeRegistry / EventTypeId](EventTypeRegistry-And-EventTypeId.md)
- 如果某个 handler 在回调内部再次对同一个 `EventBus` 做需要独占锁的订阅/取消订阅操作,重入语义需要格外小心。
- 当前没有事件排队、延迟分发或线程切换机制。 ### 订阅与退订
- [Subscribe / Unsubscribe](Subscribe-And-Unsubscribe.md)
### 发布与清空
- [Publish / Clear](Publish-And-Clear.md)
## 当前实现行为
### 类型到运行时 id 的映射
`EventTypeId<T>::Get()` 会通过函数内静态值,为每个事件类型 `T` 只生成一次 `uint32_t` 类型 id。
这个 id 来自 `EventTypeRegistry::NextId()`,后者使用原子计数器递增。
这意味着:
- 事件类型不需要手工注册。
- 同一进程内,相同模板类型总能映射到同一个 id。
- 不同事件类型不会共享 handler 桶。
### handler 存储
当前内部存储结构是:
- `std::unordered_map<uint32_t, std::vector<HandlerEntry>> m_handlers`
其中 `HandlerEntry` 保存:
- `uint64_t id`
- `std::function<void(const void*)> handler`
`Subscribe<T>()` 会把用户传入的 `std::function<void(const T&)>` 包装成 `void*` 擦除后的回调,并存入对应 `typeId` 的数组。
### 锁与发布语义
当前实现最容易被旧文档写错的点就在这里:
- `Subscribe<T>()``Unsubscribe<T>()` 在持有独占锁时修改订阅表。
- `Publish<T>()` 在共享锁下读取当前 handler 列表,但不会在持锁期间执行回调。
`Publish<T>()` 的真实流程是:
1.`typeId`
2. 在共享锁下把该类型当前的 `HandlerEntry` 数组复制到局部变量。
3. 释放锁。
4. 逐个执行局部快照中的 handler。
所以当前语义不是“持锁回调”,而是“快照式同步发布”。
## 线程与重入语义
当前实现可以视为“结构线程安全,但不负责线程切换”的同步事件总线。
这具体意味着:
- 多线程同时订阅 / 取消订阅 / 发布,不会直接破坏内部容器。
- 但事件 handler 在哪个线程里执行,完全取决于谁调用了 `Publish<T>()`
- 如果在 handler 内部再次调用 `Subscribe<T>()``Unsubscribe<T>()`,不会与当前 `Publish<T>()` 形成死锁,因为回调执行时已经离开锁保护区。
同时也要注意:
- 当前这次 `Publish<T>()` 使用的是发布前复制的 handler 快照。
- 因此在回调过程中新增或移除的 handler只会影响下一次发布不会影响这一次。
这对编辑器工具链来说通常是更稳妥的选择。
因为 UI 事件、选择事件、菜单动作请求通常都希望“这次广播的收件人集合是稳定的”。
## 设计说明
当前 `EventBus` 的设计明显偏向编辑器工具场景,而不是高吞吐异步消息系统:
- 事件是强类型模板,不是字符串 topic。
- 分发是立即同步的,不做排队。
- 不尝试做线程切换、延迟执行或优先级调度。
这种设计的好处是:
- 调用栈清晰,调试容易。
- 面板与宿主层之间解耦,但仍然保留 C++ 静态类型安全。
- 对当前这种 UI 主线程驱动的 editor 足够简单可靠。
## 当前限制
- 当前没有“只触发一次”的订阅模式。
- 当前没有 handler 优先级、事件冒泡或取消传播机制。
- 当前没有主线程投递队列若从后台线程发布handler 也会在后台线程执行。
- 若 handler 抛出异常,`EventBus` 本身不做异常屏障。
## 相关文档 ## 相关文档
- [Core](../Core.md)
- [EditorEvents](../EditorEvents/EditorEvents.md) - [EditorEvents](../EditorEvents/EditorEvents.md)
- [EditorContext](../EditorContext/EditorContext.md) - [EditorContext](../EditorContext/EditorContext.md)
- [IEditorContext](../IEditorContext/IEditorContext.md)
- [Application](../../Application/Application.md)

View File

@@ -0,0 +1,49 @@
# EventTypeRegistry / EventTypeId
**命名空间**: `XCEngine::Editor`
**类型**: `helper types`
**源文件**: `editor/src/Core/EventBus.h`
## 签名
```cpp
class EventTypeRegistry {
public:
static uint32_t NextId();
};
template<typename T>
struct EventTypeId {
static uint32_t Get();
};
```
## 作用
`EventBus` 的模板事件类型分配稳定的运行时类型 id。
## 当前实现行为
### `EventTypeRegistry::NextId()`
- 使用一个进程内静态 `std::atomic<uint32_t>` 计数器。
- 每次调用都会通过 `fetch_add(1, std::memory_order_relaxed)` 生成新的类型 id。
### `EventTypeId<T>::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)

View File

@@ -0,0 +1,52 @@
# EventBus::Publish / Clear
**命名空间**: `XCEngine::Editor`
**类型**: `template method group`
**源文件**: `editor/src/Core/EventBus.h`
## 签名
```cpp
template<typename T>
void Publish(const T& event);
void Clear();
```
## 作用
同步发布一个事件实例,或清空总线中的全部 handler。
## 当前实现行为
### `Publish<T>(event)`
- 会先取出 `EventTypeId<T>::Get()`
- 在共享锁下查找当前事件类型的 handler 数组。
- 如果不存在订阅者,直接返回。
- 如果存在,会先把该数组复制到局部变量 `handlers`
- 释放锁后,再逐个执行局部快照里的回调。
## 关键语义
- 当前不是“持锁执行回调”,而是“快照式同步发布”。
- 回调发生在哪个线程,完全取决于谁调用了 `Publish<T>()`
- 在回调里新增或移除 handler只会影响下一次发布不会影响这一次。
### `Clear()`
- 在独占锁下直接清空 `m_handlers`
- 当前不会重置 `m_nextHandlerId`
## 设计含义
- `Publish<T>()` 的快照式实现避免了持锁回调导致的死锁或长时间锁占用。
- `Clear()` 更像生命周期收口操作,适合在整个编辑器上下文销毁时使用。
## 相关文档
- [EventBus](EventBus.md)
- [Subscribe / Unsubscribe](Subscribe-And-Unsubscribe.md)
- [EditorContext](../EditorContext/EditorContext.md)

View File

@@ -0,0 +1,50 @@
# EventBus::Subscribe / Unsubscribe
**命名空间**: `XCEngine::Editor`
**类型**: `template method group`
**源文件**: `editor/src/Core/EventBus.h`
## 签名
```cpp
template<typename T>
uint64_t Subscribe(std::function<void(const T&)> handler);
template<typename T>
void Unsubscribe(uint64_t handlerId);
```
## 作用
按事件类型注册或移除同步 handler。
## 当前实现行为
### `Subscribe<T>(handler)`
- 会先取出 `EventTypeId<T>::Get()` 作为当前事件类型桶 id。
- 在独占锁保护下:
- 生成新的 `handlerId`
- 确保该 `typeId` 对应的 `std::vector<HandlerEntry>` 已存在
-`std::function<void(const T&)>` 包装成擦除后的 `std::function<void(const void*)>`
- 追加到对应 handler 数组
- 最终返回这个 `handlerId`
### `Unsubscribe<T>(handlerId)`
- 同样先解析 `typeId`
- 在独占锁下找到对应 handler 数组
- 通过 `std::remove_if(...)` 删除指定 `handlerId`
## 线程语义
- 订阅和退订都会修改内部 handler 表,因此当前都持有独占锁。
- 它们不会直接执行任何事件回调。
## 相关文档
- [EventBus](EventBus.md)
- [EventTypeRegistry / EventTypeId](EventTypeRegistry-And-EventTypeId.md)
- [Publish / Clear](Publish-And-Clear.md)

View File

@@ -24,15 +24,17 @@
## 核心接口 ## 核心接口
| 方法 | 作用 | ### 核心服务访问
|------|------|
| `GetEventBus()` | 获取事件总线。 | - [Service Accessors](Service-Accessors.md)
| `GetSelectionManager()` | 获取选择管理服务。 |
| `GetSceneManager()` | 获取场景管理服务。 | ### 视口、动作路由与运行模式
| `GetProjectManager()` | 获取项目管理服务。 |
| `GetUndoManager()` | 获取撤销管理服务。 | - [Viewport, Action Route And Runtime Mode](Viewport-Action-Route-And-Runtime-Mode.md)
| `SetActiveActionRoute()` / `GetActiveActionRoute()` | 管理当前动作路由。 |
| `SetProjectPath()` / `GetProjectPath()` | 管理当前项目路径。 | ### 项目路径
- [Project Path](Project-Path.md)
## 设计说明 ## 设计说明

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -6,23 +6,151 @@
**源文件**: `editor/src/panels/GameViewPanel.h` **源文件**: `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"` 为名字打开一个面板窗口 因此它已经是当前 Editor 里“运行时输入桥”的起点,而不是只负责显示画面。
- 在该窗口激活时通知 `Actions` 层观察焦点路由
如果你想顺着一条连续链路理解“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 画面输出 - 当前页本身不提供 play / pause / step 控件;这些仍属于菜单栏、动作路由和 `PlaySessionController`
- 它现在更像未来 Game View 能力的承载容器 - 当前没有额外的 Game View toolbar、缩放菜单或 aspect 预设逻辑
- 当前发布的是状态快照,不是字符输入或完整原始平台消息。
## 相关文档 ## 相关文档
- [panels](../panels.md) - [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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -1,5 +1,22 @@
# SceneViewport Overlay/Gizmo Rework Checkpoint # 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 ## Update 2026-04-04 Phase 5B
### Interaction Actions Completed ### Interaction Actions Completed

View File

@@ -85,6 +85,7 @@ add_executable(${PROJECT_NAME} WIN32
src/Viewport/SceneViewportHudOverlay.cpp src/Viewport/SceneViewportHudOverlay.cpp
src/Viewport/SceneViewportInteractionActions.cpp src/Viewport/SceneViewportInteractionActions.cpp
src/Viewport/SceneViewportInteractionResolver.cpp src/Viewport/SceneViewportInteractionResolver.cpp
src/Viewport/SceneViewportTransformGizmoCoordinator.cpp
src/Viewport/SceneViewportOrientationGizmo.cpp src/Viewport/SceneViewportOrientationGizmo.cpp
src/Viewport/SceneViewportOverlayBuilder.cpp src/Viewport/SceneViewportOverlayBuilder.cpp
src/Viewport/SceneViewportOverlayProviders.cpp src/Viewport/SceneViewportOverlayProviders.cpp

View File

@@ -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

View File

@@ -0,0 +1,68 @@
#pragma once
#include "SceneViewportInteractionActions.h"
#include "SceneViewportOverlayHandleBuilder.h"
#include <cstdint>
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

View File

@@ -7,8 +7,8 @@
#include "Viewport/SceneViewportHudOverlay.h" #include "Viewport/SceneViewportHudOverlay.h"
#include "Viewport/SceneViewportInteractionActions.h" #include "Viewport/SceneViewportInteractionActions.h"
#include "Viewport/SceneViewportInteractionResolver.h" #include "Viewport/SceneViewportInteractionResolver.h"
#include "Viewport/SceneViewportOverlayHandleBuilder.h"
#include "Viewport/SceneViewportMath.h" #include "Viewport/SceneViewportMath.h"
#include "Viewport/SceneViewportTransformGizmoCoordinator.h"
#include "Viewport/SceneViewportTransformGizmoFrameBuilder.h" #include "Viewport/SceneViewportTransformGizmoFrameBuilder.h"
#include "ViewportPanelContent.h" #include "ViewportPanelContent.h"
#include "Platform/Win32Utf8.h" #include "Platform/Win32Utf8.h"
@@ -346,7 +346,6 @@ void SceneViewPanel::Render() {
SceneViewportOverlayData overlay = {}; SceneViewportOverlayData overlay = {};
SceneViewportTransformGizmoFrameState gizmoFrameState = {}; SceneViewportTransformGizmoFrameState gizmoFrameState = {};
SceneViewportOverlayFrameData emptySceneOverlayFrameData = {}; SceneViewportOverlayFrameData emptySceneOverlayFrameData = {};
SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None;
if (hasInteractiveViewport) { if (hasInteractiveViewport) {
overlay = viewportHostService->GetSceneViewOverlayData(); overlay = viewportHostService->GetSceneViewOverlayData();
@@ -364,44 +363,28 @@ void SceneViewPanel::Render() {
m_rotateGizmo, m_rotateGizmo,
showingScaleGizmo, showingScaleGizmo,
m_scaleGizmo); m_scaleGizmo);
activeGizmoKind = gizmoFrameState.activeGizmoKind;
} else { } else {
CancelSceneViewportTransformGizmoDrags(*m_context, m_moveGizmo, m_rotateGizmo, m_scaleGizmo); CancelSceneViewportTransformGizmoDrags(*m_context, m_moveGizmo, m_rotateGizmo, m_scaleGizmo);
} }
const SceneViewportTransformGizmoHandleBuildInputs interactionGizmoInputs = const SceneViewportTransformGizmoOverlaySubmission interactionGizmoSubmission =
hasInteractiveViewport hasInteractiveViewport
? BuildSceneViewportTransformGizmoHandleBuildInputs( ? BuildSceneViewportTransformGizmoOverlaySubmission(
gizmoFrameState,
showingMoveGizmo, showingMoveGizmo,
m_moveGizmo, m_moveGizmo,
gizmoFrameState.moveContext,
showingRotateGizmo, showingRotateGizmo,
m_rotateGizmo, m_rotateGizmo,
gizmoFrameState.rotateContext,
showingScaleGizmo, showingScaleGizmo,
m_scaleGizmo, m_scaleGizmo)
gizmoFrameState.scaleContext) : SceneViewportTransformGizmoOverlaySubmission{};
: SceneViewportTransformGizmoHandleBuildInputs{}; SubmitSceneViewportTransformGizmoOverlaySubmission(*viewportHostService, interactionGizmoSubmission);
const SceneViewportTransformGizmoOverlayState interactionGizmoOverlayState =
BuildSceneViewportTransformGizmoOverlayState(interactionGizmoInputs);
viewportHostService->SetSceneViewTransformGizmoOverlayState(interactionGizmoOverlayState);
const SceneViewportOverlayFrameData& interactionOverlayFrameData = const SceneViewportOverlayFrameData& interactionOverlayFrameData =
hasInteractiveViewport hasInteractiveViewport
? viewportHostService->GetSceneViewEditorOverlayFrameData(*m_context) ? viewportHostService->GetSceneViewEditorOverlayFrameData(*m_context)
: emptySceneOverlayFrameData; : emptySceneOverlayFrameData;
const bool moveGizmoActive = showingMoveGizmo && m_moveGizmo.IsActive(); const SceneViewportActiveGizmoKind activeGizmoKind = interactionGizmoSubmission.activeGizmoKind;
const bool rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive(); const bool gizmoActive = interactionGizmoSubmission.GizmoActive();
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 SceneViewportHudOverlayData interactionHudOverlay = const SceneViewportHudOverlayData interactionHudOverlay =
BuildSceneViewportHudOverlayData(overlay); BuildSceneViewportHudOverlayData(overlay);
SceneViewportInteractionResult hoveredInteraction = {}; SceneViewportInteractionResult hoveredInteraction = {};
@@ -471,15 +454,13 @@ void SceneViewPanel::Render() {
ImGui::SetWindowFocus(); ImGui::SetWindowFocus();
} }
if (interactionActions.beginTransformGizmo) { ExecuteSceneViewportTransformGizmoLifecycleCommand(
if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) { BuildBeginSceneViewportTransformGizmoLifecycleCommand(interactionActions),
m_scaleGizmo.TryBeginDrag(gizmoFrameState.scaleContext, m_context->GetUndoManager()); m_context->GetUndoManager(),
} else if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) { gizmoFrameState,
m_moveGizmo.TryBeginDrag(gizmoFrameState.moveContext, m_context->GetUndoManager()); m_moveGizmo,
} else if (interactionActions.hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) { m_rotateGizmo,
m_rotateGizmo.TryBeginDrag(gizmoFrameState.rotateContext, m_context->GetUndoManager()); m_scaleGizmo);
}
}
DispatchSceneViewportInteractionActions( DispatchSceneViewportInteractionActions(
interactionActions, interactionActions,
@@ -488,25 +469,15 @@ void SceneViewPanel::Render() {
content.availableSize, content.availableSize,
localMousePosition); localMousePosition);
if (gizmoActive) { ExecuteSceneViewportTransformGizmoLifecycleCommand(
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { BuildFrameSceneViewportTransformGizmoLifecycleCommand(
if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) { activeGizmoKind,
m_moveGizmo.UpdateDrag(gizmoFrameState.moveContext); ImGui::IsMouseDown(ImGuiMouseButton_Left)),
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) { m_context->GetUndoManager(),
m_rotateGizmo.UpdateDrag(gizmoFrameState.rotateContext); gizmoFrameState,
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) { m_moveGizmo,
m_scaleGizmo.UpdateDrag(gizmoFrameState.scaleContext); m_rotateGizmo,
} m_scaleGizmo);
} 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());
}
}
}
if (beginLookDrag) { if (beginLookDrag) {
m_lookDragging = true; m_lookDragging = true;
@@ -604,18 +575,16 @@ void SceneViewPanel::Render() {
showingScaleGizmo, showingScaleGizmo,
m_scaleGizmo); m_scaleGizmo);
viewportHostService->SetSceneViewTransformGizmoOverlayState( SubmitSceneViewportTransformGizmoOverlaySubmission(
BuildSceneViewportTransformGizmoOverlayState( *viewportHostService,
BuildSceneViewportTransformGizmoHandleBuildInputs( BuildSceneViewportTransformGizmoOverlaySubmission(
showingMoveGizmo, drawGizmoFrameState,
m_moveGizmo, showingMoveGizmo,
drawGizmoFrameState.moveContext, m_moveGizmo,
showingRotateGizmo, showingRotateGizmo,
m_rotateGizmo, m_rotateGizmo,
drawGizmoFrameState.rotateContext, showingScaleGizmo,
showingScaleGizmo, m_scaleGizmo));
m_scaleGizmo,
drawGizmoFrameState.scaleContext)));
DrawSceneViewportHudOverlay( DrawSceneViewportHudOverlay(
ImGui::GetWindowDrawList(), ImGui::GetWindowDrawList(),

View File

@@ -13,6 +13,7 @@ set(EDITOR_TEST_SOURCES
test_scene_viewport_picker.cpp test_scene_viewport_picker.cpp
test_scene_viewport_interaction_actions.cpp test_scene_viewport_interaction_actions.cpp
test_scene_viewport_interaction_resolver.cpp test_scene_viewport_interaction_resolver.cpp
test_scene_viewport_transform_gizmo_coordinator.cpp
test_scene_viewport_shader_paths.cpp test_scene_viewport_shader_paths.cpp
test_scene_viewport_overlay_renderer.cpp test_scene_viewport_overlay_renderer.cpp
test_scene_viewport_overlay_providers.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/SceneViewportHudOverlay.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportInteractionActions.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportInteractionActions.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportInteractionResolver.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/SceneViewportOrientationGizmo.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayBuilder.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayBuilder.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayProviders.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayProviders.cpp

View File

@@ -0,0 +1,109 @@
#include <gtest/gtest.h>
#include "Core/IUndoManager.h"
#include "Viewport/IViewportHostService.h"
#include "Viewport/SceneViewportTransformGizmoCoordinator.h"
#include <XCEngine/Components/GameObject.h>
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());
}