feat: expand editor scripting asset and viewport flow

This commit is contained in:
2026-04-03 13:22:30 +08:00
parent ed8c27fde2
commit a05d0b80a2
124 changed files with 10397 additions and 1737 deletions

395
AGENT.md
View File

@@ -1,211 +1,242 @@
# XCEngine Agent Guide # 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) 1. [AGENT.md](AGENT.md)
2. [README.md](README.md) 2. [README.md](README.md)
3. [docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md) 3. [docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md)
4. [docs/plan/Renderer模块设计与实现.md](docs/plan/Renderer模块设计与实现.md) 4. [docs/plan/Shader与Material系统下一阶段计划.md](docs/plan/Shader与Material系统下一阶段计划.md)
5. [tests/TEST_SPEC.md](tests/TEST_SPEC.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 设计原则基线 - 如果任务落在某个模块里,先读该模块的 `CMakeLists.txt` 和最近的测试目录。
- `Renderer模块设计与实现.md` 是当前渲染层演进方向 - `tests/TEST_SPEC.md` 仍然适合作为 GT 图规则和 RHI 边界说明,但 target 名称与目录变化时,始终以当前 `tests/CMakeLists.txt` 和子模块 `CMakeLists.txt` 为准。
- `TEST_SPEC.md` 是测试组织、GT 图规则和 CMake/CTest 入口基线
## 2. 当前工程状态 ## 3. 当前工程状态
### 2.1 引擎整体 ### 3.1 Engine 与工程布局
当前仓库已经不再处于“先把底层跑起来”的阶段,而是处于 当前仓库已经不“先把底层 sample 跑起来”的阶段,而是已经形成
- `RHI` 已具备可维护的三后端基线 - `RHI`
- `Rendering` 已形成正式模块,不再只是零散 sample 代码 - `Rendering`
- `Editor` 已能通过引擎渲染链路驱动 viewport - `Editor viewport`
- `Scripting` 已具备 Mono 托管程序集构建和基础运行时测试 - `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 根因。 当前 `engine/include/XCEngine/Rendering/``engine/src/Rendering/` 已经形成正式主链:
### 2.3 Rendering
当前 `engine/include/XCEngine/Rendering/``engine/src/Rendering/` 已经落地:
- `SceneRenderer`
- `CameraRenderer`
- `RenderPipeline`
- `RenderSceneExtractor` - `RenderSceneExtractor`
- `RenderResourceCache` - `RenderResourceCache`
- `SceneRenderRequestPlanner`
- `RenderSurface` - `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`
这意味着: 这意味着:
- 当前已经存在正式的场景渲染运行时 - editor 是宿主,不是第二套 renderer。
- 不要再把真实渲染逻辑塞回 `mvs/` 样例里长期存活 - 新的世界空间 overlay不应继续堆回 `SceneViewPanel.cpp` 的 ImGui world overlay 路径。
- 新渲染功能优先落在 `Rendering` 模块与配套测试中 - 优先沿 `overlayPasses -> overlay builder -> canonical frame data -> overlay pass` 方向扩展。
### 2.4 Editor ### 3.4 Scripting
当前 editor 的关键事实 当前脚本链路由三部分组成
- 它仍然是 `D3D12` 宿主应用 - `managed/XCEngine.ScriptCore/`
- Scene/Game viewport 已通过离屏纹理接入引擎 `SceneRenderer` - `managed/GameScripts/`
- `editor/src/Viewport/ViewportHostService.h` 当前直接依赖 `RHI::D3D12Device` - `project/Assets/**/*.cs`
- Scene view 相机控制已经有独立控制器与单测
因此当前要注意 构建结果分两类
- 引擎渲染逻辑应继续收敛在 `engine/Rendering` - `xcengine_managed_assemblies` 生成引擎示例程序集
- editor 只是宿主,不应复制一套独立 renderer - `xcengine_project_managed_assemblies` 生成项目脚本程序集,并复制到 `project/Library/ScriptAssemblies/`
- editor 侧若要继续做 viewport 能力,应尽量围绕 `RenderSurface` 和引擎场景渲染入口扩展
### 2.5 Scripting `ScriptEngine` 当前已经具备:
当前脚本链路: - 脚本类发现
- 字段元数据读取
- 默认值读取
- stored override 管理
- 运行时 managed field 同步
- `managed/XCEngine.ScriptCore/`:托管 API Inspector 侧已经存在 `ScriptComponentEditor`,因此脚本相关改动通常同时影响:
- `managed/GameScripts/`:托管测试 / 示例脚本
- `engine/include/XCEngine/Scripting/``engine/src/Scripting/`:原生运行时桥接
当前脚本构建依赖: - `engine/src/Scripting/`
- `managed/`
- `project/Assets/Scripts/`
- `editor/src/ComponentEditors/`
- `tests/Scripting/`
- `tests/Editor/test_script_component_editor_utils.cpp`
- .NET SDK ### 3.5 Tests
- `参考/Fermion/Fermion/external/mono` 下的 Mono 依赖
如果环境不完整,可以通过 `-DXCENGINE_ENABLE_MONO_SCRIPTING=OFF` 暂时关闭。 当前测试主目录包括:
## 3. 当前测试基线
### 3.1 测试树
当前主要测试模块:
- `tests/Core/`
- `tests/Memory/`
- `tests/Threading/`
- `tests/Debug/`
- `tests/Components/` - `tests/Components/`
- `tests/Core/`
- `tests/Debug/`
- `tests/Editor/`
- `tests/Input/`
- `tests/Memory/`
- `tests/Rendering/`
- `tests/Resources/`
- `tests/RHI/`
- `tests/Scene/` - `tests/Scene/`
- `tests/Scripting/` - `tests/Scripting/`
- `tests/Rendering/` - `tests/Threading/`
- `tests/RHI/`
- `tests/Resources/`
- `tests/Input/`
- `tests/Editor/`
### 3.2 RHI 测试分层 需要特别记住的聚合 target
`tests/RHI/` 当前分为五块: - `rhi_all_tests`
- `rendering_all_tests`
- `rendering_phase_regression`
- `editor_tests`
- `scripting_tests`
- `tests/RHI/unit/` ## 4. 不可忽视的硬约束
- `tests/RHI/integration/`
- `tests/RHI/D3D12/`
- `tests/RHI/OpenGL/`
- `tests/RHI/Vulkan/`
边界规则: ### 4.1 文档服从真实 checkout
- `tests/RHI/unit/``tests/RHI/integration/` 只能依赖公共 RHI 抽象 如果文档与当前目录结构、target 名称、代码事实冲突:
- 后端私有 API / 原生句柄 / 后端特有断言,只能进对应后端目录
- 如果抽象层测试必须 include 后端头才能过,优先修 RHI
### 3.3 RHI 抽象层集成测试 - 先信当前 checkout
- 再更新文档
当前抽象层场景: 不要沿用“计划中但未落地”的旧说法。
- `minimal` ### 4.2 RHI 抽象层与后端层必须分层
- `triangle`
- `quad`
- `sphere`
- `backpack`
必须遵守: `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/` ### 4.3 Editor 是宿主,不是第二套渲染器
- `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`
结论 如果 viewport、outline、picking 或 gizmo 有问题,优先判断
- 新渲染功能不需要再从零搭测试体系
- 直接往现有 `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 有问题,优先判断:
-`Rendering` 模块问题 -`Rendering` 模块问题
-`RenderSurface` / RHI 输出问题 -`RenderSurface` / RHI 输出问题
- 还是 editor 宿主接线问题 - 还是 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 配置 ### 5.1 配置
@@ -213,7 +244,7 @@
cmake -S . -B build -A x64 cmake -S . -B build -A x64
``` ```
如果 Mono 如果当前任务不需要 Mono
```bash ```bash
cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF 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 ```bash
cmake --build build --config Debug --target XCEngine 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_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 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 editor_tests
cmake --build build --config Debug --target scripting_tests cmake --build build --config Debug --target scripting_tests
``` ```
补充: ### 5.3 按改动类型选择验证
- 编辑器 target 名称是 `XCVolumeRendererUI2` - `engine/RHI`:先跑 `rhi_abstraction_tests``rhi_backend_tests`,再决定是否扩展到 `rhi_all_tests`
- 输出文件名是 `editor/bin/<Config>/XCEngine.exe` - `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. 推荐验证入口 ### 5.4 全量测试入口
### 6.1 全量
```bash ```bash
ctest --test-dir build -C Debug --output-on-failure ctest --test-dir build -C Debug --output-on-failure
``` ```
### 6.2 RHI ## 6. 按任务找入口
```bash - RHI 抽象与后端:`engine/include/XCEngine/RHI/``engine/src/RHI/``tests/RHI/`
cmake --build build --config Debug --target rhi_abstraction_tests - Rendering 主链与 pass`engine/include/XCEngine/Rendering/``engine/src/Rendering/``tests/Rendering/`
cmake --build build --config Debug --target rhi_backend_tests - 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 1. 先读当前模块的 `CMakeLists.txt`、最近测试和设计文档,再动代码。
cmake --build build --config Debug --target rendering_unit_tests 2. 优先在既有模块边界里解决问题,不要绕开系统回到 sample 式实现。
cmake --build build --config Debug --target rendering_integration_backpack_scene 3. 先跑与改动最相关的最小验证,再决定是否扩大全量验证。
``` 4. 目录、target、入口、文档名改了就同步更新 README / AGENT / 相关说明。
5. 如果任务会有意重建 `project/Library/`、脚本程序集或 `.meta`,在结果里明确说明哪些文件是有意生成的。
### 6.4 Editor / Scripting 这份文档的作用是给 agent 一个“当前真实工程长什么样”的基线。它本身也必须随着工程演进一起维护,不能再落回旧状态说明。
```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)

View File

@@ -14,7 +14,7 @@ set(
"Path to the bundled Mono distribution used by the scripting runtime") "Path to the bundled Mono distribution used by the scripting runtime")
add_subdirectory(engine) add_subdirectory(engine)
add_subdirectory(Editor) add_subdirectory(editor)
add_subdirectory(managed) add_subdirectory(managed)
add_subdirectory(mvs/RenderDoc) add_subdirectory(mvs/RenderDoc)
add_subdirectory(tests) add_subdirectory(tests)

View File

@@ -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 - [README.md](D:\Xuanchi\Main\XCEngine\README.md)
- **UI 框架**: ImGui - [AGENT.md](D:\Xuanchi\Main\XCEngine\AGENT.md)
- **语言**: C++17
- **构建系统**: CMake
- **依赖库**: DirectX 12 SDK
## 项目结构 ## 当前状态
``` 这个模块仍然可以单独构建,但它有几个需要明确的事实:
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/ ├── src/
│ ├── main.cpp # 程序入口
│ ├── Application.cpp/h # 应用主类
│ ├── Theme.cpp/h # 主题系统
│ ├── Core/ │ ├── Core/
│ │ ├── GameObject.h # 游戏对象
│ │ └── LogEntry.h # 日志条目
│ ├── Managers/ │ ├── Managers/
│ ├── LogSystem.cpp/h # 日志系统 │ ├── panels/
│ ├── ProjectManager.cpp/h # 项目管理 │ ├── Application.cpp
│ ├── SceneManager.cpp/h # 场景管理 │ ├── Application.h
│ └── SelectionManager.cpp/h # 选择管理 ├── Theme.cpp
── panels/ ── Theme.h
├── Panel.cpp/h # 面板基类 └── main.cpp
│ ├── MenuBar.cpp/h # 菜单栏 ├── build/ # 历史本地构建输出
│ ├── HierarchyPanel.cpp/h # 层级面板 └── bin/ # 历史可执行文件输出
│ ├── 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 配置
``` ```
## 构建方法 主要内容是:
### 前置要求 - 早期 `Application` / `Theme` 实现
- 基础 `Hierarchy / Scene / Game / Inspector / Project / Console` 面板骨架
- 旧版 `SceneManager / ProjectManager / LogSystem`
- Windows 10/11 ## 单独构建方式
- Visual Studio 2019 或更高版本
- CMake 3.15+
### 构建步骤 如果你只是想启动这个原型,可以单独进入该目录配置:
```bash ```bash
cd ui cmake -S mvs/ui -B mvs/ui/build -A x64
mkdir build && cd build cmake --build mvs/ui/build --config Release
cmake ..
cmake --build . --config Release
``` ```
### 运行 输出可执行文件通常位于:
```bash ```bash
# 运行编译好的可执行文件 .\mvs\ui\bin\Release\XCVolumeRendererUI2.exe
.\bin\Release\XCGameEngineUI.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 而不是当前 XCEngine editor 的入口。
- 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` 中添加属性编辑器

297
README.md
View File

@@ -1,48 +1,39 @@
# XCEngine # XCEngine
XCEngine 是一个正在持续开发中的模块化 C++ 游戏引擎。当前仓库已经形成了较完整的底座: XCEngine 是一个 Windows 优先、编辑器优先的模块化 C++ 游戏引擎工作区。当前主线已经形成 `RHI -> Rendering -> Editor Viewport -> AssetDatabase/Library -> Mono Scripting` 的可运行闭环,不再只是示例代码集合。
- `RHI` 抽象层已稳定覆盖 `D3D12 / OpenGL / Vulkan` 这份 README 面向引擎用户:关注怎么进入项目、怎么构建、当前仓库各目录分别负责什么。面向构建本引擎的 coding agent请看 [AGENT.md](AGENT.md)。
- `Rendering` 模块已经落地最小可用的场景渲染链路
- `editor/` 已接入基于引擎渲染链路的 Scene/Game viewport
- `managed/` 已具备基于 Mono 的 C# 运行时与托管程序集构建链路
- `tests/` 已形成按模块分层的单元测试与集成测试体系
如果你是第一次进入当前仓库,先看 [AGENT.md](AGENT.md)。它更偏向“当前工程状态、协作约束与推荐入口”。 ## 项目定位
## 当前状态 - `engine/` 提供静态库 `XCEngine`,包含 `RHI``Rendering``Resources``Scene``Scripting` 等核心模块。
- `editor/` 提供桌面编辑器 `XCEditor`,输出文件名为 `XCEngine.exe`,默认打开仓库内的 `project/`
- 核心引擎库位于 `engine/`,包含 Audio、Components、Core、Debug、Input、Memory、Platform、Rendering、Resources、RHI、Scene、Scripting、Threading - `project/` 是当前随仓库维护的示例工程,已经采用 `Assets/ + .meta + Library/` 的工程布局
- RHI 当前正式支持 `D3D12 / OpenGL / Vulkan` 三后端,且抽象层与后端层测试都已经落地 - `managed/` 负责 `XCEngine.ScriptCore.dll`、示例 `GameScripts.dll` 以及项目脚本程序集构建
- Rendering 当前已具备 `RenderSceneExtractor``RenderResourceCache``SceneRenderer``CameraRenderer``BuiltinForwardPipeline` - `tests/` 已覆盖 Engine、RHI、Rendering、Editor、Scripting 等主要模块
- Editor 当前是 `D3D12` 宿主应用,但场景绘制已通过引擎 `Rendering + RHI` 链路输出到离屏纹理,再接入 ImGui 面板。
- Managed scripting 当前通过 `managed/XCEngine.ScriptCore``managed/GameScripts` 构建托管程序集;默认启用 Mono 运行时集成。
- 默认示例工程位于 `project/`,已包含基础场景、背包模型与脚本资产目录。
## 环境要求 ## 环境要求
建议在 Windows 上构建当前仓库 当前推荐在 Windows 上使用和构建。
- Windows 10/11 - Windows 10/11
- Visual Studio 2022 / MSVC v143 - Visual Studio 2022 / MSVC v143
- CMake 3.15+ - CMake 3.15+
- .NET SDK
- Vulkan SDK - Vulkan SDK
- .NET SDK
- Git LFS - Git LFS
如果启用默认脚本构建,还需要: 启用 Mono 脚本运行时时,还需要:
- `参考/Fermion/Fermion/external/mono` 下可用的 Mono 头文件、静态库与 `mscorlib.dll` - `参考/Fermion/Fermion/external/mono` 下可用的 Mono 头文件、静态库与 `mscorlib.dll`
如果本地暂时没有 Mono 依赖,可以在配置时关闭脚本构建 补充说明
```bash - `engine/CMakeLists.txt` 当前对 `Vulkan` 是硬依赖,未配置 Vulkan SDK 时无法完成配置。
cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF - `editor/``tests/` 首次配置会通过 `FetchContent` 从 Gitee 镜像拉取 `ImGui``googletest`;离线环境请确保已有可复用的 `_deps` 缓存。
``` - 如果需要 `mvs/3DGS-Unity/room.ply` 这类大文件示例,请先执行 Git LFS 拉取。
如果你需要 `mvs/3DGS-Unity/room.ply` 这类大文件样例,请确保已经执行过 Git LFS 拉取。 ## 快速开始
## 构建
### 1. 配置 ### 1. 配置
@@ -50,60 +41,92 @@ cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF
cmake -S . -B build -A x64 cmake -S . -B build -A x64
``` ```
### 2. 全量编译 如果本地暂时没有 Mono 依赖,可以先关闭脚本运行时:
```bash ```bash
cmake --build build --config Debug cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF
``` ```
### 3. 常用增量 target ### 2. 构建常用目标
```bash ```bash
cmake --build build --config Debug --target XCEngine 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_managed_assemblies
cmake --build build --config Debug --target rhi_all_tests cmake --build build --config Debug --target xcengine_project_managed_assemblies
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 target 名称仍然是历史遗留的 `XCVolumeRendererUI2`,但输出文件名是 `XCEngine.exe` - `XCEngine` 是引擎静态库。
- 顶层 CMake 当前会纳入 `engine/``editor/``managed/``mvs/RenderDoc/``tests/` - `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 ```bash
ctest --test-dir build -N -C Debug ctest --test-dir build -N -C Debug
```
### 运行全部测试
```bash
ctest --test-dir build -C Debug --output-on-failure ctest --test-dir build -C Debug --output-on-failure
``` ```
### 常用测试 target 按模块常用的构建 / 验证 target
```bash ```bash
cmake --build build --config Debug --target rhi_abstraction_tests cmake --build build --config Debug --target rhi_all_tests
cmake --build build --config Debug --target rhi_backend_tests cmake --build build --config Debug --target rendering_all_tests
cmake --build build --config Debug --target rendering_unit_tests cmake --build build --config Debug --target rendering_phase_regression
cmake --build build --config Debug --target editor_tests cmake --build build --config Debug --target editor_tests
cmake --build build --config Debug --target scripting_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 ```text
XCEngine/ XCEngine/
@@ -112,15 +135,33 @@ XCEngine/
├── AGENT.md ├── AGENT.md
├── CMakeLists.txt ├── CMakeLists.txt
├── README.md ├── README.md
├── build/ # 本地构建输出 ├── build/ # 本地 CMake 构建输出
├── docs/ ├── docs/
│ ├── api/ # API 文档 │ ├── api/
│ ├── issues/ # 跟踪中的设计 / 实现问题 │ ├── XCEngine/
│ ├── plan/ # 设计文档、阶段规划、归档方案 │ ├── _guides/
│ │ ├── Renderer模块设计与实现.md │ │ ├── _meta/
│ │ ── end/ │ │ ── _tools/
│ │ └── RHI模块设计与实现/ │ │ └── main.md
│ └── RHI模块总览.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/ │ ├── used/
│ ├── api-skill.md │ ├── api-skill.md
│ ├── blueprint-skill.md │ ├── blueprint-skill.md
@@ -143,13 +184,28 @@ XCEngine/
│ │ ├── UI/ │ │ ├── UI/
│ │ ├── Utils/ │ │ ├── Utils/
│ │ ├── Viewport/ │ │ ├── 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.cpp
│ │ ├── Application.h │ │ ├── Application.h
│ │ ├── EditorApp.rc │ │ ├── EditorApp.rc
│ │ ├── Theme.cpp │ │ ├── Theme.cpp
│ │ ├── Theme.h │ │ ├── Theme.h
│ │ └── main.cpp │ │ └── main.cpp
│ └── bin/ # 编辑器输出目录,输出名为 XCEngine.exe │ └── bin/ # 编辑器输出目录,输出名为 XCEngine.exe
├── engine/ ├── engine/
│ ├── CMakeLists.txt │ ├── CMakeLists.txt
│ ├── include/ │ ├── include/
@@ -167,17 +223,24 @@ XCEngine/
│ │ ├── Platform/ │ │ ├── Platform/
│ │ │ └── Windows/ │ │ │ └── Windows/
│ │ ├── Rendering/ │ │ ├── Rendering/
│ │ │ ├── Passes/
│ │ │ ├── Pipelines/ │ │ │ ├── Pipelines/
│ │ │ ├── CameraRenderRequest.h
│ │ │ ├── CameraRenderer.h │ │ │ ├── CameraRenderer.h
│ │ │ ├── CameraRenderRequest.h
│ │ │ ├── ObjectIdEncoding.h
│ │ │ ├── ObjectIdPass.h
│ │ │ ├── RenderCameraData.h │ │ │ ├── RenderCameraData.h
│ │ │ ├── RenderContext.h │ │ │ ├── RenderContext.h
│ │ │ ├── RenderMaterialUtility.h │ │ │ ├── RenderMaterialUtility.h
│ │ │ ├── RenderPass.h
│ │ │ ├── RenderPipeline.h │ │ │ ├── RenderPipeline.h
│ │ │ ├── RenderPipelineAsset.h │ │ │ ├── RenderPipelineAsset.h
│ │ │ ├── RenderResourceCache.h │ │ │ ├── RenderResourceCache.h
│ │ │ ├── RenderSceneExtractor.h │ │ │ ├── RenderSceneExtractor.h
│ │ │ ├── RenderSceneUtility.h
│ │ │ ├── RenderSurface.h │ │ │ ├── RenderSurface.h
│ │ │ ├── SceneRenderRequestPlanner.h
│ │ │ ├── SceneRenderRequestUtils.h
│ │ │ ├── SceneRenderer.h │ │ │ ├── SceneRenderer.h
│ │ │ └── VisibleRenderObject.h │ │ │ └── VisibleRenderObject.h
│ │ ├── Resources/ │ │ ├── Resources/
@@ -189,29 +252,7 @@ XCEngine/
│ │ ├── RHI/ │ │ ├── RHI/
│ │ │ ├── D3D12/ │ │ │ ├── D3D12/
│ │ │ ├── OpenGL/ │ │ │ ├── OpenGL/
│ │ │ ── Vulkan/ │ │ │ ── 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
│ │ ├── Scene/ │ │ ├── Scene/
│ │ ├── Scripting/ │ │ ├── Scripting/
│ │ │ └── Mono/ │ │ │ └── Mono/
@@ -230,6 +271,7 @@ XCEngine/
│ │ ├── Platform/ │ │ ├── Platform/
│ │ │ └── Windows/ │ │ │ └── Windows/
│ │ ├── Rendering/ │ │ ├── Rendering/
│ │ │ ├── Passes/
│ │ │ └── Pipelines/ │ │ │ └── Pipelines/
│ │ ├── Resources/ │ │ ├── Resources/
│ │ │ ├── AudioClip/ │ │ │ ├── AudioClip/
@@ -255,28 +297,45 @@ XCEngine/
│ └── renderdoc_parser/ │ └── renderdoc_parser/
├── managed/ ├── managed/
│ ├── CMakeLists.txt │ ├── CMakeLists.txt
│ ├── GameScripts/ # 示例 / 验证脚本程序集源码 │ ├── GameScripts/
│ └── XCEngine.ScriptCore/ # 引擎托管 API │ └── XCEngine.ScriptCore/
├── mvs/ ├── mvs/
│ ├── 3DGS-Unity/ # Unity 侧 3DGS 参考与资源 │ ├── 3DGS-Unity/
│ ├── D3D12/ │ ├── D3D12/
│ ├── Music fluctuations/ │ ├── Music fluctuations/
│ ├── OpenGL/ │ ├── OpenGL/
│ ├── RenderDoc/ │ ├── RenderDoc/
│ ├── Res/ │ ├── Res/
│ ├── ui/ │ ├── ui/ # 早期 ImGui + D3D12 UI 原型,非当前正式 editor
│ └── VolumeRenderer/ │ └── VolumeRenderer/
├── project/ ├── project/
│ ├── .xceditor/ │ ├── .xceditor/
│ │ ── imgui_layout.ini │ │ ── imgui_layout.ini
│ │ └── thumbs/
│ ├── Assets/ │ ├── Assets/
│ │ ├── Materials/ │ │ ├── Materials/
│ │ ├── Models/ │ │ ├── Models/
│ │ │ └── backpack/ │ │ │ └── backpack/
│ │ ├── New Folder/
│ │ ├── New Folder 1/
│ │ ├── Scenes/ │ │ ├── Scenes/
│ │ │ ├── Backpack.xc
│ │ │ └── Main.xc │ │ │ └── Main.xc
│ │ └── Scripts/ │ │ └── Scripts/
│ │ ├── ProjectScriptProbe.cs
│ │ └── Textures/
│ ├── Library/
│ │ ├── ArtifactDB/
│ │ ├── Artifacts/
│ │ ├── ScriptAssemblies/
│ │ │ ├── GameScripts.dll
│ │ │ ├── mscorlib.dll
│ │ │ └── XCEngine.ScriptCore.dll
│ │ └── SourceAssetDB/
│ ├── Assets.meta
│ └── Project.xcproject │ └── Project.xcproject
├── scripts/
│ └── Run-RendererPhaseRegression.ps1
├── tests/ ├── tests/
│ ├── CMakeLists.txt │ ├── CMakeLists.txt
│ ├── TEST_SPEC.md │ ├── TEST_SPEC.md
@@ -293,7 +352,9 @@ XCEngine/
│ ├── Memory/ │ ├── Memory/
│ ├── Rendering/ │ ├── Rendering/
│ │ ├── integration/ │ │ ├── integration/
│ │ │ ├── backpack_lit_scene/
│ │ │ ├── backpack_scene/ │ │ │ ├── backpack_scene/
│ │ │ ├── camera_stack_scene/
│ │ │ ├── cull_material_scene/ │ │ │ ├── cull_material_scene/
│ │ │ ├── depth_sort_scene/ │ │ │ ├── depth_sort_scene/
│ │ │ ├── material_state_scene/ │ │ │ ├── material_state_scene/
@@ -308,66 +369,44 @@ XCEngine/
│ │ ├── Shader/ │ │ ├── Shader/
│ │ └── Texture/ │ │ └── Texture/
│ ├── RHI/ │ ├── RHI/
│ │ ├── CMakeLists.txt
│ │ ├── D3D12/ │ │ ├── D3D12/
│ │ │ ├── integration/ │ │ │ ├── integration/
│ │ │ └── unit/ │ │ │ └── unit/
│ │ ├── OpenGL/
│ │ │ ├── integration/
│ │ │ └── unit/
│ │ ├── Vulkan/
│ │ │ ├── integration/
│ │ │ ├── unit/
│ │ │ └── TEST_SPEC.md
│ │ ├── integration/ │ │ ├── integration/
│ │ │ ├── backpack/ │ │ │ ├── backpack/
│ │ │ ├── fixtures/ │ │ │ ├── fixtures/
│ │ │ ├── minimal/ │ │ │ ├── minimal/
│ │ │ ├── quad/ │ │ │ ├── quad/
│ │ │ ├── sphere/ │ │ │ ├── sphere/
│ │ │ ── triangle/ │ │ │ ── triangle/
│ │ │ ├── compare_ppm.py │ │ ├── OpenGL/
│ │ │ ├── README.md │ │ │ ├── integration/
│ │ │ └── run_integration_test.py │ │ │ └── unit/
│ │ ── unit/ │ │ ── unit/
│ │ └── Vulkan/
│ │ ├── integration/
│ │ └── unit/
│ ├── Scene/ │ ├── Scene/
│ ├── Scripting/ │ ├── Scripting/
│ └── Threading/ │ └── Threading/
├── 参考/ ├── 参考/
── TransformGizmo/ ── Fermion/
│ ├── TransformGizmo/
│ ├── unity editor/
│ ├── unity-editor-icons/
│ ├── unity-icons/
│ └── UnityRuntimeSceneGizmo-master/
└── .vscode/ └── .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) - 协作基线与 coding agent 入口:[AGENT.md](AGENT.md)
- RHI 核心设计文档[docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md) - RHI 基线设计[docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md)
- Renderer 规划与实现说明:[docs/plan/Renderer模块设计与实现.md](docs/plan/Renderer模块设计与实现.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) - 测试规范:[tests/TEST_SPEC.md](tests/TEST_SPEC.md)
## 许可证 ## 许可证
当前仓库根目录未看到独的顶层许可证文件涉及第三方库时请分别遵循对应依赖目录中的许可证说明。 当前仓库根目录未看到独的顶层许可证文件涉及第三方库时请分别遵循其所在目录中的许可证或随附说明。

