feat: expand editor scripting asset and viewport flow
395
AGENT.md
@@ -1,211 +1,242 @@
|
||||
# XCEngine Agent Guide
|
||||
|
||||
这个文件面向进入当前仓库的开发者 / Codex / Agent。目标不是介绍项目,而是快速建立对“当前工程状态、真实约束、推荐入口与协作方式”的统一认知。
|
||||
这个文件面向在当前仓库里工作的 coding agent / 开发者。它不负责介绍项目卖点,而是给出当前 checkout 的真实工程状态、优先入口、硬约束和推荐验证方式。
|
||||
|
||||
项目介绍与目录总览看 [README.md](README.md);设计原则与当前测试规则以本文为准。
|
||||
如果 README、旧文档和当前文件树 / `CMakeLists.txt` / 测试 target 冲突,以当前 checkout 为准,并在本次工作里顺手修正文档。
|
||||
|
||||
## 1. 先看什么
|
||||
## 1. 先建立的事实
|
||||
|
||||
进入仓库后,优先读这几份文档:
|
||||
- 顶层 `CMakeLists.txt` 当前纳入 `engine/`、`editor/`、`managed/`、`mvs/RenderDoc/` 和 `tests/`。
|
||||
- `engine/` 构建静态库 `XCEngine`;`editor/` 构建 `XCEditor`,但输出文件名仍是 `editor/bin/<Config>/XCEngine.exe`。
|
||||
- editor 默认把仓库内的 `project/` 识别为工程根目录,也支持 `--project <path>` 覆盖。
|
||||
- 当前工程不再只是 `Assets/` 目录:已经真实使用 `Assets/ + .meta + Library/` 的工程布局。
|
||||
- Mono 运行时与 editor 脚本类发现都从 `<project>/Library/ScriptAssemblies/` 加载程序集。
|
||||
- `engine/CMakeLists.txt` 当前对 Vulkan 是硬依赖;`editor/` 和 `tests/` 首次配置会拉取 `ImGui` 和 `googletest`。
|
||||
|
||||
## 2. 优先阅读顺序
|
||||
|
||||
进入仓库后,优先看这些文档和入口文件:
|
||||
|
||||
1. [AGENT.md](AGENT.md)
|
||||
2. [README.md](README.md)
|
||||
3. [docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md)
|
||||
4. [docs/plan/Renderer模块设计与实现.md](docs/plan/Renderer模块设计与实现.md)
|
||||
5. [tests/TEST_SPEC.md](tests/TEST_SPEC.md)
|
||||
4. [docs/plan/Shader与Material系统下一阶段计划.md](docs/plan/Shader与Material系统下一阶段计划.md)
|
||||
5. [docs/plan/SceneViewport_Overlay_Gizmo_Rework_Plan.md](docs/plan/SceneViewport_Overlay_Gizmo_Rework_Plan.md)
|
||||
6. [tests/TEST_SPEC.md](tests/TEST_SPEC.md)
|
||||
|
||||
其中:
|
||||
额外规则:
|
||||
|
||||
- `RHI模块总览.md` 是 RHI 设计原则基线
|
||||
- `Renderer模块设计与实现.md` 是当前渲染层演进方向
|
||||
- `TEST_SPEC.md` 是测试组织、GT 图规则和 CMake/CTest 入口基线
|
||||
- 如果任务落在某个模块里,先读该模块的 `CMakeLists.txt` 和最近的测试目录。
|
||||
- `tests/TEST_SPEC.md` 仍然适合作为 GT 图规则和 RHI 边界说明,但 target 名称与目录变化时,始终以当前 `tests/CMakeLists.txt` 和子模块 `CMakeLists.txt` 为准。
|
||||
|
||||
## 2. 当前工程状态
|
||||
## 3. 当前工程状态
|
||||
|
||||
### 2.1 引擎整体
|
||||
### 3.1 Engine 与工程布局
|
||||
|
||||
当前仓库已经不再处于“先把底层跑起来”的阶段,而是处于:
|
||||
当前仓库已经不在“先把底层 sample 跑起来”的阶段,而是已经形成:
|
||||
|
||||
- `RHI` 已具备可维护的三后端基线
|
||||
- `Rendering` 已形成正式模块,不再只是零散 sample 代码
|
||||
- `Editor` 已能通过引擎渲染链路驱动 viewport
|
||||
- `Scripting` 已具备 Mono 托管程序集构建和基础运行时测试
|
||||
- `RHI`
|
||||
- `Rendering`
|
||||
- `Editor viewport`
|
||||
- `AssetDatabase / Library`
|
||||
- `Mono scripting`
|
||||
|
||||
当前主线工作不应继续封闭式堆 RHI,而应在不破坏测试基线的前提下,继续推进渲染、编辑器和脚本三者的对接。
|
||||
这几条主线之间的真实对接。
|
||||
|
||||
### 2.2 RHI
|
||||
`Core/Asset/AssetDatabase` 现在是当前工程的重要基线,不是预研代码。它已经负责:
|
||||
|
||||
当前正式支持的后端:
|
||||
- 扫描 `Assets/`
|
||||
- 为资源生成 `.meta`
|
||||
- 维护 `Library/SourceAssetDB/assets.db`
|
||||
- 维护 `Library/ArtifactDB/artifacts.db`
|
||||
- 维护哈希化 `Library/Artifacts/`
|
||||
|
||||
- `D3D12`
|
||||
- `OpenGL`
|
||||
- `Vulkan`
|
||||
因此:
|
||||
|
||||
当前 RHI 的基本判断:
|
||||
- `project/Library/` 虽然可重建,但在当前 workflow 里不是可以随手忽略的“垃圾目录”。
|
||||
- 涉及资源导入、meta、artifact、脚本程序集发现时,不要擅自删除 `project/Library/` 或 `.meta` 文件来“清环境”。
|
||||
|
||||
- 抽象层已经可用
|
||||
- 后端差异路径已经被一轮轮集成测试逼出了很多真实问题并修过
|
||||
- 但它仍然不是“完全封顶”的模块
|
||||
### 3.2 Rendering
|
||||
|
||||
看到上层新问题时,不要本能地用“测试特判 / 测试绕过 / include 私有头”来糊过去;优先判断是不是 RHI 根因。
|
||||
|
||||
### 2.3 Rendering
|
||||
|
||||
当前 `engine/include/XCEngine/Rendering/` 与 `engine/src/Rendering/` 已经落地:
|
||||
当前 `engine/include/XCEngine/Rendering/` 与 `engine/src/Rendering/` 已经形成正式主链:
|
||||
|
||||
- `SceneRenderer`
|
||||
- `CameraRenderer`
|
||||
- `RenderPipeline`
|
||||
- `RenderSceneExtractor`
|
||||
- `RenderResourceCache`
|
||||
- `SceneRenderRequestPlanner`
|
||||
- `RenderSurface`
|
||||
- `CameraRenderer`
|
||||
- `SceneRenderer`
|
||||
- `BuiltinForwardPipeline`
|
||||
- `RenderMaterialUtility`
|
||||
|
||||
当前已经落地并应被视为正式能力的内容包括:
|
||||
|
||||
- 内建 forward 主几何渲染
|
||||
- `ObjectId` 渲染与 editor picking
|
||||
- `BuiltinInfiniteGridPass`
|
||||
- `BuiltinObjectIdOutlinePass`
|
||||
- `CameraRenderRequest::overlayPasses`
|
||||
|
||||
当前 Renderer 的下一阶段主线不是 render graph,而是:
|
||||
|
||||
- shader asset contract
|
||||
- material GPU binding
|
||||
- builtin pass contract
|
||||
- renderer-owned feature contract
|
||||
|
||||
对应设计文档是 [docs/plan/Shader与Material系统下一阶段计划.md](docs/plan/Shader与Material系统下一阶段计划.md)。
|
||||
|
||||
### 3.3 Editor
|
||||
|
||||
当前 editor 的事实:
|
||||
|
||||
- 它仍然是 `D3D12` 宿主应用。
|
||||
- Scene/Game viewport 已经通过引擎 `Rendering + RHI` 输出离屏纹理。
|
||||
- `ViewportHostService` 是 editor 与 renderer 的关键接线层。
|
||||
- object-id picking、selection outline、scene icon / gizmo overlay 已经进入正规化收口阶段。
|
||||
|
||||
当前 `editor/src/Viewport/` 已经存在:
|
||||
|
||||
- `SceneViewportOverlayBuilder`
|
||||
- `SceneViewportEditorOverlayPass`
|
||||
- `SceneViewportPicker`
|
||||
- `SceneViewportMoveGizmo`
|
||||
- `SceneViewportRotateGizmo`
|
||||
- `SceneViewportScaleGizmo`
|
||||
|
||||
这意味着:
|
||||
|
||||
- 当前已经存在正式的场景渲染运行时
|
||||
- 不要再把真实渲染逻辑塞回 `mvs/` 样例里长期存活
|
||||
- 新渲染功能优先落在 `Rendering` 模块与配套测试中
|
||||
- editor 是宿主,不是第二套 renderer。
|
||||
- 新的世界空间 overlay,不应继续堆回 `SceneViewPanel.cpp` 的 ImGui world overlay 路径。
|
||||
- 优先沿 `overlayPasses -> overlay builder -> canonical frame data -> overlay pass` 方向扩展。
|
||||
|
||||
### 2.4 Editor
|
||||
### 3.4 Scripting
|
||||
|
||||
当前 editor 的关键事实:
|
||||
当前脚本链路由三部分组成:
|
||||
|
||||
- 它仍然是 `D3D12` 宿主应用
|
||||
- Scene/Game viewport 已通过离屏纹理接入引擎 `SceneRenderer`
|
||||
- `editor/src/Viewport/ViewportHostService.h` 当前直接依赖 `RHI::D3D12Device`
|
||||
- Scene view 相机控制已经有独立控制器与单测
|
||||
- `managed/XCEngine.ScriptCore/`
|
||||
- `managed/GameScripts/`
|
||||
- `project/Assets/**/*.cs`
|
||||
|
||||
因此当前要注意:
|
||||
构建结果分两类:
|
||||
|
||||
- 引擎渲染逻辑应继续收敛在 `engine/Rendering`
|
||||
- editor 只是宿主,不应复制一套独立 renderer
|
||||
- editor 侧若要继续做 viewport 能力,应尽量围绕 `RenderSurface` 和引擎场景渲染入口扩展
|
||||
- `xcengine_managed_assemblies` 生成引擎示例程序集
|
||||
- `xcengine_project_managed_assemblies` 生成项目脚本程序集,并复制到 `project/Library/ScriptAssemblies/`
|
||||
|
||||
### 2.5 Scripting
|
||||
`ScriptEngine` 当前已经具备:
|
||||
|
||||
当前脚本链路:
|
||||
- 脚本类发现
|
||||
- 字段元数据读取
|
||||
- 默认值读取
|
||||
- stored override 管理
|
||||
- 运行时 managed field 同步
|
||||
|
||||
- `managed/XCEngine.ScriptCore/`:托管 API
|
||||
- `managed/GameScripts/`:托管测试 / 示例脚本
|
||||
- `engine/include/XCEngine/Scripting/` 与 `engine/src/Scripting/`:原生运行时桥接
|
||||
Inspector 侧已经存在 `ScriptComponentEditor`,因此脚本相关改动通常同时影响:
|
||||
|
||||
当前脚本构建依赖:
|
||||
- `engine/src/Scripting/`
|
||||
- `managed/`
|
||||
- `project/Assets/Scripts/`
|
||||
- `editor/src/ComponentEditors/`
|
||||
- `tests/Scripting/`
|
||||
- `tests/Editor/test_script_component_editor_utils.cpp`
|
||||
|
||||
- .NET SDK
|
||||
- `参考/Fermion/Fermion/external/mono` 下的 Mono 依赖
|
||||
### 3.5 Tests
|
||||
|
||||
如果环境不完整,可以通过 `-DXCENGINE_ENABLE_MONO_SCRIPTING=OFF` 暂时关闭。
|
||||
当前测试主目录包括:
|
||||
|
||||
## 3. 当前测试基线
|
||||
|
||||
### 3.1 测试树
|
||||
|
||||
当前主要测试模块:
|
||||
|
||||
- `tests/Core/`
|
||||
- `tests/Memory/`
|
||||
- `tests/Threading/`
|
||||
- `tests/Debug/`
|
||||
- `tests/Components/`
|
||||
- `tests/Core/`
|
||||
- `tests/Debug/`
|
||||
- `tests/Editor/`
|
||||
- `tests/Input/`
|
||||
- `tests/Memory/`
|
||||
- `tests/Rendering/`
|
||||
- `tests/Resources/`
|
||||
- `tests/RHI/`
|
||||
- `tests/Scene/`
|
||||
- `tests/Scripting/`
|
||||
- `tests/Rendering/`
|
||||
- `tests/RHI/`
|
||||
- `tests/Resources/`
|
||||
- `tests/Input/`
|
||||
- `tests/Editor/`
|
||||
- `tests/Threading/`
|
||||
|
||||
### 3.2 RHI 测试分层
|
||||
需要特别记住的聚合 target:
|
||||
|
||||
`tests/RHI/` 当前分为五块:
|
||||
- `rhi_all_tests`
|
||||
- `rendering_all_tests`
|
||||
- `rendering_phase_regression`
|
||||
- `editor_tests`
|
||||
- `scripting_tests`
|
||||
|
||||
- `tests/RHI/unit/`
|
||||
- `tests/RHI/integration/`
|
||||
- `tests/RHI/D3D12/`
|
||||
- `tests/RHI/OpenGL/`
|
||||
- `tests/RHI/Vulkan/`
|
||||
## 4. 不可忽视的硬约束
|
||||
|
||||
边界规则:
|
||||
### 4.1 文档服从真实 checkout
|
||||
|
||||
- `tests/RHI/unit/` 和 `tests/RHI/integration/` 只能依赖公共 RHI 抽象
|
||||
- 后端私有 API / 原生句柄 / 后端特有断言,只能进对应后端目录
|
||||
- 如果抽象层测试必须 include 后端头才能过,优先修 RHI
|
||||
如果文档与当前目录结构、target 名称、代码事实冲突:
|
||||
|
||||
### 3.3 RHI 抽象层集成测试
|
||||
- 先信当前 checkout
|
||||
- 再更新文档
|
||||
|
||||
当前抽象层场景:
|
||||
不要沿用“计划中但未落地”的旧说法。
|
||||
|
||||
- `minimal`
|
||||
- `triangle`
|
||||
- `quad`
|
||||
- `sphere`
|
||||
- `backpack`
|
||||
### 4.2 RHI 抽象层与后端层必须分层
|
||||
|
||||
必须遵守:
|
||||
`tests/RHI/unit/` 与 `tests/RHI/integration/` 只能依赖公共 RHI 抽象。
|
||||
|
||||
1. 每个场景目录只维护一张 `GT.ppm`
|
||||
2. `D3D12 / OpenGL / Vulkan` 都与同一张 `GT.ppm` 做比对
|
||||
3. 运行输出可以按后端区分命名,但 GT 不能拆成多份
|
||||
4. 场景测试代码只能使用公共 RHI 接口
|
||||
不要为了让抽象层测试通过而:
|
||||
|
||||
### 3.4 Rendering 测试
|
||||
- include 后端私有头
|
||||
- 直接使用原生句柄
|
||||
- 给单一后端写抽象层特判
|
||||
|
||||
当前 `tests/Rendering/` 已经存在:
|
||||
如果必须这么做,优先修 RHI,而不是污染测试边界。
|
||||
|
||||
- `unit/`
|
||||
- `integration/backpack_scene`
|
||||
- `integration/textured_quad_scene`
|
||||
- `integration/transparent_material_scene`
|
||||
- `integration/cull_material_scene`
|
||||
- `integration/depth_sort_scene`
|
||||
- `integration/material_state_scene`
|
||||
- `integration/offscreen_scene`
|
||||
### 4.3 Editor 是宿主,不是第二套渲染器
|
||||
|
||||
结论:
|
||||
|
||||
- 新渲染功能不需要再从零搭测试体系
|
||||
- 直接往现有 `tests/Rendering` 扩展即可
|
||||
|
||||
## 4. 已知关键约束
|
||||
|
||||
### 4.1 始终遵循 RHI 设计文档
|
||||
|
||||
涉及 RHI 抽象、后端收敛、接口新增时,始终以:
|
||||
|
||||
- [docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md)
|
||||
|
||||
作为原则基线,而不是局部方便优先。
|
||||
|
||||
### 4.2 功能正确优先于抽象表面整洁
|
||||
|
||||
允许为了功能正确做必要重构,但不允许:
|
||||
|
||||
- 测试特判掩盖真实后端缺陷
|
||||
- 为了不改底层而污染抽象层测试
|
||||
- 为了“少动代码”保留明显错误的对象边界
|
||||
|
||||
### 4.3 backpack 资源导入行为必须统一
|
||||
|
||||
这是当前仓库里已经踩过的真实坑。
|
||||
|
||||
规则:
|
||||
|
||||
- editor / runtime / rendering tests / rhi abstraction tests 的 backpack 导入行为必须保持一致
|
||||
- 不要只在某个测试路径里额外加 `MeshImportFlags::FlipUVs`
|
||||
- 否则很容易出现 editor 显示正确、测试里 UV 错乱的假分叉
|
||||
|
||||
### 4.4 editor 是宿主,不是第二套渲染器
|
||||
|
||||
如果 editor viewport 有问题,优先判断:
|
||||
如果 viewport、outline、picking 或 gizmo 有问题,优先判断:
|
||||
|
||||
- 是 `Rendering` 模块问题
|
||||
- 是 `RenderSurface` / RHI 输出问题
|
||||
- 还是 editor 宿主接线问题
|
||||
|
||||
不要把问题简单归因为“editor 特殊”,然后在 editor 里复制一份独立渲染逻辑。
|
||||
不要因为 editor 当前是 D3D12 host,就把问题草率地塞回 editor 私有渲染逻辑。
|
||||
|
||||
## 5. 推荐构建入口
|
||||
### 4.4 不要再扩写 ImGui world overlay
|
||||
|
||||
当前 viewport overlay / gizmo 已有明确收口方向。新功能若仍然继续堆在:
|
||||
|
||||
- `SceneViewPanel.cpp`
|
||||
- `SceneViewportOverlayRenderer.cpp` 的 ImGui world draw 路径
|
||||
|
||||
通常就是逆着当前架构方向在走。
|
||||
|
||||
优先入口是:
|
||||
|
||||
- `CameraRenderRequest::overlayPasses`
|
||||
- `SceneViewportOverlayBuilder`
|
||||
- `SceneViewportEditorOverlayPass`
|
||||
|
||||
### 4.5 backpack 导入行为必须统一
|
||||
|
||||
这是仓库里已经踩过的真实坑。
|
||||
|
||||
`backpack` 相关资源在以下路径中的导入行为必须保持一致:
|
||||
|
||||
- editor
|
||||
- runtime
|
||||
- rendering tests
|
||||
- rhi abstraction tests
|
||||
|
||||
不要只在局部路径里额外加 `MeshImportFlags::FlipUVs` 之类的补丁。
|
||||
|
||||
### 4.6 `mvs/` 不是长期主线模块
|
||||
|
||||
`mvs/` 里有样例、研究和工具,但当前正式引擎逻辑的长期落点应优先是:
|
||||
|
||||
- `engine/`
|
||||
- `editor/`
|
||||
- `managed/`
|
||||
- `tests/`
|
||||
|
||||
不要把正式渲染逻辑重新堆回 sample 子树长期存活。
|
||||
|
||||
## 5. 推荐构建与验证入口
|
||||
|
||||
### 5.1 配置
|
||||
|
||||
@@ -213,7 +244,7 @@
|
||||
cmake -S . -B build -A x64
|
||||
```
|
||||
|
||||
如果缺 Mono:
|
||||
如果当前任务不需要 Mono:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF
|
||||
@@ -223,69 +254,45 @@ cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target XCEngine
|
||||
cmake --build build --config Debug --target XCVolumeRendererUI2
|
||||
cmake --build build --config Debug --target XCEditor
|
||||
cmake --build build --config Debug --target xcengine_managed_assemblies
|
||||
cmake --build build --config Debug --target xcengine_project_managed_assemblies
|
||||
cmake --build build --config Debug --target rhi_all_tests
|
||||
cmake --build build --config Debug --target rendering_unit_tests
|
||||
cmake --build build --config Debug --target rendering_all_tests
|
||||
cmake --build build --config Debug --target rendering_phase_regression
|
||||
cmake --build build --config Debug --target editor_tests
|
||||
cmake --build build --config Debug --target scripting_tests
|
||||
```
|
||||
|
||||
补充:
|
||||
### 5.3 按改动类型选择验证
|
||||
|
||||
- 编辑器 target 名称是 `XCVolumeRendererUI2`
|
||||
- 输出文件名是 `editor/bin/<Config>/XCEngine.exe`
|
||||
- 改 `engine/RHI`:先跑 `rhi_abstraction_tests` 或 `rhi_backend_tests`,再决定是否扩展到 `rhi_all_tests`
|
||||
- 改 `engine/Rendering`:先跑 `rendering_unit_tests` 和最相关的 `rendering_integration_*`,必要时再跑 `rendering_phase_regression`
|
||||
- 改 `editor/Viewport` 或 Inspector:先跑 `editor_tests`
|
||||
- 改 `engine/Scripting`、`managed/`、`project/Assets/Scripts/`:先构建 `xcengine_project_managed_assemblies`,再跑 `scripting_tests`
|
||||
- 改资源导入、`.meta`、artifact 相关逻辑:优先跑 `tests/Resources/` 里的对应 target
|
||||
|
||||
## 6. 推荐验证入口
|
||||
|
||||
### 6.1 全量
|
||||
### 5.4 全量测试入口
|
||||
|
||||
```bash
|
||||
ctest --test-dir build -C Debug --output-on-failure
|
||||
```
|
||||
|
||||
### 6.2 RHI
|
||||
## 6. 按任务找入口
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target rhi_abstraction_tests
|
||||
cmake --build build --config Debug --target rhi_backend_tests
|
||||
```
|
||||
- RHI 抽象与后端:`engine/include/XCEngine/RHI/`、`engine/src/RHI/`、`tests/RHI/`
|
||||
- Rendering 主链与 pass:`engine/include/XCEngine/Rendering/`、`engine/src/Rendering/`、`tests/Rendering/`
|
||||
- Editor viewport / gizmo / picking:`editor/src/Viewport/`、`editor/src/panels/SceneViewPanel.cpp`、`tests/Editor/`
|
||||
- 资源导入与工程布局:`engine/include/XCEngine/Core/Asset/`、`engine/src/Core/Asset/`、`editor/src/Managers/ProjectManager.cpp`、`project/Assets/`、`project/Library/`
|
||||
- 脚本运行时与程序集:`engine/include/XCEngine/Scripting/`、`engine/src/Scripting/`、`managed/`、`project/Assets/Scripts/`、`tests/Scripting/`
|
||||
- 默认工程与项目描述:`project/Project.xcproject`、`editor/src/Core/ProjectRootResolver.h`、`editor/src/Utils/ProjectFileUtils.h`
|
||||
|
||||
### 6.3 Rendering
|
||||
## 7. 适合当前仓库的工作方式
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target rendering_unit_tests
|
||||
cmake --build build --config Debug --target rendering_integration_backpack_scene
|
||||
```
|
||||
1. 先读当前模块的 `CMakeLists.txt`、最近测试和设计文档,再动代码。
|
||||
2. 优先在既有模块边界里解决问题,不要绕开系统回到 sample 式实现。
|
||||
3. 先跑与改动最相关的最小验证,再决定是否扩大全量验证。
|
||||
4. 目录、target、入口、文档名改了,就同步更新 README / AGENT / 相关说明。
|
||||
5. 如果任务会有意重建 `project/Library/`、脚本程序集或 `.meta`,在结果里明确说明哪些文件是有意生成的。
|
||||
|
||||
### 6.4 Editor / Scripting
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target editor_tests
|
||||
cmake --build build --config Debug --target scripting_tests
|
||||
```
|
||||
|
||||
## 7. 当前最合理的工作方式
|
||||
|
||||
适用于当前仓库的协作方式:
|
||||
|
||||
1. 先在现有模块边界里定位问题,不要绕开体系补 sample 代码
|
||||
2. 改动后立即补或更新测试
|
||||
3. 先跑和改动最相关的测试,再决定是否扩大全量验证
|
||||
4. 每个阶段完成后尽快提交,保持工作区清晰
|
||||
|
||||
特别是涉及:
|
||||
|
||||
- `RHI`
|
||||
- `Rendering`
|
||||
- `Editor viewport`
|
||||
- `Scripting runtime`
|
||||
|
||||
这些模块时,必须避免“表面修好了,但测试基线退化”的情况。
|
||||
|
||||
## 8. 文档入口
|
||||
|
||||
- 项目概览与目录树:[README.md](README.md)
|
||||
- RHI 设计基线:[docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md)
|
||||
- Renderer 设计文档:[docs/plan/Renderer模块设计与实现.md](docs/plan/Renderer模块设计与实现.md)
|
||||
- 测试规范:[tests/TEST_SPEC.md](tests/TEST_SPEC.md)
|
||||
这份文档的作用是给 agent 一个“当前真实工程长什么样”的基线。它本身也必须随着工程演进一起维护,不能再落回旧状态说明。
|
||||
|
||||
@@ -14,7 +14,7 @@ set(
|
||||
"Path to the bundled Mono distribution used by the scripting runtime")
|
||||
|
||||
add_subdirectory(engine)
|
||||
add_subdirectory(Editor)
|
||||
add_subdirectory(editor)
|
||||
add_subdirectory(managed)
|
||||
add_subdirectory(mvs/RenderDoc)
|
||||
add_subdirectory(tests)
|
||||
|
||||
214
MVS/ui/README.md
@@ -1,183 +1,87 @@
|
||||
# UI Editor
|
||||
# Legacy UI Prototype
|
||||
|
||||
Unity 风格的编辑器 UI,使用 ImGui 实现,作为 XCEngine 游戏引擎编辑器的一部分。
|
||||
`mvs/ui/` 是仓库里保留的早期 ImGui + D3D12 编辑器原型。它主要用于保留原始 UI 骨架、交互想法和历史实现参考,不是当前 XCEngine 的正式 editor 主线。
|
||||
|
||||
## 简介
|
||||
当前正式 editor 在:
|
||||
|
||||
XCGameEngine UI 是一个仿 Unity 编辑器的桌面应用程序,提供场景管理、层级视图、属性检查器等功能。
|
||||
- [editor/README.md](D:\Xuanchi\Main\XCEngine\editor\README.md)
|
||||
|
||||
## 技术栈
|
||||
当前正式构建入口在仓库根目录:
|
||||
|
||||
- **渲染 API**: DirectX 12
|
||||
- **UI 框架**: ImGui
|
||||
- **语言**: C++17
|
||||
- **构建系统**: CMake
|
||||
- **依赖库**: DirectX 12 SDK
|
||||
- [README.md](D:\Xuanchi\Main\XCEngine\README.md)
|
||||
- [AGENT.md](D:\Xuanchi\Main\XCEngine\AGENT.md)
|
||||
|
||||
## 项目结构
|
||||
## 当前状态
|
||||
|
||||
```
|
||||
ui/
|
||||
这个模块仍然可以单独构建,但它有几个需要明确的事实:
|
||||
|
||||
- 顶层 `CMakeLists.txt` 当前并不会纳入 `mvs/ui/`
|
||||
- 它使用独立的 `mvs/ui/CMakeLists.txt`
|
||||
- target 名称仍然是历史遗留的 `XCVolumeRendererUI2`
|
||||
- 它不是当前 `editor/` 目录下那套 `ViewportHostService + Rendering + Project.xcproject + ScriptAssemblies` 架构
|
||||
- 它不代表当前仓库的真实 editor 能力边界
|
||||
|
||||
因此:
|
||||
|
||||
- 想用当前引擎编辑器,请进入 `editor/`
|
||||
- 想研究早期 UI 原型、旧面板布局和最初的 ImGui 宿主结构,可以看这里
|
||||
|
||||
## 这个目录里有什么
|
||||
|
||||
```text
|
||||
mvs/ui/
|
||||
├── CMakeLists.txt
|
||||
├── README.md
|
||||
├── src/
|
||||
│ ├── main.cpp # 程序入口
|
||||
│ ├── Application.cpp/h # 应用主类
|
||||
│ ├── Theme.cpp/h # 主题系统
|
||||
│ ├── Core/
|
||||
│ │ ├── GameObject.h # 游戏对象
|
||||
│ │ └── LogEntry.h # 日志条目
|
||||
│ ├── Managers/
|
||||
│ │ ├── LogSystem.cpp/h # 日志系统
|
||||
│ │ ├── ProjectManager.cpp/h # 项目管理
|
||||
│ │ ├── SceneManager.cpp/h # 场景管理
|
||||
│ │ └── SelectionManager.cpp/h # 选择管理
|
||||
│ └── panels/
|
||||
│ ├── Panel.cpp/h # 面板基类
|
||||
│ ├── MenuBar.cpp/h # 菜单栏
|
||||
│ ├── HierarchyPanel.cpp/h # 层级面板
|
||||
│ ├── InspectorPanel.cpp/h # 检查器面板
|
||||
│ ├── SceneViewPanel.cpp/h # 场景视图
|
||||
│ ├── GameViewPanel.cpp/h # 游戏视图
|
||||
│ ├── ProjectPanel.cpp/h # 项目面板
|
||||
│ └── ConsolePanel.cpp/h # 控制台面板
|
||||
├── bin/Release/ # 输出目录
|
||||
│ ├── XCVolumeRendererUI2.exe # 可执行文件
|
||||
│ ├── imgui.ini # ImGui 配置
|
||||
│ └── Assets/
|
||||
│ └── Models/
|
||||
│ └── Character.fbx # 示例模型
|
||||
├── build/ # 构建目录
|
||||
└── CMakeLists.txt # CMake 配置
|
||||
│ ├── panels/
|
||||
│ ├── Application.cpp
|
||||
│ ├── Application.h
|
||||
│ ├── Theme.cpp
|
||||
│ ├── Theme.h
|
||||
│ └── main.cpp
|
||||
├── build/ # 历史本地构建输出
|
||||
└── bin/ # 历史可执行文件输出
|
||||
```
|
||||
|
||||
## 构建方法
|
||||
主要内容是:
|
||||
|
||||
### 前置要求
|
||||
- 早期 `Application` / `Theme` 实现
|
||||
- 基础 `Hierarchy / Scene / Game / Inspector / Project / Console` 面板骨架
|
||||
- 旧版 `SceneManager / ProjectManager / LogSystem`
|
||||
|
||||
- Windows 10/11
|
||||
- Visual Studio 2019 或更高版本
|
||||
- CMake 3.15+
|
||||
## 单独构建方式
|
||||
|
||||
### 构建步骤
|
||||
如果你只是想启动这个原型,可以单独进入该目录配置:
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
cmake --build . --config Release
|
||||
cmake -S mvs/ui -B mvs/ui/build -A x64
|
||||
cmake --build mvs/ui/build --config Release
|
||||
```
|
||||
|
||||
### 运行
|
||||
输出可执行文件通常位于:
|
||||
|
||||
```bash
|
||||
# 运行编译好的可执行文件
|
||||
.\bin\Release\XCGameEngineUI.exe
|
||||
.\mvs\ui\bin\Release\XCVolumeRendererUI2.exe
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
## 与当前正式 editor 的区别
|
||||
|
||||
### 编辑器面板
|
||||
当前正式 editor 具备而这个原型没有正式接入的能力包括:
|
||||
|
||||
#### 菜单栏(MenuBar)
|
||||
- 文件菜单(新建、打开、保存等)
|
||||
- 编辑菜单(撤销、重做等)
|
||||
- 视图菜单(面板显示/隐藏)
|
||||
- 帮助菜单
|
||||
- `engine/Rendering` 主链驱动的 scene/game viewport
|
||||
- `ViewportHostService`
|
||||
- object-id picking 与 outline
|
||||
- `Project.xcproject`
|
||||
- `Assets + .meta + Library`
|
||||
- `project/Library/ScriptAssemblies`
|
||||
- `ScriptComponent` Inspector 与脚本类发现
|
||||
|
||||
#### 层级面板(Hierarchy Panel)
|
||||
- 显示场景中所有游戏对象
|
||||
- 树形结构展示父子关系
|
||||
- 支持对象选择
|
||||
- 对象重命名
|
||||
所以这个目录更适合被理解为:
|
||||
|
||||
#### 检查器面板(Inspector Panel)
|
||||
- 显示选中对象的属性
|
||||
- 支持组件编辑
|
||||
- 变换组件(位置、旋转、缩放)
|
||||
- 材质组件
|
||||
- 历史设计参考
|
||||
- 原型实现存档
|
||||
- 某些 UI 想法的对照样本
|
||||
|
||||
#### 场景视图(Scene View)
|
||||
- 3D 场景预览
|
||||
- 相机控制(平移、旋转、缩放)
|
||||
- 对象选择
|
||||
- 辅助工具(网格、轴心)
|
||||
|
||||
#### 游戏视图(Game View)
|
||||
- 游戏运行时的画面预览
|
||||
- 分辨率设置
|
||||
- 宽高比选择
|
||||
|
||||
#### 项目面板(Project Panel)
|
||||
- 项目文件浏览器
|
||||
- 资源组织
|
||||
- 搜索过滤
|
||||
|
||||
#### 控制台面板(Console Panel)
|
||||
- 日志输出
|
||||
- 警告和错误显示
|
||||
- 日志级别过滤
|
||||
- 清空日志
|
||||
|
||||
### 管理系统
|
||||
|
||||
#### 日志系统(LogSystem)
|
||||
- 分级日志(Info、Warning、Error)
|
||||
- 时间戳
|
||||
- 日志持久化
|
||||
|
||||
#### 项目管理(ProjectManager)
|
||||
- 项目创建/打开
|
||||
- 资源路径管理
|
||||
|
||||
#### 场景管理(SceneManager)
|
||||
- 场景加载/保存
|
||||
- 对象生命周期管理
|
||||
|
||||
#### 选择管理(SelectionManager)
|
||||
- 当前选中对象追踪
|
||||
- 多选支持
|
||||
|
||||
### 主题系统
|
||||
|
||||
- 深色主题(Dark Theme)
|
||||
- 可自定义配色方案
|
||||
|
||||
## 窗口布局
|
||||
|
||||
默认布局采用经典的 Unity 编辑器风格:
|
||||
|
||||
```
|
||||
+----------------------------------------------------------+
|
||||
| 菜单栏 |
|
||||
+----------+------------------------+----------------------+
|
||||
| | | |
|
||||
| 项目 | 场景视图 | 检查器 |
|
||||
| 面板 | | |
|
||||
| | | |
|
||||
+----------+------------------------+----------------------+
|
||||
| 层级面板 | 游戏视图 |
|
||||
| | |
|
||||
+------------------------------------+----------------------+
|
||||
| 控制台面板 |
|
||||
+----------------------------------------------------------+
|
||||
```
|
||||
|
||||
## 依赖说明
|
||||
|
||||
- ImGui - 跨平台 GUI 库
|
||||
- DirectX 12 - 渲染 API
|
||||
- Windows SDK - 窗口管理
|
||||
|
||||
## 扩展开发
|
||||
|
||||
### 添加新面板
|
||||
|
||||
1. 在 `panels/` 目录下创建新的面板类
|
||||
2. 继承 `Panel` 基类
|
||||
3. 实现 `Render()` 方法
|
||||
4. 在 `Application` 中注册新面板
|
||||
|
||||
### 添加新组件
|
||||
|
||||
1. 定义组件类
|
||||
2. 在 `GameObject` 中注册组件类型
|
||||
3. 在 `InspectorPanel` 中添加属性编辑器
|
||||
而不是当前 XCEngine editor 的入口。
|
||||
|
||||
297
README.md
@@ -1,48 +1,39 @@
|
||||
# XCEngine
|
||||
|
||||
XCEngine 是一个正在持续开发中的模块化 C++ 游戏引擎。当前仓库已经形成了较完整的底座:
|
||||
XCEngine 是一个 Windows 优先、编辑器优先的模块化 C++ 游戏引擎工作区。当前主线已经形成 `RHI -> Rendering -> Editor Viewport -> AssetDatabase/Library -> Mono Scripting` 的可运行闭环,不再只是示例代码集合。
|
||||
|
||||
- `RHI` 抽象层已稳定覆盖 `D3D12 / OpenGL / Vulkan`
|
||||
- `Rendering` 模块已经落地最小可用的场景渲染链路
|
||||
- `editor/` 已接入基于引擎渲染链路的 Scene/Game viewport
|
||||
- `managed/` 已具备基于 Mono 的 C# 运行时与托管程序集构建链路
|
||||
- `tests/` 已形成按模块分层的单元测试与集成测试体系
|
||||
这份 README 面向引擎用户:关注怎么进入项目、怎么构建、当前仓库各目录分别负责什么。面向构建本引擎的 coding agent,请看 [AGENT.md](AGENT.md)。
|
||||
|
||||
如果你是第一次进入当前仓库,先看 [AGENT.md](AGENT.md)。它更偏向“当前工程状态、协作约束与推荐入口”。
|
||||
## 项目定位
|
||||
|
||||
## 当前状态
|
||||
|
||||
- 核心引擎库位于 `engine/`,包含 Audio、Components、Core、Debug、Input、Memory、Platform、Rendering、Resources、RHI、Scene、Scripting、Threading。
|
||||
- RHI 当前正式支持 `D3D12 / OpenGL / Vulkan` 三后端,且抽象层与后端层测试都已经落地。
|
||||
- Rendering 当前已具备 `RenderSceneExtractor`、`RenderResourceCache`、`SceneRenderer`、`CameraRenderer`、`BuiltinForwardPipeline`。
|
||||
- Editor 当前是 `D3D12` 宿主应用,但场景绘制已通过引擎 `Rendering + RHI` 链路输出到离屏纹理,再接入 ImGui 面板。
|
||||
- Managed scripting 当前通过 `managed/XCEngine.ScriptCore` 与 `managed/GameScripts` 构建托管程序集;默认启用 Mono 运行时集成。
|
||||
- 默认示例工程位于 `project/`,已包含基础场景、背包模型与脚本资产目录。
|
||||
- `engine/` 提供静态库 `XCEngine`,包含 `RHI`、`Rendering`、`Resources`、`Scene`、`Scripting` 等核心模块。
|
||||
- `editor/` 提供桌面编辑器 `XCEditor`,输出文件名为 `XCEngine.exe`,默认打开仓库内的 `project/`。
|
||||
- `project/` 是当前随仓库维护的示例工程,已经采用 `Assets/ + .meta + Library/` 的工程布局。
|
||||
- `managed/` 负责 `XCEngine.ScriptCore.dll`、示例 `GameScripts.dll` 以及项目脚本程序集构建。
|
||||
- `tests/` 已覆盖 Engine、RHI、Rendering、Editor、Scripting 等主要模块。
|
||||
|
||||
## 环境要求
|
||||
|
||||
建议在 Windows 上构建当前仓库。
|
||||
当前推荐在 Windows 上使用和构建。
|
||||
|
||||
- Windows 10/11
|
||||
- Visual Studio 2022 / MSVC v143
|
||||
- CMake 3.15+
|
||||
- .NET SDK
|
||||
- Vulkan SDK
|
||||
- .NET SDK
|
||||
- Git LFS
|
||||
|
||||
如果启用默认脚本构建,还需要:
|
||||
启用 Mono 脚本运行时时,还需要:
|
||||
|
||||
- `参考/Fermion/Fermion/external/mono` 下可用的 Mono 头文件、静态库与 `mscorlib.dll`
|
||||
|
||||
如果本地暂时没有 Mono 依赖,可以在配置时关闭脚本构建:
|
||||
补充说明:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF
|
||||
```
|
||||
- `engine/CMakeLists.txt` 当前对 `Vulkan` 是硬依赖,未配置 Vulkan SDK 时无法完成配置。
|
||||
- `editor/` 和 `tests/` 首次配置会通过 `FetchContent` 从 Gitee 镜像拉取 `ImGui` 与 `googletest`;离线环境请确保已有可复用的 `_deps` 缓存。
|
||||
- 如果需要 `mvs/3DGS-Unity/room.ply` 这类大文件示例,请先执行 Git LFS 拉取。
|
||||
|
||||
如果你需要 `mvs/3DGS-Unity/room.ply` 这类大文件样例,请确保已经执行过 Git LFS 拉取。
|
||||
|
||||
## 构建
|
||||
## 快速开始
|
||||
|
||||
### 1. 配置
|
||||
|
||||
@@ -50,60 +41,92 @@ cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF
|
||||
cmake -S . -B build -A x64
|
||||
```
|
||||
|
||||
### 2. 全量编译
|
||||
如果本地暂时没有 Mono 依赖,可以先关闭脚本运行时:
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug
|
||||
cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF
|
||||
```
|
||||
|
||||
### 3. 常用增量 target
|
||||
### 2. 构建常用目标
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target XCEngine
|
||||
cmake --build build --config Debug --target XCVolumeRendererUI2
|
||||
cmake --build build --config Debug --target XCEditor
|
||||
cmake --build build --config Debug --target xcengine_managed_assemblies
|
||||
cmake --build build --config Debug --target rhi_all_tests
|
||||
cmake --build build --config Debug --target rendering_unit_tests
|
||||
cmake --build build --config Debug --target editor_tests
|
||||
cmake --build build --config Debug --target scripting_tests
|
||||
cmake --build build --config Debug --target xcengine_project_managed_assemblies
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 编辑器 CMake target 名称仍然是历史遗留的 `XCVolumeRendererUI2`,但输出文件名是 `XCEngine.exe`
|
||||
- 顶层 CMake 当前会纳入 `engine/`、`editor/`、`managed/`、`mvs/RenderDoc/` 与 `tests/`
|
||||
- `XCEngine` 是引擎静态库。
|
||||
- `XCEditor` 是编辑器 target,输出文件名仍为 `editor/bin/<Config>/XCEngine.exe`。
|
||||
- `xcengine_managed_assemblies` 生成 `managed/` 示例托管程序集。
|
||||
- `xcengine_project_managed_assemblies` 会扫描 `project/Assets/**/*.cs`,并把结果输出到 `project/Library/ScriptAssemblies/`。
|
||||
|
||||
## 测试
|
||||
### 3. 启动编辑器
|
||||
|
||||
推荐始终通过 CMake / CTest 驱动测试。
|
||||
```bash
|
||||
.\editor\bin\Debug\XCEngine.exe
|
||||
```
|
||||
|
||||
### 列出测试
|
||||
默认情况下,编辑器会自动把仓库内的 `project/` 识别为工程根目录。也可以显式指定其他工程:
|
||||
|
||||
```bash
|
||||
.\editor\bin\Debug\XCEngine.exe --project D:\Path\To\MyProject
|
||||
```
|
||||
|
||||
如果 Inspector 里看不到 C# 脚本类,先确认 `project/Library/ScriptAssemblies/` 中已经生成:
|
||||
|
||||
- `XCEngine.ScriptCore.dll`
|
||||
- `GameScripts.dll`
|
||||
- `mscorlib.dll`
|
||||
|
||||
### 4. 当前推荐验证入口
|
||||
|
||||
```bash
|
||||
ctest --test-dir build -N -C Debug
|
||||
```
|
||||
|
||||
### 运行全部测试
|
||||
|
||||
```bash
|
||||
ctest --test-dir build -C Debug --output-on-failure
|
||||
```
|
||||
|
||||
### 常用测试 target
|
||||
按模块常用的构建 / 验证 target:
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target rhi_abstraction_tests
|
||||
cmake --build build --config Debug --target rhi_backend_tests
|
||||
cmake --build build --config Debug --target rendering_unit_tests
|
||||
cmake --build build --config Debug --target rhi_all_tests
|
||||
cmake --build build --config Debug --target rendering_all_tests
|
||||
cmake --build build --config Debug --target rendering_phase_regression
|
||||
cmake --build build --config Debug --target editor_tests
|
||||
cmake --build build --config Debug --target scripting_tests
|
||||
```
|
||||
|
||||
更完整的测试规则见 [tests/TEST_SPEC.md](tests/TEST_SPEC.md)。
|
||||
## 当前仓库状态
|
||||
|
||||
## 目录结构
|
||||
### Engine
|
||||
|
||||
下面的目录树用于表达当前仓库的实际入口结构与模块边界。
|
||||
- `RHI` 正式维护 `D3D12 / OpenGL / Vulkan` 三后端。
|
||||
- `Rendering` 已形成 `SceneRenderer -> CameraRenderer -> RenderPipeline` 主链,包含 `ObjectId`、`InfiniteGrid`、`Outline`、`overlayPasses` 等能力。
|
||||
- `Resources` 与 `Core/Asset` 已不只是简单加载器,当前已经具备 `Assets/.meta/Library` 风格的 `AssetDatabase` 与 artifact 缓存。
|
||||
|
||||
### Editor
|
||||
|
||||
- 当前 editor 是 `D3D12` 宿主应用,但 Scene/Game viewport 已通过引擎 `Rendering + RHI` 链路渲染到离屏纹理,再接入 ImGui。
|
||||
- `Viewport` 相关代码已经进入 overlay/gizmo 正规化阶段,`SceneViewportOverlayBuilder`、`SceneViewportEditorOverlayPass`、`ObjectId` picking 都已落地。
|
||||
- Inspector 已支持 `ScriptComponent` 的脚本类选择、字段元数据读取、字段重置与基础编辑。
|
||||
|
||||
### Project & Scripting
|
||||
|
||||
- 示例工程位于 `project/`,当前工程文件是 `Project.xcproject`,启动场景为 `Assets/Scenes/Main.xc`。
|
||||
- `project/Assets/` 现已包含 `.meta` 文件,`project/Library/` 则维护 `SourceAssetDB`、`ArtifactDB`、`Artifacts` 与 `ScriptAssemblies`。
|
||||
- `managed/` 会生成引擎脚本 API 与示例脚本程序集,项目资产下的 `.cs` 文件也会被单独编译为项目脚本程序集。
|
||||
|
||||
### Tests
|
||||
|
||||
- `tests/` 已覆盖 `Core`、`Memory`、`Threading`、`Scene`、`Resources`、`RHI`、`Rendering`、`Editor`、`Scripting` 等模块。
|
||||
- `tests/Rendering/` 当前已包含 `backpack_scene`、`backpack_lit_scene`、`camera_stack_scene`、`offscreen_scene` 等集成场景。
|
||||
- `tests/RHI/` 同时维护抽象层测试与后端专用测试,`D3D12 / OpenGL / Vulkan` 都有独立子树。
|
||||
|
||||
## 完整目录结构
|
||||
|
||||
以下目录树以当前工程入口为准,保留了当前 workflow 已经实际使用的生成目录;省略 `.git/`、`build/_deps/` 与临时文件。
|
||||
|
||||
```text
|
||||
XCEngine/
|
||||
@@ -112,15 +135,33 @@ XCEngine/
|
||||
├── AGENT.md
|
||||
├── CMakeLists.txt
|
||||
├── README.md
|
||||
├── build/ # 本地构建输出
|
||||
├── build/ # 本地 CMake 构建输出
|
||||
├── docs/
|
||||
│ ├── api/ # API 文档
|
||||
│ ├── issues/ # 跟踪中的设计 / 实现问题
|
||||
│ ├── plan/ # 设计文档、阶段规划、归档方案
|
||||
│ │ ├── Renderer模块设计与实现.md
|
||||
│ │ └── end/
|
||||
│ │ └── RHI模块设计与实现/
|
||||
│ │ └── RHI模块总览.md
|
||||
│ ├── api/
|
||||
│ │ ├── XCEngine/
|
||||
│ │ ├── _guides/
|
||||
│ │ ├── _meta/
|
||||
│ │ ├── _tools/
|
||||
│ │ └── main.md
|
||||
│ ├── issues/
|
||||
│ ├── plan/
|
||||
│ │ ├── end/
|
||||
│ │ │ ├── RHI模块设计与实现/
|
||||
│ │ │ │ ├── RHIFence.md
|
||||
│ │ │ │ └── RHI模块总览.md
|
||||
│ │ │ └── 编辑器与运行时分层架构设计.md
|
||||
│ │ ├── 开题报告和任务书/
|
||||
│ │ ├── 旧版题目/
|
||||
│ │ ├── API文档并行更新任务池_2026-04-02.md
|
||||
│ │ ├── C#脚本模块的设计与实现.md
|
||||
│ │ ├── Editor架构说明.md
|
||||
│ │ ├── SceneViewport_Overlay_Gizmo_Rework_Plan.md
|
||||
│ │ ├── Shader与Material系统下一阶段计划.md
|
||||
│ │ ├── Unity SRP API参考文档.md
|
||||
│ │ ├── Unity式Library资产导入与缓存系统重构方案.md
|
||||
│ │ ├── Unity式Tick系统与PlayMode运行时方案.md
|
||||
│ │ ├── Unity式Tick系统与PlayMode运行时方案-阶段进展.md
|
||||
│ │ └── Unity绝区零开发文档还原版.md
|
||||
│ ├── used/
|
||||
│ ├── api-skill.md
|
||||
│ ├── blueprint-skill.md
|
||||
@@ -143,13 +184,28 @@ XCEngine/
|
||||
│ │ ├── UI/
|
||||
│ │ ├── Utils/
|
||||
│ │ ├── Viewport/
|
||||
│ │ │ ├── Passes/
|
||||
│ │ │ ├── SceneViewportOverlayBuilder.cpp
|
||||
│ │ │ ├── SceneViewportOverlayBuilder.h
|
||||
│ │ │ ├── SceneViewportOverlayRenderer.cpp
|
||||
│ │ │ ├── SceneViewportOverlayRenderer.h
|
||||
│ │ │ ├── SceneViewportPicker.cpp
|
||||
│ │ │ ├── SceneViewportPicker.h
|
||||
│ │ │ ├── SceneViewportMoveGizmo.cpp
|
||||
│ │ │ ├── SceneViewportMoveGizmo.h
|
||||
│ │ │ ├── SceneViewportRotateGizmo.cpp
|
||||
│ │ │ ├── SceneViewportRotateGizmo.h
|
||||
│ │ │ ├── SceneViewportScaleGizmo.cpp
|
||||
│ │ │ ├── SceneViewportScaleGizmo.h
|
||||
│ │ │ ├── ViewportHostRenderFlowUtils.h
|
||||
│ │ │ └── ViewportHostService.h
|
||||
│ │ ├── Application.cpp
|
||||
│ │ ├── Application.h
|
||||
│ │ ├── EditorApp.rc
|
||||
│ │ ├── Theme.cpp
|
||||
│ │ ├── Theme.h
|
||||
│ │ └── main.cpp
|
||||
│ └── bin/ # 编辑器输出目录,输出名为 XCEngine.exe
|
||||
│ └── bin/ # 编辑器输出目录,输出名为 XCEngine.exe
|
||||
├── engine/
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── include/
|
||||
@@ -167,17 +223,24 @@ XCEngine/
|
||||
│ │ ├── Platform/
|
||||
│ │ │ └── Windows/
|
||||
│ │ ├── Rendering/
|
||||
│ │ │ ├── Passes/
|
||||
│ │ │ ├── Pipelines/
|
||||
│ │ │ ├── CameraRenderRequest.h
|
||||
│ │ │ ├── CameraRenderer.h
|
||||
│ │ │ ├── CameraRenderRequest.h
|
||||
│ │ │ ├── ObjectIdEncoding.h
|
||||
│ │ │ ├── ObjectIdPass.h
|
||||
│ │ │ ├── RenderCameraData.h
|
||||
│ │ │ ├── RenderContext.h
|
||||
│ │ │ ├── RenderMaterialUtility.h
|
||||
│ │ │ ├── RenderPass.h
|
||||
│ │ │ ├── RenderPipeline.h
|
||||
│ │ │ ├── RenderPipelineAsset.h
|
||||
│ │ │ ├── RenderResourceCache.h
|
||||
│ │ │ ├── RenderSceneExtractor.h
|
||||
│ │ │ ├── RenderSceneUtility.h
|
||||
│ │ │ ├── RenderSurface.h
|
||||
│ │ │ ├── SceneRenderRequestPlanner.h
|
||||
│ │ │ ├── SceneRenderRequestUtils.h
|
||||
│ │ │ ├── SceneRenderer.h
|
||||
│ │ │ └── VisibleRenderObject.h
|
||||
│ │ ├── Resources/
|
||||
@@ -189,29 +252,7 @@ XCEngine/
|
||||
│ │ ├── RHI/
|
||||
│ │ │ ├── D3D12/
|
||||
│ │ │ ├── OpenGL/
|
||||
│ │ │ ├── Vulkan/
|
||||
│ │ │ ├── RHIBuffer.h
|
||||
│ │ │ ├── RHICapabilities.h
|
||||
│ │ │ ├── RHICommandList.h
|
||||
│ │ │ ├── RHICommandQueue.h
|
||||
│ │ │ ├── RHIDescriptorPool.h
|
||||
│ │ │ ├── RHIDescriptorSet.h
|
||||
│ │ │ ├── RHIDevice.h
|
||||
│ │ │ ├── RHIEnums.h
|
||||
│ │ │ ├── RHIFactory.h
|
||||
│ │ │ ├── RHIFence.h
|
||||
│ │ │ ├── RHIFramebuffer.h
|
||||
│ │ │ ├── RHIPipelineLayout.h
|
||||
│ │ │ ├── RHIPipelineState.h
|
||||
│ │ │ ├── RHIRenderPass.h
|
||||
│ │ │ ├── RHIResource.h
|
||||
│ │ │ ├── RHIResourceView.h
|
||||
│ │ │ ├── RHISampler.h
|
||||
│ │ │ ├── RHIScreenshot.h
|
||||
│ │ │ ├── RHIShader.h
|
||||
│ │ │ ├── RHISwapChain.h
|
||||
│ │ │ ├── RHITexture.h
|
||||
│ │ │ └── RHITypes.h
|
||||
│ │ │ └── Vulkan/
|
||||
│ │ ├── Scene/
|
||||
│ │ ├── Scripting/
|
||||
│ │ │ └── Mono/
|
||||
@@ -230,6 +271,7 @@ XCEngine/
|
||||
│ │ ├── Platform/
|
||||
│ │ │ └── Windows/
|
||||
│ │ ├── Rendering/
|
||||
│ │ │ ├── Passes/
|
||||
│ │ │ └── Pipelines/
|
||||
│ │ ├── Resources/
|
||||
│ │ │ ├── AudioClip/
|
||||
@@ -255,28 +297,45 @@ XCEngine/
|
||||
│ └── renderdoc_parser/
|
||||
├── managed/
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── GameScripts/ # 示例 / 验证脚本程序集源码
|
||||
│ └── XCEngine.ScriptCore/ # 引擎托管 API
|
||||
│ ├── GameScripts/
|
||||
│ └── XCEngine.ScriptCore/
|
||||
├── mvs/
|
||||
│ ├── 3DGS-Unity/ # Unity 侧 3DGS 参考与资源
|
||||
│ ├── 3DGS-Unity/
|
||||
│ ├── D3D12/
|
||||
│ ├── Music fluctuations/
|
||||
│ ├── OpenGL/
|
||||
│ ├── RenderDoc/
|
||||
│ ├── Res/
|
||||
│ ├── ui/
|
||||
│ ├── ui/ # 早期 ImGui + D3D12 UI 原型,非当前正式 editor
|
||||
│ └── VolumeRenderer/
|
||||
├── project/
|
||||
│ ├── .xceditor/
|
||||
│ │ └── imgui_layout.ini
|
||||
│ │ ├── imgui_layout.ini
|
||||
│ │ └── thumbs/
|
||||
│ ├── Assets/
|
||||
│ │ ├── Materials/
|
||||
│ │ ├── Models/
|
||||
│ │ │ └── backpack/
|
||||
│ │ ├── New Folder/
|
||||
│ │ ├── New Folder 1/
|
||||
│ │ ├── Scenes/
|
||||
│ │ │ ├── Backpack.xc
|
||||
│ │ │ └── Main.xc
|
||||
│ │ └── Scripts/
|
||||
│ │ ├── ProjectScriptProbe.cs
|
||||
│ │ └── Textures/
|
||||
│ ├── Library/
|
||||
│ │ ├── ArtifactDB/
|
||||
│ │ ├── Artifacts/
|
||||
│ │ ├── ScriptAssemblies/
|
||||
│ │ │ ├── GameScripts.dll
|
||||
│ │ │ ├── mscorlib.dll
|
||||
│ │ │ └── XCEngine.ScriptCore.dll
|
||||
│ │ └── SourceAssetDB/
|
||||
│ ├── Assets.meta
|
||||
│ └── Project.xcproject
|
||||
├── scripts/
|
||||
│ └── Run-RendererPhaseRegression.ps1
|
||||
├── tests/
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── TEST_SPEC.md
|
||||
@@ -293,7 +352,9 @@ XCEngine/
|
||||
│ ├── Memory/
|
||||
│ ├── Rendering/
|
||||
│ │ ├── integration/
|
||||
│ │ │ ├── backpack_lit_scene/
|
||||
│ │ │ ├── backpack_scene/
|
||||
│ │ │ ├── camera_stack_scene/
|
||||
│ │ │ ├── cull_material_scene/
|
||||
│ │ │ ├── depth_sort_scene/
|
||||
│ │ │ ├── material_state_scene/
|
||||
@@ -308,66 +369,44 @@ XCEngine/
|
||||
│ │ ├── Shader/
|
||||
│ │ └── Texture/
|
||||
│ ├── RHI/
|
||||
│ │ ├── CMakeLists.txt
|
||||
│ │ ├── D3D12/
|
||||
│ │ │ ├── integration/
|
||||
│ │ │ └── unit/
|
||||
│ │ ├── OpenGL/
|
||||
│ │ │ ├── integration/
|
||||
│ │ │ └── unit/
|
||||
│ │ ├── Vulkan/
|
||||
│ │ │ ├── integration/
|
||||
│ │ │ ├── unit/
|
||||
│ │ │ └── TEST_SPEC.md
|
||||
│ │ ├── integration/
|
||||
│ │ │ ├── backpack/
|
||||
│ │ │ ├── fixtures/
|
||||
│ │ │ ├── minimal/
|
||||
│ │ │ ├── quad/
|
||||
│ │ │ ├── sphere/
|
||||
│ │ │ ├── triangle/
|
||||
│ │ │ ├── compare_ppm.py
|
||||
│ │ │ ├── README.md
|
||||
│ │ │ └── run_integration_test.py
|
||||
│ │ └── unit/
|
||||
│ │ │ └── triangle/
|
||||
│ │ ├── OpenGL/
|
||||
│ │ │ ├── integration/
|
||||
│ │ │ └── unit/
|
||||
│ │ ├── unit/
|
||||
│ │ └── Vulkan/
|
||||
│ │ ├── integration/
|
||||
│ │ └── unit/
|
||||
│ ├── Scene/
|
||||
│ ├── Scripting/
|
||||
│ └── Threading/
|
||||
├── 参考/
|
||||
│ └── TransformGizmo/
|
||||
│ ├── Fermion/
|
||||
│ ├── TransformGizmo/
|
||||
│ ├── unity editor/
|
||||
│ ├── unity-editor-icons/
|
||||
│ ├── unity-icons/
|
||||
│ └── UnityRuntimeSceneGizmo-master/
|
||||
└── .vscode/
|
||||
```
|
||||
|
||||
## 模块概览
|
||||
|
||||
### Engine
|
||||
|
||||
- `RHI`:统一图形 API 抽象,当前三后端并行维护
|
||||
- `Rendering`:位于 Scene/Resources 与 RHI 之间,负责把场景提取成真实 draw path
|
||||
- `Resources`:Mesh / Texture / Material / Shader / AudioClip 导入与加载
|
||||
- `Scene + Components`:游戏对象、相机、灯光、网格与脚本组件
|
||||
- `Scripting`:原生脚本运行时与 Mono 托管桥接
|
||||
|
||||
### Editor
|
||||
|
||||
- 当前是 D3D12 桌面宿主应用
|
||||
- Scene/Game viewport 已走引擎离屏渲染链路
|
||||
- 包含 Actions、Commands、Panels、Viewport、Managers 等编辑器子系统
|
||||
|
||||
### Tests
|
||||
|
||||
- `tests/RHI/`:RHI 抽象层与三后端测试
|
||||
- `tests/Rendering/`:渲染链路单元与场景级集成测试
|
||||
- `tests/Scripting/`:脚本运行时与托管程序集集成测试
|
||||
- `tests/Editor/`:编辑器动作路由与 Scene viewport 相机控制测试
|
||||
|
||||
## 关键文档入口
|
||||
|
||||
- 当前工程协作入口:[AGENT.md](AGENT.md)
|
||||
- RHI 核心设计文档:[docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md)
|
||||
- Renderer 规划与实现说明:[docs/plan/Renderer模块设计与实现.md](docs/plan/Renderer模块设计与实现.md)
|
||||
- 协作基线与 coding agent 入口:[AGENT.md](AGENT.md)
|
||||
- RHI 基线设计:[docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md)
|
||||
- 当前 Shader / Material 主线:[docs/plan/Shader与Material系统下一阶段计划.md](docs/plan/Shader与Material系统下一阶段计划.md)
|
||||
- Scene viewport overlay 重构:[docs/plan/SceneViewport_Overlay_Gizmo_Rework_Plan.md](docs/plan/SceneViewport_Overlay_Gizmo_Rework_Plan.md)
|
||||
- 测试规范:[tests/TEST_SPEC.md](tests/TEST_SPEC.md)
|
||||
|
||||
## 许可证
|
||||
|
||||
当前仓库根目录未看到单独的顶层许可证文件;涉及第三方库时请分别遵循对应依赖目录中的许可证说明。
|
||||
当前仓库根目录未看到独立的顶层许可证文件。涉及第三方库时,请分别遵循其所在目录中的许可证或随附说明。
|
||||
|
||||
@@ -49,6 +49,9 @@ if(NOT TARGET XCEngine)
|
||||
add_subdirectory("${XCENGINE_ENGINE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/engine_dependency")
|
||||
endif()
|
||||
|
||||
file(TO_CMAKE_PATH "${XCENGINE_ROOT_DIR}" XCENGINE_ROOT_DIR_CMAKE)
|
||||
file(TO_CMAKE_PATH "${XCENGINE_MONO_ROOT_DIR}" XCENGINE_MONO_ROOT_DIR_CMAKE)
|
||||
|
||||
set(IMGUI_SOURCES
|
||||
${imgui_SOURCE_DIR}/imgui.cpp
|
||||
${imgui_SOURCE_DIR}/imgui_demo.cpp
|
||||
@@ -63,6 +66,7 @@ add_executable(${PROJECT_NAME} WIN32
|
||||
src/EditorApp.rc
|
||||
src/main.cpp
|
||||
src/Application.cpp
|
||||
src/Scripting/EditorScriptAssemblyBuilder.cpp
|
||||
src/Theme.cpp
|
||||
src/Core/UndoManager.cpp
|
||||
src/Core/PlaySessionController.cpp
|
||||
@@ -101,6 +105,13 @@ target_include_directories(${PROJECT_NAME} PRIVATE
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE UNICODE _UNICODE)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8)
|
||||
|
||||
if(XCENGINE_ENABLE_MONO_SCRIPTING)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||
XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
XCENGINE_EDITOR_REPO_ROOT="${XCENGINE_ROOT_DIR_CMAKE}"
|
||||
XCENGINE_EDITOR_MONO_ROOT_DIR="${XCENGINE_MONO_ROOT_DIR_CMAKE}")
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /FS)
|
||||
set_property(TARGET ${PROJECT_NAME} PROPERTY
|
||||
|
||||
326
editor/README.md
@@ -1,183 +1,205 @@
|
||||
# UI Editor
|
||||
# XCEditor
|
||||
|
||||
Unity 风格的编辑器 UI,使用 ImGui 实现,作为 XCEngine 游戏引擎编辑器的一部分。
|
||||
`editor/` 是 XCEngine 当前随仓库维护的桌面编辑器模块。它不是一套独立渲染器,而是 `D3D12` 宿主应用,用来承接引擎 `Rendering + RHI + Scene + Scripting` 主链。
|
||||
|
||||
## 简介
|
||||
当前 editor 已经具备:
|
||||
|
||||
XCGameEngine UI 是一个仿 Unity 编辑器的桌面应用程序,提供场景管理、层级视图、属性检查器等功能。
|
||||
- Scene / Game viewport 离屏渲染接入
|
||||
- object-id picking 与选中描边
|
||||
- scene overlay / gizmo 正规化收口
|
||||
- 项目根目录解析与 `Project.xcproject` 加载
|
||||
- `Assets + .meta + Library` 风格项目目录接入
|
||||
- `ScriptComponent` 的脚本类与字段编辑入口
|
||||
|
||||
## 技术栈
|
||||
## 当前定位
|
||||
|
||||
- **渲染 API**: DirectX 12
|
||||
- **UI 框架**: ImGui
|
||||
- **语言**: C++17
|
||||
- **构建系统**: CMake
|
||||
- **依赖库**: DirectX 12 SDK
|
||||
如果你想理解当前 editor,先把它当成三层:
|
||||
|
||||
## 项目结构
|
||||
1. `Win32 + D3D12` 宿主窗口与 ImGui backend
|
||||
2. `ViewportHostService` 对引擎渲染链路的接线
|
||||
3. `panels/`、`Managers/`、`ComponentEditors/` 这些编辑器业务层
|
||||
|
||||
```
|
||||
ui/
|
||||
├── src/
|
||||
│ ├── main.cpp # 程序入口
|
||||
│ ├── Application.cpp/h # 应用主类
|
||||
│ ├── Theme.cpp/h # 主题系统
|
||||
│ ├── Core/
|
||||
│ │ ├── GameObject.h # 游戏对象
|
||||
│ │ └── LogEntry.h # 日志条目
|
||||
│ ├── Managers/
|
||||
│ │ ├── LogSystem.cpp/h # 日志系统
|
||||
│ │ ├── ProjectManager.cpp/h # 项目管理
|
||||
│ │ ├── SceneManager.cpp/h # 场景管理
|
||||
│ │ └── SelectionManager.cpp/h # 选择管理
|
||||
│ └── panels/
|
||||
│ ├── Panel.cpp/h # 面板基类
|
||||
│ ├── MenuBar.cpp/h # 菜单栏
|
||||
│ ├── HierarchyPanel.cpp/h # 层级面板
|
||||
│ ├── InspectorPanel.cpp/h # 检查器面板
|
||||
│ ├── SceneViewPanel.cpp/h # 场景视图
|
||||
│ ├── GameViewPanel.cpp/h # 游戏视图
|
||||
│ ├── ProjectPanel.cpp/h # 项目面板
|
||||
│ └── ConsolePanel.cpp/h # 控制台面板
|
||||
├── bin/Release/ # 输出目录
|
||||
│ ├── XCVolumeRendererUI2.exe # 可执行文件
|
||||
│ ├── imgui.ini # ImGui 配置
|
||||
│ └── Assets/
|
||||
│ └── Models/
|
||||
│ └── Character.fbx # 示例模型
|
||||
├── build/ # 构建目录
|
||||
└── CMakeLists.txt # CMake 配置
|
||||
```
|
||||
当前不应再把 editor 视为旧式“UI sample”。它已经是引擎工作区的正式入口之一。
|
||||
|
||||
## 构建方法
|
||||
## 构建
|
||||
|
||||
推荐直接在仓库根目录构建,而不是单独进入 `editor/` 目录。
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Windows 10/11
|
||||
- Visual Studio 2019 或更高版本
|
||||
- Visual Studio 2022 / MSVC v143
|
||||
- CMake 3.15+
|
||||
- Vulkan SDK
|
||||
|
||||
### 构建步骤
|
||||
如果需要启用 Mono 脚本运行时,还需要:
|
||||
|
||||
- .NET SDK
|
||||
- `参考/Fermion/Fermion/external/mono` 下的 Mono 依赖
|
||||
|
||||
### 配置
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
cmake --build . --config Release
|
||||
cmake -S .. -B ..\build -A x64
|
||||
```
|
||||
|
||||
### 运行
|
||||
更常见的做法是直接在仓库根目录运行:
|
||||
|
||||
```bash
|
||||
# 运行编译好的可执行文件
|
||||
.\bin\Release\XCGameEngineUI.exe
|
||||
cmake -S . -B build -A x64
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
如果本地暂时没有 Mono,可以先关闭:
|
||||
|
||||
### 编辑器面板
|
||||
|
||||
#### 菜单栏(MenuBar)
|
||||
- 文件菜单(新建、打开、保存等)
|
||||
- 编辑菜单(撤销、重做等)
|
||||
- 视图菜单(面板显示/隐藏)
|
||||
- 帮助菜单
|
||||
|
||||
#### 层级面板(Hierarchy Panel)
|
||||
- 显示场景中所有游戏对象
|
||||
- 树形结构展示父子关系
|
||||
- 支持对象选择
|
||||
- 对象重命名
|
||||
|
||||
#### 检查器面板(Inspector Panel)
|
||||
- 显示选中对象的属性
|
||||
- 支持组件编辑
|
||||
- 变换组件(位置、旋转、缩放)
|
||||
- 材质组件
|
||||
|
||||
#### 场景视图(Scene View)
|
||||
- 3D 场景预览
|
||||
- 相机控制(平移、旋转、缩放)
|
||||
- 对象选择
|
||||
- 辅助工具(网格、轴心)
|
||||
|
||||
#### 游戏视图(Game View)
|
||||
- 游戏运行时的画面预览
|
||||
- 分辨率设置
|
||||
- 宽高比选择
|
||||
|
||||
#### 项目面板(Project Panel)
|
||||
- 项目文件浏览器
|
||||
- 资源组织
|
||||
- 搜索过滤
|
||||
|
||||
#### 控制台面板(Console Panel)
|
||||
- 日志输出
|
||||
- 警告和错误显示
|
||||
- 日志级别过滤
|
||||
- 清空日志
|
||||
|
||||
### 管理系统
|
||||
|
||||
#### 日志系统(LogSystem)
|
||||
- 分级日志(Info、Warning、Error)
|
||||
- 时间戳
|
||||
- 日志持久化
|
||||
|
||||
#### 项目管理(ProjectManager)
|
||||
- 项目创建/打开
|
||||
- 资源路径管理
|
||||
|
||||
#### 场景管理(SceneManager)
|
||||
- 场景加载/保存
|
||||
- 对象生命周期管理
|
||||
|
||||
#### 选择管理(SelectionManager)
|
||||
- 当前选中对象追踪
|
||||
- 多选支持
|
||||
|
||||
### 主题系统
|
||||
|
||||
- 深色主题(Dark Theme)
|
||||
- 可自定义配色方案
|
||||
|
||||
## 窗口布局
|
||||
|
||||
默认布局采用经典的 Unity 编辑器风格:
|
||||
|
||||
```
|
||||
+----------------------------------------------------------+
|
||||
| 菜单栏 |
|
||||
+----------+------------------------+----------------------+
|
||||
| | | |
|
||||
| 项目 | 场景视图 | 检查器 |
|
||||
| 面板 | | |
|
||||
| | | |
|
||||
+----------+------------------------+----------------------+
|
||||
| 层级面板 | 游戏视图 |
|
||||
| | |
|
||||
+------------------------------------+----------------------+
|
||||
| 控制台面板 |
|
||||
+----------------------------------------------------------+
|
||||
```bash
|
||||
cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF
|
||||
```
|
||||
|
||||
## 依赖说明
|
||||
### 构建 editor
|
||||
|
||||
- ImGui - 跨平台 GUI 库
|
||||
- DirectX 12 - 渲染 API
|
||||
- Windows SDK - 窗口管理
|
||||
```bash
|
||||
cmake --build build --config Debug --target XCEditor
|
||||
```
|
||||
|
||||
## 扩展开发
|
||||
说明:
|
||||
|
||||
### 添加新面板
|
||||
- target 名称是 `XCEditor`
|
||||
- 输出文件名仍然是 `XCEngine.exe`
|
||||
- 输出目录是 `editor/bin/<Config>/`
|
||||
|
||||
1. 在 `panels/` 目录下创建新的面板类
|
||||
2. 继承 `Panel` 基类
|
||||
3. 实现 `Render()` 方法
|
||||
4. 在 `Application` 中注册新面板
|
||||
## 运行
|
||||
|
||||
### 添加新组件
|
||||
```bash
|
||||
.\editor\bin\Debug\XCEngine.exe
|
||||
```
|
||||
|
||||
1. 定义组件类
|
||||
2. 在 `GameObject` 中注册组件类型
|
||||
3. 在 `InspectorPanel` 中添加属性编辑器
|
||||
默认情况下,editor 会自动把仓库内的 `project/` 识别为工程根目录。也可以显式指定工程:
|
||||
|
||||
```bash
|
||||
.\editor\bin\Debug\XCEngine.exe --project D:\Path\To\Project
|
||||
```
|
||||
|
||||
如果需要 C# 脚本类发现与 Inspector 字段编辑,先构建:
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target xcengine_project_managed_assemblies
|
||||
```
|
||||
|
||||
该 target 会把程序集放到:
|
||||
|
||||
- `project/Library/ScriptAssemblies/XCEngine.ScriptCore.dll`
|
||||
- `project/Library/ScriptAssemblies/GameScripts.dll`
|
||||
- `project/Library/ScriptAssemblies/mscorlib.dll`
|
||||
|
||||
## 当前目录结构
|
||||
|
||||
```text
|
||||
editor/
|
||||
├── CMakeLists.txt
|
||||
├── README.md
|
||||
├── resources/
|
||||
│ └── Icons/
|
||||
├── src/
|
||||
│ ├── Actions/ # 编辑器动作路由
|
||||
│ ├── Commands/ # 命令与实体操作
|
||||
│ ├── ComponentEditors/ # Inspector 组件编辑器
|
||||
│ ├── Core/ # 应用生命周期、日志、项目根解析、撤销等
|
||||
│ ├── Layers/ # EditorLayer 等高层组装
|
||||
│ ├── Layout/
|
||||
│ ├── Managers/ # SceneManager / ProjectManager
|
||||
│ ├── panels/ # Hierarchy / Scene / Game / Inspector / Project / Console
|
||||
│ ├── Platform/ # Win32 host、D3D12 backend 辅助
|
||||
│ ├── UI/ # ImGui bridge 与通用 widget
|
||||
│ ├── Utils/
|
||||
│ ├── Viewport/
|
||||
│ │ ├── Passes/ # editor viewport overlay pass
|
||||
│ │ ├── SceneViewportOverlayBuilder.*
|
||||
│ │ ├── SceneViewportPicker.*
|
||||
│ │ ├── SceneViewportMoveGizmo.*
|
||||
│ │ ├── SceneViewportRotateGizmo.*
|
||||
│ │ ├── SceneViewportScaleGizmo.*
|
||||
│ │ ├── ViewportHostRenderFlowUtils.h
|
||||
│ │ └── ViewportHostService.h
|
||||
│ ├── Application.cpp
|
||||
│ ├── Application.h
|
||||
│ ├── EditorApp.rc
|
||||
│ ├── Theme.cpp
|
||||
│ ├── Theme.h
|
||||
│ └── main.cpp
|
||||
└── bin/
|
||||
```
|
||||
|
||||
## 关键模块
|
||||
|
||||
### Application
|
||||
|
||||
- `src/Application.cpp`
|
||||
- `src/Application.h`
|
||||
|
||||
负责:
|
||||
|
||||
- editor 初始化与关闭
|
||||
- resource root 设置
|
||||
- scripting runtime 初始化
|
||||
- ImGui backend 初始化
|
||||
- `ViewportHostService` 接线
|
||||
|
||||
### Project Root
|
||||
|
||||
- `src/Core/ProjectRootResolver.h`
|
||||
- `src/Utils/ProjectFileUtils.h`
|
||||
|
||||
负责:
|
||||
|
||||
- 自动识别仓库内 `project/`
|
||||
- 解析 `--project`
|
||||
- 读写 `Project.xcproject`
|
||||
|
||||
### Viewport
|
||||
|
||||
- `src/Viewport/ViewportHostService.h`
|
||||
- `src/Viewport/ViewportHostRenderFlowUtils.h`
|
||||
- `src/Viewport/SceneViewportOverlayBuilder.*`
|
||||
- `src/Viewport/Passes/SceneViewportEditorOverlayPass.*`
|
||||
|
||||
负责:
|
||||
|
||||
- 组装 scene/game viewport 渲染请求
|
||||
- 把 editor overlay 接入 `CameraRenderRequest::overlayPasses`
|
||||
- object-id picking、outline、overlay pass 等 editor 视口能力
|
||||
|
||||
### Panels
|
||||
|
||||
当前主要面板:
|
||||
|
||||
- `HierarchyPanel`
|
||||
- `SceneViewPanel`
|
||||
- `GameViewPanel`
|
||||
- `InspectorPanel`
|
||||
- `ProjectPanel`
|
||||
- `ConsolePanel`
|
||||
|
||||
### Component Editors
|
||||
|
||||
`ComponentEditors/` 当前不仅负责基础组件,也已经包含 `ScriptComponent` 的 Inspector 编辑入口。
|
||||
|
||||
## 开发约束
|
||||
|
||||
- editor 是宿主,不是第二套 renderer。
|
||||
- 新的世界空间 overlay / gizmo,不应继续堆到 ImGui world draw 路径。
|
||||
- viewport 相关问题优先检查 `engine/Rendering`、`RenderSurface` 与 `ViewportHostService` 的接线,而不是直接在 panel 里复制渲染逻辑。
|
||||
- 与项目资源、脚本程序集、`.meta`、`Library` 相关的问题,不要假设 editor 仍处于“无工程状态”的旧结构。
|
||||
|
||||
## 推荐验证
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target editor_tests
|
||||
cmake --build build --config Debug --target rendering_phase_regression
|
||||
```
|
||||
|
||||
如果改动影响脚本类发现或 Inspector 脚本字段编辑,再补:
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target xcengine_project_managed_assemblies
|
||||
cmake --build build --config Debug --target scripting_tests
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 180 B |
BIN
editor/color.png
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 29 KiB |
@@ -1,28 +0,0 @@
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def get_dominant_color(image_path):
|
||||
img = Image.open(image_path).convert("RGB")
|
||||
img = img.resize((1, 1), Image.Resampling.LANCZOS)
|
||||
r, g, b = img.getpixel((0, 0))
|
||||
return r, g, b
|
||||
|
||||
|
||||
def rename_with_color(base_path):
|
||||
files = ["color.png", "color2.png"]
|
||||
for f in files:
|
||||
old_path = os.path.join(base_path, f)
|
||||
if os.path.exists(old_path):
|
||||
r, g, b = get_dominant_color(old_path)
|
||||
new_name = f"color-({r},{g},{b}).png"
|
||||
new_path = os.path.join(base_path, new_name)
|
||||
os.rename(old_path, new_path)
|
||||
print(f"Renamed: {f} -> {new_name}")
|
||||
else:
|
||||
print(f"File not found: {old_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
base = r"D:\Xuanchi\Main\XCEngine\editor"
|
||||
rename_with_color(base)
|
||||
BIN
editor/resources/Icons/camera_gizmo.png
Normal file
|
After Width: | Height: | Size: 672 B |
BIN
editor/resources/Icons/main_light_gizmo.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 232 B |
BIN
editor/resources/Icons/mesh_icondd.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
editor/resources/Icons/view_move.png
Normal file
|
After Width: | Height: | Size: 657 B |
BIN
editor/resources/Icons/view_orbit.png
Normal file
|
After Width: | Height: | Size: 381 B |
@@ -28,6 +28,14 @@ inline ActionBinding MakeSaveProjectAction(bool enabled = true) {
|
||||
return MakeAction("Save Project", nullptr, false, enabled);
|
||||
}
|
||||
|
||||
inline ActionBinding MakeRebuildScriptsAction(bool enabled = true) {
|
||||
return MakeAction("Rebuild Script Assemblies", nullptr, false, enabled);
|
||||
}
|
||||
|
||||
inline ActionBinding MakeMigrateSceneAssetReferencesAction(bool enabled = true) {
|
||||
return MakeAction("Migrate Scene AssetRefs", nullptr, false, enabled);
|
||||
}
|
||||
|
||||
inline ActionBinding MakeNewSceneAction(bool enabled = true) {
|
||||
return MakeAction("New Scene", "Ctrl+N", false, enabled, true, Shortcut(ImGuiKey_N, true));
|
||||
}
|
||||
|
||||
@@ -168,6 +168,19 @@ inline void DrawHierarchySortOptionsPopup(
|
||||
}
|
||||
|
||||
inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Components::GameObject* parent) {
|
||||
const auto drawPrimitiveCreateAction = [&](
|
||||
const ActionBinding& action,
|
||||
::XCEngine::Resources::BuiltinPrimitiveType primitiveType) {
|
||||
const char* label = ::XCEngine::Resources::GetBuiltinPrimitiveDisplayName(primitiveType);
|
||||
DrawMenuAction(action, [&]() {
|
||||
TraceHierarchyPopup(std::string("Hierarchy create clicked: ") + label);
|
||||
auto* created = Commands::CreatePrimitiveEntity(context, primitiveType, parent);
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: ") + label + ", createdId=" +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
};
|
||||
|
||||
DrawMenuAction(MakeCreateEmptyEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Empty Object");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Entity", "GameObject");
|
||||
@@ -175,6 +188,16 @@ inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Comp
|
||||
std::string("Hierarchy create result: Empty Object, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
|
||||
UI::DrawContextSubmenu("3D Object", [&]() {
|
||||
drawPrimitiveCreateAction(MakeCreateCubeEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Cube);
|
||||
drawPrimitiveCreateAction(MakeCreateSphereEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Sphere);
|
||||
drawPrimitiveCreateAction(MakeCreateCapsuleEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Capsule);
|
||||
drawPrimitiveCreateAction(MakeCreateCylinderEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Cylinder);
|
||||
drawPrimitiveCreateAction(MakeCreatePlaneEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Plane);
|
||||
drawPrimitiveCreateAction(MakeCreateQuadEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Quad);
|
||||
});
|
||||
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeCreateCameraEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Camera");
|
||||
@@ -190,28 +213,6 @@ inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Comp
|
||||
std::string("Hierarchy create result: Light, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeCreateCubeEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Cube");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Cube", "Cube");
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Cube, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
DrawMenuAction(MakeCreateSphereEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Sphere");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Sphere", "Sphere");
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Sphere, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
DrawMenuAction(MakeCreatePlaneEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Plane");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Plane", "Plane");
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Plane, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
}
|
||||
|
||||
inline void HandleHierarchyItemContextRequest(
|
||||
@@ -275,7 +276,7 @@ inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::Def
|
||||
backgroundContextMenu.ConsumeOpenRequest("HierarchyContextMenu");
|
||||
static bool s_lastBackgroundPopupOpen = false;
|
||||
|
||||
if (!UI::BeginPopup("HierarchyContextMenu")) {
|
||||
if (!UI::BeginContextMenu("HierarchyContextMenu")) {
|
||||
if (s_lastBackgroundPopupOpen) {
|
||||
TraceHierarchyPopup("Hierarchy background popup closed");
|
||||
s_lastBackgroundPopupOpen = false;
|
||||
@@ -289,7 +290,7 @@ inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::Def
|
||||
}
|
||||
|
||||
DrawHierarchyContextActions(context, nullptr, true);
|
||||
UI::EndPopup();
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
inline void DrawHierarchyEntityContextPopup(
|
||||
@@ -298,7 +299,7 @@ inline void DrawHierarchyEntityContextPopup(
|
||||
itemContextMenu.ConsumeOpenRequest("HierarchyEntityContextMenu");
|
||||
static bool s_lastEntityPopupOpen = false;
|
||||
|
||||
if (!UI::BeginPopup("HierarchyEntityContextMenu")) {
|
||||
if (!UI::BeginContextMenu("HierarchyEntityContextMenu")) {
|
||||
if (s_lastEntityPopupOpen) {
|
||||
TraceHierarchyPopup("Hierarchy entity popup closed");
|
||||
s_lastEntityPopupOpen = false;
|
||||
@@ -314,7 +315,7 @@ inline void DrawHierarchyEntityContextPopup(
|
||||
if (itemContextMenu.HasTarget()) {
|
||||
DrawHierarchyContextActions(context, itemContextMenu.TargetValue());
|
||||
}
|
||||
UI::EndPopup();
|
||||
UI::EndContextMenu();
|
||||
|
||||
if (!ImGui::IsPopupOpen("HierarchyEntityContextMenu") && !itemContextMenu.HasPendingOpenRequest()) {
|
||||
itemContextMenu.Clear();
|
||||
|
||||
@@ -34,6 +34,14 @@ inline void ExecuteSaveProject(IEditorContext& context) {
|
||||
Commands::SaveProject(context);
|
||||
}
|
||||
|
||||
inline void ExecuteRebuildScriptAssemblies(IEditorContext& context) {
|
||||
Commands::RebuildScriptAssemblies(context);
|
||||
}
|
||||
|
||||
inline void ExecuteMigrateSceneAssetReferences(IEditorContext& context) {
|
||||
Commands::MigrateSceneAssetReferences(context);
|
||||
}
|
||||
|
||||
inline void ExecuteOpenScene(IEditorContext& context) {
|
||||
Commands::OpenSceneWithDialog(context);
|
||||
}
|
||||
@@ -143,6 +151,10 @@ inline void DrawFileMenuActions(IEditorContext& context) {
|
||||
DrawMenuAction(MakeSaveSceneAction(canEditDocuments), [&]() { ExecuteSaveScene(context); });
|
||||
DrawMenuAction(MakeSaveSceneAsAction(canEditDocuments), [&]() { ExecuteSaveSceneAs(context); });
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(
|
||||
MakeMigrateSceneAssetReferencesAction(Commands::CanMigrateSceneAssetReferences(context)),
|
||||
[&]() { ExecuteMigrateSceneAssetReferences(context); });
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeExitAction(), [&]() { RequestEditorExit(context); });
|
||||
}
|
||||
|
||||
@@ -160,6 +172,12 @@ inline void DrawRunMenuActions(IEditorContext& context) {
|
||||
});
|
||||
}
|
||||
|
||||
inline void DrawScriptsMenuActions(IEditorContext& context) {
|
||||
DrawMenuAction(
|
||||
MakeRebuildScriptsAction(Commands::CanRebuildScriptAssemblies(context)),
|
||||
[&]() { ExecuteRebuildScriptAssemblies(context); });
|
||||
}
|
||||
|
||||
inline void DrawViewMenuActions(IEditorContext& context) {
|
||||
DrawMenuAction(MakeResetLayoutAction(), [&]() { RequestDockLayoutReset(context); });
|
||||
}
|
||||
@@ -186,6 +204,9 @@ inline void DrawMainMenuBar(IEditorContext& context, UI::DeferredPopupState& abo
|
||||
UI::DrawMenuScope("Run", [&]() {
|
||||
DrawRunMenuActions(context);
|
||||
});
|
||||
UI::DrawMenuScope("Scripts", [&]() {
|
||||
DrawScriptsMenuActions(context);
|
||||
});
|
||||
UI::DrawMenuScope("View", [&]() {
|
||||
DrawViewMenuActions(context);
|
||||
});
|
||||
|
||||
@@ -6,12 +6,18 @@
|
||||
#include "Core/EditorContext.h"
|
||||
#include "Core/EditorEvents.h"
|
||||
#include "Core/EventBus.h"
|
||||
#include "Scripting/EditorScriptAssemblyBuilder.h"
|
||||
#include "UI/BuiltInIcons.h"
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "Platform/WindowsProcessDiagnostics.h"
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <XCEngine/Scripting/ScriptEngine.h>
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
|
||||
#endif
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <windows.h>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -22,6 +28,123 @@ Application& Application::Get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
void Application::InitializeScriptingRuntime(const std::string& projectPath) {
|
||||
ShutdownScriptingRuntime();
|
||||
|
||||
const std::filesystem::path assemblyDirectoryPath =
|
||||
std::filesystem::path(Platform::Utf8ToWide(projectPath)) / L"Library" / L"ScriptAssemblies";
|
||||
m_scriptRuntimeStatus.assemblyDirectory = Platform::WideToUtf8(assemblyDirectoryPath.wstring());
|
||||
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
auto& logger = Debug::Logger::Get();
|
||||
const fs::path assemblyDirectory = assemblyDirectoryPath;
|
||||
m_scriptRuntimeStatus.backendEnabled = true;
|
||||
|
||||
::XCEngine::Scripting::MonoScriptRuntime::Settings settings;
|
||||
settings.assemblyDirectory = assemblyDirectory;
|
||||
settings.corlibDirectory = assemblyDirectory;
|
||||
settings.coreAssemblyPath = assemblyDirectory / L"XCEngine.ScriptCore.dll";
|
||||
settings.appAssemblyPath = assemblyDirectory / L"GameScripts.dll";
|
||||
|
||||
std::error_code ec;
|
||||
const bool hasCoreAssembly = fs::exists(settings.coreAssemblyPath, ec);
|
||||
ec.clear();
|
||||
const bool hasAppAssembly = fs::exists(settings.appAssemblyPath, ec);
|
||||
ec.clear();
|
||||
const bool hasCorlibAssembly = fs::exists(assemblyDirectory / L"mscorlib.dll", ec);
|
||||
m_scriptRuntimeStatus.assembliesFound = hasCoreAssembly && hasAppAssembly && hasCorlibAssembly;
|
||||
|
||||
if (!hasCoreAssembly || !hasAppAssembly || !hasCorlibAssembly) {
|
||||
m_scriptRuntimeStatus.statusMessage =
|
||||
"Script assemblies were not found in " + Platform::WideToUtf8(assemblyDirectory.wstring()) +
|
||||
". Script class discovery is disabled until the managed assemblies are built.";
|
||||
logger.Warning(Debug::LogCategory::Scripting, m_scriptRuntimeStatus.statusMessage.c_str());
|
||||
::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
auto runtime = std::make_unique<::XCEngine::Scripting::MonoScriptRuntime>(settings);
|
||||
if (!runtime->Initialize()) {
|
||||
m_scriptRuntimeStatus.statusMessage =
|
||||
"Failed to initialize editor script runtime: " + runtime->GetLastError();
|
||||
logger.Warning(Debug::LogCategory::Scripting, m_scriptRuntimeStatus.statusMessage.c_str());
|
||||
::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(runtime.get());
|
||||
m_scriptRuntimeStatus.runtimeLoaded = true;
|
||||
m_scriptRuntime = std::move(runtime);
|
||||
logger.Info(Debug::LogCategory::Scripting, "Editor script runtime initialized.");
|
||||
#else
|
||||
(void)projectPath;
|
||||
m_scriptRuntimeStatus.backendEnabled = false;
|
||||
m_scriptRuntimeStatus.statusMessage = "This editor build does not include Mono scripting support.";
|
||||
::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Application::ShutdownScriptingRuntime() {
|
||||
::XCEngine::Scripting::ScriptEngine::Get().OnRuntimeStop();
|
||||
::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr);
|
||||
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
m_scriptRuntime.reset();
|
||||
#endif
|
||||
|
||||
m_scriptRuntimeStatus = {};
|
||||
}
|
||||
|
||||
bool Application::ReloadScriptingRuntime() {
|
||||
if (!m_editorContext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string& projectPath = m_editorContext->GetProjectPath();
|
||||
if (projectPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
InitializeScriptingRuntime(projectPath);
|
||||
return m_scriptRuntimeStatus.runtimeLoaded;
|
||||
}
|
||||
|
||||
bool Application::RebuildScriptingAssemblies() {
|
||||
if (!m_editorContext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string& projectPath = m_editorContext->GetProjectPath();
|
||||
if (projectPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
auto& logger = Debug::Logger::Get();
|
||||
logger.Info(Debug::LogCategory::Scripting, "Rebuilding project script assemblies...");
|
||||
|
||||
// Release the currently loaded project assembly before invoking the compiler.
|
||||
// Otherwise GameScripts.dll can remain locked by the active Mono app domain.
|
||||
ShutdownScriptingRuntime();
|
||||
|
||||
const ::XCEngine::Editor::Scripting::EditorScriptAssemblyBuildResult buildResult =
|
||||
::XCEngine::Editor::Scripting::EditorScriptAssemblyBuilder::RebuildProjectAssemblies(projectPath);
|
||||
if (!buildResult.succeeded) {
|
||||
m_scriptRuntimeStatus.statusMessage = buildResult.message;
|
||||
logger.Error(Debug::LogCategory::Scripting, buildResult.message.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.Info(Debug::LogCategory::Scripting, buildResult.message.c_str());
|
||||
return ReloadScriptingRuntime();
|
||||
#else
|
||||
m_scriptRuntimeStatus.statusMessage = "This editor build does not include Mono scripting support.";
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Application::InitializeWindowRenderer(HWND hwnd) {
|
||||
RECT clientRect = {};
|
||||
if (!GetClientRect(hwnd, &clientRect)) {
|
||||
@@ -150,6 +273,7 @@ bool Application::Initialize(HWND hwnd) {
|
||||
|
||||
logger.Info(Debug::LogCategory::General, "Initializing editor context...");
|
||||
InitializeEditorContext(projectRoot);
|
||||
InitializeScriptingRuntime(projectRoot);
|
||||
logger.Info(Debug::LogCategory::General, "Initializing ImGui backend...");
|
||||
InitializeImGui(hwnd);
|
||||
logger.Info(Debug::LogCategory::General, "Attaching editor layer...");
|
||||
@@ -171,6 +295,7 @@ void Application::Shutdown() {
|
||||
UI::ShutdownBuiltInIcons();
|
||||
m_imguiBackend.Shutdown();
|
||||
m_imguiSession.Shutdown();
|
||||
ShutdownScriptingRuntime();
|
||||
ShutdownEditorContext();
|
||||
if (m_resourceManagerInitialized) {
|
||||
::XCEngine::Resources::ResourceManager::Get().Shutdown();
|
||||
@@ -230,6 +355,7 @@ bool Application::SwitchProject(const std::string& projectPath) {
|
||||
logger.Info(Debug::LogCategory::General, infoMessage.c_str());
|
||||
|
||||
::XCEngine::Resources::ResourceManager::Get().SetResourceRoot(projectPath.c_str());
|
||||
InitializeScriptingRuntime(projectPath);
|
||||
|
||||
m_lastWindowTitle.clear();
|
||||
UpdateWindowTitle();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Platform/D3D12WindowRenderer.h"
|
||||
#include "Scripting/EditorScriptRuntimeStatus.h"
|
||||
#include "UI/ImGuiBackendBridge.h"
|
||||
#include "UI/ImGuiSession.h"
|
||||
#include "Viewport/ViewportHostService.h"
|
||||
@@ -19,6 +20,12 @@ class RHIDevice;
|
||||
class RHISwapChain;
|
||||
} // namespace RHI
|
||||
|
||||
namespace Scripting {
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
class MonoScriptRuntime;
|
||||
#endif
|
||||
} // namespace Scripting
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class EditorLayer;
|
||||
@@ -33,6 +40,8 @@ public:
|
||||
void Render();
|
||||
void OnResize(int width, int height);
|
||||
bool SwitchProject(const std::string& projectPath);
|
||||
bool ReloadScriptingRuntime();
|
||||
bool RebuildScriptingAssemblies();
|
||||
void SaveProjectState();
|
||||
Rendering::RenderContext GetMainRenderContext() const { return m_windowRenderer.GetRenderContext(); }
|
||||
RHI::RHIDevice* GetMainRHIDevice() const { return m_windowRenderer.GetRHIDevice(); }
|
||||
@@ -42,6 +51,7 @@ public:
|
||||
HWND GetWindowHandle() const { return m_hwnd; }
|
||||
|
||||
IEditorContext& GetEditorContext() const { return *m_editorContext; }
|
||||
const EditorScriptRuntimeStatus& GetScriptRuntimeStatus() const { return m_scriptRuntimeStatus; }
|
||||
|
||||
private:
|
||||
Application() = default;
|
||||
@@ -52,6 +62,8 @@ private:
|
||||
void AttachEditorLayer();
|
||||
void DetachEditorLayer();
|
||||
void ShutdownEditorContext();
|
||||
void InitializeScriptingRuntime(const std::string& projectPath);
|
||||
void ShutdownScriptingRuntime();
|
||||
void RenderEditorFrame();
|
||||
void UpdateWindowTitle();
|
||||
|
||||
@@ -70,6 +82,10 @@ private:
|
||||
bool m_hasLastFrameTime = false;
|
||||
bool m_renderReady = false;
|
||||
bool m_resourceManagerInitialized = false;
|
||||
EditorScriptRuntimeStatus m_scriptRuntimeStatus;
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
std::unique_ptr<::XCEngine::Scripting::MonoScriptRuntime> m_scriptRuntime;
|
||||
#endif
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/LightComponent.h>
|
||||
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
#include <XCEngine/Resources/BuiltinResources.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
@@ -80,6 +83,31 @@ inline ::XCEngine::Components::GameObject* CreateLightEntity(
|
||||
});
|
||||
}
|
||||
|
||||
inline ::XCEngine::Components::GameObject* CreatePrimitiveEntity(
|
||||
IEditorContext& context,
|
||||
::XCEngine::Resources::BuiltinPrimitiveType primitiveType,
|
||||
::XCEngine::Components::GameObject* parent = nullptr) {
|
||||
const char* primitiveName = ::XCEngine::Resources::GetBuiltinPrimitiveDisplayName(primitiveType);
|
||||
return CreateEntity(
|
||||
context,
|
||||
std::string("Create ") + primitiveName,
|
||||
primitiveName,
|
||||
parent,
|
||||
[primitiveType](::XCEngine::Components::GameObject& entity, ISceneManager&) {
|
||||
auto* meshFilter = entity.GetComponent<::XCEngine::Components::MeshFilterComponent>();
|
||||
if (meshFilter == nullptr) {
|
||||
meshFilter = entity.AddComponent<::XCEngine::Components::MeshFilterComponent>();
|
||||
}
|
||||
meshFilter->SetMeshPath(::XCEngine::Resources::GetBuiltinPrimitiveMeshPath(primitiveType).CStr());
|
||||
|
||||
auto* meshRenderer = entity.GetComponent<::XCEngine::Components::MeshRendererComponent>();
|
||||
if (meshRenderer == nullptr) {
|
||||
meshRenderer = entity.AddComponent<::XCEngine::Components::MeshRendererComponent>();
|
||||
}
|
||||
meshRenderer->SetMaterialPath(0, ::XCEngine::Resources::GetBuiltinDefaultPrimitiveMaterialPath().CStr());
|
||||
});
|
||||
}
|
||||
|
||||
inline bool RenameEntity(
|
||||
IEditorContext& context,
|
||||
::XCEngine::Components::GameObject::ID entityId,
|
||||
|
||||
@@ -314,6 +314,35 @@ inline bool SaveProject(IEditorContext& context) {
|
||||
return SaveProjectDescriptor(context);
|
||||
}
|
||||
|
||||
inline bool CanRebuildScriptAssemblies(const IEditorContext& context) {
|
||||
return IsProjectDocumentEditingAllowed(context) && !context.GetProjectPath().empty();
|
||||
}
|
||||
|
||||
inline bool RebuildScriptAssemblies(IEditorContext& context) {
|
||||
if (!CanRebuildScriptAssemblies(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool rebuilt = Application::Get().RebuildScriptingAssemblies();
|
||||
if (rebuilt) {
|
||||
context.GetProjectManager().RefreshCurrentFolder();
|
||||
}
|
||||
|
||||
return rebuilt;
|
||||
}
|
||||
|
||||
inline bool CanMigrateSceneAssetReferences(const IEditorContext& context) {
|
||||
return IsProjectDocumentEditingAllowed(context) && !context.GetProjectPath().empty();
|
||||
}
|
||||
|
||||
inline IProjectManager::SceneAssetReferenceMigrationReport MigrateSceneAssetReferences(IEditorContext& context) {
|
||||
if (!CanMigrateSceneAssetReferences(context)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return context.GetProjectManager().MigrateSceneAssetReferences();
|
||||
}
|
||||
|
||||
inline bool SwitchProject(IEditorContext& context, const std::string& projectPath) {
|
||||
if (!IsProjectDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
|
||||
@@ -30,7 +30,7 @@ inline AssetReferenceInteraction DrawAssetReferenceProperty(
|
||||
pickerOptions.assetsTabLabel = "Assets";
|
||||
pickerOptions.sceneTabLabel = "Scene";
|
||||
pickerOptions.showAssetsTab = true;
|
||||
pickerOptions.showSceneTab = true;
|
||||
pickerOptions.showSceneTab = false;
|
||||
pickerOptions.supportedAssetExtensions = supportedExtensions;
|
||||
|
||||
UI::DrawPropertyRow(label, UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "ComponentEditors/LightComponentEditor.h"
|
||||
#include "ComponentEditors/MeshFilterComponentEditor.h"
|
||||
#include "ComponentEditors/MeshRendererComponentEditor.h"
|
||||
#include "ComponentEditors/ScriptComponentEditor.h"
|
||||
#include "ComponentEditors/TransformComponentEditor.h"
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -20,6 +21,7 @@ ComponentEditorRegistry::ComponentEditorRegistry() {
|
||||
RegisterEditor(std::make_unique<LightComponentEditor>());
|
||||
RegisterEditor(std::make_unique<MeshFilterComponentEditor>());
|
||||
RegisterEditor(std::make_unique<MeshRendererComponentEditor>());
|
||||
RegisterEditor(std::make_unique<ScriptComponentEditor>());
|
||||
}
|
||||
|
||||
void ComponentEditorRegistry::RegisterEditor(std::unique_ptr<IComponentEditor> editor) {
|
||||
|
||||
515
editor/src/ComponentEditors/ScriptComponentEditor.h
Normal file
@@ -0,0 +1,515 @@
|
||||
#pragma once
|
||||
|
||||
#include "Application.h"
|
||||
#include "IComponentEditor.h"
|
||||
#include "ScriptComponentEditorUtils.h"
|
||||
#include "Core/IUndoManager.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <XCEngine/Scripting/ScriptComponent.h>
|
||||
#include <XCEngine/Scripting/ScriptEngine.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
class ScriptComponentEditor : public IComponentEditor {
|
||||
public:
|
||||
const char* GetComponentTypeName() const override {
|
||||
return "ScriptComponent";
|
||||
}
|
||||
|
||||
const char* GetDisplayName() const override {
|
||||
return "Script";
|
||||
}
|
||||
|
||||
bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override {
|
||||
auto* scriptComponent = dynamic_cast<::XCEngine::Scripting::ScriptComponent*>(component);
|
||||
if (!scriptComponent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
changed |= RenderScriptClassSelector(*scriptComponent, undoManager);
|
||||
|
||||
::XCEngine::Scripting::ScriptFieldModel model;
|
||||
if (!::XCEngine::Scripting::ScriptEngine::Get().TryGetScriptFieldModel(scriptComponent, model)) {
|
||||
UI::DrawHintText("Failed to query script field metadata.");
|
||||
return changed;
|
||||
}
|
||||
|
||||
switch (model.classStatus) {
|
||||
case ::XCEngine::Scripting::ScriptFieldClassStatus::Unassigned:
|
||||
UI::DrawHintText("Assign a C# script to edit serialized fields.");
|
||||
return changed;
|
||||
case ::XCEngine::Scripting::ScriptFieldClassStatus::Missing:
|
||||
UI::DrawHintText("Assigned script class is not available in the loaded script assemblies.");
|
||||
break;
|
||||
case ::XCEngine::Scripting::ScriptFieldClassStatus::Available:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (model.fields.empty()) {
|
||||
UI::DrawHintText(
|
||||
model.classStatus == ::XCEngine::Scripting::ScriptFieldClassStatus::Available
|
||||
? "Selected script exposes no supported public instance fields."
|
||||
: "No serialized script fields are currently available.");
|
||||
return changed;
|
||||
}
|
||||
|
||||
for (const ::XCEngine::Scripting::ScriptFieldSnapshot& field : model.fields) {
|
||||
changed |= RenderScriptField(*scriptComponent, model.classStatus, field, undoManager);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override {
|
||||
return gameObject != nullptr;
|
||||
}
|
||||
|
||||
const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override {
|
||||
return gameObject ? nullptr : "Invalid";
|
||||
}
|
||||
|
||||
bool CanRemove(::XCEngine::Components::Component* component) const override {
|
||||
return CanEdit(component);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr size_t kStringBufferSize = 512;
|
||||
|
||||
struct StringFieldEditState {
|
||||
std::array<char, kStringBufferSize> buffer{};
|
||||
std::string lastSyncedValue;
|
||||
bool initialized = false;
|
||||
bool editing = false;
|
||||
};
|
||||
|
||||
bool RenderScriptClassSelector(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
IUndoManager* undoManager) {
|
||||
std::vector<::XCEngine::Scripting::ScriptClassDescriptor> scriptClasses;
|
||||
const bool hasLoadedClasses =
|
||||
::XCEngine::Scripting::ScriptEngine::Get().TryGetAvailableScriptClasses(scriptClasses);
|
||||
|
||||
const ::XCEngine::Scripting::ScriptClassDescriptor currentDescriptor{
|
||||
scriptComponent.GetAssemblyName(),
|
||||
scriptComponent.GetNamespaceName(),
|
||||
scriptComponent.GetClassName()
|
||||
};
|
||||
|
||||
std::string currentLabel = "None";
|
||||
if (scriptComponent.HasScriptClass()) {
|
||||
const auto currentIt = std::find(scriptClasses.begin(), scriptClasses.end(), currentDescriptor);
|
||||
currentLabel = currentIt != scriptClasses.end()
|
||||
? ScriptComponentEditorUtils::BuildScriptClassDisplayName(*currentIt)
|
||||
: ScriptComponentEditorUtils::BuildScriptClassDisplayName(scriptComponent) + " (Missing)";
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
UI::DrawPropertyRow("Script", UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
ImGui::SetNextItemWidth(layout.controlWidth);
|
||||
if (!ImGui::BeginCombo("##ScriptClass", currentLabel.c_str())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ImGui::Selectable("None", !scriptComponent.HasScriptClass())) {
|
||||
changed |= ApplyScriptClassSelection(scriptComponent, nullptr, undoManager);
|
||||
}
|
||||
|
||||
if (!scriptClasses.empty()) {
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
for (const ::XCEngine::Scripting::ScriptClassDescriptor& descriptor : scriptClasses) {
|
||||
const bool selected = descriptor == currentDescriptor;
|
||||
const std::string label =
|
||||
ScriptComponentEditorUtils::BuildScriptClassDisplayName(descriptor);
|
||||
if (ImGui::Selectable(label.c_str(), selected)) {
|
||||
changed |= ApplyScriptClassSelection(scriptComponent, &descriptor, undoManager);
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasLoadedClasses) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("No script assemblies are currently loaded.");
|
||||
}
|
||||
|
||||
ImGui::EndCombo();
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!hasLoadedClasses) {
|
||||
const EditorScriptRuntimeStatus& runtimeStatus = Application::Get().GetScriptRuntimeStatus();
|
||||
const std::string hintText =
|
||||
ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(runtimeStatus);
|
||||
UI::DrawHintText(hintText.c_str());
|
||||
|
||||
if (ScriptComponentEditorUtils::CanRebuildScriptAssemblies(runtimeStatus)) {
|
||||
if (UI::InspectorActionButton("Rebuild Scripts", ImVec2(120.0f, 0.0f))) {
|
||||
Application::Get().RebuildScriptingAssemblies();
|
||||
}
|
||||
}
|
||||
|
||||
if (ScriptComponentEditorUtils::CanReloadScriptRuntime(runtimeStatus)) {
|
||||
if (ScriptComponentEditorUtils::CanRebuildScriptAssemblies(runtimeStatus)) {
|
||||
ImGui::SameLine();
|
||||
}
|
||||
if (UI::InspectorActionButton("Reload Scripts", ImVec2(120.0f, 0.0f))) {
|
||||
Application::Get().ReloadScriptingRuntime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool ApplyScriptClassSelection(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptClassDescriptor* descriptor,
|
||||
IUndoManager* undoManager) const {
|
||||
if (!descriptor) {
|
||||
if (!scriptComponent.HasScriptClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (undoManager) {
|
||||
undoManager->BeginInteractiveChange("Modify Script Component");
|
||||
}
|
||||
scriptComponent.ClearScriptClass();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (scriptComponent.GetAssemblyName() == descriptor->assemblyName &&
|
||||
scriptComponent.GetNamespaceName() == descriptor->namespaceName &&
|
||||
scriptComponent.GetClassName() == descriptor->className) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (undoManager) {
|
||||
undoManager->BeginInteractiveChange("Modify Script Component");
|
||||
}
|
||||
scriptComponent.SetScriptClass(
|
||||
descriptor->assemblyName,
|
||||
descriptor->namespaceName,
|
||||
descriptor->className);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderScriptField(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
::XCEngine::Scripting::ScriptFieldClassStatus classStatus,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
IUndoManager* undoManager) {
|
||||
bool changed = false;
|
||||
const bool canEdit = ScriptComponentEditorUtils::CanEditScriptField(classStatus, field);
|
||||
|
||||
if (canEdit) {
|
||||
changed |= RenderScriptFieldEditor(scriptComponent, field, undoManager);
|
||||
} else {
|
||||
RenderReadOnlyScriptField(field);
|
||||
}
|
||||
|
||||
const std::string issueText = ScriptComponentEditorUtils::BuildScriptFieldIssueText(field);
|
||||
if (!issueText.empty()) {
|
||||
UI::DrawHintText(issueText.c_str());
|
||||
}
|
||||
|
||||
if (ScriptComponentEditorUtils::CanClearScriptFieldOverride(field)) {
|
||||
ImGui::PushID((field.metadata.name + "##Reset").c_str());
|
||||
if (UI::InspectorActionButton("Reset", ImVec2(72.0f, 0.0f))) {
|
||||
changed |= ClearScriptFieldOverride(scriptComponent, field, undoManager);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool RenderScriptFieldEditor(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
IUndoManager* undoManager) {
|
||||
using namespace ::XCEngine::Scripting;
|
||||
|
||||
switch (field.metadata.type) {
|
||||
case ScriptFieldType::Float: {
|
||||
float value = std::get<float>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyFloat(field.metadata.name.c_str(), value, 0.1f);
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::Double: {
|
||||
double value = std::get<double>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
ImGui::SetNextItemWidth(layout.controlWidth);
|
||||
return ImGui::InputDouble("##Value", &value, 0.0, 0.0, "%.3f");
|
||||
});
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::Bool: {
|
||||
bool value = std::get<bool>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyBool(field.metadata.name.c_str(), value);
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::Int32: {
|
||||
int value = std::get<int32_t>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyInt(field.metadata.name.c_str(), value, 1);
|
||||
return widgetChanged &&
|
||||
ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(static_cast<int32_t>(value)), undoManager);
|
||||
}
|
||||
case ScriptFieldType::UInt64: {
|
||||
uint64_t value = std::get<uint64_t>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
ImGui::SetNextItemWidth(layout.controlWidth);
|
||||
return ImGui::InputScalar("##Value", ImGuiDataType_U64, &value);
|
||||
});
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::String:
|
||||
return RenderStringScriptFieldEditor(scriptComponent, field, undoManager);
|
||||
case ScriptFieldType::Vector2: {
|
||||
::XCEngine::Math::Vector2 value = std::get<::XCEngine::Math::Vector2>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyVec2(field.metadata.name.c_str(), value, 0.0f, 0.1f);
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::Vector3: {
|
||||
::XCEngine::Math::Vector3 value = std::get<::XCEngine::Math::Vector3>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyVec3Input(field.metadata.name.c_str(), value, 0.1f);
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::Vector4: {
|
||||
::XCEngine::Math::Vector4 value = std::get<::XCEngine::Math::Vector4>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
ImGui::SetNextItemWidth(layout.controlWidth);
|
||||
return ImGui::DragFloat4("##Value", &value.x, 0.1f);
|
||||
});
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::GameObject:
|
||||
return RenderGameObjectScriptFieldEditor(scriptComponent, field, undoManager);
|
||||
case ScriptFieldType::None:
|
||||
default:
|
||||
RenderReadOnlyScriptField(field);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderStringScriptFieldEditor(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
IUndoManager* undoManager) {
|
||||
StringFieldEditState& editState =
|
||||
m_stringFieldStates[scriptComponent.GetScriptComponentUUID()][field.metadata.name];
|
||||
const std::string currentValue = std::get<std::string>(field.value);
|
||||
|
||||
if (!editState.initialized || (!editState.editing && editState.lastSyncedValue != currentValue)) {
|
||||
SyncStringFieldEditState(editState, currentValue);
|
||||
}
|
||||
|
||||
bool isEditing = false;
|
||||
const bool widgetChanged = UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
ImGui::SetNextItemWidth(layout.controlWidth);
|
||||
const bool changed = ImGui::InputText("##Value", editState.buffer.data(), editState.buffer.size());
|
||||
isEditing = ImGui::IsItemActive();
|
||||
return changed;
|
||||
});
|
||||
editState.editing = isEditing;
|
||||
|
||||
if (!widgetChanged) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string editedValue(editState.buffer.data());
|
||||
if (!ApplyScriptFieldWrite(
|
||||
scriptComponent,
|
||||
field,
|
||||
::XCEngine::Scripting::ScriptFieldValue(editedValue),
|
||||
undoManager)) {
|
||||
SyncStringFieldEditState(editState, currentValue);
|
||||
return false;
|
||||
}
|
||||
|
||||
editState.lastSyncedValue = editedValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderGameObjectScriptFieldEditor(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
IUndoManager* undoManager) {
|
||||
using namespace ::XCEngine::Scripting;
|
||||
|
||||
const GameObjectReference currentReference = std::get<GameObjectReference>(field.value);
|
||||
const auto& sceneRoots = Application::Get().GetEditorContext().GetSceneManager().GetRootEntities();
|
||||
const ::XCEngine::Components::GameObject::ID currentGameObjectId =
|
||||
ScriptComponentEditorUtils::FindGameObjectIdByUuid(sceneRoots, currentReference.gameObjectUUID);
|
||||
|
||||
UI::ReferencePickerOptions pickerOptions;
|
||||
pickerOptions.popupTitle = "Select GameObject";
|
||||
pickerOptions.emptyHint = "None";
|
||||
pickerOptions.searchHint = "Search";
|
||||
pickerOptions.noSceneText = "No scene objects.";
|
||||
pickerOptions.showAssetsTab = false;
|
||||
pickerOptions.showSceneTab = true;
|
||||
|
||||
GameObjectReference pendingReference = currentReference;
|
||||
const bool widgetChanged = UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
const UI::ReferencePickerInteraction interaction =
|
||||
UI::DrawReferencePickerControl(
|
||||
std::string(),
|
||||
currentGameObjectId,
|
||||
pickerOptions,
|
||||
layout.controlWidth);
|
||||
|
||||
if (interaction.clearRequested) {
|
||||
pendingReference = GameObjectReference{};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (interaction.assignedSceneObjectId == ::XCEngine::Components::GameObject::INVALID_ID ||
|
||||
interaction.assignedSceneObjectId == currentGameObjectId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
::XCEngine::Components::GameObject* assignedGameObject =
|
||||
Application::Get().GetEditorContext().GetSceneManager().GetEntity(interaction.assignedSceneObjectId);
|
||||
if (!assignedGameObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pendingReference = GameObjectReference{assignedGameObject->GetUUID()};
|
||||
return true;
|
||||
});
|
||||
|
||||
return widgetChanged &&
|
||||
ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(pendingReference), undoManager);
|
||||
}
|
||||
|
||||
void RenderReadOnlyScriptField(const ::XCEngine::Scripting::ScriptFieldSnapshot& field) const {
|
||||
const std::string valueText = DescribeScriptFieldValue(field.metadata.type, field.value);
|
||||
UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetTextLineHeight());
|
||||
ImGui::TextDisabled("%s", valueText.c_str());
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
bool ApplyScriptFieldWrite(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
const ::XCEngine::Scripting::ScriptFieldValue& value,
|
||||
IUndoManager* undoManager) const {
|
||||
std::vector<::XCEngine::Scripting::ScriptFieldWriteResult> results;
|
||||
if (undoManager) {
|
||||
undoManager->BeginInteractiveChange("Modify Script Field");
|
||||
}
|
||||
|
||||
const bool applied = ::XCEngine::Scripting::ScriptEngine::Get().ApplyScriptFieldWrites(
|
||||
&scriptComponent,
|
||||
{ ::XCEngine::Scripting::ScriptFieldWriteRequest{field.metadata.name, field.metadata.type, value} },
|
||||
results);
|
||||
|
||||
if (!applied || results.empty() ||
|
||||
results.front().status != ::XCEngine::Scripting::ScriptFieldWriteStatus::Applied) {
|
||||
if (undoManager && undoManager->HasPendingInteractiveChange()) {
|
||||
undoManager->CancelInteractiveChange();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClearScriptFieldOverride(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
IUndoManager* undoManager) const {
|
||||
std::vector<::XCEngine::Scripting::ScriptFieldClearResult> results;
|
||||
if (undoManager) {
|
||||
undoManager->BeginInteractiveChange("Clear Script Field Override");
|
||||
}
|
||||
|
||||
const bool cleared = ::XCEngine::Scripting::ScriptEngine::Get().ClearScriptFieldOverrides(
|
||||
&scriptComponent,
|
||||
{ ::XCEngine::Scripting::ScriptFieldClearRequest{field.metadata.name} },
|
||||
results);
|
||||
|
||||
if (!cleared || results.empty() ||
|
||||
results.front().status != ::XCEngine::Scripting::ScriptFieldClearStatus::Applied) {
|
||||
if (undoManager && undoManager->HasPendingInteractiveChange()) {
|
||||
undoManager->CancelInteractiveChange();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void SyncStringFieldEditState(StringFieldEditState& editState, const std::string& value) {
|
||||
editState.buffer.fill('\0');
|
||||
const size_t copyLength = (std::min)(value.size(), editState.buffer.size() - 1);
|
||||
if (copyLength > 0) {
|
||||
std::memcpy(editState.buffer.data(), value.data(), copyLength);
|
||||
}
|
||||
editState.buffer[copyLength] = '\0';
|
||||
editState.lastSyncedValue = value;
|
||||
editState.initialized = true;
|
||||
}
|
||||
|
||||
static std::string DescribeScriptFieldValue(
|
||||
::XCEngine::Scripting::ScriptFieldType type,
|
||||
const ::XCEngine::Scripting::ScriptFieldValue& value) {
|
||||
if (type == ::XCEngine::Scripting::ScriptFieldType::GameObject) {
|
||||
const auto reference = std::get<::XCEngine::Scripting::GameObjectReference>(value);
|
||||
if (reference.gameObjectUUID == 0) {
|
||||
return "None";
|
||||
}
|
||||
|
||||
return "GameObject (" + std::to_string(reference.gameObjectUUID) + ")";
|
||||
}
|
||||
|
||||
return ::XCEngine::Scripting::SerializeScriptFieldValue(type, value);
|
||||
}
|
||||
|
||||
std::unordered_map<uint64_t, std::unordered_map<std::string, StringFieldEditState>> m_stringFieldStates;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
125
editor/src/ComponentEditors/ScriptComponentEditorUtils.h
Normal file
@@ -0,0 +1,125 @@
|
||||
#pragma once
|
||||
|
||||
#include "Scripting/EditorScriptRuntimeStatus.h"
|
||||
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Scripting/IScriptRuntime.h>
|
||||
#include <XCEngine/Scripting/ScriptComponent.h>
|
||||
#include <XCEngine/Scripting/ScriptField.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace ScriptComponentEditorUtils {
|
||||
|
||||
inline std::string BuildScriptClassDisplayName(const ::XCEngine::Scripting::ScriptClassDescriptor& descriptor) {
|
||||
const std::string fullName = descriptor.GetFullName();
|
||||
if (descriptor.assemblyName.empty() || descriptor.assemblyName == "GameScripts") {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
return fullName + " (" + descriptor.assemblyName + ")";
|
||||
}
|
||||
|
||||
inline std::string BuildScriptClassDisplayName(const ::XCEngine::Scripting::ScriptComponent& component) {
|
||||
if (!component.HasScriptClass()) {
|
||||
return "None";
|
||||
}
|
||||
|
||||
return BuildScriptClassDisplayName(::XCEngine::Scripting::ScriptClassDescriptor{
|
||||
component.GetAssemblyName(),
|
||||
component.GetNamespaceName(),
|
||||
component.GetClassName()
|
||||
});
|
||||
}
|
||||
|
||||
inline ::XCEngine::Components::GameObject::ID FindGameObjectIdByUuidRecursive(
|
||||
::XCEngine::Components::GameObject* gameObject,
|
||||
uint64_t uuid) {
|
||||
if (!gameObject || uuid == 0) {
|
||||
return ::XCEngine::Components::GameObject::INVALID_ID;
|
||||
}
|
||||
|
||||
if (gameObject->GetUUID() == uuid) {
|
||||
return gameObject->GetID();
|
||||
}
|
||||
|
||||
for (::XCEngine::Components::GameObject* child : gameObject->GetChildren()) {
|
||||
const ::XCEngine::Components::GameObject::ID childId =
|
||||
FindGameObjectIdByUuidRecursive(child, uuid);
|
||||
if (childId != ::XCEngine::Components::GameObject::INVALID_ID) {
|
||||
return childId;
|
||||
}
|
||||
}
|
||||
|
||||
return ::XCEngine::Components::GameObject::INVALID_ID;
|
||||
}
|
||||
|
||||
inline ::XCEngine::Components::GameObject::ID FindGameObjectIdByUuid(
|
||||
const std::vector<::XCEngine::Components::GameObject*>& roots,
|
||||
uint64_t uuid) {
|
||||
for (::XCEngine::Components::GameObject* root : roots) {
|
||||
const ::XCEngine::Components::GameObject::ID gameObjectId =
|
||||
FindGameObjectIdByUuidRecursive(root, uuid);
|
||||
if (gameObjectId != ::XCEngine::Components::GameObject::INVALID_ID) {
|
||||
return gameObjectId;
|
||||
}
|
||||
}
|
||||
|
||||
return ::XCEngine::Components::GameObject::INVALID_ID;
|
||||
}
|
||||
|
||||
inline bool CanEditScriptField(
|
||||
::XCEngine::Scripting::ScriptFieldClassStatus classStatus,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field) {
|
||||
return classStatus != ::XCEngine::Scripting::ScriptFieldClassStatus::Available || field.declaredInClass;
|
||||
}
|
||||
|
||||
inline bool CanClearScriptFieldOverride(const ::XCEngine::Scripting::ScriptFieldSnapshot& field) {
|
||||
return field.hasStoredValue || field.valueSource == ::XCEngine::Scripting::ScriptFieldValueSource::ManagedValue;
|
||||
}
|
||||
|
||||
inline std::string BuildScriptFieldIssueText(const ::XCEngine::Scripting::ScriptFieldSnapshot& field) {
|
||||
switch (field.issue) {
|
||||
case ::XCEngine::Scripting::ScriptFieldIssue::StoredOnly:
|
||||
return "Stored override is not declared by the selected script.";
|
||||
case ::XCEngine::Scripting::ScriptFieldIssue::TypeMismatch:
|
||||
return "Stored override type is " +
|
||||
::XCEngine::Scripting::ScriptFieldTypeToString(field.storedType) +
|
||||
", but the script field expects " +
|
||||
::XCEngine::Scripting::ScriptFieldTypeToString(field.metadata.type) + ".";
|
||||
case ::XCEngine::Scripting::ScriptFieldIssue::None:
|
||||
default:
|
||||
return std::string();
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string BuildScriptRuntimeUnavailableHint(const EditorScriptRuntimeStatus& status) {
|
||||
if (!status.statusMessage.empty()) {
|
||||
return status.statusMessage;
|
||||
}
|
||||
|
||||
if (!status.backendEnabled) {
|
||||
return "This editor build does not include Mono scripting support.";
|
||||
}
|
||||
|
||||
if (!status.assemblyDirectory.empty()) {
|
||||
return "No script assemblies are currently loaded from " + status.assemblyDirectory + ".";
|
||||
}
|
||||
|
||||
return "No script assemblies are currently loaded.";
|
||||
}
|
||||
|
||||
inline bool CanReloadScriptRuntime(const EditorScriptRuntimeStatus& status) {
|
||||
return status.backendEnabled && !status.assemblyDirectory.empty();
|
||||
}
|
||||
|
||||
inline bool CanRebuildScriptAssemblies(const EditorScriptRuntimeStatus& status) {
|
||||
return status.backendEnabled && !status.assemblyDirectory.empty();
|
||||
}
|
||||
|
||||
} // namespace ScriptComponentEditorUtils
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
#include "EditorRuntimeMode.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
@@ -67,6 +72,19 @@ struct PlayModeResumeRequestedEvent {
|
||||
struct PlayModeStepRequestedEvent {
|
||||
};
|
||||
|
||||
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 = {};
|
||||
};
|
||||
|
||||
struct EditorModeChangedEvent {
|
||||
EditorRuntimeMode oldMode = EditorRuntimeMode::Edit;
|
||||
EditorRuntimeMode newMode = EditorRuntimeMode::Edit;
|
||||
|
||||
@@ -83,12 +83,19 @@ public:
|
||||
static_assert(sizeof(T) > 0, "Event type must be defined");
|
||||
uint32_t typeId = EventTypeId<T>::Get();
|
||||
|
||||
std::shared_lock<std::shared_mutex> lock(m_mutex);
|
||||
auto it = m_handlers.find(typeId);
|
||||
if (it != m_handlers.end()) {
|
||||
for (const auto& entry : it->second) {
|
||||
entry.handler(&event);
|
||||
std::vector<HandlerEntry> handlers;
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(m_mutex);
|
||||
auto it = m_handlers.find(typeId);
|
||||
if (it == m_handlers.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
handlers = it->second;
|
||||
}
|
||||
|
||||
for (const auto& entry : handlers) {
|
||||
entry.handler(&event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
@@ -10,6 +11,13 @@ namespace Editor {
|
||||
|
||||
class IProjectManager {
|
||||
public:
|
||||
struct SceneAssetReferenceMigrationReport {
|
||||
size_t scannedSceneCount = 0;
|
||||
size_t migratedSceneCount = 0;
|
||||
size_t unchangedSceneCount = 0;
|
||||
size_t failedSceneCount = 0;
|
||||
};
|
||||
|
||||
virtual ~IProjectManager() = default;
|
||||
|
||||
virtual const std::vector<AssetItemPtr>& GetCurrentItems() const = 0;
|
||||
@@ -39,6 +47,7 @@ public:
|
||||
virtual bool DeleteItem(const std::string& fullPath) = 0;
|
||||
virtual bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) = 0;
|
||||
virtual bool RenameItem(const std::string& sourceFullPath, const std::string& newName) = 0;
|
||||
virtual SceneAssetReferenceMigrationReport MigrateSceneAssetReferences() = 0;
|
||||
|
||||
virtual const std::string& GetProjectPath() const = 0;
|
||||
};
|
||||
|
||||
@@ -6,9 +6,25 @@
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "Core/IUndoManager.h"
|
||||
|
||||
#include <XCEngine/Input/InputManager.h>
|
||||
#include <XCEngine/Scripting/ScriptEngine.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsModifierKeyDown(const GameViewInputFrameEvent& input, XCEngine::Input::KeyCode key) {
|
||||
const size_t index = static_cast<size_t>(key);
|
||||
return index < input.keyDown.size() && input.keyDown[index];
|
||||
}
|
||||
|
||||
bool IsGameViewInputActive(const GameViewInputFrameEvent& input) {
|
||||
return input.hovered || input.focused;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PlaySessionController::Attach(IEditorContext& context) {
|
||||
if (m_playStartRequestedHandlerId == 0) {
|
||||
m_playStartRequestedHandlerId = context.GetEventBus().Subscribe<PlayModeStartRequestedEvent>(
|
||||
@@ -44,6 +60,14 @@ void PlaySessionController::Attach(IEditorContext& context) {
|
||||
StepPlay(context);
|
||||
});
|
||||
}
|
||||
|
||||
if (m_gameViewInputFrameHandlerId == 0) {
|
||||
m_gameViewInputFrameHandlerId = context.GetEventBus().Subscribe<GameViewInputFrameEvent>(
|
||||
[this](const GameViewInputFrameEvent& event) {
|
||||
m_pendingGameViewInput = event;
|
||||
m_hasPendingGameViewInput = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void PlaySessionController::Detach(IEditorContext& context) {
|
||||
@@ -73,6 +97,13 @@ void PlaySessionController::Detach(IEditorContext& context) {
|
||||
context.GetEventBus().Unsubscribe<PlayModeStepRequestedEvent>(m_playStepRequestedHandlerId);
|
||||
m_playStepRequestedHandlerId = 0;
|
||||
}
|
||||
|
||||
if (m_gameViewInputFrameHandlerId != 0) {
|
||||
context.GetEventBus().Unsubscribe<GameViewInputFrameEvent>(m_gameViewInputFrameHandlerId);
|
||||
m_gameViewInputFrameHandlerId = 0;
|
||||
}
|
||||
|
||||
ResetRuntimeInputBridge();
|
||||
}
|
||||
|
||||
void PlaySessionController::Update(IEditorContext& context, float deltaTime) {
|
||||
@@ -81,6 +112,7 @@ void PlaySessionController::Update(IEditorContext& context, float deltaTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyGameViewInputFrame(deltaTime);
|
||||
m_runtimeLoop.Tick(deltaTime);
|
||||
}
|
||||
|
||||
@@ -104,6 +136,10 @@ bool PlaySessionController::StartPlay(IEditorContext& context) {
|
||||
}
|
||||
|
||||
sceneManager.SetSceneDocumentDirtyTrackingEnabled(false);
|
||||
XCEngine::Scripting::ScriptEngine::Get().SetRuntimeFixedDeltaTime(m_runtimeLoop.GetSettings().fixedDeltaTime);
|
||||
ResetRuntimeInputBridge();
|
||||
XCEngine::Input::InputManager::Get().Shutdown();
|
||||
XCEngine::Input::InputManager::Get().Initialize(nullptr);
|
||||
m_runtimeLoop.Start(sceneManager.GetScene());
|
||||
context.GetUndoManager().ClearHistory();
|
||||
context.SetRuntimeMode(EditorRuntimeMode::Play);
|
||||
@@ -118,6 +154,8 @@ bool PlaySessionController::StopPlay(IEditorContext& context) {
|
||||
|
||||
auto& sceneManager = context.GetSceneManager();
|
||||
m_runtimeLoop.Stop();
|
||||
ResetRuntimeInputBridge();
|
||||
XCEngine::Input::InputManager::Get().Shutdown();
|
||||
sceneManager.SetSceneDocumentDirtyTrackingEnabled(true);
|
||||
|
||||
if (!sceneManager.RestoreSceneSnapshot(m_editorSnapshot)) {
|
||||
@@ -162,5 +200,83 @@ bool PlaySessionController::StepPlay(IEditorContext& context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlaySessionController::ResetRuntimeInputBridge() {
|
||||
m_pendingGameViewInput = {};
|
||||
m_appliedGameViewInput = {};
|
||||
m_hasPendingGameViewInput = false;
|
||||
}
|
||||
|
||||
void PlaySessionController::ApplyGameViewInputFrame(float deltaTime) {
|
||||
using XCEngine::Input::InputManager;
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::Input::MouseButton;
|
||||
|
||||
InputManager& inputManager = InputManager::Get();
|
||||
inputManager.Update(deltaTime);
|
||||
|
||||
const GameViewInputFrameEvent input = m_hasPendingGameViewInput
|
||||
? m_pendingGameViewInput
|
||||
: GameViewInputFrameEvent{};
|
||||
m_hasPendingGameViewInput = false;
|
||||
|
||||
const bool inputActive = IsGameViewInputActive(input);
|
||||
const bool alt = inputActive &&
|
||||
(IsModifierKeyDown(input, KeyCode::LeftAlt) || IsModifierKeyDown(input, KeyCode::RightAlt));
|
||||
const bool ctrl = inputActive &&
|
||||
(IsModifierKeyDown(input, KeyCode::LeftCtrl) || IsModifierKeyDown(input, KeyCode::RightCtrl));
|
||||
const bool shift = inputActive &&
|
||||
(IsModifierKeyDown(input, KeyCode::LeftShift) || IsModifierKeyDown(input, KeyCode::RightShift));
|
||||
|
||||
for (size_t index = 0; index < input.keyDown.size(); ++index) {
|
||||
const bool wasDown = m_appliedGameViewInput.keyDown[index];
|
||||
const bool isDown = inputActive && input.keyDown[index];
|
||||
if (wasDown == isDown) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const KeyCode key = static_cast<KeyCode>(index);
|
||||
if (isDown) {
|
||||
inputManager.ProcessKeyDown(key, false, alt, ctrl, shift, false);
|
||||
} else {
|
||||
inputManager.ProcessKeyUp(key, alt, ctrl, shift, false);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < input.mouseButtonDown.size(); ++index) {
|
||||
const bool wasDown = m_appliedGameViewInput.mouseButtonDown[index];
|
||||
const bool isDown = inputActive && input.mouseButtonDown[index];
|
||||
if (wasDown == isDown) {
|
||||
continue;
|
||||
}
|
||||
|
||||
inputManager.ProcessMouseButton(
|
||||
static_cast<MouseButton>(index),
|
||||
isDown,
|
||||
static_cast<int>(input.mousePosition.x),
|
||||
static_cast<int>(input.mousePosition.y));
|
||||
}
|
||||
|
||||
if (inputActive &&
|
||||
(input.mousePosition != m_appliedGameViewInput.mousePosition || input.mouseDelta != XCEngine::Math::Vector2::Zero())) {
|
||||
inputManager.ProcessMouseMove(
|
||||
static_cast<int>(input.mousePosition.x),
|
||||
static_cast<int>(input.mousePosition.y),
|
||||
static_cast<int>(input.mouseDelta.x),
|
||||
static_cast<int>(input.mouseDelta.y));
|
||||
}
|
||||
|
||||
if (inputActive && input.mouseWheel != 0.0f) {
|
||||
inputManager.ProcessMouseWheel(
|
||||
input.mouseWheel,
|
||||
static_cast<int>(input.mousePosition.x),
|
||||
static_cast<int>(input.mousePosition.y));
|
||||
}
|
||||
|
||||
m_appliedGameViewInput = {};
|
||||
if (inputActive) {
|
||||
m_appliedGameViewInput = input;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
#include "EditorRuntimeMode.h"
|
||||
#include "SceneSnapshot.h"
|
||||
|
||||
#include "Core/EditorEvents.h"
|
||||
|
||||
#include <XCEngine/Scene/RuntimeLoop.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -26,12 +29,19 @@ public:
|
||||
bool StepPlay(IEditorContext& context);
|
||||
|
||||
private:
|
||||
void ResetRuntimeInputBridge();
|
||||
void ApplyGameViewInputFrame(float deltaTime);
|
||||
|
||||
uint64_t m_playStartRequestedHandlerId = 0;
|
||||
uint64_t m_playStopRequestedHandlerId = 0;
|
||||
uint64_t m_playPauseRequestedHandlerId = 0;
|
||||
uint64_t m_playResumeRequestedHandlerId = 0;
|
||||
uint64_t m_playStepRequestedHandlerId = 0;
|
||||
uint64_t m_gameViewInputFrameHandlerId = 0;
|
||||
SceneSnapshot m_editorSnapshot = {};
|
||||
GameViewInputFrameEvent m_pendingGameViewInput = {};
|
||||
GameViewInputFrameEvent m_appliedGameViewInput = {};
|
||||
bool m_hasPendingGameViewInput = false;
|
||||
XCEngine::Components::RuntimeLoop m_runtimeLoop;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
#include "ProjectManager.h"
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cwctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <initializer_list>
|
||||
#include <string_view>
|
||||
#include <windows.h>
|
||||
@@ -212,6 +217,27 @@ bool CanPreviewImageAssetExtension(std::wstring_view extension) {
|
||||
});
|
||||
}
|
||||
|
||||
bool IsSceneAssetFile(const fs::path& path) {
|
||||
if (!fs::is_regular_file(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring extension = path.extension().wstring();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower);
|
||||
return extension == L".xc";
|
||||
}
|
||||
|
||||
std::string ReadFileText(const fs::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << input.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<AssetItemPtr>& ProjectManager::GetCurrentItems() const {
|
||||
@@ -496,6 +522,89 @@ bool ProjectManager::RenameItem(const std::string& sourceFullPath, const std::st
|
||||
}
|
||||
}
|
||||
|
||||
IProjectManager::SceneAssetReferenceMigrationReport ProjectManager::MigrateSceneAssetReferences() {
|
||||
IProjectManager::SceneAssetReferenceMigrationReport report;
|
||||
if (m_projectPath.empty()) {
|
||||
return report;
|
||||
}
|
||||
|
||||
const fs::path assetsPath = fs::path(Utf8PathToWstring(m_projectPath)) / L"Assets";
|
||||
if (!fs::exists(assetsPath) || !fs::is_directory(assetsPath)) {
|
||||
return report;
|
||||
}
|
||||
|
||||
auto& logger = ::XCEngine::Debug::Logger::Get();
|
||||
::XCEngine::Resources::ResourceManager& resourceManager = ::XCEngine::Resources::ResourceManager::Get();
|
||||
resourceManager.Initialize();
|
||||
|
||||
const std::string previousRoot = resourceManager.GetResourceRoot().CStr();
|
||||
const bool restoreResourceRoot = previousRoot != m_projectPath;
|
||||
if (restoreResourceRoot) {
|
||||
resourceManager.SetResourceRoot(m_projectPath.c_str());
|
||||
}
|
||||
|
||||
try {
|
||||
for (const fs::directory_entry& entry : fs::recursive_directory_iterator(assetsPath)) {
|
||||
if (!IsSceneAssetFile(entry.path())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
++report.scannedSceneCount;
|
||||
const fs::path scenePath = entry.path();
|
||||
const std::string scenePathUtf8 = WstringPathToUtf8(scenePath.wstring());
|
||||
|
||||
try {
|
||||
const std::string before = ReadFileText(scenePath);
|
||||
::XCEngine::Components::Scene scene;
|
||||
{
|
||||
::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope(resourceManager);
|
||||
scene.Load(scenePathUtf8);
|
||||
}
|
||||
|
||||
scene.Save(scenePathUtf8);
|
||||
const std::string after = ReadFileText(scenePath);
|
||||
if (after != before) {
|
||||
++report.migratedSceneCount;
|
||||
} else {
|
||||
++report.unchangedSceneCount;
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
++report.failedSceneCount;
|
||||
logger.Error(
|
||||
::XCEngine::Debug::LogCategory::FileSystem,
|
||||
("Failed to migrate scene asset references: " + scenePathUtf8 + " - " + exception.what()).c_str());
|
||||
} catch (...) {
|
||||
++report.failedSceneCount;
|
||||
logger.Error(
|
||||
::XCEngine::Debug::LogCategory::FileSystem,
|
||||
("Failed to migrate scene asset references: " + scenePathUtf8 + " - unknown error").c_str());
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
logger.Error(
|
||||
::XCEngine::Debug::LogCategory::FileSystem,
|
||||
("Scene asset reference migration aborted: " + std::string(exception.what())).c_str());
|
||||
} catch (...) {
|
||||
logger.Error(
|
||||
::XCEngine::Debug::LogCategory::FileSystem,
|
||||
"Scene asset reference migration aborted: unknown error");
|
||||
}
|
||||
|
||||
if (restoreResourceRoot) {
|
||||
resourceManager.SetResourceRoot(previousRoot.c_str());
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
::XCEngine::Debug::LogCategory::FileSystem,
|
||||
("Scene asset reference migration finished. scanned=" + std::to_string(report.scannedSceneCount) +
|
||||
" migrated=" + std::to_string(report.migratedSceneCount) +
|
||||
" unchanged=" + std::to_string(report.unchangedSceneCount) +
|
||||
" failed=" + std::to_string(report.failedSceneCount)).c_str());
|
||||
|
||||
RefreshCurrentFolder();
|
||||
return report;
|
||||
}
|
||||
|
||||
AssetItemPtr ProjectManager::FindCurrentItemByPath(const std::string& fullPath) const {
|
||||
const int index = FindCurrentItemIndex(fullPath);
|
||||
if (index < 0) {
|
||||
|
||||
@@ -38,6 +38,7 @@ public:
|
||||
bool DeleteItem(const std::string& fullPath) override;
|
||||
bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) override;
|
||||
bool RenameItem(const std::string& sourceFullPath, const std::string& newName) override;
|
||||
SceneAssetReferenceMigrationReport MigrateSceneAssetReferences() override;
|
||||
|
||||
const std::string& GetProjectPath() const override { return m_projectPath; }
|
||||
|
||||
|
||||
372
editor/src/Scripting/EditorScriptAssemblyBuilder.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
#include "Scripting/EditorScriptAssemblyBuilder.h"
|
||||
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "Scripting/EditorScriptAssemblyBuilderUtils.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef XCENGINE_EDITOR_REPO_ROOT
|
||||
#define XCENGINE_EDITOR_REPO_ROOT ""
|
||||
#endif
|
||||
|
||||
#ifndef XCENGINE_EDITOR_MONO_ROOT_DIR
|
||||
#define XCENGINE_EDITOR_MONO_ROOT_DIR ""
|
||||
#endif
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Scripting {
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path GetFallbackRepositoryRoot() {
|
||||
std::filesystem::path repoRoot = std::filesystem::path(Platform::Utf8ToWide(Platform::GetExecutableDirectoryUtf8()));
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (repoRoot.has_parent_path()) {
|
||||
repoRoot = repoRoot.parent_path();
|
||||
}
|
||||
}
|
||||
return repoRoot.lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path GetRepositoryRoot() {
|
||||
const std::string configuredRoot = XCENGINE_EDITOR_REPO_ROOT;
|
||||
if (!configuredRoot.empty()) {
|
||||
return std::filesystem::path(Platform::Utf8ToWide(configuredRoot)).lexically_normal();
|
||||
}
|
||||
|
||||
return GetFallbackRepositoryRoot();
|
||||
}
|
||||
|
||||
std::filesystem::path FindBundledMonoRootDirectory(const std::filesystem::path& repositoryRoot) {
|
||||
std::error_code ec;
|
||||
if (repositoryRoot.empty() || !std::filesystem::exists(repositoryRoot, ec)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
for (std::filesystem::directory_iterator it(repositoryRoot, ec), end; it != end && !ec; it.increment(ec)) {
|
||||
if (ec || !it->is_directory(ec)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::filesystem::path candidate =
|
||||
it->path() / "Fermion" / "Fermion" / "external" / "mono";
|
||||
if (std::filesystem::exists(candidate / "binary" / "mscorlib.dll", ec)) {
|
||||
return candidate.lexically_normal();
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path GetMonoRootDirectory() {
|
||||
const std::filesystem::path repositoryRoot = GetRepositoryRoot();
|
||||
const std::filesystem::path bundledMonoRoot = FindBundledMonoRootDirectory(repositoryRoot);
|
||||
if (!bundledMonoRoot.empty()) {
|
||||
return bundledMonoRoot;
|
||||
}
|
||||
|
||||
const std::string configuredRoot = XCENGINE_EDITOR_MONO_ROOT_DIR;
|
||||
if (!configuredRoot.empty()) {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path configuredPath =
|
||||
std::filesystem::path(Platform::Utf8ToWide(configuredRoot)).lexically_normal();
|
||||
if (std::filesystem::exists(configuredPath / "binary" / "mscorlib.dll", ec)) {
|
||||
return configuredPath;
|
||||
}
|
||||
}
|
||||
|
||||
return (repositoryRoot / "managed" / "mono").lexically_normal();
|
||||
}
|
||||
|
||||
std::wstring QuotePath(const std::filesystem::path& path) {
|
||||
return L"\"" + path.wstring() + L"\"";
|
||||
}
|
||||
|
||||
bool FindExecutableOnPath(const wchar_t* executableName, std::filesystem::path& outPath) {
|
||||
DWORD requiredLength = SearchPathW(nullptr, executableName, nullptr, 0, nullptr, nullptr);
|
||||
if (requiredLength == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring buffer(requiredLength, L'\0');
|
||||
const DWORD resolvedLength = SearchPathW(
|
||||
nullptr,
|
||||
executableName,
|
||||
nullptr,
|
||||
static_cast<DWORD>(buffer.size()),
|
||||
buffer.data(),
|
||||
nullptr);
|
||||
if (resolvedLength == 0 || resolvedLength >= buffer.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer.resize(resolvedLength);
|
||||
outPath = std::filesystem::path(buffer).lexically_normal();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunProcessAndCapture(
|
||||
const std::filesystem::path& executablePath,
|
||||
const std::wstring& arguments,
|
||||
const std::filesystem::path& workingDirectory,
|
||||
DWORD& outExitCode,
|
||||
std::string& outOutput) {
|
||||
SECURITY_ATTRIBUTES securityAttributes = {};
|
||||
securityAttributes.nLength = sizeof(securityAttributes);
|
||||
securityAttributes.bInheritHandle = TRUE;
|
||||
|
||||
HANDLE readPipe = nullptr;
|
||||
HANDLE writePipe = nullptr;
|
||||
if (!CreatePipe(&readPipe, &writePipe, &securityAttributes, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0);
|
||||
|
||||
STARTUPINFOW startupInfo = {};
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
startupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||
startupInfo.hStdOutput = writePipe;
|
||||
startupInfo.hStdError = writePipe;
|
||||
|
||||
PROCESS_INFORMATION processInfo = {};
|
||||
std::wstring commandLine = QuotePath(executablePath) + L" " + arguments;
|
||||
std::vector<wchar_t> commandLineBuffer(commandLine.begin(), commandLine.end());
|
||||
commandLineBuffer.push_back(L'\0');
|
||||
|
||||
const wchar_t* currentDirectory = workingDirectory.empty() ? nullptr : workingDirectory.c_str();
|
||||
const BOOL created = CreateProcessW(
|
||||
nullptr,
|
||||
commandLineBuffer.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
TRUE,
|
||||
CREATE_NO_WINDOW,
|
||||
nullptr,
|
||||
currentDirectory,
|
||||
&startupInfo,
|
||||
&processInfo);
|
||||
|
||||
CloseHandle(writePipe);
|
||||
writePipe = nullptr;
|
||||
|
||||
if (!created) {
|
||||
CloseHandle(readPipe);
|
||||
return false;
|
||||
}
|
||||
|
||||
char buffer[4096] = {};
|
||||
DWORD bytesRead = 0;
|
||||
while (ReadFile(readPipe, buffer, sizeof(buffer), &bytesRead, nullptr) && bytesRead > 0) {
|
||||
outOutput.append(buffer, bytesRead);
|
||||
}
|
||||
|
||||
WaitForSingleObject(processInfo.hProcess, INFINITE);
|
||||
GetExitCodeProcess(processInfo.hProcess, &outExitCode);
|
||||
|
||||
CloseHandle(processInfo.hThread);
|
||||
CloseHandle(processInfo.hProcess);
|
||||
CloseHandle(readPipe);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::wstring BuildCompilerArguments(
|
||||
const std::filesystem::path& outputPath,
|
||||
const std::vector<std::filesystem::path>& referencePaths,
|
||||
const std::vector<std::filesystem::path>& sourcePaths) {
|
||||
std::wstring arguments = L"/nologo /target:library /langversion:latest /nostdlib+ ";
|
||||
arguments += L"/out:" + QuotePath(outputPath);
|
||||
|
||||
for (const std::filesystem::path& referencePath : referencePaths) {
|
||||
arguments += L" /reference:" + QuotePath(referencePath);
|
||||
}
|
||||
|
||||
for (const std::filesystem::path& sourcePath : sourcePaths) {
|
||||
arguments += L" " + QuotePath(sourcePath);
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
EditorScriptAssemblyBuildResult BuildFailure(const std::string& message) {
|
||||
return EditorScriptAssemblyBuildResult{false, message};
|
||||
}
|
||||
|
||||
bool RunCSharpCompiler(
|
||||
const std::filesystem::path& dotnetExecutable,
|
||||
const std::filesystem::path& cscDllPath,
|
||||
const std::filesystem::path& workingDirectory,
|
||||
const std::filesystem::path& outputPath,
|
||||
const std::vector<std::filesystem::path>& referencePaths,
|
||||
const std::vector<std::filesystem::path>& sourcePaths,
|
||||
std::string& outError) {
|
||||
std::wstring arguments = QuotePath(cscDllPath);
|
||||
arguments += L" ";
|
||||
arguments += BuildCompilerArguments(outputPath, referencePaths, sourcePaths);
|
||||
|
||||
DWORD exitCode = 0;
|
||||
std::string processOutput;
|
||||
if (!RunProcessAndCapture(dotnetExecutable, arguments, workingDirectory, exitCode, processOutput)) {
|
||||
outError = "Failed to launch dotnet to compile managed script assemblies.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exitCode != 0) {
|
||||
outError = processOutput.empty()
|
||||
? "The C# compiler failed to build managed script assemblies."
|
||||
: processOutput;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorScriptAssemblyBuildResult EditorScriptAssemblyBuilder::RebuildProjectAssemblies(const std::string& projectPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
try {
|
||||
if (projectPath.empty()) {
|
||||
return BuildFailure("Cannot rebuild script assemblies without a loaded project.");
|
||||
}
|
||||
|
||||
const fs::path projectRoot = fs::path(Platform::Utf8ToWide(projectPath)).lexically_normal();
|
||||
const fs::path repositoryRoot = GetRepositoryRoot();
|
||||
const fs::path monoRoot = GetMonoRootDirectory();
|
||||
const fs::path managedRoot = repositoryRoot / "managed";
|
||||
const fs::path scriptCoreSourceRoot = managedRoot / "XCEngine.ScriptCore";
|
||||
const fs::path outputDirectory = projectRoot / "Library" / "ScriptAssemblies";
|
||||
const fs::path generatedDirectory = outputDirectory / "Generated";
|
||||
const fs::path scriptCoreOutputPath = outputDirectory / "XCEngine.ScriptCore.dll";
|
||||
const fs::path gameScriptsOutputPath = outputDirectory / "GameScripts.dll";
|
||||
const fs::path corlibOutputPath = outputDirectory / "mscorlib.dll";
|
||||
const fs::path monoCorlibSourcePath = monoRoot / "binary" / "mscorlib.dll";
|
||||
const fs::path frameworkReferenceDirectory =
|
||||
L"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.7.2";
|
||||
|
||||
std::error_code ec;
|
||||
fs::create_directories(outputDirectory, ec);
|
||||
if (ec) {
|
||||
return BuildFailure("Failed to create the project script assembly directory: " +
|
||||
ScriptBuilderPathToUtf8(outputDirectory));
|
||||
}
|
||||
|
||||
fs::path dotnetExecutable;
|
||||
if (!FindExecutableOnPath(L"dotnet.exe", dotnetExecutable)) {
|
||||
return BuildFailure("dotnet.exe was not found on PATH.");
|
||||
}
|
||||
|
||||
std::string sdkListOutput;
|
||||
DWORD sdkListExitCode = 0;
|
||||
if (!RunProcessAndCapture(dotnetExecutable, L"--list-sdks", projectRoot, sdkListExitCode, sdkListOutput) ||
|
||||
sdkListExitCode != 0) {
|
||||
return BuildFailure("Failed to query installed .NET SDKs with dotnet --list-sdks.");
|
||||
}
|
||||
|
||||
const std::string sdkVersion = ParseLatestDotnetSdkVersion(sdkListOutput);
|
||||
if (sdkVersion.empty()) {
|
||||
return BuildFailure("Failed to resolve a usable .NET SDK version from dotnet --list-sdks.");
|
||||
}
|
||||
|
||||
const fs::path cscDllPath =
|
||||
fs::path(L"C:\\Program Files\\dotnet\\sdk") /
|
||||
fs::path(Platform::Utf8ToWide(sdkVersion)) /
|
||||
"Roslyn" /
|
||||
"bincore" /
|
||||
"csc.dll";
|
||||
if (!fs::exists(cscDllPath, ec)) {
|
||||
return BuildFailure("Roslyn csc.dll was not found: " + ScriptBuilderPathToUtf8(cscDllPath));
|
||||
}
|
||||
|
||||
const std::vector<fs::path> frameworkReferences = {
|
||||
frameworkReferenceDirectory / "mscorlib.dll",
|
||||
frameworkReferenceDirectory / "System.dll",
|
||||
frameworkReferenceDirectory / "System.Core.dll"
|
||||
};
|
||||
for (const fs::path& referencePath : frameworkReferences) {
|
||||
if (!fs::exists(referencePath, ec)) {
|
||||
return BuildFailure("Required .NET Framework reference is missing: " +
|
||||
ScriptBuilderPathToUtf8(referencePath));
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs::exists(monoCorlibSourcePath, ec)) {
|
||||
return BuildFailure("Mono corlib was not found: " + ScriptBuilderPathToUtf8(monoCorlibSourcePath));
|
||||
}
|
||||
|
||||
std::vector<fs::path> scriptCoreSources = CollectCSharpSourceFiles(scriptCoreSourceRoot);
|
||||
if (scriptCoreSources.empty()) {
|
||||
return BuildFailure("No ScriptCore C# source files were found under: " +
|
||||
ScriptBuilderPathToUtf8(scriptCoreSourceRoot));
|
||||
}
|
||||
|
||||
std::vector<fs::path> projectScriptSources = CollectCSharpSourceFiles(projectRoot / "Assets");
|
||||
std::string placeholderError;
|
||||
if (!EnsurePlaceholderProjectScriptSource(
|
||||
projectScriptSources,
|
||||
generatedDirectory / "EmptyProjectGameScripts.cs",
|
||||
placeholderError)) {
|
||||
return BuildFailure(placeholderError);
|
||||
}
|
||||
|
||||
std::string compileError;
|
||||
if (!RunCSharpCompiler(
|
||||
dotnetExecutable,
|
||||
cscDllPath,
|
||||
projectRoot,
|
||||
scriptCoreOutputPath,
|
||||
frameworkReferences,
|
||||
scriptCoreSources,
|
||||
compileError)) {
|
||||
return BuildFailure("Failed to build XCEngine.ScriptCore.dll: " + compileError);
|
||||
}
|
||||
|
||||
// Mono can keep the project-local corlib mapped for the lifetime of the process.
|
||||
// Once it exists in the output folder, reuse it across incremental rebuilds.
|
||||
ec.clear();
|
||||
const bool hasProjectCorlib = fs::exists(corlibOutputPath, ec);
|
||||
if (ec) {
|
||||
return BuildFailure("Failed to inspect the project script assembly corlib path.");
|
||||
}
|
||||
|
||||
if (!hasProjectCorlib) {
|
||||
fs::copy_file(monoCorlibSourcePath, corlibOutputPath, fs::copy_options::overwrite_existing, ec);
|
||||
if (ec) {
|
||||
return BuildFailure("Failed to copy mscorlib.dll into the project script assembly directory.");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<fs::path> gameScriptReferences = frameworkReferences;
|
||||
gameScriptReferences.push_back(scriptCoreOutputPath);
|
||||
if (!RunCSharpCompiler(
|
||||
dotnetExecutable,
|
||||
cscDllPath,
|
||||
projectRoot,
|
||||
gameScriptsOutputPath,
|
||||
gameScriptReferences,
|
||||
projectScriptSources,
|
||||
compileError)) {
|
||||
return BuildFailure("Failed to build GameScripts.dll: " + compileError);
|
||||
}
|
||||
|
||||
return EditorScriptAssemblyBuildResult{
|
||||
true,
|
||||
"Rebuilt script assemblies in " + ScriptBuilderPathToUtf8(outputDirectory)
|
||||
};
|
||||
} catch (const std::exception& exception) {
|
||||
return BuildFailure("Script assembly rebuild threw an exception: " + std::string(exception.what()));
|
||||
} catch (...) {
|
||||
return BuildFailure("Script assembly rebuild threw an unknown exception.");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Scripting
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
21
editor/src/Scripting/EditorScriptAssemblyBuilder.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Scripting {
|
||||
|
||||
struct EditorScriptAssemblyBuildResult {
|
||||
bool succeeded = false;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
class EditorScriptAssemblyBuilder {
|
||||
public:
|
||||
static EditorScriptAssemblyBuildResult RebuildProjectAssemblies(const std::string& projectPath);
|
||||
};
|
||||
|
||||
} // namespace Scripting
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
106
editor/src/Scripting/EditorScriptAssemblyBuilderUtils.h
Normal file
@@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
|
||||
#include "Platform/Win32Utf8.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Scripting {
|
||||
|
||||
inline std::string ScriptBuilderPathToUtf8(const std::filesystem::path& path) {
|
||||
return Platform::WideToUtf8(path.wstring());
|
||||
}
|
||||
|
||||
inline std::vector<std::filesystem::path> CollectCSharpSourceFiles(const std::filesystem::path& root) {
|
||||
std::vector<std::filesystem::path> sourceFiles;
|
||||
std::error_code ec;
|
||||
if (root.empty() || !std::filesystem::exists(root, ec)) {
|
||||
return sourceFiles;
|
||||
}
|
||||
|
||||
for (std::filesystem::recursive_directory_iterator it(root, ec), end; it != end && !ec; it.increment(ec)) {
|
||||
if (ec || !it->is_regular_file(ec)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::filesystem::path path = it->path();
|
||||
if (path.extension() == ".cs") {
|
||||
sourceFiles.push_back(path.lexically_normal());
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(sourceFiles.begin(), sourceFiles.end());
|
||||
return sourceFiles;
|
||||
}
|
||||
|
||||
inline std::string ParseLatestDotnetSdkVersion(const std::string& sdkListOutput) {
|
||||
std::string latestVersion;
|
||||
size_t lineStart = 0;
|
||||
while (lineStart < sdkListOutput.size()) {
|
||||
const size_t lineEnd = sdkListOutput.find_first_of("\r\n", lineStart);
|
||||
const std::string line = sdkListOutput.substr(
|
||||
lineStart,
|
||||
lineEnd == std::string::npos ? std::string::npos : lineEnd - lineStart);
|
||||
const size_t delimiter = line.find(" [");
|
||||
if (delimiter != std::string::npos) {
|
||||
latestVersion = line.substr(0, delimiter);
|
||||
}
|
||||
|
||||
if (lineEnd == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
|
||||
lineStart = lineEnd + 1;
|
||||
if (lineStart < sdkListOutput.size() &&
|
||||
sdkListOutput[lineEnd] == '\r' &&
|
||||
sdkListOutput[lineStart] == '\n') {
|
||||
++lineStart;
|
||||
}
|
||||
}
|
||||
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
inline bool EnsurePlaceholderProjectScriptSource(
|
||||
std::vector<std::filesystem::path>& ioSourceFiles,
|
||||
const std::filesystem::path& placeholderPath,
|
||||
std::string& outError) {
|
||||
if (!ioSourceFiles.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(placeholderPath.parent_path(), ec);
|
||||
if (ec) {
|
||||
outError = "Failed to create the placeholder script directory: " +
|
||||
ScriptBuilderPathToUtf8(placeholderPath.parent_path());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream output(placeholderPath, std::ios::out | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
outError = "Failed to create the placeholder project script source: " +
|
||||
ScriptBuilderPathToUtf8(placeholderPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
output << "namespace XCEngine.Generated { public static class EmptyProjectGameScriptsMarker {} }\n";
|
||||
output.close();
|
||||
if (!output.good()) {
|
||||
outError = "Failed to write the placeholder project script source: " +
|
||||
ScriptBuilderPathToUtf8(placeholderPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
ioSourceFiles.push_back(placeholderPath.lexically_normal());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Scripting
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
17
editor/src/Scripting/EditorScriptRuntimeStatus.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
struct EditorScriptRuntimeStatus {
|
||||
bool backendEnabled = false;
|
||||
bool assembliesFound = false;
|
||||
bool runtimeLoaded = false;
|
||||
std::string assemblyDirectory;
|
||||
std::string statusMessage;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -47,6 +47,20 @@ struct AssetTileOptions {
|
||||
bool drawLabel = true;
|
||||
};
|
||||
|
||||
inline ImVec2 ComputeAssetTileSize(
|
||||
const char* label,
|
||||
const AssetTileOptions& options = AssetTileOptions()) {
|
||||
const ImVec2 textSize = ImGui::CalcTextSize(label ? label : "");
|
||||
ImVec2 tileSize = options.size;
|
||||
tileSize.x = std::max(tileSize.x, options.iconSize.x + AssetTileTextPadding().x * 2.0f);
|
||||
tileSize.y = std::max(
|
||||
tileSize.y,
|
||||
options.iconOffset.y +
|
||||
options.iconSize.y +
|
||||
(options.drawLabel ? AssetTileIconTextGap() + textSize.y + AssetTileTextPadding().y : 0.0f));
|
||||
return tileSize;
|
||||
}
|
||||
|
||||
enum class DialogActionResult {
|
||||
None,
|
||||
Primary,
|
||||
@@ -428,16 +442,8 @@ inline AssetTileResult DrawAssetTile(
|
||||
bool dimmed,
|
||||
DrawIconFn&& drawIcon,
|
||||
const AssetTileOptions& options = AssetTileOptions()) {
|
||||
const ImVec2 textSize = ImGui::CalcTextSize(label);
|
||||
ImVec2 tileSize = options.size;
|
||||
tileSize.x = std::max(tileSize.x, options.iconSize.x + AssetTileTextPadding().x * 2.0f);
|
||||
tileSize.y = std::max(
|
||||
tileSize.y,
|
||||
options.iconOffset.y +
|
||||
options.iconSize.y +
|
||||
AssetTileIconTextGap() +
|
||||
textSize.y +
|
||||
AssetTileTextPadding().y);
|
||||
const ImVec2 textSize = ImGui::CalcTextSize(label ? label : "");
|
||||
const ImVec2 tileSize = ComputeAssetTileSize(label, options);
|
||||
ImGui::InvisibleButton("##AssetBtn", tileSize);
|
||||
|
||||
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||
@@ -470,13 +476,13 @@ inline AssetTileResult DrawAssetTile(
|
||||
drawIcon(drawList, iconMin, iconMax);
|
||||
|
||||
const ImVec2 textPadding = AssetTileTextPadding();
|
||||
const float labelHeight = ImGui::GetFrameHeight();
|
||||
const float labelHeight = (std::max)(ImGui::GetFrameHeight(), textSize.y);
|
||||
const ImVec2 labelMin(min.x + textPadding.x, max.y - labelHeight - textPadding.y * 0.5f);
|
||||
const ImVec2 labelMax(max.x - textPadding.x, labelMin.y + labelHeight);
|
||||
if (options.drawLabel) {
|
||||
const float textAreaWidth = labelMax.x - labelMin.x;
|
||||
const float centeredTextX = labelMin.x + std::max(0.0f, (textAreaWidth - textSize.x) * 0.5f);
|
||||
const float textY = labelMin.y + (labelHeight - textSize.y) * 0.5f;
|
||||
const float textY = labelMin.y + (std::max)(0.0f, (labelHeight - textSize.y) * 0.5f);
|
||||
ImGui::PushClipRect(labelMin, labelMax, true);
|
||||
drawList->AddText(ImVec2(centeredTextX, textY), ImGui::GetColorU32(AssetTileTextColor(selected)), label);
|
||||
ImGui::PopClipRect();
|
||||
|
||||
@@ -15,6 +15,8 @@ struct RenderContext;
|
||||
namespace Editor {
|
||||
|
||||
class IEditorContext;
|
||||
struct SceneViewportOverlayFrameData;
|
||||
struct SceneViewportTransformGizmoHandleBuildInputs;
|
||||
|
||||
enum class EditorViewportKind {
|
||||
Scene,
|
||||
@@ -83,6 +85,14 @@ public:
|
||||
const ImVec2& viewportMousePosition) = 0;
|
||||
virtual void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis axis) = 0;
|
||||
virtual SceneViewportOverlayData GetSceneViewOverlayData() const = 0;
|
||||
virtual const SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext& context) = 0;
|
||||
virtual const SceneViewportOverlayFrameData& GetSceneViewInteractionOverlayFrameData(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) = 0;
|
||||
virtual void SetSceneViewTransientTransformGizmoOverlayData(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) = 0;
|
||||
virtual void RenderRequestedViewports(
|
||||
IEditorContext& context,
|
||||
const Rendering::RenderContext& renderContext) = 0;
|
||||
|
||||
1255
editor/src/Viewport/Passes/SceneViewportEditorOverlayPass.cpp
Normal file
89
editor/src/Viewport/Passes/SceneViewportEditorOverlayPass.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include "Viewport/SceneViewportEditorOverlayData.h"
|
||||
|
||||
#include <XCEngine/Rendering/RenderContext.h>
|
||||
#include <XCEngine/Rendering/RenderPass.h>
|
||||
#include <XCEngine/Rendering/RenderSurface.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace RHI {
|
||||
class RHIBuffer;
|
||||
class RHIDescriptorPool;
|
||||
class RHIDescriptorSet;
|
||||
class RHIDevice;
|
||||
class RHIPipelineLayout;
|
||||
class RHIPipelineState;
|
||||
class RHIResourceView;
|
||||
class RHISampler;
|
||||
class RHITexture;
|
||||
enum class RHIType : uint8_t;
|
||||
} // namespace RHI
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class SceneViewportEditorOverlayPassRenderer {
|
||||
public:
|
||||
~SceneViewportEditorOverlayPassRenderer() = default;
|
||||
|
||||
void Shutdown();
|
||||
|
||||
bool Render(
|
||||
const Rendering::RenderContext& renderContext,
|
||||
const Rendering::RenderSurface& surface,
|
||||
const SceneViewportOverlayFrameData& frameData);
|
||||
|
||||
private:
|
||||
struct OverlaySpriteTextureResources {
|
||||
RHI::RHITexture* texture = nullptr;
|
||||
RHI::RHIResourceView* shaderView = nullptr;
|
||||
RHI::RHIDescriptorSet* textureSet = nullptr;
|
||||
};
|
||||
|
||||
bool EnsureInitialized(const Rendering::RenderContext& renderContext);
|
||||
bool CreateResources(const Rendering::RenderContext& renderContext);
|
||||
bool EnsureLineBufferCapacity(size_t requiredVertexCount);
|
||||
bool EnsureScreenTriangleBufferCapacity(size_t requiredVertexCount);
|
||||
bool EnsureSpriteBufferCapacity(size_t requiredVertexCount);
|
||||
bool EnsureIconTexturesLoaded();
|
||||
void DestroyResources();
|
||||
|
||||
RHI::RHIDevice* m_device = nullptr;
|
||||
RHI::RHIType m_backendType = RHI::RHIType::D3D12;
|
||||
RHI::RHIPipelineLayout* m_linePipelineLayout = nullptr;
|
||||
RHI::RHIPipelineLayout* m_spritePipelineLayout = nullptr;
|
||||
RHI::RHIPipelineState* m_depthTestedLinePipelineState = nullptr;
|
||||
RHI::RHIPipelineState* m_alwaysOnTopLinePipelineState = nullptr;
|
||||
RHI::RHIPipelineState* m_depthTestedScreenTrianglePipelineState = nullptr;
|
||||
RHI::RHIPipelineState* m_alwaysOnTopScreenTrianglePipelineState = nullptr;
|
||||
RHI::RHIPipelineState* m_depthTestedSpritePipelineState = nullptr;
|
||||
RHI::RHIPipelineState* m_alwaysOnTopSpritePipelineState = nullptr;
|
||||
RHI::RHIDescriptorPool* m_constantPool = nullptr;
|
||||
RHI::RHIDescriptorPool* m_texturePool = nullptr;
|
||||
RHI::RHIDescriptorPool* m_samplerPool = nullptr;
|
||||
RHI::RHIDescriptorSet* m_constantSet = nullptr;
|
||||
RHI::RHIDescriptorSet* m_samplerSet = nullptr;
|
||||
RHI::RHISampler* m_sampler = nullptr;
|
||||
RHI::RHIBuffer* m_lineVertexBuffer = nullptr;
|
||||
RHI::RHIResourceView* m_lineVertexBufferView = nullptr;
|
||||
RHI::RHIBuffer* m_screenTriangleVertexBuffer = nullptr;
|
||||
RHI::RHIResourceView* m_screenTriangleVertexBufferView = nullptr;
|
||||
RHI::RHIBuffer* m_spriteVertexBuffer = nullptr;
|
||||
RHI::RHIResourceView* m_spriteVertexBufferView = nullptr;
|
||||
uint64_t m_lineVertexBufferCapacity = 0;
|
||||
uint64_t m_screenTriangleVertexBufferCapacity = 0;
|
||||
uint64_t m_spriteVertexBufferCapacity = 0;
|
||||
std::array<OverlaySpriteTextureResources, 2> m_overlaySpriteTextures = {};
|
||||
};
|
||||
|
||||
std::unique_ptr<Rendering::RenderPass> CreateSceneViewportEditorOverlayPass(
|
||||
SceneViewportEditorOverlayPassRenderer& renderer,
|
||||
const SceneViewportOverlayFrameData& frameData);
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -128,13 +128,21 @@ public:
|
||||
|
||||
if (std::abs(input.lookDeltaX) > Math::EPSILON ||
|
||||
std::abs(input.lookDeltaY) > Math::EPSILON) {
|
||||
ApplyRotationDelta(input.lookDeltaX, input.lookDeltaY);
|
||||
ApplyRotationDelta(
|
||||
input.lookDeltaX,
|
||||
input.lookDeltaY,
|
||||
kLookYawSensitivity,
|
||||
kLookPitchSensitivity);
|
||||
UpdateFocalPointFromPosition();
|
||||
}
|
||||
|
||||
if (std::abs(input.orbitDeltaX) > Math::EPSILON ||
|
||||
std::abs(input.orbitDeltaY) > Math::EPSILON) {
|
||||
ApplyRotationDelta(input.orbitDeltaX, input.orbitDeltaY);
|
||||
ApplyRotationDelta(
|
||||
input.orbitDeltaX,
|
||||
input.orbitDeltaY,
|
||||
kOrbitYawSensitivity,
|
||||
kOrbitPitchSensitivity);
|
||||
UpdatePositionFromFocalPoint();
|
||||
}
|
||||
|
||||
@@ -200,6 +208,10 @@ public:
|
||||
|
||||
private:
|
||||
static constexpr float kSnapDurationSeconds = 0.22f;
|
||||
static constexpr float kLookYawSensitivity = 0.18f;
|
||||
static constexpr float kLookPitchSensitivity = 0.12f;
|
||||
static constexpr float kOrbitYawSensitivity = 0.30f;
|
||||
static constexpr float kOrbitPitchSensitivity = 0.20f;
|
||||
|
||||
struct OrientationAngles {
|
||||
float yawDegrees = 0.0f;
|
||||
@@ -239,9 +251,13 @@ private:
|
||||
return result;
|
||||
}
|
||||
|
||||
void ApplyRotationDelta(float deltaX, float deltaY) {
|
||||
m_yawDegrees += deltaX * 0.30f;
|
||||
m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * 0.20f, -89.0f, 89.0f);
|
||||
void ApplyRotationDelta(
|
||||
float deltaX,
|
||||
float deltaY,
|
||||
float yawSensitivity,
|
||||
float pitchSensitivity) {
|
||||
m_yawDegrees += deltaX * yawSensitivity;
|
||||
m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * pitchSensitivity, -89.0f, 89.0f);
|
||||
}
|
||||
|
||||
Math::Vector3 GetRight() const {
|
||||
|
||||
144
editor/src/Viewport/SceneViewportEditorOverlayData.h
Normal file
@@ -0,0 +1,144 @@
|
||||
#pragma once
|
||||
|
||||
#include "IViewportHostService.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
#include <array>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
enum class SceneViewportOverlayDepthMode : uint8_t {
|
||||
DepthTested = 0,
|
||||
AlwaysOnTop
|
||||
};
|
||||
|
||||
enum class SceneViewportOverlaySpriteTextureKind : uint8_t {
|
||||
Camera = 0,
|
||||
Light = 1
|
||||
};
|
||||
|
||||
enum class SceneViewportOverlayHandleKind : uint8_t {
|
||||
None = 0,
|
||||
SceneIcon,
|
||||
MoveAxis,
|
||||
MovePlane,
|
||||
RotateAxis,
|
||||
ScaleAxis,
|
||||
ScaleUniform
|
||||
};
|
||||
|
||||
enum class SceneViewportOverlayHandleShape : uint8_t {
|
||||
None = 0,
|
||||
WorldRect,
|
||||
ScreenSegment,
|
||||
ScreenRect,
|
||||
ScreenQuad
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayLinePrimitive {
|
||||
Math::Vector3 startWorld = Math::Vector3::Zero();
|
||||
Math::Vector3 endWorld = Math::Vector3::Zero();
|
||||
Math::Color color = Math::Color::White();
|
||||
float thicknessPixels = 1.0f;
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop;
|
||||
};
|
||||
|
||||
struct SceneViewportOverlaySpritePrimitive {
|
||||
Math::Vector3 worldPosition = Math::Vector3::Zero();
|
||||
Math::Vector2 sizePixels = Math::Vector2::Zero();
|
||||
Math::Color tintColor = Math::Color::White();
|
||||
float sortDepth = 0.0f;
|
||||
uint64_t entityId = 0;
|
||||
SceneViewportOverlaySpriteTextureKind textureKind = SceneViewportOverlaySpriteTextureKind::Camera;
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop;
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayScreenTriangleVertex {
|
||||
Math::Vector2 screenPosition = Math::Vector2::Zero();
|
||||
Math::Color color = Math::Color::White();
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayScreenTrianglePrimitive {
|
||||
std::array<SceneViewportOverlayScreenTriangleVertex, 3> vertices = {};
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop;
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayHandleRecord {
|
||||
SceneViewportOverlayHandleKind kind = SceneViewportOverlayHandleKind::None;
|
||||
uint64_t handleId = 0;
|
||||
uint64_t entityId = 0;
|
||||
SceneViewportOverlayHandleShape shape = SceneViewportOverlayHandleShape::None;
|
||||
int priority = 0;
|
||||
Math::Vector3 worldPosition = Math::Vector3::Zero();
|
||||
Math::Vector2 sizePixels = Math::Vector2::Zero();
|
||||
float sortDepth = 0.0f;
|
||||
Math::Vector2 screenStart = Math::Vector2::Zero();
|
||||
Math::Vector2 screenEnd = Math::Vector2::Zero();
|
||||
Math::Vector2 screenCenter = Math::Vector2::Zero();
|
||||
Math::Vector2 screenHalfSize = Math::Vector2::Zero();
|
||||
std::array<Math::Vector2, 4> screenQuad = {};
|
||||
float hitThicknessPixels = 0.0f;
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayHandleHitResult {
|
||||
SceneViewportOverlayHandleKind kind = SceneViewportOverlayHandleKind::None;
|
||||
uint64_t handleId = 0;
|
||||
uint64_t entityId = 0;
|
||||
int priority = (std::numeric_limits<int>::min)();
|
||||
float distanceSq = (std::numeric_limits<float>::max)();
|
||||
float depth = (std::numeric_limits<float>::max)();
|
||||
|
||||
bool HasHit() const {
|
||||
return kind != SceneViewportOverlayHandleKind::None;
|
||||
}
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayFrameData {
|
||||
SceneViewportOverlayData overlay = {};
|
||||
std::vector<SceneViewportOverlayLinePrimitive> worldLines = {};
|
||||
std::vector<SceneViewportOverlaySpritePrimitive> worldSprites = {};
|
||||
std::vector<SceneViewportOverlayScreenTrianglePrimitive> screenTriangles = {};
|
||||
std::vector<SceneViewportOverlayHandleRecord> handleRecords = {};
|
||||
|
||||
bool HasOverlayPrimitives() const {
|
||||
return overlay.valid && (!worldLines.empty() || !worldSprites.empty() || !screenTriangles.empty());
|
||||
}
|
||||
|
||||
bool HasWorldOverlay() const {
|
||||
return HasOverlayPrimitives();
|
||||
}
|
||||
};
|
||||
|
||||
inline void AppendSceneViewportOverlayFrameData(
|
||||
SceneViewportOverlayFrameData& target,
|
||||
const SceneViewportOverlayFrameData& source) {
|
||||
if (!target.overlay.valid && source.overlay.valid) {
|
||||
target.overlay = source.overlay;
|
||||
}
|
||||
|
||||
target.worldLines.insert(
|
||||
target.worldLines.end(),
|
||||
source.worldLines.begin(),
|
||||
source.worldLines.end());
|
||||
target.worldSprites.insert(
|
||||
target.worldSprites.end(),
|
||||
source.worldSprites.begin(),
|
||||
source.worldSprites.end());
|
||||
target.screenTriangles.insert(
|
||||
target.screenTriangles.end(),
|
||||
source.screenTriangles.begin(),
|
||||
source.screenTriangles.end());
|
||||
target.handleRecords.insert(
|
||||
target.handleRecords.end(),
|
||||
source.handleRecords.begin(),
|
||||
source.handleRecords.end());
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -14,7 +14,7 @@ namespace {
|
||||
constexpr float kMoveGizmoHandleLengthPixels = 100.0f;
|
||||
constexpr float kMoveGizmoHoverThresholdPixels = 10.0f;
|
||||
|
||||
Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) {
|
||||
Math::Vector3 GetBaseAxisVector(SceneViewportGizmoAxis axis) {
|
||||
switch (axis) {
|
||||
case SceneViewportGizmoAxis::X:
|
||||
return Math::Vector3::Right();
|
||||
@@ -28,6 +28,16 @@ Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) {
|
||||
}
|
||||
}
|
||||
|
||||
Math::Vector3 GetAxisVector(
|
||||
SceneViewportGizmoAxis axis,
|
||||
const Math::Quaternion& orientation) {
|
||||
const Math::Vector3 baseAxis = GetBaseAxisVector(axis);
|
||||
const Math::Vector3 orientedAxis = orientation * baseAxis;
|
||||
return orientedAxis.SqrMagnitude() <= Math::EPSILON
|
||||
? baseAxis
|
||||
: orientedAxis.Normalized();
|
||||
}
|
||||
|
||||
Math::Color GetAxisBaseColor(SceneViewportGizmoAxis axis) {
|
||||
switch (axis) {
|
||||
case SceneViewportGizmoAxis::X:
|
||||
@@ -61,20 +71,21 @@ SceneViewportGizmoPlane GetPlaneForIndex(size_t index) {
|
||||
|
||||
void GetPlaneAxes(
|
||||
SceneViewportGizmoPlane plane,
|
||||
const Math::Quaternion& orientation,
|
||||
Math::Vector3& outAxisA,
|
||||
Math::Vector3& outAxisB) {
|
||||
switch (plane) {
|
||||
case SceneViewportGizmoPlane::XY:
|
||||
outAxisA = Math::Vector3::Right();
|
||||
outAxisB = Math::Vector3::Up();
|
||||
outAxisA = GetAxisVector(SceneViewportGizmoAxis::X, orientation);
|
||||
outAxisB = GetAxisVector(SceneViewportGizmoAxis::Y, orientation);
|
||||
return;
|
||||
case SceneViewportGizmoPlane::XZ:
|
||||
outAxisA = Math::Vector3::Right();
|
||||
outAxisB = Math::Vector3::Forward();
|
||||
outAxisA = GetAxisVector(SceneViewportGizmoAxis::X, orientation);
|
||||
outAxisB = GetAxisVector(SceneViewportGizmoAxis::Z, orientation);
|
||||
return;
|
||||
case SceneViewportGizmoPlane::YZ:
|
||||
outAxisA = Math::Vector3::Up();
|
||||
outAxisB = Math::Vector3::Forward();
|
||||
outAxisA = GetAxisVector(SceneViewportGizmoAxis::Y, orientation);
|
||||
outAxisB = GetAxisVector(SceneViewportGizmoAxis::Z, orientation);
|
||||
return;
|
||||
case SceneViewportGizmoPlane::None:
|
||||
default:
|
||||
@@ -84,14 +95,16 @@ void GetPlaneAxes(
|
||||
}
|
||||
}
|
||||
|
||||
Math::Vector3 GetPlaneNormal(SceneViewportGizmoPlane plane) {
|
||||
Math::Vector3 GetPlaneNormal(
|
||||
SceneViewportGizmoPlane plane,
|
||||
const Math::Quaternion& orientation) {
|
||||
switch (plane) {
|
||||
case SceneViewportGizmoPlane::XY:
|
||||
return Math::Vector3::Forward();
|
||||
return GetAxisVector(SceneViewportGizmoAxis::Z, orientation);
|
||||
case SceneViewportGizmoPlane::XZ:
|
||||
return Math::Vector3::Up();
|
||||
return GetAxisVector(SceneViewportGizmoAxis::Y, orientation);
|
||||
case SceneViewportGizmoPlane::YZ:
|
||||
return Math::Vector3::Right();
|
||||
return GetAxisVector(SceneViewportGizmoAxis::X, orientation);
|
||||
case SceneViewportGizmoPlane::None:
|
||||
default:
|
||||
return Math::Vector3::Zero();
|
||||
@@ -185,10 +198,9 @@ float ComputeWorldUnitsPerPixel(
|
||||
void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) {
|
||||
BuildDrawData(context);
|
||||
if (m_dragMode == DragMode::None && IsMouseInsideViewport(context)) {
|
||||
m_hoveredAxis = HitTestAxis(context.mousePosition);
|
||||
m_hoveredPlane = m_hoveredAxis == SceneViewportGizmoAxis::None
|
||||
? HitTestPlane(context.mousePosition)
|
||||
: SceneViewportGizmoPlane::None;
|
||||
const SceneViewportMoveGizmoHitResult hitResult = EvaluateHit(context.mousePosition);
|
||||
m_hoveredAxis = hitResult.axis;
|
||||
m_hoveredPlane = hitResult.plane;
|
||||
} else if (m_dragMode == DragMode::None) {
|
||||
m_hoveredAxis = SceneViewportGizmoAxis::None;
|
||||
m_hoveredPlane = SceneViewportGizmoPlane::None;
|
||||
@@ -221,18 +233,17 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
|
||||
Math::Vector3 dragPlaneNormal = Math::Vector3::Zero();
|
||||
Math::Vector3 worldAxis = Math::Vector3::Zero();
|
||||
|
||||
if (m_hoveredAxis != SceneViewportGizmoAxis::None) {
|
||||
worldAxis = GetAxisVector(m_hoveredAxis);
|
||||
worldAxis = GetAxisVector(m_hoveredAxis, context.axisOrientation);
|
||||
if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
dragPlaneNormal = GetPlaneNormal(m_hoveredPlane);
|
||||
dragPlaneNormal = GetPlaneNormal(m_hoveredPlane, context.axisOrientation);
|
||||
if (dragPlaneNormal.SqrMagnitude() <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
@@ -257,10 +268,21 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
|
||||
m_activeAxisDirection = worldAxis;
|
||||
m_activePlaneNormal = dragPlaneNormal;
|
||||
m_dragPlane = dragPlane;
|
||||
m_dragStartObjectWorldPosition = objectWorldPosition;
|
||||
m_dragStartPivotWorldPosition = pivotWorldPosition;
|
||||
m_dragStartHitWorldPosition = hitPoint;
|
||||
m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - pivotWorldPosition, worldAxis);
|
||||
m_dragObjects = context.selectedObjects;
|
||||
if (m_dragObjects.empty()) {
|
||||
m_dragObjects.push_back(context.selectedObject);
|
||||
}
|
||||
m_dragStartObjectWorldPositions.clear();
|
||||
m_dragStartObjectWorldPositions.reserve(m_dragObjects.size());
|
||||
for (Components::GameObject* gameObject : m_dragObjects) {
|
||||
m_dragStartObjectWorldPositions.push_back(
|
||||
gameObject != nullptr && gameObject->GetTransform() != nullptr
|
||||
? gameObject->GetTransform()->GetPosition()
|
||||
: Math::Vector3::Zero());
|
||||
}
|
||||
RefreshHandleState();
|
||||
return true;
|
||||
}
|
||||
@@ -268,7 +290,9 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
|
||||
void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& context) {
|
||||
if (m_dragMode == DragMode::None ||
|
||||
context.selectedObject == nullptr ||
|
||||
context.selectedObject->GetID() != m_activeEntityId) {
|
||||
context.selectedObject->GetID() != m_activeEntityId ||
|
||||
m_dragObjects.empty() ||
|
||||
m_dragObjects.size() != m_dragStartObjectWorldPositions.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -290,17 +314,28 @@ void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& con
|
||||
if (m_dragMode == DragMode::Axis) {
|
||||
const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection);
|
||||
const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar;
|
||||
context.selectedObject->GetTransform()->SetPosition(
|
||||
m_dragStartObjectWorldPosition + m_activeAxisDirection * deltaScalar);
|
||||
const Math::Vector3 worldDelta = m_activeAxisDirection * deltaScalar;
|
||||
for (size_t index = 0; index < m_dragObjects.size(); ++index) {
|
||||
if (m_dragObjects[index] == nullptr || m_dragObjects[index]->GetTransform() == nullptr) {
|
||||
continue;
|
||||
}
|
||||
m_dragObjects[index]->GetTransform()->SetPosition(
|
||||
m_dragStartObjectWorldPositions[index] + worldDelta);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_dragMode == DragMode::Plane) {
|
||||
const Math::Vector3 planeDelta = Math::Vector3::ProjectOnPlane(
|
||||
const Math::Vector3 worldDelta = Math::Vector3::ProjectOnPlane(
|
||||
hitPoint - m_dragStartHitWorldPosition,
|
||||
m_activePlaneNormal);
|
||||
context.selectedObject->GetTransform()->SetPosition(
|
||||
m_dragStartObjectWorldPosition + planeDelta);
|
||||
for (size_t index = 0; index < m_dragObjects.size(); ++index) {
|
||||
if (m_dragObjects[index] == nullptr || m_dragObjects[index]->GetTransform() == nullptr) {
|
||||
continue;
|
||||
}
|
||||
m_dragObjects[index]->GetTransform()->SetPosition(
|
||||
m_dragStartObjectWorldPositions[index] + worldDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,10 +354,11 @@ void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) {
|
||||
m_activeEntityId = 0;
|
||||
m_activeAxisDirection = Math::Vector3::Zero();
|
||||
m_activePlaneNormal = Math::Vector3::Zero();
|
||||
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartHitWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartAxisScalar = 0.0f;
|
||||
m_dragObjects.clear();
|
||||
m_dragStartObjectWorldPositions.clear();
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
@@ -337,10 +373,11 @@ void SceneViewportMoveGizmo::CancelDrag(IUndoManager* undoManager) {
|
||||
m_activeEntityId = 0;
|
||||
m_activeAxisDirection = Math::Vector3::Zero();
|
||||
m_activePlaneNormal = Math::Vector3::Zero();
|
||||
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartHitWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartAxisScalar = 0.0f;
|
||||
m_dragObjects.clear();
|
||||
m_dragStartObjectWorldPositions.clear();
|
||||
m_hoveredAxis = SceneViewportGizmoAxis::None;
|
||||
m_hoveredPlane = SceneViewportGizmoPlane::None;
|
||||
RefreshHandleState();
|
||||
@@ -363,19 +400,74 @@ const SceneViewportMoveGizmoDrawData& SceneViewportMoveGizmo::GetDrawData() cons
|
||||
return m_drawData;
|
||||
}
|
||||
|
||||
SceneViewportMoveGizmoHitResult SceneViewportMoveGizmo::EvaluateHit(const Math::Vector2& mousePosition) const {
|
||||
SceneViewportMoveGizmoHitResult result = {};
|
||||
if (!m_drawData.visible) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const float hoverThresholdSq = kMoveGizmoHoverThresholdPixels * kMoveGizmoHoverThresholdPixels;
|
||||
for (const SceneViewportMoveGizmoHandleDrawData& handle : m_drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end);
|
||||
if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.axis = handle.axis;
|
||||
result.plane = SceneViewportGizmoPlane::None;
|
||||
result.distanceSq = distanceSq;
|
||||
}
|
||||
|
||||
if (result.axis != SceneViewportGizmoAxis::None) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoPlaneDrawData& plane : m_drawData.planes) {
|
||||
if (!plane.visible || !PointInQuad(mousePosition, plane.corners)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = (QuadCenter(plane.corners) - mousePosition).SqrMagnitude();
|
||||
if (distanceSq >= result.distanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.axis = SceneViewportGizmoAxis::None;
|
||||
result.plane = plane.plane;
|
||||
result.distanceSq = distanceSq;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void SceneViewportMoveGizmo::SetHoveredHandle(
|
||||
SceneViewportGizmoAxis axis,
|
||||
SceneViewportGizmoPlane plane) {
|
||||
if (m_dragMode != DragMode::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_hoveredAxis = axis;
|
||||
m_hoveredPlane = axis == SceneViewportGizmoAxis::None ? plane : SceneViewportGizmoPlane::None;
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& context) {
|
||||
m_drawData = {};
|
||||
m_drawData.pivotRadius = 5.0f;
|
||||
|
||||
const Components::GameObject* selectedObject = context.selectedObject;
|
||||
if (selectedObject == nullptr ||
|
||||
if ((context.selectedObject == nullptr && context.selectedObjects.empty()) ||
|
||||
!context.overlay.valid ||
|
||||
context.viewportSize.x <= 1.0f ||
|
||||
context.viewportSize.y <= 1.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 gizmoWorldOrigin = selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 gizmoWorldOrigin = context.pivotWorldPosition;
|
||||
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
@@ -411,7 +503,7 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
|
||||
handle.start = projectedPivot.screenPosition;
|
||||
|
||||
const Math::Vector3 axisEndWorld =
|
||||
gizmoWorldOrigin + GetAxisVector(handle.axis) * axisLengthWorld;
|
||||
gizmoWorldOrigin + GetAxisVector(handle.axis, context.axisOrientation) * axisLengthWorld;
|
||||
const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
@@ -436,7 +528,7 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
|
||||
|
||||
Math::Vector3 axisA = Math::Vector3::Zero();
|
||||
Math::Vector3 axisB = Math::Vector3::Zero();
|
||||
GetPlaneAxes(plane.plane, axisA, axisB);
|
||||
GetPlaneAxes(plane.plane, context.axisOrientation, axisA, axisB);
|
||||
if (axisA.SqrMagnitude() <= Math::EPSILON || axisB.SqrMagnitude() <= Math::EPSILON) {
|
||||
continue;
|
||||
}
|
||||
@@ -502,55 +594,5 @@ void SceneViewportMoveGizmo::RefreshHandleState() {
|
||||
}
|
||||
}
|
||||
|
||||
SceneViewportGizmoAxis SceneViewportMoveGizmo::HitTestAxis(const Math::Vector2& mousePosition) const {
|
||||
if (!m_drawData.visible) {
|
||||
return SceneViewportGizmoAxis::None;
|
||||
}
|
||||
|
||||
const float hoverThresholdSq = kMoveGizmoHoverThresholdPixels * kMoveGizmoHoverThresholdPixels;
|
||||
SceneViewportGizmoAxis bestAxis = SceneViewportGizmoAxis::None;
|
||||
float bestDistanceSq = hoverThresholdSq;
|
||||
|
||||
for (const SceneViewportMoveGizmoHandleDrawData& handle : m_drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end);
|
||||
if (distanceSq > bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestAxis = handle.axis;
|
||||
}
|
||||
|
||||
return bestAxis;
|
||||
}
|
||||
|
||||
SceneViewportGizmoPlane SceneViewportMoveGizmo::HitTestPlane(const Math::Vector2& mousePosition) const {
|
||||
if (!m_drawData.visible) {
|
||||
return SceneViewportGizmoPlane::None;
|
||||
}
|
||||
|
||||
SceneViewportGizmoPlane bestPlane = SceneViewportGizmoPlane::None;
|
||||
float bestDistanceSq = Math::FLOAT_MAX;
|
||||
for (const SceneViewportMoveGizmoPlaneDrawData& plane : m_drawData.planes) {
|
||||
if (!plane.visible || !PointInQuad(mousePosition, plane.corners)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = (QuadCenter(plane.corners) - mousePosition).SqrMagnitude();
|
||||
if (distanceSq >= bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestPlane = plane.plane;
|
||||
}
|
||||
|
||||
return bestPlane;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/Core/Math/Plane.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Components {
|
||||
@@ -66,6 +68,19 @@ struct SceneViewportMoveGizmoContext {
|
||||
Math::Vector2 viewportSize = Math::Vector2::Zero();
|
||||
Math::Vector2 mousePosition = Math::Vector2::Zero();
|
||||
Components::GameObject* selectedObject = nullptr;
|
||||
std::vector<Components::GameObject*> selectedObjects = {};
|
||||
Math::Vector3 pivotWorldPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion axisOrientation = Math::Quaternion::Identity();
|
||||
};
|
||||
|
||||
struct SceneViewportMoveGizmoHitResult {
|
||||
SceneViewportGizmoAxis axis = SceneViewportGizmoAxis::None;
|
||||
SceneViewportGizmoPlane plane = SceneViewportGizmoPlane::None;
|
||||
float distanceSq = Math::FLOAT_MAX;
|
||||
|
||||
bool HasHit() const {
|
||||
return axis != SceneViewportGizmoAxis::None || plane != SceneViewportGizmoPlane::None;
|
||||
}
|
||||
};
|
||||
|
||||
class SceneViewportMoveGizmo {
|
||||
@@ -80,6 +95,8 @@ public:
|
||||
bool IsActive() const;
|
||||
uint64_t GetActiveEntityId() const;
|
||||
const SceneViewportMoveGizmoDrawData& GetDrawData() const;
|
||||
SceneViewportMoveGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const;
|
||||
void SetHoveredHandle(SceneViewportGizmoAxis axis, SceneViewportGizmoPlane plane);
|
||||
|
||||
private:
|
||||
enum class DragMode : uint8_t {
|
||||
@@ -90,8 +107,6 @@ private:
|
||||
|
||||
void BuildDrawData(const SceneViewportMoveGizmoContext& context);
|
||||
void RefreshHandleState();
|
||||
SceneViewportGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const;
|
||||
SceneViewportGizmoPlane HitTestPlane(const Math::Vector2& mousePosition) const;
|
||||
|
||||
SceneViewportMoveGizmoDrawData m_drawData = {};
|
||||
SceneViewportGizmoAxis m_hoveredAxis = SceneViewportGizmoAxis::None;
|
||||
@@ -103,10 +118,11 @@ private:
|
||||
Math::Vector3 m_activeAxisDirection = Math::Vector3::Zero();
|
||||
Math::Vector3 m_activePlaneNormal = Math::Vector3::Zero();
|
||||
Math::Plane m_dragPlane = {};
|
||||
Math::Vector3 m_dragStartObjectWorldPosition = Math::Vector3::Zero();
|
||||
Math::Vector3 m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
Math::Vector3 m_dragStartHitWorldPosition = Math::Vector3::Zero();
|
||||
float m_dragStartAxisScalar = 0.0f;
|
||||
std::vector<Components::GameObject*> m_dragObjects = {};
|
||||
std::vector<Math::Vector3> m_dragStartObjectWorldPositions = {};
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
|
||||
431
editor/src/Viewport/SceneViewportOverlayBuilder.cpp
Normal file
@@ -0,0 +1,431 @@
|
||||
#include "SceneViewportOverlayBuilder.h"
|
||||
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "SceneViewportOverlayHandleBuilder.h"
|
||||
#include "SceneViewportMath.h"
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Components/LightComponent.h>
|
||||
#include <XCEngine/Components/TransformComponent.h>
|
||||
#include <XCEngine/Core/Math/Rect.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool CanBuildOverlayForGameObject(const Components::GameObject* gameObject) {
|
||||
return gameObject != nullptr &&
|
||||
gameObject->GetTransform() != nullptr &&
|
||||
gameObject->IsActiveInHierarchy();
|
||||
}
|
||||
|
||||
float ResolveCameraAspect(
|
||||
const Components::CameraComponent& camera,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight) {
|
||||
const Math::Rect viewportRect = camera.GetViewportRect();
|
||||
const float resolvedWidth = static_cast<float>(viewportWidth) *
|
||||
(viewportRect.width > Math::EPSILON ? viewportRect.width : 1.0f);
|
||||
const float resolvedHeight = static_cast<float>(viewportHeight) *
|
||||
(viewportRect.height > Math::EPSILON ? viewportRect.height : 1.0f);
|
||||
return resolvedHeight > Math::EPSILON
|
||||
? resolvedWidth / resolvedHeight
|
||||
: 1.0f;
|
||||
}
|
||||
|
||||
float ComputeWorldUnitsPerPixel(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector3& worldPoint,
|
||||
uint32_t viewportHeight) {
|
||||
if (!overlay.valid || viewportHeight <= 1u) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const Math::Vector3 cameraForward = overlay.cameraForward.Normalized();
|
||||
const float depth = Math::Vector3::Dot(worldPoint - overlay.cameraPosition, cameraForward);
|
||||
if (depth <= Math::EPSILON) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) /
|
||||
static_cast<float>(viewportHeight);
|
||||
}
|
||||
|
||||
void AppendWorldLine(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector3& startWorld,
|
||||
const Math::Vector3& endWorld,
|
||||
const Math::Color& color,
|
||||
float thicknessPixels,
|
||||
SceneViewportOverlayDepthMode depthMode) {
|
||||
SceneViewportOverlayLinePrimitive& line = frameData.worldLines.emplace_back();
|
||||
line.startWorld = startWorld;
|
||||
line.endWorld = endWorld;
|
||||
line.color = color;
|
||||
line.thicknessPixels = thicknessPixels;
|
||||
line.depthMode = depthMode;
|
||||
}
|
||||
|
||||
void AppendWorldSprite(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector3& worldPosition,
|
||||
const Math::Vector2& sizePixels,
|
||||
const Math::Color& tintColor,
|
||||
float sortDepth,
|
||||
uint64_t entityId,
|
||||
SceneViewportOverlaySpriteTextureKind textureKind,
|
||||
SceneViewportOverlayDepthMode depthMode) {
|
||||
if (entityId == 0 || sizePixels.x <= Math::EPSILON || sizePixels.y <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
SceneViewportOverlaySpritePrimitive& sprite = frameData.worldSprites.emplace_back();
|
||||
sprite.worldPosition = worldPosition;
|
||||
sprite.sizePixels = sizePixels;
|
||||
sprite.tintColor = tintColor;
|
||||
sprite.sortDepth = sortDepth;
|
||||
sprite.entityId = entityId;
|
||||
sprite.textureKind = textureKind;
|
||||
sprite.depthMode = depthMode;
|
||||
}
|
||||
|
||||
void AppendHandleRecord(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
SceneViewportOverlayHandleKind kind,
|
||||
uint64_t handleId,
|
||||
uint64_t entityId,
|
||||
const Math::Vector3& worldPosition,
|
||||
const Math::Vector2& sizePixels,
|
||||
float sortDepth) {
|
||||
if (kind == SceneViewportOverlayHandleKind::None ||
|
||||
handleId == 0 ||
|
||||
entityId == 0 ||
|
||||
sizePixels.x <= Math::EPSILON ||
|
||||
sizePixels.y <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
SceneViewportOverlayHandleRecord& handleRecord = frameData.handleRecords.emplace_back();
|
||||
handleRecord.kind = kind;
|
||||
handleRecord.handleId = handleId;
|
||||
handleRecord.entityId = entityId;
|
||||
handleRecord.shape = SceneViewportOverlayHandleShape::WorldRect;
|
||||
handleRecord.priority = Detail::kSceneViewportHandlePrioritySceneIcon;
|
||||
handleRecord.worldPosition = worldPosition;
|
||||
handleRecord.sizePixels = sizePixels;
|
||||
handleRecord.sortDepth = sortDepth;
|
||||
}
|
||||
|
||||
void AppendSceneIconOverlay(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight,
|
||||
const Components::GameObject& gameObject,
|
||||
const Math::Vector2& sizePixels,
|
||||
SceneViewportOverlaySpriteTextureKind textureKind) {
|
||||
const Components::TransformComponent* transform = gameObject.GetTransform();
|
||||
if (transform == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint(
|
||||
overlay,
|
||||
static_cast<float>(viewportWidth),
|
||||
static_cast<float>(viewportHeight),
|
||||
transform->GetPosition());
|
||||
if (!projectedPoint.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppendWorldSprite(
|
||||
frameData,
|
||||
transform->GetPosition(),
|
||||
sizePixels,
|
||||
Math::Color::White(),
|
||||
projectedPoint.ndcDepth,
|
||||
gameObject.GetID(),
|
||||
textureKind,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
AppendHandleRecord(
|
||||
frameData,
|
||||
SceneViewportOverlayHandleKind::SceneIcon,
|
||||
gameObject.GetID(),
|
||||
gameObject.GetID(),
|
||||
transform->GetPosition(),
|
||||
sizePixels,
|
||||
projectedPoint.ndcDepth);
|
||||
}
|
||||
|
||||
void AppendCameraFrustumOverlay(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Components::CameraComponent& camera,
|
||||
const Components::GameObject& gameObject,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight) {
|
||||
const Components::TransformComponent* transform = gameObject.GetTransform();
|
||||
if (transform == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 position = transform->GetPosition();
|
||||
const Math::Vector3 forward = transform->GetForward().Normalized();
|
||||
const Math::Vector3 right = transform->GetRight().Normalized();
|
||||
const Math::Vector3 up = transform->GetUp().Normalized();
|
||||
if (forward.SqrMagnitude() <= Math::EPSILON ||
|
||||
right.SqrMagnitude() <= Math::EPSILON ||
|
||||
up.SqrMagnitude() <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float nearClip = (std::max)(camera.GetNearClipPlane(), 0.01f);
|
||||
const float farClip = (std::max)(camera.GetFarClipPlane(), nearClip + 0.01f);
|
||||
const float aspect = ResolveCameraAspect(camera, viewportWidth, viewportHeight);
|
||||
|
||||
float nearHalfHeight = 0.0f;
|
||||
float nearHalfWidth = 0.0f;
|
||||
float farHalfHeight = 0.0f;
|
||||
float farHalfWidth = 0.0f;
|
||||
if (camera.GetProjectionType() == Components::CameraProjectionType::Perspective) {
|
||||
const float halfFovRadians =
|
||||
std::clamp(camera.GetFieldOfView(), 1.0f, 179.0f) * Math::DEG_TO_RAD * 0.5f;
|
||||
nearHalfHeight = std::tan(halfFovRadians) * nearClip;
|
||||
nearHalfWidth = nearHalfHeight * aspect;
|
||||
farHalfHeight = std::tan(halfFovRadians) * farClip;
|
||||
farHalfWidth = farHalfHeight * aspect;
|
||||
} else {
|
||||
const float halfHeight = (std::max)(camera.GetOrthographicSize(), 0.01f);
|
||||
const float halfWidth = halfHeight * aspect;
|
||||
nearHalfHeight = halfHeight;
|
||||
nearHalfWidth = halfWidth;
|
||||
farHalfHeight = halfHeight;
|
||||
farHalfWidth = halfWidth;
|
||||
}
|
||||
|
||||
const Math::Vector3 nearCenter = position + forward * nearClip;
|
||||
const Math::Vector3 farCenter = position + forward * farClip;
|
||||
const std::array<Math::Vector3, 8> corners = {{
|
||||
nearCenter + up * nearHalfHeight - right * nearHalfWidth,
|
||||
nearCenter + up * nearHalfHeight + right * nearHalfWidth,
|
||||
nearCenter - up * nearHalfHeight + right * nearHalfWidth,
|
||||
nearCenter - up * nearHalfHeight - right * nearHalfWidth,
|
||||
farCenter + up * farHalfHeight - right * farHalfWidth,
|
||||
farCenter + up * farHalfHeight + right * farHalfWidth,
|
||||
farCenter - up * farHalfHeight + right * farHalfWidth,
|
||||
farCenter - up * farHalfHeight - right * farHalfWidth
|
||||
}};
|
||||
|
||||
static constexpr std::array<std::pair<size_t, size_t>, 12> kFrustumEdges = {{
|
||||
{ 0u, 1u }, { 1u, 2u }, { 2u, 3u }, { 3u, 0u },
|
||||
{ 4u, 5u }, { 5u, 6u }, { 6u, 7u }, { 7u, 4u },
|
||||
{ 0u, 4u }, { 1u, 5u }, { 2u, 6u }, { 3u, 7u }
|
||||
}};
|
||||
constexpr Math::Color kFrustumColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
for (const auto& edge : kFrustumEdges) {
|
||||
AppendWorldLine(
|
||||
frameData,
|
||||
corners[edge.first],
|
||||
corners[edge.second],
|
||||
kFrustumColor,
|
||||
1.6f,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendDirectionalLightOverlay(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Components::GameObject& gameObject,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportHeight) {
|
||||
const Components::TransformComponent* transform = gameObject.GetTransform();
|
||||
if (transform == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 position = transform->GetPosition();
|
||||
const Math::Vector3 lightDirection = (transform->GetForward() * -1.0f).Normalized();
|
||||
const Math::Vector3 right = transform->GetRight().Normalized();
|
||||
const Math::Vector3 up = transform->GetUp().Normalized();
|
||||
if (lightDirection.SqrMagnitude() <= Math::EPSILON ||
|
||||
right.SqrMagnitude() <= Math::EPSILON ||
|
||||
up.SqrMagnitude() <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(overlay, position, viewportHeight);
|
||||
if (worldUnitsPerPixel <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr Math::Color kDirectionalLightColor(1.0f, 0.92f, 0.24f, 1.0f);
|
||||
constexpr float kLineThickness = 1.8f;
|
||||
constexpr size_t kRingSegmentCount = 32;
|
||||
constexpr std::array<float, 6> kRayAngles = {{
|
||||
0.0f,
|
||||
Math::PI / 3.0f,
|
||||
Math::PI * 2.0f / 3.0f,
|
||||
Math::PI,
|
||||
Math::PI * 4.0f / 3.0f,
|
||||
Math::PI * 5.0f / 3.0f
|
||||
}};
|
||||
|
||||
const float ringRadius = worldUnitsPerPixel * 26.0f;
|
||||
const float ringOffset = worldUnitsPerPixel * 54.0f;
|
||||
const float innerRayRadius = ringRadius * 0.52f;
|
||||
const float rayLength = worldUnitsPerPixel * 96.0f;
|
||||
const Math::Vector3 ringCenter = position + lightDirection * ringOffset;
|
||||
|
||||
for (size_t segmentIndex = 0; segmentIndex < kRingSegmentCount; ++segmentIndex) {
|
||||
const float angle0 =
|
||||
static_cast<float>(segmentIndex) / static_cast<float>(kRingSegmentCount) * Math::PI * 2.0f;
|
||||
const float angle1 =
|
||||
static_cast<float>(segmentIndex + 1u) / static_cast<float>(kRingSegmentCount) * Math::PI * 2.0f;
|
||||
const Math::Vector3 p0 =
|
||||
ringCenter + right * std::cos(angle0) * ringRadius + up * std::sin(angle0) * ringRadius;
|
||||
const Math::Vector3 p1 =
|
||||
ringCenter + right * std::cos(angle1) * ringRadius + up * std::sin(angle1) * ringRadius;
|
||||
AppendWorldLine(
|
||||
frameData,
|
||||
p0,
|
||||
p1,
|
||||
kDirectionalLightColor,
|
||||
kLineThickness,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
}
|
||||
|
||||
AppendWorldLine(
|
||||
frameData,
|
||||
position,
|
||||
ringCenter,
|
||||
kDirectionalLightColor,
|
||||
kLineThickness,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
AppendWorldLine(
|
||||
frameData,
|
||||
ringCenter,
|
||||
ringCenter + lightDirection * rayLength,
|
||||
kDirectionalLightColor,
|
||||
kLineThickness,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
|
||||
for (float angle : kRayAngles) {
|
||||
const Math::Vector3 rayStart =
|
||||
ringCenter + right * std::cos(angle) * innerRayRadius + up * std::sin(angle) * innerRayRadius;
|
||||
AppendWorldLine(
|
||||
frameData,
|
||||
rayStart,
|
||||
rayStart + lightDirection * rayLength,
|
||||
kDirectionalLightColor,
|
||||
kLineThickness,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendSceneObjectIconOverlays(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Components::Scene& scene,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight) {
|
||||
constexpr Math::Vector2 kCameraIconSize(90.0f, 90.0f);
|
||||
constexpr Math::Vector2 kLightIconSize(100.0f, 100.0f);
|
||||
|
||||
for (Components::CameraComponent* camera : scene.FindObjectsOfType<Components::CameraComponent>()) {
|
||||
if (camera == nullptr || !camera->IsEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Components::GameObject* gameObject = camera->GetGameObject();
|
||||
if (!CanBuildOverlayForGameObject(gameObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AppendSceneIconOverlay(
|
||||
frameData,
|
||||
overlay,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
*gameObject,
|
||||
kCameraIconSize,
|
||||
SceneViewportOverlaySpriteTextureKind::Camera);
|
||||
}
|
||||
|
||||
for (Components::LightComponent* light : scene.FindObjectsOfType<Components::LightComponent>()) {
|
||||
if (light == nullptr || !light->IsEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Components::GameObject* gameObject = light->GetGameObject();
|
||||
if (!CanBuildOverlayForGameObject(gameObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AppendSceneIconOverlay(
|
||||
frameData,
|
||||
overlay,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
*gameObject,
|
||||
kLightIconSize,
|
||||
SceneViewportOverlaySpriteTextureKind::Light);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SceneViewportOverlayFrameData SceneViewportOverlayBuilder::Build(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight,
|
||||
const std::vector<uint64_t>& selectedObjectIds) {
|
||||
SceneViewportOverlayFrameData frameData = {};
|
||||
frameData.overlay = overlay;
|
||||
if (!overlay.valid || viewportWidth == 0u || viewportHeight == 0u) {
|
||||
return frameData;
|
||||
}
|
||||
|
||||
const Components::Scene* scene = context.GetSceneManager().GetScene();
|
||||
if (scene == nullptr) {
|
||||
return frameData;
|
||||
}
|
||||
|
||||
AppendSceneObjectIconOverlays(frameData, *scene, overlay, viewportWidth, viewportHeight);
|
||||
|
||||
for (uint64_t entityId : selectedObjectIds) {
|
||||
if (entityId == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Components::GameObject* gameObject = context.GetSceneManager().GetEntity(entityId);
|
||||
if (!CanBuildOverlayForGameObject(gameObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Components::CameraComponent* camera = gameObject->GetComponent<Components::CameraComponent>();
|
||||
camera != nullptr && camera->IsEnabled()) {
|
||||
AppendCameraFrustumOverlay(frameData, *camera, *gameObject, viewportWidth, viewportHeight);
|
||||
}
|
||||
|
||||
if (Components::LightComponent* light = gameObject->GetComponent<Components::LightComponent>();
|
||||
light != nullptr &&
|
||||
light->IsEnabled() &&
|
||||
light->GetLightType() == Components::LightType::Directional) {
|
||||
AppendDirectionalLightOverlay(frameData, *gameObject, overlay, viewportHeight);
|
||||
}
|
||||
}
|
||||
|
||||
return frameData;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
25
editor/src/Viewport/SceneViewportOverlayBuilder.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "IViewportHostService.h"
|
||||
#include "SceneViewportEditorOverlayData.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
class IEditorContext;
|
||||
|
||||
class SceneViewportOverlayBuilder {
|
||||
public:
|
||||
static SceneViewportOverlayFrameData Build(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight,
|
||||
const std::vector<uint64_t>& selectedObjectIds);
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
543
editor/src/Viewport/SceneViewportOverlayHandleBuilder.h
Normal file
@@ -0,0 +1,543 @@
|
||||
#pragma once
|
||||
|
||||
#include "SceneViewportEditorOverlayData.h"
|
||||
#include "SceneViewportMoveGizmo.h"
|
||||
#include "SceneViewportRotateGizmo.h"
|
||||
#include "SceneViewportScaleGizmo.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
struct SceneViewportTransformGizmoHandleBuildInputs {
|
||||
const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr;
|
||||
uint64_t moveEntityId = 0;
|
||||
const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr;
|
||||
uint64_t rotateEntityId = 0;
|
||||
const SceneViewportScaleGizmoDrawData* scaleGizmo = nullptr;
|
||||
uint64_t scaleEntityId = 0;
|
||||
};
|
||||
|
||||
inline SceneViewportTransformGizmoHandleBuildInputs BuildSceneViewportTransformGizmoHandleBuildInputs(
|
||||
bool showingMoveGizmo,
|
||||
const SceneViewportMoveGizmo& moveGizmo,
|
||||
const SceneViewportMoveGizmoContext& moveGizmoContext,
|
||||
bool showingRotateGizmo,
|
||||
const SceneViewportRotateGizmo& rotateGizmo,
|
||||
const SceneViewportRotateGizmoContext& rotateGizmoContext,
|
||||
bool showingScaleGizmo,
|
||||
const SceneViewportScaleGizmo& scaleGizmo,
|
||||
const SceneViewportScaleGizmoContext& scaleGizmoContext) {
|
||||
SceneViewportTransformGizmoHandleBuildInputs inputs = {};
|
||||
if (showingMoveGizmo && moveGizmoContext.selectedObject != nullptr) {
|
||||
inputs.moveGizmo = &moveGizmo.GetDrawData();
|
||||
inputs.moveEntityId = moveGizmoContext.selectedObject->GetID();
|
||||
}
|
||||
if (showingRotateGizmo && rotateGizmoContext.selectedObject != nullptr) {
|
||||
inputs.rotateGizmo = &rotateGizmo.GetDrawData();
|
||||
inputs.rotateEntityId = rotateGizmoContext.selectedObject->GetID();
|
||||
}
|
||||
if (showingScaleGizmo && scaleGizmoContext.selectedObject != nullptr) {
|
||||
inputs.scaleGizmo = &scaleGizmo.GetDrawData();
|
||||
inputs.scaleEntityId = scaleGizmoContext.selectedObject->GetID();
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
namespace Detail {
|
||||
|
||||
inline constexpr int kSceneViewportHandlePrioritySceneIcon = 100;
|
||||
inline constexpr int kSceneViewportHandlePriorityRotateAxis = 311;
|
||||
inline constexpr int kSceneViewportHandlePriorityMovePlane = 321;
|
||||
inline constexpr int kSceneViewportHandlePriorityMoveAxis = 322;
|
||||
inline constexpr int kSceneViewportHandlePriorityScaleAxisLine = 331;
|
||||
inline constexpr int kSceneViewportHandlePriorityScaleAxisCap = 332;
|
||||
inline constexpr int kSceneViewportHandlePriorityScaleUniform = 333;
|
||||
|
||||
inline constexpr float kSceneViewportMoveAxisHitThicknessPixels = 10.0f;
|
||||
inline constexpr float kSceneViewportRotateAxisHitThicknessPixels = 9.0f;
|
||||
inline constexpr float kSceneViewportScaleAxisHitThicknessPixels = 10.0f;
|
||||
inline constexpr float kSceneViewportScaleCapHitPaddingPixels = 2.0f;
|
||||
|
||||
inline constexpr float kSceneViewportMoveArrowLengthPixels = 14.0f;
|
||||
inline constexpr float kSceneViewportMoveArrowHalfWidthPixels = 7.0f;
|
||||
|
||||
inline Math::Color WithAlpha(const Math::Color& color, float alpha) {
|
||||
return Math::Color(color.r, color.g, color.b, alpha);
|
||||
}
|
||||
|
||||
inline Math::Color LerpColor(const Math::Color& a, const Math::Color& b, float t) {
|
||||
return Math::Color(
|
||||
a.r + (b.r - a.r) * t,
|
||||
a.g + (b.g - a.g) * t,
|
||||
a.b + (b.b - a.b) * t,
|
||||
a.a + (b.a - a.a) * t);
|
||||
}
|
||||
|
||||
inline Math::Vector2 NormalizeVector2(
|
||||
const Math::Vector2& value,
|
||||
const Math::Vector2& fallback = Math::Vector2(1.0f, 0.0f)) {
|
||||
const float lengthSq = value.SqrMagnitude();
|
||||
if (lengthSq <= Math::EPSILON) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return value / std::sqrt(lengthSq);
|
||||
}
|
||||
|
||||
inline void AppendScreenTriangle(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& a,
|
||||
const Math::Vector2& b,
|
||||
const Math::Vector2& c,
|
||||
const Math::Color& color,
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
|
||||
SceneViewportOverlayScreenTrianglePrimitive& triangle = frameData.screenTriangles.emplace_back();
|
||||
triangle.vertices[0].screenPosition = a;
|
||||
triangle.vertices[0].color = color;
|
||||
triangle.vertices[1].screenPosition = b;
|
||||
triangle.vertices[1].color = color;
|
||||
triangle.vertices[2].screenPosition = c;
|
||||
triangle.vertices[2].color = color;
|
||||
triangle.depthMode = depthMode;
|
||||
}
|
||||
|
||||
inline void AppendScreenQuad(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& a,
|
||||
const Math::Vector2& b,
|
||||
const Math::Vector2& c,
|
||||
const Math::Vector2& d,
|
||||
const Math::Color& color,
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
|
||||
AppendScreenTriangle(frameData, a, b, c, color, depthMode);
|
||||
AppendScreenTriangle(frameData, a, c, d, color, depthMode);
|
||||
}
|
||||
|
||||
inline void AppendScreenRect(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& center,
|
||||
const Math::Vector2& halfSize,
|
||||
const Math::Color& color,
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
|
||||
AppendScreenQuad(
|
||||
frameData,
|
||||
Math::Vector2(center.x - halfSize.x, center.y - halfSize.y),
|
||||
Math::Vector2(center.x + halfSize.x, center.y - halfSize.y),
|
||||
Math::Vector2(center.x + halfSize.x, center.y + halfSize.y),
|
||||
Math::Vector2(center.x - halfSize.x, center.y + halfSize.y),
|
||||
color,
|
||||
depthMode);
|
||||
}
|
||||
|
||||
inline void AppendScreenSegmentQuad(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& start,
|
||||
const Math::Vector2& end,
|
||||
float thicknessPixels,
|
||||
const Math::Color& color,
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
|
||||
const Math::Vector2 delta = end - start;
|
||||
if (delta.SqrMagnitude() <= Math::EPSILON || thicknessPixels <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector2 direction = NormalizeVector2(delta);
|
||||
const Math::Vector2 normal(-direction.y, direction.x);
|
||||
const Math::Vector2 offset = normal * (thicknessPixels * 0.5f);
|
||||
AppendScreenQuad(
|
||||
frameData,
|
||||
start + offset,
|
||||
start - offset,
|
||||
end - offset,
|
||||
end + offset,
|
||||
color,
|
||||
depthMode);
|
||||
}
|
||||
|
||||
inline void AppendScreenQuadOutline(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const std::array<Math::Vector2, 4>& corners,
|
||||
float thicknessPixels,
|
||||
const Math::Color& color) {
|
||||
for (size_t index = 0; index < corners.size(); ++index) {
|
||||
AppendScreenSegmentQuad(
|
||||
frameData,
|
||||
corners[index],
|
||||
corners[(index + 1u) % corners.size()],
|
||||
thicknessPixels,
|
||||
color);
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendScreenRectOutline(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& center,
|
||||
const Math::Vector2& halfSize,
|
||||
float thicknessPixels,
|
||||
const Math::Color& color) {
|
||||
const std::array<Math::Vector2, 4> corners = {{
|
||||
Math::Vector2(center.x - halfSize.x, center.y - halfSize.y),
|
||||
Math::Vector2(center.x + halfSize.x, center.y - halfSize.y),
|
||||
Math::Vector2(center.x + halfSize.x, center.y + halfSize.y),
|
||||
Math::Vector2(center.x - halfSize.x, center.y + halfSize.y)
|
||||
}};
|
||||
AppendScreenQuadOutline(frameData, corners, thicknessPixels, color);
|
||||
}
|
||||
|
||||
inline void AppendMoveGizmoHandleRecords(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportMoveGizmoDrawData& drawData,
|
||||
uint64_t entityId) {
|
||||
if (!drawData.visible || entityId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (!handle.visible || handle.axis == SceneViewportGizmoAxis::None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SceneViewportOverlayHandleRecord& record = frameData.handleRecords.emplace_back();
|
||||
record.kind = SceneViewportOverlayHandleKind::MoveAxis;
|
||||
record.handleId = static_cast<uint64_t>(handle.axis);
|
||||
record.entityId = entityId;
|
||||
record.shape = SceneViewportOverlayHandleShape::ScreenSegment;
|
||||
record.priority = kSceneViewportHandlePriorityMoveAxis;
|
||||
record.screenStart = handle.start;
|
||||
record.screenEnd = handle.end;
|
||||
record.hitThicknessPixels = kSceneViewportMoveAxisHitThicknessPixels;
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoPlaneDrawData& plane : drawData.planes) {
|
||||
if (!plane.visible || plane.plane == SceneViewportGizmoPlane::None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SceneViewportOverlayHandleRecord& record = frameData.handleRecords.emplace_back();
|
||||
record.kind = SceneViewportOverlayHandleKind::MovePlane;
|
||||
record.handleId = static_cast<uint64_t>(plane.plane);
|
||||
record.entityId = entityId;
|
||||
record.shape = SceneViewportOverlayHandleShape::ScreenQuad;
|
||||
record.priority = kSceneViewportHandlePriorityMovePlane;
|
||||
record.screenQuad = plane.corners;
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendRotateGizmoHandleRecords(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportRotateGizmoDrawData& drawData,
|
||||
uint64_t entityId) {
|
||||
if (!drawData.visible || entityId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (!handle.visible || handle.axis == SceneViewportRotateGizmoAxis::None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible ||
|
||||
(handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SceneViewportOverlayHandleRecord& record = frameData.handleRecords.emplace_back();
|
||||
record.kind = SceneViewportOverlayHandleKind::RotateAxis;
|
||||
record.handleId = static_cast<uint64_t>(handle.axis);
|
||||
record.entityId = entityId;
|
||||
record.shape = SceneViewportOverlayHandleShape::ScreenSegment;
|
||||
record.priority = kSceneViewportHandlePriorityRotateAxis;
|
||||
record.screenStart = segment.start;
|
||||
record.screenEnd = segment.end;
|
||||
record.hitThicknessPixels = kSceneViewportRotateAxisHitThicknessPixels;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendScaleGizmoHandleRecords(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportScaleGizmoDrawData& drawData,
|
||||
uint64_t entityId) {
|
||||
if (!drawData.visible || entityId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (drawData.centerHandle.visible) {
|
||||
SceneViewportOverlayHandleRecord& uniformRecord = frameData.handleRecords.emplace_back();
|
||||
uniformRecord.kind = SceneViewportOverlayHandleKind::ScaleUniform;
|
||||
uniformRecord.handleId = static_cast<uint64_t>(SceneViewportScaleGizmoHandle::Uniform);
|
||||
uniformRecord.entityId = entityId;
|
||||
uniformRecord.shape = SceneViewportOverlayHandleShape::ScreenRect;
|
||||
uniformRecord.priority = kSceneViewportHandlePriorityScaleUniform;
|
||||
uniformRecord.screenCenter = drawData.centerHandle.center;
|
||||
uniformRecord.screenHalfSize = Math::Vector2(
|
||||
drawData.centerHandle.halfSize + kSceneViewportScaleCapHitPaddingPixels,
|
||||
drawData.centerHandle.halfSize + kSceneViewportScaleCapHitPaddingPixels);
|
||||
}
|
||||
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : drawData.axisHandles) {
|
||||
if (!handle.visible || handle.handle == SceneViewportScaleGizmoHandle::None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SceneViewportOverlayHandleRecord& capRecord = frameData.handleRecords.emplace_back();
|
||||
capRecord.kind = SceneViewportOverlayHandleKind::ScaleAxis;
|
||||
capRecord.handleId = static_cast<uint64_t>(handle.handle);
|
||||
capRecord.entityId = entityId;
|
||||
capRecord.shape = SceneViewportOverlayHandleShape::ScreenRect;
|
||||
capRecord.priority = kSceneViewportHandlePriorityScaleAxisCap;
|
||||
capRecord.screenCenter = handle.capCenter;
|
||||
capRecord.screenHalfSize = Math::Vector2(
|
||||
handle.capHalfSize + kSceneViewportScaleCapHitPaddingPixels,
|
||||
handle.capHalfSize + kSceneViewportScaleCapHitPaddingPixels);
|
||||
|
||||
SceneViewportOverlayHandleRecord& lineRecord = frameData.handleRecords.emplace_back();
|
||||
lineRecord.kind = SceneViewportOverlayHandleKind::ScaleAxis;
|
||||
lineRecord.handleId = static_cast<uint64_t>(handle.handle);
|
||||
lineRecord.entityId = entityId;
|
||||
lineRecord.shape = SceneViewportOverlayHandleShape::ScreenSegment;
|
||||
lineRecord.priority = kSceneViewportHandlePriorityScaleAxisLine;
|
||||
lineRecord.screenStart = handle.start;
|
||||
lineRecord.screenEnd = handle.end;
|
||||
lineRecord.hitThicknessPixels = kSceneViewportScaleAxisHitThicknessPixels;
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendMoveGizmoScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportMoveGizmoDrawData& drawData) {
|
||||
if (!drawData.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoPlaneDrawData& plane : drawData.planes) {
|
||||
if (!plane.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AppendScreenQuad(
|
||||
frameData,
|
||||
plane.corners[0],
|
||||
plane.corners[1],
|
||||
plane.corners[2],
|
||||
plane.corners[3],
|
||||
plane.fillColor);
|
||||
AppendScreenQuadOutline(
|
||||
frameData,
|
||||
plane.corners,
|
||||
plane.active ? 2.6f : (plane.hovered ? 2.0f : 1.4f),
|
||||
plane.outlineColor);
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f);
|
||||
const Math::Vector2 direction = NormalizeVector2(handle.end - handle.start);
|
||||
const float arrowLength =
|
||||
(std::min)(kSceneViewportMoveArrowLengthPixels, (handle.end - handle.start).Magnitude());
|
||||
const Math::Vector2 normal(-direction.y, direction.x);
|
||||
const Math::Vector2 arrowBase = handle.end - direction * arrowLength;
|
||||
const Math::Vector2 arrowLeft = arrowBase + normal * kSceneViewportMoveArrowHalfWidthPixels;
|
||||
const Math::Vector2 arrowRight = arrowBase - normal * kSceneViewportMoveArrowHalfWidthPixels;
|
||||
|
||||
AppendScreenSegmentQuad(frameData, handle.start, arrowBase, thickness, handle.color);
|
||||
AppendScreenTriangle(frameData, handle.end, arrowLeft, arrowRight, handle.color);
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendRotateGizmoHandleScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportRotateGizmoHandleDrawData& handle,
|
||||
bool frontPass) {
|
||||
if (!handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool isViewHandle = handle.axis == SceneViewportRotateGizmoAxis::View;
|
||||
if (isViewHandle && !frontPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float thickness = handle.active ? 3.6f : (handle.hovered ? 3.0f : 2.1f);
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible || (!isViewHandle && segment.frontFacing != frontPass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Math::Color drawColor = handle.color;
|
||||
if (!isViewHandle && !frontPass) {
|
||||
drawColor = LerpColor(handle.color, Math::Color(0.72f, 0.72f, 0.72f, 1.0f), 0.78f);
|
||||
drawColor = WithAlpha(drawColor, handle.active ? 0.55f : 0.38f);
|
||||
} else if (isViewHandle) {
|
||||
drawColor = WithAlpha(drawColor, handle.active ? 0.95f : (handle.hovered ? 0.88f : 0.78f));
|
||||
}
|
||||
|
||||
AppendScreenSegmentQuad(frameData, segment.start, segment.end, thickness, drawColor);
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendRotateGizmoAngleFillScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportRotateGizmoAngleFillDrawData& angleFill) {
|
||||
if (!angleFill.visible || angleFill.arcPointCount < 2u) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t index = 0; index + 1u < angleFill.arcPointCount; ++index) {
|
||||
AppendScreenTriangle(
|
||||
frameData,
|
||||
angleFill.pivot,
|
||||
angleFill.arcPoints[index],
|
||||
angleFill.arcPoints[index + 1u],
|
||||
angleFill.fillColor);
|
||||
}
|
||||
|
||||
for (size_t index = 0; index + 1u < angleFill.arcPointCount; ++index) {
|
||||
AppendScreenSegmentQuad(
|
||||
frameData,
|
||||
angleFill.arcPoints[index],
|
||||
angleFill.arcPoints[index + 1u],
|
||||
2.0f,
|
||||
angleFill.outlineColor);
|
||||
}
|
||||
|
||||
AppendScreenSegmentQuad(
|
||||
frameData,
|
||||
angleFill.pivot,
|
||||
angleFill.arcPoints[0],
|
||||
1.6f,
|
||||
angleFill.outlineColor);
|
||||
AppendScreenSegmentQuad(
|
||||
frameData,
|
||||
angleFill.pivot,
|
||||
angleFill.arcPoints[angleFill.arcPointCount - 1u],
|
||||
1.6f,
|
||||
angleFill.outlineColor);
|
||||
}
|
||||
|
||||
inline void AppendRotateGizmoScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportRotateGizmoDrawData& drawData) {
|
||||
if (!drawData.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (handle.axis == SceneViewportRotateGizmoAxis::View) {
|
||||
AppendRotateGizmoHandleScreenTriangles(frameData, handle, true);
|
||||
}
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
AppendRotateGizmoHandleScreenTriangles(frameData, handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
AppendRotateGizmoAngleFillScreenTriangles(frameData, drawData.angleFill);
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
AppendRotateGizmoHandleScreenTriangles(frameData, handle, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendScaleGizmoScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportScaleGizmoDrawData& drawData) {
|
||||
if (!drawData.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr Math::Color kScaleCapOutlineColor(24.0f / 255.0f, 24.0f / 255.0f, 24.0f / 255.0f, 220.0f / 255.0f);
|
||||
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : drawData.axisHandles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.2f);
|
||||
const Math::Vector2 direction = NormalizeVector2(handle.capCenter - handle.start);
|
||||
const Math::Vector2 lineEnd = handle.capCenter - direction * handle.capHalfSize;
|
||||
const Math::Vector2 capHalfSize(handle.capHalfSize, handle.capHalfSize);
|
||||
AppendScreenSegmentQuad(frameData, handle.start, lineEnd, thickness, handle.color);
|
||||
AppendScreenRect(frameData, handle.capCenter, capHalfSize, handle.color);
|
||||
AppendScreenRectOutline(
|
||||
frameData,
|
||||
handle.capCenter,
|
||||
capHalfSize,
|
||||
handle.active ? 2.0f : 1.0f,
|
||||
kScaleCapOutlineColor);
|
||||
}
|
||||
|
||||
if (drawData.centerHandle.visible) {
|
||||
const Math::Vector2 halfSize(drawData.centerHandle.halfSize, drawData.centerHandle.halfSize);
|
||||
AppendScreenRect(
|
||||
frameData,
|
||||
drawData.centerHandle.center,
|
||||
halfSize,
|
||||
drawData.centerHandle.fillColor);
|
||||
AppendScreenRectOutline(
|
||||
frameData,
|
||||
drawData.centerHandle.center,
|
||||
halfSize,
|
||||
drawData.centerHandle.active ? 2.0f : 1.0f,
|
||||
drawData.centerHandle.outlineColor);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
inline void AppendTransformGizmoHandleRecords(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) {
|
||||
if (inputs.moveGizmo != nullptr) {
|
||||
Detail::AppendMoveGizmoHandleRecords(frameData, *inputs.moveGizmo, inputs.moveEntityId);
|
||||
}
|
||||
|
||||
if (inputs.rotateGizmo != nullptr) {
|
||||
Detail::AppendRotateGizmoHandleRecords(frameData, *inputs.rotateGizmo, inputs.rotateEntityId);
|
||||
}
|
||||
|
||||
if (inputs.scaleGizmo != nullptr) {
|
||||
Detail::AppendScaleGizmoHandleRecords(frameData, *inputs.scaleGizmo, inputs.scaleEntityId);
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendTransformGizmoScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) {
|
||||
if (inputs.moveGizmo != nullptr) {
|
||||
Detail::AppendMoveGizmoScreenTriangles(frameData, *inputs.moveGizmo);
|
||||
}
|
||||
|
||||
if (inputs.rotateGizmo != nullptr) {
|
||||
Detail::AppendRotateGizmoScreenTriangles(frameData, *inputs.rotateGizmo);
|
||||
}
|
||||
|
||||
if (inputs.scaleGizmo != nullptr) {
|
||||
Detail::AppendScaleGizmoScreenTriangles(frameData, *inputs.scaleGizmo);
|
||||
}
|
||||
}
|
||||
|
||||
inline SceneViewportOverlayFrameData BuildSceneViewportTransformGizmoOverlayFrameData(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) {
|
||||
SceneViewportOverlayFrameData frameData = {};
|
||||
frameData.overlay = overlay;
|
||||
AppendTransformGizmoScreenTriangles(frameData, inputs);
|
||||
AppendTransformGizmoHandleRecords(frameData, inputs);
|
||||
return frameData;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
186
editor/src/Viewport/SceneViewportOverlayHitTester.h
Normal file
@@ -0,0 +1,186 @@
|
||||
#pragma once
|
||||
|
||||
#include "SceneViewportEditorOverlayData.h"
|
||||
#include "SceneViewportMath.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace Detail {
|
||||
|
||||
inline bool IsPointInsideSceneViewportScreenRect(
|
||||
const Math::Vector2& point,
|
||||
const Math::Vector2& center,
|
||||
const Math::Vector2& halfSize) {
|
||||
return std::abs(point.x - center.x) <= halfSize.x &&
|
||||
std::abs(point.y - center.y) <= halfSize.y;
|
||||
}
|
||||
|
||||
inline Math::Vector2 ComputeSceneViewportScreenQuadCenter(
|
||||
const std::array<Math::Vector2, 4>& corners) {
|
||||
Math::Vector2 center = Math::Vector2::Zero();
|
||||
for (const Math::Vector2& corner : corners) {
|
||||
center += corner;
|
||||
}
|
||||
return center * 0.25f;
|
||||
}
|
||||
|
||||
inline bool IsPointInsideSceneViewportScreenQuad(
|
||||
const Math::Vector2& point,
|
||||
const std::array<Math::Vector2, 4>& corners) {
|
||||
float previousCross = 0.0f;
|
||||
for (size_t index = 0; index < corners.size(); ++index) {
|
||||
const Math::Vector2 edgeStart = corners[index];
|
||||
const Math::Vector2 edgeEnd = corners[(index + 1u) % corners.size()];
|
||||
const Math::Vector2 edge = edgeEnd - edgeStart;
|
||||
const Math::Vector2 toPoint = point - edgeStart;
|
||||
const float cross = Math::Vector2::Cross(edge, toPoint);
|
||||
if (std::abs(cross) <= Math::EPSILON) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousCross != 0.0f && cross * previousCross < 0.0f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
previousCross = cross;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool TryBuildSceneViewportOverlayHandleHitMetrics(
|
||||
const SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& viewportSize,
|
||||
const SceneViewportOverlayHandleRecord& handleRecord,
|
||||
const Math::Vector2& viewportMousePosition,
|
||||
float& outDistanceSq,
|
||||
float& outDepth) {
|
||||
if (!frameData.overlay.valid ||
|
||||
viewportSize.x <= 1.0f ||
|
||||
viewportSize.y <= 1.0f ||
|
||||
handleRecord.kind == SceneViewportOverlayHandleKind::None) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (handleRecord.shape) {
|
||||
case SceneViewportOverlayHandleShape::WorldRect: {
|
||||
if (handleRecord.sizePixels.x <= Math::EPSILON ||
|
||||
handleRecord.sizePixels.y <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint(
|
||||
frameData.overlay,
|
||||
viewportSize.x,
|
||||
viewportSize.y,
|
||||
handleRecord.worldPosition);
|
||||
if (!projectedPoint.visible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector2 center = projectedPoint.screenPosition;
|
||||
const Math::Vector2 halfSize = handleRecord.sizePixels * 0.5f;
|
||||
if (!IsPointInsideSceneViewportScreenRect(viewportMousePosition, center, halfSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outDistanceSq = (center - viewportMousePosition).SqrMagnitude();
|
||||
outDepth = projectedPoint.ndcDepth;
|
||||
return true;
|
||||
}
|
||||
case SceneViewportOverlayHandleShape::ScreenSegment: {
|
||||
if (handleRecord.hitThicknessPixels <= Math::EPSILON ||
|
||||
(handleRecord.screenEnd - handleRecord.screenStart).SqrMagnitude() <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float maxDistanceSq = handleRecord.hitThicknessPixels * handleRecord.hitThicknessPixels;
|
||||
const float distanceSq = DistanceToSegmentSquared(
|
||||
viewportMousePosition,
|
||||
handleRecord.screenStart,
|
||||
handleRecord.screenEnd);
|
||||
if (distanceSq > maxDistanceSq) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outDistanceSq = distanceSq;
|
||||
outDepth = handleRecord.sortDepth;
|
||||
return true;
|
||||
}
|
||||
case SceneViewportOverlayHandleShape::ScreenRect: {
|
||||
if (handleRecord.screenHalfSize.x <= Math::EPSILON ||
|
||||
handleRecord.screenHalfSize.y <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsPointInsideSceneViewportScreenRect(
|
||||
viewportMousePosition,
|
||||
handleRecord.screenCenter,
|
||||
handleRecord.screenHalfSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outDistanceSq = (handleRecord.screenCenter - viewportMousePosition).SqrMagnitude();
|
||||
outDepth = handleRecord.sortDepth;
|
||||
return true;
|
||||
}
|
||||
case SceneViewportOverlayHandleShape::ScreenQuad: {
|
||||
if (!IsPointInsideSceneViewportScreenQuad(viewportMousePosition, handleRecord.screenQuad)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector2 center = ComputeSceneViewportScreenQuadCenter(handleRecord.screenQuad);
|
||||
outDistanceSq = (center - viewportMousePosition).SqrMagnitude();
|
||||
outDepth = handleRecord.sortDepth;
|
||||
return true;
|
||||
}
|
||||
case SceneViewportOverlayHandleShape::None:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
inline SceneViewportOverlayHandleHitResult HitTestSceneViewportOverlayHandles(
|
||||
const SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& viewportSize,
|
||||
const Math::Vector2& viewportMousePosition) {
|
||||
constexpr float kMetricEpsilon = 0.001f;
|
||||
|
||||
SceneViewportOverlayHandleHitResult result = {};
|
||||
for (const SceneViewportOverlayHandleRecord& handleRecord : frameData.handleRecords) {
|
||||
float distanceSq = 0.0f;
|
||||
float depth = 0.0f;
|
||||
if (!Detail::TryBuildSceneViewportOverlayHandleHitMetrics(
|
||||
frameData,
|
||||
viewportSize,
|
||||
handleRecord,
|
||||
viewportMousePosition,
|
||||
distanceSq,
|
||||
depth)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handleRecord.priority > result.priority ||
|
||||
(handleRecord.priority == result.priority &&
|
||||
(depth + kMetricEpsilon < result.depth ||
|
||||
(std::abs(depth - result.depth) <= kMetricEpsilon && distanceSq < result.distanceSq)))) {
|
||||
result.kind = handleRecord.kind;
|
||||
result.handleId = handleRecord.handleId;
|
||||
result.entityId = handleRecord.entityId;
|
||||
result.priority = handleRecord.priority;
|
||||
result.distanceSq = distanceSq;
|
||||
result.depth = depth;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -1,305 +1,16 @@
|
||||
#include "SceneViewportOverlayRenderer.h"
|
||||
#include "SceneViewportOrientationGizmo.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include "SceneViewportOrientationGizmo.h"
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kMoveGizmoArrowLength = 14.0f;
|
||||
constexpr float kMoveGizmoArrowHalfWidth = 7.0f;
|
||||
|
||||
ImU32 ToImGuiColor(const Math::Color& color) {
|
||||
const auto toChannel = [](float value) -> int {
|
||||
return static_cast<int>(std::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
};
|
||||
|
||||
return IM_COL32(
|
||||
toChannel(color.r),
|
||||
toChannel(color.g),
|
||||
toChannel(color.b),
|
||||
toChannel(color.a));
|
||||
}
|
||||
|
||||
Math::Color WithAlpha(const Math::Color& color, float alpha) {
|
||||
return Math::Color(color.r, color.g, color.b, alpha);
|
||||
}
|
||||
|
||||
Math::Color LerpColor(const Math::Color& a, const Math::Color& b, float t) {
|
||||
return Math::Color(
|
||||
a.r + (b.r - a.r) * t,
|
||||
a.g + (b.g - a.g) * t,
|
||||
a.b + (b.b - a.b) * t,
|
||||
a.a + (b.a - a.a) * t);
|
||||
}
|
||||
|
||||
ImVec2 NormalizeImVec2(const ImVec2& value, const ImVec2& fallback = ImVec2(1.0f, 0.0f)) {
|
||||
const float lengthSq = value.x * value.x + value.y * value.y;
|
||||
if (lengthSq <= 1e-6f) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const float inverseLength = 1.0f / std::sqrt(lengthSq);
|
||||
return ImVec2(value.x * inverseLength, value.y * inverseLength);
|
||||
}
|
||||
|
||||
void DrawSceneMoveGizmoPlane(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportMoveGizmoPlaneDrawData& plane) {
|
||||
if (drawList == nullptr || !plane.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImVec2 points[4] = {};
|
||||
for (size_t index = 0; index < plane.corners.size(); ++index) {
|
||||
points[index] = ImVec2(
|
||||
viewportMin.x + plane.corners[index].x,
|
||||
viewportMin.y + plane.corners[index].y);
|
||||
}
|
||||
|
||||
drawList->AddConvexPolyFilled(points, 4, ToImGuiColor(plane.fillColor));
|
||||
drawList->AddPolyline(
|
||||
points,
|
||||
4,
|
||||
ToImGuiColor(plane.outlineColor),
|
||||
true,
|
||||
plane.active ? 2.6f : (plane.hovered ? 2.0f : 1.4f));
|
||||
}
|
||||
|
||||
void DrawSceneMoveGizmoAxis(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportMoveGizmoHandleDrawData& handle) {
|
||||
if (drawList == nullptr || !handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImU32 color = ToImGuiColor(handle.color);
|
||||
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f);
|
||||
const ImVec2 start(viewportMin.x + handle.start.x, viewportMin.y + handle.start.y);
|
||||
const ImVec2 end(viewportMin.x + handle.end.x, viewportMin.y + handle.end.y);
|
||||
const ImVec2 direction = NormalizeImVec2(ImVec2(end.x - start.x, end.y - start.y));
|
||||
const ImVec2 normal(-direction.y, direction.x);
|
||||
const ImVec2 arrowBase(
|
||||
end.x - direction.x * kMoveGizmoArrowLength,
|
||||
end.y - direction.y * kMoveGizmoArrowLength);
|
||||
const ImVec2 arrowLeft(
|
||||
arrowBase.x + normal.x * kMoveGizmoArrowHalfWidth,
|
||||
arrowBase.y + normal.y * kMoveGizmoArrowHalfWidth);
|
||||
const ImVec2 arrowRight(
|
||||
arrowBase.x - normal.x * kMoveGizmoArrowHalfWidth,
|
||||
arrowBase.y - normal.y * kMoveGizmoArrowHalfWidth);
|
||||
|
||||
drawList->AddLine(start, arrowBase, color, thickness);
|
||||
const ImVec2 triangle[3] = { end, arrowLeft, arrowRight };
|
||||
drawList->AddConvexPolyFilled(triangle, 3, color);
|
||||
}
|
||||
|
||||
void DrawSceneRotateGizmoHandle(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportRotateGizmoHandleDrawData& handle,
|
||||
bool frontPass) {
|
||||
if (drawList == nullptr || !handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool isViewHandle = handle.axis == SceneViewportRotateGizmoAxis::View;
|
||||
if (isViewHandle && !frontPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float thickness = handle.active ? 3.6f : (handle.hovered ? 3.0f : 2.1f);
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible || (!isViewHandle && segment.frontFacing != frontPass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Math::Color drawColor = handle.color;
|
||||
if (!isViewHandle && !frontPass) {
|
||||
drawColor = LerpColor(handle.color, Math::Color(0.72f, 0.72f, 0.72f, 1.0f), 0.78f);
|
||||
drawColor = WithAlpha(drawColor, handle.active ? 0.55f : 0.38f);
|
||||
} else if (isViewHandle) {
|
||||
drawColor = WithAlpha(drawColor, handle.active ? 0.95f : (handle.hovered ? 0.88f : 0.78f));
|
||||
}
|
||||
|
||||
drawList->AddLine(
|
||||
ImVec2(viewportMin.x + segment.start.x, viewportMin.y + segment.start.y),
|
||||
ImVec2(viewportMin.x + segment.end.x, viewportMin.y + segment.end.y),
|
||||
ToImGuiColor(drawColor),
|
||||
thickness);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSceneRotateGizmoAngleFill(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportRotateGizmoAngleFillDrawData& angleFill) {
|
||||
if (drawList == nullptr || !angleFill.visible || angleFill.arcPointCount < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImVec2 pivot(viewportMin.x + angleFill.pivot.x, viewportMin.y + angleFill.pivot.y);
|
||||
const ImU32 fillColor = ToImGuiColor(angleFill.fillColor);
|
||||
const ImU32 outlineColor = ToImGuiColor(angleFill.outlineColor);
|
||||
|
||||
ImVec2 fillPoints[kSceneViewportRotateGizmoAngleFillPointCount + 1] = {};
|
||||
fillPoints[0] = pivot;
|
||||
for (size_t index = 0; index < angleFill.arcPointCount; ++index) {
|
||||
fillPoints[index + 1] = ImVec2(
|
||||
viewportMin.x + angleFill.arcPoints[index].x,
|
||||
viewportMin.y + angleFill.arcPoints[index].y);
|
||||
}
|
||||
drawList->AddConvexPolyFilled(
|
||||
fillPoints,
|
||||
static_cast<int>(angleFill.arcPointCount + 1),
|
||||
fillColor);
|
||||
|
||||
for (size_t index = 0; index + 1 < angleFill.arcPointCount; ++index) {
|
||||
drawList->AddLine(
|
||||
ImVec2(viewportMin.x + angleFill.arcPoints[index].x, viewportMin.y + angleFill.arcPoints[index].y),
|
||||
ImVec2(
|
||||
viewportMin.x + angleFill.arcPoints[index + 1].x,
|
||||
viewportMin.y + angleFill.arcPoints[index + 1].y),
|
||||
outlineColor,
|
||||
2.0f);
|
||||
}
|
||||
|
||||
drawList->AddLine(
|
||||
pivot,
|
||||
ImVec2(viewportMin.x + angleFill.arcPoints.front().x, viewportMin.y + angleFill.arcPoints.front().y),
|
||||
outlineColor,
|
||||
1.6f);
|
||||
drawList->AddLine(
|
||||
pivot,
|
||||
ImVec2(
|
||||
viewportMin.x + angleFill.arcPoints[angleFill.arcPointCount - 1].x,
|
||||
viewportMin.y + angleFill.arcPoints[angleFill.arcPointCount - 1].y),
|
||||
outlineColor,
|
||||
1.6f);
|
||||
}
|
||||
|
||||
void DrawSceneScaleGizmoAxis(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportScaleGizmoAxisHandleDrawData& handle) {
|
||||
if (drawList == nullptr || !handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImU32 color = ToImGuiColor(handle.color);
|
||||
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.2f);
|
||||
const ImVec2 start(viewportMin.x + handle.start.x, viewportMin.y + handle.start.y);
|
||||
const ImVec2 capCenter(viewportMin.x + handle.capCenter.x, viewportMin.y + handle.capCenter.y);
|
||||
const ImVec2 direction = NormalizeImVec2(ImVec2(capCenter.x - start.x, capCenter.y - start.y));
|
||||
const ImVec2 lineEnd(
|
||||
capCenter.x - direction.x * handle.capHalfSize,
|
||||
capCenter.y - direction.y * handle.capHalfSize);
|
||||
const ImVec2 capMin(capCenter.x - handle.capHalfSize, capCenter.y - handle.capHalfSize);
|
||||
const ImVec2 capMax(capCenter.x + handle.capHalfSize, capCenter.y + handle.capHalfSize);
|
||||
|
||||
drawList->AddLine(start, lineEnd, color, thickness);
|
||||
drawList->AddRectFilled(capMin, capMax, color, 1.2f);
|
||||
drawList->AddRect(capMin, capMax, IM_COL32(24, 24, 24, 220), 1.2f, 0, handle.active ? 2.0f : 1.0f);
|
||||
}
|
||||
|
||||
void DrawSceneScaleGizmoCenterHandle(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportScaleGizmoCenterHandleDrawData& handle) {
|
||||
if (drawList == nullptr || !handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImVec2 center(viewportMin.x + handle.center.x, viewportMin.y + handle.center.y);
|
||||
const ImVec2 handleMin(center.x - handle.halfSize, center.y - handle.halfSize);
|
||||
const ImVec2 handleMax(center.x + handle.halfSize, center.y + handle.halfSize);
|
||||
drawList->AddRectFilled(handleMin, handleMax, ToImGuiColor(handle.fillColor), 1.2f);
|
||||
drawList->AddRect(
|
||||
handleMin,
|
||||
handleMax,
|
||||
ToImGuiColor(handle.outlineColor),
|
||||
1.2f,
|
||||
0,
|
||||
handle.active ? 2.0f : 1.0f);
|
||||
}
|
||||
|
||||
void DrawSceneMoveGizmo(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportMoveGizmoDrawData& moveGizmo) {
|
||||
if (drawList == nullptr || !moveGizmo.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoPlaneDrawData& plane : moveGizmo.planes) {
|
||||
DrawSceneMoveGizmoPlane(drawList, viewportMin, plane);
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoHandleDrawData& handle : moveGizmo.handles) {
|
||||
DrawSceneMoveGizmoAxis(drawList, viewportMin, handle);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSceneRotateGizmo(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportRotateGizmoDrawData& rotateGizmo) {
|
||||
if (drawList == nullptr || !rotateGizmo.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) {
|
||||
if (handle.axis == SceneViewportRotateGizmoAxis::View) {
|
||||
DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, true);
|
||||
}
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) {
|
||||
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
DrawSceneRotateGizmoAngleFill(drawList, viewportMin, rotateGizmo.angleFill);
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) {
|
||||
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSceneScaleGizmo(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportScaleGizmoDrawData& scaleGizmo) {
|
||||
if (drawList == nullptr || !scaleGizmo.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : scaleGizmo.axisHandles) {
|
||||
DrawSceneScaleGizmoAxis(drawList, viewportMin, handle);
|
||||
}
|
||||
|
||||
DrawSceneScaleGizmoCenterHandle(drawList, viewportMin, scaleGizmo.centerHandle);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DrawSceneViewportOverlay(
|
||||
ImDrawList* drawList,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportMax,
|
||||
const ImVec2& viewportSize,
|
||||
const SceneViewportMoveGizmoDrawData* moveGizmo,
|
||||
const SceneViewportRotateGizmoDrawData* rotateGizmo,
|
||||
const SceneViewportScaleGizmoDrawData* scaleGizmo) {
|
||||
const ImVec2& viewportSize) {
|
||||
if (drawList == nullptr || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) {
|
||||
return;
|
||||
}
|
||||
@@ -308,15 +19,6 @@ void DrawSceneViewportOverlay(
|
||||
if (overlay.valid) {
|
||||
DrawSceneViewportOrientationGizmo(drawList, overlay, viewportMin, viewportMax);
|
||||
}
|
||||
if (moveGizmo != nullptr) {
|
||||
DrawSceneMoveGizmo(drawList, viewportMin, *moveGizmo);
|
||||
}
|
||||
if (rotateGizmo != nullptr) {
|
||||
DrawSceneRotateGizmo(drawList, viewportMin, *rotateGizmo);
|
||||
}
|
||||
if (scaleGizmo != nullptr) {
|
||||
DrawSceneScaleGizmo(drawList, viewportMin, *scaleGizmo);
|
||||
}
|
||||
drawList->PopClipRect();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "IViewportHostService.h"
|
||||
#include "SceneViewportMoveGizmo.h"
|
||||
#include "SceneViewportRotateGizmo.h"
|
||||
#include "SceneViewportScaleGizmo.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
@@ -15,10 +12,7 @@ void DrawSceneViewportOverlay(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportMax,
|
||||
const ImVec2& viewportSize,
|
||||
const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr,
|
||||
const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr,
|
||||
const SceneViewportScaleGizmoDrawData* scaleGizmo = nullptr);
|
||||
const ImVec2& viewportSize);
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -19,6 +19,21 @@ constexpr float kRotateGizmoViewRadiusPixels = 106.0f;
|
||||
constexpr float kRotateGizmoHoverThresholdPixels = 9.0f;
|
||||
constexpr float kRotateGizmoAngleFillMinRadians = 0.01f;
|
||||
|
||||
Math::Vector3 GetBaseRotateAxisVector(SceneViewportRotateGizmoAxis axis) {
|
||||
switch (axis) {
|
||||
case SceneViewportRotateGizmoAxis::X:
|
||||
return Math::Vector3::Right();
|
||||
case SceneViewportRotateGizmoAxis::Y:
|
||||
return Math::Vector3::Up();
|
||||
case SceneViewportRotateGizmoAxis::Z:
|
||||
return Math::Vector3::Forward();
|
||||
case SceneViewportRotateGizmoAxis::View:
|
||||
case SceneViewportRotateGizmoAxis::None:
|
||||
default:
|
||||
return Math::Vector3::Zero();
|
||||
}
|
||||
}
|
||||
|
||||
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
|
||||
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
|
||||
}
|
||||
@@ -30,6 +45,22 @@ bool IsMouseInsideViewport(const SceneViewportRotateGizmoContext& context) {
|
||||
context.mousePosition.y <= context.viewportSize.y;
|
||||
}
|
||||
|
||||
Math::Quaternion ComputeStableWorldRotation(const Components::GameObject* gameObject) {
|
||||
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
|
||||
return Math::Quaternion::Identity();
|
||||
}
|
||||
|
||||
const Components::TransformComponent* transform = gameObject->GetTransform();
|
||||
Math::Quaternion worldRotation = transform->GetLocalRotation();
|
||||
for (const Components::TransformComponent* parent = transform->GetParent();
|
||||
parent != nullptr;
|
||||
parent = parent->GetParent()) {
|
||||
worldRotation = parent->GetLocalRotation() * worldRotation;
|
||||
}
|
||||
|
||||
return worldRotation.Normalized();
|
||||
}
|
||||
|
||||
Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) {
|
||||
switch (axis) {
|
||||
case SceneViewportRotateGizmoAxis::X:
|
||||
@@ -48,14 +79,13 @@ Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) {
|
||||
|
||||
Math::Vector3 GetRotateAxisVector(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const SceneViewportOverlayData& overlay) {
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Quaternion& axisOrientation) {
|
||||
switch (axis) {
|
||||
case SceneViewportRotateGizmoAxis::X:
|
||||
return Math::Vector3::Right();
|
||||
case SceneViewportRotateGizmoAxis::Y:
|
||||
return Math::Vector3::Up();
|
||||
case SceneViewportRotateGizmoAxis::Z:
|
||||
return Math::Vector3::Forward();
|
||||
return NormalizeVector3(axisOrientation * GetBaseRotateAxisVector(axis), GetBaseRotateAxisVector(axis));
|
||||
case SceneViewportRotateGizmoAxis::View:
|
||||
return NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward());
|
||||
case SceneViewportRotateGizmoAxis::None:
|
||||
@@ -67,20 +97,21 @@ Math::Vector3 GetRotateAxisVector(
|
||||
bool GetRotateRingBasis(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Quaternion& axisOrientation,
|
||||
Math::Vector3& outBasisA,
|
||||
Math::Vector3& outBasisB) {
|
||||
switch (axis) {
|
||||
case SceneViewportRotateGizmoAxis::X:
|
||||
outBasisA = Math::Vector3::Up();
|
||||
outBasisB = Math::Vector3::Forward();
|
||||
outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up());
|
||||
outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward());
|
||||
return true;
|
||||
case SceneViewportRotateGizmoAxis::Y:
|
||||
outBasisA = Math::Vector3::Forward();
|
||||
outBasisB = Math::Vector3::Right();
|
||||
outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward());
|
||||
outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right());
|
||||
return true;
|
||||
case SceneViewportRotateGizmoAxis::Z:
|
||||
outBasisA = Math::Vector3::Right();
|
||||
outBasisB = Math::Vector3::Up();
|
||||
outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right());
|
||||
outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up());
|
||||
return true;
|
||||
case SceneViewportRotateGizmoAxis::View:
|
||||
outBasisA = NormalizeVector3(overlay.cameraRight, Math::Vector3::Right());
|
||||
@@ -147,11 +178,12 @@ SceneViewportRotateGizmoAxis GetRotateAxisForIndex(size_t index) {
|
||||
bool TryComputeRingAngleFromWorldDirection(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Quaternion& axisOrientation,
|
||||
const Math::Vector3& directionWorld,
|
||||
float& outAngle) {
|
||||
Math::Vector3 basisA = Math::Vector3::Zero();
|
||||
Math::Vector3 basisB = Math::Vector3::Zero();
|
||||
if (!GetRotateRingBasis(axis, overlay, basisA, basisB)) {
|
||||
if (!GetRotateRingBasis(axis, overlay, axisOrientation, basisA, basisB)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -171,7 +203,7 @@ bool TryComputeRingAngleFromWorldDirection(
|
||||
void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) {
|
||||
BuildDrawData(context);
|
||||
if (m_activeAxis == SceneViewportRotateGizmoAxis::None && IsMouseInsideViewport(context)) {
|
||||
m_hoveredAxis = HitTestAxis(context.mousePosition);
|
||||
m_hoveredAxis = EvaluateHit(context.mousePosition).axis;
|
||||
} else if (m_activeAxis == SceneViewportRotateGizmoAxis::None) {
|
||||
m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
|
||||
} else {
|
||||
@@ -190,8 +222,8 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay);
|
||||
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
|
||||
const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay, context.axisOrientation);
|
||||
if (worldAxis.SqrMagnitude() <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
@@ -225,6 +257,7 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
|
||||
if (!TryComputeRingAngleFromWorldDirection(
|
||||
m_hoveredAxis,
|
||||
context.overlay,
|
||||
context.axisOrientation,
|
||||
startDirection,
|
||||
startRingAngle)) {
|
||||
return false;
|
||||
@@ -238,12 +271,31 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
|
||||
|
||||
m_activeAxis = m_hoveredAxis;
|
||||
m_activeEntityId = context.selectedObject->GetID();
|
||||
m_localSpace = context.localSpace && m_hoveredAxis != SceneViewportRotateGizmoAxis::View;
|
||||
m_rotateAroundSharedPivot = context.rotateAroundSharedPivot;
|
||||
m_activeWorldAxis = worldAxis.Normalized();
|
||||
m_screenSpaceDrag = useScreenSpaceDrag;
|
||||
m_dragPlane = dragPlane;
|
||||
m_dragStartWorldRotation = context.selectedObject->GetTransform()->GetRotation();
|
||||
m_dragStartRingAngle = startRingAngle;
|
||||
m_dragCurrentDeltaRadians = 0.0f;
|
||||
m_dragStartPivotWorldPosition = pivotWorldPosition;
|
||||
m_dragObjects = context.selectedObjects;
|
||||
if (m_dragObjects.empty()) {
|
||||
m_dragObjects.push_back(context.selectedObject);
|
||||
}
|
||||
m_dragStartWorldPositions.clear();
|
||||
m_dragStartWorldRotations.clear();
|
||||
m_dragStartWorldPositions.reserve(m_dragObjects.size());
|
||||
m_dragStartWorldRotations.reserve(m_dragObjects.size());
|
||||
for (Components::GameObject* gameObject : m_dragObjects) {
|
||||
if (gameObject != nullptr && gameObject->GetTransform() != nullptr) {
|
||||
m_dragStartWorldPositions.push_back(gameObject->GetTransform()->GetPosition());
|
||||
m_dragStartWorldRotations.push_back(gameObject->GetTransform()->GetRotation());
|
||||
} else {
|
||||
m_dragStartWorldPositions.push_back(Math::Vector3::Zero());
|
||||
m_dragStartWorldRotations.push_back(Math::Quaternion::Identity());
|
||||
}
|
||||
}
|
||||
RefreshHandleState();
|
||||
return true;
|
||||
}
|
||||
@@ -251,7 +303,10 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
|
||||
void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& context) {
|
||||
if (m_activeAxis == SceneViewportRotateGizmoAxis::None ||
|
||||
context.selectedObject == nullptr ||
|
||||
context.selectedObject->GetID() != m_activeEntityId) {
|
||||
context.selectedObject->GetID() != m_activeEntityId ||
|
||||
m_dragObjects.empty() ||
|
||||
m_dragObjects.size() != m_dragStartWorldPositions.size() ||
|
||||
m_dragObjects.size() != m_dragStartWorldRotations.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -275,7 +330,7 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = m_dragStartPivotWorldPosition;
|
||||
const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance);
|
||||
const Math::Vector3 currentDirection = Math::Vector3::ProjectOnPlane(hitPoint - pivotWorldPosition, m_activeWorldAxis);
|
||||
if (currentDirection.SqrMagnitude() <= Math::EPSILON) {
|
||||
@@ -285,6 +340,7 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
|
||||
if (!TryComputeRingAngleFromWorldDirection(
|
||||
m_activeAxis,
|
||||
context.overlay,
|
||||
context.axisOrientation,
|
||||
currentDirection,
|
||||
currentRingAngle)) {
|
||||
return;
|
||||
@@ -293,9 +349,37 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
|
||||
|
||||
const float deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle);
|
||||
m_dragCurrentDeltaRadians = deltaRadians;
|
||||
const Math::Quaternion deltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians);
|
||||
context.selectedObject->GetTransform()->SetRotation(deltaRotation * m_dragStartWorldRotation);
|
||||
BuildDrawData(context);
|
||||
const Math::Quaternion worldDeltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians);
|
||||
const Math::Vector3 localAxis = GetBaseRotateAxisVector(m_activeAxis);
|
||||
const Math::Quaternion localDeltaRotation =
|
||||
localAxis.SqrMagnitude() > Math::EPSILON
|
||||
? Math::Quaternion::FromAxisAngle(localAxis, deltaRadians)
|
||||
: Math::Quaternion::Identity();
|
||||
for (size_t index = 0; index < m_dragObjects.size(); ++index) {
|
||||
Components::GameObject* gameObject = m_dragObjects[index];
|
||||
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m_rotateAroundSharedPivot) {
|
||||
gameObject->GetTransform()->SetPosition(
|
||||
m_dragStartPivotWorldPosition +
|
||||
worldDeltaRotation * (m_dragStartWorldPositions[index] - m_dragStartPivotWorldPosition));
|
||||
} else {
|
||||
gameObject->GetTransform()->SetPosition(m_dragStartWorldPositions[index]);
|
||||
}
|
||||
if (m_localSpace && m_activeAxis != SceneViewportRotateGizmoAxis::View) {
|
||||
gameObject->GetTransform()->SetRotation(m_dragStartWorldRotations[index] * localDeltaRotation);
|
||||
} else {
|
||||
gameObject->GetTransform()->SetRotation(worldDeltaRotation * m_dragStartWorldRotations[index]);
|
||||
}
|
||||
}
|
||||
SceneViewportRotateGizmoContext drawContext = context;
|
||||
drawContext.pivotWorldPosition = m_dragStartPivotWorldPosition;
|
||||
if (drawContext.localSpace && drawContext.selectedObject != nullptr) {
|
||||
drawContext.axisOrientation = ComputeStableWorldRotation(drawContext.selectedObject);
|
||||
}
|
||||
BuildDrawData(drawContext);
|
||||
m_hoveredAxis = m_activeAxis;
|
||||
RefreshHandleState();
|
||||
}
|
||||
@@ -312,10 +396,15 @@ void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) {
|
||||
m_activeAxis = SceneViewportRotateGizmoAxis::None;
|
||||
m_activeEntityId = 0;
|
||||
m_screenSpaceDrag = false;
|
||||
m_localSpace = false;
|
||||
m_rotateAroundSharedPivot = false;
|
||||
m_activeWorldAxis = Math::Vector3::Zero();
|
||||
m_dragStartWorldRotation = Math::Quaternion::Identity();
|
||||
m_dragStartRingAngle = 0.0f;
|
||||
m_dragCurrentDeltaRadians = 0.0f;
|
||||
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
m_dragObjects.clear();
|
||||
m_dragStartWorldPositions.clear();
|
||||
m_dragStartWorldRotations.clear();
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
@@ -327,10 +416,15 @@ void SceneViewportRotateGizmo::CancelDrag(IUndoManager* undoManager) {
|
||||
m_activeAxis = SceneViewportRotateGizmoAxis::None;
|
||||
m_activeEntityId = 0;
|
||||
m_screenSpaceDrag = false;
|
||||
m_localSpace = false;
|
||||
m_rotateAroundSharedPivot = false;
|
||||
m_activeWorldAxis = Math::Vector3::Zero();
|
||||
m_dragStartWorldRotation = Math::Quaternion::Identity();
|
||||
m_dragStartRingAngle = 0.0f;
|
||||
m_dragCurrentDeltaRadians = 0.0f;
|
||||
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
m_dragObjects.clear();
|
||||
m_dragStartWorldPositions.clear();
|
||||
m_dragStartWorldRotations.clear();
|
||||
m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
|
||||
RefreshHandleState();
|
||||
}
|
||||
@@ -351,18 +445,57 @@ const SceneViewportRotateGizmoDrawData& SceneViewportRotateGizmo::GetDrawData()
|
||||
return m_drawData;
|
||||
}
|
||||
|
||||
SceneViewportRotateGizmoHitResult SceneViewportRotateGizmo::EvaluateHit(const Math::Vector2& mousePosition) const {
|
||||
SceneViewportRotateGizmoHitResult result = {};
|
||||
if (!m_drawData.visible) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const float hoverThresholdSq = kRotateGizmoHoverThresholdPixels * kRotateGizmoHoverThresholdPixels;
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible ||
|
||||
(handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end);
|
||||
if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.axis = handle.axis;
|
||||
result.distanceSq = distanceSq;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void SceneViewportRotateGizmo::SetHoveredHandle(SceneViewportRotateGizmoAxis axis) {
|
||||
if (m_activeAxis != SceneViewportRotateGizmoAxis::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_hoveredAxis = axis;
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoContext& context) {
|
||||
m_drawData = {};
|
||||
|
||||
const Components::GameObject* selectedObject = context.selectedObject;
|
||||
if (selectedObject == nullptr ||
|
||||
if ((context.selectedObject == nullptr && context.selectedObjects.empty()) ||
|
||||
!context.overlay.valid ||
|
||||
context.viewportSize.x <= 1.0f ||
|
||||
context.viewportSize.y <= 1.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
|
||||
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
@@ -383,6 +516,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
|
||||
m_drawData.visible = true;
|
||||
m_drawData.pivot = projectedPivot.screenPosition;
|
||||
const bool hasActiveDragFeedback =
|
||||
!context.localSpace &&
|
||||
m_activeAxis != SceneViewportRotateGizmoAxis::None &&
|
||||
m_activeAxis != SceneViewportRotateGizmoAxis::View &&
|
||||
std::abs(m_dragCurrentDeltaRadians) > Math::EPSILON;
|
||||
@@ -398,7 +532,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
|
||||
|
||||
Math::Vector3 basisA = Math::Vector3::Zero();
|
||||
Math::Vector3 basisB = Math::Vector3::Zero();
|
||||
if (!GetRotateRingBasis(handle.axis, context.overlay, basisA, basisB)) {
|
||||
if (!GetRotateRingBasis(handle.axis, context.overlay, context.axisOrientation, basisA, basisB)) {
|
||||
continue;
|
||||
}
|
||||
if (hasActiveDragFeedback && handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
@@ -468,7 +602,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
|
||||
|
||||
Math::Vector3 basisA = Math::Vector3::Zero();
|
||||
Math::Vector3 basisB = Math::Vector3::Zero();
|
||||
if (GetRotateRingBasis(m_activeAxis, context.overlay, basisA, basisB)) {
|
||||
if (GetRotateRingBasis(m_activeAxis, context.overlay, context.axisOrientation, basisA, basisB)) {
|
||||
const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(m_activeAxis);
|
||||
const float sweepRadians = NormalizeSignedAngleRadians(m_dragCurrentDeltaRadians);
|
||||
const float sweepAbs = std::abs(sweepRadians);
|
||||
@@ -519,39 +653,6 @@ void SceneViewportRotateGizmo::RefreshHandleState() {
|
||||
}
|
||||
}
|
||||
|
||||
SceneViewportRotateGizmoAxis SceneViewportRotateGizmo::HitTestAxis(const Math::Vector2& mousePosition) const {
|
||||
if (!m_drawData.visible) {
|
||||
return SceneViewportRotateGizmoAxis::None;
|
||||
}
|
||||
|
||||
const float hoverThresholdSq = kRotateGizmoHoverThresholdPixels * kRotateGizmoHoverThresholdPixels;
|
||||
SceneViewportRotateGizmoAxis bestAxis = SceneViewportRotateGizmoAxis::None;
|
||||
float bestDistanceSq = hoverThresholdSq;
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible ||
|
||||
(handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end);
|
||||
if (distanceSq > bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestAxis = handle.axis;
|
||||
}
|
||||
}
|
||||
|
||||
return bestAxis;
|
||||
}
|
||||
|
||||
bool SceneViewportRotateGizmo::TryGetClosestRingAngle(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const Math::Vector2& mousePosition,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Components {
|
||||
@@ -71,6 +72,20 @@ struct SceneViewportRotateGizmoContext {
|
||||
Math::Vector2 viewportSize = Math::Vector2::Zero();
|
||||
Math::Vector2 mousePosition = Math::Vector2::Zero();
|
||||
Components::GameObject* selectedObject = nullptr;
|
||||
std::vector<Components::GameObject*> selectedObjects = {};
|
||||
Math::Vector3 pivotWorldPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion axisOrientation = Math::Quaternion::Identity();
|
||||
bool localSpace = false;
|
||||
bool rotateAroundSharedPivot = false;
|
||||
};
|
||||
|
||||
struct SceneViewportRotateGizmoHitResult {
|
||||
SceneViewportRotateGizmoAxis axis = SceneViewportRotateGizmoAxis::None;
|
||||
float distanceSq = Math::FLOAT_MAX;
|
||||
|
||||
bool HasHit() const {
|
||||
return axis != SceneViewportRotateGizmoAxis::None;
|
||||
}
|
||||
};
|
||||
|
||||
class SceneViewportRotateGizmo {
|
||||
@@ -85,11 +100,12 @@ public:
|
||||
bool IsActive() const;
|
||||
uint64_t GetActiveEntityId() const;
|
||||
const SceneViewportRotateGizmoDrawData& GetDrawData() const;
|
||||
SceneViewportRotateGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const;
|
||||
void SetHoveredHandle(SceneViewportRotateGizmoAxis axis);
|
||||
|
||||
private:
|
||||
void BuildDrawData(const SceneViewportRotateGizmoContext& context);
|
||||
void RefreshHandleState();
|
||||
SceneViewportRotateGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const;
|
||||
bool TryGetClosestRingAngle(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const Math::Vector2& mousePosition,
|
||||
@@ -101,11 +117,16 @@ private:
|
||||
SceneViewportRotateGizmoAxis m_activeAxis = SceneViewportRotateGizmoAxis::None;
|
||||
uint64_t m_activeEntityId = 0;
|
||||
bool m_screenSpaceDrag = false;
|
||||
bool m_localSpace = false;
|
||||
bool m_rotateAroundSharedPivot = false;
|
||||
Math::Vector3 m_activeWorldAxis = Math::Vector3::Zero();
|
||||
Math::Plane m_dragPlane = {};
|
||||
Math::Quaternion m_dragStartWorldRotation = Math::Quaternion::Identity();
|
||||
float m_dragStartRingAngle = 0.0f;
|
||||
float m_dragCurrentDeltaRadians = 0.0f;
|
||||
Math::Vector3 m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
std::vector<Components::GameObject*> m_dragObjects = {};
|
||||
std::vector<Math::Vector3> m_dragStartWorldPositions = {};
|
||||
std::vector<Math::Quaternion> m_dragStartWorldRotations = {};
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
|
||||
@@ -130,7 +130,7 @@ float ComputeVisualScaleFactor(float current, float start) {
|
||||
void SceneViewportScaleGizmo::Update(const SceneViewportScaleGizmoContext& context) {
|
||||
BuildDrawData(context);
|
||||
if (m_activeHandle == SceneViewportScaleGizmoHandle::None && IsMouseInsideViewport(context)) {
|
||||
m_hoveredHandle = HitTestHandle(context.mousePosition);
|
||||
m_hoveredHandle = EvaluateHit(context.mousePosition).handle;
|
||||
} else if (m_activeHandle == SceneViewportScaleGizmoHandle::None) {
|
||||
m_hoveredHandle = SceneViewportScaleGizmoHandle::None;
|
||||
} else {
|
||||
@@ -158,7 +158,7 @@ bool SceneViewportScaleGizmo::TryBeginDrag(const SceneViewportScaleGizmoContext&
|
||||
|
||||
activeScreenDirection = handle->end - handle->start;
|
||||
if (activeScreenDirection.SqrMagnitude() <= Math::EPSILON) {
|
||||
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
|
||||
if (!ProjectSceneViewportAxisDirectionAtPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
@@ -310,6 +310,68 @@ const SceneViewportScaleGizmoDrawData& SceneViewportScaleGizmo::GetDrawData() co
|
||||
return m_drawData;
|
||||
}
|
||||
|
||||
SceneViewportScaleGizmoHitResult SceneViewportScaleGizmo::EvaluateHit(const Math::Vector2& mousePosition) const {
|
||||
SceneViewportScaleGizmoHitResult result = {};
|
||||
if (!m_drawData.visible) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (m_drawData.centerHandle.visible &&
|
||||
IsPointInsideSquare(
|
||||
mousePosition,
|
||||
m_drawData.centerHandle.center,
|
||||
m_drawData.centerHandle.halfSize + 2.0f)) {
|
||||
result.handle = SceneViewportScaleGizmoHandle::Uniform;
|
||||
result.distanceSq = (m_drawData.centerHandle.center - mousePosition).SqrMagnitude();
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) {
|
||||
if (!handle.visible ||
|
||||
!IsPointInsideSquare(mousePosition, handle.capCenter, handle.capHalfSize + 2.0f)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = (handle.capCenter - mousePosition).SqrMagnitude();
|
||||
if (distanceSq >= result.distanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.handle = handle.handle;
|
||||
result.distanceSq = distanceSq;
|
||||
}
|
||||
|
||||
if (result.handle != SceneViewportScaleGizmoHandle::None) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const float hoverThresholdSq = kScaleGizmoHoverThresholdPixels * kScaleGizmoHoverThresholdPixels;
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end);
|
||||
if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.handle = handle.handle;
|
||||
result.distanceSq = distanceSq;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void SceneViewportScaleGizmo::SetHoveredHandle(SceneViewportScaleGizmoHandle handle) {
|
||||
if (m_activeHandle != SceneViewportScaleGizmoHandle::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_hoveredHandle = handle;
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
void SceneViewportScaleGizmo::BuildDrawData(const SceneViewportScaleGizmoContext& context) {
|
||||
m_drawData = {};
|
||||
|
||||
@@ -321,7 +383,7 @@ void SceneViewportScaleGizmo::BuildDrawData(const SceneViewportScaleGizmoContext
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
|
||||
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
@@ -435,58 +497,6 @@ void SceneViewportScaleGizmo::RefreshHandleState() {
|
||||
m_drawData.centerHandle.active ? 1.0f : (m_drawData.centerHandle.hovered ? 0.96f : 0.88f));
|
||||
}
|
||||
|
||||
SceneViewportScaleGizmoHandle SceneViewportScaleGizmo::HitTestHandle(const Math::Vector2& mousePosition) const {
|
||||
if (!m_drawData.visible) {
|
||||
return SceneViewportScaleGizmoHandle::None;
|
||||
}
|
||||
|
||||
if (m_drawData.centerHandle.visible &&
|
||||
IsPointInsideSquare(
|
||||
mousePosition,
|
||||
m_drawData.centerHandle.center,
|
||||
m_drawData.centerHandle.halfSize + 2.0f)) {
|
||||
return SceneViewportScaleGizmoHandle::Uniform;
|
||||
}
|
||||
|
||||
SceneViewportScaleGizmoHandle bestHandle = SceneViewportScaleGizmoHandle::None;
|
||||
float bestDistanceSq = Math::FLOAT_MAX;
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) {
|
||||
if (!handle.visible ||
|
||||
!IsPointInsideSquare(mousePosition, handle.capCenter, handle.capHalfSize + 2.0f)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = (handle.capCenter - mousePosition).SqrMagnitude();
|
||||
if (distanceSq >= bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestHandle = handle.handle;
|
||||
}
|
||||
|
||||
if (bestHandle != SceneViewportScaleGizmoHandle::None) {
|
||||
return bestHandle;
|
||||
}
|
||||
|
||||
bestDistanceSq = kScaleGizmoHoverThresholdPixels * kScaleGizmoHoverThresholdPixels;
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end);
|
||||
if (distanceSq > bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestHandle = handle.handle;
|
||||
}
|
||||
|
||||
return bestHandle;
|
||||
}
|
||||
|
||||
const SceneViewportScaleGizmoAxisHandleDrawData* SceneViewportScaleGizmo::FindAxisHandleDrawData(
|
||||
SceneViewportScaleGizmoHandle handle) const {
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& drawHandle : m_drawData.axisHandles) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "IViewportHostService.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
@@ -59,9 +60,20 @@ struct SceneViewportScaleGizmoContext {
|
||||
Math::Vector2 viewportSize = Math::Vector2::Zero();
|
||||
Math::Vector2 mousePosition = Math::Vector2::Zero();
|
||||
Components::GameObject* selectedObject = nullptr;
|
||||
Math::Vector3 pivotWorldPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion axisOrientation = Math::Quaternion::Identity();
|
||||
bool uniformOnly = false;
|
||||
};
|
||||
|
||||
struct SceneViewportScaleGizmoHitResult {
|
||||
SceneViewportScaleGizmoHandle handle = SceneViewportScaleGizmoHandle::None;
|
||||
float distanceSq = Math::FLOAT_MAX;
|
||||
|
||||
bool HasHit() const {
|
||||
return handle != SceneViewportScaleGizmoHandle::None;
|
||||
}
|
||||
};
|
||||
|
||||
class SceneViewportScaleGizmo {
|
||||
public:
|
||||
void Update(const SceneViewportScaleGizmoContext& context);
|
||||
@@ -74,11 +86,12 @@ public:
|
||||
bool IsActive() const;
|
||||
uint64_t GetActiveEntityId() const;
|
||||
const SceneViewportScaleGizmoDrawData& GetDrawData() const;
|
||||
SceneViewportScaleGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const;
|
||||
void SetHoveredHandle(SceneViewportScaleGizmoHandle handle);
|
||||
|
||||
private:
|
||||
void BuildDrawData(const SceneViewportScaleGizmoContext& context);
|
||||
void RefreshHandleState();
|
||||
SceneViewportScaleGizmoHandle HitTestHandle(const Math::Vector2& mousePosition) const;
|
||||
const SceneViewportScaleGizmoAxisHandleDrawData* FindAxisHandleDrawData(
|
||||
SceneViewportScaleGizmoHandle handle) const;
|
||||
|
||||
|
||||
323
editor/src/Viewport/SceneViewportTransformGizmoFrameBuilder.h
Normal file
@@ -0,0 +1,323 @@
|
||||
#pragma once
|
||||
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "Core/ISelectionManager.h"
|
||||
#include "SceneViewportEditorOverlayData.h"
|
||||
#include "SceneViewportMoveGizmo.h"
|
||||
#include "SceneViewportRotateGizmo.h"
|
||||
#include "SceneViewportScaleGizmo.h"
|
||||
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
enum class SceneViewportActiveGizmoKind : uint8_t {
|
||||
None = 0,
|
||||
Move,
|
||||
Rotate,
|
||||
Scale
|
||||
};
|
||||
|
||||
struct SceneViewportSelectionGizmoState {
|
||||
Components::GameObject* primaryObject = nullptr;
|
||||
std::vector<Components::GameObject*> selectedObjects = {};
|
||||
Math::Vector3 pivotWorldPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion primaryWorldRotation = Math::Quaternion::Identity();
|
||||
};
|
||||
|
||||
struct SceneViewportTransformGizmoFrameState {
|
||||
SceneViewportOverlayData overlay = {};
|
||||
SceneViewportSelectionGizmoState selectionState = {};
|
||||
SceneViewportMoveGizmoContext moveContext = {};
|
||||
SceneViewportRotateGizmoContext rotateContext = {};
|
||||
SceneViewportScaleGizmoContext scaleContext = {};
|
||||
SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None;
|
||||
};
|
||||
|
||||
inline SceneViewportActiveGizmoKind GetActiveSceneViewportGizmoKind(
|
||||
const SceneViewportMoveGizmo& moveGizmo,
|
||||
const SceneViewportRotateGizmo& rotateGizmo,
|
||||
const SceneViewportScaleGizmo& scaleGizmo) {
|
||||
if (moveGizmo.IsActive()) {
|
||||
return SceneViewportActiveGizmoKind::Move;
|
||||
}
|
||||
if (rotateGizmo.IsActive()) {
|
||||
return SceneViewportActiveGizmoKind::Rotate;
|
||||
}
|
||||
if (scaleGizmo.IsActive()) {
|
||||
return SceneViewportActiveGizmoKind::Scale;
|
||||
}
|
||||
|
||||
return SceneViewportActiveGizmoKind::None;
|
||||
}
|
||||
|
||||
inline Math::Quaternion ComputeStableWorldRotation(const Components::GameObject* gameObject) {
|
||||
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
|
||||
return Math::Quaternion::Identity();
|
||||
}
|
||||
|
||||
const auto* transform = gameObject->GetTransform();
|
||||
Math::Quaternion worldRotation = transform->GetLocalRotation();
|
||||
for (const auto* parent = transform->GetParent();
|
||||
parent != nullptr;
|
||||
parent = parent->GetParent()) {
|
||||
worldRotation = parent->GetLocalRotation() * worldRotation;
|
||||
}
|
||||
|
||||
return worldRotation.Normalized();
|
||||
}
|
||||
|
||||
inline Math::Vector3 GetGameObjectPivotWorldPosition(const Components::GameObject* gameObject) {
|
||||
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
|
||||
return Math::Vector3::Zero();
|
||||
}
|
||||
|
||||
return gameObject->GetTransform()->GetPosition();
|
||||
}
|
||||
|
||||
inline Math::Vector3 GetGameObjectCenterWorldPosition(const Components::GameObject* gameObject) {
|
||||
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
|
||||
return Math::Vector3::Zero();
|
||||
}
|
||||
|
||||
if (auto* meshFilter = gameObject->GetComponent<Components::MeshFilterComponent>()) {
|
||||
if (Resources::Mesh* mesh = meshFilter->GetMesh();
|
||||
mesh != nullptr && mesh->IsValid()) {
|
||||
return gameObject->GetTransform()->TransformPoint(mesh->GetBounds().center);
|
||||
}
|
||||
}
|
||||
|
||||
return gameObject->GetTransform()->GetPosition();
|
||||
}
|
||||
|
||||
inline SceneViewportSelectionGizmoState BuildSceneViewportSelectionGizmoState(
|
||||
IEditorContext& context,
|
||||
bool useCenterPivot) {
|
||||
SceneViewportSelectionGizmoState state = {};
|
||||
const uint64_t primaryEntityId = context.GetSelectionManager().GetSelectedEntity();
|
||||
if (primaryEntityId != 0) {
|
||||
state.primaryObject = context.GetSceneManager().GetEntity(primaryEntityId);
|
||||
}
|
||||
|
||||
const std::vector<uint64_t>& selectedEntities = context.GetSelectionManager().GetSelectedEntities();
|
||||
state.selectedObjects.reserve(selectedEntities.size());
|
||||
for (uint64_t entityId : selectedEntities) {
|
||||
if (entityId == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (auto* gameObject = context.GetSceneManager().GetEntity(entityId)) {
|
||||
state.selectedObjects.push_back(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.primaryObject == nullptr && !state.selectedObjects.empty()) {
|
||||
state.primaryObject = state.selectedObjects.back();
|
||||
}
|
||||
if (state.primaryObject != nullptr && state.selectedObjects.empty()) {
|
||||
state.selectedObjects.push_back(state.primaryObject);
|
||||
}
|
||||
if (state.primaryObject != nullptr) {
|
||||
state.primaryWorldRotation = ComputeStableWorldRotation(state.primaryObject);
|
||||
}
|
||||
if (state.selectedObjects.empty()) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (useCenterPivot) {
|
||||
Math::Vector3 centerSum = Math::Vector3::Zero();
|
||||
for (const auto* gameObject : state.selectedObjects) {
|
||||
centerSum += GetGameObjectCenterWorldPosition(gameObject);
|
||||
}
|
||||
state.pivotWorldPosition = centerSum / static_cast<float>(state.selectedObjects.size());
|
||||
} else {
|
||||
state.pivotWorldPosition = GetGameObjectPivotWorldPosition(state.primaryObject);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
inline SceneViewportMoveGizmoContext BuildMoveGizmoContext(
|
||||
const SceneViewportSelectionGizmoState& selectionState,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector2& viewportSize,
|
||||
const Math::Vector2& mousePosition,
|
||||
bool localSpace) {
|
||||
SceneViewportMoveGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = viewportSize;
|
||||
gizmoContext.mousePosition = mousePosition;
|
||||
gizmoContext.selectedObject = selectionState.primaryObject;
|
||||
gizmoContext.selectedObjects = selectionState.selectedObjects;
|
||||
gizmoContext.pivotWorldPosition = selectionState.pivotWorldPosition;
|
||||
gizmoContext.axisOrientation = localSpace
|
||||
? selectionState.primaryWorldRotation
|
||||
: Math::Quaternion::Identity();
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
inline SceneViewportRotateGizmoContext BuildRotateGizmoContext(
|
||||
const SceneViewportSelectionGizmoState& selectionState,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector2& viewportSize,
|
||||
const Math::Vector2& mousePosition,
|
||||
bool localSpace,
|
||||
bool rotateAroundSharedPivot) {
|
||||
SceneViewportRotateGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = viewportSize;
|
||||
gizmoContext.mousePosition = mousePosition;
|
||||
gizmoContext.selectedObject = selectionState.primaryObject;
|
||||
gizmoContext.selectedObjects = selectionState.selectedObjects;
|
||||
gizmoContext.pivotWorldPosition = selectionState.pivotWorldPosition;
|
||||
gizmoContext.axisOrientation = localSpace
|
||||
? selectionState.primaryWorldRotation
|
||||
: Math::Quaternion::Identity();
|
||||
gizmoContext.localSpace = localSpace;
|
||||
gizmoContext.rotateAroundSharedPivot = rotateAroundSharedPivot;
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
inline SceneViewportScaleGizmoContext BuildScaleGizmoContext(
|
||||
const SceneViewportSelectionGizmoState& selectionState,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector2& viewportSize,
|
||||
const Math::Vector2& mousePosition,
|
||||
bool localSpace) {
|
||||
SceneViewportScaleGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = viewportSize;
|
||||
gizmoContext.mousePosition = mousePosition;
|
||||
gizmoContext.selectedObject = selectionState.primaryObject;
|
||||
gizmoContext.pivotWorldPosition = selectionState.pivotWorldPosition;
|
||||
gizmoContext.axisOrientation = localSpace
|
||||
? selectionState.primaryWorldRotation
|
||||
: Math::Quaternion::Identity();
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
inline void CancelSceneViewportTransformGizmoDrags(
|
||||
IEditorContext& context,
|
||||
SceneViewportMoveGizmo& moveGizmo,
|
||||
SceneViewportRotateGizmo& rotateGizmo,
|
||||
SceneViewportScaleGizmo& scaleGizmo) {
|
||||
if (moveGizmo.IsActive()) {
|
||||
moveGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
if (rotateGizmo.IsActive()) {
|
||||
rotateGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
if (scaleGizmo.IsActive()) {
|
||||
scaleGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
}
|
||||
|
||||
inline SceneViewportTransformGizmoFrameState RefreshSceneViewportTransformGizmos(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector2& viewportSize,
|
||||
const Math::Vector2& mousePosition,
|
||||
bool useCenterPivot,
|
||||
bool localSpace,
|
||||
bool usingTransformTool,
|
||||
bool showingMoveGizmo,
|
||||
SceneViewportMoveGizmo& moveGizmo,
|
||||
bool showingRotateGizmo,
|
||||
SceneViewportRotateGizmo& rotateGizmo,
|
||||
bool showingScaleGizmo,
|
||||
SceneViewportScaleGizmo& scaleGizmo) {
|
||||
SceneViewportTransformGizmoFrameState state = {};
|
||||
state.overlay = overlay;
|
||||
state.selectionState = BuildSceneViewportSelectionGizmoState(context, useCenterPivot);
|
||||
|
||||
if (showingMoveGizmo) {
|
||||
state.moveContext = BuildMoveGizmoContext(
|
||||
state.selectionState,
|
||||
overlay,
|
||||
viewportSize,
|
||||
mousePosition,
|
||||
localSpace);
|
||||
if (moveGizmo.IsActive() &&
|
||||
(state.moveContext.selectedObject == nullptr ||
|
||||
context.GetSelectionManager().GetSelectedEntity() != moveGizmo.GetActiveEntityId())) {
|
||||
moveGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
} else if (moveGizmo.IsActive()) {
|
||||
moveGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
|
||||
if (showingRotateGizmo) {
|
||||
state.rotateContext = BuildRotateGizmoContext(
|
||||
state.selectionState,
|
||||
overlay,
|
||||
viewportSize,
|
||||
mousePosition,
|
||||
localSpace,
|
||||
useCenterPivot);
|
||||
if (rotateGizmo.IsActive() &&
|
||||
(state.rotateContext.selectedObject == nullptr ||
|
||||
context.GetSelectionManager().GetSelectedEntity() != rotateGizmo.GetActiveEntityId())) {
|
||||
rotateGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
} else if (rotateGizmo.IsActive()) {
|
||||
rotateGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
|
||||
if (showingScaleGizmo) {
|
||||
state.scaleContext = BuildScaleGizmoContext(
|
||||
state.selectionState,
|
||||
overlay,
|
||||
viewportSize,
|
||||
mousePosition,
|
||||
localSpace);
|
||||
state.scaleContext.uniformOnly = usingTransformTool;
|
||||
if (scaleGizmo.IsActive() &&
|
||||
(state.scaleContext.selectedObject == nullptr ||
|
||||
context.GetSelectionManager().GetSelectedEntity() != scaleGizmo.GetActiveEntityId())) {
|
||||
scaleGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
} else if (scaleGizmo.IsActive()) {
|
||||
scaleGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
|
||||
state.activeGizmoKind = GetActiveSceneViewportGizmoKind(moveGizmo, rotateGizmo, scaleGizmo);
|
||||
|
||||
if (showingMoveGizmo) {
|
||||
SceneViewportMoveGizmoContext updateContext = state.moveContext;
|
||||
if (state.activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
state.activeGizmoKind != SceneViewportActiveGizmoKind::Move) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
moveGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingRotateGizmo) {
|
||||
SceneViewportRotateGizmoContext updateContext = state.rotateContext;
|
||||
if (state.activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
state.activeGizmoKind != SceneViewportActiveGizmoKind::Rotate) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
rotateGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingScaleGizmo) {
|
||||
SceneViewportScaleGizmoContext updateContext = state.scaleContext;
|
||||
if (state.activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
state.activeGizmoKind != SceneViewportActiveGizmoKind::Scale) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
scaleGizmo.Update(updateContext);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -166,7 +166,9 @@ inline void ApplySceneViewportRenderRequestSetup(
|
||||
const Rendering::BuiltinPostProcessRequest* builtinPostProcess,
|
||||
Rendering::RenderPassSequence* postPasses,
|
||||
Rendering::CameraRenderRequest& request) {
|
||||
request.preScenePasses = nullptr;
|
||||
request.postScenePasses = nullptr;
|
||||
request.overlayPasses = nullptr;
|
||||
request.objectId = {};
|
||||
request.builtinPostProcess = {};
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "Core/ISelectionManager.h"
|
||||
#include "IViewportHostService.h"
|
||||
#include "Passes/SceneViewportEditorOverlayPass.h"
|
||||
#include "SceneViewportCameraController.h"
|
||||
#include "SceneViewportEditorOverlayData.h"
|
||||
#include "SceneViewportOverlayHandleBuilder.h"
|
||||
#include "SceneViewportOverlayBuilder.h"
|
||||
#include "ViewportHostRenderFlowUtils.h"
|
||||
#include "ViewportHostRenderTargets.h"
|
||||
#include "ViewportObjectIdPicker.h"
|
||||
@@ -12,6 +16,7 @@
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Components/LightComponent.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/RHI/RHIDevice.h>
|
||||
#include <XCEngine/RHI/RHIEnums.h>
|
||||
@@ -24,7 +29,9 @@
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -35,6 +42,133 @@ namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr bool kDebugSceneSelectionMask = false;
|
||||
constexpr uint64_t kSceneViewportOverlaySignatureOffsetBasis = 14695981039346656037ull;
|
||||
constexpr uint64_t kSceneViewportOverlaySignaturePrime = 1099511628211ull;
|
||||
|
||||
void HashSceneViewportOverlayBytes(uint64_t& hash, const void* data, size_t size) {
|
||||
const auto* bytes = static_cast<const uint8_t*>(data);
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
hash ^= static_cast<uint64_t>(bytes[index]);
|
||||
hash *= kSceneViewportOverlaySignaturePrime;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename TValue>
|
||||
void HashSceneViewportOverlayValue(uint64_t& hash, const TValue& value) {
|
||||
HashSceneViewportOverlayBytes(hash, &value, sizeof(TValue));
|
||||
}
|
||||
|
||||
void HashSceneViewportOverlayFloat(uint64_t& hash, float value) {
|
||||
uint32_t bits = 0u;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
HashSceneViewportOverlayValue(hash, bits);
|
||||
}
|
||||
|
||||
void HashSceneViewportOverlayVector3(uint64_t& hash, const Math::Vector3& value) {
|
||||
HashSceneViewportOverlayFloat(hash, value.x);
|
||||
HashSceneViewportOverlayFloat(hash, value.y);
|
||||
HashSceneViewportOverlayFloat(hash, value.z);
|
||||
}
|
||||
|
||||
void HashSceneViewportOverlayQuaternion(uint64_t& hash, const Math::Quaternion& value) {
|
||||
HashSceneViewportOverlayFloat(hash, value.x);
|
||||
HashSceneViewportOverlayFloat(hash, value.y);
|
||||
HashSceneViewportOverlayFloat(hash, value.z);
|
||||
HashSceneViewportOverlayFloat(hash, value.w);
|
||||
}
|
||||
|
||||
void HashSceneViewportOverlayRect(uint64_t& hash, const Math::Rect& value) {
|
||||
HashSceneViewportOverlayFloat(hash, value.x);
|
||||
HashSceneViewportOverlayFloat(hash, value.y);
|
||||
HashSceneViewportOverlayFloat(hash, value.width);
|
||||
HashSceneViewportOverlayFloat(hash, value.height);
|
||||
}
|
||||
|
||||
void HashSceneViewportOverlayTransform(uint64_t& hash, const Components::TransformComponent& transform) {
|
||||
HashSceneViewportOverlayVector3(hash, transform.GetPosition());
|
||||
HashSceneViewportOverlayQuaternion(hash, transform.GetRotation());
|
||||
HashSceneViewportOverlayVector3(hash, transform.GetScale());
|
||||
}
|
||||
|
||||
uint64_t BuildSceneViewEditorOverlayContentSignature(
|
||||
const Components::Scene* scene,
|
||||
const std::vector<uint64_t>& selectedObjectIds) {
|
||||
uint64_t hash = kSceneViewportOverlaySignatureOffsetBasis;
|
||||
|
||||
HashSceneViewportOverlayValue(hash, static_cast<uint64_t>(selectedObjectIds.size()));
|
||||
for (uint64_t entityId : selectedObjectIds) {
|
||||
HashSceneViewportOverlayValue(hash, entityId);
|
||||
}
|
||||
|
||||
if (scene == nullptr) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
for (Components::CameraComponent* camera : scene->FindObjectsOfType<Components::CameraComponent>()) {
|
||||
Components::GameObject* gameObject = camera != nullptr ? camera->GetGameObject() : nullptr;
|
||||
HashSceneViewportOverlayValue(hash, static_cast<uint8_t>(1u));
|
||||
HashSceneViewportOverlayValue(hash, gameObject != nullptr ? gameObject->GetID() : 0ull);
|
||||
HashSceneViewportOverlayValue(hash, camera != nullptr && camera->IsEnabled());
|
||||
HashSceneViewportOverlayValue(hash, gameObject != nullptr && gameObject->IsActiveInHierarchy());
|
||||
if (camera == nullptr ||
|
||||
gameObject == nullptr ||
|
||||
!camera->IsEnabled() ||
|
||||
!gameObject->IsActiveInHierarchy() ||
|
||||
gameObject->GetTransform() == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSceneViewportOverlayTransform(hash, *gameObject->GetTransform());
|
||||
HashSceneViewportOverlayValue(hash, static_cast<uint32_t>(camera->GetProjectionType()));
|
||||
HashSceneViewportOverlayFloat(hash, camera->GetFieldOfView());
|
||||
HashSceneViewportOverlayFloat(hash, camera->GetOrthographicSize());
|
||||
HashSceneViewportOverlayFloat(hash, camera->GetNearClipPlane());
|
||||
HashSceneViewportOverlayFloat(hash, camera->GetFarClipPlane());
|
||||
HashSceneViewportOverlayRect(hash, camera->GetViewportRect());
|
||||
}
|
||||
|
||||
for (Components::LightComponent* light : scene->FindObjectsOfType<Components::LightComponent>()) {
|
||||
Components::GameObject* gameObject = light != nullptr ? light->GetGameObject() : nullptr;
|
||||
HashSceneViewportOverlayValue(hash, static_cast<uint8_t>(2u));
|
||||
HashSceneViewportOverlayValue(hash, gameObject != nullptr ? gameObject->GetID() : 0ull);
|
||||
HashSceneViewportOverlayValue(hash, light != nullptr && light->IsEnabled());
|
||||
HashSceneViewportOverlayValue(hash, gameObject != nullptr && gameObject->IsActiveInHierarchy());
|
||||
if (light == nullptr ||
|
||||
gameObject == nullptr ||
|
||||
!light->IsEnabled() ||
|
||||
!gameObject->IsActiveInHierarchy() ||
|
||||
gameObject->GetTransform() == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSceneViewportOverlayTransform(hash, *gameObject->GetTransform());
|
||||
HashSceneViewportOverlayValue(hash, static_cast<uint32_t>(light->GetLightType()));
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool AreEqualSceneViewportVector3(const Math::Vector3& lhs, const Math::Vector3& rhs) {
|
||||
constexpr float kEpsilon = 1e-4f;
|
||||
return std::abs(lhs.x - rhs.x) <= kEpsilon &&
|
||||
std::abs(lhs.y - rhs.y) <= kEpsilon &&
|
||||
std::abs(lhs.z - rhs.z) <= kEpsilon;
|
||||
}
|
||||
|
||||
bool AreEqualSceneViewportOverlayData(
|
||||
const SceneViewportOverlayData& lhs,
|
||||
const SceneViewportOverlayData& rhs) {
|
||||
constexpr float kEpsilon = 1e-4f;
|
||||
return lhs.valid == rhs.valid &&
|
||||
AreEqualSceneViewportVector3(lhs.cameraPosition, rhs.cameraPosition) &&
|
||||
AreEqualSceneViewportVector3(lhs.cameraForward, rhs.cameraForward) &&
|
||||
AreEqualSceneViewportVector3(lhs.cameraRight, rhs.cameraRight) &&
|
||||
AreEqualSceneViewportVector3(lhs.cameraUp, rhs.cameraUp) &&
|
||||
std::abs(lhs.verticalFovDegrees - rhs.verticalFovDegrees) <= kEpsilon &&
|
||||
std::abs(lhs.nearClipPlane - rhs.nearClipPlane) <= kEpsilon &&
|
||||
std::abs(lhs.farClipPlane - rhs.farClipPlane) <= kEpsilon &&
|
||||
std::abs(lhs.orbitDistance - rhs.orbitDistance) <= kEpsilon;
|
||||
}
|
||||
|
||||
Math::Vector3 GetSceneViewportOrientationAxisVector(SceneViewportOrientationAxis axis) {
|
||||
switch (axis) {
|
||||
@@ -71,7 +205,9 @@ public:
|
||||
entry = {};
|
||||
}
|
||||
|
||||
m_sceneViewportEditorOverlayRenderer.Shutdown();
|
||||
m_sceneViewCamera = {};
|
||||
ResetSceneViewEditorOverlayFrameData();
|
||||
m_sceneViewLastRenderContext = {};
|
||||
m_device = nullptr;
|
||||
m_backend = nullptr;
|
||||
@@ -84,6 +220,8 @@ public:
|
||||
entry.requestedWidth = 0;
|
||||
entry.requestedHeight = 0;
|
||||
}
|
||||
m_sceneViewTransientTransformGizmoOverlay = {};
|
||||
m_sceneViewTransientTransformGizmoInputs = {};
|
||||
}
|
||||
|
||||
EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) override {
|
||||
@@ -212,6 +350,30 @@ public:
|
||||
return data;
|
||||
}
|
||||
|
||||
const SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext& context) override {
|
||||
EnsureSceneViewEditorOverlayFrameData(context);
|
||||
return m_sceneViewEditorOverlayFrameData;
|
||||
}
|
||||
|
||||
const SceneViewportOverlayFrameData& GetSceneViewInteractionOverlayFrameData(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) override {
|
||||
EnsureSceneViewEditorOverlayFrameData(context);
|
||||
m_sceneViewInteractionOverlayFrameData = m_sceneViewEditorOverlayFrameData;
|
||||
AppendSceneViewportOverlayFrameData(
|
||||
m_sceneViewInteractionOverlayFrameData,
|
||||
BuildSceneViewportTransformGizmoOverlayFrameData(overlay, inputs));
|
||||
return m_sceneViewInteractionOverlayFrameData;
|
||||
}
|
||||
|
||||
void SetSceneViewTransientTransformGizmoOverlayData(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) override {
|
||||
m_sceneViewTransientTransformGizmoOverlay = overlay;
|
||||
m_sceneViewTransientTransformGizmoInputs = inputs;
|
||||
}
|
||||
|
||||
void RenderRequestedViewports(
|
||||
IEditorContext& context,
|
||||
const Rendering::RenderContext& renderContext) override {
|
||||
@@ -256,6 +418,7 @@ private:
|
||||
struct SceneViewportRenderState {
|
||||
SceneViewportOverlayData overlay = {};
|
||||
Rendering::BuiltinPostProcessRequest builtinPostProcess = {};
|
||||
SceneViewportOverlayFrameData editorOverlayFrameData = {};
|
||||
std::vector<uint64_t> selectedObjectIds;
|
||||
};
|
||||
|
||||
@@ -365,6 +528,86 @@ private:
|
||||
return BuildViewportColorSurface(entry.renderTargets);
|
||||
}
|
||||
|
||||
void ResetSceneViewEditorOverlayFrameData() {
|
||||
m_sceneViewEditorOverlayFrameData = {};
|
||||
m_sceneViewInteractionOverlayFrameData = {};
|
||||
m_sceneViewEditorOverlayScene = nullptr;
|
||||
m_sceneViewEditorOverlaySelectedObjectIds.clear();
|
||||
m_sceneViewEditorOverlayViewportWidth = 0u;
|
||||
m_sceneViewEditorOverlayViewportHeight = 0u;
|
||||
m_sceneViewEditorOverlayContentSignature = 0u;
|
||||
m_sceneViewEditorOverlayCached = false;
|
||||
}
|
||||
|
||||
void ResolveSceneViewEditorOverlayViewportSize(
|
||||
const ViewportEntry& entry,
|
||||
uint32_t& outWidth,
|
||||
uint32_t& outHeight) const {
|
||||
outWidth = entry.requestedWidth > 0u ? entry.requestedWidth : entry.renderTargets.width;
|
||||
outHeight = entry.requestedHeight > 0u ? entry.requestedHeight : entry.renderTargets.height;
|
||||
}
|
||||
|
||||
bool ShouldRebuildSceneViewEditorOverlayFrameData(
|
||||
const Components::Scene* scene,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight,
|
||||
const std::vector<uint64_t>& selectedObjectIds,
|
||||
uint64_t contentSignature) const {
|
||||
return !m_sceneViewEditorOverlayCached ||
|
||||
m_sceneViewEditorOverlayScene != scene ||
|
||||
m_sceneViewEditorOverlayViewportWidth != viewportWidth ||
|
||||
m_sceneViewEditorOverlayViewportHeight != viewportHeight ||
|
||||
m_sceneViewEditorOverlaySelectedObjectIds != selectedObjectIds ||
|
||||
m_sceneViewEditorOverlayContentSignature != contentSignature ||
|
||||
!AreEqualSceneViewportOverlayData(m_sceneViewEditorOverlayFrameData.overlay, overlay);
|
||||
}
|
||||
|
||||
void EnsureSceneViewEditorOverlayFrameData(IEditorContext& context) {
|
||||
if (!EnsureSceneViewCamera()) {
|
||||
ResetSceneViewEditorOverlayFrameData();
|
||||
return;
|
||||
}
|
||||
|
||||
const ViewportEntry& entry = GetEntry(EditorViewportKind::Scene);
|
||||
uint32_t viewportWidth = 0u;
|
||||
uint32_t viewportHeight = 0u;
|
||||
ResolveSceneViewEditorOverlayViewportSize(entry, viewportWidth, viewportHeight);
|
||||
|
||||
const Components::Scene* scene = context.GetSceneManager().GetScene();
|
||||
const SceneViewportOverlayData overlay = GetSceneViewOverlayData();
|
||||
const std::vector<uint64_t> selectedObjectIds = context.GetSelectionManager().GetSelectedEntities();
|
||||
const uint64_t contentSignature =
|
||||
BuildSceneViewEditorOverlayContentSignature(scene, selectedObjectIds);
|
||||
if (!ShouldRebuildSceneViewEditorOverlayFrameData(
|
||||
scene,
|
||||
overlay,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
selectedObjectIds,
|
||||
contentSignature)) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sceneViewEditorOverlayFrameData = {};
|
||||
m_sceneViewEditorOverlayFrameData.overlay = overlay;
|
||||
if (scene != nullptr && overlay.valid && viewportWidth > 0u && viewportHeight > 0u) {
|
||||
m_sceneViewEditorOverlayFrameData = SceneViewportOverlayBuilder::Build(
|
||||
context,
|
||||
overlay,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
selectedObjectIds);
|
||||
}
|
||||
|
||||
m_sceneViewEditorOverlayScene = scene;
|
||||
m_sceneViewEditorOverlaySelectedObjectIds = selectedObjectIds;
|
||||
m_sceneViewEditorOverlayViewportWidth = viewportWidth;
|
||||
m_sceneViewEditorOverlayViewportHeight = viewportHeight;
|
||||
m_sceneViewEditorOverlayContentSignature = contentSignature;
|
||||
m_sceneViewEditorOverlayCached = true;
|
||||
}
|
||||
|
||||
void ApplyViewportRenderFailure(
|
||||
ViewportEntry& entry,
|
||||
const Rendering::RenderContext& renderContext,
|
||||
@@ -399,6 +642,7 @@ private:
|
||||
}
|
||||
|
||||
outState.selectedObjectIds = context.GetSelectionManager().GetSelectedEntities();
|
||||
outState.editorOverlayFrameData = GetSceneViewEditorOverlayFrameData(context);
|
||||
const SceneViewportBuiltinPostProcessBuildResult builtinPostProcess =
|
||||
BuildSceneViewportBuiltinPostProcess(
|
||||
outState.overlay,
|
||||
@@ -456,6 +700,18 @@ private:
|
||||
&sceneState.builtinPostProcess,
|
||||
nullptr,
|
||||
requests[0]);
|
||||
SceneViewportOverlayFrameData renderOverlayFrameData = sceneState.editorOverlayFrameData;
|
||||
AppendSceneViewportOverlayFrameData(
|
||||
renderOverlayFrameData,
|
||||
BuildSceneViewTransientTransformGizmoOverlayFrameData());
|
||||
Rendering::RenderPassSequence overlayPassSequence = {};
|
||||
if (renderOverlayFrameData.HasOverlayPrimitives()) {
|
||||
overlayPassSequence.AddPass(
|
||||
CreateSceneViewportEditorOverlayPass(
|
||||
m_sceneViewportEditorOverlayRenderer,
|
||||
renderOverlayFrameData));
|
||||
requests[0].overlayPasses = &overlayPassSequence;
|
||||
}
|
||||
requests[0].hasClearColorOverride = true;
|
||||
requests[0].clearColorOverride = Math::Color(0.27f, 0.27f, 0.27f, 1.0f);
|
||||
|
||||
@@ -600,12 +856,29 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
SceneViewportOverlayFrameData BuildSceneViewTransientTransformGizmoOverlayFrameData() const {
|
||||
return BuildSceneViewportTransformGizmoOverlayFrameData(
|
||||
m_sceneViewTransientTransformGizmoOverlay,
|
||||
m_sceneViewTransientTransformGizmoInputs);
|
||||
}
|
||||
|
||||
UI::ImGuiBackendBridge* m_backend = nullptr;
|
||||
RHI::RHIDevice* m_device = nullptr;
|
||||
std::unique_ptr<Rendering::SceneRenderer> m_sceneRenderer;
|
||||
Rendering::RenderContext m_sceneViewLastRenderContext = {};
|
||||
std::array<ViewportEntry, 2> m_entries = {};
|
||||
SceneViewCameraState m_sceneViewCamera;
|
||||
SceneViewportOverlayFrameData m_sceneViewEditorOverlayFrameData = {};
|
||||
SceneViewportOverlayFrameData m_sceneViewInteractionOverlayFrameData = {};
|
||||
SceneViewportOverlayData m_sceneViewTransientTransformGizmoOverlay = {};
|
||||
SceneViewportTransformGizmoHandleBuildInputs m_sceneViewTransientTransformGizmoInputs = {};
|
||||
const Components::Scene* m_sceneViewEditorOverlayScene = nullptr;
|
||||
std::vector<uint64_t> m_sceneViewEditorOverlaySelectedObjectIds = {};
|
||||
uint32_t m_sceneViewEditorOverlayViewportWidth = 0u;
|
||||
uint32_t m_sceneViewEditorOverlayViewportHeight = 0u;
|
||||
uint64_t m_sceneViewEditorOverlayContentSignature = 0u;
|
||||
bool m_sceneViewEditorOverlayCached = false;
|
||||
SceneViewportEditorOverlayPassRenderer m_sceneViewportEditorOverlayRenderer;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
|
||||
@@ -1,12 +1,166 @@
|
||||
#include "Actions/ActionRouting.h"
|
||||
#include "Core/EventBus.h"
|
||||
#include "Core/EditorEvents.h"
|
||||
#include "GameViewPanel.h"
|
||||
#include "ViewportPanelContent.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
void SetKeyState(GameViewInputFrameEvent& event, XCEngine::Input::KeyCode key, bool isDown) {
|
||||
const size_t index = static_cast<size_t>(key);
|
||||
if (index < event.keyDown.size()) {
|
||||
event.keyDown[index] = isDown;
|
||||
}
|
||||
}
|
||||
|
||||
void SetMouseButtonState(GameViewInputFrameEvent& event, XCEngine::Input::MouseButton button, bool isDown) {
|
||||
const size_t index = static_cast<size_t>(button);
|
||||
if (index < event.mouseButtonDown.size()) {
|
||||
event.mouseButtonDown[index] = isDown;
|
||||
}
|
||||
}
|
||||
|
||||
void FillGameViewKeyboardState(const ImGuiIO& io, GameViewInputFrameEvent& event) {
|
||||
using XCEngine::Input::KeyCode;
|
||||
|
||||
const struct KeyMapping {
|
||||
ImGuiKey imguiKey;
|
||||
KeyCode keyCode;
|
||||
} keyMappings[] = {
|
||||
{ImGuiKey_A, KeyCode::A},
|
||||
{ImGuiKey_B, KeyCode::B},
|
||||
{ImGuiKey_C, KeyCode::C},
|
||||
{ImGuiKey_D, KeyCode::D},
|
||||
{ImGuiKey_E, KeyCode::E},
|
||||
{ImGuiKey_F, KeyCode::F},
|
||||
{ImGuiKey_G, KeyCode::G},
|
||||
{ImGuiKey_H, KeyCode::H},
|
||||
{ImGuiKey_I, KeyCode::I},
|
||||
{ImGuiKey_J, KeyCode::J},
|
||||
{ImGuiKey_K, KeyCode::K},
|
||||
{ImGuiKey_L, KeyCode::L},
|
||||
{ImGuiKey_M, KeyCode::M},
|
||||
{ImGuiKey_N, KeyCode::N},
|
||||
{ImGuiKey_O, KeyCode::O},
|
||||
{ImGuiKey_P, KeyCode::P},
|
||||
{ImGuiKey_Q, KeyCode::Q},
|
||||
{ImGuiKey_R, KeyCode::R},
|
||||
{ImGuiKey_S, KeyCode::S},
|
||||
{ImGuiKey_T, KeyCode::T},
|
||||
{ImGuiKey_U, KeyCode::U},
|
||||
{ImGuiKey_V, KeyCode::V},
|
||||
{ImGuiKey_W, KeyCode::W},
|
||||
{ImGuiKey_X, KeyCode::X},
|
||||
{ImGuiKey_Y, KeyCode::Y},
|
||||
{ImGuiKey_Z, KeyCode::Z},
|
||||
{ImGuiKey_0, KeyCode::Zero},
|
||||
{ImGuiKey_1, KeyCode::One},
|
||||
{ImGuiKey_2, KeyCode::Two},
|
||||
{ImGuiKey_3, KeyCode::Three},
|
||||
{ImGuiKey_4, KeyCode::Four},
|
||||
{ImGuiKey_5, KeyCode::Five},
|
||||
{ImGuiKey_6, KeyCode::Six},
|
||||
{ImGuiKey_7, KeyCode::Seven},
|
||||
{ImGuiKey_8, KeyCode::Eight},
|
||||
{ImGuiKey_9, KeyCode::Nine},
|
||||
{ImGuiKey_Space, KeyCode::Space},
|
||||
{ImGuiKey_Tab, KeyCode::Tab},
|
||||
{ImGuiKey_Enter, KeyCode::Enter},
|
||||
{ImGuiKey_Escape, KeyCode::Escape},
|
||||
{ImGuiKey_LeftShift, KeyCode::LeftShift},
|
||||
{ImGuiKey_RightShift, KeyCode::RightShift},
|
||||
{ImGuiKey_LeftCtrl, KeyCode::LeftCtrl},
|
||||
{ImGuiKey_RightCtrl, KeyCode::RightCtrl},
|
||||
{ImGuiKey_LeftAlt, KeyCode::LeftAlt},
|
||||
{ImGuiKey_RightAlt, KeyCode::RightAlt},
|
||||
{ImGuiKey_UpArrow, KeyCode::Up},
|
||||
{ImGuiKey_DownArrow, KeyCode::Down},
|
||||
{ImGuiKey_LeftArrow, KeyCode::Left},
|
||||
{ImGuiKey_RightArrow, KeyCode::Right},
|
||||
{ImGuiKey_Home, KeyCode::Home},
|
||||
{ImGuiKey_End, KeyCode::End},
|
||||
{ImGuiKey_PageUp, KeyCode::PageUp},
|
||||
{ImGuiKey_PageDown, KeyCode::PageDown},
|
||||
{ImGuiKey_Delete, KeyCode::Delete},
|
||||
{ImGuiKey_Backspace, KeyCode::Backspace},
|
||||
{ImGuiKey_F1, KeyCode::F1},
|
||||
{ImGuiKey_F2, KeyCode::F2},
|
||||
{ImGuiKey_F3, KeyCode::F3},
|
||||
{ImGuiKey_F4, KeyCode::F4},
|
||||
{ImGuiKey_F5, KeyCode::F5},
|
||||
{ImGuiKey_F6, KeyCode::F6},
|
||||
{ImGuiKey_F7, KeyCode::F7},
|
||||
{ImGuiKey_F8, KeyCode::F8},
|
||||
{ImGuiKey_F9, KeyCode::F9},
|
||||
{ImGuiKey_F10, KeyCode::F10},
|
||||
{ImGuiKey_F11, KeyCode::F11},
|
||||
{ImGuiKey_F12, KeyCode::F12},
|
||||
{ImGuiKey_Minus, KeyCode::Minus},
|
||||
{ImGuiKey_Equal, KeyCode::Equals},
|
||||
{ImGuiKey_LeftBracket, KeyCode::BracketLeft},
|
||||
{ImGuiKey_RightBracket, KeyCode::BracketRight},
|
||||
{ImGuiKey_Semicolon, KeyCode::Semicolon},
|
||||
{ImGuiKey_Apostrophe, KeyCode::Quote},
|
||||
{ImGuiKey_Comma, KeyCode::Comma},
|
||||
{ImGuiKey_Period, KeyCode::Period},
|
||||
{ImGuiKey_Slash, KeyCode::Slash},
|
||||
{ImGuiKey_Backslash, KeyCode::Backslash},
|
||||
{ImGuiKey_GraveAccent, KeyCode::Backtick},
|
||||
};
|
||||
|
||||
for (const KeyMapping& mapping : keyMappings) {
|
||||
SetKeyState(event, mapping.keyCode, ImGui::IsKeyDown(mapping.imguiKey));
|
||||
}
|
||||
}
|
||||
|
||||
void FillGameViewMouseState(const ImGuiIO& io, GameViewInputFrameEvent& event) {
|
||||
(void)io;
|
||||
SetMouseButtonState(event, XCEngine::Input::MouseButton::Left, ImGui::IsMouseDown(ImGuiMouseButton_Left));
|
||||
SetMouseButtonState(event, XCEngine::Input::MouseButton::Right, ImGui::IsMouseDown(ImGuiMouseButton_Right));
|
||||
SetMouseButtonState(event, XCEngine::Input::MouseButton::Middle, ImGui::IsMouseDown(ImGuiMouseButton_Middle));
|
||||
}
|
||||
|
||||
GameViewInputFrameEvent BuildGameViewInputFrame(const ViewportPanelContentResult& content) {
|
||||
GameViewInputFrameEvent event = {};
|
||||
if (!content.hasViewportArea) {
|
||||
return event;
|
||||
}
|
||||
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
event.hovered = content.hovered;
|
||||
event.focused = content.focused;
|
||||
event.mousePosition = XCEngine::Math::Vector2(
|
||||
io.MousePos.x - content.itemMin.x,
|
||||
io.MousePos.y - content.itemMin.y);
|
||||
event.mouseDelta = XCEngine::Math::Vector2(io.MouseDelta.x, io.MouseDelta.y);
|
||||
event.mouseWheel = content.hovered ? io.MouseWheel : 0.0f;
|
||||
|
||||
if (event.hovered || event.focused) {
|
||||
FillGameViewKeyboardState(io, event);
|
||||
FillGameViewMouseState(io, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
void PublishGameViewInputFrame(IEditorContext* context, const GameViewInputFrameEvent& event) {
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
context->GetEventBus().Publish(event);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GameViewPanel::GameViewPanel() : Panel("Game") {}
|
||||
|
||||
void GameViewPanel::Render() {
|
||||
@@ -14,10 +168,12 @@ void GameViewPanel::Render() {
|
||||
UI::PanelWindowScope panel(m_name.c_str());
|
||||
ImGui::PopStyleVar();
|
||||
if (!panel.IsOpen()) {
|
||||
PublishGameViewInputFrame(m_context, GameViewInputFrameEvent{});
|
||||
return;
|
||||
}
|
||||
|
||||
RenderViewportPanelContent(*m_context, EditorViewportKind::Game);
|
||||
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Game);
|
||||
PublishGameViewInputFrame(m_context, BuildGameViewInputFrame(content));
|
||||
Actions::ObserveInactiveActionRoute(*m_context);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "Panel.h"
|
||||
#include "Core/AssetItem.h"
|
||||
#include "Core/EditorActionRoute.h"
|
||||
#include "UI/PopupState.h"
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceHandle.h>
|
||||
#include <XCEngine/Resources/Material/Material.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Components {
|
||||
@@ -23,7 +31,55 @@ public:
|
||||
void OnDetach() override;
|
||||
void Render() override;
|
||||
|
||||
enum class SubjectMode {
|
||||
None,
|
||||
GameObject,
|
||||
MaterialAsset,
|
||||
UnsupportedAsset
|
||||
};
|
||||
|
||||
struct MaterialTagEditRow {
|
||||
std::array<char, 64> name{};
|
||||
std::array<char, 128> value{};
|
||||
};
|
||||
|
||||
struct MaterialAssetState {
|
||||
std::string assetPath;
|
||||
std::string assetFullPath;
|
||||
std::string assetName;
|
||||
std::array<char, 260> shaderPath{};
|
||||
std::array<char, 128> shaderPass{};
|
||||
int renderQueue = static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Geometry);
|
||||
::XCEngine::Resources::MaterialRenderState renderState{};
|
||||
std::vector<MaterialTagEditRow> tags;
|
||||
std::string errorMessage;
|
||||
bool dirty = false;
|
||||
bool loaded = false;
|
||||
|
||||
void Reset() {
|
||||
assetPath.clear();
|
||||
assetFullPath.clear();
|
||||
assetName.clear();
|
||||
shaderPath.fill('\0');
|
||||
shaderPass.fill('\0');
|
||||
renderQueue = static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Geometry);
|
||||
renderState = ::XCEngine::Resources::MaterialRenderState();
|
||||
tags.clear();
|
||||
errorMessage.clear();
|
||||
dirty = false;
|
||||
loaded = false;
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
void SyncSubject();
|
||||
void SetSubjectMode(SubjectMode mode);
|
||||
void InspectMaterialAsset(const AssetItemPtr& item);
|
||||
void ClearMaterialAsset();
|
||||
void RenderMaterialAsset();
|
||||
void RenderUnsupportedAsset();
|
||||
bool SaveMaterialAsset();
|
||||
void ReloadMaterialAsset();
|
||||
void RenderGameObject(::XCEngine::Components::GameObject* gameObject);
|
||||
void RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject);
|
||||
void RenderEmptyState(const char* title, const char* subtitle = nullptr);
|
||||
@@ -31,6 +87,11 @@ private:
|
||||
|
||||
uint64_t m_selectionHandlerId = 0;
|
||||
uint64_t m_selectedEntityId = 0;
|
||||
SubjectMode m_subjectMode = SubjectMode::None;
|
||||
EditorActionRoute m_lastExplicitRoute = EditorActionRoute::None;
|
||||
AssetItemPtr m_selectedAssetItem;
|
||||
::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Material> m_selectedMaterial;
|
||||
MaterialAssetState m_materialAssetState;
|
||||
UI::DeferredPopupState m_addComponentPopup;
|
||||
std::function<void()> m_deferredContextAction;
|
||||
};
|
||||
|
||||
@@ -21,6 +21,10 @@ constexpr float kRunToolbarHeight = 32.0f;
|
||||
constexpr float kRunToolbarButtonExtent = 24.0f;
|
||||
constexpr float kRunToolbarButtonSpacing = 8.0f;
|
||||
constexpr float kRunToolbarIconInset = 3.0f;
|
||||
constexpr ImVec2 kMainMenuFramePadding(6.0f, 2.0f);
|
||||
constexpr ImVec4 kMainMenuTextColor(0.08f, 0.08f, 0.08f, 1.0f);
|
||||
constexpr ImVec4 kMainMenuItemHoveredColor(0.88f, 0.88f, 0.88f, 1.0f);
|
||||
constexpr ImVec4 kMainMenuItemActiveColor(0.82f, 0.82f, 0.82f, 1.0f);
|
||||
constexpr ImVec4 kRunToolbarBackgroundColor(0.1f, 0.1f, 0.1f, 1.0f);
|
||||
|
||||
std::string BuildRunToolbarIconPath(const char* fileName) {
|
||||
@@ -108,9 +112,14 @@ void MenuBar::RenderChrome() {
|
||||
}
|
||||
|
||||
Actions::HandleMenuBarShortcuts(*m_context);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.08f, 0.08f, 0.08f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, kMainMenuFramePadding);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, kMainMenuTextColor);
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, kMainMenuItemActiveColor);
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, kMainMenuItemHoveredColor);
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, kMainMenuItemActiveColor);
|
||||
Actions::DrawMainMenuBar(*m_context, m_aboutPopup);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar();
|
||||
RenderRunToolbar();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/IProjectManager.h"
|
||||
#include "Core/AssetItem.h"
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "Utils/ProjectFileUtils.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <imgui.h>
|
||||
#include <shellapi.h>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
@@ -86,6 +91,52 @@ UI::AssetTileOptions MakeProjectAssetTileOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
std::string BuildProjectRelativeAssetPath(const std::string& projectPath, const std::string& fullPath) {
|
||||
if (projectPath.empty() || fullPath.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return ProjectFileUtils::MakeProjectRelativePath(projectPath, fullPath);
|
||||
}
|
||||
|
||||
bool ShowPathInExplorer(const std::string& fullPath, bool selectTarget) {
|
||||
if (fullPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::error_code ec;
|
||||
const fs::path targetPath = fs::path(Platform::Utf8ToWide(fullPath)).lexically_normal();
|
||||
if (targetPath.empty() || !fs::exists(targetPath, ec)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HINSTANCE result = nullptr;
|
||||
if (selectTarget) {
|
||||
const std::wstring parameters = L"/select,\"" + targetPath.native() + L"\"";
|
||||
const std::wstring workingDirectory = targetPath.parent_path().native();
|
||||
result = ShellExecuteW(
|
||||
nullptr,
|
||||
L"open",
|
||||
L"explorer.exe",
|
||||
parameters.c_str(),
|
||||
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
||||
SW_SHOWNORMAL);
|
||||
} else {
|
||||
const std::wstring workingDirectory = targetPath.parent_path().native();
|
||||
result = ShellExecuteW(
|
||||
nullptr,
|
||||
L"open",
|
||||
targetPath.c_str(),
|
||||
nullptr,
|
||||
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
||||
SW_SHOWNORMAL);
|
||||
}
|
||||
|
||||
return reinterpret_cast<INT_PTR>(result) > 32;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProjectPanel::ProjectPanel() : Panel("Project") {
|
||||
@@ -186,6 +237,93 @@ void ProjectPanel::CancelRename() {
|
||||
m_renameState.Cancel();
|
||||
}
|
||||
|
||||
ProjectPanel::ContextMenuTarget ProjectPanel::BuildContextMenuTarget(
|
||||
IProjectManager& manager,
|
||||
const AssetItemPtr& item) const {
|
||||
ContextMenuTarget target;
|
||||
target.item = item;
|
||||
if (item) {
|
||||
target.subjectPath = item->fullPath;
|
||||
target.createFolderPath = item->isFolder ? item->fullPath : std::string();
|
||||
target.showInExplorerSelect = true;
|
||||
return target;
|
||||
}
|
||||
|
||||
if (const AssetItemPtr currentFolder = manager.GetCurrentFolder()) {
|
||||
target.subjectPath = currentFolder->fullPath;
|
||||
target.createFolderPath = currentFolder->fullPath;
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
void ProjectPanel::DrawProjectContextMenu(IProjectManager& manager, const ContextMenuTarget& target) {
|
||||
auto* managerPtr = &manager;
|
||||
const bool canCreate = !target.createFolderPath.empty();
|
||||
const bool canShowInExplorer = !target.subjectPath.empty();
|
||||
const bool canOpen = target.item != nullptr && Commands::CanOpenAsset(target.item);
|
||||
const bool canDelete = target.item != nullptr;
|
||||
const bool canRename = target.item != nullptr;
|
||||
const std::string copyPath = BuildProjectRelativeAssetPath(
|
||||
m_context ? m_context->GetProjectPath() : std::string(),
|
||||
target.subjectPath);
|
||||
const bool canCopyPath = !copyPath.empty();
|
||||
|
||||
const auto queueCreateAsset = [this, managerPtr, target](auto createFn) {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, managerPtr, target, createFn]() {
|
||||
if (!target.createFolderPath.empty() && target.item && target.item->isFolder) {
|
||||
managerPtr->NavigateToFolder(target.item);
|
||||
}
|
||||
|
||||
if (AssetItemPtr createdItem = createFn(*managerPtr)) {
|
||||
BeginRename(createdItem);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
UI::DrawContextSubmenu("Create", [&]() {
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Folder", nullptr, false, canCreate), [&]() {
|
||||
queueCreateAsset([](IProjectManager& createManager) {
|
||||
return Commands::CreateFolder(createManager, "New Folder");
|
||||
});
|
||||
});
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Material", nullptr, false, canCreate), [&]() {
|
||||
queueCreateAsset([](IProjectManager& createManager) {
|
||||
return Commands::CreateMaterial(createManager, "New Material");
|
||||
});
|
||||
});
|
||||
}, canCreate);
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Show in Explore", nullptr, false, canShowInExplorer), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [target]() {
|
||||
ShowPathInExplorer(target.subjectPath, target.showInExplorerSelect);
|
||||
});
|
||||
});
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeOpenAssetAction(canOpen), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, target]() {
|
||||
Actions::OpenProjectAsset(*m_context, target.item);
|
||||
});
|
||||
});
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(canDelete), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, target]() {
|
||||
Commands::DeleteAsset(m_context->GetProjectManager(), target.item);
|
||||
});
|
||||
});
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, canRename), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, target]() {
|
||||
BeginRename(target.item);
|
||||
});
|
||||
});
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Copy Path", nullptr, false, canCopyPath), [copyPath]() {
|
||||
ImGui::SetClipboardText(copyPath.c_str());
|
||||
});
|
||||
}
|
||||
|
||||
void ProjectPanel::Render() {
|
||||
UI::PanelWindowScope panel(m_name.c_str());
|
||||
if (!panel.IsOpen()) {
|
||||
@@ -265,7 +403,6 @@ void ProjectPanel::RenderToolbar() {
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
|
||||
auto* managerPtr = &manager;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding());
|
||||
const bool open = ImGui::BeginChild("ProjectFolderTree", ImVec2(m_navigationWidth, 0.0f), false);
|
||||
@@ -287,13 +424,7 @@ void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
|
||||
}
|
||||
|
||||
if (UI::BeginContextMenuForWindow("##ProjectFolderTreeContext")) {
|
||||
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
|
||||
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
|
||||
BeginRename(createdFolder);
|
||||
}
|
||||
});
|
||||
});
|
||||
DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr));
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
@@ -351,7 +482,6 @@ void ProjectPanel::RenderFolderTreeNode(
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
auto* managerPtr = &manager;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
const bool open = ImGui::BeginChild("ProjectBrowser", ImVec2(0.0f, 0.0f), false);
|
||||
@@ -393,7 +523,6 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
}
|
||||
|
||||
const float tileWidth = UI::AssetTileSize().x;
|
||||
const float tileHeight = UI::AssetTileSize().y;
|
||||
const float spacing = UI::AssetGridSpacing().x;
|
||||
const float rowSpacing = UI::AssetGridSpacing().y;
|
||||
const float panelWidth = ImGui::GetContentRegionAvail().x;
|
||||
@@ -402,19 +531,41 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
columns = 1;
|
||||
}
|
||||
|
||||
const int rowCount = visibleItems.empty() ? 0 : (static_cast<int>(visibleItems.size()) + columns - 1) / columns;
|
||||
std::vector<float> rowHeights(static_cast<size_t>(rowCount), UI::AssetTileSize().y);
|
||||
|
||||
AssetItemPtr pendingSelection;
|
||||
AssetItemPtr pendingOpenTarget;
|
||||
const std::string selectedItemPath = manager.GetSelectedItemPath();
|
||||
const ImVec2 gridOrigin = ImGui::GetCursorPos();
|
||||
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
|
||||
const AssetItemPtr& item = visibleItems[visibleIndex];
|
||||
const bool isRenaming = item && m_renameState.IsEditing(item->fullPath);
|
||||
UI::AssetTileOptions tileOptions = MakeProjectAssetTileOptions();
|
||||
tileOptions.drawLabel = !isRenaming;
|
||||
const ImVec2 tileSize = UI::ComputeAssetTileSize(GetProjectAssetDisplayName(item).c_str(), tileOptions);
|
||||
const int row = visibleIndex / columns;
|
||||
rowHeights[static_cast<size_t>(row)] = (std::max)(rowHeights[static_cast<size_t>(row)], tileSize.y);
|
||||
}
|
||||
|
||||
std::vector<float> rowOffsets(static_cast<size_t>(rowCount), gridOrigin.y);
|
||||
float nextRowY = gridOrigin.y;
|
||||
for (int row = 0; row < rowCount; ++row) {
|
||||
rowOffsets[static_cast<size_t>(row)] = nextRowY;
|
||||
nextRowY += rowHeights[static_cast<size_t>(row)] + rowSpacing;
|
||||
}
|
||||
|
||||
int renderedItemCount = 0;
|
||||
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
|
||||
const int column = visibleIndex % columns;
|
||||
const int row = visibleIndex / columns;
|
||||
ImGui::SetCursorPos(ImVec2(
|
||||
gridOrigin.x + column * (tileWidth + spacing),
|
||||
gridOrigin.y + row * (tileHeight + rowSpacing)));
|
||||
rowOffsets[static_cast<size_t>(row)]));
|
||||
|
||||
const AssetItemPtr& item = visibleItems[visibleIndex];
|
||||
const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath);
|
||||
++renderedItemCount;
|
||||
if (interaction.clicked) {
|
||||
pendingSelection = item;
|
||||
}
|
||||
@@ -427,9 +578,14 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!visibleItems.empty()) {
|
||||
const int rowCount = (static_cast<int>(visibleItems.size()) + columns - 1) / columns;
|
||||
ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing);
|
||||
if (renderedItemCount > 0) {
|
||||
const int renderedRowCount = (renderedItemCount + columns - 1) / columns;
|
||||
float contentBottom = gridOrigin.y;
|
||||
for (int row = 0; row < renderedRowCount; ++row) {
|
||||
contentBottom = rowOffsets[static_cast<size_t>(row)] + rowHeights[static_cast<size_t>(row)];
|
||||
}
|
||||
ImGui::SetCursorPos(ImVec2(gridOrigin.x, contentBottom));
|
||||
ImGui::Dummy(ImVec2(0.0f, 0.0f));
|
||||
}
|
||||
|
||||
if (visibleItems.empty() && !searchQuery.Empty()) {
|
||||
@@ -449,21 +605,7 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
}
|
||||
|
||||
if (UI::BeginContextMenuForWindow("##ProjectBrowserContext")) {
|
||||
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
|
||||
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
|
||||
BeginRename(createdFolder);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (manager.CanNavigateBack()) {
|
||||
Actions::DrawMenuSeparator();
|
||||
Actions::DrawMenuAction(Actions::MakeNavigateBackAction(true), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [managerPtr]() {
|
||||
managerPtr->NavigateBack();
|
||||
});
|
||||
});
|
||||
}
|
||||
DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr));
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
@@ -570,21 +712,7 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem
|
||||
Actions::BeginProjectAssetDrag(item, iconKind);
|
||||
|
||||
if (UI::BeginContextMenuForLastItem("##ProjectItemContext")) {
|
||||
Actions::DrawMenuAction(Actions::MakeOpenAssetAction(Commands::CanOpenAsset(item)), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, item]() {
|
||||
Actions::OpenProjectAsset(*m_context, item);
|
||||
});
|
||||
});
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, item != nullptr), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, item]() {
|
||||
BeginRename(item);
|
||||
});
|
||||
});
|
||||
Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(item != nullptr), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, item]() {
|
||||
Commands::DeleteAsset(m_context->GetProjectManager(), item);
|
||||
});
|
||||
});
|
||||
DrawProjectContextMenu(m_context->GetProjectManager(), BuildContextMenuTarget(m_context->GetProjectManager(), item));
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,12 +41,21 @@ private:
|
||||
bool openRequested = false;
|
||||
};
|
||||
|
||||
struct ContextMenuTarget {
|
||||
AssetItemPtr item;
|
||||
std::string subjectPath;
|
||||
std::string createFolderPath;
|
||||
bool showInExplorerSelect = false;
|
||||
};
|
||||
|
||||
void BeginAssetDragDropFrame();
|
||||
void RegisterFolderDropTarget(IProjectManager& manager, const AssetItemPtr& folder);
|
||||
void FinalizeAssetDragDrop(IProjectManager& manager);
|
||||
void BeginRename(const AssetItemPtr& item);
|
||||
bool CommitRename(IProjectManager& manager);
|
||||
void CancelRename();
|
||||
ContextMenuTarget BuildContextMenuTarget(IProjectManager& manager, const AssetItemPtr& item) const;
|
||||
void DrawProjectContextMenu(IProjectManager& manager, const ContextMenuTarget& target);
|
||||
void RenderToolbar();
|
||||
void RenderFolderTreePane(IProjectManager& manager);
|
||||
void RenderFolderTreeNode(IProjectManager& manager, const AssetItemPtr& folder, const std::string& currentFolderPath);
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "Core/ISelectionManager.h"
|
||||
#include "SceneViewPanel.h"
|
||||
#include "Viewport/SceneViewportEditorOverlayData.h"
|
||||
#include "Viewport/SceneViewportOverlayHandleBuilder.h"
|
||||
#include "Viewport/SceneViewportOverlayHitTester.h"
|
||||
#include "Viewport/SceneViewportMath.h"
|
||||
#include "Viewport/SceneViewportOrientationGizmo.h"
|
||||
#include "Viewport/SceneViewportOverlayRenderer.h"
|
||||
#include "Viewport/SceneViewportTransformGizmoFrameBuilder.h"
|
||||
#include "ViewportPanelContent.h"
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "UI/UI.h"
|
||||
@@ -13,9 +18,11 @@
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
@@ -28,13 +35,209 @@ struct SceneViewportToolOverlayResult {
|
||||
SceneViewportToolMode clickedTool = SceneViewportToolMode::Move;
|
||||
};
|
||||
|
||||
enum class SceneViewportActiveGizmoKind : uint8_t {
|
||||
enum class SceneViewportInteractionKind : uint8_t {
|
||||
None = 0,
|
||||
Move,
|
||||
Rotate,
|
||||
Scale
|
||||
MoveGizmo,
|
||||
RotateGizmo,
|
||||
ScaleGizmo,
|
||||
OrientationGizmo,
|
||||
SceneIcon
|
||||
};
|
||||
|
||||
struct SceneViewportInteractionCandidate {
|
||||
SceneViewportInteractionKind kind = SceneViewportInteractionKind::None;
|
||||
int priority = 0;
|
||||
int secondaryPriority = 0;
|
||||
float distanceSq = Math::FLOAT_MAX;
|
||||
float depth = Math::FLOAT_MAX;
|
||||
uint64_t entityId = 0;
|
||||
SceneViewportGizmoAxis moveAxis = SceneViewportGizmoAxis::None;
|
||||
SceneViewportGizmoPlane movePlane = SceneViewportGizmoPlane::None;
|
||||
SceneViewportRotateGizmoAxis rotateAxis = SceneViewportRotateGizmoAxis::None;
|
||||
SceneViewportScaleGizmoHandle scaleHandle = SceneViewportScaleGizmoHandle::None;
|
||||
SceneViewportOrientationAxis orientationAxis = SceneViewportOrientationAxis::None;
|
||||
|
||||
bool HasHit() const {
|
||||
return kind != SceneViewportInteractionKind::None;
|
||||
}
|
||||
};
|
||||
|
||||
const char* GetSceneViewportPivotModeLabel(SceneViewportPivotMode mode) {
|
||||
return mode == SceneViewportPivotMode::Pivot ? "Pivot" : "Center";
|
||||
}
|
||||
|
||||
const char* GetSceneViewportTransformSpaceModeLabel(SceneViewportTransformSpaceMode mode) {
|
||||
return mode == SceneViewportTransformSpaceMode::Global ? "Global" : "Local";
|
||||
}
|
||||
|
||||
SceneViewportActiveGizmoKind ToActiveGizmoKind(SceneViewportInteractionKind kind) {
|
||||
switch (kind) {
|
||||
case SceneViewportInteractionKind::MoveGizmo:
|
||||
return SceneViewportActiveGizmoKind::Move;
|
||||
case SceneViewportInteractionKind::RotateGizmo:
|
||||
return SceneViewportActiveGizmoKind::Rotate;
|
||||
case SceneViewportInteractionKind::ScaleGizmo:
|
||||
return SceneViewportActiveGizmoKind::Scale;
|
||||
case SceneViewportInteractionKind::OrientationGizmo:
|
||||
case SceneViewportInteractionKind::SceneIcon:
|
||||
case SceneViewportInteractionKind::None:
|
||||
default:
|
||||
return SceneViewportActiveGizmoKind::None;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsBetterSceneViewportInteractionCandidate(
|
||||
const SceneViewportInteractionCandidate& candidate,
|
||||
const SceneViewportInteractionCandidate& current) {
|
||||
constexpr float kMetricEpsilon = 0.001f;
|
||||
|
||||
if (!candidate.HasHit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!current.HasHit()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (candidate.priority != current.priority) {
|
||||
return candidate.priority > current.priority;
|
||||
}
|
||||
|
||||
if (candidate.distanceSq + kMetricEpsilon < current.distanceSq) {
|
||||
return true;
|
||||
}
|
||||
if (current.distanceSq + kMetricEpsilon < candidate.distanceSq) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (candidate.depth + kMetricEpsilon < current.depth) {
|
||||
return true;
|
||||
}
|
||||
if (current.depth + kMetricEpsilon < candidate.depth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return candidate.secondaryPriority > current.secondaryPriority;
|
||||
}
|
||||
|
||||
void AccumulateSceneViewportInteractionCandidate(
|
||||
const SceneViewportInteractionCandidate& candidate,
|
||||
SceneViewportInteractionCandidate& bestCandidate) {
|
||||
if (IsBetterSceneViewportInteractionCandidate(candidate, bestCandidate)) {
|
||||
bestCandidate = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
SceneViewportInteractionCandidate BuildOrientationGizmoInteractionCandidate(
|
||||
SceneViewportOrientationAxis axis) {
|
||||
SceneViewportInteractionCandidate candidate = {};
|
||||
if (axis == SceneViewportOrientationAxis::None) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
candidate.kind = SceneViewportInteractionKind::OrientationGizmo;
|
||||
candidate.priority = 200;
|
||||
candidate.distanceSq = 0.0f;
|
||||
candidate.depth = 0.0f;
|
||||
candidate.orientationAxis = axis;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
SceneViewportInteractionCandidate BuildOverlayHandleInteractionCandidate(
|
||||
const SceneViewportOverlayHandleHitResult& hitResult) {
|
||||
SceneViewportInteractionCandidate candidate = {};
|
||||
if (!hitResult.HasHit()) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
candidate.priority = hitResult.priority;
|
||||
candidate.distanceSq = hitResult.distanceSq;
|
||||
candidate.depth = hitResult.depth;
|
||||
candidate.entityId = hitResult.entityId;
|
||||
switch (hitResult.kind) {
|
||||
case SceneViewportOverlayHandleKind::SceneIcon:
|
||||
candidate.kind = SceneViewportInteractionKind::SceneIcon;
|
||||
return candidate;
|
||||
case SceneViewportOverlayHandleKind::MoveAxis:
|
||||
candidate.kind = SceneViewportInteractionKind::MoveGizmo;
|
||||
candidate.moveAxis = static_cast<SceneViewportGizmoAxis>(hitResult.handleId);
|
||||
return candidate;
|
||||
case SceneViewportOverlayHandleKind::MovePlane:
|
||||
candidate.kind = SceneViewportInteractionKind::MoveGizmo;
|
||||
candidate.movePlane = static_cast<SceneViewportGizmoPlane>(hitResult.handleId);
|
||||
return candidate;
|
||||
case SceneViewportOverlayHandleKind::RotateAxis:
|
||||
candidate.kind = SceneViewportInteractionKind::RotateGizmo;
|
||||
candidate.rotateAxis = static_cast<SceneViewportRotateGizmoAxis>(hitResult.handleId);
|
||||
return candidate;
|
||||
case SceneViewportOverlayHandleKind::ScaleAxis:
|
||||
case SceneViewportOverlayHandleKind::ScaleUniform:
|
||||
candidate.kind = SceneViewportInteractionKind::ScaleGizmo;
|
||||
candidate.scaleHandle = static_cast<SceneViewportScaleGizmoHandle>(hitResult.handleId);
|
||||
return candidate;
|
||||
case SceneViewportOverlayHandleKind::None:
|
||||
default:
|
||||
return SceneViewportInteractionCandidate{};
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
float GetSceneToolbarToggleWidth(const char* firstLabel, const char* secondLabel) {
|
||||
constexpr float kHorizontalPadding = 10.0f;
|
||||
constexpr float kMinWidth = 68.0f;
|
||||
const float maxLabelWidth = (std::max)(
|
||||
ImGui::CalcTextSize(firstLabel).x,
|
||||
ImGui::CalcTextSize(secondLabel).x);
|
||||
return (std::max)(kMinWidth, maxLabelWidth + kHorizontalPadding * 2.0f);
|
||||
}
|
||||
|
||||
void RenderSceneViewportTopBar(
|
||||
SceneViewportPivotMode& pivotMode,
|
||||
SceneViewportTransformSpaceMode& transformSpaceMode) {
|
||||
constexpr float kSceneToolbarHeight = 24.0f;
|
||||
constexpr float kSceneToolbarPaddingY = 0.0f;
|
||||
constexpr float kSceneToolbarButtonHeight = kSceneToolbarHeight - kSceneToolbarPaddingY * 2.0f;
|
||||
constexpr ImVec2 kSceneToolbarButtonFramePadding(8.0f, 1.0f);
|
||||
|
||||
UI::PanelToolbarScope toolbar(
|
||||
"SceneToolbar",
|
||||
kSceneToolbarHeight,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse,
|
||||
true,
|
||||
ImVec2(UI::ToolbarPadding().x, kSceneToolbarPaddingY),
|
||||
ImVec2(0.0f, UI::ToolbarItemSpacing().y),
|
||||
ImVec4(0.23f, 0.23f, 0.23f, 1.0f));
|
||||
if (!toolbar.IsOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, kSceneToolbarButtonFramePadding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
|
||||
|
||||
if (UI::ToolbarButton(
|
||||
GetSceneViewportPivotModeLabel(pivotMode),
|
||||
true,
|
||||
ImVec2(GetSceneToolbarToggleWidth("Pivot", "Center"), kSceneToolbarButtonHeight))) {
|
||||
pivotMode = pivotMode == SceneViewportPivotMode::Pivot
|
||||
? SceneViewportPivotMode::Center
|
||||
: SceneViewportPivotMode::Pivot;
|
||||
}
|
||||
|
||||
ImGui::SameLine(0.0f, 0.0f);
|
||||
|
||||
if (UI::ToolbarButton(
|
||||
GetSceneViewportTransformSpaceModeLabel(transformSpaceMode),
|
||||
true,
|
||||
ImVec2(GetSceneToolbarToggleWidth("Global", "Local"), kSceneToolbarButtonHeight))) {
|
||||
transformSpaceMode = transformSpaceMode == SceneViewportTransformSpaceMode::Global
|
||||
? SceneViewportTransformSpaceMode::Local
|
||||
: SceneViewportTransformSpaceMode::Global;
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
const char* GetSceneViewportToolTooltip(SceneViewportToolMode toolMode) {
|
||||
switch (toolMode) {
|
||||
case SceneViewportToolMode::ViewMove:
|
||||
@@ -69,6 +272,19 @@ const char* GetSceneViewportToolIconBaseName(SceneViewportToolMode toolMode) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string BuildSceneViewportIconPath(const char* iconBaseName, bool active = false) {
|
||||
const std::filesystem::path exeDir(
|
||||
XCEngine::Editor::Platform::Utf8ToWide(XCEngine::Editor::Platform::GetExecutableDirectoryUtf8()));
|
||||
std::filesystem::path iconPath =
|
||||
exeDir / L".." / L".." / L"resources" / L"Icons" /
|
||||
std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(iconBaseName));
|
||||
if (active) {
|
||||
iconPath += L"_on";
|
||||
}
|
||||
iconPath += L".png";
|
||||
return XCEngine::Editor::Platform::WideToUtf8(iconPath.lexically_normal().wstring());
|
||||
}
|
||||
|
||||
const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode, bool active) {
|
||||
static std::string cachedPaths[5][2] = {};
|
||||
const size_t toolIndex = static_cast<size_t>(toolMode);
|
||||
@@ -78,16 +294,7 @@ const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode,
|
||||
return cachedPath;
|
||||
}
|
||||
|
||||
const std::filesystem::path exeDir(
|
||||
XCEngine::Editor::Platform::Utf8ToWide(XCEngine::Editor::Platform::GetExecutableDirectoryUtf8()));
|
||||
std::filesystem::path iconPath =
|
||||
(exeDir / L".." / L".." / L"resources" / L"Icons" /
|
||||
std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(GetSceneViewportToolIconBaseName(toolMode))));
|
||||
if (active) {
|
||||
iconPath += L"_on";
|
||||
}
|
||||
iconPath += L".png";
|
||||
cachedPath = XCEngine::Editor::Platform::WideToUtf8(iconPath.lexically_normal().wstring());
|
||||
cachedPath = BuildSceneViewportIconPath(GetSceneViewportToolIconBaseName(toolMode), active);
|
||||
return cachedPath;
|
||||
}
|
||||
|
||||
@@ -205,72 +412,6 @@ bool ShouldBeginSceneViewportNavigationDrag(
|
||||
ImGui::IsMouseClicked(button);
|
||||
}
|
||||
|
||||
SceneViewportMoveGizmoContext BuildMoveGizmoContext(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ViewportPanelContentResult& content,
|
||||
const ImVec2& mousePosition) {
|
||||
SceneViewportMoveGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y);
|
||||
gizmoContext.mousePosition = Math::Vector2(
|
||||
mousePosition.x - content.itemMin.x,
|
||||
mousePosition.y - content.itemMin.y);
|
||||
|
||||
if (context.GetSelectionManager().GetSelectionCount() == 1) {
|
||||
const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity();
|
||||
if (selectedEntity != 0) {
|
||||
gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
SceneViewportRotateGizmoContext BuildRotateGizmoContext(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ViewportPanelContentResult& content,
|
||||
const ImVec2& mousePosition) {
|
||||
SceneViewportRotateGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y);
|
||||
gizmoContext.mousePosition = Math::Vector2(
|
||||
mousePosition.x - content.itemMin.x,
|
||||
mousePosition.y - content.itemMin.y);
|
||||
|
||||
if (context.GetSelectionManager().GetSelectionCount() == 1) {
|
||||
const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity();
|
||||
if (selectedEntity != 0) {
|
||||
gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
SceneViewportScaleGizmoContext BuildScaleGizmoContext(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ViewportPanelContentResult& content,
|
||||
const ImVec2& mousePosition) {
|
||||
SceneViewportScaleGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y);
|
||||
gizmoContext.mousePosition = Math::Vector2(
|
||||
mousePosition.x - content.itemMin.x,
|
||||
mousePosition.y - content.itemMin.y);
|
||||
|
||||
if (context.GetSelectionManager().GetSelectionCount() == 1) {
|
||||
const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity();
|
||||
if (selectedEntity != 0) {
|
||||
gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SceneViewPanel::SceneViewPanel() : Panel("Scene") {}
|
||||
@@ -283,6 +424,7 @@ void SceneViewPanel::Render() {
|
||||
return;
|
||||
}
|
||||
|
||||
RenderSceneViewportTopBar(m_pivotMode, m_transformSpaceMode);
|
||||
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene);
|
||||
if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) {
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
@@ -304,8 +446,20 @@ void SceneViewPanel::Render() {
|
||||
m_toolMode = toolOverlay.clickedTool;
|
||||
}
|
||||
|
||||
if (content.focused && !io.WantTextInput && !m_lookDragging && !m_panDragging) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_W, false)) {
|
||||
const bool allowToolShortcut = !io.WantTextInput && !m_lookDragging && !m_panDragging;
|
||||
if (allowToolShortcut) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Q, false)) {
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
if (m_rotateGizmo.IsActive()) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
if (m_scaleGizmo.IsActive()) {
|
||||
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
m_toolMode = SceneViewportToolMode::ViewMove;
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_W, false)) {
|
||||
if (m_rotateGizmo.IsActive()) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
@@ -337,103 +491,68 @@ void SceneViewPanel::Render() {
|
||||
const bool showingMoveGizmo = m_toolMode == SceneViewportToolMode::Move || usingTransformTool;
|
||||
const bool showingRotateGizmo = m_toolMode == SceneViewportToolMode::Rotate || usingTransformTool;
|
||||
const bool showingScaleGizmo = m_toolMode == SceneViewportToolMode::Scale || usingTransformTool;
|
||||
const bool useCenterPivot = m_pivotMode == SceneViewportPivotMode::Center;
|
||||
const bool localSpace = m_transformSpaceMode == SceneViewportTransformSpaceMode::Local;
|
||||
const Math::Vector2 viewportSize(content.availableSize.x, content.availableSize.y);
|
||||
const Math::Vector2 localMousePosition(
|
||||
io.MousePos.x - content.itemMin.x,
|
||||
io.MousePos.y - content.itemMin.y);
|
||||
SceneViewportOverlayData overlay = {};
|
||||
SceneViewportMoveGizmoContext moveGizmoContext = {};
|
||||
SceneViewportRotateGizmoContext rotateGizmoContext = {};
|
||||
SceneViewportScaleGizmoContext scaleGizmoContext = {};
|
||||
SceneViewportTransformGizmoFrameState gizmoFrameState = {};
|
||||
SceneViewportOverlayFrameData emptySceneOverlayFrameData = {};
|
||||
SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None;
|
||||
|
||||
if (hasInteractiveViewport) {
|
||||
overlay = viewportHostService->GetSceneViewOverlayData();
|
||||
if (showingMoveGizmo) {
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
if (m_moveGizmo.IsActive() &&
|
||||
(moveGizmoContext.selectedObject == nullptr ||
|
||||
m_context->GetSelectionManager().GetSelectedEntity() != m_moveGizmo.GetActiveEntityId())) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
} else if (m_moveGizmo.IsActive()) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
|
||||
if (showingRotateGizmo) {
|
||||
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
if (m_rotateGizmo.IsActive() &&
|
||||
(rotateGizmoContext.selectedObject == nullptr ||
|
||||
m_context->GetSelectionManager().GetSelectedEntity() != m_rotateGizmo.GetActiveEntityId())) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
} else if (m_rotateGizmo.IsActive()) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
|
||||
if (showingScaleGizmo) {
|
||||
scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
scaleGizmoContext.uniformOnly = usingTransformTool;
|
||||
if (m_scaleGizmo.IsActive() &&
|
||||
(scaleGizmoContext.selectedObject == nullptr ||
|
||||
m_context->GetSelectionManager().GetSelectedEntity() != m_scaleGizmo.GetActiveEntityId())) {
|
||||
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
} else if (m_scaleGizmo.IsActive()) {
|
||||
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
activeGizmoKind = SceneViewportActiveGizmoKind::Move;
|
||||
} else if (m_rotateGizmo.IsActive()) {
|
||||
activeGizmoKind = SceneViewportActiveGizmoKind::Rotate;
|
||||
} else if (m_scaleGizmo.IsActive()) {
|
||||
activeGizmoKind = SceneViewportActiveGizmoKind::Scale;
|
||||
}
|
||||
|
||||
if (showingMoveGizmo) {
|
||||
SceneViewportMoveGizmoContext updateContext = moveGizmoContext;
|
||||
if (activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
activeGizmoKind != SceneViewportActiveGizmoKind::Move) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_moveGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingRotateGizmo) {
|
||||
SceneViewportRotateGizmoContext updateContext = rotateGizmoContext;
|
||||
if (activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
activeGizmoKind != SceneViewportActiveGizmoKind::Rotate) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_rotateGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingScaleGizmo) {
|
||||
SceneViewportScaleGizmoContext updateContext = scaleGizmoContext;
|
||||
if (activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
activeGizmoKind != SceneViewportActiveGizmoKind::Scale) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_scaleGizmo.Update(updateContext);
|
||||
}
|
||||
gizmoFrameState = RefreshSceneViewportTransformGizmos(
|
||||
*m_context,
|
||||
overlay,
|
||||
viewportSize,
|
||||
localMousePosition,
|
||||
useCenterPivot,
|
||||
localSpace,
|
||||
usingTransformTool,
|
||||
showingMoveGizmo,
|
||||
m_moveGizmo,
|
||||
showingRotateGizmo,
|
||||
m_rotateGizmo,
|
||||
showingScaleGizmo,
|
||||
m_scaleGizmo);
|
||||
activeGizmoKind = gizmoFrameState.activeGizmoKind;
|
||||
} else {
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
if (m_rotateGizmo.IsActive()) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
if (m_scaleGizmo.IsActive()) {
|
||||
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
CancelSceneViewportTransformGizmoDrags(*m_context, m_moveGizmo, m_rotateGizmo, m_scaleGizmo);
|
||||
}
|
||||
|
||||
const bool moveGizmoHovering = showingMoveGizmo && m_moveGizmo.IsHoveringHandle();
|
||||
const bool rotateGizmoHovering = showingRotateGizmo && m_rotateGizmo.IsHoveringHandle();
|
||||
const bool scaleGizmoHovering = showingScaleGizmo && m_scaleGizmo.IsHoveringHandle();
|
||||
const SceneViewportTransformGizmoHandleBuildInputs interactionGizmoInputs =
|
||||
hasInteractiveViewport
|
||||
? BuildSceneViewportTransformGizmoHandleBuildInputs(
|
||||
showingMoveGizmo,
|
||||
m_moveGizmo,
|
||||
gizmoFrameState.moveContext,
|
||||
showingRotateGizmo,
|
||||
m_rotateGizmo,
|
||||
gizmoFrameState.rotateContext,
|
||||
showingScaleGizmo,
|
||||
m_scaleGizmo,
|
||||
gizmoFrameState.scaleContext)
|
||||
: SceneViewportTransformGizmoHandleBuildInputs{};
|
||||
const SceneViewportOverlayFrameData& interactionOverlayFrameData =
|
||||
hasInteractiveViewport
|
||||
? viewportHostService->GetSceneViewInteractionOverlayFrameData(
|
||||
*m_context,
|
||||
overlay,
|
||||
interactionGizmoInputs)
|
||||
: emptySceneOverlayFrameData;
|
||||
const SceneViewportOverlayHandleHitResult overlayHandleHit =
|
||||
hasInteractiveViewport
|
||||
? HitTestSceneViewportOverlayHandles(
|
||||
interactionOverlayFrameData,
|
||||
Math::Vector2(content.availableSize.x, content.availableSize.y),
|
||||
localMousePosition)
|
||||
: SceneViewportOverlayHandleHitResult{};
|
||||
const bool moveGizmoActive = showingMoveGizmo && m_moveGizmo.IsActive();
|
||||
const bool rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive();
|
||||
const bool scaleGizmoActive = showingScaleGizmo && m_scaleGizmo.IsActive();
|
||||
const SceneViewportActiveGizmoKind hoveredGizmoKind = scaleGizmoHovering
|
||||
? SceneViewportActiveGizmoKind::Scale
|
||||
: (moveGizmoHovering ? SceneViewportActiveGizmoKind::Move
|
||||
: (rotateGizmoHovering ? SceneViewportActiveGizmoKind::Rotate
|
||||
: SceneViewportActiveGizmoKind::None));
|
||||
if (moveGizmoActive) {
|
||||
activeGizmoKind = SceneViewportActiveGizmoKind::Move;
|
||||
} else if (rotateGizmoActive) {
|
||||
@@ -443,44 +562,83 @@ void SceneViewPanel::Render() {
|
||||
} else {
|
||||
activeGizmoKind = SceneViewportActiveGizmoKind::None;
|
||||
}
|
||||
const bool gizmoHovering = hoveredGizmoKind != SceneViewportActiveGizmoKind::None;
|
||||
const bool gizmoActive = activeGizmoKind != SceneViewportActiveGizmoKind::None;
|
||||
|
||||
const bool beginTransformGizmo =
|
||||
hasInteractiveViewport &&
|
||||
content.clickedLeft &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
!toolOverlay.hovered &&
|
||||
gizmoHovering;
|
||||
const SceneViewportOrientationAxis orientationAxisHit =
|
||||
SceneViewportInteractionCandidate hoveredInteraction = {};
|
||||
const bool canResolveViewportInteraction =
|
||||
hasInteractiveViewport &&
|
||||
viewportContentHovered &&
|
||||
!usingViewMoveTool &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
!gizmoHovering &&
|
||||
!gizmoActive
|
||||
? HitTestSceneViewportOrientationGizmo(
|
||||
overlay,
|
||||
content.itemMin,
|
||||
content.itemMax,
|
||||
io.MousePos)
|
||||
: SceneViewportOrientationAxis::None;
|
||||
!toolOverlay.hovered &&
|
||||
!gizmoActive;
|
||||
if (canResolveViewportInteraction) {
|
||||
AccumulateSceneViewportInteractionCandidate(
|
||||
BuildOverlayHandleInteractionCandidate(overlayHandleHit),
|
||||
hoveredInteraction);
|
||||
|
||||
AccumulateSceneViewportInteractionCandidate(
|
||||
BuildOrientationGizmoInteractionCandidate(
|
||||
HitTestSceneViewportOrientationGizmo(
|
||||
overlay,
|
||||
content.itemMin,
|
||||
content.itemMax,
|
||||
io.MousePos)),
|
||||
hoveredInteraction);
|
||||
}
|
||||
|
||||
if (!gizmoActive) {
|
||||
if (showingMoveGizmo) {
|
||||
m_moveGizmo.SetHoveredHandle(
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::MoveGizmo
|
||||
? hoveredInteraction.moveAxis
|
||||
: SceneViewportGizmoAxis::None,
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::MoveGizmo
|
||||
? hoveredInteraction.movePlane
|
||||
: SceneViewportGizmoPlane::None);
|
||||
}
|
||||
if (showingRotateGizmo) {
|
||||
m_rotateGizmo.SetHoveredHandle(
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::RotateGizmo
|
||||
? hoveredInteraction.rotateAxis
|
||||
: SceneViewportRotateGizmoAxis::None);
|
||||
}
|
||||
if (showingScaleGizmo) {
|
||||
m_scaleGizmo.SetHoveredHandle(
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::ScaleGizmo
|
||||
? hoveredInteraction.scaleHandle
|
||||
: SceneViewportScaleGizmoHandle::None);
|
||||
}
|
||||
}
|
||||
|
||||
const SceneViewportActiveGizmoKind hoveredGizmoKind =
|
||||
ToActiveGizmoKind(hoveredInteraction.kind);
|
||||
const bool gizmoHovering = hoveredGizmoKind != SceneViewportActiveGizmoKind::None;
|
||||
const SceneViewportOrientationAxis orientationAxisHit =
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::OrientationGizmo
|
||||
? hoveredInteraction.orientationAxis
|
||||
: SceneViewportOrientationAxis::None;
|
||||
const uint64_t clickedSceneIconEntity =
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::SceneIcon
|
||||
? hoveredInteraction.entityId
|
||||
: 0;
|
||||
const bool beginTransformGizmo =
|
||||
hasInteractiveViewport &&
|
||||
content.clickedLeft &&
|
||||
gizmoHovering;
|
||||
const bool orientationGizmoClick =
|
||||
hasInteractiveViewport &&
|
||||
content.clickedLeft &&
|
||||
orientationAxisHit != SceneViewportOrientationAxis::None;
|
||||
const bool sceneIconClick =
|
||||
hasInteractiveViewport &&
|
||||
content.clickedLeft &&
|
||||
clickedSceneIconEntity != 0;
|
||||
const bool selectClick =
|
||||
hasInteractiveViewport &&
|
||||
content.clickedLeft &&
|
||||
viewportContentHovered &&
|
||||
!usingViewMoveTool &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
!orientationGizmoClick &&
|
||||
!gizmoHovering &&
|
||||
!gizmoActive;
|
||||
canResolveViewportInteraction &&
|
||||
!hoveredInteraction.HasHit();
|
||||
const bool beginLeftPanDrag = usingViewMoveTool
|
||||
? ShouldBeginSceneViewportNavigationDrag(
|
||||
hasInteractiveViewport,
|
||||
@@ -525,47 +683,32 @@ void SceneViewPanel::Render() {
|
||||
io.MouseDelta.y);
|
||||
}
|
||||
|
||||
if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || selectClick || beginLookDrag ||
|
||||
if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || sceneIconClick || selectClick || beginLookDrag ||
|
||||
beginPanDrag) {
|
||||
ImGui::SetWindowFocus();
|
||||
}
|
||||
|
||||
if (beginTransformGizmo) {
|
||||
if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) {
|
||||
m_scaleGizmo.TryBeginDrag(scaleGizmoContext, m_context->GetUndoManager());
|
||||
m_scaleGizmo.TryBeginDrag(gizmoFrameState.scaleContext, m_context->GetUndoManager());
|
||||
} else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) {
|
||||
m_moveGizmo.TryBeginDrag(moveGizmoContext, m_context->GetUndoManager());
|
||||
m_moveGizmo.TryBeginDrag(gizmoFrameState.moveContext, m_context->GetUndoManager());
|
||||
} else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) {
|
||||
m_rotateGizmo.TryBeginDrag(rotateGizmoContext, m_context->GetUndoManager());
|
||||
m_rotateGizmo.TryBeginDrag(gizmoFrameState.rotateContext, m_context->GetUndoManager());
|
||||
}
|
||||
}
|
||||
|
||||
if (orientationGizmoClick) {
|
||||
viewportHostService->AlignSceneViewToOrientationAxis(orientationAxisHit);
|
||||
overlay = viewportHostService->GetSceneViewOverlayData();
|
||||
if (showingMoveGizmo) {
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
m_moveGizmo.Update(moveGizmoContext);
|
||||
}
|
||||
if (showingRotateGizmo) {
|
||||
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
m_rotateGizmo.Update(rotateGizmoContext);
|
||||
}
|
||||
if (showingScaleGizmo) {
|
||||
scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
scaleGizmoContext.uniformOnly = usingTransformTool;
|
||||
m_scaleGizmo.Update(scaleGizmoContext);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectClick) {
|
||||
const ImVec2 localMousePosition(
|
||||
io.MousePos.x - content.itemMin.x,
|
||||
io.MousePos.y - content.itemMin.y);
|
||||
if (sceneIconClick) {
|
||||
m_context->GetSelectionManager().SetSelectedEntity(clickedSceneIconEntity);
|
||||
} else if (selectClick) {
|
||||
const uint64_t selectedEntity = viewportHostService->PickSceneViewEntity(
|
||||
*m_context,
|
||||
content.availableSize,
|
||||
localMousePosition);
|
||||
ImVec2(localMousePosition.x, localMousePosition.y));
|
||||
if (selectedEntity != 0) {
|
||||
m_context->GetSelectionManager().SetSelectedEntity(selectedEntity);
|
||||
} else {
|
||||
@@ -576,11 +719,11 @@ void SceneViewPanel::Render() {
|
||||
if (gizmoActive) {
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) {
|
||||
m_moveGizmo.UpdateDrag(moveGizmoContext);
|
||||
m_moveGizmo.UpdateDrag(gizmoFrameState.moveContext);
|
||||
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) {
|
||||
m_rotateGizmo.UpdateDrag(rotateGizmoContext);
|
||||
m_rotateGizmo.UpdateDrag(gizmoFrameState.rotateContext);
|
||||
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) {
|
||||
m_scaleGizmo.UpdateDrag(scaleGizmoContext);
|
||||
m_scaleGizmo.UpdateDrag(gizmoFrameState.scaleContext);
|
||||
}
|
||||
} else {
|
||||
if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) {
|
||||
@@ -707,52 +850,41 @@ void SceneViewPanel::Render() {
|
||||
|
||||
if (content.hasViewportArea && content.frame.hasTexture) {
|
||||
overlay = viewportHostService->GetSceneViewOverlayData();
|
||||
SceneViewportActiveGizmoKind drawActiveGizmoKind = SceneViewportActiveGizmoKind::None;
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Move;
|
||||
} else if (m_rotateGizmo.IsActive()) {
|
||||
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Rotate;
|
||||
} else if (m_scaleGizmo.IsActive()) {
|
||||
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Scale;
|
||||
}
|
||||
if (showingMoveGizmo) {
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
SceneViewportMoveGizmoContext updateContext = moveGizmoContext;
|
||||
if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
drawActiveGizmoKind != SceneViewportActiveGizmoKind::Move) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_moveGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingRotateGizmo) {
|
||||
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
SceneViewportRotateGizmoContext updateContext = rotateGizmoContext;
|
||||
if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
drawActiveGizmoKind != SceneViewportActiveGizmoKind::Rotate) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_rotateGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingScaleGizmo) {
|
||||
scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
scaleGizmoContext.uniformOnly = usingTransformTool;
|
||||
SceneViewportScaleGizmoContext updateContext = scaleGizmoContext;
|
||||
if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
drawActiveGizmoKind != SceneViewportActiveGizmoKind::Scale) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_scaleGizmo.Update(updateContext);
|
||||
}
|
||||
const SceneViewportTransformGizmoFrameState drawGizmoFrameState =
|
||||
RefreshSceneViewportTransformGizmos(
|
||||
*m_context,
|
||||
overlay,
|
||||
viewportSize,
|
||||
localMousePosition,
|
||||
useCenterPivot,
|
||||
localSpace,
|
||||
usingTransformTool,
|
||||
showingMoveGizmo,
|
||||
m_moveGizmo,
|
||||
showingRotateGizmo,
|
||||
m_rotateGizmo,
|
||||
showingScaleGizmo,
|
||||
m_scaleGizmo);
|
||||
|
||||
viewportHostService->SetSceneViewTransientTransformGizmoOverlayData(
|
||||
overlay,
|
||||
BuildSceneViewportTransformGizmoHandleBuildInputs(
|
||||
showingMoveGizmo,
|
||||
m_moveGizmo,
|
||||
drawGizmoFrameState.moveContext,
|
||||
showingRotateGizmo,
|
||||
m_rotateGizmo,
|
||||
drawGizmoFrameState.rotateContext,
|
||||
showingScaleGizmo,
|
||||
m_scaleGizmo,
|
||||
drawGizmoFrameState.scaleContext));
|
||||
|
||||
DrawSceneViewportOverlay(
|
||||
ImGui::GetWindowDrawList(),
|
||||
overlay,
|
||||
content.itemMin,
|
||||
content.itemMax,
|
||||
content.availableSize,
|
||||
showingMoveGizmo ? &m_moveGizmo.GetDrawData() : nullptr,
|
||||
showingRotateGizmo ? &m_rotateGizmo.GetDrawData() : nullptr,
|
||||
showingScaleGizmo ? &m_scaleGizmo.GetDrawData() : nullptr);
|
||||
content.availableSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,16 @@ enum class SceneViewportToolMode : uint8_t {
|
||||
Transform
|
||||
};
|
||||
|
||||
enum class SceneViewportPivotMode : uint8_t {
|
||||
Pivot = 0,
|
||||
Center
|
||||
};
|
||||
|
||||
enum class SceneViewportTransformSpaceMode : uint8_t {
|
||||
Global = 0,
|
||||
Local
|
||||
};
|
||||
|
||||
class SceneViewPanel : public Panel {
|
||||
public:
|
||||
SceneViewPanel();
|
||||
@@ -25,6 +35,8 @@ public:
|
||||
|
||||
private:
|
||||
SceneViewportToolMode m_toolMode = SceneViewportToolMode::Move;
|
||||
SceneViewportPivotMode m_pivotMode = SceneViewportPivotMode::Pivot;
|
||||
SceneViewportTransformSpaceMode m_transformSpaceMode = SceneViewportTransformSpaceMode::Global;
|
||||
bool m_lookDragging = false;
|
||||
bool m_panDragging = false;
|
||||
int m_panDragButton = ImGuiMouseButton_Middle;
|
||||
|
||||
@@ -59,6 +59,7 @@ inline void RenderViewportInteractionSurface(
|
||||
ViewportPanelContentResult& result,
|
||||
EditorViewportKind kind,
|
||||
const ImVec2& interactionSize) {
|
||||
ImGui::SetNextItemAllowOverlap();
|
||||
ImGui::InvisibleButton(
|
||||
GetViewportInteractionSurfaceId(kind),
|
||||
interactionSize,
|
||||
|
||||
BIN
editor/tab.png
|
Before Width: | Height: | Size: 2.4 KiB |
BIN
editor/unity.png
|
Before Width: | Height: | Size: 786 KiB |
|
Before Width: | Height: | Size: 117 KiB |
@@ -246,6 +246,8 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetRef.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ArtifactFormats.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetDatabase.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetImportService.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ProjectAssetIndex.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ResourceTypes.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ImportSettings.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ResourceHandle.h
|
||||
@@ -255,6 +257,8 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ResourceDependencyGraph.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetGUID.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetDatabase.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetImportService.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ProjectAssetIndex.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ResourceManager.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ResourceCache.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AsyncLoader.cpp
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <XCEngine/Components/Component.h>
|
||||
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||
#include <XCEngine/Core/Asset/ResourceHandle.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||
|
||||
#include <memory>
|
||||
@@ -32,12 +33,14 @@ private:
|
||||
struct PendingMeshLoadState;
|
||||
|
||||
void BeginAsyncMeshLoad(const std::string& meshPath);
|
||||
void EnsureDeferredAsyncMeshLoadStarted();
|
||||
void ResolvePendingMesh();
|
||||
|
||||
Resources::ResourceHandle<Resources::Mesh> m_mesh;
|
||||
std::string m_meshPath;
|
||||
Resources::AssetRef m_meshRef;
|
||||
std::shared_ptr<PendingMeshLoadState> m_pendingMeshLoad;
|
||||
bool m_asyncMeshLoadRequested = false;
|
||||
};
|
||||
|
||||
} // namespace Components
|
||||
|
||||
@@ -46,6 +46,7 @@ private:
|
||||
struct PendingMaterialLoadState;
|
||||
|
||||
void BeginAsyncMaterialLoad(size_t index, const std::string& materialPath);
|
||||
void EnsureDeferredAsyncMaterialLoadStarted(size_t index);
|
||||
void ResolvePendingMaterials();
|
||||
void EnsureMaterialSlot(size_t index);
|
||||
static std::string MaterialPathFromHandle(const Resources::ResourceHandle<Resources::Material>& material);
|
||||
@@ -54,6 +55,7 @@ private:
|
||||
std::vector<std::string> m_materialPaths;
|
||||
std::vector<Resources::AssetRef> m_materialRefs;
|
||||
std::vector<std::shared_ptr<PendingMaterialLoadState>> m_pendingMaterialLoads;
|
||||
std::vector<bool> m_asyncMaterialLoadRequested;
|
||||
bool m_castShadows = true;
|
||||
bool m_receiveShadows = true;
|
||||
uint32_t m_renderLayer = 0;
|
||||
|
||||
@@ -6,12 +6,23 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class Mesh;
|
||||
class Material;
|
||||
|
||||
class AssetDatabase {
|
||||
public:
|
||||
struct ArtifactDependencyRecord {
|
||||
Containers::String path;
|
||||
Containers::String hash;
|
||||
Core::uint64 fileSize = 0;
|
||||
Core::uint64 writeTime = 0;
|
||||
};
|
||||
|
||||
struct SourceAssetRecord {
|
||||
AssetGUID guid;
|
||||
Containers::String relativePath;
|
||||
@@ -39,6 +50,7 @@ public:
|
||||
Core::uint64 sourceFileSize = 0;
|
||||
Core::uint64 sourceWriteTime = 0;
|
||||
LocalID mainLocalID = kMainAssetLocalID;
|
||||
std::vector<ArtifactDependencyRecord> dependencies;
|
||||
};
|
||||
|
||||
struct ResolvedAsset {
|
||||
@@ -66,13 +78,15 @@ public:
|
||||
ResourceType requestedType,
|
||||
ResolvedAsset& outAsset);
|
||||
bool TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const;
|
||||
void BuildLookupSnapshot(std::unordered_map<std::string, AssetGUID>& outPathToGuid,
|
||||
std::unordered_map<AssetGUID, Containers::String>& outGuidToPath) const;
|
||||
|
||||
const Containers::String& GetProjectRoot() const { return m_projectRoot; }
|
||||
const Containers::String& GetAssetsRoot() const { return m_assetsRoot; }
|
||||
const Containers::String& GetLibraryRoot() const { return m_libraryRoot; }
|
||||
|
||||
private:
|
||||
static constexpr Core::uint32 kCurrentImporterVersion = 2;
|
||||
static constexpr Core::uint32 kCurrentImporterVersion = 3;
|
||||
|
||||
void EnsureProjectLayout();
|
||||
void LoadSourceAssetDB();
|
||||
@@ -110,12 +124,24 @@ private:
|
||||
bool ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord);
|
||||
|
||||
Containers::String BuildArtifactKey(const SourceAssetRecord& sourceRecord) const;
|
||||
Containers::String BuildArtifactKey(
|
||||
const SourceAssetRecord& sourceRecord,
|
||||
const std::vector<ArtifactDependencyRecord>& dependencies = {}) const;
|
||||
Containers::String BuildArtifactDirectory(const Containers::String& artifactKey) const;
|
||||
static Containers::String ReadWholeFileText(const std::filesystem::path& path);
|
||||
static Containers::String ComputeFileHash(const std::filesystem::path& path);
|
||||
static Core::uint64 GetFileSizeValue(const std::filesystem::path& path);
|
||||
static Core::uint64 GetFileWriteTimeValue(const std::filesystem::path& path);
|
||||
Containers::String NormalizeDependencyPath(const std::filesystem::path& path) const;
|
||||
std::filesystem::path ResolveDependencyPath(const Containers::String& path) const;
|
||||
bool CaptureDependencyRecord(const std::filesystem::path& path,
|
||||
ArtifactDependencyRecord& outRecord) const;
|
||||
bool AreDependenciesCurrent(const std::vector<ArtifactDependencyRecord>& dependencies) const;
|
||||
bool CollectModelDependencies(const SourceAssetRecord& sourceRecord,
|
||||
const Mesh& mesh,
|
||||
std::vector<ArtifactDependencyRecord>& outDependencies) const;
|
||||
bool CollectMaterialDependencies(const Material& material,
|
||||
std::vector<ArtifactDependencyRecord>& outDependencies) const;
|
||||
|
||||
Containers::String m_projectRoot;
|
||||
Containers::String m_assetsRoot;
|
||||
|
||||
38
engine/include/XCEngine/Core/Asset/AssetImportService.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "AssetDatabase.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class AssetImportService {
|
||||
public:
|
||||
void Initialize();
|
||||
void Shutdown();
|
||||
|
||||
void SetProjectRoot(const Containers::String& projectRoot);
|
||||
Containers::String GetProjectRoot() const;
|
||||
|
||||
void Refresh();
|
||||
|
||||
bool EnsureArtifact(const Containers::String& requestPath,
|
||||
ResourceType requestedType,
|
||||
AssetDatabase::ResolvedAsset& outAsset);
|
||||
bool TryGetAssetRef(const Containers::String& requestPath,
|
||||
ResourceType resourceType,
|
||||
AssetRef& outRef) const;
|
||||
bool TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const;
|
||||
void BuildLookupSnapshot(std::unordered_map<std::string, AssetGUID>& outPathToGuid,
|
||||
std::unordered_map<AssetGUID, Containers::String>& outGuidToPath) const;
|
||||
|
||||
private:
|
||||
mutable std::recursive_mutex m_mutex;
|
||||
Containers::String m_projectRoot;
|
||||
AssetDatabase m_assetDatabase;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
37
engine/include/XCEngine/Core/Asset/ProjectAssetIndex.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class AssetImportService;
|
||||
|
||||
class ProjectAssetIndex {
|
||||
public:
|
||||
void ResetProjectRoot(const Containers::String& projectRoot = Containers::String());
|
||||
void RefreshFrom(const AssetImportService& importService);
|
||||
|
||||
bool TryGetAssetRef(AssetImportService& importService,
|
||||
const Containers::String& path,
|
||||
ResourceType resourceType,
|
||||
AssetRef& outRef) const;
|
||||
bool TryResolveAssetPath(const AssetImportService& importService,
|
||||
const AssetRef& assetRef,
|
||||
Containers::String& outPath) const;
|
||||
void RememberResolvedPath(const AssetGUID& assetGuid, const Containers::String& relativePath);
|
||||
|
||||
private:
|
||||
mutable std::shared_mutex m_mutex;
|
||||
Containers::String m_projectRoot;
|
||||
std::unordered_map<std::string, AssetGUID> m_assetGuidByPathKey;
|
||||
std::unordered_map<AssetGUID, Containers::String> m_assetPathByGuid;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
#include "AssetDatabase.h"
|
||||
#include "AssetImportService.h"
|
||||
#include "ProjectAssetIndex.h"
|
||||
#include "ResourceCache.h"
|
||||
#include "AsyncLoader.h"
|
||||
#include "ResourceHandle.h"
|
||||
@@ -165,12 +166,12 @@ private:
|
||||
size_t m_memoryUsage = 0;
|
||||
size_t m_memoryBudget = 512 * 1024 * 1024;
|
||||
|
||||
AssetDatabase m_assetDatabase;
|
||||
mutable AssetImportService m_assetImportService;
|
||||
mutable ProjectAssetIndex m_projectAssetIndex;
|
||||
ResourceCache m_cache;
|
||||
Core::UniqueRef<AsyncLoader> m_asyncLoader;
|
||||
Threading::Mutex m_mutex;
|
||||
std::mutex m_initializeMutex;
|
||||
mutable std::recursive_mutex m_ioMutex;
|
||||
std::mutex m_inFlightLoadsMutex;
|
||||
std::unordered_map<InFlightLoadKey, std::shared_ptr<InFlightLoadState>, InFlightLoadKeyHasher> m_inFlightLoads;
|
||||
std::atomic<Core::uint32> m_deferredSceneLoadDepth{0};
|
||||
|
||||
@@ -21,6 +21,7 @@ public:
|
||||
bool IsKeyDown(KeyCode key) const;
|
||||
bool IsKeyUp(KeyCode key) const;
|
||||
bool IsKeyPressed(KeyCode key) const;
|
||||
bool IsKeyReleased(KeyCode key) const;
|
||||
|
||||
Math::Vector2 GetMousePosition() const;
|
||||
Math::Vector2 GetMouseDelta() const;
|
||||
@@ -28,6 +29,7 @@ public:
|
||||
bool IsMouseButtonDown(MouseButton button) const;
|
||||
bool IsMouseButtonUp(MouseButton button) const;
|
||||
bool IsMouseButtonClicked(MouseButton button) const;
|
||||
bool IsMouseButtonReleased(MouseButton button) const;
|
||||
|
||||
int GetTouchCount() const;
|
||||
TouchState GetTouch(int index) const;
|
||||
@@ -38,6 +40,8 @@ public:
|
||||
bool GetButton(const Containers::String& buttonName) const;
|
||||
bool GetButtonDown(const Containers::String& buttonName) const;
|
||||
bool GetButtonUp(const Containers::String& buttonName) const;
|
||||
bool IsAnyKeyDown() const;
|
||||
bool IsAnyKeyPressed() const;
|
||||
|
||||
void RegisterAxis(const InputAxis& axis);
|
||||
void RegisterButton(const Containers::String& name, KeyCode key);
|
||||
@@ -68,6 +72,7 @@ private:
|
||||
|
||||
std::vector<bool> m_keyDownThisFrame;
|
||||
std::vector<bool> m_keyDownLastFrame;
|
||||
std::vector<bool> m_keyUpThisFrame;
|
||||
std::vector<bool> m_keyDown;
|
||||
|
||||
Math::Vector2 m_mousePosition;
|
||||
@@ -75,6 +80,7 @@ private:
|
||||
float m_mouseScrollDelta = 0.0f;
|
||||
std::vector<bool> m_mouseButtonDownThisFrame;
|
||||
std::vector<bool> m_mouseButtonDownLastFrame;
|
||||
std::vector<bool> m_mouseButtonUpThisFrame;
|
||||
std::vector<bool> m_mouseButtonDown;
|
||||
|
||||
std::vector<TouchState> m_touches;
|
||||
|
||||
@@ -57,6 +57,7 @@ struct CameraRenderRequest {
|
||||
Math::Color clearColorOverride = Math::Color::Black();
|
||||
RenderPassSequence* preScenePasses = nullptr;
|
||||
RenderPassSequence* postScenePasses = nullptr;
|
||||
RenderPassSequence* overlayPasses = nullptr;
|
||||
|
||||
bool IsValid() const {
|
||||
return scene != nullptr &&
|
||||
|
||||
@@ -28,6 +28,26 @@ enum class ScriptLifecycleMethod {
|
||||
OnDestroy
|
||||
};
|
||||
|
||||
struct ScriptClassDescriptor {
|
||||
std::string assemblyName;
|
||||
std::string namespaceName;
|
||||
std::string className;
|
||||
|
||||
std::string GetFullName() const {
|
||||
return namespaceName.empty() ? className : namespaceName + "." + className;
|
||||
}
|
||||
|
||||
bool operator==(const ScriptClassDescriptor& other) const {
|
||||
return assemblyName == other.assemblyName
|
||||
&& namespaceName == other.namespaceName
|
||||
&& className == other.className;
|
||||
}
|
||||
|
||||
bool operator!=(const ScriptClassDescriptor& other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
|
||||
struct ScriptRuntimeContext {
|
||||
Components::Scene* scene = nullptr;
|
||||
Components::GameObject* gameObject = nullptr;
|
||||
@@ -43,6 +63,9 @@ public:
|
||||
virtual void OnRuntimeStart(Components::Scene* scene) = 0;
|
||||
virtual void OnRuntimeStop(Components::Scene* scene) = 0;
|
||||
|
||||
virtual bool TryGetAvailableScriptClasses(
|
||||
std::vector<ScriptClassDescriptor>& outClasses) const = 0;
|
||||
|
||||
virtual bool TryGetClassFieldMetadata(
|
||||
const std::string& assemblyName,
|
||||
const std::string& namespaceName,
|
||||
|
||||
@@ -50,6 +50,8 @@ public:
|
||||
const std::string& namespaceName,
|
||||
const std::string& className) const;
|
||||
std::vector<std::string> GetScriptClassNames(const std::string& assemblyName = std::string()) const;
|
||||
bool TryGetAvailableScriptClasses(
|
||||
std::vector<ScriptClassDescriptor>& outClasses) const override;
|
||||
bool TryGetClassFieldMetadata(
|
||||
const std::string& assemblyName,
|
||||
const std::string& namespaceName,
|
||||
|
||||
@@ -9,6 +9,8 @@ class NullScriptRuntime : public IScriptRuntime {
|
||||
public:
|
||||
void OnRuntimeStart(Components::Scene* scene) override;
|
||||
void OnRuntimeStop(Components::Scene* scene) override;
|
||||
bool TryGetAvailableScriptClasses(
|
||||
std::vector<ScriptClassDescriptor>& outClasses) const override;
|
||||
bool TryGetClassFieldMetadata(
|
||||
const std::string& assemblyName,
|
||||
const std::string& namespaceName,
|
||||
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
|
||||
void SetScriptClass(const std::string& namespaceName, const std::string& className);
|
||||
void SetScriptClass(const std::string& assemblyName, const std::string& namespaceName, const std::string& className);
|
||||
void ClearScriptClass();
|
||||
|
||||
bool HasScriptClass() const { return !m_className.empty(); }
|
||||
std::string GetFullClassName() const;
|
||||
|
||||
@@ -19,9 +19,12 @@ class ScriptComponent;
|
||||
class ScriptEngine {
|
||||
public:
|
||||
static ScriptEngine& Get();
|
||||
static constexpr float DefaultFixedDeltaTime = 1.0f / 50.0f;
|
||||
|
||||
void SetRuntime(IScriptRuntime* runtime);
|
||||
IScriptRuntime* GetRuntime() const { return m_runtime; }
|
||||
void SetRuntimeFixedDeltaTime(float fixedDeltaTime);
|
||||
float GetRuntimeFixedDeltaTime() const { return m_runtimeFixedDeltaTime; }
|
||||
|
||||
void OnRuntimeStart(Components::Scene* scene);
|
||||
void OnRuntimeStop();
|
||||
@@ -33,6 +36,7 @@ public:
|
||||
void OnScriptComponentEnabled(ScriptComponent* component);
|
||||
void OnScriptComponentDisabled(ScriptComponent* component);
|
||||
void OnScriptComponentDestroyed(ScriptComponent* component);
|
||||
void OnScriptComponentClassChanged(ScriptComponent* component);
|
||||
|
||||
bool IsRuntimeRunning() const { return m_runtimeRunning; }
|
||||
Components::Scene* GetRuntimeScene() const { return m_runtimeScene; }
|
||||
@@ -40,6 +44,9 @@ public:
|
||||
bool HasTrackedScriptComponent(const ScriptComponent* component) const;
|
||||
bool HasRuntimeInstance(const ScriptComponent* component) const;
|
||||
size_t GetTrackedScriptCount() const { return m_scriptOrder.size(); }
|
||||
bool TryGetAvailableScriptClasses(
|
||||
std::vector<ScriptClassDescriptor>& outClasses,
|
||||
const std::string& assemblyName = std::string()) const;
|
||||
bool TrySetScriptFieldValue(
|
||||
ScriptComponent* component,
|
||||
const std::string& fieldName,
|
||||
@@ -138,6 +145,7 @@ private:
|
||||
IScriptRuntime* m_runtime = &m_nullRuntime;
|
||||
Components::Scene* m_runtimeScene = nullptr;
|
||||
bool m_runtimeRunning = false;
|
||||
float m_runtimeFixedDeltaTime = DefaultFixedDeltaTime;
|
||||
uint64_t m_runtimeSceneCreatedSubscription = 0;
|
||||
|
||||
std::unordered_map<ScriptInstanceKey, ScriptInstanceState, ScriptInstanceKeyHasher> m_scriptStates;
|
||||
|
||||
@@ -20,6 +20,10 @@ bool ShouldTraceMeshPath(const std::string& path) {
|
||||
path.find("backpack") != std::string::npos;
|
||||
}
|
||||
|
||||
bool HasVirtualPathScheme(const std::string& path) {
|
||||
return path.find("://") != std::string::npos;
|
||||
}
|
||||
|
||||
std::string EncodeAssetRef(const Resources::AssetRef& assetRef) {
|
||||
if (!assetRef.IsValid()) {
|
||||
return std::string();
|
||||
@@ -67,17 +71,20 @@ struct MeshFilterComponent::PendingMeshLoadState {
|
||||
};
|
||||
|
||||
Resources::Mesh* MeshFilterComponent::GetMesh() const {
|
||||
const_cast<MeshFilterComponent*>(this)->EnsureDeferredAsyncMeshLoadStarted();
|
||||
const_cast<MeshFilterComponent*>(this)->ResolvePendingMesh();
|
||||
return m_mesh.Get();
|
||||
}
|
||||
|
||||
const Resources::ResourceHandle<Resources::Mesh>& MeshFilterComponent::GetMeshHandle() const {
|
||||
const_cast<MeshFilterComponent*>(this)->EnsureDeferredAsyncMeshLoadStarted();
|
||||
const_cast<MeshFilterComponent*>(this)->ResolvePendingMesh();
|
||||
return m_mesh;
|
||||
}
|
||||
|
||||
void MeshFilterComponent::SetMeshPath(const std::string& meshPath) {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_asyncMeshLoadRequested = false;
|
||||
m_meshPath = meshPath;
|
||||
if (m_meshPath.empty()) {
|
||||
m_mesh.Reset();
|
||||
@@ -101,6 +108,7 @@ void MeshFilterComponent::SetMeshPath(const std::string& meshPath) {
|
||||
|
||||
void MeshFilterComponent::SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh) {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_asyncMeshLoadRequested = false;
|
||||
m_mesh = mesh;
|
||||
m_meshPath = mesh.Get() != nullptr ? ToStdString(mesh->GetPath()) : std::string();
|
||||
if (m_meshPath.empty()) {
|
||||
@@ -116,18 +124,29 @@ void MeshFilterComponent::SetMesh(Resources::Mesh* mesh) {
|
||||
|
||||
void MeshFilterComponent::ClearMesh() {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_asyncMeshLoadRequested = false;
|
||||
m_mesh.Reset();
|
||||
m_meshPath.clear();
|
||||
m_meshRef.Reset();
|
||||
}
|
||||
|
||||
void MeshFilterComponent::Serialize(std::ostream& os) const {
|
||||
os << "mesh=" << m_meshPath << ";";
|
||||
os << "meshRef=" << EncodeAssetRef(m_meshRef) << ";";
|
||||
Resources::AssetRef meshRef = m_meshRef;
|
||||
if (!meshRef.IsValid() &&
|
||||
!m_meshPath.empty() &&
|
||||
!HasVirtualPathScheme(m_meshPath) &&
|
||||
Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, meshRef)) {
|
||||
}
|
||||
|
||||
os << "meshRef=" << EncodeAssetRef(meshRef) << ";";
|
||||
if (!meshRef.IsValid() && !m_meshPath.empty()) {
|
||||
os << "meshPath=" << m_meshPath << ";";
|
||||
}
|
||||
}
|
||||
|
||||
void MeshFilterComponent::Deserialize(std::istream& is) {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_asyncMeshLoadRequested = false;
|
||||
m_mesh.Reset();
|
||||
m_meshPath.clear();
|
||||
m_meshRef.Reset();
|
||||
@@ -148,7 +167,7 @@ void MeshFilterComponent::Deserialize(std::istream& is) {
|
||||
const std::string key = token.substr(0, eqPos);
|
||||
const std::string value = token.substr(eqPos + 1);
|
||||
|
||||
if (key == "mesh") {
|
||||
if (key == "mesh" || key == "meshPath") {
|
||||
pendingMeshPath = value;
|
||||
} else if (key == "meshRef") {
|
||||
TryDecodeAssetRef(value, pendingMeshRef);
|
||||
@@ -172,7 +191,6 @@ void MeshFilterComponent::Deserialize(std::istream& is) {
|
||||
if (ShouldTraceMeshPath(m_meshPath)) {
|
||||
TraceMeshFilter(*this, std::string("Resolved meshRef to path=") + m_meshPath);
|
||||
}
|
||||
BeginAsyncMeshLoad(m_meshPath);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -191,7 +209,6 @@ void MeshFilterComponent::Deserialize(std::istream& is) {
|
||||
if (!Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, m_meshRef)) {
|
||||
m_meshRef.Reset();
|
||||
}
|
||||
BeginAsyncMeshLoad(m_meshPath);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -202,10 +219,12 @@ void MeshFilterComponent::Deserialize(std::istream& is) {
|
||||
void MeshFilterComponent::BeginAsyncMeshLoad(const std::string& meshPath) {
|
||||
if (meshPath.empty()) {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_asyncMeshLoadRequested = false;
|
||||
m_mesh.Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
m_asyncMeshLoadRequested = true;
|
||||
m_mesh.Reset();
|
||||
m_pendingMeshLoad = std::make_shared<PendingMeshLoadState>();
|
||||
if (ShouldTraceMeshPath(meshPath)) {
|
||||
@@ -219,7 +238,15 @@ void MeshFilterComponent::BeginAsyncMeshLoad(const std::string& meshPath) {
|
||||
state->result = std::move(result);
|
||||
state->completed = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void MeshFilterComponent::EnsureDeferredAsyncMeshLoadStarted() {
|
||||
if (m_asyncMeshLoadRequested || m_mesh.Get() != nullptr || m_meshPath.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BeginAsyncMeshLoad(m_meshPath);
|
||||
}
|
||||
|
||||
void MeshFilterComponent::ResolvePendingMesh() {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "Core/Asset/ResourceManager.h"
|
||||
#include "Debug/Logger.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -21,6 +22,10 @@ bool ShouldTraceMaterialPath(const std::string& path) {
|
||||
path.find("New Material.mat") != std::string::npos;
|
||||
}
|
||||
|
||||
bool HasVirtualPathScheme(const std::string& path) {
|
||||
return path.find("://") != std::string::npos;
|
||||
}
|
||||
|
||||
std::string EncodeAssetRef(const Resources::AssetRef& assetRef) {
|
||||
if (!assetRef.IsValid()) {
|
||||
return std::string();
|
||||
@@ -145,11 +150,13 @@ struct MeshRendererComponent::PendingMaterialLoadState {
|
||||
};
|
||||
|
||||
Resources::Material* MeshRendererComponent::GetMaterial(size_t index) const {
|
||||
const_cast<MeshRendererComponent*>(this)->EnsureDeferredAsyncMaterialLoadStarted(index);
|
||||
const_cast<MeshRendererComponent*>(this)->ResolvePendingMaterials();
|
||||
return index < m_materials.size() ? m_materials[index].Get() : nullptr;
|
||||
}
|
||||
|
||||
const Resources::ResourceHandle<Resources::Material>& MeshRendererComponent::GetMaterialHandle(size_t index) const {
|
||||
const_cast<MeshRendererComponent*>(this)->EnsureDeferredAsyncMaterialLoadStarted(index);
|
||||
const_cast<MeshRendererComponent*>(this)->ResolvePendingMaterials();
|
||||
static const Resources::ResourceHandle<Resources::Material> kNullHandle;
|
||||
return index < m_materials.size() ? m_materials[index] : kNullHandle;
|
||||
@@ -163,6 +170,7 @@ const std::string& MeshRendererComponent::GetMaterialPath(size_t index) const {
|
||||
void MeshRendererComponent::SetMaterialPath(size_t index, const std::string& materialPath) {
|
||||
EnsureMaterialSlot(index);
|
||||
m_pendingMaterialLoads[index].reset();
|
||||
m_asyncMaterialLoadRequested[index] = false;
|
||||
m_materialPaths[index] = materialPath;
|
||||
if (materialPath.empty()) {
|
||||
m_materials[index].Reset();
|
||||
@@ -188,6 +196,7 @@ void MeshRendererComponent::SetMaterialPath(size_t index, const std::string& mat
|
||||
void MeshRendererComponent::SetMaterial(size_t index, const Resources::ResourceHandle<Resources::Material>& material) {
|
||||
EnsureMaterialSlot(index);
|
||||
m_pendingMaterialLoads[index].reset();
|
||||
m_asyncMaterialLoadRequested[index] = false;
|
||||
m_materials[index] = material;
|
||||
m_materialPaths[index] = MaterialPathFromHandle(material);
|
||||
if (m_materialPaths[index].empty() ||
|
||||
@@ -206,6 +215,8 @@ void MeshRendererComponent::SetMaterials(const std::vector<Resources::ResourceHa
|
||||
m_materialRefs.resize(materials.size());
|
||||
m_pendingMaterialLoads.clear();
|
||||
m_pendingMaterialLoads.resize(materials.size());
|
||||
m_asyncMaterialLoadRequested.clear();
|
||||
m_asyncMaterialLoadRequested.resize(materials.size(), false);
|
||||
for (size_t i = 0; i < materials.size(); ++i) {
|
||||
m_materialPaths[i] = MaterialPathFromHandle(materials[i]);
|
||||
if (m_materialPaths[i].empty() ||
|
||||
@@ -220,23 +231,45 @@ void MeshRendererComponent::ClearMaterials() {
|
||||
m_materialPaths.clear();
|
||||
m_materialRefs.clear();
|
||||
m_pendingMaterialLoads.clear();
|
||||
m_asyncMaterialLoadRequested.clear();
|
||||
}
|
||||
|
||||
void MeshRendererComponent::Serialize(std::ostream& os) const {
|
||||
os << "materials=";
|
||||
for (size_t i = 0; i < m_materialPaths.size(); ++i) {
|
||||
const size_t slotCount = std::max(m_materialPaths.size(), m_materialRefs.size());
|
||||
std::vector<Resources::AssetRef> serializedRefs = m_materialRefs;
|
||||
serializedRefs.resize(slotCount);
|
||||
std::vector<std::string> serializedPaths = m_materialPaths;
|
||||
serializedPaths.resize(slotCount);
|
||||
std::vector<std::string> fallbackPaths(slotCount);
|
||||
for (size_t i = 0; i < slotCount; ++i) {
|
||||
if (!serializedRefs[i].IsValid() &&
|
||||
!serializedPaths[i].empty() &&
|
||||
!HasVirtualPathScheme(serializedPaths[i]) &&
|
||||
Resources::ResourceManager::Get().TryGetAssetRef(
|
||||
serializedPaths[i].c_str(),
|
||||
Resources::ResourceType::Material,
|
||||
serializedRefs[i])) {
|
||||
}
|
||||
|
||||
if (!serializedRefs[i].IsValid()) {
|
||||
fallbackPaths[i] = serializedPaths[i];
|
||||
}
|
||||
}
|
||||
|
||||
os << "materialPaths=";
|
||||
for (size_t i = 0; i < slotCount; ++i) {
|
||||
if (i > 0) {
|
||||
os << "|";
|
||||
}
|
||||
os << m_materialPaths[i];
|
||||
os << fallbackPaths[i];
|
||||
}
|
||||
os << ";";
|
||||
os << "materialRefs=";
|
||||
for (size_t i = 0; i < m_materialRefs.size(); ++i) {
|
||||
for (size_t i = 0; i < serializedRefs.size(); ++i) {
|
||||
if (i > 0) {
|
||||
os << "|";
|
||||
}
|
||||
os << EncodeAssetRef(m_materialRefs[i]);
|
||||
os << EncodeAssetRef(serializedRefs[i]);
|
||||
}
|
||||
os << ";";
|
||||
os << "castShadows=" << (m_castShadows ? 1 : 0) << ";";
|
||||
@@ -265,11 +298,12 @@ void MeshRendererComponent::Deserialize(std::istream& is) {
|
||||
const std::string key = token.substr(0, eqPos);
|
||||
const std::string value = token.substr(eqPos + 1);
|
||||
|
||||
if (key == "materials") {
|
||||
if (key == "materials" || key == "materialPaths") {
|
||||
m_materialPaths = SplitMaterialPaths(value);
|
||||
m_materials.resize(m_materialPaths.size());
|
||||
m_materialRefs.resize(m_materialPaths.size());
|
||||
m_pendingMaterialLoads.resize(m_materialPaths.size());
|
||||
m_asyncMaterialLoadRequested.resize(m_materialPaths.size(), false);
|
||||
} else if (key == "materialRefs") {
|
||||
pendingMaterialRefs = SplitMaterialRefs(value);
|
||||
} else if (key == "castShadows") {
|
||||
@@ -286,6 +320,7 @@ void MeshRendererComponent::Deserialize(std::istream& is) {
|
||||
m_materials.resize(pendingMaterialRefs.size());
|
||||
m_materialRefs.resize(pendingMaterialRefs.size());
|
||||
m_pendingMaterialLoads.resize(pendingMaterialRefs.size());
|
||||
m_asyncMaterialLoadRequested.resize(pendingMaterialRefs.size(), false);
|
||||
}
|
||||
|
||||
if (ShouldTraceAnyMaterialPath(m_materialPaths)) {
|
||||
@@ -310,7 +345,6 @@ void MeshRendererComponent::Deserialize(std::istream& is) {
|
||||
std::string("Resolved materialRef slot=") + std::to_string(i) +
|
||||
" path=" + m_materialPaths[i]);
|
||||
}
|
||||
BeginAsyncMaterialLoad(i, m_materialPaths[i]);
|
||||
restoredOrQueued = true;
|
||||
}
|
||||
}
|
||||
@@ -336,7 +370,6 @@ void MeshRendererComponent::Deserialize(std::istream& is) {
|
||||
m_materialRefs[i])) {
|
||||
m_materialRefs[i].Reset();
|
||||
}
|
||||
BeginAsyncMaterialLoad(i, m_materialPaths[i]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -349,10 +382,12 @@ void MeshRendererComponent::BeginAsyncMaterialLoad(size_t index, const std::stri
|
||||
EnsureMaterialSlot(index);
|
||||
if (materialPath.empty()) {
|
||||
m_pendingMaterialLoads[index].reset();
|
||||
m_asyncMaterialLoadRequested[index] = false;
|
||||
m_materials[index].Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
m_asyncMaterialLoadRequested[index] = true;
|
||||
m_materials[index].Reset();
|
||||
m_pendingMaterialLoads[index] = std::make_shared<PendingMaterialLoadState>();
|
||||
if (ShouldTraceMaterialPath(materialPath)) {
|
||||
@@ -369,7 +404,22 @@ void MeshRendererComponent::BeginAsyncMaterialLoad(size_t index, const std::stri
|
||||
state->result = std::move(result);
|
||||
state->completed = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void MeshRendererComponent::EnsureDeferredAsyncMaterialLoadStarted(size_t index) {
|
||||
if (index >= m_materialPaths.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureMaterialSlot(index);
|
||||
if (m_asyncMaterialLoadRequested[index] ||
|
||||
m_materials[index].Get() != nullptr ||
|
||||
m_materialPaths[index].empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BeginAsyncMaterialLoad(index, m_materialPaths[index]);
|
||||
}
|
||||
|
||||
void MeshRendererComponent::ResolvePendingMaterials() {
|
||||
@@ -428,6 +478,9 @@ void MeshRendererComponent::EnsureMaterialSlot(size_t index) {
|
||||
if (index >= m_pendingMaterialLoads.size()) {
|
||||
m_pendingMaterialLoads.resize(index + 1);
|
||||
}
|
||||
if (index >= m_asyncMaterialLoadRequested.size()) {
|
||||
m_asyncMaterialLoadRequested.resize(index + 1, false);
|
||||
}
|
||||
}
|
||||
|
||||
std::string MeshRendererComponent::MaterialPathFromHandle(const Resources::ResourceHandle<Resources::Material>& material) {
|
||||
|
||||
@@ -592,6 +592,28 @@ bool AssetDatabase::TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::St
|
||||
return true;
|
||||
}
|
||||
|
||||
void AssetDatabase::BuildLookupSnapshot(std::unordered_map<std::string, AssetGUID>& outPathToGuid,
|
||||
std::unordered_map<AssetGUID, Containers::String>& outGuidToPath) const {
|
||||
outPathToGuid.clear();
|
||||
outGuidToPath.clear();
|
||||
outPathToGuid.reserve(m_sourcesByPathKey.size());
|
||||
outGuidToPath.reserve(m_sourcesByGuid.size());
|
||||
|
||||
for (const auto& [pathKey, record] : m_sourcesByPathKey) {
|
||||
if (!record.guid.IsValid() || record.relativePath.Empty()) {
|
||||
continue;
|
||||
}
|
||||
outPathToGuid[pathKey] = record.guid;
|
||||
}
|
||||
|
||||
for (const auto& [guid, record] : m_sourcesByGuid) {
|
||||
if (!guid.IsValid() || record.relativePath.Empty()) {
|
||||
continue;
|
||||
}
|
||||
outGuidToPath[guid] = record.relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
void AssetDatabase::EnsureProjectLayout() {
|
||||
std::error_code ec;
|
||||
fs::create_directories(fs::path(m_assetsRoot.CStr()), ec);
|
||||
|
||||
88
engine/src/Core/Asset/AssetImportService.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#include <XCEngine/Core/Asset/AssetImportService.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
void AssetImportService::Initialize() {
|
||||
}
|
||||
|
||||
void AssetImportService::Shutdown() {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
m_assetDatabase.Shutdown();
|
||||
m_projectRoot.Clear();
|
||||
}
|
||||
|
||||
void AssetImportService::SetProjectRoot(const Containers::String& projectRoot) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
if (m_projectRoot == projectRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_projectRoot.Empty()) {
|
||||
m_assetDatabase.Shutdown();
|
||||
}
|
||||
|
||||
m_projectRoot = projectRoot;
|
||||
if (!m_projectRoot.Empty()) {
|
||||
m_assetDatabase.Initialize(m_projectRoot);
|
||||
}
|
||||
}
|
||||
|
||||
Containers::String AssetImportService::GetProjectRoot() const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
return m_projectRoot;
|
||||
}
|
||||
|
||||
void AssetImportService::Refresh() {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (!m_projectRoot.Empty()) {
|
||||
m_assetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetImportService::EnsureArtifact(const Containers::String& requestPath,
|
||||
ResourceType requestedType,
|
||||
AssetDatabase::ResolvedAsset& outAsset) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_assetDatabase.EnsureArtifact(requestPath, requestedType, outAsset);
|
||||
}
|
||||
|
||||
bool AssetImportService::TryGetAssetRef(const Containers::String& requestPath,
|
||||
ResourceType resourceType,
|
||||
AssetRef& outRef) const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_assetDatabase.TryGetAssetRef(requestPath, resourceType, outRef);
|
||||
}
|
||||
|
||||
bool AssetImportService::TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_assetDatabase.TryGetPrimaryAssetPath(guid, outRelativePath);
|
||||
}
|
||||
|
||||
void AssetImportService::BuildLookupSnapshot(std::unordered_map<std::string, AssetGUID>& outPathToGuid,
|
||||
std::unordered_map<AssetGUID, Containers::String>& outGuidToPath) const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
outPathToGuid.clear();
|
||||
outGuidToPath.clear();
|
||||
if (m_projectRoot.Empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_assetDatabase.BuildLookupSnapshot(outPathToGuid, outGuidToPath);
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
173
engine/src/Core/Asset/ProjectAssetIndex.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
#include <XCEngine/Core/Asset/ProjectAssetIndex.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetImportService.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ToStdString(const Containers::String& value) {
|
||||
return std::string(value.CStr());
|
||||
}
|
||||
|
||||
Containers::String NormalizePathString(const std::filesystem::path& path) {
|
||||
return Containers::String(path.lexically_normal().generic_string().c_str());
|
||||
}
|
||||
|
||||
bool HasVirtualPathScheme(const Containers::String& value) {
|
||||
return ToStdString(value).find("://") != std::string::npos;
|
||||
}
|
||||
|
||||
Containers::String MakeAssetLookupRelativePath(const Containers::String& projectRoot,
|
||||
const Containers::String& requestPath) {
|
||||
if (requestPath.Empty() || HasVirtualPathScheme(requestPath)) {
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
const std::filesystem::path inputPath(requestPath.CStr());
|
||||
if (inputPath.is_absolute()) {
|
||||
if (projectRoot.Empty()) {
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const std::filesystem::path relativePath =
|
||||
std::filesystem::relative(inputPath, std::filesystem::path(projectRoot.CStr()), ec);
|
||||
if (ec) {
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
const Containers::String normalizedRelative = NormalizePathString(relativePath);
|
||||
if (normalizedRelative.StartsWith("Assets/") || normalizedRelative == "Assets") {
|
||||
return normalizedRelative;
|
||||
}
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
const Containers::String normalizedRequest = NormalizePathString(inputPath);
|
||||
if (normalizedRequest.StartsWith("Assets/") || normalizedRequest == "Assets") {
|
||||
return normalizedRequest;
|
||||
}
|
||||
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
std::string MakeAssetLookupPathKey(const Containers::String& relativePath) {
|
||||
std::string key = ToStdString(relativePath);
|
||||
std::transform(key.begin(), key.end(), key.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ProjectAssetIndex::ResetProjectRoot(const Containers::String& projectRoot) {
|
||||
std::unique_lock<std::shared_mutex> lock(m_mutex);
|
||||
m_projectRoot = projectRoot;
|
||||
m_assetGuidByPathKey.clear();
|
||||
m_assetPathByGuid.clear();
|
||||
}
|
||||
|
||||
void ProjectAssetIndex::RefreshFrom(const AssetImportService& importService) {
|
||||
std::unordered_map<std::string, AssetGUID> pathToGuid;
|
||||
std::unordered_map<AssetGUID, Containers::String> guidToPath;
|
||||
const Containers::String projectRoot = importService.GetProjectRoot();
|
||||
if (!projectRoot.Empty()) {
|
||||
importService.BuildLookupSnapshot(pathToGuid, guidToPath);
|
||||
}
|
||||
|
||||
std::unique_lock<std::shared_mutex> lock(m_mutex);
|
||||
m_projectRoot = projectRoot;
|
||||
m_assetGuidByPathKey = std::move(pathToGuid);
|
||||
m_assetPathByGuid = std::move(guidToPath);
|
||||
}
|
||||
|
||||
bool ProjectAssetIndex::TryGetAssetRef(AssetImportService& importService,
|
||||
const Containers::String& path,
|
||||
ResourceType resourceType,
|
||||
AssetRef& outRef) const {
|
||||
bool resolved = false;
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(m_mutex);
|
||||
const Containers::String relativePath = MakeAssetLookupRelativePath(m_projectRoot, path);
|
||||
if (!relativePath.Empty()) {
|
||||
const auto lookupIt = m_assetGuidByPathKey.find(MakeAssetLookupPathKey(relativePath));
|
||||
if (lookupIt != m_assetGuidByPathKey.end()) {
|
||||
outRef.assetGuid = lookupIt->second;
|
||||
outRef.localID = kMainAssetLocalID;
|
||||
outRef.resourceType = resourceType;
|
||||
resolved = outRef.IsValid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
resolved = importService.TryGetAssetRef(path, resourceType, outRef);
|
||||
if (!resolved) {
|
||||
const Containers::String projectRoot = importService.GetProjectRoot();
|
||||
const Containers::String relativePath = MakeAssetLookupRelativePath(projectRoot, path);
|
||||
if (!relativePath.Empty() && !projectRoot.Empty()) {
|
||||
auto* index = const_cast<ProjectAssetIndex*>(this);
|
||||
importService.Refresh();
|
||||
index->RefreshFrom(importService);
|
||||
resolved = importService.TryGetAssetRef(path, resourceType, outRef);
|
||||
}
|
||||
}
|
||||
|
||||
if (resolved) {
|
||||
Containers::String relativePath;
|
||||
if (importService.TryGetPrimaryAssetPath(outRef.assetGuid, relativePath)) {
|
||||
const_cast<ProjectAssetIndex*>(this)->RememberResolvedPath(outRef.assetGuid, relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
bool ProjectAssetIndex::TryResolveAssetPath(const AssetImportService& importService,
|
||||
const AssetRef& assetRef,
|
||||
Containers::String& outPath) const {
|
||||
if (!assetRef.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool resolved = false;
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(m_mutex);
|
||||
const auto lookupIt = m_assetPathByGuid.find(assetRef.assetGuid);
|
||||
if (lookupIt != m_assetPathByGuid.end()) {
|
||||
outPath = lookupIt->second;
|
||||
resolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
resolved = importService.TryGetPrimaryAssetPath(assetRef.assetGuid, outPath);
|
||||
if (resolved) {
|
||||
const_cast<ProjectAssetIndex*>(this)->RememberResolvedPath(assetRef.assetGuid, outPath);
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
void ProjectAssetIndex::RememberResolvedPath(const AssetGUID& assetGuid, const Containers::String& relativePath) {
|
||||
if (!assetGuid.IsValid() || relativePath.Empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_lock<std::shared_mutex> lock(m_mutex);
|
||||
m_assetGuidByPathKey[MakeAssetLookupPathKey(relativePath)] = assetGuid;
|
||||
m_assetPathByGuid[assetGuid] = relativePath;
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -85,6 +85,7 @@ void ResourceManager::EnsureInitialized() {
|
||||
RegisterBuiltinLoader(*this, g_meshLoader);
|
||||
RegisterBuiltinLoader(*this, g_shaderLoader);
|
||||
RegisterBuiltinLoader(*this, g_textureLoader);
|
||||
m_assetImportService.Initialize();
|
||||
|
||||
m_asyncLoader = std::move(asyncLoader);
|
||||
}
|
||||
@@ -96,23 +97,24 @@ void ResourceManager::Shutdown() {
|
||||
m_asyncLoader.reset();
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
|
||||
m_assetDatabase.Shutdown();
|
||||
m_assetImportService.Shutdown();
|
||||
ResourceFileSystem::Get().Shutdown();
|
||||
m_projectAssetIndex.ResetProjectRoot();
|
||||
|
||||
std::lock_guard<std::mutex> inFlightLock(m_inFlightLoadsMutex);
|
||||
m_inFlightLoads.clear();
|
||||
}
|
||||
|
||||
void ResourceManager::SetResourceRoot(const Containers::String& rootPath) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
|
||||
m_resourceRoot = rootPath;
|
||||
if (!m_resourceRoot.Empty()) {
|
||||
ResourceFileSystem::Get().Initialize(rootPath);
|
||||
m_assetDatabase.Initialize(rootPath);
|
||||
m_assetImportService.SetProjectRoot(rootPath);
|
||||
m_projectAssetIndex.RefreshFrom(m_assetImportService);
|
||||
} else {
|
||||
m_assetImportService.SetProjectRoot(Containers::String());
|
||||
ResourceFileSystem::Get().Shutdown();
|
||||
m_assetDatabase.Shutdown();
|
||||
m_projectAssetIndex.ResetProjectRoot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,14 +362,14 @@ void ResourceManager::UnloadGroup(const Containers::Array<ResourceGUID>& guids)
|
||||
|
||||
void ResourceManager::RefreshAssetDatabase() {
|
||||
if (!m_resourceRoot.Empty()) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
|
||||
m_assetDatabase.Refresh();
|
||||
m_assetImportService.Refresh();
|
||||
m_projectAssetIndex.RefreshFrom(m_assetImportService);
|
||||
}
|
||||
}
|
||||
|
||||
bool ResourceManager::TryGetAssetRef(const Containers::String& path, ResourceType resourceType, AssetRef& outRef) const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
|
||||
const bool resolved = m_assetDatabase.TryGetAssetRef(path, resourceType, outRef);
|
||||
const bool resolved = m_projectAssetIndex.TryGetAssetRef(m_assetImportService, path, resourceType, outRef);
|
||||
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
@@ -384,12 +386,8 @@ bool ResourceManager::TryGetAssetRef(const Containers::String& path, ResourceTyp
|
||||
}
|
||||
|
||||
bool ResourceManager::TryResolveAssetPath(const AssetRef& assetRef, Containers::String& outPath) const {
|
||||
if (!assetRef.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
const bool resolved = m_projectAssetIndex.TryResolveAssetPath(m_assetImportService, assetRef, outPath);
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
|
||||
const bool resolved = m_assetDatabase.TryGetPrimaryAssetPath(assetRef.assetGuid, outPath);
|
||||
if (resolved && ShouldTraceResourcePath(outPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
@@ -512,30 +510,27 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
}
|
||||
|
||||
Containers::String loadPath = path;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> ioLock(m_ioMutex);
|
||||
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
if (!m_resourceRoot.Empty() &&
|
||||
m_assetDatabase.EnsureArtifact(path, type, resolvedAsset) &&
|
||||
resolvedAsset.artifactReady) {
|
||||
loadPath = resolvedAsset.artifactMainPath;
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource artifact path=") +
|
||||
path +
|
||||
" artifact=" +
|
||||
loadPath);
|
||||
}
|
||||
} else if (ShouldTraceResourcePath(path)) {
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
if (!m_resourceRoot.Empty() &&
|
||||
m_assetImportService.EnsureArtifact(path, type, resolvedAsset) &&
|
||||
resolvedAsset.artifactReady) {
|
||||
m_projectAssetIndex.RememberResolvedPath(resolvedAsset.assetGuid, resolvedAsset.relativePath);
|
||||
loadPath = resolvedAsset.artifactMainPath;
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource direct path=") +
|
||||
Containers::String("[ResourceManager] LoadResource artifact path=") +
|
||||
path +
|
||||
" loadPath=" +
|
||||
" artifact=" +
|
||||
loadPath);
|
||||
}
|
||||
} else if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource direct path=") +
|
||||
path +
|
||||
" loadPath=" +
|
||||
loadPath);
|
||||
}
|
||||
|
||||
LoadResult result;
|
||||
|
||||
@@ -16,10 +16,12 @@ void InputManager::Initialize(void* platformWindowHandle) {
|
||||
|
||||
m_keyDownThisFrame.resize(256, false);
|
||||
m_keyDownLastFrame.resize(256, false);
|
||||
m_keyUpThisFrame.resize(256, false);
|
||||
m_keyDown.resize(256, false);
|
||||
|
||||
m_mouseButtonDownThisFrame.resize(5, false);
|
||||
m_mouseButtonDownLastFrame.resize(5, false);
|
||||
m_mouseButtonUpThisFrame.resize(5, false);
|
||||
m_mouseButtonDown.resize(5, false);
|
||||
|
||||
m_buttonDownThisFrame.resize(32, false);
|
||||
@@ -39,14 +41,21 @@ void InputManager::Initialize(void* platformWindowHandle) {
|
||||
}
|
||||
|
||||
void InputManager::Shutdown() {
|
||||
m_mousePosition = Math::Vector2::Zero();
|
||||
m_mouseDelta = Math::Vector2::Zero();
|
||||
m_mouseScrollDelta = 0.0f;
|
||||
m_touches.clear();
|
||||
|
||||
if (!m_initialized) return;
|
||||
|
||||
m_keyDownThisFrame.clear();
|
||||
m_keyDownLastFrame.clear();
|
||||
m_keyUpThisFrame.clear();
|
||||
m_keyDown.clear();
|
||||
|
||||
m_mouseButtonDownThisFrame.clear();
|
||||
m_mouseButtonDownLastFrame.clear();
|
||||
m_mouseButtonUpThisFrame.clear();
|
||||
m_mouseButtonDown.clear();
|
||||
|
||||
m_axes.clear();
|
||||
@@ -64,10 +73,14 @@ void InputManager::Update(float deltaTime) {
|
||||
m_keyDownLastFrame = m_keyDownThisFrame;
|
||||
m_keyDownThisFrame.clear();
|
||||
m_keyDownThisFrame.resize(256, false);
|
||||
m_keyUpThisFrame.clear();
|
||||
m_keyUpThisFrame.resize(256, false);
|
||||
|
||||
m_mouseButtonDownLastFrame = m_mouseButtonDownThisFrame;
|
||||
m_mouseButtonDownThisFrame.clear();
|
||||
m_mouseButtonDownThisFrame.resize(5, false);
|
||||
m_mouseButtonUpThisFrame.clear();
|
||||
m_mouseButtonUpThisFrame.resize(5, false);
|
||||
|
||||
m_buttonDownLastFrame = m_buttonDownThisFrame;
|
||||
m_buttonDownThisFrame.clear();
|
||||
@@ -104,6 +117,13 @@ bool InputManager::IsKeyPressed(KeyCode key) const {
|
||||
return m_keyDownThisFrame[index] && !m_keyDownLastFrame[index];
|
||||
}
|
||||
|
||||
bool InputManager::IsKeyReleased(KeyCode key) const {
|
||||
if (!m_initialized) return false;
|
||||
size_t index = GetKeyIndex(key);
|
||||
if (index >= m_keyUpThisFrame.size()) return false;
|
||||
return m_keyUpThisFrame[index];
|
||||
}
|
||||
|
||||
Math::Vector2 InputManager::GetMousePosition() const {
|
||||
return m_mousePosition;
|
||||
}
|
||||
@@ -135,6 +155,13 @@ bool InputManager::IsMouseButtonClicked(MouseButton button) const {
|
||||
return m_mouseButtonDownThisFrame[index] && !m_mouseButtonDownLastFrame[index];
|
||||
}
|
||||
|
||||
bool InputManager::IsMouseButtonReleased(MouseButton button) const {
|
||||
if (!m_initialized) return false;
|
||||
size_t index = GetMouseButtonIndex(button);
|
||||
if (index >= m_mouseButtonUpThisFrame.size()) return false;
|
||||
return m_mouseButtonUpThisFrame[index];
|
||||
}
|
||||
|
||||
int InputManager::GetTouchCount() const {
|
||||
return static_cast<int>(m_touches.size());
|
||||
}
|
||||
@@ -170,10 +197,10 @@ float InputManager::GetAxisRaw(const Containers::String& axisName) const {
|
||||
const auto& axis = it->second;
|
||||
float value = 0.0f;
|
||||
|
||||
if (axis.GetPositiveKey() != KeyCode::None && IsKeyPressed(axis.GetPositiveKey())) {
|
||||
if (axis.GetPositiveKey() != KeyCode::None && IsKeyDown(axis.GetPositiveKey())) {
|
||||
value += 1.0f;
|
||||
}
|
||||
if (axis.GetNegativeKey() != KeyCode::None && IsKeyPressed(axis.GetNegativeKey())) {
|
||||
if (axis.GetNegativeKey() != KeyCode::None && IsKeyDown(axis.GetNegativeKey())) {
|
||||
value -= 1.0f;
|
||||
}
|
||||
|
||||
@@ -194,8 +221,34 @@ bool InputManager::GetButtonDown(const Containers::String& buttonName) const {
|
||||
|
||||
bool InputManager::GetButtonUp(const Containers::String& buttonName) const {
|
||||
auto it = m_buttons.find(buttonName);
|
||||
if (it == m_buttons.end()) return true;
|
||||
return IsKeyUp(it->second);
|
||||
if (it == m_buttons.end()) return false;
|
||||
return IsKeyReleased(it->second);
|
||||
}
|
||||
|
||||
bool InputManager::IsAnyKeyDown() const {
|
||||
if (!m_initialized) return false;
|
||||
|
||||
return std::any_of(
|
||||
m_keyDown.begin(),
|
||||
m_keyDown.end(),
|
||||
[](bool isDown) { return isDown; })
|
||||
|| std::any_of(
|
||||
m_mouseButtonDown.begin(),
|
||||
m_mouseButtonDown.end(),
|
||||
[](bool isDown) { return isDown; });
|
||||
}
|
||||
|
||||
bool InputManager::IsAnyKeyPressed() const {
|
||||
if (!m_initialized) return false;
|
||||
|
||||
return std::any_of(
|
||||
m_keyDownThisFrame.begin(),
|
||||
m_keyDownThisFrame.end(),
|
||||
[](bool isPressed) { return isPressed; })
|
||||
|| std::any_of(
|
||||
m_mouseButtonDownThisFrame.begin(),
|
||||
m_mouseButtonDownThisFrame.end(),
|
||||
[](bool isPressed) { return isPressed; });
|
||||
}
|
||||
|
||||
void InputManager::RegisterAxis(const InputAxis& axis) {
|
||||
@@ -238,6 +291,7 @@ void InputManager::ProcessKeyUp(KeyCode key, bool alt, bool ctrl, bool shift, bo
|
||||
if (index >= m_keyDown.size()) return;
|
||||
|
||||
m_keyDown[index] = false;
|
||||
m_keyUpThisFrame[index] = true;
|
||||
|
||||
KeyEvent event;
|
||||
event.keyCode = key;
|
||||
@@ -274,6 +328,8 @@ void InputManager::ProcessMouseButton(MouseButton button, bool pressed, int x, i
|
||||
m_mouseButtonDown[index] = pressed;
|
||||
if (pressed) {
|
||||
m_mouseButtonDownThisFrame[index] = true;
|
||||
} else {
|
||||
m_mouseButtonUpThisFrame[index] = true;
|
||||
}
|
||||
|
||||
MouseButtonEvent event;
|
||||
|
||||
@@ -231,6 +231,25 @@ bool CameraRenderer::Render(
|
||||
}
|
||||
|
||||
ShutdownPassSequence(&builtinPostProcessPasses, builtinPostProcessPassesInitialized);
|
||||
|
||||
bool overlayPassesInitialized = false;
|
||||
if (!InitializePassSequence(
|
||||
request.overlayPasses,
|
||||
request.context,
|
||||
overlayPassesInitialized)) {
|
||||
ShutdownPassSequence(request.postScenePasses, postScenePassesInitialized);
|
||||
ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized);
|
||||
return false;
|
||||
}
|
||||
if (request.overlayPasses != nullptr &&
|
||||
!request.overlayPasses->Execute(passContext)) {
|
||||
ShutdownPassSequence(request.overlayPasses, overlayPassesInitialized);
|
||||
ShutdownPassSequence(request.postScenePasses, postScenePassesInitialized);
|
||||
ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized);
|
||||
return false;
|
||||
}
|
||||
|
||||
ShutdownPassSequence(request.overlayPasses, overlayPassesInitialized);
|
||||
ShutdownPassSequence(request.postScenePasses, postScenePassesInitialized);
|
||||
ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
@@ -64,6 +65,56 @@ Containers::String NormalizePathString(const std::filesystem::path& path) {
|
||||
return Containers::String(path.lexically_normal().generic_string().c_str());
|
||||
}
|
||||
|
||||
bool IsProjectRelativePath(const std::filesystem::path& path) {
|
||||
const std::string generic = path.generic_string();
|
||||
return !generic.empty() &&
|
||||
generic != "." &&
|
||||
generic != ".." &&
|
||||
generic.rfind("../", 0) != 0;
|
||||
}
|
||||
|
||||
Containers::String ToProjectRelativeIfPossible(const std::filesystem::path& path) {
|
||||
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
|
||||
const std::filesystem::path normalizedPath = path.lexically_normal();
|
||||
if (!resourceRoot.Empty() && normalizedPath.is_absolute()) {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path relativePath =
|
||||
std::filesystem::relative(normalizedPath, std::filesystem::path(resourceRoot.CStr()), ec);
|
||||
if (!ec && IsProjectRelativePath(relativePath)) {
|
||||
return NormalizePathString(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return NormalizePathString(normalizedPath);
|
||||
}
|
||||
|
||||
Containers::String ResolveSourceDependencyPath(const Containers::String& dependencyPath,
|
||||
const Containers::String& sourcePath) {
|
||||
if (dependencyPath.Empty()) {
|
||||
return dependencyPath;
|
||||
}
|
||||
|
||||
std::filesystem::path dependencyFsPath(dependencyPath.CStr());
|
||||
if (dependencyFsPath.is_absolute()) {
|
||||
return NormalizePathString(dependencyFsPath);
|
||||
}
|
||||
|
||||
const std::filesystem::path sourceFsPath(sourcePath.CStr());
|
||||
if (sourceFsPath.is_absolute()) {
|
||||
return ToProjectRelativeIfPossible(sourceFsPath.parent_path() / dependencyFsPath);
|
||||
}
|
||||
|
||||
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
|
||||
if (!resourceRoot.Empty()) {
|
||||
return ToProjectRelativeIfPossible(
|
||||
std::filesystem::path(resourceRoot.CStr()) /
|
||||
sourceFsPath.parent_path() /
|
||||
dependencyFsPath);
|
||||
}
|
||||
|
||||
return NormalizePathString(sourceFsPath.parent_path() / dependencyFsPath);
|
||||
}
|
||||
|
||||
Containers::String ResolveArtifactDependencyPath(const Containers::String& dependencyPath,
|
||||
const Containers::String& ownerArtifactPath) {
|
||||
if (dependencyPath.Empty()) {
|
||||
@@ -358,6 +409,125 @@ bool TryParseTagMap(const std::string& objectText, Material* material) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryParseStringMapObject(
|
||||
const std::string& objectText,
|
||||
const std::function<void(const Containers::String&, const Containers::String&)>& onEntry) {
|
||||
if (!onEntry || objectText.empty() || objectText.front() != '{' || objectText.back() != '}') {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t pos = 1;
|
||||
while (pos < objectText.size()) {
|
||||
pos = SkipWhitespace(objectText, pos);
|
||||
if (pos >= objectText.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (objectText[pos] == '}') {
|
||||
return true;
|
||||
}
|
||||
|
||||
Containers::String key;
|
||||
if (!ParseQuotedString(objectText, pos, key, &pos)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pos = SkipWhitespace(objectText, pos);
|
||||
if (pos >= objectText.size() || objectText[pos] != ':') {
|
||||
return false;
|
||||
}
|
||||
|
||||
pos = SkipWhitespace(objectText, pos + 1);
|
||||
Containers::String value;
|
||||
if (!ParseQuotedString(objectText, pos, value, &pos)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
onEntry(key, value);
|
||||
|
||||
pos = SkipWhitespace(objectText, pos);
|
||||
if (pos >= objectText.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (objectText[pos] == ',') {
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (objectText[pos] == '}') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryApplyTexturePath(Material* material,
|
||||
const Containers::String& textureName,
|
||||
const Containers::String& texturePath) {
|
||||
if (material == nullptr || textureName.Empty() || texturePath.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
material->SetTexturePath(
|
||||
textureName,
|
||||
ResolveSourceDependencyPath(texturePath, material->GetPath()));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseMaterialTextureBindings(const std::string& jsonText, Material* material) {
|
||||
if (material == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char* const kKnownTextureKeys[] = {
|
||||
"baseColorTexture",
|
||||
"_BaseColorTexture",
|
||||
"_MainTex",
|
||||
"normalTexture",
|
||||
"_BumpMap",
|
||||
"specularTexture",
|
||||
"emissiveTexture",
|
||||
"metallicTexture",
|
||||
"roughnessTexture",
|
||||
"occlusionTexture",
|
||||
"opacityTexture"
|
||||
};
|
||||
|
||||
for (const char* key : kKnownTextureKeys) {
|
||||
if (!HasKey(jsonText, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Containers::String texturePath;
|
||||
if (!TryParseStringValue(jsonText, key, texturePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TryApplyTexturePath(material, Containers::String(key), texturePath);
|
||||
}
|
||||
|
||||
if (HasKey(jsonText, "textures")) {
|
||||
std::string texturesObject;
|
||||
if (!TryExtractObject(jsonText, "textures", texturesObject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseStringMapObject(
|
||||
texturesObject,
|
||||
[material](const Containers::String& name, const Containers::String& value) {
|
||||
TryApplyTexturePath(material, name, value);
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseCullMode(const Containers::String& value, MaterialCullMode& outMode) {
|
||||
const Containers::String normalized = value.Trim().ToLower();
|
||||
if (normalized == "none" || normalized == "off") {
|
||||
@@ -953,6 +1123,10 @@ bool MaterialLoader::ParseMaterialData(const Containers::Array<Core::uint8>& dat
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryParseMaterialTextureBindings(jsonText, material)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "Components/MeshRendererComponent.h"
|
||||
#include "Components/TransformComponent.h"
|
||||
#include "Debug/Logger.h"
|
||||
#include "Input/InputManager.h"
|
||||
#include "Scene/Scene.h"
|
||||
#include "Scripting/ScriptComponent.h"
|
||||
#include "Scripting/ScriptEngine.h"
|
||||
@@ -356,6 +357,92 @@ float InternalCall_Time_GetDeltaTime() {
|
||||
return GetInternalCallDeltaTime();
|
||||
}
|
||||
|
||||
float InternalCall_Time_GetFixedDeltaTime() {
|
||||
return ScriptEngine::Get().GetRuntimeFixedDeltaTime();
|
||||
}
|
||||
|
||||
mono_bool InternalCall_Input_GetKey(int32_t keyCode) {
|
||||
return XCEngine::Input::InputManager::Get().IsKeyDown(
|
||||
static_cast<XCEngine::Input::KeyCode>(keyCode)) ? 1 : 0;
|
||||
}
|
||||
|
||||
mono_bool InternalCall_Input_GetKeyDown(int32_t keyCode) {
|
||||
return XCEngine::Input::InputManager::Get().IsKeyPressed(
|
||||
static_cast<XCEngine::Input::KeyCode>(keyCode)) ? 1 : 0;
|
||||
}
|
||||
|
||||
mono_bool InternalCall_Input_GetKeyUp(int32_t keyCode) {
|
||||
return XCEngine::Input::InputManager::Get().IsKeyReleased(
|
||||
static_cast<XCEngine::Input::KeyCode>(keyCode)) ? 1 : 0;
|
||||
}
|
||||
|
||||
mono_bool InternalCall_Input_GetMouseButton(int32_t button) {
|
||||
return XCEngine::Input::InputManager::Get().IsMouseButtonDown(
|
||||
static_cast<XCEngine::Input::MouseButton>(button)) ? 1 : 0;
|
||||
}
|
||||
|
||||
mono_bool InternalCall_Input_GetMouseButtonDown(int32_t button) {
|
||||
return XCEngine::Input::InputManager::Get().IsMouseButtonClicked(
|
||||
static_cast<XCEngine::Input::MouseButton>(button)) ? 1 : 0;
|
||||
}
|
||||
|
||||
mono_bool InternalCall_Input_GetMouseButtonUp(int32_t button) {
|
||||
return XCEngine::Input::InputManager::Get().IsMouseButtonReleased(
|
||||
static_cast<XCEngine::Input::MouseButton>(button)) ? 1 : 0;
|
||||
}
|
||||
|
||||
mono_bool InternalCall_Input_GetButton(MonoString* buttonName) {
|
||||
return XCEngine::Input::InputManager::Get().GetButton(
|
||||
XCEngine::Containers::String(MonoStringToUtf8(buttonName).c_str())) ? 1 : 0;
|
||||
}
|
||||
|
||||
mono_bool InternalCall_Input_GetButtonDown(MonoString* buttonName) {
|
||||
return XCEngine::Input::InputManager::Get().GetButtonDown(
|
||||
XCEngine::Containers::String(MonoStringToUtf8(buttonName).c_str())) ? 1 : 0;
|
||||
}
|
||||
|
||||
mono_bool InternalCall_Input_GetButtonUp(MonoString* buttonName) {
|
||||
return XCEngine::Input::InputManager::Get().GetButtonUp(
|
||||
XCEngine::Containers::String(MonoStringToUtf8(buttonName).c_str())) ? 1 : 0;
|
||||
}
|
||||
|
||||
float InternalCall_Input_GetAxis(MonoString* axisName) {
|
||||
return XCEngine::Input::InputManager::Get().GetAxis(
|
||||
XCEngine::Containers::String(MonoStringToUtf8(axisName).c_str()));
|
||||
}
|
||||
|
||||
float InternalCall_Input_GetAxisRaw(MonoString* axisName) {
|
||||
return XCEngine::Input::InputManager::Get().GetAxisRaw(
|
||||
XCEngine::Containers::String(MonoStringToUtf8(axisName).c_str()));
|
||||
}
|
||||
|
||||
mono_bool InternalCall_Input_GetAnyKey() {
|
||||
return XCEngine::Input::InputManager::Get().IsAnyKeyDown() ? 1 : 0;
|
||||
}
|
||||
|
||||
mono_bool InternalCall_Input_GetAnyKeyDown() {
|
||||
return XCEngine::Input::InputManager::Get().IsAnyKeyPressed() ? 1 : 0;
|
||||
}
|
||||
|
||||
void InternalCall_Input_GetMousePosition(XCEngine::Math::Vector3* outPosition) {
|
||||
if (!outPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const XCEngine::Math::Vector2 position = XCEngine::Input::InputManager::Get().GetMousePosition();
|
||||
*outPosition = XCEngine::Math::Vector3(position.x, position.y, 0.0f);
|
||||
}
|
||||
|
||||
void InternalCall_Input_GetMouseScrollDelta(XCEngine::Math::Vector2* outDelta) {
|
||||
if (!outDelta) {
|
||||
return;
|
||||
}
|
||||
|
||||
*outDelta = XCEngine::Math::Vector2(
|
||||
0.0f,
|
||||
XCEngine::Input::InputManager::Get().GetMouseScrollDelta());
|
||||
}
|
||||
|
||||
MonoString* InternalCall_GameObject_GetName(uint64_t gameObjectUUID) {
|
||||
Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID);
|
||||
return mono_string_new(
|
||||
@@ -1131,6 +1218,22 @@ void RegisterInternalCalls() {
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Debug_LogWarning", reinterpret_cast<const void*>(&InternalCall_Debug_LogWarning));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Debug_LogError", reinterpret_cast<const void*>(&InternalCall_Debug_LogError));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Time_GetDeltaTime", reinterpret_cast<const void*>(&InternalCall_Time_GetDeltaTime));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Time_GetFixedDeltaTime", reinterpret_cast<const void*>(&InternalCall_Time_GetFixedDeltaTime));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetKey", reinterpret_cast<const void*>(&InternalCall_Input_GetKey));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetKeyDown", reinterpret_cast<const void*>(&InternalCall_Input_GetKeyDown));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetKeyUp", reinterpret_cast<const void*>(&InternalCall_Input_GetKeyUp));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetMouseButton", reinterpret_cast<const void*>(&InternalCall_Input_GetMouseButton));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetMouseButtonDown", reinterpret_cast<const void*>(&InternalCall_Input_GetMouseButtonDown));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetMouseButtonUp", reinterpret_cast<const void*>(&InternalCall_Input_GetMouseButtonUp));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetButton", reinterpret_cast<const void*>(&InternalCall_Input_GetButton));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetButtonDown", reinterpret_cast<const void*>(&InternalCall_Input_GetButtonDown));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetButtonUp", reinterpret_cast<const void*>(&InternalCall_Input_GetButtonUp));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetAxis", reinterpret_cast<const void*>(&InternalCall_Input_GetAxis));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetAxisRaw", reinterpret_cast<const void*>(&InternalCall_Input_GetAxisRaw));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetAnyKey", reinterpret_cast<const void*>(&InternalCall_Input_GetAnyKey));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetAnyKeyDown", reinterpret_cast<const void*>(&InternalCall_Input_GetAnyKeyDown));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetMousePosition", reinterpret_cast<const void*>(&InternalCall_Input_GetMousePosition));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::Input_GetMouseScrollDelta", reinterpret_cast<const void*>(&InternalCall_Input_GetMouseScrollDelta));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetName", reinterpret_cast<const void*>(&InternalCall_GameObject_GetName));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::GameObject_SetName", reinterpret_cast<const void*>(&InternalCall_GameObject_SetName));
|
||||
mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetActiveSelf", reinterpret_cast<const void*>(&InternalCall_GameObject_GetActiveSelf));
|
||||
@@ -1281,20 +1384,57 @@ bool MonoScriptRuntime::IsClassAvailable(
|
||||
return FindClassMetadata(assemblyName, namespaceName, className) != nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::string> MonoScriptRuntime::GetScriptClassNames(const std::string& assemblyName) const {
|
||||
std::vector<std::string> classNames;
|
||||
classNames.reserve(m_classes.size());
|
||||
bool MonoScriptRuntime::TryGetAvailableScriptClasses(
|
||||
std::vector<ScriptClassDescriptor>& outClasses) const {
|
||||
outClasses.clear();
|
||||
if (!m_initialized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outClasses.reserve(m_classes.size());
|
||||
for (const auto& [key, metadata] : m_classes) {
|
||||
(void)key;
|
||||
if (!assemblyName.empty() && metadata.assemblyName != assemblyName) {
|
||||
outClasses.push_back(
|
||||
ScriptClassDescriptor{
|
||||
metadata.assemblyName,
|
||||
metadata.namespaceName,
|
||||
metadata.className
|
||||
});
|
||||
}
|
||||
|
||||
std::sort(
|
||||
outClasses.begin(),
|
||||
outClasses.end(),
|
||||
[](const ScriptClassDescriptor& lhs, const ScriptClassDescriptor& rhs) {
|
||||
if (lhs.assemblyName != rhs.assemblyName) {
|
||||
return lhs.assemblyName < rhs.assemblyName;
|
||||
}
|
||||
if (lhs.namespaceName != rhs.namespaceName) {
|
||||
return lhs.namespaceName < rhs.namespaceName;
|
||||
}
|
||||
return lhs.className < rhs.className;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> MonoScriptRuntime::GetScriptClassNames(const std::string& assemblyName) const {
|
||||
std::vector<ScriptClassDescriptor> classes;
|
||||
if (!TryGetAvailableScriptClasses(classes)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> classNames;
|
||||
classNames.reserve(classes.size());
|
||||
|
||||
for (const ScriptClassDescriptor& descriptor : classes) {
|
||||
if (!assemblyName.empty() && descriptor.assemblyName != assemblyName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
classNames.push_back(metadata.fullName);
|
||||
classNames.push_back(descriptor.GetFullName());
|
||||
}
|
||||
|
||||
std::sort(classNames.begin(), classNames.end());
|
||||
return classNames;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ void NullScriptRuntime::OnRuntimeStop(Components::Scene* scene) {
|
||||
(void)scene;
|
||||
}
|
||||
|
||||
bool NullScriptRuntime::TryGetAvailableScriptClasses(
|
||||
std::vector<ScriptClassDescriptor>& outClasses) const {
|
||||
outClasses.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NullScriptRuntime::TryGetClassFieldMetadata(
|
||||
const std::string& assemblyName,
|
||||
const std::string& namespaceName,
|
||||
|
||||
@@ -25,23 +25,36 @@ ScriptComponent::ScriptComponent()
|
||||
|
||||
void ScriptComponent::SetScriptClass(const std::string& namespaceName, const std::string& className) {
|
||||
const bool hadScriptClass = HasScriptClass();
|
||||
const bool changed = m_namespaceName != namespaceName || m_className != className;
|
||||
m_namespaceName = namespaceName;
|
||||
m_className = className;
|
||||
if (!hadScriptClass && HasScriptClass()) {
|
||||
ScriptEngine::Get().OnScriptComponentEnabled(this);
|
||||
} else if (hadScriptClass && changed) {
|
||||
ScriptEngine::Get().OnScriptComponentClassChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptComponent::SetScriptClass(const std::string& assemblyName, const std::string& namespaceName, const std::string& className) {
|
||||
const bool hadScriptClass = HasScriptClass();
|
||||
const bool changed =
|
||||
m_assemblyName != assemblyName ||
|
||||
m_namespaceName != namespaceName ||
|
||||
m_className != className;
|
||||
m_assemblyName = assemblyName;
|
||||
m_namespaceName = namespaceName;
|
||||
m_className = className;
|
||||
if (!hadScriptClass && HasScriptClass()) {
|
||||
ScriptEngine::Get().OnScriptComponentEnabled(this);
|
||||
} else if (hadScriptClass && changed) {
|
||||
ScriptEngine::Get().OnScriptComponentClassChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptComponent::ClearScriptClass() {
|
||||
SetScriptClass(m_assemblyName, std::string(), std::string());
|
||||
}
|
||||
|
||||
std::string ScriptComponent::GetFullClassName() const {
|
||||
if (m_className.empty()) {
|
||||
return std::string();
|
||||
|
||||
@@ -63,8 +63,19 @@ void ScriptEngine::SetRuntime(IScriptRuntime* runtime) {
|
||||
m_runtime = runtime ? runtime : &m_nullRuntime;
|
||||
}
|
||||
|
||||
void ScriptEngine::SetRuntimeFixedDeltaTime(float fixedDeltaTime) {
|
||||
if (fixedDeltaTime > 0.0f) {
|
||||
m_runtimeFixedDeltaTime = fixedDeltaTime;
|
||||
return;
|
||||
}
|
||||
|
||||
m_runtimeFixedDeltaTime = DefaultFixedDeltaTime;
|
||||
}
|
||||
|
||||
void ScriptEngine::OnRuntimeStart(Components::Scene* scene) {
|
||||
const float configuredFixedDeltaTime = m_runtimeFixedDeltaTime;
|
||||
OnRuntimeStop();
|
||||
m_runtimeFixedDeltaTime = configuredFixedDeltaTime;
|
||||
|
||||
if (!scene) {
|
||||
return;
|
||||
@@ -109,6 +120,7 @@ void ScriptEngine::OnRuntimeStop() {
|
||||
m_runtimeScene = nullptr;
|
||||
m_scriptStates.clear();
|
||||
m_scriptOrder.clear();
|
||||
m_runtimeFixedDeltaTime = DefaultFixedDeltaTime;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -125,6 +137,7 @@ void ScriptEngine::OnRuntimeStop() {
|
||||
m_scriptOrder.clear();
|
||||
m_runtimeRunning = false;
|
||||
m_runtimeScene = nullptr;
|
||||
m_runtimeFixedDeltaTime = DefaultFixedDeltaTime;
|
||||
m_runtime->OnRuntimeStop(stoppedScene);
|
||||
}
|
||||
|
||||
@@ -239,6 +252,33 @@ void ScriptEngine::OnScriptComponentDestroyed(ScriptComponent* component) {
|
||||
StopTrackingScript(*state, false);
|
||||
}
|
||||
|
||||
void ScriptEngine::OnScriptComponentClassChanged(ScriptComponent* component) {
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_runtimeRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ScriptInstanceState* state = FindState(component)) {
|
||||
StopTrackingScript(*state, false);
|
||||
}
|
||||
|
||||
if (!component->HasScriptClass()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScriptInstanceState* state = TrackScriptComponent(component);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ShouldScriptRun(*state)) {
|
||||
EnsureScriptReady(*state, true);
|
||||
}
|
||||
}
|
||||
|
||||
bool ScriptEngine::HasTrackedScriptComponent(const ScriptComponent* component) const {
|
||||
return FindState(component) != nullptr;
|
||||
}
|
||||
@@ -248,6 +288,45 @@ bool ScriptEngine::HasRuntimeInstance(const ScriptComponent* component) const {
|
||||
return state && state->instanceCreated;
|
||||
}
|
||||
|
||||
bool ScriptEngine::TryGetAvailableScriptClasses(
|
||||
std::vector<ScriptClassDescriptor>& outClasses,
|
||||
const std::string& assemblyName) const {
|
||||
outClasses.clear();
|
||||
|
||||
std::vector<ScriptClassDescriptor> runtimeClasses;
|
||||
if (!m_runtime->TryGetAvailableScriptClasses(runtimeClasses)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outClasses.reserve(runtimeClasses.size());
|
||||
for (const ScriptClassDescriptor& descriptor : runtimeClasses) {
|
||||
if (!assemblyName.empty() && descriptor.assemblyName != assemblyName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (descriptor.className.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
outClasses.push_back(descriptor);
|
||||
}
|
||||
|
||||
std::sort(
|
||||
outClasses.begin(),
|
||||
outClasses.end(),
|
||||
[](const ScriptClassDescriptor& lhs, const ScriptClassDescriptor& rhs) {
|
||||
if (lhs.assemblyName != rhs.assemblyName) {
|
||||
return lhs.assemblyName < rhs.assemblyName;
|
||||
}
|
||||
if (lhs.namespaceName != rhs.namespaceName) {
|
||||
return lhs.namespaceName < rhs.namespaceName;
|
||||
}
|
||||
return lhs.className < rhs.className;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScriptEngine::TrySetScriptFieldValue(
|
||||
ScriptComponent* component,
|
||||
const std::string& fieldName,
|
||||
|
||||
@@ -37,6 +37,31 @@ set(XCENGINE_MONO_MSCORLIB_PATH "${XCENGINE_MONO_CORLIB_DIR}/mscorlib.dll")
|
||||
|
||||
set(XCENGINE_SCRIPT_CORE_DLL "${XCENGINE_MANAGED_OUTPUT_DIR}/XCEngine.ScriptCore.dll" CACHE FILEPATH "Generated XCEngine.ScriptCore assembly")
|
||||
set(XCENGINE_GAME_SCRIPTS_DLL "${XCENGINE_MANAGED_OUTPUT_DIR}/GameScripts.dll" CACHE FILEPATH "Generated GameScripts assembly")
|
||||
set(
|
||||
XCENGINE_PROJECT_ASSETS_DIR
|
||||
"${CMAKE_SOURCE_DIR}/project/Assets"
|
||||
CACHE PATH
|
||||
"Project asset root scanned for user C# scripts")
|
||||
set(
|
||||
XCENGINE_PROJECT_MANAGED_OUTPUT_DIR
|
||||
"${CMAKE_SOURCE_DIR}/project/Library/ScriptAssemblies"
|
||||
CACHE PATH
|
||||
"Output directory for project managed assemblies")
|
||||
set(
|
||||
XCENGINE_PROJECT_SCRIPT_CORE_DLL
|
||||
"${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}/XCEngine.ScriptCore.dll"
|
||||
CACHE FILEPATH
|
||||
"Generated script core assembly copied into the project script assembly directory")
|
||||
set(
|
||||
XCENGINE_PROJECT_GAME_SCRIPTS_DLL
|
||||
"${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}/GameScripts.dll"
|
||||
CACHE FILEPATH
|
||||
"Generated project game scripts assembly")
|
||||
set(
|
||||
XCENGINE_PROJECT_MONO_MSCORLIB_PATH
|
||||
"${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}/mscorlib.dll"
|
||||
CACHE FILEPATH
|
||||
"Mono corlib copied into the project script assembly directory")
|
||||
|
||||
foreach(XCENGINE_REQUIRED_PATH
|
||||
"${XCENGINE_CSC_DLL}"
|
||||
@@ -55,7 +80,9 @@ set(XCENGINE_SCRIPT_CORE_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Component.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Debug.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/GameObject.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Input.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/InternalCalls.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/KeyCode.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Light.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MeshFilter.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MeshRenderer.cs
|
||||
@@ -76,15 +103,33 @@ set(XCENGINE_GAME_SCRIPT_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ScriptComponentApiProbe.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/RuntimeGameObjectProbe.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/HierarchyProbe.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/InputProbe.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/LifecycleProbe.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshComponentProbe.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererEdgeCaseProbe.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TickLogProbe.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformConversionProbe.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformMotionProbe.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformOrientationProbe.cs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformSpaceProbe.cs
|
||||
)
|
||||
|
||||
file(
|
||||
GLOB_RECURSE
|
||||
XCENGINE_PROJECT_GAME_SCRIPT_SOURCES
|
||||
CONFIGURE_DEPENDS
|
||||
LIST_DIRECTORIES FALSE
|
||||
"${XCENGINE_PROJECT_ASSETS_DIR}/*.cs")
|
||||
list(SORT XCENGINE_PROJECT_GAME_SCRIPT_SOURCES)
|
||||
|
||||
if(NOT XCENGINE_PROJECT_GAME_SCRIPT_SOURCES)
|
||||
set(XCENGINE_PROJECT_SCRIPT_PLACEHOLDER "${CMAKE_CURRENT_BINARY_DIR}/Generated/EmptyProjectGameScripts.cs")
|
||||
file(GENERATE
|
||||
OUTPUT "${XCENGINE_PROJECT_SCRIPT_PLACEHOLDER}"
|
||||
CONTENT "namespace XCEngine.Generated { public static class EmptyProjectGameScriptsMarker {} }\n")
|
||||
set(XCENGINE_PROJECT_GAME_SCRIPT_SOURCES "${XCENGINE_PROJECT_SCRIPT_PLACEHOLDER}")
|
||||
endif()
|
||||
|
||||
set(XCENGINE_MANAGED_FRAMEWORK_REFERENCES
|
||||
/reference:${XCENGINE_NET472_REFERENCE_DIR}/mscorlib.dll
|
||||
/reference:${XCENGINE_NET472_REFERENCE_DIR}/System.dll
|
||||
@@ -140,3 +185,47 @@ add_custom_target(
|
||||
${XCENGINE_MANAGED_OUTPUT_DIR}/mscorlib.dll
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${XCENGINE_PROJECT_SCRIPT_CORE_DLL}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${XCENGINE_SCRIPT_CORE_DLL}
|
||||
${XCENGINE_PROJECT_SCRIPT_CORE_DLL}
|
||||
DEPENDS ${XCENGINE_SCRIPT_CORE_DLL}
|
||||
VERBATIM
|
||||
COMMENT "Copying XCEngine.ScriptCore.dll into the project script assembly directory")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${XCENGINE_PROJECT_GAME_SCRIPTS_DLL}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}
|
||||
COMMAND ${XCENGINE_DOTNET_EXECUTABLE} ${XCENGINE_CSC_DLL}
|
||||
/nologo
|
||||
/target:library
|
||||
/langversion:latest
|
||||
/nostdlib+
|
||||
/out:${XCENGINE_PROJECT_GAME_SCRIPTS_DLL}
|
||||
${XCENGINE_MANAGED_FRAMEWORK_REFERENCES}
|
||||
/reference:${XCENGINE_PROJECT_SCRIPT_CORE_DLL}
|
||||
${XCENGINE_PROJECT_GAME_SCRIPT_SOURCES}
|
||||
DEPENDS ${XCENGINE_PROJECT_GAME_SCRIPT_SOURCES} ${XCENGINE_PROJECT_SCRIPT_CORE_DLL}
|
||||
VERBATIM
|
||||
COMMENT "Building project GameScripts.dll from project asset scripts")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${XCENGINE_PROJECT_MONO_MSCORLIB_PATH}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${XCENGINE_MONO_MSCORLIB_PATH}
|
||||
${XCENGINE_PROJECT_MONO_MSCORLIB_PATH}
|
||||
DEPENDS ${XCENGINE_MONO_MSCORLIB_PATH}
|
||||
VERBATIM
|
||||
COMMENT "Copying mscorlib.dll into the project script assembly directory")
|
||||
|
||||
add_custom_target(
|
||||
xcengine_project_managed_assemblies ALL
|
||||
DEPENDS
|
||||
${XCENGINE_PROJECT_SCRIPT_CORE_DLL}
|
||||
${XCENGINE_PROJECT_GAME_SCRIPTS_DLL}
|
||||
${XCENGINE_PROJECT_MONO_MSCORLIB_PATH}
|
||||
)
|
||||
|
||||
|
||||
52
managed/GameScripts/InputProbe.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using XCEngine;
|
||||
|
||||
namespace Gameplay
|
||||
{
|
||||
public sealed class InputProbe : MonoBehaviour
|
||||
{
|
||||
public int UpdateCount;
|
||||
public bool ObservedKeyA;
|
||||
public bool ObservedKeyADown;
|
||||
public bool ObservedKeyAUp;
|
||||
public bool ObservedKeySpace;
|
||||
public bool ObservedJump;
|
||||
public bool ObservedJumpDown;
|
||||
public bool ObservedJumpUp;
|
||||
public bool ObservedFire1;
|
||||
public bool ObservedFire1Down;
|
||||
public bool ObservedFire1Up;
|
||||
public bool ObservedAnyKey;
|
||||
public bool ObservedAnyKeyDown;
|
||||
public bool ObservedLeftMouse;
|
||||
public bool ObservedLeftMouseDown;
|
||||
public bool ObservedLeftMouseUp;
|
||||
public float ObservedHorizontal;
|
||||
public float ObservedHorizontalRaw;
|
||||
public Vector3 ObservedMousePosition;
|
||||
public Vector2 ObservedMouseScrollDelta;
|
||||
|
||||
public void Update()
|
||||
{
|
||||
UpdateCount += 1;
|
||||
ObservedKeyA = Input.GetKey(KeyCode.A);
|
||||
ObservedKeyADown = Input.GetKeyDown(KeyCode.A);
|
||||
ObservedKeyAUp = Input.GetKeyUp(KeyCode.A);
|
||||
ObservedKeySpace = Input.GetKey(KeyCode.Space);
|
||||
ObservedJump = Input.GetButton("Jump");
|
||||
ObservedJumpDown = Input.GetButtonDown("Jump");
|
||||
ObservedJumpUp = Input.GetButtonUp("Jump");
|
||||
ObservedFire1 = Input.GetButton("Fire1");
|
||||
ObservedFire1Down = Input.GetButtonDown("Fire1");
|
||||
ObservedFire1Up = Input.GetButtonUp("Fire1");
|
||||
ObservedAnyKey = Input.anyKey;
|
||||
ObservedAnyKeyDown = Input.anyKeyDown;
|
||||
ObservedLeftMouse = Input.GetMouseButton(0);
|
||||
ObservedLeftMouseDown = Input.GetMouseButtonDown(0);
|
||||
ObservedLeftMouseUp = Input.GetMouseButtonUp(0);
|
||||
ObservedHorizontal = Input.GetAxis("Horizontal");
|
||||
ObservedHorizontalRaw = Input.GetAxisRaw("Horizontal");
|
||||
ObservedMousePosition = Input.mousePosition;
|
||||
ObservedMouseScrollDelta = Input.mouseScrollDelta;
|
||||
}
|
||||
}
|
||||
}
|
||||