docs: add editor runtime flow docs
This commit is contained in:
@@ -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` 负责把启动路径和底层状态翻译成可见的宿主行为
|
||||
|
||||
## 相关文档
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
## 当前实现边界
|
||||
|
||||
- 当前所有服务都在构造时固定实例化,不是懒加载。
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -6,38 +6,125 @@
|
||||
|
||||
**源文件**: `editor/src/Core/EventBus.h`
|
||||
|
||||
**描述**: 提供编辑器内部的轻量级泛型事件总线,支持按事件类型订阅、取消订阅、发布和清空。
|
||||
**描述**: 编辑器内部的同步类型事件总线,提供基于模板事件类型的订阅、取消订阅、发布与清空能力。
|
||||
|
||||
## 概述
|
||||
|
||||
`EventBus` 是当前编辑器内部通信的基础设施。
|
||||
`EventBus` 是当前 Editor 内部最基础的解耦设施之一。
|
||||
|
||||
它的核心机制是:
|
||||
它的角色很明确:
|
||||
|
||||
- 每个事件类型通过 `EventTypeId<T>` 获得一个静态类型 ID。
|
||||
- `Subscribe<T>()` 返回 handler ID。
|
||||
- `Publish<T>()` 根据类型 ID 找到对应处理器并逐个调用。
|
||||
- 让面板、宿主层和 manager 可以通过事件协作。
|
||||
- 避免 `MainMenu`、`Application`、`HierarchyPanel`、`InspectorPanel` 之间直接互相依赖。
|
||||
- 保持事件类型是静态类型,而不是字符串 topic。
|
||||
|
||||
同时这个头里还定义了:
|
||||
例如当前实际用到的事件就包括:
|
||||
|
||||
- `EventTypeRegistry`
|
||||
- `EventTypeId<T>`
|
||||
- [SelectionChangedEvent](../EditorEvents/EditorEvents.md)
|
||||
- `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。
|
||||
- 如果某个 handler 在回调内部再次对同一个 `EventBus` 做需要独占锁的订阅/取消订阅操作,重入语义需要格外小心。
|
||||
- 当前没有事件排队、延迟分发或线程切换机制。
|
||||
- [EventTypeRegistry / EventTypeId](EventTypeRegistry-And-EventTypeId.md)
|
||||
|
||||
### 订阅与退订
|
||||
|
||||
- [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)
|
||||
- [EditorContext](../EditorContext/EditorContext.md)
|
||||
- [IEditorContext](../IEditorContext/IEditorContext.md)
|
||||
- [Application](../../Application/Application.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<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)
|
||||
52
docs/api/XCEngine/Editor/Core/EventBus/Publish-And-Clear.md
Normal file
52
docs/api/XCEngine/Editor/Core/EventBus/Publish-And-Clear.md
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
## 设计说明
|
||||
|
||||
|
||||
39
docs/api/XCEngine/Editor/Core/IEditorContext/Project-Path.md
Normal file
39
docs/api/XCEngine/Editor/Core/IEditorContext/Project-Path.md
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
40
docs/api/XCEngine/Editor/panels/GameViewPanel/Constructor.md
Normal file
40
docs/api/XCEngine/Editor/panels/GameViewPanel/Constructor.md
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
93
docs/api/XCEngine/Editor/panels/GameViewPanel/Render.md
Normal file
93
docs/api/XCEngine/Editor/panels/GameViewPanel/Render.md
Normal 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)
|
||||
195
docs/api/_guides/Editor/Game-View-Runtime-Input-Bridge.md
Normal file
195
docs/api/_guides/Editor/Game-View-Runtime-Input-Bridge.md
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
156
editor/src/Viewport/SceneViewportTransformGizmoCoordinator.cpp
Normal file
156
editor/src/Viewport/SceneViewportTransformGizmoCoordinator.cpp
Normal 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
|
||||
68
editor/src/Viewport/SceneViewportTransformGizmoCoordinator.h
Normal file
68
editor/src/Viewport/SceneViewportTransformGizmoCoordinator.h
Normal 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
|
||||
@@ -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(
|
||||
SubmitSceneViewportTransformGizmoOverlaySubmission(
|
||||
*viewportHostService,
|
||||
BuildSceneViewportTransformGizmoOverlaySubmission(
|
||||
drawGizmoFrameState,
|
||||
showingMoveGizmo,
|
||||
m_moveGizmo,
|
||||
drawGizmoFrameState.moveContext,
|
||||
showingRotateGizmo,
|
||||
m_rotateGizmo,
|
||||
drawGizmoFrameState.rotateContext,
|
||||
showingScaleGizmo,
|
||||
m_scaleGizmo,
|
||||
drawGizmoFrameState.scaleContext)));
|
||||
m_scaleGizmo));
|
||||
|
||||
DrawSceneViewportHudOverlay(
|
||||
ImGui::GetWindowDrawList(),
|
||||
|
||||
@@ -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
|
||||
|
||||
109
tests/editor/test_scene_viewport_transform_gizmo_coordinator.cpp
Normal file
109
tests/editor/test_scene_viewport_transform_gizmo_coordinator.cpp
Normal 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());
|
||||
}
|
||||
Reference in New Issue
Block a user