View File

@@ -49,6 +49,9 @@ if(NOT TARGET XCEngine)
add_subdirectory("${XCENGINE_ENGINE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/engine_dependency") add_subdirectory("${XCENGINE_ENGINE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/engine_dependency")
endif() 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 set(IMGUI_SOURCES
${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_demo.cpp ${imgui_SOURCE_DIR}/imgui_demo.cpp
@@ -63,6 +66,7 @@ add_executable(${PROJECT_NAME} WIN32
src/EditorApp.rc src/EditorApp.rc
src/main.cpp src/main.cpp
src/Application.cpp src/Application.cpp
src/Scripting/EditorScriptAssemblyBuilder.cpp
src/Theme.cpp src/Theme.cpp
src/Core/UndoManager.cpp src/Core/UndoManager.cpp
src/Core/PlaySessionController.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_definitions(${PROJECT_NAME} PRIVATE UNICODE _UNICODE)
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8) 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) if(MSVC)
target_compile_options(${PROJECT_NAME} PRIVATE /FS) target_compile_options(${PROJECT_NAME} PRIVATE /FS)
set_property(TARGET ${PROJECT_NAME} PROPERTY set_property(TARGET ${PROJECT_NAME} PROPERTY

View File

@@ -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 如果你想理解当前 editor先把它当成三层
- **UI 框架**: ImGui
- **语言**: C++17
- **构建系统**: CMake
- **依赖库**: DirectX 12 SDK
## 项目结构 1. `Win32 + D3D12` 宿主窗口与 ImGui backend
2. `ViewportHostService` 对引擎渲染链路的接线
3. `panels/``Managers/``ComponentEditors/` 这些编辑器业务层
``` 当前不应再把 editor 视为旧式“UI sample”。它已经是引擎工作区的正式入口之一。
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/` 目录。
### 前置要求 ### 前置要求
- Windows 10/11 - Windows 10/11
- Visual Studio 2019 或更高版本 - Visual Studio 2022 / MSVC v143
- CMake 3.15+ - CMake 3.15+
- Vulkan SDK
### 构建步骤 如果需要启用 Mono 脚本运行时,还需要:
- .NET SDK
- `参考/Fermion/Fermion/external/mono` 下的 Mono 依赖
### 配置
```bash ```bash
cd ui cmake -S .. -B ..\build -A x64
mkdir build && cd build
cmake ..
cmake --build . --config Release
``` ```
### 运行 更常见的做法是直接在仓库根目录运行:
```bash ```bash
# 运行编译好的可执行文件 cmake -S . -B build -A x64
.\bin\Release\XCGameEngineUI.exe
``` ```
## 功能特性 如果本地暂时没有 Mono可以先关闭
### 编辑器面板 ```bash
cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF
#### 菜单栏MenuBar
- 文件菜单(新建、打开、保存等)
- 编辑菜单(撤销、重做等)
- 视图菜单(面板显示/隐藏)
- 帮助菜单
#### 层级面板Hierarchy Panel
- 显示场景中所有游戏对象
- 树形结构展示父子关系
- 支持对象选择
- 对象重命名
#### 检查器面板Inspector Panel
- 显示选中对象的属性
- 支持组件编辑
- 变换组件(位置、旋转、缩放)
- 材质组件
#### 场景视图Scene View
- 3D 场景预览
- 相机控制(平移、旋转、缩放)
- 对象选择
- 辅助工具(网格、轴心)
#### 游戏视图Game View
- 游戏运行时的画面预览
- 分辨率设置
- 宽高比选择
#### 项目面板Project Panel
- 项目文件浏览器
- 资源组织
- 搜索过滤
#### 控制台面板Console Panel
- 日志输出
- 警告和错误显示
- 日志级别过滤
- 清空日志
### 管理系统
#### 日志系统LogSystem
- 分级日志Info、Warning、Error
- 时间戳
- 日志持久化
#### 项目管理ProjectManager
- 项目创建/打开
- 资源路径管理
#### 场景管理SceneManager
- 场景加载/保存
- 对象生命周期管理
#### 选择管理SelectionManager
- 当前选中对象追踪
- 多选支持
### 主题系统
- 深色主题Dark Theme
- 可自定义配色方案
## 窗口布局
默认布局采用经典的 Unity 编辑器风格:
```
+----------------------------------------------------------+
| 菜单栏 |
+----------+------------------------+----------------------+
| | | |
| 项目 | 场景视图 | 检查器 |
| 面板 | | |
| | | |
+----------+------------------------+----------------------+
| 层级面板 | 游戏视图 |
| | |
+------------------------------------+----------------------+
| 控制台面板 |
+----------------------------------------------------------+
``` ```
## 依赖说明 ### 构建 editor
- ImGui - 跨平台 GUI 库 ```bash
- DirectX 12 - 渲染 API cmake --build build --config Debug --target XCEditor
- Windows SDK - 窗口管理 ```
## 扩展开发 说明:
### 添加新面板 - target 名称是 `XCEditor`
- 输出文件名仍然是 `XCEngine.exe`
- 输出目录是 `editor/bin/<Config>/`
1.`panels/` 目录下创建新的面板类 ## 运行
2. 继承 `Panel` 基类
3. 实现 `Render()` 方法
4.`Application` 中注册新面板
### 添加新组件 ```bash
.\editor\bin\Debug\XCEngine.exe
```
1. 定义组件类 默认情况下editor 会自动把仓库内的 `project/` 识别为工程根目录。也可以显式指定工程:
2.`GameObject` 中注册组件类型
3.`InspectorPanel` 中添加属性编辑器 ```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
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

View File

@@ -28,6 +28,14 @@ inline ActionBinding MakeSaveProjectAction(bool enabled = true) {
return MakeAction("Save Project", nullptr, false, enabled); 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) { inline ActionBinding MakeNewSceneAction(bool enabled = true) {
return MakeAction("New Scene", "Ctrl+N", false, enabled, true, Shortcut(ImGuiKey_N, true)); return MakeAction("New Scene", "Ctrl+N", false, enabled, true, Shortcut(ImGuiKey_N, true));
} }

View File

@@ -168,6 +168,19 @@ inline void DrawHierarchySortOptionsPopup(
} }
inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Components::GameObject* parent) { 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(), [&]() { DrawMenuAction(MakeCreateEmptyEntityAction(), [&]() {
TraceHierarchyPopup("Hierarchy create clicked: Empty Object"); TraceHierarchyPopup("Hierarchy create clicked: Empty Object");
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Entity", "GameObject"); 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::string("Hierarchy create result: Empty Object, createdId=") +
std::to_string(created ? created->GetID() : 0)); 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(); DrawMenuSeparator();
DrawMenuAction(MakeCreateCameraEntityAction(), [&]() { DrawMenuAction(MakeCreateCameraEntityAction(), [&]() {
TraceHierarchyPopup("Hierarchy create clicked: Camera"); TraceHierarchyPopup("Hierarchy create clicked: Camera");
@@ -190,28 +213,6 @@ inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Comp
std::string("Hierarchy create result: Light, createdId=") + std::string("Hierarchy create result: Light, createdId=") +
std::to_string(created ? created->GetID() : 0)); 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( inline void HandleHierarchyItemContextRequest(
@@ -275,7 +276,7 @@ inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::Def
backgroundContextMenu.ConsumeOpenRequest("HierarchyContextMenu"); backgroundContextMenu.ConsumeOpenRequest("HierarchyContextMenu");
static bool s_lastBackgroundPopupOpen = false; static bool s_lastBackgroundPopupOpen = false;
if (!UI::BeginPopup("HierarchyContextMenu")) { if (!UI::BeginContextMenu("HierarchyContextMenu")) {
if (s_lastBackgroundPopupOpen) { if (s_lastBackgroundPopupOpen) {
TraceHierarchyPopup("Hierarchy background popup closed"); TraceHierarchyPopup("Hierarchy background popup closed");
s_lastBackgroundPopupOpen = false; s_lastBackgroundPopupOpen = false;
@@ -289,7 +290,7 @@ inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::Def
} }
DrawHierarchyContextActions(context, nullptr, true); DrawHierarchyContextActions(context, nullptr, true);
UI::EndPopup(); UI::EndContextMenu();
} }
inline void DrawHierarchyEntityContextPopup( inline void DrawHierarchyEntityContextPopup(
@@ -298,7 +299,7 @@ inline void DrawHierarchyEntityContextPopup(
itemContextMenu.ConsumeOpenRequest("HierarchyEntityContextMenu"); itemContextMenu.ConsumeOpenRequest("HierarchyEntityContextMenu");
static bool s_lastEntityPopupOpen = false; static bool s_lastEntityPopupOpen = false;
if (!UI::BeginPopup("HierarchyEntityContextMenu")) { if (!UI::BeginContextMenu("HierarchyEntityContextMenu")) {
if (s_lastEntityPopupOpen) { if (s_lastEntityPopupOpen) {
TraceHierarchyPopup("Hierarchy entity popup closed"); TraceHierarchyPopup("Hierarchy entity popup closed");
s_lastEntityPopupOpen = false; s_lastEntityPopupOpen = false;
@@ -314,7 +315,7 @@ inline void DrawHierarchyEntityContextPopup(
if (itemContextMenu.HasTarget()) { if (itemContextMenu.HasTarget()) {
DrawHierarchyContextActions(context, itemContextMenu.TargetValue()); DrawHierarchyContextActions(context, itemContextMenu.TargetValue());
} }
UI::EndPopup(); UI::EndContextMenu();
if (!ImGui::IsPopupOpen("HierarchyEntityContextMenu") && !itemContextMenu.HasPendingOpenRequest()) { if (!ImGui::IsPopupOpen("HierarchyEntityContextMenu") && !itemContextMenu.HasPendingOpenRequest()) {
itemContextMenu.Clear(); itemContextMenu.Clear();

View File

@@ -34,6 +34,14 @@ inline void ExecuteSaveProject(IEditorContext& context) {
Commands::SaveProject(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) { inline void ExecuteOpenScene(IEditorContext& context) {
Commands::OpenSceneWithDialog(context); Commands::OpenSceneWithDialog(context);
} }
@@ -143,6 +151,10 @@ inline void DrawFileMenuActions(IEditorContext& context) {
DrawMenuAction(MakeSaveSceneAction(canEditDocuments), [&]() { ExecuteSaveScene(context); }); DrawMenuAction(MakeSaveSceneAction(canEditDocuments), [&]() { ExecuteSaveScene(context); });
DrawMenuAction(MakeSaveSceneAsAction(canEditDocuments), [&]() { ExecuteSaveSceneAs(context); }); DrawMenuAction(MakeSaveSceneAsAction(canEditDocuments), [&]() { ExecuteSaveSceneAs(context); });
DrawMenuSeparator(); DrawMenuSeparator();
DrawMenuAction(
MakeMigrateSceneAssetReferencesAction(Commands::CanMigrateSceneAssetReferences(context)),
[&]() { ExecuteMigrateSceneAssetReferences(context); });
DrawMenuSeparator();
DrawMenuAction(MakeExitAction(), [&]() { RequestEditorExit(context); }); 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) { inline void DrawViewMenuActions(IEditorContext& context) {
DrawMenuAction(MakeResetLayoutAction(), [&]() { RequestDockLayoutReset(context); }); DrawMenuAction(MakeResetLayoutAction(), [&]() { RequestDockLayoutReset(context); });
} }
@@ -186,6 +204,9 @@ inline void DrawMainMenuBar(IEditorContext& context, UI::DeferredPopupState& abo
UI::DrawMenuScope("Run", [&]() { UI::DrawMenuScope("Run", [&]() {
DrawRunMenuActions(context); DrawRunMenuActions(context);
}); });
UI::DrawMenuScope("Scripts", [&]() {
DrawScriptsMenuActions(context);
});
UI::DrawMenuScope("View", [&]() { UI::DrawMenuScope("View", [&]() {
DrawViewMenuActions(context); DrawViewMenuActions(context);
}); });

View File

@@ -6,12 +6,18 @@
#include "Core/EditorContext.h" #include "Core/EditorContext.h"
#include "Core/EditorEvents.h" #include "Core/EditorEvents.h"
#include "Core/EventBus.h" #include "Core/EventBus.h"
#include "Scripting/EditorScriptAssemblyBuilder.h"
#include "UI/BuiltInIcons.h" #include "UI/BuiltInIcons.h"
#include "Platform/Win32Utf8.h" #include "Platform/Win32Utf8.h"
#include "Platform/WindowsProcessDiagnostics.h" #include "Platform/WindowsProcessDiagnostics.h"
#include <XCEngine/Core/Asset/ResourceManager.h> #include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Debug/Logger.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 <chrono>
#include <filesystem>
#include <windows.h> #include <windows.h>
namespace XCEngine { namespace XCEngine {
@@ -22,6 +28,123 @@ Application& Application::Get() {
return instance; 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) { bool Application::InitializeWindowRenderer(HWND hwnd) {
RECT clientRect = {}; RECT clientRect = {};
if (!GetClientRect(hwnd, &clientRect)) { if (!GetClientRect(hwnd, &clientRect)) {
@@ -150,6 +273,7 @@ bool Application::Initialize(HWND hwnd) {
logger.Info(Debug::LogCategory::General, "Initializing editor context..."); logger.Info(Debug::LogCategory::General, "Initializing editor context...");
InitializeEditorContext(projectRoot); InitializeEditorContext(projectRoot);
InitializeScriptingRuntime(projectRoot);
logger.Info(Debug::LogCategory::General, "Initializing ImGui backend..."); logger.Info(Debug::LogCategory::General, "Initializing ImGui backend...");
InitializeImGui(hwnd); InitializeImGui(hwnd);
logger.Info(Debug::LogCategory::General, "Attaching editor layer..."); logger.Info(Debug::LogCategory::General, "Attaching editor layer...");
@@ -171,6 +295,7 @@ void Application::Shutdown() {
UI::ShutdownBuiltInIcons(); UI::ShutdownBuiltInIcons();
m_imguiBackend.Shutdown(); m_imguiBackend.Shutdown();
m_imguiSession.Shutdown(); m_imguiSession.Shutdown();
ShutdownScriptingRuntime();
ShutdownEditorContext(); ShutdownEditorContext();
if (m_resourceManagerInitialized) { if (m_resourceManagerInitialized) {
::XCEngine::Resources::ResourceManager::Get().Shutdown(); ::XCEngine::Resources::ResourceManager::Get().Shutdown();
@@ -230,6 +355,7 @@ bool Application::SwitchProject(const std::string& projectPath) {
logger.Info(Debug::LogCategory::General, infoMessage.c_str()); logger.Info(Debug::LogCategory::General, infoMessage.c_str());
::XCEngine::Resources::ResourceManager::Get().SetResourceRoot(projectPath.c_str()); ::XCEngine::Resources::ResourceManager::Get().SetResourceRoot(projectPath.c_str());
InitializeScriptingRuntime(projectPath);
m_lastWindowTitle.clear(); m_lastWindowTitle.clear();
UpdateWindowTitle(); UpdateWindowTitle();

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "Platform/D3D12WindowRenderer.h" #include "Platform/D3D12WindowRenderer.h"
#include "Scripting/EditorScriptRuntimeStatus.h"
#include "UI/ImGuiBackendBridge.h" #include "UI/ImGuiBackendBridge.h"
#include "UI/ImGuiSession.h" #include "UI/ImGuiSession.h"
#include "Viewport/ViewportHostService.h" #include "Viewport/ViewportHostService.h"
@@ -19,6 +20,12 @@ class RHIDevice;
class RHISwapChain; class RHISwapChain;
} // namespace RHI } // namespace RHI
namespace Scripting {
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
class MonoScriptRuntime;
#endif
} // namespace Scripting
namespace Editor { namespace Editor {
class EditorLayer; class EditorLayer;
@@ -33,6 +40,8 @@ public:
void Render(); void Render();
void OnResize(int width, int height); void OnResize(int width, int height);
bool SwitchProject(const std::string& projectPath); bool SwitchProject(const std::string& projectPath);
bool ReloadScriptingRuntime();
bool RebuildScriptingAssemblies();
void SaveProjectState(); void SaveProjectState();
Rendering::RenderContext GetMainRenderContext() const { return m_windowRenderer.GetRenderContext(); } Rendering::RenderContext GetMainRenderContext() const { return m_windowRenderer.GetRenderContext(); }
RHI::RHIDevice* GetMainRHIDevice() const { return m_windowRenderer.GetRHIDevice(); } RHI::RHIDevice* GetMainRHIDevice() const { return m_windowRenderer.GetRHIDevice(); }
@@ -42,6 +51,7 @@ public:
HWND GetWindowHandle() const { return m_hwnd; } HWND GetWindowHandle() const { return m_hwnd; }
IEditorContext& GetEditorContext() const { return *m_editorContext; } IEditorContext& GetEditorContext() const { return *m_editorContext; }
const EditorScriptRuntimeStatus& GetScriptRuntimeStatus() const { return m_scriptRuntimeStatus; }
private: private:
Application() = default; Application() = default;
@@ -52,6 +62,8 @@ private:
void AttachEditorLayer(); void AttachEditorLayer();
void DetachEditorLayer(); void DetachEditorLayer();
void ShutdownEditorContext(); void ShutdownEditorContext();
void InitializeScriptingRuntime(const std::string& projectPath);
void ShutdownScriptingRuntime();
void RenderEditorFrame(); void RenderEditorFrame();
void UpdateWindowTitle(); void UpdateWindowTitle();
@@ -70,6 +82,10 @@ private:
bool m_hasLastFrameTime = false; bool m_hasLastFrameTime = false;
bool m_renderReady = false; bool m_renderReady = false;
bool m_resourceManagerInitialized = false; bool m_resourceManagerInitialized = false;
EditorScriptRuntimeStatus m_scriptRuntimeStatus;
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
std::unique_ptr<::XCEngine::Scripting::MonoScriptRuntime> m_scriptRuntime;
#endif
}; };
} }

View File

@@ -7,8 +7,11 @@
#include <XCEngine/Components/CameraComponent.h> #include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.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/Quaternion.h>
#include <XCEngine/Core/Math/Vector3.h> #include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <string> #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( inline bool RenameEntity(
IEditorContext& context, IEditorContext& context,
::XCEngine::Components::GameObject::ID entityId, ::XCEngine::Components::GameObject::ID entityId,

View File

@@ -314,6 +314,35 @@ inline bool SaveProject(IEditorContext& context) {
return SaveProjectDescriptor(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) { inline bool SwitchProject(IEditorContext& context, const std::string& projectPath) {
if (!IsProjectDocumentEditingAllowed(context)) { if (!IsProjectDocumentEditingAllowed(context)) {
return false; return false;

View File

@@ -30,7 +30,7 @@ inline AssetReferenceInteraction DrawAssetReferenceProperty(
pickerOptions.assetsTabLabel = "Assets"; pickerOptions.assetsTabLabel = "Assets";
pickerOptions.sceneTabLabel = "Scene"; pickerOptions.sceneTabLabel = "Scene";
pickerOptions.showAssetsTab = true; pickerOptions.showAssetsTab = true;
pickerOptions.showSceneTab = true; pickerOptions.showSceneTab = false;
pickerOptions.supportedAssetExtensions = supportedExtensions; pickerOptions.supportedAssetExtensions = supportedExtensions;
UI::DrawPropertyRow(label, UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) { UI::DrawPropertyRow(label, UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) {

View File

@@ -4,6 +4,7 @@
#include "ComponentEditors/LightComponentEditor.h" #include "ComponentEditors/LightComponentEditor.h"
#include "ComponentEditors/MeshFilterComponentEditor.h" #include "ComponentEditors/MeshFilterComponentEditor.h"
#include "ComponentEditors/MeshRendererComponentEditor.h" #include "ComponentEditors/MeshRendererComponentEditor.h"
#include "ComponentEditors/ScriptComponentEditor.h"
#include "ComponentEditors/TransformComponentEditor.h" #include "ComponentEditors/TransformComponentEditor.h"
namespace XCEngine { namespace XCEngine {
@@ -20,6 +21,7 @@ ComponentEditorRegistry::ComponentEditorRegistry() {
RegisterEditor(std::make_unique<LightComponentEditor>()); RegisterEditor(std::make_unique<LightComponentEditor>());
RegisterEditor(std::make_unique<MeshFilterComponentEditor>()); RegisterEditor(std::make_unique<MeshFilterComponentEditor>());
RegisterEditor(std::make_unique<MeshRendererComponentEditor>()); RegisterEditor(std::make_unique<MeshRendererComponentEditor>());
RegisterEditor(std::make_unique<ScriptComponentEditor>());
} }
void ComponentEditorRegistry::RegisterEditor(std::unique_ptr<IComponentEditor> editor) { void ComponentEditorRegistry::RegisterEditor(std::unique_ptr<IComponentEditor> editor) {

View 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

View 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

View File

@@ -2,6 +2,11 @@
#include "EditorRuntimeMode.h" #include "EditorRuntimeMode.h"
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Input/InputTypes.h>
#include <array>
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <vector> #include <vector>
@@ -67,6 +72,19 @@ struct PlayModeResumeRequestedEvent {
struct PlayModeStepRequestedEvent { 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 { struct EditorModeChangedEvent {
EditorRuntimeMode oldMode = EditorRuntimeMode::Edit; EditorRuntimeMode oldMode = EditorRuntimeMode::Edit;
EditorRuntimeMode newMode = EditorRuntimeMode::Edit; EditorRuntimeMode newMode = EditorRuntimeMode::Edit;

View File

@@ -82,13 +82,20 @@ public:
void Publish(const T& event) { void Publish(const T& event) {
static_assert(sizeof(T) > 0, "Event type must be defined"); static_assert(sizeof(T) > 0, "Event type must be defined");
uint32_t typeId = EventTypeId<T>::Get(); uint32_t typeId = EventTypeId<T>::Get();
std::shared_lock<std::shared_mutex> lock(m_mutex); std::vector<HandlerEntry> handlers;
auto it = m_handlers.find(typeId); {
if (it != m_handlers.end()) { std::shared_lock<std::shared_mutex> lock(m_mutex);
for (const auto& entry : it->second) { auto it = m_handlers.find(typeId);
entry.handler(&event); if (it == m_handlers.end()) {
return;
} }
handlers = it->second;
}
for (const auto& entry : handlers) {
entry.handler(&event);
} }
} }

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstddef>
#include <string> #include <string>
#include <vector> #include <vector>
#include <memory> #include <memory>
@@ -10,6 +11,13 @@ namespace Editor {
class IProjectManager { class IProjectManager {
public: public:
struct SceneAssetReferenceMigrationReport {
size_t scannedSceneCount = 0;
size_t migratedSceneCount = 0;
size_t unchangedSceneCount = 0;
size_t failedSceneCount = 0;
};
virtual ~IProjectManager() = default; virtual ~IProjectManager() = default;
virtual const std::vector<AssetItemPtr>& GetCurrentItems() const = 0; virtual const std::vector<AssetItemPtr>& GetCurrentItems() const = 0;
@@ -39,6 +47,7 @@ public:
virtual bool DeleteItem(const std::string& fullPath) = 0; virtual bool DeleteItem(const std::string& fullPath) = 0;
virtual bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) = 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 bool RenameItem(const std::string& sourceFullPath, const std::string& newName) = 0;
virtual SceneAssetReferenceMigrationReport MigrateSceneAssetReferences() = 0;
virtual const std::string& GetProjectPath() const = 0; virtual const std::string& GetProjectPath() const = 0;
}; };

View File

@@ -6,9 +6,25 @@
#include "Core/ISceneManager.h" #include "Core/ISceneManager.h"
#include "Core/IUndoManager.h" #include "Core/IUndoManager.h"
#include <XCEngine/Input/InputManager.h>
#include <XCEngine/Scripting/ScriptEngine.h>
namespace XCEngine { namespace XCEngine {
namespace Editor { 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) { void PlaySessionController::Attach(IEditorContext& context) {
if (m_playStartRequestedHandlerId == 0) { if (m_playStartRequestedHandlerId == 0) {
m_playStartRequestedHandlerId = context.GetEventBus().Subscribe<PlayModeStartRequestedEvent>( m_playStartRequestedHandlerId = context.GetEventBus().Subscribe<PlayModeStartRequestedEvent>(
@@ -44,6 +60,14 @@ void PlaySessionController::Attach(IEditorContext& context) {
StepPlay(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) { void PlaySessionController::Detach(IEditorContext& context) {
@@ -73,6 +97,13 @@ void PlaySessionController::Detach(IEditorContext& context) {
context.GetEventBus().Unsubscribe<PlayModeStepRequestedEvent>(m_playStepRequestedHandlerId); context.GetEventBus().Unsubscribe<PlayModeStepRequestedEvent>(m_playStepRequestedHandlerId);
m_playStepRequestedHandlerId = 0; m_playStepRequestedHandlerId = 0;
} }
if (m_gameViewInputFrameHandlerId != 0) {
context.GetEventBus().Unsubscribe<GameViewInputFrameEvent>(m_gameViewInputFrameHandlerId);
m_gameViewInputFrameHandlerId = 0;
}
ResetRuntimeInputBridge();
} }
void PlaySessionController::Update(IEditorContext& context, float deltaTime) { void PlaySessionController::Update(IEditorContext& context, float deltaTime) {
@@ -81,6 +112,7 @@ void PlaySessionController::Update(IEditorContext& context, float deltaTime) {
return; return;
} }
ApplyGameViewInputFrame(deltaTime);
m_runtimeLoop.Tick(deltaTime); m_runtimeLoop.Tick(deltaTime);
} }
@@ -104,6 +136,10 @@ bool PlaySessionController::StartPlay(IEditorContext& context) {
} }
sceneManager.SetSceneDocumentDirtyTrackingEnabled(false); 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()); m_runtimeLoop.Start(sceneManager.GetScene());
context.GetUndoManager().ClearHistory(); context.GetUndoManager().ClearHistory();
context.SetRuntimeMode(EditorRuntimeMode::Play); context.SetRuntimeMode(EditorRuntimeMode::Play);
@@ -118,6 +154,8 @@ bool PlaySessionController::StopPlay(IEditorContext& context) {
auto& sceneManager = context.GetSceneManager(); auto& sceneManager = context.GetSceneManager();
m_runtimeLoop.Stop(); m_runtimeLoop.Stop();
ResetRuntimeInputBridge();
XCEngine::Input::InputManager::Get().Shutdown();
sceneManager.SetSceneDocumentDirtyTrackingEnabled(true); sceneManager.SetSceneDocumentDirtyTrackingEnabled(true);
if (!sceneManager.RestoreSceneSnapshot(m_editorSnapshot)) { if (!sceneManager.RestoreSceneSnapshot(m_editorSnapshot)) {
@@ -162,5 +200,83 @@ bool PlaySessionController::StepPlay(IEditorContext& context) {
return true; 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 Editor
} // namespace XCEngine } // namespace XCEngine

View File

@@ -3,8 +3,11 @@
#include "EditorRuntimeMode.h" #include "EditorRuntimeMode.h"
#include "SceneSnapshot.h" #include "SceneSnapshot.h"
#include "Core/EditorEvents.h"
#include <XCEngine/Scene/RuntimeLoop.h> #include <XCEngine/Scene/RuntimeLoop.h>
#include <cstddef>
#include <cstdint> #include <cstdint>
namespace XCEngine { namespace XCEngine {
@@ -26,12 +29,19 @@ public:
bool StepPlay(IEditorContext& context); bool StepPlay(IEditorContext& context);
private: private:
void ResetRuntimeInputBridge();
void ApplyGameViewInputFrame(float deltaTime);
uint64_t m_playStartRequestedHandlerId = 0; uint64_t m_playStartRequestedHandlerId = 0;
uint64_t m_playStopRequestedHandlerId = 0; uint64_t m_playStopRequestedHandlerId = 0;
uint64_t m_playPauseRequestedHandlerId = 0; uint64_t m_playPauseRequestedHandlerId = 0;
uint64_t m_playResumeRequestedHandlerId = 0; uint64_t m_playResumeRequestedHandlerId = 0;
uint64_t m_playStepRequestedHandlerId = 0; uint64_t m_playStepRequestedHandlerId = 0;
uint64_t m_gameViewInputFrameHandlerId = 0;
SceneSnapshot m_editorSnapshot = {}; SceneSnapshot m_editorSnapshot = {};
GameViewInputFrameEvent m_pendingGameViewInput = {};
GameViewInputFrameEvent m_appliedGameViewInput = {};
bool m_hasPendingGameViewInput = false;
XCEngine::Components::RuntimeLoop m_runtimeLoop; XCEngine::Components::RuntimeLoop m_runtimeLoop;
}; };

View File

@@ -1,9 +1,14 @@
#include "ProjectManager.h" #include "ProjectManager.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Scene/Scene.h>
#include <filesystem> #include <filesystem>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <cwctype> #include <cwctype>
#include <fstream> #include <fstream>
#include <sstream>
#include <initializer_list> #include <initializer_list>
#include <string_view> #include <string_view>
#include <windows.h> #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 } // namespace
const std::vector<AssetItemPtr>& ProjectManager::GetCurrentItems() const { 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 { AssetItemPtr ProjectManager::FindCurrentItemByPath(const std::string& fullPath) const {
const int index = FindCurrentItemIndex(fullPath); const int index = FindCurrentItemIndex(fullPath);
if (index < 0) { if (index < 0) {

View File

@@ -38,6 +38,7 @@ public:
bool DeleteItem(const std::string& fullPath) override; bool DeleteItem(const std::string& fullPath) override;
bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) override; bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) override;
bool RenameItem(const std::string& sourceFullPath, const std::string& newName) override; bool RenameItem(const std::string& sourceFullPath, const std::string& newName) override;
SceneAssetReferenceMigrationReport MigrateSceneAssetReferences() override;
const std::string& GetProjectPath() const override { return m_projectPath; } const std::string& GetProjectPath() const override { return m_projectPath; }

View 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

View 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

View 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

View 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

View File

@@ -47,6 +47,20 @@ struct AssetTileOptions {
bool drawLabel = true; 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 { enum class DialogActionResult {
None, None,
Primary, Primary,
@@ -428,16 +442,8 @@ inline AssetTileResult DrawAssetTile(
bool dimmed, bool dimmed,
DrawIconFn&& drawIcon, DrawIconFn&& drawIcon,
const AssetTileOptions& options = AssetTileOptions()) { const AssetTileOptions& options = AssetTileOptions()) {
const ImVec2 textSize = ImGui::CalcTextSize(label); const ImVec2 textSize = ImGui::CalcTextSize(label ? label : "");
ImVec2 tileSize = options.size; const ImVec2 tileSize = ComputeAssetTileSize(label, options);
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);
ImGui::InvisibleButton("##AssetBtn", tileSize); ImGui::InvisibleButton("##AssetBtn", tileSize);
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
@@ -470,13 +476,13 @@ inline AssetTileResult DrawAssetTile(
drawIcon(drawList, iconMin, iconMax); drawIcon(drawList, iconMin, iconMax);
const ImVec2 textPadding = AssetTileTextPadding(); 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 labelMin(min.x + textPadding.x, max.y - labelHeight - textPadding.y * 0.5f);
const ImVec2 labelMax(max.x - textPadding.x, labelMin.y + labelHeight); const ImVec2 labelMax(max.x - textPadding.x, labelMin.y + labelHeight);
if (options.drawLabel) { if (options.drawLabel) {
const float textAreaWidth = labelMax.x - labelMin.x; const float textAreaWidth = labelMax.x - labelMin.x;
const float centeredTextX = labelMin.x + std::max(0.0f, (textAreaWidth - textSize.x) * 0.5f); 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); ImGui::PushClipRect(labelMin, labelMax, true);
drawList->AddText(ImVec2(centeredTextX, textY), ImGui::GetColorU32(AssetTileTextColor(selected)), label); drawList->AddText(ImVec2(centeredTextX, textY), ImGui::GetColorU32(AssetTileTextColor(selected)), label);
ImGui::PopClipRect(); ImGui::PopClipRect();

View File

@@ -15,6 +15,8 @@ struct RenderContext;
namespace Editor { namespace Editor {
class IEditorContext; class IEditorContext;
struct SceneViewportOverlayFrameData;
struct SceneViewportTransformGizmoHandleBuildInputs;
enum class EditorViewportKind { enum class EditorViewportKind {
Scene, Scene,
@@ -83,6 +85,14 @@ public:
const ImVec2& viewportMousePosition) = 0; const ImVec2& viewportMousePosition) = 0;
virtual void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis axis) = 0; virtual void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis axis) = 0;
virtual SceneViewportOverlayData GetSceneViewOverlayData() const = 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( virtual void RenderRequestedViewports(
IEditorContext& context, IEditorContext& context,
const Rendering::RenderContext& renderContext) = 0; const Rendering::RenderContext& renderContext) = 0;

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -128,13 +128,21 @@ public:
if (std::abs(input.lookDeltaX) > Math::EPSILON || if (std::abs(input.lookDeltaX) > Math::EPSILON ||
std::abs(input.lookDeltaY) > Math::EPSILON) { std::abs(input.lookDeltaY) > Math::EPSILON) {
ApplyRotationDelta(input.lookDeltaX, input.lookDeltaY); ApplyRotationDelta(
input.lookDeltaX,
input.lookDeltaY,
kLookYawSensitivity,
kLookPitchSensitivity);
UpdateFocalPointFromPosition(); UpdateFocalPointFromPosition();
} }
if (std::abs(input.orbitDeltaX) > Math::EPSILON || if (std::abs(input.orbitDeltaX) > Math::EPSILON ||
std::abs(input.orbitDeltaY) > Math::EPSILON) { std::abs(input.orbitDeltaY) > Math::EPSILON) {
ApplyRotationDelta(input.orbitDeltaX, input.orbitDeltaY); ApplyRotationDelta(
input.orbitDeltaX,
input.orbitDeltaY,
kOrbitYawSensitivity,
kOrbitPitchSensitivity);
UpdatePositionFromFocalPoint(); UpdatePositionFromFocalPoint();
} }
@@ -200,6 +208,10 @@ public:
private: private:
static constexpr float kSnapDurationSeconds = 0.22f; 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 { struct OrientationAngles {
float yawDegrees = 0.0f; float yawDegrees = 0.0f;
@@ -239,9 +251,13 @@ private:
return result; return result;
} }
void ApplyRotationDelta(float deltaX, float deltaY) { void ApplyRotationDelta(
m_yawDegrees += deltaX * 0.30f; float deltaX,
m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * 0.20f, -89.0f, 89.0f); 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 { Math::Vector3 GetRight() const {

View 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

View File

@@ -14,7 +14,7 @@ namespace {
constexpr float kMoveGizmoHandleLengthPixels = 100.0f; constexpr float kMoveGizmoHandleLengthPixels = 100.0f;
constexpr float kMoveGizmoHoverThresholdPixels = 10.0f; constexpr float kMoveGizmoHoverThresholdPixels = 10.0f;
Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) { Math::Vector3 GetBaseAxisVector(SceneViewportGizmoAxis axis) {
switch (axis) { switch (axis) {
case SceneViewportGizmoAxis::X: case SceneViewportGizmoAxis::X:
return Math::Vector3::Right(); 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) { Math::Color GetAxisBaseColor(SceneViewportGizmoAxis axis) {
switch (axis) { switch (axis) {
case SceneViewportGizmoAxis::X: case SceneViewportGizmoAxis::X:
@@ -61,20 +71,21 @@ SceneViewportGizmoPlane GetPlaneForIndex(size_t index) {
void GetPlaneAxes( void GetPlaneAxes(
SceneViewportGizmoPlane plane, SceneViewportGizmoPlane plane,
const Math::Quaternion& orientation,
Math::Vector3& outAxisA, Math::Vector3& outAxisA,
Math::Vector3& outAxisB) { Math::Vector3& outAxisB) {
switch (plane) { switch (plane) {
case SceneViewportGizmoPlane::XY: case SceneViewportGizmoPlane::XY:
outAxisA = Math::Vector3::Right(); outAxisA = GetAxisVector(SceneViewportGizmoAxis::X, orientation);
outAxisB = Math::Vector3::Up(); outAxisB = GetAxisVector(SceneViewportGizmoAxis::Y, orientation);
return; return;
case SceneViewportGizmoPlane::XZ: case SceneViewportGizmoPlane::XZ:
outAxisA = Math::Vector3::Right(); outAxisA = GetAxisVector(SceneViewportGizmoAxis::X, orientation);
outAxisB = Math::Vector3::Forward(); outAxisB = GetAxisVector(SceneViewportGizmoAxis::Z, orientation);
return; return;
case SceneViewportGizmoPlane::YZ: case SceneViewportGizmoPlane::YZ:
outAxisA = Math::Vector3::Up(); outAxisA = GetAxisVector(SceneViewportGizmoAxis::Y, orientation);
outAxisB = Math::Vector3::Forward(); outAxisB = GetAxisVector(SceneViewportGizmoAxis::Z, orientation);
return; return;
case SceneViewportGizmoPlane::None: case SceneViewportGizmoPlane::None:
default: default:
@@ -84,14 +95,16 @@ void GetPlaneAxes(
} }
} }
Math::Vector3 GetPlaneNormal(SceneViewportGizmoPlane plane) { Math::Vector3 GetPlaneNormal(
SceneViewportGizmoPlane plane,
const Math::Quaternion& orientation) {
switch (plane) { switch (plane) {
case SceneViewportGizmoPlane::XY: case SceneViewportGizmoPlane::XY:
return Math::Vector3::Forward(); return GetAxisVector(SceneViewportGizmoAxis::Z, orientation);
case SceneViewportGizmoPlane::XZ: case SceneViewportGizmoPlane::XZ:
return Math::Vector3::Up(); return GetAxisVector(SceneViewportGizmoAxis::Y, orientation);
case SceneViewportGizmoPlane::YZ: case SceneViewportGizmoPlane::YZ:
return Math::Vector3::Right(); return GetAxisVector(SceneViewportGizmoAxis::X, orientation);
case SceneViewportGizmoPlane::None: case SceneViewportGizmoPlane::None:
default: default:
return Math::Vector3::Zero(); return Math::Vector3::Zero();
@@ -185,10 +198,9 @@ float ComputeWorldUnitsPerPixel(
void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) { void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) {
BuildDrawData(context); BuildDrawData(context);
if (m_dragMode == DragMode::None && IsMouseInsideViewport(context)) { if (m_dragMode == DragMode::None && IsMouseInsideViewport(context)) {
m_hoveredAxis = HitTestAxis(context.mousePosition); const SceneViewportMoveGizmoHitResult hitResult = EvaluateHit(context.mousePosition);
m_hoveredPlane = m_hoveredAxis == SceneViewportGizmoAxis::None m_hoveredAxis = hitResult.axis;
? HitTestPlane(context.mousePosition) m_hoveredPlane = hitResult.plane;
: SceneViewportGizmoPlane::None;
} else if (m_dragMode == DragMode::None) { } else if (m_dragMode == DragMode::None) {
m_hoveredAxis = SceneViewportGizmoAxis::None; m_hoveredAxis = SceneViewportGizmoAxis::None;
m_hoveredPlane = SceneViewportGizmoPlane::None; m_hoveredPlane = SceneViewportGizmoPlane::None;
@@ -221,18 +233,17 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
return false; return false;
} }
const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition(); const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
Math::Vector3 dragPlaneNormal = Math::Vector3::Zero(); Math::Vector3 dragPlaneNormal = Math::Vector3::Zero();
Math::Vector3 worldAxis = Math::Vector3::Zero(); Math::Vector3 worldAxis = Math::Vector3::Zero();
if (m_hoveredAxis != SceneViewportGizmoAxis::None) { if (m_hoveredAxis != SceneViewportGizmoAxis::None) {
worldAxis = GetAxisVector(m_hoveredAxis); worldAxis = GetAxisVector(m_hoveredAxis, context.axisOrientation);
if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) { if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) {
return false; return false;
} }
} else { } else {
dragPlaneNormal = GetPlaneNormal(m_hoveredPlane); dragPlaneNormal = GetPlaneNormal(m_hoveredPlane, context.axisOrientation);
if (dragPlaneNormal.SqrMagnitude() <= Math::EPSILON) { if (dragPlaneNormal.SqrMagnitude() <= Math::EPSILON) {
return false; return false;
} }
@@ -257,10 +268,21 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
m_activeAxisDirection = worldAxis; m_activeAxisDirection = worldAxis;
m_activePlaneNormal = dragPlaneNormal; m_activePlaneNormal = dragPlaneNormal;
m_dragPlane = dragPlane; m_dragPlane = dragPlane;
m_dragStartObjectWorldPosition = objectWorldPosition;
m_dragStartPivotWorldPosition = pivotWorldPosition; m_dragStartPivotWorldPosition = pivotWorldPosition;
m_dragStartHitWorldPosition = hitPoint; m_dragStartHitWorldPosition = hitPoint;
m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - pivotWorldPosition, worldAxis); 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(); RefreshHandleState();
return true; return true;
} }
@@ -268,7 +290,9 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& context) { void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& context) {
if (m_dragMode == DragMode::None || if (m_dragMode == DragMode::None ||
context.selectedObject == nullptr || context.selectedObject == nullptr ||
context.selectedObject->GetID() != m_activeEntityId) { context.selectedObject->GetID() != m_activeEntityId ||
m_dragObjects.empty() ||
m_dragObjects.size() != m_dragStartObjectWorldPositions.size()) {
return; return;
} }
@@ -290,17 +314,28 @@ void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& con
if (m_dragMode == DragMode::Axis) { if (m_dragMode == DragMode::Axis) {
const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection); const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection);
const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar; const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar;
context.selectedObject->GetTransform()->SetPosition( const Math::Vector3 worldDelta = m_activeAxisDirection * deltaScalar;
m_dragStartObjectWorldPosition + 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; return;
} }
if (m_dragMode == DragMode::Plane) { if (m_dragMode == DragMode::Plane) {
const Math::Vector3 planeDelta = Math::Vector3::ProjectOnPlane( const Math::Vector3 worldDelta = Math::Vector3::ProjectOnPlane(
hitPoint - m_dragStartHitWorldPosition, hitPoint - m_dragStartHitWorldPosition,
m_activePlaneNormal); m_activePlaneNormal);
context.selectedObject->GetTransform()->SetPosition( for (size_t index = 0; index < m_dragObjects.size(); ++index) {
m_dragStartObjectWorldPosition + planeDelta); 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_activeEntityId = 0;
m_activeAxisDirection = Math::Vector3::Zero(); m_activeAxisDirection = Math::Vector3::Zero();
m_activePlaneNormal = Math::Vector3::Zero(); m_activePlaneNormal = Math::Vector3::Zero();
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
m_dragStartPivotWorldPosition = Math::Vector3::Zero(); m_dragStartPivotWorldPosition = Math::Vector3::Zero();
m_dragStartHitWorldPosition = Math::Vector3::Zero(); m_dragStartHitWorldPosition = Math::Vector3::Zero();
m_dragStartAxisScalar = 0.0f; m_dragStartAxisScalar = 0.0f;
m_dragObjects.clear();
m_dragStartObjectWorldPositions.clear();
RefreshHandleState(); RefreshHandleState();
} }
@@ -337,10 +373,11 @@ void SceneViewportMoveGizmo::CancelDrag(IUndoManager* undoManager) {
m_activeEntityId = 0; m_activeEntityId = 0;
m_activeAxisDirection = Math::Vector3::Zero(); m_activeAxisDirection = Math::Vector3::Zero();
m_activePlaneNormal = Math::Vector3::Zero(); m_activePlaneNormal = Math::Vector3::Zero();
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
m_dragStartPivotWorldPosition = Math::Vector3::Zero(); m_dragStartPivotWorldPosition = Math::Vector3::Zero();
m_dragStartHitWorldPosition = Math::Vector3::Zero(); m_dragStartHitWorldPosition = Math::Vector3::Zero();
m_dragStartAxisScalar = 0.0f; m_dragStartAxisScalar = 0.0f;
m_dragObjects.clear();
m_dragStartObjectWorldPositions.clear();
m_hoveredAxis = SceneViewportGizmoAxis::None; m_hoveredAxis = SceneViewportGizmoAxis::None;
m_hoveredPlane = SceneViewportGizmoPlane::None; m_hoveredPlane = SceneViewportGizmoPlane::None;
RefreshHandleState(); RefreshHandleState();
@@ -363,19 +400,74 @@ const SceneViewportMoveGizmoDrawData& SceneViewportMoveGizmo::GetDrawData() cons
return m_drawData; 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) { void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& context) {
m_drawData = {}; m_drawData = {};
m_drawData.pivotRadius = 5.0f; m_drawData.pivotRadius = 5.0f;
const Components::GameObject* selectedObject = context.selectedObject; if ((context.selectedObject == nullptr && context.selectedObjects.empty()) ||
if (selectedObject == nullptr ||
!context.overlay.valid || !context.overlay.valid ||
context.viewportSize.x <= 1.0f || context.viewportSize.x <= 1.0f ||
context.viewportSize.y <= 1.0f) { context.viewportSize.y <= 1.0f) {
return; return;
} }
const Math::Vector3 gizmoWorldOrigin = selectedObject->GetTransform()->GetPosition(); const Math::Vector3 gizmoWorldOrigin = context.pivotWorldPosition;
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint( const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
context.overlay, context.overlay,
context.viewportSize.x, context.viewportSize.x,
@@ -411,7 +503,7 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
handle.start = projectedPivot.screenPosition; handle.start = projectedPivot.screenPosition;
const Math::Vector3 axisEndWorld = const Math::Vector3 axisEndWorld =
gizmoWorldOrigin + GetAxisVector(handle.axis) * axisLengthWorld; gizmoWorldOrigin + GetAxisVector(handle.axis, context.axisOrientation) * axisLengthWorld;
const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint( const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint(
context.overlay, context.overlay,
context.viewportSize.x, context.viewportSize.x,
@@ -436,7 +528,7 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
Math::Vector3 axisA = Math::Vector3::Zero(); Math::Vector3 axisA = Math::Vector3::Zero();
Math::Vector3 axisB = 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) { if (axisA.SqrMagnitude() <= Math::EPSILON || axisB.SqrMagnitude() <= Math::EPSILON) {
continue; 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 Editor
} // namespace XCEngine } // namespace XCEngine

View File

@@ -4,11 +4,13 @@
#include <XCEngine/Core/Math/Color.h> #include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Core/Math/Plane.h> #include <XCEngine/Core/Math/Plane.h>
#include <XCEngine/Core/Math/Quaternion.h>
#include <XCEngine/Core/Math/Vector2.h> #include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Math/Vector3.h> #include <XCEngine/Core/Math/Vector3.h>
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <vector>
namespace XCEngine { namespace XCEngine {
namespace Components { namespace Components {
@@ -66,6 +68,19 @@ struct SceneViewportMoveGizmoContext {
Math::Vector2 viewportSize = Math::Vector2::Zero(); Math::Vector2 viewportSize = Math::Vector2::Zero();
Math::Vector2 mousePosition = Math::Vector2::Zero(); Math::Vector2 mousePosition = Math::Vector2::Zero();
Components::GameObject* selectedObject = nullptr; 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 { class SceneViewportMoveGizmo {
@@ -80,6 +95,8 @@ public:
bool IsActive() const; bool IsActive() const;
uint64_t GetActiveEntityId() const; uint64_t GetActiveEntityId() const;
const SceneViewportMoveGizmoDrawData& GetDrawData() const; const SceneViewportMoveGizmoDrawData& GetDrawData() const;
SceneViewportMoveGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const;
void SetHoveredHandle(SceneViewportGizmoAxis axis, SceneViewportGizmoPlane plane);
private: private:
enum class DragMode : uint8_t { enum class DragMode : uint8_t {
@@ -90,8 +107,6 @@ private:
void BuildDrawData(const SceneViewportMoveGizmoContext& context); void BuildDrawData(const SceneViewportMoveGizmoContext& context);
void RefreshHandleState(); void RefreshHandleState();
SceneViewportGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const;
SceneViewportGizmoPlane HitTestPlane(const Math::Vector2& mousePosition) const;
SceneViewportMoveGizmoDrawData m_drawData = {}; SceneViewportMoveGizmoDrawData m_drawData = {};
SceneViewportGizmoAxis m_hoveredAxis = SceneViewportGizmoAxis::None; SceneViewportGizmoAxis m_hoveredAxis = SceneViewportGizmoAxis::None;
@@ -103,10 +118,11 @@ private:
Math::Vector3 m_activeAxisDirection = Math::Vector3::Zero(); Math::Vector3 m_activeAxisDirection = Math::Vector3::Zero();
Math::Vector3 m_activePlaneNormal = Math::Vector3::Zero(); Math::Vector3 m_activePlaneNormal = Math::Vector3::Zero();
Math::Plane m_dragPlane = {}; Math::Plane m_dragPlane = {};
Math::Vector3 m_dragStartObjectWorldPosition = Math::Vector3::Zero();
Math::Vector3 m_dragStartPivotWorldPosition = Math::Vector3::Zero(); Math::Vector3 m_dragStartPivotWorldPosition = Math::Vector3::Zero();
Math::Vector3 m_dragStartHitWorldPosition = Math::Vector3::Zero(); Math::Vector3 m_dragStartHitWorldPosition = Math::Vector3::Zero();
float m_dragStartAxisScalar = 0.0f; float m_dragStartAxisScalar = 0.0f;
std::vector<Components::GameObject*> m_dragObjects = {};
std::vector<Math::Vector3> m_dragStartObjectWorldPositions = {};
}; };
} // namespace Editor } // namespace Editor

View 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

View 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

View 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

View 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

View File

@@ -1,305 +1,16 @@
#include "SceneViewportOverlayRenderer.h" #include "SceneViewportOverlayRenderer.h"
#include "SceneViewportOrientationGizmo.h"
#include <algorithm> #include "SceneViewportOrientationGizmo.h"
#include <cmath>
namespace XCEngine { namespace XCEngine {
namespace Editor { 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( void DrawSceneViewportOverlay(
ImDrawList* drawList, ImDrawList* drawList,
const SceneViewportOverlayData& overlay, const SceneViewportOverlayData& overlay,
const ImVec2& viewportMin, const ImVec2& viewportMin,
const ImVec2& viewportMax, const ImVec2& viewportMax,
const ImVec2& viewportSize, const ImVec2& viewportSize) {
const SceneViewportMoveGizmoDrawData* moveGizmo,
const SceneViewportRotateGizmoDrawData* rotateGizmo,
const SceneViewportScaleGizmoDrawData* scaleGizmo) {
if (drawList == nullptr || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) { if (drawList == nullptr || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) {
return; return;
} }
@@ -308,15 +19,6 @@ void DrawSceneViewportOverlay(
if (overlay.valid) { if (overlay.valid) {
DrawSceneViewportOrientationGizmo(drawList, overlay, viewportMin, viewportMax); 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(); drawList->PopClipRect();
} }

View File

@@ -1,9 +1,6 @@
#pragma once #pragma once
#include "IViewportHostService.h" #include "IViewportHostService.h"
#include "SceneViewportMoveGizmo.h"
#include "SceneViewportRotateGizmo.h"
#include "SceneViewportScaleGizmo.h"
#include <imgui.h> #include <imgui.h>
@@ -15,10 +12,7 @@ void DrawSceneViewportOverlay(
const SceneViewportOverlayData& overlay, const SceneViewportOverlayData& overlay,
const ImVec2& viewportMin, const ImVec2& viewportMin,
const ImVec2& viewportMax, const ImVec2& viewportMax,
const ImVec2& viewportSize, const ImVec2& viewportSize);
const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr,
const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr,
const SceneViewportScaleGizmoDrawData* scaleGizmo = nullptr);
} // namespace Editor } // namespace Editor
} // namespace XCEngine } // namespace XCEngine

View File

@@ -19,6 +19,21 @@ constexpr float kRotateGizmoViewRadiusPixels = 106.0f;
constexpr float kRotateGizmoHoverThresholdPixels = 9.0f; constexpr float kRotateGizmoHoverThresholdPixels = 9.0f;
constexpr float kRotateGizmoAngleFillMinRadians = 0.01f; 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) { Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized(); return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
} }
@@ -30,6 +45,22 @@ bool IsMouseInsideViewport(const SceneViewportRotateGizmoContext& context) {
context.mousePosition.y <= context.viewportSize.y; 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) { Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) {
switch (axis) { switch (axis) {
case SceneViewportRotateGizmoAxis::X: case SceneViewportRotateGizmoAxis::X:
@@ -48,14 +79,13 @@ Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) {
Math::Vector3 GetRotateAxisVector( Math::Vector3 GetRotateAxisVector(
SceneViewportRotateGizmoAxis axis, SceneViewportRotateGizmoAxis axis,
const SceneViewportOverlayData& overlay) { const SceneViewportOverlayData& overlay,
const Math::Quaternion& axisOrientation) {
switch (axis) { switch (axis) {
case SceneViewportRotateGizmoAxis::X: case SceneViewportRotateGizmoAxis::X:
return Math::Vector3::Right();
case SceneViewportRotateGizmoAxis::Y: case SceneViewportRotateGizmoAxis::Y:
return Math::Vector3::Up();
case SceneViewportRotateGizmoAxis::Z: case SceneViewportRotateGizmoAxis::Z:
return Math::Vector3::Forward(); return NormalizeVector3(axisOrientation * GetBaseRotateAxisVector(axis), GetBaseRotateAxisVector(axis));
case SceneViewportRotateGizmoAxis::View: case SceneViewportRotateGizmoAxis::View:
return NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward()); return NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward());
case SceneViewportRotateGizmoAxis::None: case SceneViewportRotateGizmoAxis::None:
@@ -67,20 +97,21 @@ Math::Vector3 GetRotateAxisVector(
bool GetRotateRingBasis( bool GetRotateRingBasis(
SceneViewportRotateGizmoAxis axis, SceneViewportRotateGizmoAxis axis,
const SceneViewportOverlayData& overlay, const SceneViewportOverlayData& overlay,
const Math::Quaternion& axisOrientation,
Math::Vector3& outBasisA, Math::Vector3& outBasisA,
Math::Vector3& outBasisB) { Math::Vector3& outBasisB) {
switch (axis) { switch (axis) {
case SceneViewportRotateGizmoAxis::X: case SceneViewportRotateGizmoAxis::X:
outBasisA = Math::Vector3::Up(); outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up());
outBasisB = Math::Vector3::Forward(); outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward());
return true; return true;
case SceneViewportRotateGizmoAxis::Y: case SceneViewportRotateGizmoAxis::Y:
outBasisA = Math::Vector3::Forward(); outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward());
outBasisB = Math::Vector3::Right(); outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right());
return true; return true;
case SceneViewportRotateGizmoAxis::Z: case SceneViewportRotateGizmoAxis::Z:
outBasisA = Math::Vector3::Right(); outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right());
outBasisB = Math::Vector3::Up(); outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up());
return true; return true;
case SceneViewportRotateGizmoAxis::View: case SceneViewportRotateGizmoAxis::View:
outBasisA = NormalizeVector3(overlay.cameraRight, Math::Vector3::Right()); outBasisA = NormalizeVector3(overlay.cameraRight, Math::Vector3::Right());
@@ -147,11 +178,12 @@ SceneViewportRotateGizmoAxis GetRotateAxisForIndex(size_t index) {
bool TryComputeRingAngleFromWorldDirection( bool TryComputeRingAngleFromWorldDirection(
SceneViewportRotateGizmoAxis axis, SceneViewportRotateGizmoAxis axis,
const SceneViewportOverlayData& overlay, const SceneViewportOverlayData& overlay,
const Math::Quaternion& axisOrientation,
const Math::Vector3& directionWorld, const Math::Vector3& directionWorld,
float& outAngle) { float& outAngle) {
Math::Vector3 basisA = Math::Vector3::Zero(); Math::Vector3 basisA = Math::Vector3::Zero();
Math::Vector3 basisB = Math::Vector3::Zero(); Math::Vector3 basisB = Math::Vector3::Zero();
if (!GetRotateRingBasis(axis, overlay, basisA, basisB)) { if (!GetRotateRingBasis(axis, overlay, axisOrientation, basisA, basisB)) {
return false; return false;
} }
@@ -171,7 +203,7 @@ bool TryComputeRingAngleFromWorldDirection(
void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) { void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) {
BuildDrawData(context); BuildDrawData(context);
if (m_activeAxis == SceneViewportRotateGizmoAxis::None && IsMouseInsideViewport(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) { } else if (m_activeAxis == SceneViewportRotateGizmoAxis::None) {
m_hoveredAxis = SceneViewportRotateGizmoAxis::None; m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
} else { } else {
@@ -190,8 +222,8 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
return false; return false;
} }
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition(); const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay); const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay, context.axisOrientation);
if (worldAxis.SqrMagnitude() <= Math::EPSILON) { if (worldAxis.SqrMagnitude() <= Math::EPSILON) {
return false; return false;
} }
@@ -225,6 +257,7 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
if (!TryComputeRingAngleFromWorldDirection( if (!TryComputeRingAngleFromWorldDirection(
m_hoveredAxis, m_hoveredAxis,
context.overlay, context.overlay,
context.axisOrientation,
startDirection, startDirection,
startRingAngle)) { startRingAngle)) {
return false; return false;
@@ -238,12 +271,31 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
m_activeAxis = m_hoveredAxis; m_activeAxis = m_hoveredAxis;
m_activeEntityId = context.selectedObject->GetID(); m_activeEntityId = context.selectedObject->GetID();
m_localSpace = context.localSpace && m_hoveredAxis != SceneViewportRotateGizmoAxis::View;
m_rotateAroundSharedPivot = context.rotateAroundSharedPivot;
m_activeWorldAxis = worldAxis.Normalized(); m_activeWorldAxis = worldAxis.Normalized();
m_screenSpaceDrag = useScreenSpaceDrag; m_screenSpaceDrag = useScreenSpaceDrag;
m_dragPlane = dragPlane; m_dragPlane = dragPlane;
m_dragStartWorldRotation = context.selectedObject->GetTransform()->GetRotation();
m_dragStartRingAngle = startRingAngle; m_dragStartRingAngle = startRingAngle;
m_dragCurrentDeltaRadians = 0.0f; 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(); RefreshHandleState();
return true; return true;
} }
@@ -251,7 +303,10 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& context) { void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& context) {
if (m_activeAxis == SceneViewportRotateGizmoAxis::None || if (m_activeAxis == SceneViewportRotateGizmoAxis::None ||
context.selectedObject == nullptr || 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; return;
} }
@@ -275,7 +330,7 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
return; 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 hitPoint = worldRay.GetPoint(hitDistance);
const Math::Vector3 currentDirection = Math::Vector3::ProjectOnPlane(hitPoint - pivotWorldPosition, m_activeWorldAxis); const Math::Vector3 currentDirection = Math::Vector3::ProjectOnPlane(hitPoint - pivotWorldPosition, m_activeWorldAxis);
if (currentDirection.SqrMagnitude() <= Math::EPSILON) { if (currentDirection.SqrMagnitude() <= Math::EPSILON) {
@@ -285,6 +340,7 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
if (!TryComputeRingAngleFromWorldDirection( if (!TryComputeRingAngleFromWorldDirection(
m_activeAxis, m_activeAxis,
context.overlay, context.overlay,
context.axisOrientation,
currentDirection, currentDirection,
currentRingAngle)) { currentRingAngle)) {
return; return;
@@ -293,9 +349,37 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
const float deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle); const float deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle);
m_dragCurrentDeltaRadians = deltaRadians; m_dragCurrentDeltaRadians = deltaRadians;
const Math::Quaternion deltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians); const Math::Quaternion worldDeltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians);
context.selectedObject->GetTransform()->SetRotation(deltaRotation * m_dragStartWorldRotation); const Math::Vector3 localAxis = GetBaseRotateAxisVector(m_activeAxis);
BuildDrawData(context); 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; m_hoveredAxis = m_activeAxis;
RefreshHandleState(); RefreshHandleState();
} }
@@ -312,10 +396,15 @@ void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) {
m_activeAxis = SceneViewportRotateGizmoAxis::None; m_activeAxis = SceneViewportRotateGizmoAxis::None;
m_activeEntityId = 0; m_activeEntityId = 0;
m_screenSpaceDrag = false; m_screenSpaceDrag = false;
m_localSpace = false;
m_rotateAroundSharedPivot = false;
m_activeWorldAxis = Math::Vector3::Zero(); m_activeWorldAxis = Math::Vector3::Zero();
m_dragStartWorldRotation = Math::Quaternion::Identity();
m_dragStartRingAngle = 0.0f; m_dragStartRingAngle = 0.0f;
m_dragCurrentDeltaRadians = 0.0f; m_dragCurrentDeltaRadians = 0.0f;
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
m_dragObjects.clear();
m_dragStartWorldPositions.clear();
m_dragStartWorldRotations.clear();
RefreshHandleState(); RefreshHandleState();
} }
@@ -327,10 +416,15 @@ void SceneViewportRotateGizmo::CancelDrag(IUndoManager* undoManager) {
m_activeAxis = SceneViewportRotateGizmoAxis::None; m_activeAxis = SceneViewportRotateGizmoAxis::None;
m_activeEntityId = 0; m_activeEntityId = 0;
m_screenSpaceDrag = false; m_screenSpaceDrag = false;
m_localSpace = false;
m_rotateAroundSharedPivot = false;
m_activeWorldAxis = Math::Vector3::Zero(); m_activeWorldAxis = Math::Vector3::Zero();
m_dragStartWorldRotation = Math::Quaternion::Identity();
m_dragStartRingAngle = 0.0f; m_dragStartRingAngle = 0.0f;
m_dragCurrentDeltaRadians = 0.0f; m_dragCurrentDeltaRadians = 0.0f;
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
m_dragObjects.clear();
m_dragStartWorldPositions.clear();
m_dragStartWorldRotations.clear();
m_hoveredAxis = SceneViewportRotateGizmoAxis::None; m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
RefreshHandleState(); RefreshHandleState();
} }
@@ -351,18 +445,57 @@ const SceneViewportRotateGizmoDrawData& SceneViewportRotateGizmo::GetDrawData()
return m_drawData; 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) { void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoContext& context) {
m_drawData = {}; m_drawData = {};
const Components::GameObject* selectedObject = context.selectedObject; if ((context.selectedObject == nullptr && context.selectedObjects.empty()) ||
if (selectedObject == nullptr ||
!context.overlay.valid || !context.overlay.valid ||
context.viewportSize.x <= 1.0f || context.viewportSize.x <= 1.0f ||
context.viewportSize.y <= 1.0f) { context.viewportSize.y <= 1.0f) {
return; return;
} }
const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition(); const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint( const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
context.overlay, context.overlay,
context.viewportSize.x, context.viewportSize.x,
@@ -383,6 +516,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
m_drawData.visible = true; m_drawData.visible = true;
m_drawData.pivot = projectedPivot.screenPosition; m_drawData.pivot = projectedPivot.screenPosition;
const bool hasActiveDragFeedback = const bool hasActiveDragFeedback =
!context.localSpace &&
m_activeAxis != SceneViewportRotateGizmoAxis::None && m_activeAxis != SceneViewportRotateGizmoAxis::None &&
m_activeAxis != SceneViewportRotateGizmoAxis::View && m_activeAxis != SceneViewportRotateGizmoAxis::View &&
std::abs(m_dragCurrentDeltaRadians) > Math::EPSILON; std::abs(m_dragCurrentDeltaRadians) > Math::EPSILON;
@@ -398,7 +532,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
Math::Vector3 basisA = Math::Vector3::Zero(); Math::Vector3 basisA = Math::Vector3::Zero();
Math::Vector3 basisB = 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; continue;
} }
if (hasActiveDragFeedback && handle.axis != SceneViewportRotateGizmoAxis::View) { if (hasActiveDragFeedback && handle.axis != SceneViewportRotateGizmoAxis::View) {
@@ -468,7 +602,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
Math::Vector3 basisA = Math::Vector3::Zero(); Math::Vector3 basisA = Math::Vector3::Zero();
Math::Vector3 basisB = 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 ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(m_activeAxis);
const float sweepRadians = NormalizeSignedAngleRadians(m_dragCurrentDeltaRadians); const float sweepRadians = NormalizeSignedAngleRadians(m_dragCurrentDeltaRadians);
const float sweepAbs = std::abs(sweepRadians); 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( bool SceneViewportRotateGizmo::TryGetClosestRingAngle(
SceneViewportRotateGizmoAxis axis, SceneViewportRotateGizmoAxis axis,
const Math::Vector2& mousePosition, const Math::Vector2& mousePosition,

View File

@@ -10,6 +10,7 @@
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <vector>
namespace XCEngine { namespace XCEngine {
namespace Components { namespace Components {
@@ -71,6 +72,20 @@ struct SceneViewportRotateGizmoContext {
Math::Vector2 viewportSize = Math::Vector2::Zero(); Math::Vector2 viewportSize = Math::Vector2::Zero();
Math::Vector2 mousePosition = Math::Vector2::Zero(); Math::Vector2 mousePosition = Math::Vector2::Zero();
Components::GameObject* selectedObject = nullptr; 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 { class SceneViewportRotateGizmo {
@@ -85,11 +100,12 @@ public:
bool IsActive() const; bool IsActive() const;
uint64_t GetActiveEntityId() const; uint64_t GetActiveEntityId() const;
const SceneViewportRotateGizmoDrawData& GetDrawData() const; const SceneViewportRotateGizmoDrawData& GetDrawData() const;
SceneViewportRotateGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const;
void SetHoveredHandle(SceneViewportRotateGizmoAxis axis);
private: private:
void BuildDrawData(const SceneViewportRotateGizmoContext& context); void BuildDrawData(const SceneViewportRotateGizmoContext& context);
void RefreshHandleState(); void RefreshHandleState();
SceneViewportRotateGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const;
bool TryGetClosestRingAngle( bool TryGetClosestRingAngle(
SceneViewportRotateGizmoAxis axis, SceneViewportRotateGizmoAxis axis,
const Math::Vector2& mousePosition, const Math::Vector2& mousePosition,
@@ -101,11 +117,16 @@ private:
SceneViewportRotateGizmoAxis m_activeAxis = SceneViewportRotateGizmoAxis::None; SceneViewportRotateGizmoAxis m_activeAxis = SceneViewportRotateGizmoAxis::None;
uint64_t m_activeEntityId = 0; uint64_t m_activeEntityId = 0;
bool m_screenSpaceDrag = false; bool m_screenSpaceDrag = false;
bool m_localSpace = false;
bool m_rotateAroundSharedPivot = false;
Math::Vector3 m_activeWorldAxis = Math::Vector3::Zero(); Math::Vector3 m_activeWorldAxis = Math::Vector3::Zero();
Math::Plane m_dragPlane = {}; Math::Plane m_dragPlane = {};
Math::Quaternion m_dragStartWorldRotation = Math::Quaternion::Identity();
float m_dragStartRingAngle = 0.0f; float m_dragStartRingAngle = 0.0f;
float m_dragCurrentDeltaRadians = 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 } // namespace Editor

View File

@@ -130,7 +130,7 @@ float ComputeVisualScaleFactor(float current, float start) {
void SceneViewportScaleGizmo::Update(const SceneViewportScaleGizmoContext& context) { void SceneViewportScaleGizmo::Update(const SceneViewportScaleGizmoContext& context) {
BuildDrawData(context); BuildDrawData(context);
if (m_activeHandle == SceneViewportScaleGizmoHandle::None && IsMouseInsideViewport(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) { } else if (m_activeHandle == SceneViewportScaleGizmoHandle::None) {
m_hoveredHandle = SceneViewportScaleGizmoHandle::None; m_hoveredHandle = SceneViewportScaleGizmoHandle::None;
} else { } else {
@@ -158,7 +158,7 @@ bool SceneViewportScaleGizmo::TryBeginDrag(const SceneViewportScaleGizmoContext&
activeScreenDirection = handle->end - handle->start; activeScreenDirection = handle->end - handle->start;
if (activeScreenDirection.SqrMagnitude() <= Math::EPSILON) { if (activeScreenDirection.SqrMagnitude() <= Math::EPSILON) {
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition(); const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
if (!ProjectSceneViewportAxisDirectionAtPoint( if (!ProjectSceneViewportAxisDirectionAtPoint(
context.overlay, context.overlay,
context.viewportSize.x, context.viewportSize.x,
@@ -310,6 +310,68 @@ const SceneViewportScaleGizmoDrawData& SceneViewportScaleGizmo::GetDrawData() co
return m_drawData; 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) { void SceneViewportScaleGizmo::BuildDrawData(const SceneViewportScaleGizmoContext& context) {
m_drawData = {}; m_drawData = {};
@@ -321,7 +383,7 @@ void SceneViewportScaleGizmo::BuildDrawData(const SceneViewportScaleGizmoContext
return; return;
} }
const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition(); const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint( const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
context.overlay, context.overlay,
context.viewportSize.x, context.viewportSize.x,
@@ -435,58 +497,6 @@ void SceneViewportScaleGizmo::RefreshHandleState() {
m_drawData.centerHandle.active ? 1.0f : (m_drawData.centerHandle.hovered ? 0.96f : 0.88f)); 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( const SceneViewportScaleGizmoAxisHandleDrawData* SceneViewportScaleGizmo::FindAxisHandleDrawData(
SceneViewportScaleGizmoHandle handle) const { SceneViewportScaleGizmoHandle handle) const {
for (const SceneViewportScaleGizmoAxisHandleDrawData& drawHandle : m_drawData.axisHandles) { for (const SceneViewportScaleGizmoAxisHandleDrawData& drawHandle : m_drawData.axisHandles) {

View File

@@ -3,6 +3,7 @@
#include "IViewportHostService.h" #include "IViewportHostService.h"
#include <XCEngine/Core/Math/Color.h> #include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Core/Math/Quaternion.h>
#include <XCEngine/Core/Math/Vector2.h> #include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Math/Vector3.h> #include <XCEngine/Core/Math/Vector3.h>
@@ -59,9 +60,20 @@ struct SceneViewportScaleGizmoContext {
Math::Vector2 viewportSize = Math::Vector2::Zero(); Math::Vector2 viewportSize = Math::Vector2::Zero();
Math::Vector2 mousePosition = Math::Vector2::Zero(); Math::Vector2 mousePosition = Math::Vector2::Zero();
Components::GameObject* selectedObject = nullptr; Components::GameObject* selectedObject = nullptr;
Math::Vector3 pivotWorldPosition = Math::Vector3::Zero();
Math::Quaternion axisOrientation = Math::Quaternion::Identity();
bool uniformOnly = false; bool uniformOnly = false;
}; };
struct SceneViewportScaleGizmoHitResult {
SceneViewportScaleGizmoHandle handle = SceneViewportScaleGizmoHandle::None;
float distanceSq = Math::FLOAT_MAX;
bool HasHit() const {
return handle != SceneViewportScaleGizmoHandle::None;
}
};
class SceneViewportScaleGizmo { class SceneViewportScaleGizmo {
public: public:
void Update(const SceneViewportScaleGizmoContext& context); void Update(const SceneViewportScaleGizmoContext& context);
@@ -74,11 +86,12 @@ public:
bool IsActive() const; bool IsActive() const;
uint64_t GetActiveEntityId() const; uint64_t GetActiveEntityId() const;
const SceneViewportScaleGizmoDrawData& GetDrawData() const; const SceneViewportScaleGizmoDrawData& GetDrawData() const;
SceneViewportScaleGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const;
void SetHoveredHandle(SceneViewportScaleGizmoHandle handle);
private: private:
void BuildDrawData(const SceneViewportScaleGizmoContext& context); void BuildDrawData(const SceneViewportScaleGizmoContext& context);
void RefreshHandleState(); void RefreshHandleState();
SceneViewportScaleGizmoHandle HitTestHandle(const Math::Vector2& mousePosition) const;
const SceneViewportScaleGizmoAxisHandleDrawData* FindAxisHandleDrawData( const SceneViewportScaleGizmoAxisHandleDrawData* FindAxisHandleDrawData(
SceneViewportScaleGizmoHandle handle) const; SceneViewportScaleGizmoHandle handle) const;

View 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

View File

@@ -166,7 +166,9 @@ inline void ApplySceneViewportRenderRequestSetup(
const Rendering::BuiltinPostProcessRequest* builtinPostProcess, const Rendering::BuiltinPostProcessRequest* builtinPostProcess,
Rendering::RenderPassSequence* postPasses, Rendering::RenderPassSequence* postPasses,
Rendering::CameraRenderRequest& request) { Rendering::CameraRenderRequest& request) {
request.preScenePasses = nullptr;
request.postScenePasses = nullptr; request.postScenePasses = nullptr;
request.overlayPasses = nullptr;
request.objectId = {}; request.objectId = {};
request.builtinPostProcess = {}; request.builtinPostProcess = {};

View File

@@ -4,7 +4,11 @@
#include "Core/ISceneManager.h" #include "Core/ISceneManager.h"
#include "Core/ISelectionManager.h" #include "Core/ISelectionManager.h"
#include "IViewportHostService.h" #include "IViewportHostService.h"
#include "Passes/SceneViewportEditorOverlayPass.h"
#include "SceneViewportCameraController.h" #include "SceneViewportCameraController.h"
#include "SceneViewportEditorOverlayData.h"
#include "SceneViewportOverlayHandleBuilder.h"
#include "SceneViewportOverlayBuilder.h"
#include "ViewportHostRenderFlowUtils.h" #include "ViewportHostRenderFlowUtils.h"
#include "ViewportHostRenderTargets.h" #include "ViewportHostRenderTargets.h"
#include "ViewportObjectIdPicker.h" #include "ViewportObjectIdPicker.h"
@@ -12,6 +16,7 @@
#include <XCEngine/Components/CameraComponent.h> #include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/GameObject.h> #include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/LightComponent.h>
#include <XCEngine/Core/Asset/ResourceManager.h> #include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/RHI/RHIDevice.h> #include <XCEngine/RHI/RHIDevice.h>
#include <XCEngine/RHI/RHIEnums.h> #include <XCEngine/RHI/RHIEnums.h>
@@ -24,7 +29,9 @@
#include <XCEngine/Scene/Scene.h> #include <XCEngine/Scene/Scene.h>
#include <array> #include <array>
#include <cmath>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -35,6 +42,133 @@ namespace Editor {
namespace { namespace {
constexpr bool kDebugSceneSelectionMask = false; 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) { Math::Vector3 GetSceneViewportOrientationAxisVector(SceneViewportOrientationAxis axis) {
switch (axis) { switch (axis) {
@@ -71,7 +205,9 @@ public:
entry = {}; entry = {};
} }
m_sceneViewportEditorOverlayRenderer.Shutdown();
m_sceneViewCamera = {}; m_sceneViewCamera = {};
ResetSceneViewEditorOverlayFrameData();
m_sceneViewLastRenderContext = {}; m_sceneViewLastRenderContext = {};
m_device = nullptr; m_device = nullptr;
m_backend = nullptr; m_backend = nullptr;
@@ -84,6 +220,8 @@ public:
entry.requestedWidth = 0; entry.requestedWidth = 0;
entry.requestedHeight = 0; entry.requestedHeight = 0;
} }
m_sceneViewTransientTransformGizmoOverlay = {};
m_sceneViewTransientTransformGizmoInputs = {};
} }
EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) override { EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) override {
@@ -212,6 +350,30 @@ public:
return data; 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( void RenderRequestedViewports(
IEditorContext& context, IEditorContext& context,
const Rendering::RenderContext& renderContext) override { const Rendering::RenderContext& renderContext) override {
@@ -256,6 +418,7 @@ private:
struct SceneViewportRenderState { struct SceneViewportRenderState {
SceneViewportOverlayData overlay = {}; SceneViewportOverlayData overlay = {};
Rendering::BuiltinPostProcessRequest builtinPostProcess = {}; Rendering::BuiltinPostProcessRequest builtinPostProcess = {};
SceneViewportOverlayFrameData editorOverlayFrameData = {};
std::vector<uint64_t> selectedObjectIds; std::vector<uint64_t> selectedObjectIds;
}; };
@@ -365,6 +528,86 @@ private:
return BuildViewportColorSurface(entry.renderTargets); 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( void ApplyViewportRenderFailure(
ViewportEntry& entry, ViewportEntry& entry,
const Rendering::RenderContext& renderContext, const Rendering::RenderContext& renderContext,
@@ -399,6 +642,7 @@ private:
} }
outState.selectedObjectIds = context.GetSelectionManager().GetSelectedEntities(); outState.selectedObjectIds = context.GetSelectionManager().GetSelectedEntities();
outState.editorOverlayFrameData = GetSceneViewEditorOverlayFrameData(context);
const SceneViewportBuiltinPostProcessBuildResult builtinPostProcess = const SceneViewportBuiltinPostProcessBuildResult builtinPostProcess =
BuildSceneViewportBuiltinPostProcess( BuildSceneViewportBuiltinPostProcess(
outState.overlay, outState.overlay,
@@ -456,6 +700,18 @@ private:
&sceneState.builtinPostProcess, &sceneState.builtinPostProcess,
nullptr, nullptr,
requests[0]); 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].hasClearColorOverride = true;
requests[0].clearColorOverride = Math::Color(0.27f, 0.27f, 0.27f, 1.0f); 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; UI::ImGuiBackendBridge* m_backend = nullptr;
RHI::RHIDevice* m_device = nullptr; RHI::RHIDevice* m_device = nullptr;
std::unique_ptr<Rendering::SceneRenderer> m_sceneRenderer; std::unique_ptr<Rendering::SceneRenderer> m_sceneRenderer;
Rendering::RenderContext m_sceneViewLastRenderContext = {}; Rendering::RenderContext m_sceneViewLastRenderContext = {};
std::array<ViewportEntry, 2> m_entries = {}; std::array<ViewportEntry, 2> m_entries = {};
SceneViewCameraState m_sceneViewCamera; 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 } // namespace Editor

View File

@@ -1,12 +1,166 @@
#include "Actions/ActionRouting.h" #include "Actions/ActionRouting.h"
#include "Core/EventBus.h"
#include "Core/EditorEvents.h"
#include "GameViewPanel.h" #include "GameViewPanel.h"
#include "ViewportPanelContent.h" #include "ViewportPanelContent.h"
#include "UI/UI.h" #include "UI/UI.h"
#include <XCEngine/Input/InputTypes.h>
#include <imgui.h> #include <imgui.h>
namespace XCEngine { namespace XCEngine {
namespace Editor { 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") {} GameViewPanel::GameViewPanel() : Panel("Game") {}
void GameViewPanel::Render() { void GameViewPanel::Render() {
@@ -14,10 +168,12 @@ void GameViewPanel::Render() {
UI::PanelWindowScope panel(m_name.c_str()); UI::PanelWindowScope panel(m_name.c_str());
ImGui::PopStyleVar(); ImGui::PopStyleVar();
if (!panel.IsOpen()) { if (!panel.IsOpen()) {
PublishGameViewInputFrame(m_context, GameViewInputFrameEvent{});
return; return;
} }
RenderViewportPanelContent(*m_context, EditorViewportKind::Game); const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Game);
PublishGameViewInputFrame(m_context, BuildGameViewInputFrame(content));
Actions::ObserveInactiveActionRoute(*m_context); Actions::ObserveInactiveActionRoute(*m_context);
} }

View File

@@ -1,10 +1,18 @@
#pragma once #pragma once
#include "Panel.h" #include "Panel.h"
#include "Core/AssetItem.h"
#include "Core/EditorActionRoute.h"
#include "UI/PopupState.h" #include "UI/PopupState.h"
#include <XCEngine/Core/Asset/ResourceHandle.h>
#include <XCEngine/Resources/Material/Material.h>
#include <array>
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
#include <string>
#include <vector>
namespace XCEngine { namespace XCEngine {
namespace Components { namespace Components {
@@ -23,7 +31,55 @@ public:
void OnDetach() override; void OnDetach() override;
void Render() 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: 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 RenderGameObject(::XCEngine::Components::GameObject* gameObject);
void RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject); void RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject);
void RenderEmptyState(const char* title, const char* subtitle = nullptr); void RenderEmptyState(const char* title, const char* subtitle = nullptr);
@@ -31,6 +87,11 @@ private:
uint64_t m_selectionHandlerId = 0; uint64_t m_selectionHandlerId = 0;
uint64_t m_selectedEntityId = 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; UI::DeferredPopupState m_addComponentPopup;
std::function<void()> m_deferredContextAction; std::function<void()> m_deferredContextAction;
}; };

View File

@@ -21,6 +21,10 @@ constexpr float kRunToolbarHeight = 32.0f;
constexpr float kRunToolbarButtonExtent = 24.0f; constexpr float kRunToolbarButtonExtent = 24.0f;
constexpr float kRunToolbarButtonSpacing = 8.0f; constexpr float kRunToolbarButtonSpacing = 8.0f;
constexpr float kRunToolbarIconInset = 3.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); constexpr ImVec4 kRunToolbarBackgroundColor(0.1f, 0.1f, 0.1f, 1.0f);
std::string BuildRunToolbarIconPath(const char* fileName) { std::string BuildRunToolbarIconPath(const char* fileName) {
@@ -108,9 +112,14 @@ void MenuBar::RenderChrome() {
} }
Actions::HandleMenuBarShortcuts(*m_context); 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); Actions::DrawMainMenuBar(*m_context, m_aboutPopup);
ImGui::PopStyleColor(); ImGui::PopStyleColor(4);
ImGui::PopStyleVar();
RenderRunToolbar(); RenderRunToolbar();
} }

View File

@@ -5,11 +5,16 @@
#include "Core/IEditorContext.h" #include "Core/IEditorContext.h"
#include "Core/IProjectManager.h" #include "Core/IProjectManager.h"
#include "Core/AssetItem.h" #include "Core/AssetItem.h"
#include "Platform/Win32Utf8.h"
#include "Utils/ProjectFileUtils.h"
#include "UI/UI.h" #include "UI/UI.h"
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <filesystem>
#include <imgui.h> #include <imgui.h>
#include <shellapi.h>
#include <vector>
namespace XCEngine { namespace XCEngine {
namespace Editor { namespace Editor {
@@ -86,6 +91,52 @@ UI::AssetTileOptions MakeProjectAssetTileOptions() {
return options; 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 } // namespace
ProjectPanel::ProjectPanel() : Panel("Project") { ProjectPanel::ProjectPanel() : Panel("Project") {
@@ -186,6 +237,93 @@ void ProjectPanel::CancelRename() {
m_renameState.Cancel(); 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() { void ProjectPanel::Render() {
UI::PanelWindowScope panel(m_name.c_str()); UI::PanelWindowScope panel(m_name.c_str());
if (!panel.IsOpen()) { if (!panel.IsOpen()) {
@@ -265,7 +403,6 @@ void ProjectPanel::RenderToolbar() {
} }
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) { void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
auto* managerPtr = &manager;
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor()); ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding()); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding());
const bool open = ImGui::BeginChild("ProjectFolderTree", ImVec2(m_navigationWidth, 0.0f), false); 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")) { if (UI::BeginContextMenuForWindow("##ProjectFolderTreeContext")) {
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() { DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr));
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
BeginRename(createdFolder);
}
});
});
UI::EndContextMenu(); UI::EndContextMenu();
} }
@@ -351,7 +482,6 @@ void ProjectPanel::RenderFolderTreeNode(
} }
void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
auto* managerPtr = &manager;
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor()); ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
const bool open = ImGui::BeginChild("ProjectBrowser", ImVec2(0.0f, 0.0f), false); 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 tileWidth = UI::AssetTileSize().x;
const float tileHeight = UI::AssetTileSize().y;
const float spacing = UI::AssetGridSpacing().x; const float spacing = UI::AssetGridSpacing().x;
const float rowSpacing = UI::AssetGridSpacing().y; const float rowSpacing = UI::AssetGridSpacing().y;
const float panelWidth = ImGui::GetContentRegionAvail().x; const float panelWidth = ImGui::GetContentRegionAvail().x;
@@ -402,19 +531,41 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
columns = 1; 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 pendingSelection;
AssetItemPtr pendingOpenTarget; AssetItemPtr pendingOpenTarget;
const std::string selectedItemPath = manager.GetSelectedItemPath(); const std::string selectedItemPath = manager.GetSelectedItemPath();
const ImVec2 gridOrigin = ImGui::GetCursorPos(); 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) { for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
const int column = visibleIndex % columns; const int column = visibleIndex % columns;
const int row = visibleIndex / columns; const int row = visibleIndex / columns;
ImGui::SetCursorPos(ImVec2( ImGui::SetCursorPos(ImVec2(
gridOrigin.x + column * (tileWidth + spacing), gridOrigin.x + column * (tileWidth + spacing),
gridOrigin.y + row * (tileHeight + rowSpacing))); rowOffsets[static_cast<size_t>(row)]));
const AssetItemPtr& item = visibleItems[visibleIndex]; const AssetItemPtr& item = visibleItems[visibleIndex];
const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath); const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath);
++renderedItemCount;
if (interaction.clicked) { if (interaction.clicked) {
pendingSelection = item; pendingSelection = item;
} }
@@ -427,9 +578,14 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
} }
} }
if (!visibleItems.empty()) { if (renderedItemCount > 0) {
const int rowCount = (static_cast<int>(visibleItems.size()) + columns - 1) / columns; const int renderedRowCount = (renderedItemCount + columns - 1) / columns;
ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing); 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()) { if (visibleItems.empty() && !searchQuery.Empty()) {
@@ -449,21 +605,7 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
} }
if (UI::BeginContextMenuForWindow("##ProjectBrowserContext")) { if (UI::BeginContextMenuForWindow("##ProjectBrowserContext")) {
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() { DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr));
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();
});
});
}
UI::EndContextMenu(); UI::EndContextMenu();
} }
@@ -570,21 +712,7 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem
Actions::BeginProjectAssetDrag(item, iconKind); Actions::BeginProjectAssetDrag(item, iconKind);
if (UI::BeginContextMenuForLastItem("##ProjectItemContext")) { if (UI::BeginContextMenuForLastItem("##ProjectItemContext")) {
Actions::DrawMenuAction(Actions::MakeOpenAssetAction(Commands::CanOpenAsset(item)), [&]() { DrawProjectContextMenu(m_context->GetProjectManager(), BuildContextMenuTarget(m_context->GetProjectManager(), 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);
});
});
UI::EndContextMenu(); UI::EndContextMenu();
} }

View File

@@ -41,12 +41,21 @@ private:
bool openRequested = false; bool openRequested = false;
}; };
struct ContextMenuTarget {
AssetItemPtr item;
std::string subjectPath;
std::string createFolderPath;
bool showInExplorerSelect = false;
};
void BeginAssetDragDropFrame(); void BeginAssetDragDropFrame();
void RegisterFolderDropTarget(IProjectManager& manager, const AssetItemPtr& folder); void RegisterFolderDropTarget(IProjectManager& manager, const AssetItemPtr& folder);
void FinalizeAssetDragDrop(IProjectManager& manager); void FinalizeAssetDragDrop(IProjectManager& manager);
void BeginRename(const AssetItemPtr& item); void BeginRename(const AssetItemPtr& item);
bool CommitRename(IProjectManager& manager); bool CommitRename(IProjectManager& manager);
void CancelRename(); void CancelRename();
ContextMenuTarget BuildContextMenuTarget(IProjectManager& manager, const AssetItemPtr& item) const;
void DrawProjectContextMenu(IProjectManager& manager, const ContextMenuTarget& target);
void RenderToolbar(); void RenderToolbar();
void RenderFolderTreePane(IProjectManager& manager); void RenderFolderTreePane(IProjectManager& manager);
void RenderFolderTreeNode(IProjectManager& manager, const AssetItemPtr& folder, const std::string& currentFolderPath); void RenderFolderTreeNode(IProjectManager& manager, const AssetItemPtr& folder, const std::string& currentFolderPath);

View File

@@ -3,8 +3,13 @@
#include "Core/ISceneManager.h" #include "Core/ISceneManager.h"
#include "Core/ISelectionManager.h" #include "Core/ISelectionManager.h"
#include "SceneViewPanel.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/SceneViewportOrientationGizmo.h"
#include "Viewport/SceneViewportOverlayRenderer.h" #include "Viewport/SceneViewportOverlayRenderer.h"
#include "Viewport/SceneViewportTransformGizmoFrameBuilder.h"
#include "ViewportPanelContent.h" #include "ViewportPanelContent.h"
#include "Platform/Win32Utf8.h" #include "Platform/Win32Utf8.h"
#include "UI/UI.h" #include "UI/UI.h"
@@ -13,9 +18,11 @@
#include <imgui.h> #include <imgui.h>
#include <algorithm>
#include <cstdarg> #include <cstdarg>
#include <cstdio> #include <cstdio>
#include <filesystem> #include <filesystem>
#include <vector>
namespace XCEngine { namespace XCEngine {
namespace Editor { namespace Editor {
@@ -28,13 +35,209 @@ struct SceneViewportToolOverlayResult {
SceneViewportToolMode clickedTool = SceneViewportToolMode::Move; SceneViewportToolMode clickedTool = SceneViewportToolMode::Move;
}; };
enum class SceneViewportActiveGizmoKind : uint8_t { enum class SceneViewportInteractionKind : uint8_t {
None = 0, None = 0,
Move, MoveGizmo,
Rotate, RotateGizmo,
Scale 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) { const char* GetSceneViewportToolTooltip(SceneViewportToolMode toolMode) {
switch (toolMode) { switch (toolMode) {
case SceneViewportToolMode::ViewMove: 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) { const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode, bool active) {
static std::string cachedPaths[5][2] = {}; static std::string cachedPaths[5][2] = {};
const size_t toolIndex = static_cast<size_t>(toolMode); const size_t toolIndex = static_cast<size_t>(toolMode);
@@ -78,16 +294,7 @@ const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode,
return cachedPath; return cachedPath;
} }
const std::filesystem::path exeDir( cachedPath = BuildSceneViewportIconPath(GetSceneViewportToolIconBaseName(toolMode), active);
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());
return cachedPath; return cachedPath;
} }
@@ -205,72 +412,6 @@ bool ShouldBeginSceneViewportNavigationDrag(
ImGui::IsMouseClicked(button); 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 } // namespace
SceneViewPanel::SceneViewPanel() : Panel("Scene") {} SceneViewPanel::SceneViewPanel() : Panel("Scene") {}
@@ -283,6 +424,7 @@ void SceneViewPanel::Render() {
return; return;
} }
RenderSceneViewportTopBar(m_pivotMode, m_transformSpaceMode);
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene); const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene);
if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) { if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) {
const ImGuiIO& io = ImGui::GetIO(); const ImGuiIO& io = ImGui::GetIO();
@@ -304,8 +446,20 @@ void SceneViewPanel::Render() {
m_toolMode = toolOverlay.clickedTool; m_toolMode = toolOverlay.clickedTool;
} }
if (content.focused && !io.WantTextInput && !m_lookDragging && !m_panDragging) { const bool allowToolShortcut = !io.WantTextInput && !m_lookDragging && !m_panDragging;
if (ImGui::IsKeyPressed(ImGuiKey_W, false)) { 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()) { if (m_rotateGizmo.IsActive()) {
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
} }
@@ -337,103 +491,68 @@ void SceneViewPanel::Render() {
const bool showingMoveGizmo = m_toolMode == SceneViewportToolMode::Move || usingTransformTool; const bool showingMoveGizmo = m_toolMode == SceneViewportToolMode::Move || usingTransformTool;
const bool showingRotateGizmo = m_toolMode == SceneViewportToolMode::Rotate || usingTransformTool; const bool showingRotateGizmo = m_toolMode == SceneViewportToolMode::Rotate || usingTransformTool;
const bool showingScaleGizmo = m_toolMode == SceneViewportToolMode::Scale || 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 = {}; SceneViewportOverlayData overlay = {};
SceneViewportMoveGizmoContext moveGizmoContext = {}; SceneViewportTransformGizmoFrameState gizmoFrameState = {};
SceneViewportRotateGizmoContext rotateGizmoContext = {}; SceneViewportOverlayFrameData emptySceneOverlayFrameData = {};
SceneViewportScaleGizmoContext scaleGizmoContext = {};
SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None; SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None;
if (hasInteractiveViewport) { if (hasInteractiveViewport) {
overlay = viewportHostService->GetSceneViewOverlayData(); overlay = viewportHostService->GetSceneViewOverlayData();
if (showingMoveGizmo) { gizmoFrameState = RefreshSceneViewportTransformGizmos(
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos); *m_context,
if (m_moveGizmo.IsActive() && overlay,
(moveGizmoContext.selectedObject == nullptr || viewportSize,
m_context->GetSelectionManager().GetSelectedEntity() != m_moveGizmo.GetActiveEntityId())) { localMousePosition,
m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); useCenterPivot,
} localSpace,
} else if (m_moveGizmo.IsActive()) { usingTransformTool,
m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); showingMoveGizmo,
} m_moveGizmo,
showingRotateGizmo,
if (showingRotateGizmo) { m_rotateGizmo,
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos); showingScaleGizmo,
if (m_rotateGizmo.IsActive() && m_scaleGizmo);
(rotateGizmoContext.selectedObject == nullptr || activeGizmoKind = gizmoFrameState.activeGizmoKind;
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);
}
} else { } else {
if (m_moveGizmo.IsActive()) { CancelSceneViewportTransformGizmoDrags(*m_context, m_moveGizmo, m_rotateGizmo, m_scaleGizmo);
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());
}
} }
const bool moveGizmoHovering = showingMoveGizmo && m_moveGizmo.IsHoveringHandle(); const SceneViewportTransformGizmoHandleBuildInputs interactionGizmoInputs =
const bool rotateGizmoHovering = showingRotateGizmo && m_rotateGizmo.IsHoveringHandle(); hasInteractiveViewport
const bool scaleGizmoHovering = showingScaleGizmo && m_scaleGizmo.IsHoveringHandle(); ? 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 moveGizmoActive = showingMoveGizmo && m_moveGizmo.IsActive();
const bool rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive(); const bool rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive();
const bool scaleGizmoActive = showingScaleGizmo && m_scaleGizmo.IsActive(); const bool scaleGizmoActive = showingScaleGizmo && m_scaleGizmo.IsActive();
const SceneViewportActiveGizmoKind hoveredGizmoKind = scaleGizmoHovering
? SceneViewportActiveGizmoKind::Scale
: (moveGizmoHovering ? SceneViewportActiveGizmoKind::Move
: (rotateGizmoHovering ? SceneViewportActiveGizmoKind::Rotate
: SceneViewportActiveGizmoKind::None));
if (moveGizmoActive) { if (moveGizmoActive) {
activeGizmoKind = SceneViewportActiveGizmoKind::Move; activeGizmoKind = SceneViewportActiveGizmoKind::Move;
} else if (rotateGizmoActive) { } else if (rotateGizmoActive) {
@@ -443,44 +562,83 @@ void SceneViewPanel::Render() {
} else { } else {
activeGizmoKind = SceneViewportActiveGizmoKind::None; activeGizmoKind = SceneViewportActiveGizmoKind::None;
} }
const bool gizmoHovering = hoveredGizmoKind != SceneViewportActiveGizmoKind::None;
const bool gizmoActive = activeGizmoKind != SceneViewportActiveGizmoKind::None; const bool gizmoActive = activeGizmoKind != SceneViewportActiveGizmoKind::None;
SceneViewportInteractionCandidate hoveredInteraction = {};
const bool beginTransformGizmo = const bool canResolveViewportInteraction =
hasInteractiveViewport &&
content.clickedLeft &&
!m_lookDragging &&
!m_panDragging &&
!toolOverlay.hovered &&
gizmoHovering;
const SceneViewportOrientationAxis orientationAxisHit =
hasInteractiveViewport && hasInteractiveViewport &&
viewportContentHovered && viewportContentHovered &&
!usingViewMoveTool && !usingViewMoveTool &&
!m_lookDragging && !m_lookDragging &&
!m_panDragging && !m_panDragging &&
!gizmoHovering && !toolOverlay.hovered &&
!gizmoActive !gizmoActive;
? HitTestSceneViewportOrientationGizmo( if (canResolveViewportInteraction) {
overlay, AccumulateSceneViewportInteractionCandidate(
content.itemMin, BuildOverlayHandleInteractionCandidate(overlayHandleHit),
content.itemMax, hoveredInteraction);
io.MousePos)
: SceneViewportOrientationAxis::None; 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 = const bool orientationGizmoClick =
hasInteractiveViewport && hasInteractiveViewport &&
content.clickedLeft && content.clickedLeft &&
orientationAxisHit != SceneViewportOrientationAxis::None; orientationAxisHit != SceneViewportOrientationAxis::None;
const bool sceneIconClick =
hasInteractiveViewport &&
content.clickedLeft &&
clickedSceneIconEntity != 0;
const bool selectClick = const bool selectClick =
hasInteractiveViewport && hasInteractiveViewport &&
content.clickedLeft && content.clickedLeft &&
viewportContentHovered && canResolveViewportInteraction &&
!usingViewMoveTool && !hoveredInteraction.HasHit();
!m_lookDragging &&
!m_panDragging &&
!orientationGizmoClick &&
!gizmoHovering &&
!gizmoActive;
const bool beginLeftPanDrag = usingViewMoveTool const bool beginLeftPanDrag = usingViewMoveTool
? ShouldBeginSceneViewportNavigationDrag( ? ShouldBeginSceneViewportNavigationDrag(
hasInteractiveViewport, hasInteractiveViewport,
@@ -525,47 +683,32 @@ void SceneViewPanel::Render() {
io.MouseDelta.y); io.MouseDelta.y);
} }
if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || selectClick || beginLookDrag || if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || sceneIconClick || selectClick || beginLookDrag ||
beginPanDrag) { beginPanDrag) {
ImGui::SetWindowFocus(); ImGui::SetWindowFocus();
} }
if (beginTransformGizmo) { if (beginTransformGizmo) {
if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) { if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) {
m_scaleGizmo.TryBeginDrag(scaleGizmoContext, m_context->GetUndoManager()); m_scaleGizmo.TryBeginDrag(gizmoFrameState.scaleContext, m_context->GetUndoManager());
} else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) { } 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) { } else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) {
m_rotateGizmo.TryBeginDrag(rotateGizmoContext, m_context->GetUndoManager()); m_rotateGizmo.TryBeginDrag(gizmoFrameState.rotateContext, m_context->GetUndoManager());
} }
} }
if (orientationGizmoClick) { if (orientationGizmoClick) {
viewportHostService->AlignSceneViewToOrientationAxis(orientationAxisHit); 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) { if (sceneIconClick) {
const ImVec2 localMousePosition( m_context->GetSelectionManager().SetSelectedEntity(clickedSceneIconEntity);
io.MousePos.x - content.itemMin.x, } else if (selectClick) {
io.MousePos.y - content.itemMin.y);
const uint64_t selectedEntity = viewportHostService->PickSceneViewEntity( const uint64_t selectedEntity = viewportHostService->PickSceneViewEntity(
*m_context, *m_context,
content.availableSize, content.availableSize,
localMousePosition); ImVec2(localMousePosition.x, localMousePosition.y));
if (selectedEntity != 0) { if (selectedEntity != 0) {
m_context->GetSelectionManager().SetSelectedEntity(selectedEntity); m_context->GetSelectionManager().SetSelectedEntity(selectedEntity);
} else { } else {
@@ -576,11 +719,11 @@ void SceneViewPanel::Render() {
if (gizmoActive) { if (gizmoActive) {
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) { if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) {
m_moveGizmo.UpdateDrag(moveGizmoContext); m_moveGizmo.UpdateDrag(gizmoFrameState.moveContext);
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) { } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) {
m_rotateGizmo.UpdateDrag(rotateGizmoContext); m_rotateGizmo.UpdateDrag(gizmoFrameState.rotateContext);
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) { } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) {
m_scaleGizmo.UpdateDrag(scaleGizmoContext); m_scaleGizmo.UpdateDrag(gizmoFrameState.scaleContext);
} }
} else { } else {
if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) { if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) {
@@ -707,52 +850,41 @@ void SceneViewPanel::Render() {
if (content.hasViewportArea && content.frame.hasTexture) { if (content.hasViewportArea && content.frame.hasTexture) {
overlay = viewportHostService->GetSceneViewOverlayData(); overlay = viewportHostService->GetSceneViewOverlayData();
SceneViewportActiveGizmoKind drawActiveGizmoKind = SceneViewportActiveGizmoKind::None; const SceneViewportTransformGizmoFrameState drawGizmoFrameState =
if (m_moveGizmo.IsActive()) { RefreshSceneViewportTransformGizmos(
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Move; *m_context,
} else if (m_rotateGizmo.IsActive()) { overlay,
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Rotate; viewportSize,
} else if (m_scaleGizmo.IsActive()) { localMousePosition,
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Scale; useCenterPivot,
} localSpace,
if (showingMoveGizmo) { usingTransformTool,
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos); showingMoveGizmo,
SceneViewportMoveGizmoContext updateContext = moveGizmoContext; m_moveGizmo,
if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None && showingRotateGizmo,
drawActiveGizmoKind != SceneViewportActiveGizmoKind::Move) { m_rotateGizmo,
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); showingScaleGizmo,
} m_scaleGizmo);
m_moveGizmo.Update(updateContext);
} viewportHostService->SetSceneViewTransientTransformGizmoOverlayData(
if (showingRotateGizmo) { overlay,
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos); BuildSceneViewportTransformGizmoHandleBuildInputs(
SceneViewportRotateGizmoContext updateContext = rotateGizmoContext; showingMoveGizmo,
if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None && m_moveGizmo,
drawActiveGizmoKind != SceneViewportActiveGizmoKind::Rotate) { drawGizmoFrameState.moveContext,
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); showingRotateGizmo,
} m_rotateGizmo,
m_rotateGizmo.Update(updateContext); drawGizmoFrameState.rotateContext,
} showingScaleGizmo,
if (showingScaleGizmo) { m_scaleGizmo,
scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos); drawGizmoFrameState.scaleContext));
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);
}
DrawSceneViewportOverlay( DrawSceneViewportOverlay(
ImGui::GetWindowDrawList(), ImGui::GetWindowDrawList(),
overlay, overlay,
content.itemMin, content.itemMin,
content.itemMax, content.itemMax,
content.availableSize, content.availableSize);
showingMoveGizmo ? &m_moveGizmo.GetDrawData() : nullptr,
showingRotateGizmo ? &m_rotateGizmo.GetDrawData() : nullptr,
showingScaleGizmo ? &m_scaleGizmo.GetDrawData() : nullptr);
} }
} }

View File

@@ -18,6 +18,16 @@ enum class SceneViewportToolMode : uint8_t {
Transform Transform
}; };
enum class SceneViewportPivotMode : uint8_t {
Pivot = 0,
Center
};
enum class SceneViewportTransformSpaceMode : uint8_t {
Global = 0,
Local
};
class SceneViewPanel : public Panel { class SceneViewPanel : public Panel {
public: public:
SceneViewPanel(); SceneViewPanel();
@@ -25,6 +35,8 @@ public:
private: private:
SceneViewportToolMode m_toolMode = SceneViewportToolMode::Move; SceneViewportToolMode m_toolMode = SceneViewportToolMode::Move;
SceneViewportPivotMode m_pivotMode = SceneViewportPivotMode::Pivot;
SceneViewportTransformSpaceMode m_transformSpaceMode = SceneViewportTransformSpaceMode::Global;
bool m_lookDragging = false; bool m_lookDragging = false;
bool m_panDragging = false; bool m_panDragging = false;
int m_panDragButton = ImGuiMouseButton_Middle; int m_panDragButton = ImGuiMouseButton_Middle;

View File

@@ -59,6 +59,7 @@ inline void RenderViewportInteractionSurface(
ViewportPanelContentResult& result, ViewportPanelContentResult& result,
EditorViewportKind kind, EditorViewportKind kind,
const ImVec2& interactionSize) { const ImVec2& interactionSize) {
ImGui::SetNextItemAllowOverlap();
ImGui::InvisibleButton( ImGui::InvisibleButton(
GetViewportInteractionSurfaceId(kind), GetViewportInteractionSurfaceId(kind),
interactionSize, interactionSize,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -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/AssetRef.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ArtifactFormats.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/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/ResourceTypes.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ImportSettings.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ImportSettings.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ResourceHandle.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}/include/XCEngine/Core/Asset/ResourceDependencyGraph.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetGUID.cpp ${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/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/ResourceManager.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ResourceCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ResourceCache.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AsyncLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AsyncLoader.cpp

View File

@@ -3,6 +3,7 @@
#include <XCEngine/Components/Component.h> #include <XCEngine/Components/Component.h>
#include <XCEngine/Core/Asset/AssetRef.h> #include <XCEngine/Core/Asset/AssetRef.h>
#include <XCEngine/Core/Asset/ResourceHandle.h> #include <XCEngine/Core/Asset/ResourceHandle.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Resources/Mesh/Mesh.h> #include <XCEngine/Resources/Mesh/Mesh.h>
#include <memory> #include <memory>
@@ -32,12 +33,14 @@ private:
struct PendingMeshLoadState; struct PendingMeshLoadState;
void BeginAsyncMeshLoad(const std::string& meshPath); void BeginAsyncMeshLoad(const std::string& meshPath);
void EnsureDeferredAsyncMeshLoadStarted();
void ResolvePendingMesh(); void ResolvePendingMesh();
Resources::ResourceHandle<Resources::Mesh> m_mesh; Resources::ResourceHandle<Resources::Mesh> m_mesh;
std::string m_meshPath; std::string m_meshPath;
Resources::AssetRef m_meshRef; Resources::AssetRef m_meshRef;
std::shared_ptr<PendingMeshLoadState> m_pendingMeshLoad; std::shared_ptr<PendingMeshLoadState> m_pendingMeshLoad;
bool m_asyncMeshLoadRequested = false;
}; };
} // namespace Components } // namespace Components

View File

@@ -46,6 +46,7 @@ private:
struct PendingMaterialLoadState; struct PendingMaterialLoadState;
void BeginAsyncMaterialLoad(size_t index, const std::string& materialPath); void BeginAsyncMaterialLoad(size_t index, const std::string& materialPath);
void EnsureDeferredAsyncMaterialLoadStarted(size_t index);
void ResolvePendingMaterials(); void ResolvePendingMaterials();
void EnsureMaterialSlot(size_t index); void EnsureMaterialSlot(size_t index);
static std::string MaterialPathFromHandle(const Resources::ResourceHandle<Resources::Material>& material); static std::string MaterialPathFromHandle(const Resources::ResourceHandle<Resources::Material>& material);
@@ -54,6 +55,7 @@ private:
std::vector<std::string> m_materialPaths; std::vector<std::string> m_materialPaths;
std::vector<Resources::AssetRef> m_materialRefs; std::vector<Resources::AssetRef> m_materialRefs;
std::vector<std::shared_ptr<PendingMaterialLoadState>> m_pendingMaterialLoads; std::vector<std::shared_ptr<PendingMaterialLoadState>> m_pendingMaterialLoads;
std::vector<bool> m_asyncMaterialLoadRequested;
bool m_castShadows = true; bool m_castShadows = true;
bool m_receiveShadows = true; bool m_receiveShadows = true;
uint32_t m_renderLayer = 0; uint32_t m_renderLayer = 0;

View File

@@ -6,12 +6,23 @@
#include <filesystem> #include <filesystem>
#include <unordered_map> #include <unordered_map>
#include <vector>
namespace XCEngine { namespace XCEngine {
namespace Resources { namespace Resources {
class Mesh;
class Material;
class AssetDatabase { class AssetDatabase {
public: public:
struct ArtifactDependencyRecord {
Containers::String path;
Containers::String hash;
Core::uint64 fileSize = 0;
Core::uint64 writeTime = 0;
};
struct SourceAssetRecord { struct SourceAssetRecord {
AssetGUID guid; AssetGUID guid;
Containers::String relativePath; Containers::String relativePath;
@@ -39,6 +50,7 @@ public:
Core::uint64 sourceFileSize = 0; Core::uint64 sourceFileSize = 0;
Core::uint64 sourceWriteTime = 0; Core::uint64 sourceWriteTime = 0;
LocalID mainLocalID = kMainAssetLocalID; LocalID mainLocalID = kMainAssetLocalID;
std::vector<ArtifactDependencyRecord> dependencies;
}; };
struct ResolvedAsset { struct ResolvedAsset {
@@ -66,13 +78,15 @@ public:
ResourceType requestedType, ResourceType requestedType,
ResolvedAsset& outAsset); ResolvedAsset& outAsset);
bool TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) 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;
const Containers::String& GetProjectRoot() const { return m_projectRoot; } const Containers::String& GetProjectRoot() const { return m_projectRoot; }
const Containers::String& GetAssetsRoot() const { return m_assetsRoot; } const Containers::String& GetAssetsRoot() const { return m_assetsRoot; }
const Containers::String& GetLibraryRoot() const { return m_libraryRoot; } const Containers::String& GetLibraryRoot() const { return m_libraryRoot; }
private: private:
static constexpr Core::uint32 kCurrentImporterVersion = 2; static constexpr Core::uint32 kCurrentImporterVersion = 3;
void EnsureProjectLayout(); void EnsureProjectLayout();
void LoadSourceAssetDB(); void LoadSourceAssetDB();
@@ -110,12 +124,24 @@ private:
bool ImportModelAsset(const SourceAssetRecord& sourceRecord, bool ImportModelAsset(const SourceAssetRecord& sourceRecord,
ArtifactRecord& outRecord); 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; Containers::String BuildArtifactDirectory(const Containers::String& artifactKey) const;
static Containers::String ReadWholeFileText(const std::filesystem::path& path); static Containers::String ReadWholeFileText(const std::filesystem::path& path);
static Containers::String ComputeFileHash(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 GetFileSizeValue(const std::filesystem::path& path);
static Core::uint64 GetFileWriteTimeValue(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_projectRoot;
Containers::String m_assetsRoot; Containers::String m_assetsRoot;

View 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

View 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

View File

@@ -1,7 +1,8 @@
#pragma once #pragma once
#include <XCEngine/Core/IO/IResourceLoader.h> #include <XCEngine/Core/IO/IResourceLoader.h>
#include "AssetDatabase.h" #include "AssetImportService.h"
#include "ProjectAssetIndex.h"
#include "ResourceCache.h" #include "ResourceCache.h"
#include "AsyncLoader.h" #include "AsyncLoader.h"
#include "ResourceHandle.h" #include "ResourceHandle.h"
@@ -165,12 +166,12 @@ private:
size_t m_memoryUsage = 0; size_t m_memoryUsage = 0;
size_t m_memoryBudget = 512 * 1024 * 1024; size_t m_memoryBudget = 512 * 1024 * 1024;
AssetDatabase m_assetDatabase; mutable AssetImportService m_assetImportService;
mutable ProjectAssetIndex m_projectAssetIndex;
ResourceCache m_cache; ResourceCache m_cache;
Core::UniqueRef<AsyncLoader> m_asyncLoader; Core::UniqueRef<AsyncLoader> m_asyncLoader;
Threading::Mutex m_mutex; Threading::Mutex m_mutex;
std::mutex m_initializeMutex; std::mutex m_initializeMutex;
mutable std::recursive_mutex m_ioMutex;
std::mutex m_inFlightLoadsMutex; std::mutex m_inFlightLoadsMutex;
std::unordered_map<InFlightLoadKey, std::shared_ptr<InFlightLoadState>, InFlightLoadKeyHasher> m_inFlightLoads; std::unordered_map<InFlightLoadKey, std::shared_ptr<InFlightLoadState>, InFlightLoadKeyHasher> m_inFlightLoads;
std::atomic<Core::uint32> m_deferredSceneLoadDepth{0}; std::atomic<Core::uint32> m_deferredSceneLoadDepth{0};

View File

@@ -21,6 +21,7 @@ public:
bool IsKeyDown(KeyCode key) const; bool IsKeyDown(KeyCode key) const;
bool IsKeyUp(KeyCode key) const; bool IsKeyUp(KeyCode key) const;
bool IsKeyPressed(KeyCode key) const; bool IsKeyPressed(KeyCode key) const;
bool IsKeyReleased(KeyCode key) const;
Math::Vector2 GetMousePosition() const; Math::Vector2 GetMousePosition() const;
Math::Vector2 GetMouseDelta() const; Math::Vector2 GetMouseDelta() const;
@@ -28,6 +29,7 @@ public:
bool IsMouseButtonDown(MouseButton button) const; bool IsMouseButtonDown(MouseButton button) const;
bool IsMouseButtonUp(MouseButton button) const; bool IsMouseButtonUp(MouseButton button) const;
bool IsMouseButtonClicked(MouseButton button) const; bool IsMouseButtonClicked(MouseButton button) const;
bool IsMouseButtonReleased(MouseButton button) const;
int GetTouchCount() const; int GetTouchCount() const;
TouchState GetTouch(int index) const; TouchState GetTouch(int index) const;
@@ -38,6 +40,8 @@ public:
bool GetButton(const Containers::String& buttonName) const; bool GetButton(const Containers::String& buttonName) const;
bool GetButtonDown(const Containers::String& buttonName) const; bool GetButtonDown(const Containers::String& buttonName) const;
bool GetButtonUp(const Containers::String& buttonName) const; bool GetButtonUp(const Containers::String& buttonName) const;
bool IsAnyKeyDown() const;
bool IsAnyKeyPressed() const;
void RegisterAxis(const InputAxis& axis); void RegisterAxis(const InputAxis& axis);
void RegisterButton(const Containers::String& name, KeyCode key); void RegisterButton(const Containers::String& name, KeyCode key);
@@ -68,6 +72,7 @@ private:
std::vector<bool> m_keyDownThisFrame; std::vector<bool> m_keyDownThisFrame;
std::vector<bool> m_keyDownLastFrame; std::vector<bool> m_keyDownLastFrame;
std::vector<bool> m_keyUpThisFrame;
std::vector<bool> m_keyDown; std::vector<bool> m_keyDown;
Math::Vector2 m_mousePosition; Math::Vector2 m_mousePosition;
@@ -75,6 +80,7 @@ private:
float m_mouseScrollDelta = 0.0f; float m_mouseScrollDelta = 0.0f;
std::vector<bool> m_mouseButtonDownThisFrame; std::vector<bool> m_mouseButtonDownThisFrame;
std::vector<bool> m_mouseButtonDownLastFrame; std::vector<bool> m_mouseButtonDownLastFrame;
std::vector<bool> m_mouseButtonUpThisFrame;
std::vector<bool> m_mouseButtonDown; std::vector<bool> m_mouseButtonDown;
std::vector<TouchState> m_touches; std::vector<TouchState> m_touches;

View File

@@ -57,6 +57,7 @@ struct CameraRenderRequest {
Math::Color clearColorOverride = Math::Color::Black(); Math::Color clearColorOverride = Math::Color::Black();
RenderPassSequence* preScenePasses = nullptr; RenderPassSequence* preScenePasses = nullptr;
RenderPassSequence* postScenePasses = nullptr; RenderPassSequence* postScenePasses = nullptr;
RenderPassSequence* overlayPasses = nullptr;
bool IsValid() const { bool IsValid() const {
return scene != nullptr && return scene != nullptr &&

View File

@@ -28,6 +28,26 @@ enum class ScriptLifecycleMethod {
OnDestroy 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 { struct ScriptRuntimeContext {
Components::Scene* scene = nullptr; Components::Scene* scene = nullptr;
Components::GameObject* gameObject = nullptr; Components::GameObject* gameObject = nullptr;
@@ -43,6 +63,9 @@ public:
virtual void OnRuntimeStart(Components::Scene* scene) = 0; virtual void OnRuntimeStart(Components::Scene* scene) = 0;
virtual void OnRuntimeStop(Components::Scene* scene) = 0; virtual void OnRuntimeStop(Components::Scene* scene) = 0;
virtual bool TryGetAvailableScriptClasses(
std::vector<ScriptClassDescriptor>& outClasses) const = 0;
virtual bool TryGetClassFieldMetadata( virtual bool TryGetClassFieldMetadata(
const std::string& assemblyName, const std::string& assemblyName,
const std::string& namespaceName, const std::string& namespaceName,

View File

@@ -50,6 +50,8 @@ public:
const std::string& namespaceName, const std::string& namespaceName,
const std::string& className) const; const std::string& className) const;
std::vector<std::string> GetScriptClassNames(const std::string& assemblyName = std::string()) const; std::vector<std::string> GetScriptClassNames(const std::string& assemblyName = std::string()) const;
bool TryGetAvailableScriptClasses(
std::vector<ScriptClassDescriptor>& outClasses) const override;
bool TryGetClassFieldMetadata( bool TryGetClassFieldMetadata(
const std::string& assemblyName, const std::string& assemblyName,
const std::string& namespaceName, const std::string& namespaceName,

View File

@@ -9,6 +9,8 @@ class NullScriptRuntime : public IScriptRuntime {
public: public:
void OnRuntimeStart(Components::Scene* scene) override; void OnRuntimeStart(Components::Scene* scene) override;
void OnRuntimeStop(Components::Scene* scene) override; void OnRuntimeStop(Components::Scene* scene) override;
bool TryGetAvailableScriptClasses(
std::vector<ScriptClassDescriptor>& outClasses) const override;
bool TryGetClassFieldMetadata( bool TryGetClassFieldMetadata(
const std::string& assemblyName, const std::string& assemblyName,
const std::string& namespaceName, const std::string& namespaceName,

View File

@@ -28,6 +28,7 @@ public:
void SetScriptClass(const std::string& namespaceName, const std::string& className); 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 SetScriptClass(const std::string& assemblyName, const std::string& namespaceName, const std::string& className);
void ClearScriptClass();
bool HasScriptClass() const { return !m_className.empty(); } bool HasScriptClass() const { return !m_className.empty(); }
std::string GetFullClassName() const; std::string GetFullClassName() const;

View File

@@ -19,9 +19,12 @@ class ScriptComponent;
class ScriptEngine { class ScriptEngine {
public: public:
static ScriptEngine& Get(); static ScriptEngine& Get();
static constexpr float DefaultFixedDeltaTime = 1.0f / 50.0f;
void SetRuntime(IScriptRuntime* runtime); void SetRuntime(IScriptRuntime* runtime);
IScriptRuntime* GetRuntime() const { return m_runtime; } IScriptRuntime* GetRuntime() const { return m_runtime; }
void SetRuntimeFixedDeltaTime(float fixedDeltaTime);
float GetRuntimeFixedDeltaTime() const { return m_runtimeFixedDeltaTime; }
void OnRuntimeStart(Components::Scene* scene); void OnRuntimeStart(Components::Scene* scene);
void OnRuntimeStop(); void OnRuntimeStop();
@@ -33,6 +36,7 @@ public:
void OnScriptComponentEnabled(ScriptComponent* component); void OnScriptComponentEnabled(ScriptComponent* component);
void OnScriptComponentDisabled(ScriptComponent* component); void OnScriptComponentDisabled(ScriptComponent* component);
void OnScriptComponentDestroyed(ScriptComponent* component); void OnScriptComponentDestroyed(ScriptComponent* component);
void OnScriptComponentClassChanged(ScriptComponent* component);
bool IsRuntimeRunning() const { return m_runtimeRunning; } bool IsRuntimeRunning() const { return m_runtimeRunning; }
Components::Scene* GetRuntimeScene() const { return m_runtimeScene; } Components::Scene* GetRuntimeScene() const { return m_runtimeScene; }
@@ -40,6 +44,9 @@ public:
bool HasTrackedScriptComponent(const ScriptComponent* component) const; bool HasTrackedScriptComponent(const ScriptComponent* component) const;
bool HasRuntimeInstance(const ScriptComponent* component) const; bool HasRuntimeInstance(const ScriptComponent* component) const;
size_t GetTrackedScriptCount() const { return m_scriptOrder.size(); } size_t GetTrackedScriptCount() const { return m_scriptOrder.size(); }
bool TryGetAvailableScriptClasses(
std::vector<ScriptClassDescriptor>& outClasses,
const std::string& assemblyName = std::string()) const;
bool TrySetScriptFieldValue( bool TrySetScriptFieldValue(
ScriptComponent* component, ScriptComponent* component,
const std::string& fieldName, const std::string& fieldName,
@@ -138,6 +145,7 @@ private:
IScriptRuntime* m_runtime = &m_nullRuntime; IScriptRuntime* m_runtime = &m_nullRuntime;
Components::Scene* m_runtimeScene = nullptr; Components::Scene* m_runtimeScene = nullptr;
bool m_runtimeRunning = false; bool m_runtimeRunning = false;
float m_runtimeFixedDeltaTime = DefaultFixedDeltaTime;
uint64_t m_runtimeSceneCreatedSubscription = 0; uint64_t m_runtimeSceneCreatedSubscription = 0;
std::unordered_map<ScriptInstanceKey, ScriptInstanceState, ScriptInstanceKeyHasher> m_scriptStates; std::unordered_map<ScriptInstanceKey, ScriptInstanceState, ScriptInstanceKeyHasher> m_scriptStates;

View File

@@ -20,6 +20,10 @@ bool ShouldTraceMeshPath(const std::string& path) {
path.find("backpack") != std::string::npos; 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) { std::string EncodeAssetRef(const Resources::AssetRef& assetRef) {
if (!assetRef.IsValid()) { if (!assetRef.IsValid()) {
return std::string(); return std::string();
@@ -67,17 +71,20 @@ struct MeshFilterComponent::PendingMeshLoadState {
}; };
Resources::Mesh* MeshFilterComponent::GetMesh() const { Resources::Mesh* MeshFilterComponent::GetMesh() const {
const_cast<MeshFilterComponent*>(this)->EnsureDeferredAsyncMeshLoadStarted();
const_cast<MeshFilterComponent*>(this)->ResolvePendingMesh(); const_cast<MeshFilterComponent*>(this)->ResolvePendingMesh();
return m_mesh.Get(); return m_mesh.Get();
} }
const Resources::ResourceHandle<Resources::Mesh>& MeshFilterComponent::GetMeshHandle() const { const Resources::ResourceHandle<Resources::Mesh>& MeshFilterComponent::GetMeshHandle() const {
const_cast<MeshFilterComponent*>(this)->EnsureDeferredAsyncMeshLoadStarted();
const_cast<MeshFilterComponent*>(this)->ResolvePendingMesh(); const_cast<MeshFilterComponent*>(this)->ResolvePendingMesh();
return m_mesh; return m_mesh;
} }
void MeshFilterComponent::SetMeshPath(const std::string& meshPath) { void MeshFilterComponent::SetMeshPath(const std::string& meshPath) {
m_pendingMeshLoad.reset(); m_pendingMeshLoad.reset();
m_asyncMeshLoadRequested = false;
m_meshPath = meshPath; m_meshPath = meshPath;
if (m_meshPath.empty()) { if (m_meshPath.empty()) {
m_mesh.Reset(); m_mesh.Reset();
@@ -101,6 +108,7 @@ void MeshFilterComponent::SetMeshPath(const std::string& meshPath) {
void MeshFilterComponent::SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh) { void MeshFilterComponent::SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh) {
m_pendingMeshLoad.reset(); m_pendingMeshLoad.reset();
m_asyncMeshLoadRequested = false;
m_mesh = mesh; m_mesh = mesh;
m_meshPath = mesh.Get() != nullptr ? ToStdString(mesh->GetPath()) : std::string(); m_meshPath = mesh.Get() != nullptr ? ToStdString(mesh->GetPath()) : std::string();
if (m_meshPath.empty()) { if (m_meshPath.empty()) {
@@ -116,18 +124,29 @@ void MeshFilterComponent::SetMesh(Resources::Mesh* mesh) {
void MeshFilterComponent::ClearMesh() { void MeshFilterComponent::ClearMesh() {
m_pendingMeshLoad.reset(); m_pendingMeshLoad.reset();
m_asyncMeshLoadRequested = false;
m_mesh.Reset(); m_mesh.Reset();
m_meshPath.clear(); m_meshPath.clear();
m_meshRef.Reset(); m_meshRef.Reset();
} }
void MeshFilterComponent::Serialize(std::ostream& os) const { void MeshFilterComponent::Serialize(std::ostream& os) const {
os << "mesh=" << m_meshPath << ";"; Resources::AssetRef meshRef = m_meshRef;
os << "meshRef=" << EncodeAssetRef(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) { void MeshFilterComponent::Deserialize(std::istream& is) {
m_pendingMeshLoad.reset(); m_pendingMeshLoad.reset();
m_asyncMeshLoadRequested = false;
m_mesh.Reset(); m_mesh.Reset();
m_meshPath.clear(); m_meshPath.clear();
m_meshRef.Reset(); m_meshRef.Reset();
@@ -148,7 +167,7 @@ void MeshFilterComponent::Deserialize(std::istream& is) {
const std::string key = token.substr(0, eqPos); const std::string key = token.substr(0, eqPos);
const std::string value = token.substr(eqPos + 1); const std::string value = token.substr(eqPos + 1);
if (key == "mesh") { if (key == "mesh" || key == "meshPath") {
pendingMeshPath = value; pendingMeshPath = value;
} else if (key == "meshRef") { } else if (key == "meshRef") {
TryDecodeAssetRef(value, pendingMeshRef); TryDecodeAssetRef(value, pendingMeshRef);
@@ -172,7 +191,6 @@ void MeshFilterComponent::Deserialize(std::istream& is) {
if (ShouldTraceMeshPath(m_meshPath)) { if (ShouldTraceMeshPath(m_meshPath)) {
TraceMeshFilter(*this, std::string("Resolved meshRef to path=") + m_meshPath); TraceMeshFilter(*this, std::string("Resolved meshRef to path=") + m_meshPath);
} }
BeginAsyncMeshLoad(m_meshPath);
return; 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)) { if (!Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, m_meshRef)) {
m_meshRef.Reset(); m_meshRef.Reset();
} }
BeginAsyncMeshLoad(m_meshPath);
return; return;
} }
@@ -202,10 +219,12 @@ void MeshFilterComponent::Deserialize(std::istream& is) {
void MeshFilterComponent::BeginAsyncMeshLoad(const std::string& meshPath) { void MeshFilterComponent::BeginAsyncMeshLoad(const std::string& meshPath) {
if (meshPath.empty()) { if (meshPath.empty()) {
m_pendingMeshLoad.reset(); m_pendingMeshLoad.reset();
m_asyncMeshLoadRequested = false;
m_mesh.Reset(); m_mesh.Reset();
return; return;
} }
m_asyncMeshLoadRequested = true;
m_mesh.Reset(); m_mesh.Reset();
m_pendingMeshLoad = std::make_shared<PendingMeshLoadState>(); m_pendingMeshLoad = std::make_shared<PendingMeshLoadState>();
if (ShouldTraceMeshPath(meshPath)) { if (ShouldTraceMeshPath(meshPath)) {
@@ -219,7 +238,15 @@ void MeshFilterComponent::BeginAsyncMeshLoad(const std::string& meshPath) {
state->result = std::move(result); state->result = std::move(result);
state->completed = true; state->completed = true;
} }
}); });
}
void MeshFilterComponent::EnsureDeferredAsyncMeshLoadStarted() {
if (m_asyncMeshLoadRequested || m_mesh.Get() != nullptr || m_meshPath.empty()) {
return;
}
BeginAsyncMeshLoad(m_meshPath);
} }
void MeshFilterComponent::ResolvePendingMesh() { void MeshFilterComponent::ResolvePendingMesh() {

View File

@@ -4,6 +4,7 @@
#include "Core/Asset/ResourceManager.h" #include "Core/Asset/ResourceManager.h"
#include "Debug/Logger.h" #include "Debug/Logger.h"
#include <algorithm>
#include <sstream> #include <sstream>
namespace XCEngine { namespace XCEngine {
@@ -21,6 +22,10 @@ bool ShouldTraceMaterialPath(const std::string& path) {
path.find("New Material.mat") != std::string::npos; 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) { std::string EncodeAssetRef(const Resources::AssetRef& assetRef) {
if (!assetRef.IsValid()) { if (!assetRef.IsValid()) {
return std::string(); return std::string();
@@ -145,11 +150,13 @@ struct MeshRendererComponent::PendingMaterialLoadState {
}; };
Resources::Material* MeshRendererComponent::GetMaterial(size_t index) const { Resources::Material* MeshRendererComponent::GetMaterial(size_t index) const {
const_cast<MeshRendererComponent*>(this)->EnsureDeferredAsyncMaterialLoadStarted(index);
const_cast<MeshRendererComponent*>(this)->ResolvePendingMaterials(); const_cast<MeshRendererComponent*>(this)->ResolvePendingMaterials();
return index < m_materials.size() ? m_materials[index].Get() : nullptr; return index < m_materials.size() ? m_materials[index].Get() : nullptr;
} }
const Resources::ResourceHandle<Resources::Material>& MeshRendererComponent::GetMaterialHandle(size_t index) const { const Resources::ResourceHandle<Resources::Material>& MeshRendererComponent::GetMaterialHandle(size_t index) const {
const_cast<MeshRendererComponent*>(this)->EnsureDeferredAsyncMaterialLoadStarted(index);
const_cast<MeshRendererComponent*>(this)->ResolvePendingMaterials(); const_cast<MeshRendererComponent*>(this)->ResolvePendingMaterials();
static const Resources::ResourceHandle<Resources::Material> kNullHandle; static const Resources::ResourceHandle<Resources::Material> kNullHandle;
return index < m_materials.size() ? m_materials[index] : 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) { void MeshRendererComponent::SetMaterialPath(size_t index, const std::string& materialPath) {
EnsureMaterialSlot(index); EnsureMaterialSlot(index);
m_pendingMaterialLoads[index].reset(); m_pendingMaterialLoads[index].reset();
m_asyncMaterialLoadRequested[index] = false;
m_materialPaths[index] = materialPath; m_materialPaths[index] = materialPath;
if (materialPath.empty()) { if (materialPath.empty()) {
m_materials[index].Reset(); 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) { void MeshRendererComponent::SetMaterial(size_t index, const Resources::ResourceHandle<Resources::Material>& material) {
EnsureMaterialSlot(index); EnsureMaterialSlot(index);
m_pendingMaterialLoads[index].reset(); m_pendingMaterialLoads[index].reset();
m_asyncMaterialLoadRequested[index] = false;
m_materials[index] = material; m_materials[index] = material;
m_materialPaths[index] = MaterialPathFromHandle(material); m_materialPaths[index] = MaterialPathFromHandle(material);
if (m_materialPaths[index].empty() || if (m_materialPaths[index].empty() ||
@@ -206,6 +215,8 @@ void MeshRendererComponent::SetMaterials(const std::vector<Resources::ResourceHa
m_materialRefs.resize(materials.size()); m_materialRefs.resize(materials.size());
m_pendingMaterialLoads.clear(); m_pendingMaterialLoads.clear();
m_pendingMaterialLoads.resize(materials.size()); m_pendingMaterialLoads.resize(materials.size());
m_asyncMaterialLoadRequested.clear();
m_asyncMaterialLoadRequested.resize(materials.size(), false);
for (size_t i = 0; i < materials.size(); ++i) { for (size_t i = 0; i < materials.size(); ++i) {
m_materialPaths[i] = MaterialPathFromHandle(materials[i]); m_materialPaths[i] = MaterialPathFromHandle(materials[i]);
if (m_materialPaths[i].empty() || if (m_materialPaths[i].empty() ||
@@ -220,23 +231,45 @@ void MeshRendererComponent::ClearMaterials() {
m_materialPaths.clear(); m_materialPaths.clear();
m_materialRefs.clear(); m_materialRefs.clear();
m_pendingMaterialLoads.clear(); m_pendingMaterialLoads.clear();
m_asyncMaterialLoadRequested.clear();
} }
void MeshRendererComponent::Serialize(std::ostream& os) const { void MeshRendererComponent::Serialize(std::ostream& os) const {
os << "materials="; const size_t slotCount = std::max(m_materialPaths.size(), m_materialRefs.size());
for (size_t i = 0; i < m_materialPaths.size(); ++i) { 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) { if (i > 0) {
os << "|"; os << "|";
} }
os << m_materialPaths[i]; os << fallbackPaths[i];
} }
os << ";"; os << ";";
os << "materialRefs="; os << "materialRefs=";
for (size_t i = 0; i < m_materialRefs.size(); ++i) { for (size_t i = 0; i < serializedRefs.size(); ++i) {
if (i > 0) { if (i > 0) {
os << "|"; os << "|";
} }
os << EncodeAssetRef(m_materialRefs[i]); os << EncodeAssetRef(serializedRefs[i]);
} }
os << ";"; os << ";";
os << "castShadows=" << (m_castShadows ? 1 : 0) << ";"; 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 key = token.substr(0, eqPos);
const std::string value = token.substr(eqPos + 1); const std::string value = token.substr(eqPos + 1);
if (key == "materials") { if (key == "materials" || key == "materialPaths") {
m_materialPaths = SplitMaterialPaths(value); m_materialPaths = SplitMaterialPaths(value);
m_materials.resize(m_materialPaths.size()); m_materials.resize(m_materialPaths.size());
m_materialRefs.resize(m_materialPaths.size()); m_materialRefs.resize(m_materialPaths.size());
m_pendingMaterialLoads.resize(m_materialPaths.size()); m_pendingMaterialLoads.resize(m_materialPaths.size());
m_asyncMaterialLoadRequested.resize(m_materialPaths.size(), false);
} else if (key == "materialRefs") { } else if (key == "materialRefs") {
pendingMaterialRefs = SplitMaterialRefs(value); pendingMaterialRefs = SplitMaterialRefs(value);
} else if (key == "castShadows") { } else if (key == "castShadows") {
@@ -286,6 +320,7 @@ void MeshRendererComponent::Deserialize(std::istream& is) {
m_materials.resize(pendingMaterialRefs.size()); m_materials.resize(pendingMaterialRefs.size());
m_materialRefs.resize(pendingMaterialRefs.size()); m_materialRefs.resize(pendingMaterialRefs.size());
m_pendingMaterialLoads.resize(pendingMaterialRefs.size()); m_pendingMaterialLoads.resize(pendingMaterialRefs.size());
m_asyncMaterialLoadRequested.resize(pendingMaterialRefs.size(), false);
} }
if (ShouldTraceAnyMaterialPath(m_materialPaths)) { if (ShouldTraceAnyMaterialPath(m_materialPaths)) {
@@ -310,7 +345,6 @@ void MeshRendererComponent::Deserialize(std::istream& is) {
std::string("Resolved materialRef slot=") + std::to_string(i) + std::string("Resolved materialRef slot=") + std::to_string(i) +
" path=" + m_materialPaths[i]); " path=" + m_materialPaths[i]);
} }
BeginAsyncMaterialLoad(i, m_materialPaths[i]);
restoredOrQueued = true; restoredOrQueued = true;
} }
} }
@@ -336,7 +370,6 @@ void MeshRendererComponent::Deserialize(std::istream& is) {
m_materialRefs[i])) { m_materialRefs[i])) {
m_materialRefs[i].Reset(); m_materialRefs[i].Reset();
} }
BeginAsyncMaterialLoad(i, m_materialPaths[i]);
} }
continue; continue;
} }
@@ -349,10 +382,12 @@ void MeshRendererComponent::BeginAsyncMaterialLoad(size_t index, const std::stri
EnsureMaterialSlot(index); EnsureMaterialSlot(index);
if (materialPath.empty()) { if (materialPath.empty()) {
m_pendingMaterialLoads[index].reset(); m_pendingMaterialLoads[index].reset();
m_asyncMaterialLoadRequested[index] = false;
m_materials[index].Reset(); m_materials[index].Reset();
return; return;
} }
m_asyncMaterialLoadRequested[index] = true;
m_materials[index].Reset(); m_materials[index].Reset();
m_pendingMaterialLoads[index] = std::make_shared<PendingMaterialLoadState>(); m_pendingMaterialLoads[index] = std::make_shared<PendingMaterialLoadState>();
if (ShouldTraceMaterialPath(materialPath)) { if (ShouldTraceMaterialPath(materialPath)) {
@@ -369,7 +404,22 @@ void MeshRendererComponent::BeginAsyncMaterialLoad(size_t index, const std::stri
state->result = std::move(result); state->result = std::move(result);
state->completed = true; 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() { void MeshRendererComponent::ResolvePendingMaterials() {
@@ -428,6 +478,9 @@ void MeshRendererComponent::EnsureMaterialSlot(size_t index) {
if (index >= m_pendingMaterialLoads.size()) { if (index >= m_pendingMaterialLoads.size()) {
m_pendingMaterialLoads.resize(index + 1); 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) { std::string MeshRendererComponent::MaterialPathFromHandle(const Resources::ResourceHandle<Resources::Material>& material) {

View File

@@ -592,6 +592,28 @@ bool AssetDatabase::TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::St
return true; 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() { void AssetDatabase::EnsureProjectLayout() {
std::error_code ec; std::error_code ec;
fs::create_directories(fs::path(m_assetsRoot.CStr()), ec); fs::create_directories(fs::path(m_assetsRoot.CStr()), ec);

View 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

View 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

View File

@@ -85,6 +85,7 @@ void ResourceManager::EnsureInitialized() {
RegisterBuiltinLoader(*this, g_meshLoader); RegisterBuiltinLoader(*this, g_meshLoader);
RegisterBuiltinLoader(*this, g_shaderLoader); RegisterBuiltinLoader(*this, g_shaderLoader);
RegisterBuiltinLoader(*this, g_textureLoader); RegisterBuiltinLoader(*this, g_textureLoader);
m_assetImportService.Initialize();
m_asyncLoader = std::move(asyncLoader); m_asyncLoader = std::move(asyncLoader);
} }
@@ -96,23 +97,24 @@ void ResourceManager::Shutdown() {
m_asyncLoader.reset(); m_asyncLoader.reset();
} }
std::lock_guard<std::recursive_mutex> lock(m_ioMutex); m_assetImportService.Shutdown();
m_assetDatabase.Shutdown();
ResourceFileSystem::Get().Shutdown(); ResourceFileSystem::Get().Shutdown();
m_projectAssetIndex.ResetProjectRoot();
std::lock_guard<std::mutex> inFlightLock(m_inFlightLoadsMutex); std::lock_guard<std::mutex> inFlightLock(m_inFlightLoadsMutex);
m_inFlightLoads.clear(); m_inFlightLoads.clear();
} }
void ResourceManager::SetResourceRoot(const Containers::String& rootPath) { void ResourceManager::SetResourceRoot(const Containers::String& rootPath) {
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
m_resourceRoot = rootPath; m_resourceRoot = rootPath;
if (!m_resourceRoot.Empty()) { if (!m_resourceRoot.Empty()) {
ResourceFileSystem::Get().Initialize(rootPath); ResourceFileSystem::Get().Initialize(rootPath);
m_assetDatabase.Initialize(rootPath); m_assetImportService.SetProjectRoot(rootPath);
m_projectAssetIndex.RefreshFrom(m_assetImportService);
} else { } else {
m_assetImportService.SetProjectRoot(Containers::String());
ResourceFileSystem::Get().Shutdown(); ResourceFileSystem::Get().Shutdown();
m_assetDatabase.Shutdown(); m_projectAssetIndex.ResetProjectRoot();
} }
} }
@@ -360,14 +362,14 @@ void ResourceManager::UnloadGroup(const Containers::Array<ResourceGUID>& guids)
void ResourceManager::RefreshAssetDatabase() { void ResourceManager::RefreshAssetDatabase() {
if (!m_resourceRoot.Empty()) { if (!m_resourceRoot.Empty()) {
std::lock_guard<std::recursive_mutex> lock(m_ioMutex); m_assetImportService.Refresh();
m_assetDatabase.Refresh(); m_projectAssetIndex.RefreshFrom(m_assetImportService);
} }
} }
bool ResourceManager::TryGetAssetRef(const Containers::String& path, ResourceType resourceType, AssetRef& outRef) const { 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_projectAssetIndex.TryGetAssetRef(m_assetImportService, path, resourceType, outRef);
const bool resolved = m_assetDatabase.TryGetAssetRef(path, resourceType, outRef);
if (ShouldTraceResourcePath(path)) { if (ShouldTraceResourcePath(path)) {
Debug::Logger::Get().Info( Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem, 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 { bool ResourceManager::TryResolveAssetPath(const AssetRef& assetRef, Containers::String& outPath) const {
if (!assetRef.IsValid()) { const bool resolved = m_projectAssetIndex.TryResolveAssetPath(m_assetImportService, assetRef, outPath);
return false;
}
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
const bool resolved = m_assetDatabase.TryGetPrimaryAssetPath(assetRef.assetGuid, outPath);
if (resolved && ShouldTraceResourcePath(outPath)) { if (resolved && ShouldTraceResourcePath(outPath)) {
Debug::Logger::Get().Info( Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem, Debug::LogCategory::FileSystem,
@@ -512,30 +510,27 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
} }
Containers::String loadPath = path; Containers::String loadPath = path;
{ AssetDatabase::ResolvedAsset resolvedAsset;
std::lock_guard<std::recursive_mutex> ioLock(m_ioMutex); if (!m_resourceRoot.Empty() &&
m_assetImportService.EnsureArtifact(path, type, resolvedAsset) &&
AssetDatabase::ResolvedAsset resolvedAsset; resolvedAsset.artifactReady) {
if (!m_resourceRoot.Empty() && m_projectAssetIndex.RememberResolvedPath(resolvedAsset.assetGuid, resolvedAsset.relativePath);
m_assetDatabase.EnsureArtifact(path, type, resolvedAsset) && loadPath = resolvedAsset.artifactMainPath;
resolvedAsset.artifactReady) { if (ShouldTraceResourcePath(path)) {
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)) {
Debug::Logger::Get().Info( Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem, Debug::LogCategory::FileSystem,
Containers::String("[ResourceManager] LoadResource direct path=") + Containers::String("[ResourceManager] LoadResource artifact path=") +
path + path +
" loadPath=" + " artifact=" +
loadPath); loadPath);
} }
} else if (ShouldTraceResourcePath(path)) {
Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem,
Containers::String("[ResourceManager] LoadResource direct path=") +
path +
" loadPath=" +
loadPath);
} }
LoadResult result; LoadResult result;

View File

@@ -16,10 +16,12 @@ void InputManager::Initialize(void* platformWindowHandle) {
m_keyDownThisFrame.resize(256, false); m_keyDownThisFrame.resize(256, false);
m_keyDownLastFrame.resize(256, false); m_keyDownLastFrame.resize(256, false);
m_keyUpThisFrame.resize(256, false);
m_keyDown.resize(256, false); m_keyDown.resize(256, false);
m_mouseButtonDownThisFrame.resize(5, false); m_mouseButtonDownThisFrame.resize(5, false);
m_mouseButtonDownLastFrame.resize(5, false); m_mouseButtonDownLastFrame.resize(5, false);
m_mouseButtonUpThisFrame.resize(5, false);
m_mouseButtonDown.resize(5, false); m_mouseButtonDown.resize(5, false);
m_buttonDownThisFrame.resize(32, false); m_buttonDownThisFrame.resize(32, false);
@@ -39,14 +41,21 @@ void InputManager::Initialize(void* platformWindowHandle) {
} }
void InputManager::Shutdown() { void InputManager::Shutdown() {
m_mousePosition = Math::Vector2::Zero();
m_mouseDelta = Math::Vector2::Zero();
m_mouseScrollDelta = 0.0f;
m_touches.clear();
if (!m_initialized) return; if (!m_initialized) return;
m_keyDownThisFrame.clear(); m_keyDownThisFrame.clear();
m_keyDownLastFrame.clear(); m_keyDownLastFrame.clear();
m_keyUpThisFrame.clear();
m_keyDown.clear(); m_keyDown.clear();
m_mouseButtonDownThisFrame.clear(); m_mouseButtonDownThisFrame.clear();
m_mouseButtonDownLastFrame.clear(); m_mouseButtonDownLastFrame.clear();
m_mouseButtonUpThisFrame.clear();
m_mouseButtonDown.clear(); m_mouseButtonDown.clear();
m_axes.clear(); m_axes.clear();
@@ -64,10 +73,14 @@ void InputManager::Update(float deltaTime) {
m_keyDownLastFrame = m_keyDownThisFrame; m_keyDownLastFrame = m_keyDownThisFrame;
m_keyDownThisFrame.clear(); m_keyDownThisFrame.clear();
m_keyDownThisFrame.resize(256, false); m_keyDownThisFrame.resize(256, false);
m_keyUpThisFrame.clear();
m_keyUpThisFrame.resize(256, false);
m_mouseButtonDownLastFrame = m_mouseButtonDownThisFrame; m_mouseButtonDownLastFrame = m_mouseButtonDownThisFrame;
m_mouseButtonDownThisFrame.clear(); m_mouseButtonDownThisFrame.clear();
m_mouseButtonDownThisFrame.resize(5, false); m_mouseButtonDownThisFrame.resize(5, false);
m_mouseButtonUpThisFrame.clear();
m_mouseButtonUpThisFrame.resize(5, false);
m_buttonDownLastFrame = m_buttonDownThisFrame; m_buttonDownLastFrame = m_buttonDownThisFrame;
m_buttonDownThisFrame.clear(); m_buttonDownThisFrame.clear();
@@ -104,6 +117,13 @@ bool InputManager::IsKeyPressed(KeyCode key) const {
return m_keyDownThisFrame[index] && !m_keyDownLastFrame[index]; 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 { Math::Vector2 InputManager::GetMousePosition() const {
return m_mousePosition; return m_mousePosition;
} }
@@ -135,6 +155,13 @@ bool InputManager::IsMouseButtonClicked(MouseButton button) const {
return m_mouseButtonDownThisFrame[index] && !m_mouseButtonDownLastFrame[index]; 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 { int InputManager::GetTouchCount() const {
return static_cast<int>(m_touches.size()); return static_cast<int>(m_touches.size());
} }
@@ -170,10 +197,10 @@ float InputManager::GetAxisRaw(const Containers::String& axisName) const {
const auto& axis = it->second; const auto& axis = it->second;
float value = 0.0f; float value = 0.0f;
if (axis.GetPositiveKey() != KeyCode::None && IsKeyPressed(axis.GetPositiveKey())) { if (axis.GetPositiveKey() != KeyCode::None && IsKeyDown(axis.GetPositiveKey())) {
value += 1.0f; value += 1.0f;
} }
if (axis.GetNegativeKey() != KeyCode::None && IsKeyPressed(axis.GetNegativeKey())) { if (axis.GetNegativeKey() != KeyCode::None && IsKeyDown(axis.GetNegativeKey())) {
value -= 1.0f; value -= 1.0f;
} }
@@ -194,8 +221,34 @@ bool InputManager::GetButtonDown(const Containers::String& buttonName) const {
bool InputManager::GetButtonUp(const Containers::String& buttonName) const { bool InputManager::GetButtonUp(const Containers::String& buttonName) const {
auto it = m_buttons.find(buttonName); auto it = m_buttons.find(buttonName);
if (it == m_buttons.end()) return true; if (it == m_buttons.end()) return false;
return IsKeyUp(it->second); 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) { 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; if (index >= m_keyDown.size()) return;
m_keyDown[index] = false; m_keyDown[index] = false;
m_keyUpThisFrame[index] = true;
KeyEvent event; KeyEvent event;
event.keyCode = key; event.keyCode = key;
@@ -274,6 +328,8 @@ void InputManager::ProcessMouseButton(MouseButton button, bool pressed, int x, i
m_mouseButtonDown[index] = pressed; m_mouseButtonDown[index] = pressed;
if (pressed) { if (pressed) {
m_mouseButtonDownThisFrame[index] = true; m_mouseButtonDownThisFrame[index] = true;
} else {
m_mouseButtonUpThisFrame[index] = true;
} }
MouseButtonEvent event; MouseButtonEvent event;

View File

@@ -231,6 +231,25 @@ bool CameraRenderer::Render(
} }
ShutdownPassSequence(&builtinPostProcessPasses, builtinPostProcessPassesInitialized); 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.postScenePasses, postScenePassesInitialized);
ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized); ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized);

View File

@@ -9,6 +9,7 @@
#include <cstring> #include <cstring>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
#include <functional>
#include <fstream> #include <fstream>
#include <string> #include <string>
@@ -64,6 +65,56 @@ Containers::String NormalizePathString(const std::filesystem::path& path) {
return Containers::String(path.lexically_normal().generic_string().c_str()); 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, Containers::String ResolveArtifactDependencyPath(const Containers::String& dependencyPath,
const Containers::String& ownerArtifactPath) { const Containers::String& ownerArtifactPath) {
if (dependencyPath.Empty()) { if (dependencyPath.Empty()) {
@@ -358,6 +409,125 @@ bool TryParseTagMap(const std::string& objectText, Material* material) {
return false; 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) { bool TryParseCullMode(const Containers::String& value, MaterialCullMode& outMode) {
const Containers::String normalized = value.Trim().ToLower(); const Containers::String normalized = value.Trim().ToLower();
if (normalized == "none" || normalized == "off") { 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; return true;
} }

View File

@@ -7,6 +7,7 @@
#include "Components/MeshRendererComponent.h" #include "Components/MeshRendererComponent.h"
#include "Components/TransformComponent.h" #include "Components/TransformComponent.h"
#include "Debug/Logger.h" #include "Debug/Logger.h"
#include "Input/InputManager.h"
#include "Scene/Scene.h" #include "Scene/Scene.h"
#include "Scripting/ScriptComponent.h" #include "Scripting/ScriptComponent.h"
#include "Scripting/ScriptEngine.h" #include "Scripting/ScriptEngine.h"
@@ -356,6 +357,92 @@ float InternalCall_Time_GetDeltaTime() {
return GetInternalCallDeltaTime(); 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) { MonoString* InternalCall_GameObject_GetName(uint64_t gameObjectUUID) {
Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID);
return mono_string_new( 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_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::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_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_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_SetName", reinterpret_cast<const void*>(&InternalCall_GameObject_SetName));
mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetActiveSelf", reinterpret_cast<const void*>(&InternalCall_GameObject_GetActiveSelf)); 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; return FindClassMetadata(assemblyName, namespaceName, className) != nullptr;
} }
std::vector<std::string> MonoScriptRuntime::GetScriptClassNames(const std::string& assemblyName) const { bool MonoScriptRuntime::TryGetAvailableScriptClasses(
std::vector<std::string> classNames; std::vector<ScriptClassDescriptor>& outClasses) const {
classNames.reserve(m_classes.size()); outClasses.clear();
if (!m_initialized) {
return false;
}
outClasses.reserve(m_classes.size());
for (const auto& [key, metadata] : m_classes) { for (const auto& [key, metadata] : m_classes) {
(void)key; (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; continue;
} }
classNames.push_back(metadata.fullName); classNames.push_back(descriptor.GetFullName());
} }
std::sort(classNames.begin(), classNames.end());
return classNames; return classNames;
} }

View File

@@ -11,6 +11,12 @@ void NullScriptRuntime::OnRuntimeStop(Components::Scene* scene) {
(void)scene; (void)scene;
} }
bool NullScriptRuntime::TryGetAvailableScriptClasses(
std::vector<ScriptClassDescriptor>& outClasses) const {
outClasses.clear();
return false;
}
bool NullScriptRuntime::TryGetClassFieldMetadata( bool NullScriptRuntime::TryGetClassFieldMetadata(
const std::string& assemblyName, const std::string& assemblyName,
const std::string& namespaceName, const std::string& namespaceName,

View File

@@ -25,23 +25,36 @@ ScriptComponent::ScriptComponent()
void ScriptComponent::SetScriptClass(const std::string& namespaceName, const std::string& className) { void ScriptComponent::SetScriptClass(const std::string& namespaceName, const std::string& className) {
const bool hadScriptClass = HasScriptClass(); const bool hadScriptClass = HasScriptClass();
const bool changed = m_namespaceName != namespaceName || m_className != className;
m_namespaceName = namespaceName; m_namespaceName = namespaceName;
m_className = className; m_className = className;
if (!hadScriptClass && HasScriptClass()) { if (!hadScriptClass && HasScriptClass()) {
ScriptEngine::Get().OnScriptComponentEnabled(this); 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) { void ScriptComponent::SetScriptClass(const std::string& assemblyName, const std::string& namespaceName, const std::string& className) {
const bool hadScriptClass = HasScriptClass(); const bool hadScriptClass = HasScriptClass();
const bool changed =
m_assemblyName != assemblyName ||
m_namespaceName != namespaceName ||
m_className != className;
m_assemblyName = assemblyName; m_assemblyName = assemblyName;
m_namespaceName = namespaceName; m_namespaceName = namespaceName;
m_className = className; m_className = className;
if (!hadScriptClass && HasScriptClass()) { if (!hadScriptClass && HasScriptClass()) {
ScriptEngine::Get().OnScriptComponentEnabled(this); 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 { std::string ScriptComponent::GetFullClassName() const {
if (m_className.empty()) { if (m_className.empty()) {
return std::string(); return std::string();

View File

@@ -63,8 +63,19 @@ void ScriptEngine::SetRuntime(IScriptRuntime* runtime) {
m_runtime = runtime ? runtime : &m_nullRuntime; 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) { void ScriptEngine::OnRuntimeStart(Components::Scene* scene) {
const float configuredFixedDeltaTime = m_runtimeFixedDeltaTime;
OnRuntimeStop(); OnRuntimeStop();
m_runtimeFixedDeltaTime = configuredFixedDeltaTime;
if (!scene) { if (!scene) {
return; return;
@@ -109,6 +120,7 @@ void ScriptEngine::OnRuntimeStop() {
m_runtimeScene = nullptr; m_runtimeScene = nullptr;
m_scriptStates.clear(); m_scriptStates.clear();
m_scriptOrder.clear(); m_scriptOrder.clear();
m_runtimeFixedDeltaTime = DefaultFixedDeltaTime;
return; return;
} }
@@ -125,6 +137,7 @@ void ScriptEngine::OnRuntimeStop() {
m_scriptOrder.clear(); m_scriptOrder.clear();
m_runtimeRunning = false; m_runtimeRunning = false;
m_runtimeScene = nullptr; m_runtimeScene = nullptr;
m_runtimeFixedDeltaTime = DefaultFixedDeltaTime;
m_runtime->OnRuntimeStop(stoppedScene); m_runtime->OnRuntimeStop(stoppedScene);
} }
@@ -239,6 +252,33 @@ void ScriptEngine::OnScriptComponentDestroyed(ScriptComponent* component) {
StopTrackingScript(*state, false); 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 { bool ScriptEngine::HasTrackedScriptComponent(const ScriptComponent* component) const {
return FindState(component) != nullptr; return FindState(component) != nullptr;
} }
@@ -248,6 +288,45 @@ bool ScriptEngine::HasRuntimeInstance(const ScriptComponent* component) const {
return state && state->instanceCreated; 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( bool ScriptEngine::TrySetScriptFieldValue(
ScriptComponent* component, ScriptComponent* component,
const std::string& fieldName, const std::string& fieldName,

View File

@@ -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_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_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 foreach(XCENGINE_REQUIRED_PATH
"${XCENGINE_CSC_DLL}" "${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/Component.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Debug.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Debug.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/GameObject.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/InternalCalls.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/KeyCode.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Light.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Light.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MeshFilter.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MeshFilter.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MeshRenderer.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/ScriptComponentApiProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/RuntimeGameObjectProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/RuntimeGameObjectProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/HierarchyProbe.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/LifecycleProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshComponentProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshComponentProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererEdgeCaseProbe.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/TransformConversionProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformMotionProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformMotionProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformOrientationProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformOrientationProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformSpaceProbe.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 set(XCENGINE_MANAGED_FRAMEWORK_REFERENCES
/reference:${XCENGINE_NET472_REFERENCE_DIR}/mscorlib.dll /reference:${XCENGINE_NET472_REFERENCE_DIR}/mscorlib.dll
/reference:${XCENGINE_NET472_REFERENCE_DIR}/System.dll /reference:${XCENGINE_NET472_REFERENCE_DIR}/System.dll
@@ -140,3 +185,47 @@ add_custom_target(
${XCENGINE_MANAGED_OUTPUT_DIR}/mscorlib.dll ${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}
)

View 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;
}
}
}

Some files were not shown because too many files have changed in this diff Show More