diff --git a/AGENT.md b/AGENT.md index 11be9597..55fcf007 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,211 +1,242 @@ # XCEngine Agent Guide -这个文件面向进入当前仓库的开发者 / Codex / Agent。目标不是介绍项目,而是快速建立对“当前工程状态、真实约束、推荐入口与协作方式”的统一认知。 +这个文件面向在当前仓库里工作的 coding agent / 开发者。它不负责介绍项目卖点,而是给出当前 checkout 的真实工程状态、优先入口、硬约束和推荐验证方式。 -项目介绍与目录总览看 [README.md](README.md);设计原则与当前测试规则以本文为准。 +如果 README、旧文档和当前文件树 / `CMakeLists.txt` / 测试 target 冲突,以当前 checkout 为准,并在本次工作里顺手修正文档。 -## 1. 先看什么 +## 1. 先建立的事实 -进入仓库后,优先读这几份文档: +- 顶层 `CMakeLists.txt` 当前纳入 `engine/`、`editor/`、`managed/`、`mvs/RenderDoc/` 和 `tests/`。 +- `engine/` 构建静态库 `XCEngine`;`editor/` 构建 `XCEditor`,但输出文件名仍是 `editor/bin//XCEngine.exe`。 +- editor 默认把仓库内的 `project/` 识别为工程根目录,也支持 `--project ` 覆盖。 +- 当前工程不再只是 `Assets/` 目录:已经真实使用 `Assets/ + .meta + Library/` 的工程布局。 +- Mono 运行时与 editor 脚本类发现都从 `/Library/ScriptAssemblies/` 加载程序集。 +- `engine/CMakeLists.txt` 当前对 Vulkan 是硬依赖;`editor/` 和 `tests/` 首次配置会拉取 `ImGui` 和 `googletest`。 + +## 2. 优先阅读顺序 + +进入仓库后,优先看这些文档和入口文件: 1. [AGENT.md](AGENT.md) 2. [README.md](README.md) 3. [docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md) -4. [docs/plan/Renderer模块设计与实现.md](docs/plan/Renderer模块设计与实现.md) -5. [tests/TEST_SPEC.md](tests/TEST_SPEC.md) +4. [docs/plan/Shader与Material系统下一阶段计划.md](docs/plan/Shader与Material系统下一阶段计划.md) +5. [docs/plan/SceneViewport_Overlay_Gizmo_Rework_Plan.md](docs/plan/SceneViewport_Overlay_Gizmo_Rework_Plan.md) +6. [tests/TEST_SPEC.md](tests/TEST_SPEC.md) -其中: +额外规则: -- `RHI模块总览.md` 是 RHI 设计原则基线 -- `Renderer模块设计与实现.md` 是当前渲染层演进方向 -- `TEST_SPEC.md` 是测试组织、GT 图规则和 CMake/CTest 入口基线 +- 如果任务落在某个模块里,先读该模块的 `CMakeLists.txt` 和最近的测试目录。 +- `tests/TEST_SPEC.md` 仍然适合作为 GT 图规则和 RHI 边界说明,但 target 名称与目录变化时,始终以当前 `tests/CMakeLists.txt` 和子模块 `CMakeLists.txt` 为准。 -## 2. 当前工程状态 +## 3. 当前工程状态 -### 2.1 引擎整体 +### 3.1 Engine 与工程布局 -当前仓库已经不再处于“先把底层跑起来”的阶段,而是处于: +当前仓库已经不在“先把底层 sample 跑起来”的阶段,而是已经形成: -- `RHI` 已具备可维护的三后端基线 -- `Rendering` 已形成正式模块,不再只是零散 sample 代码 -- `Editor` 已能通过引擎渲染链路驱动 viewport -- `Scripting` 已具备 Mono 托管程序集构建和基础运行时测试 +- `RHI` +- `Rendering` +- `Editor viewport` +- `AssetDatabase / Library` +- `Mono scripting` -当前主线工作不应继续封闭式堆 RHI,而应在不破坏测试基线的前提下,继续推进渲染、编辑器和脚本三者的对接。 +这几条主线之间的真实对接。 -### 2.2 RHI +`Core/Asset/AssetDatabase` 现在是当前工程的重要基线,不是预研代码。它已经负责: -当前正式支持的后端: +- 扫描 `Assets/` +- 为资源生成 `.meta` +- 维护 `Library/SourceAssetDB/assets.db` +- 维护 `Library/ArtifactDB/artifacts.db` +- 维护哈希化 `Library/Artifacts/` -- `D3D12` -- `OpenGL` -- `Vulkan` +因此: -当前 RHI 的基本判断: +- `project/Library/` 虽然可重建,但在当前 workflow 里不是可以随手忽略的“垃圾目录”。 +- 涉及资源导入、meta、artifact、脚本程序集发现时,不要擅自删除 `project/Library/` 或 `.meta` 文件来“清环境”。 -- 抽象层已经可用 -- 后端差异路径已经被一轮轮集成测试逼出了很多真实问题并修过 -- 但它仍然不是“完全封顶”的模块 +### 3.2 Rendering -看到上层新问题时,不要本能地用“测试特判 / 测试绕过 / include 私有头”来糊过去;优先判断是不是 RHI 根因。 - -### 2.3 Rendering - -当前 `engine/include/XCEngine/Rendering/` 与 `engine/src/Rendering/` 已经落地: +当前 `engine/include/XCEngine/Rendering/` 与 `engine/src/Rendering/` 已经形成正式主链: +- `SceneRenderer` +- `CameraRenderer` +- `RenderPipeline` - `RenderSceneExtractor` - `RenderResourceCache` +- `SceneRenderRequestPlanner` - `RenderSurface` -- `CameraRenderer` -- `SceneRenderer` -- `BuiltinForwardPipeline` -- `RenderMaterialUtility` + +当前已经落地并应被视为正式能力的内容包括: + +- 内建 forward 主几何渲染 +- `ObjectId` 渲染与 editor picking +- `BuiltinInfiniteGridPass` +- `BuiltinObjectIdOutlinePass` +- `CameraRenderRequest::overlayPasses` + +当前 Renderer 的下一阶段主线不是 render graph,而是: + +- shader asset contract +- material GPU binding +- builtin pass contract +- renderer-owned feature contract + +对应设计文档是 [docs/plan/Shader与Material系统下一阶段计划.md](docs/plan/Shader与Material系统下一阶段计划.md)。 + +### 3.3 Editor + +当前 editor 的事实: + +- 它仍然是 `D3D12` 宿主应用。 +- Scene/Game viewport 已经通过引擎 `Rendering + RHI` 输出离屏纹理。 +- `ViewportHostService` 是 editor 与 renderer 的关键接线层。 +- object-id picking、selection outline、scene icon / gizmo overlay 已经进入正规化收口阶段。 + +当前 `editor/src/Viewport/` 已经存在: + +- `SceneViewportOverlayBuilder` +- `SceneViewportEditorOverlayPass` +- `SceneViewportPicker` +- `SceneViewportMoveGizmo` +- `SceneViewportRotateGizmo` +- `SceneViewportScaleGizmo` 这意味着: -- 当前已经存在正式的场景渲染运行时 -- 不要再把真实渲染逻辑塞回 `mvs/` 样例里长期存活 -- 新渲染功能优先落在 `Rendering` 模块与配套测试中 +- editor 是宿主,不是第二套 renderer。 +- 新的世界空间 overlay,不应继续堆回 `SceneViewPanel.cpp` 的 ImGui world overlay 路径。 +- 优先沿 `overlayPasses -> overlay builder -> canonical frame data -> overlay pass` 方向扩展。 -### 2.4 Editor +### 3.4 Scripting -当前 editor 的关键事实: +当前脚本链路由三部分组成: -- 它仍然是 `D3D12` 宿主应用 -- Scene/Game viewport 已通过离屏纹理接入引擎 `SceneRenderer` -- `editor/src/Viewport/ViewportHostService.h` 当前直接依赖 `RHI::D3D12Device` -- Scene view 相机控制已经有独立控制器与单测 +- `managed/XCEngine.ScriptCore/` +- `managed/GameScripts/` +- `project/Assets/**/*.cs` -因此当前要注意: +构建结果分两类: -- 引擎渲染逻辑应继续收敛在 `engine/Rendering` -- editor 只是宿主,不应复制一套独立 renderer -- editor 侧若要继续做 viewport 能力,应尽量围绕 `RenderSurface` 和引擎场景渲染入口扩展 +- `xcengine_managed_assemblies` 生成引擎示例程序集 +- `xcengine_project_managed_assemblies` 生成项目脚本程序集,并复制到 `project/Library/ScriptAssemblies/` -### 2.5 Scripting +`ScriptEngine` 当前已经具备: -当前脚本链路: +- 脚本类发现 +- 字段元数据读取 +- 默认值读取 +- stored override 管理 +- 运行时 managed field 同步 -- `managed/XCEngine.ScriptCore/`:托管 API -- `managed/GameScripts/`:托管测试 / 示例脚本 -- `engine/include/XCEngine/Scripting/` 与 `engine/src/Scripting/`:原生运行时桥接 +Inspector 侧已经存在 `ScriptComponentEditor`,因此脚本相关改动通常同时影响: -当前脚本构建依赖: +- `engine/src/Scripting/` +- `managed/` +- `project/Assets/Scripts/` +- `editor/src/ComponentEditors/` +- `tests/Scripting/` +- `tests/Editor/test_script_component_editor_utils.cpp` -- .NET SDK -- `参考/Fermion/Fermion/external/mono` 下的 Mono 依赖 +### 3.5 Tests -如果环境不完整,可以通过 `-DXCENGINE_ENABLE_MONO_SCRIPTING=OFF` 暂时关闭。 +当前测试主目录包括: -## 3. 当前测试基线 - -### 3.1 测试树 - -当前主要测试模块: - -- `tests/Core/` -- `tests/Memory/` -- `tests/Threading/` -- `tests/Debug/` - `tests/Components/` +- `tests/Core/` +- `tests/Debug/` +- `tests/Editor/` +- `tests/Input/` +- `tests/Memory/` +- `tests/Rendering/` +- `tests/Resources/` +- `tests/RHI/` - `tests/Scene/` - `tests/Scripting/` -- `tests/Rendering/` -- `tests/RHI/` -- `tests/Resources/` -- `tests/Input/` -- `tests/Editor/` +- `tests/Threading/` -### 3.2 RHI 测试分层 +需要特别记住的聚合 target: -`tests/RHI/` 当前分为五块: +- `rhi_all_tests` +- `rendering_all_tests` +- `rendering_phase_regression` +- `editor_tests` +- `scripting_tests` -- `tests/RHI/unit/` -- `tests/RHI/integration/` -- `tests/RHI/D3D12/` -- `tests/RHI/OpenGL/` -- `tests/RHI/Vulkan/` +## 4. 不可忽视的硬约束 -边界规则: +### 4.1 文档服从真实 checkout -- `tests/RHI/unit/` 和 `tests/RHI/integration/` 只能依赖公共 RHI 抽象 -- 后端私有 API / 原生句柄 / 后端特有断言,只能进对应后端目录 -- 如果抽象层测试必须 include 后端头才能过,优先修 RHI +如果文档与当前目录结构、target 名称、代码事实冲突: -### 3.3 RHI 抽象层集成测试 +- 先信当前 checkout +- 再更新文档 -当前抽象层场景: +不要沿用“计划中但未落地”的旧说法。 -- `minimal` -- `triangle` -- `quad` -- `sphere` -- `backpack` +### 4.2 RHI 抽象层与后端层必须分层 -必须遵守: +`tests/RHI/unit/` 与 `tests/RHI/integration/` 只能依赖公共 RHI 抽象。 -1. 每个场景目录只维护一张 `GT.ppm` -2. `D3D12 / OpenGL / Vulkan` 都与同一张 `GT.ppm` 做比对 -3. 运行输出可以按后端区分命名,但 GT 不能拆成多份 -4. 场景测试代码只能使用公共 RHI 接口 +不要为了让抽象层测试通过而: -### 3.4 Rendering 测试 +- include 后端私有头 +- 直接使用原生句柄 +- 给单一后端写抽象层特判 -当前 `tests/Rendering/` 已经存在: +如果必须这么做,优先修 RHI,而不是污染测试边界。 -- `unit/` -- `integration/backpack_scene` -- `integration/textured_quad_scene` -- `integration/transparent_material_scene` -- `integration/cull_material_scene` -- `integration/depth_sort_scene` -- `integration/material_state_scene` -- `integration/offscreen_scene` +### 4.3 Editor 是宿主,不是第二套渲染器 -结论: - -- 新渲染功能不需要再从零搭测试体系 -- 直接往现有 `tests/Rendering` 扩展即可 - -## 4. 已知关键约束 - -### 4.1 始终遵循 RHI 设计文档 - -涉及 RHI 抽象、后端收敛、接口新增时,始终以: - -- [docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md) - -作为原则基线,而不是局部方便优先。 - -### 4.2 功能正确优先于抽象表面整洁 - -允许为了功能正确做必要重构,但不允许: - -- 测试特判掩盖真实后端缺陷 -- 为了不改底层而污染抽象层测试 -- 为了“少动代码”保留明显错误的对象边界 - -### 4.3 backpack 资源导入行为必须统一 - -这是当前仓库里已经踩过的真实坑。 - -规则: - -- editor / runtime / rendering tests / rhi abstraction tests 的 backpack 导入行为必须保持一致 -- 不要只在某个测试路径里额外加 `MeshImportFlags::FlipUVs` -- 否则很容易出现 editor 显示正确、测试里 UV 错乱的假分叉 - -### 4.4 editor 是宿主,不是第二套渲染器 - -如果 editor viewport 有问题,优先判断: +如果 viewport、outline、picking 或 gizmo 有问题,优先判断: - 是 `Rendering` 模块问题 - 是 `RenderSurface` / RHI 输出问题 - 还是 editor 宿主接线问题 -不要把问题简单归因为“editor 特殊”,然后在 editor 里复制一份独立渲染逻辑。 +不要因为 editor 当前是 D3D12 host,就把问题草率地塞回 editor 私有渲染逻辑。 -## 5. 推荐构建入口 +### 4.4 不要再扩写 ImGui world overlay + +当前 viewport overlay / gizmo 已有明确收口方向。新功能若仍然继续堆在: + +- `SceneViewPanel.cpp` +- `SceneViewportOverlayRenderer.cpp` 的 ImGui world draw 路径 + +通常就是逆着当前架构方向在走。 + +优先入口是: + +- `CameraRenderRequest::overlayPasses` +- `SceneViewportOverlayBuilder` +- `SceneViewportEditorOverlayPass` + +### 4.5 backpack 导入行为必须统一 + +这是仓库里已经踩过的真实坑。 + +`backpack` 相关资源在以下路径中的导入行为必须保持一致: + +- editor +- runtime +- rendering tests +- rhi abstraction tests + +不要只在局部路径里额外加 `MeshImportFlags::FlipUVs` 之类的补丁。 + +### 4.6 `mvs/` 不是长期主线模块 + +`mvs/` 里有样例、研究和工具,但当前正式引擎逻辑的长期落点应优先是: + +- `engine/` +- `editor/` +- `managed/` +- `tests/` + +不要把正式渲染逻辑重新堆回 sample 子树长期存活。 + +## 5. 推荐构建与验证入口 ### 5.1 配置 @@ -213,7 +244,7 @@ cmake -S . -B build -A x64 ``` -如果缺 Mono: +如果当前任务不需要 Mono: ```bash cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF @@ -223,69 +254,45 @@ cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF ```bash cmake --build build --config Debug --target XCEngine -cmake --build build --config Debug --target XCVolumeRendererUI2 +cmake --build build --config Debug --target XCEditor cmake --build build --config Debug --target xcengine_managed_assemblies +cmake --build build --config Debug --target xcengine_project_managed_assemblies cmake --build build --config Debug --target rhi_all_tests -cmake --build build --config Debug --target rendering_unit_tests +cmake --build build --config Debug --target rendering_all_tests +cmake --build build --config Debug --target rendering_phase_regression cmake --build build --config Debug --target editor_tests cmake --build build --config Debug --target scripting_tests ``` -补充: +### 5.3 按改动类型选择验证 -- 编辑器 target 名称是 `XCVolumeRendererUI2` -- 输出文件名是 `editor/bin//XCEngine.exe` +- 改 `engine/RHI`:先跑 `rhi_abstraction_tests` 或 `rhi_backend_tests`,再决定是否扩展到 `rhi_all_tests` +- 改 `engine/Rendering`:先跑 `rendering_unit_tests` 和最相关的 `rendering_integration_*`,必要时再跑 `rendering_phase_regression` +- 改 `editor/Viewport` 或 Inspector:先跑 `editor_tests` +- 改 `engine/Scripting`、`managed/`、`project/Assets/Scripts/`:先构建 `xcengine_project_managed_assemblies`,再跑 `scripting_tests` +- 改资源导入、`.meta`、artifact 相关逻辑:优先跑 `tests/Resources/` 里的对应 target -## 6. 推荐验证入口 - -### 6.1 全量 +### 5.4 全量测试入口 ```bash ctest --test-dir build -C Debug --output-on-failure ``` -### 6.2 RHI +## 6. 按任务找入口 -```bash -cmake --build build --config Debug --target rhi_abstraction_tests -cmake --build build --config Debug --target rhi_backend_tests -``` +- RHI 抽象与后端:`engine/include/XCEngine/RHI/`、`engine/src/RHI/`、`tests/RHI/` +- Rendering 主链与 pass:`engine/include/XCEngine/Rendering/`、`engine/src/Rendering/`、`tests/Rendering/` +- Editor viewport / gizmo / picking:`editor/src/Viewport/`、`editor/src/panels/SceneViewPanel.cpp`、`tests/Editor/` +- 资源导入与工程布局:`engine/include/XCEngine/Core/Asset/`、`engine/src/Core/Asset/`、`editor/src/Managers/ProjectManager.cpp`、`project/Assets/`、`project/Library/` +- 脚本运行时与程序集:`engine/include/XCEngine/Scripting/`、`engine/src/Scripting/`、`managed/`、`project/Assets/Scripts/`、`tests/Scripting/` +- 默认工程与项目描述:`project/Project.xcproject`、`editor/src/Core/ProjectRootResolver.h`、`editor/src/Utils/ProjectFileUtils.h` -### 6.3 Rendering +## 7. 适合当前仓库的工作方式 -```bash -cmake --build build --config Debug --target rendering_unit_tests -cmake --build build --config Debug --target rendering_integration_backpack_scene -``` +1. 先读当前模块的 `CMakeLists.txt`、最近测试和设计文档,再动代码。 +2. 优先在既有模块边界里解决问题,不要绕开系统回到 sample 式实现。 +3. 先跑与改动最相关的最小验证,再决定是否扩大全量验证。 +4. 目录、target、入口、文档名改了,就同步更新 README / AGENT / 相关说明。 +5. 如果任务会有意重建 `project/Library/`、脚本程序集或 `.meta`,在结果里明确说明哪些文件是有意生成的。 -### 6.4 Editor / Scripting - -```bash -cmake --build build --config Debug --target editor_tests -cmake --build build --config Debug --target scripting_tests -``` - -## 7. 当前最合理的工作方式 - -适用于当前仓库的协作方式: - -1. 先在现有模块边界里定位问题,不要绕开体系补 sample 代码 -2. 改动后立即补或更新测试 -3. 先跑和改动最相关的测试,再决定是否扩大全量验证 -4. 每个阶段完成后尽快提交,保持工作区清晰 - -特别是涉及: - -- `RHI` -- `Rendering` -- `Editor viewport` -- `Scripting runtime` - -这些模块时,必须避免“表面修好了,但测试基线退化”的情况。 - -## 8. 文档入口 - -- 项目概览与目录树:[README.md](README.md) -- RHI 设计基线:[docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md) -- Renderer 设计文档:[docs/plan/Renderer模块设计与实现.md](docs/plan/Renderer模块设计与实现.md) -- 测试规范:[tests/TEST_SPEC.md](tests/TEST_SPEC.md) +这份文档的作用是给 agent 一个“当前真实工程长什么样”的基线。它本身也必须随着工程演进一起维护,不能再落回旧状态说明。 diff --git a/CMakeLists.txt b/CMakeLists.txt index ba9e3ce8..9ec8fc3d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ set( "Path to the bundled Mono distribution used by the scripting runtime") add_subdirectory(engine) -add_subdirectory(Editor) +add_subdirectory(editor) add_subdirectory(managed) add_subdirectory(mvs/RenderDoc) add_subdirectory(tests) diff --git a/MVS/ui/README.md b/MVS/ui/README.md index 55e935f7..80b6e109 100644 --- a/MVS/ui/README.md +++ b/MVS/ui/README.md @@ -1,183 +1,87 @@ -# UI Editor +# Legacy UI Prototype -Unity 风格的编辑器 UI,使用 ImGui 实现,作为 XCEngine 游戏引擎编辑器的一部分。 +`mvs/ui/` 是仓库里保留的早期 ImGui + D3D12 编辑器原型。它主要用于保留原始 UI 骨架、交互想法和历史实现参考,不是当前 XCEngine 的正式 editor 主线。 -## 简介 +当前正式 editor 在: -XCGameEngine UI 是一个仿 Unity 编辑器的桌面应用程序,提供场景管理、层级视图、属性检查器等功能。 +- [editor/README.md](D:\Xuanchi\Main\XCEngine\editor\README.md) -## 技术栈 +当前正式构建入口在仓库根目录: -- **渲染 API**: DirectX 12 -- **UI 框架**: ImGui -- **语言**: C++17 -- **构建系统**: CMake -- **依赖库**: DirectX 12 SDK +- [README.md](D:\Xuanchi\Main\XCEngine\README.md) +- [AGENT.md](D:\Xuanchi\Main\XCEngine\AGENT.md) -## 项目结构 +## 当前状态 -``` -ui/ +这个模块仍然可以单独构建,但它有几个需要明确的事实: + +- 顶层 `CMakeLists.txt` 当前并不会纳入 `mvs/ui/` +- 它使用独立的 `mvs/ui/CMakeLists.txt` +- target 名称仍然是历史遗留的 `XCVolumeRendererUI2` +- 它不是当前 `editor/` 目录下那套 `ViewportHostService + Rendering + Project.xcproject + ScriptAssemblies` 架构 +- 它不代表当前仓库的真实 editor 能力边界 + +因此: + +- 想用当前引擎编辑器,请进入 `editor/` +- 想研究早期 UI 原型、旧面板布局和最初的 ImGui 宿主结构,可以看这里 + +## 这个目录里有什么 + +```text +mvs/ui/ +├── CMakeLists.txt +├── README.md ├── src/ -│ ├── main.cpp # 程序入口 -│ ├── Application.cpp/h # 应用主类 -│ ├── Theme.cpp/h # 主题系统 │ ├── Core/ -│ │ ├── GameObject.h # 游戏对象 -│ │ └── LogEntry.h # 日志条目 │ ├── Managers/ -│ │ ├── LogSystem.cpp/h # 日志系统 -│ │ ├── ProjectManager.cpp/h # 项目管理 -│ │ ├── SceneManager.cpp/h # 场景管理 -│ │ └── SelectionManager.cpp/h # 选择管理 -│ └── panels/ -│ ├── Panel.cpp/h # 面板基类 -│ ├── MenuBar.cpp/h # 菜单栏 -│ ├── HierarchyPanel.cpp/h # 层级面板 -│ ├── InspectorPanel.cpp/h # 检查器面板 -│ ├── SceneViewPanel.cpp/h # 场景视图 -│ ├── GameViewPanel.cpp/h # 游戏视图 -│ ├── ProjectPanel.cpp/h # 项目面板 -│ └── ConsolePanel.cpp/h # 控制台面板 -├── bin/Release/ # 输出目录 -│ ├── XCVolumeRendererUI2.exe # 可执行文件 -│ ├── imgui.ini # ImGui 配置 -│ └── Assets/ -│ └── Models/ -│ └── Character.fbx # 示例模型 -├── build/ # 构建目录 -└── CMakeLists.txt # CMake 配置 +│ ├── panels/ +│ ├── Application.cpp +│ ├── Application.h +│ ├── Theme.cpp +│ ├── Theme.h +│ └── main.cpp +├── build/ # 历史本地构建输出 +└── bin/ # 历史可执行文件输出 ``` -## 构建方法 +主要内容是: -### 前置要求 +- 早期 `Application` / `Theme` 实现 +- 基础 `Hierarchy / Scene / Game / Inspector / Project / Console` 面板骨架 +- 旧版 `SceneManager / ProjectManager / LogSystem` -- Windows 10/11 -- Visual Studio 2019 或更高版本 -- CMake 3.15+ +## 单独构建方式 -### 构建步骤 +如果你只是想启动这个原型,可以单独进入该目录配置: ```bash -cd ui -mkdir build && cd build -cmake .. -cmake --build . --config Release +cmake -S mvs/ui -B mvs/ui/build -A x64 +cmake --build mvs/ui/build --config Release ``` -### 运行 +输出可执行文件通常位于: ```bash -# 运行编译好的可执行文件 -.\bin\Release\XCGameEngineUI.exe +.\mvs\ui\bin\Release\XCVolumeRendererUI2.exe ``` -## 功能特性 +## 与当前正式 editor 的区别 -### 编辑器面板 +当前正式 editor 具备而这个原型没有正式接入的能力包括: -#### 菜单栏(MenuBar) -- 文件菜单(新建、打开、保存等) -- 编辑菜单(撤销、重做等) -- 视图菜单(面板显示/隐藏) -- 帮助菜单 +- `engine/Rendering` 主链驱动的 scene/game viewport +- `ViewportHostService` +- object-id picking 与 outline +- `Project.xcproject` +- `Assets + .meta + Library` +- `project/Library/ScriptAssemblies` +- `ScriptComponent` Inspector 与脚本类发现 -#### 层级面板(Hierarchy Panel) -- 显示场景中所有游戏对象 -- 树形结构展示父子关系 -- 支持对象选择 -- 对象重命名 +所以这个目录更适合被理解为: -#### 检查器面板(Inspector Panel) -- 显示选中对象的属性 -- 支持组件编辑 -- 变换组件(位置、旋转、缩放) -- 材质组件 +- 历史设计参考 +- 原型实现存档 +- 某些 UI 想法的对照样本 -#### 场景视图(Scene View) -- 3D 场景预览 -- 相机控制(平移、旋转、缩放) -- 对象选择 -- 辅助工具(网格、轴心) - -#### 游戏视图(Game View) -- 游戏运行时的画面预览 -- 分辨率设置 -- 宽高比选择 - -#### 项目面板(Project Panel) -- 项目文件浏览器 -- 资源组织 -- 搜索过滤 - -#### 控制台面板(Console Panel) -- 日志输出 -- 警告和错误显示 -- 日志级别过滤 -- 清空日志 - -### 管理系统 - -#### 日志系统(LogSystem) -- 分级日志(Info、Warning、Error) -- 时间戳 -- 日志持久化 - -#### 项目管理(ProjectManager) -- 项目创建/打开 -- 资源路径管理 - -#### 场景管理(SceneManager) -- 场景加载/保存 -- 对象生命周期管理 - -#### 选择管理(SelectionManager) -- 当前选中对象追踪 -- 多选支持 - -### 主题系统 - -- 深色主题(Dark Theme) -- 可自定义配色方案 - -## 窗口布局 - -默认布局采用经典的 Unity 编辑器风格: - -``` -+----------------------------------------------------------+ -| 菜单栏 | -+----------+------------------------+----------------------+ -| | | | -| 项目 | 场景视图 | 检查器 | -| 面板 | | | -| | | | -+----------+------------------------+----------------------+ -| 层级面板 | 游戏视图 | -| | | -+------------------------------------+----------------------+ -| 控制台面板 | -+----------------------------------------------------------+ -``` - -## 依赖说明 - -- ImGui - 跨平台 GUI 库 -- DirectX 12 - 渲染 API -- Windows SDK - 窗口管理 - -## 扩展开发 - -### 添加新面板 - -1. 在 `panels/` 目录下创建新的面板类 -2. 继承 `Panel` 基类 -3. 实现 `Render()` 方法 -4. 在 `Application` 中注册新面板 - -### 添加新组件 - -1. 定义组件类 -2. 在 `GameObject` 中注册组件类型 -3. 在 `InspectorPanel` 中添加属性编辑器 +而不是当前 XCEngine editor 的入口。 diff --git a/README.md b/README.md index 2b1f71a1..aeed9f89 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,39 @@ # XCEngine -XCEngine 是一个正在持续开发中的模块化 C++ 游戏引擎。当前仓库已经形成了较完整的底座: +XCEngine 是一个 Windows 优先、编辑器优先的模块化 C++ 游戏引擎工作区。当前主线已经形成 `RHI -> Rendering -> Editor Viewport -> AssetDatabase/Library -> Mono Scripting` 的可运行闭环,不再只是示例代码集合。 -- `RHI` 抽象层已稳定覆盖 `D3D12 / OpenGL / Vulkan` -- `Rendering` 模块已经落地最小可用的场景渲染链路 -- `editor/` 已接入基于引擎渲染链路的 Scene/Game viewport -- `managed/` 已具备基于 Mono 的 C# 运行时与托管程序集构建链路 -- `tests/` 已形成按模块分层的单元测试与集成测试体系 +这份 README 面向引擎用户:关注怎么进入项目、怎么构建、当前仓库各目录分别负责什么。面向构建本引擎的 coding agent,请看 [AGENT.md](AGENT.md)。 -如果你是第一次进入当前仓库,先看 [AGENT.md](AGENT.md)。它更偏向“当前工程状态、协作约束与推荐入口”。 +## 项目定位 -## 当前状态 - -- 核心引擎库位于 `engine/`,包含 Audio、Components、Core、Debug、Input、Memory、Platform、Rendering、Resources、RHI、Scene、Scripting、Threading。 -- RHI 当前正式支持 `D3D12 / OpenGL / Vulkan` 三后端,且抽象层与后端层测试都已经落地。 -- Rendering 当前已具备 `RenderSceneExtractor`、`RenderResourceCache`、`SceneRenderer`、`CameraRenderer`、`BuiltinForwardPipeline`。 -- Editor 当前是 `D3D12` 宿主应用,但场景绘制已通过引擎 `Rendering + RHI` 链路输出到离屏纹理,再接入 ImGui 面板。 -- Managed scripting 当前通过 `managed/XCEngine.ScriptCore` 与 `managed/GameScripts` 构建托管程序集;默认启用 Mono 运行时集成。 -- 默认示例工程位于 `project/`,已包含基础场景、背包模型与脚本资产目录。 +- `engine/` 提供静态库 `XCEngine`,包含 `RHI`、`Rendering`、`Resources`、`Scene`、`Scripting` 等核心模块。 +- `editor/` 提供桌面编辑器 `XCEditor`,输出文件名为 `XCEngine.exe`,默认打开仓库内的 `project/`。 +- `project/` 是当前随仓库维护的示例工程,已经采用 `Assets/ + .meta + Library/` 的工程布局。 +- `managed/` 负责 `XCEngine.ScriptCore.dll`、示例 `GameScripts.dll` 以及项目脚本程序集构建。 +- `tests/` 已覆盖 Engine、RHI、Rendering、Editor、Scripting 等主要模块。 ## 环境要求 -建议在 Windows 上构建当前仓库。 +当前推荐在 Windows 上使用和构建。 - Windows 10/11 - Visual Studio 2022 / MSVC v143 - CMake 3.15+ -- .NET SDK - Vulkan SDK +- .NET SDK - Git LFS -如果启用默认脚本构建,还需要: +启用 Mono 脚本运行时时,还需要: - `参考/Fermion/Fermion/external/mono` 下可用的 Mono 头文件、静态库与 `mscorlib.dll` -如果本地暂时没有 Mono 依赖,可以在配置时关闭脚本构建: +补充说明: -```bash -cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF -``` +- `engine/CMakeLists.txt` 当前对 `Vulkan` 是硬依赖,未配置 Vulkan SDK 时无法完成配置。 +- `editor/` 和 `tests/` 首次配置会通过 `FetchContent` 从 Gitee 镜像拉取 `ImGui` 与 `googletest`;离线环境请确保已有可复用的 `_deps` 缓存。 +- 如果需要 `mvs/3DGS-Unity/room.ply` 这类大文件示例,请先执行 Git LFS 拉取。 -如果你需要 `mvs/3DGS-Unity/room.ply` 这类大文件样例,请确保已经执行过 Git LFS 拉取。 - -## 构建 +## 快速开始 ### 1. 配置 @@ -50,60 +41,92 @@ cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF cmake -S . -B build -A x64 ``` -### 2. 全量编译 +如果本地暂时没有 Mono 依赖,可以先关闭脚本运行时: ```bash -cmake --build build --config Debug +cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF ``` -### 3. 常用增量 target +### 2. 构建常用目标 ```bash cmake --build build --config Debug --target XCEngine -cmake --build build --config Debug --target XCVolumeRendererUI2 +cmake --build build --config Debug --target XCEditor cmake --build build --config Debug --target xcengine_managed_assemblies -cmake --build build --config Debug --target rhi_all_tests -cmake --build build --config Debug --target rendering_unit_tests -cmake --build build --config Debug --target editor_tests -cmake --build build --config Debug --target scripting_tests +cmake --build build --config Debug --target xcengine_project_managed_assemblies ``` 说明: -- 编辑器 CMake target 名称仍然是历史遗留的 `XCVolumeRendererUI2`,但输出文件名是 `XCEngine.exe` -- 顶层 CMake 当前会纳入 `engine/`、`editor/`、`managed/`、`mvs/RenderDoc/` 与 `tests/` +- `XCEngine` 是引擎静态库。 +- `XCEditor` 是编辑器 target,输出文件名仍为 `editor/bin//XCEngine.exe`。 +- `xcengine_managed_assemblies` 生成 `managed/` 示例托管程序集。 +- `xcengine_project_managed_assemblies` 会扫描 `project/Assets/**/*.cs`,并把结果输出到 `project/Library/ScriptAssemblies/`。 -## 测试 +### 3. 启动编辑器 -推荐始终通过 CMake / CTest 驱动测试。 +```bash +.\editor\bin\Debug\XCEngine.exe +``` -### 列出测试 +默认情况下,编辑器会自动把仓库内的 `project/` 识别为工程根目录。也可以显式指定其他工程: + +```bash +.\editor\bin\Debug\XCEngine.exe --project D:\Path\To\MyProject +``` + +如果 Inspector 里看不到 C# 脚本类,先确认 `project/Library/ScriptAssemblies/` 中已经生成: + +- `XCEngine.ScriptCore.dll` +- `GameScripts.dll` +- `mscorlib.dll` + +### 4. 当前推荐验证入口 ```bash ctest --test-dir build -N -C Debug -``` - -### 运行全部测试 - -```bash ctest --test-dir build -C Debug --output-on-failure ``` -### 常用测试 target +按模块常用的构建 / 验证 target: ```bash -cmake --build build --config Debug --target rhi_abstraction_tests -cmake --build build --config Debug --target rhi_backend_tests -cmake --build build --config Debug --target rendering_unit_tests +cmake --build build --config Debug --target rhi_all_tests +cmake --build build --config Debug --target rendering_all_tests +cmake --build build --config Debug --target rendering_phase_regression cmake --build build --config Debug --target editor_tests cmake --build build --config Debug --target scripting_tests ``` -更完整的测试规则见 [tests/TEST_SPEC.md](tests/TEST_SPEC.md)。 +## 当前仓库状态 -## 目录结构 +### Engine -下面的目录树用于表达当前仓库的实际入口结构与模块边界。 +- `RHI` 正式维护 `D3D12 / OpenGL / Vulkan` 三后端。 +- `Rendering` 已形成 `SceneRenderer -> CameraRenderer -> RenderPipeline` 主链,包含 `ObjectId`、`InfiniteGrid`、`Outline`、`overlayPasses` 等能力。 +- `Resources` 与 `Core/Asset` 已不只是简单加载器,当前已经具备 `Assets/.meta/Library` 风格的 `AssetDatabase` 与 artifact 缓存。 + +### Editor + +- 当前 editor 是 `D3D12` 宿主应用,但 Scene/Game viewport 已通过引擎 `Rendering + RHI` 链路渲染到离屏纹理,再接入 ImGui。 +- `Viewport` 相关代码已经进入 overlay/gizmo 正规化阶段,`SceneViewportOverlayBuilder`、`SceneViewportEditorOverlayPass`、`ObjectId` picking 都已落地。 +- Inspector 已支持 `ScriptComponent` 的脚本类选择、字段元数据读取、字段重置与基础编辑。 + +### Project & Scripting + +- 示例工程位于 `project/`,当前工程文件是 `Project.xcproject`,启动场景为 `Assets/Scenes/Main.xc`。 +- `project/Assets/` 现已包含 `.meta` 文件,`project/Library/` 则维护 `SourceAssetDB`、`ArtifactDB`、`Artifacts` 与 `ScriptAssemblies`。 +- `managed/` 会生成引擎脚本 API 与示例脚本程序集,项目资产下的 `.cs` 文件也会被单独编译为项目脚本程序集。 + +### Tests + +- `tests/` 已覆盖 `Core`、`Memory`、`Threading`、`Scene`、`Resources`、`RHI`、`Rendering`、`Editor`、`Scripting` 等模块。 +- `tests/Rendering/` 当前已包含 `backpack_scene`、`backpack_lit_scene`、`camera_stack_scene`、`offscreen_scene` 等集成场景。 +- `tests/RHI/` 同时维护抽象层测试与后端专用测试,`D3D12 / OpenGL / Vulkan` 都有独立子树。 + +## 完整目录结构 + +以下目录树以当前工程入口为准,保留了当前 workflow 已经实际使用的生成目录;省略 `.git/`、`build/_deps/` 与临时文件。 ```text XCEngine/ @@ -112,15 +135,33 @@ XCEngine/ ├── AGENT.md ├── CMakeLists.txt ├── README.md -├── build/ # 本地构建输出 +├── build/ # 本地 CMake 构建输出 ├── docs/ -│ ├── api/ # API 文档 -│ ├── issues/ # 跟踪中的设计 / 实现问题 -│ ├── plan/ # 设计文档、阶段规划、归档方案 -│ │ ├── Renderer模块设计与实现.md -│ │ └── end/ -│ │ └── RHI模块设计与实现/ -│ │ └── RHI模块总览.md +│ ├── api/ +│ │ ├── XCEngine/ +│ │ ├── _guides/ +│ │ ├── _meta/ +│ │ ├── _tools/ +│ │ └── main.md +│ ├── issues/ +│ ├── plan/ +│ │ ├── end/ +│ │ │ ├── RHI模块设计与实现/ +│ │ │ │ ├── RHIFence.md +│ │ │ │ └── RHI模块总览.md +│ │ │ └── 编辑器与运行时分层架构设计.md +│ │ ├── 开题报告和任务书/ +│ │ ├── 旧版题目/ +│ │ ├── API文档并行更新任务池_2026-04-02.md +│ │ ├── C#脚本模块的设计与实现.md +│ │ ├── Editor架构说明.md +│ │ ├── SceneViewport_Overlay_Gizmo_Rework_Plan.md +│ │ ├── Shader与Material系统下一阶段计划.md +│ │ ├── Unity SRP API参考文档.md +│ │ ├── Unity式Library资产导入与缓存系统重构方案.md +│ │ ├── Unity式Tick系统与PlayMode运行时方案.md +│ │ ├── Unity式Tick系统与PlayMode运行时方案-阶段进展.md +│ │ └── Unity绝区零开发文档还原版.md │ ├── used/ │ ├── api-skill.md │ ├── blueprint-skill.md @@ -143,13 +184,28 @@ XCEngine/ │ │ ├── UI/ │ │ ├── Utils/ │ │ ├── Viewport/ +│ │ │ ├── Passes/ +│ │ │ ├── SceneViewportOverlayBuilder.cpp +│ │ │ ├── SceneViewportOverlayBuilder.h +│ │ │ ├── SceneViewportOverlayRenderer.cpp +│ │ │ ├── SceneViewportOverlayRenderer.h +│ │ │ ├── SceneViewportPicker.cpp +│ │ │ ├── SceneViewportPicker.h +│ │ │ ├── SceneViewportMoveGizmo.cpp +│ │ │ ├── SceneViewportMoveGizmo.h +│ │ │ ├── SceneViewportRotateGizmo.cpp +│ │ │ ├── SceneViewportRotateGizmo.h +│ │ │ ├── SceneViewportScaleGizmo.cpp +│ │ │ ├── SceneViewportScaleGizmo.h +│ │ │ ├── ViewportHostRenderFlowUtils.h +│ │ │ └── ViewportHostService.h │ │ ├── Application.cpp │ │ ├── Application.h │ │ ├── EditorApp.rc │ │ ├── Theme.cpp │ │ ├── Theme.h │ │ └── main.cpp -│ └── bin/ # 编辑器输出目录,输出名为 XCEngine.exe +│ └── bin/ # 编辑器输出目录,输出名为 XCEngine.exe ├── engine/ │ ├── CMakeLists.txt │ ├── include/ @@ -167,17 +223,24 @@ XCEngine/ │ │ ├── Platform/ │ │ │ └── Windows/ │ │ ├── Rendering/ +│ │ │ ├── Passes/ │ │ │ ├── Pipelines/ -│ │ │ ├── CameraRenderRequest.h │ │ │ ├── CameraRenderer.h +│ │ │ ├── CameraRenderRequest.h +│ │ │ ├── ObjectIdEncoding.h +│ │ │ ├── ObjectIdPass.h │ │ │ ├── RenderCameraData.h │ │ │ ├── RenderContext.h │ │ │ ├── RenderMaterialUtility.h +│ │ │ ├── RenderPass.h │ │ │ ├── RenderPipeline.h │ │ │ ├── RenderPipelineAsset.h │ │ │ ├── RenderResourceCache.h │ │ │ ├── RenderSceneExtractor.h +│ │ │ ├── RenderSceneUtility.h │ │ │ ├── RenderSurface.h +│ │ │ ├── SceneRenderRequestPlanner.h +│ │ │ ├── SceneRenderRequestUtils.h │ │ │ ├── SceneRenderer.h │ │ │ └── VisibleRenderObject.h │ │ ├── Resources/ @@ -189,29 +252,7 @@ XCEngine/ │ │ ├── RHI/ │ │ │ ├── D3D12/ │ │ │ ├── OpenGL/ -│ │ │ ├── Vulkan/ -│ │ │ ├── RHIBuffer.h -│ │ │ ├── RHICapabilities.h -│ │ │ ├── RHICommandList.h -│ │ │ ├── RHICommandQueue.h -│ │ │ ├── RHIDescriptorPool.h -│ │ │ ├── RHIDescriptorSet.h -│ │ │ ├── RHIDevice.h -│ │ │ ├── RHIEnums.h -│ │ │ ├── RHIFactory.h -│ │ │ ├── RHIFence.h -│ │ │ ├── RHIFramebuffer.h -│ │ │ ├── RHIPipelineLayout.h -│ │ │ ├── RHIPipelineState.h -│ │ │ ├── RHIRenderPass.h -│ │ │ ├── RHIResource.h -│ │ │ ├── RHIResourceView.h -│ │ │ ├── RHISampler.h -│ │ │ ├── RHIScreenshot.h -│ │ │ ├── RHIShader.h -│ │ │ ├── RHISwapChain.h -│ │ │ ├── RHITexture.h -│ │ │ └── RHITypes.h +│ │ │ └── Vulkan/ │ │ ├── Scene/ │ │ ├── Scripting/ │ │ │ └── Mono/ @@ -230,6 +271,7 @@ XCEngine/ │ │ ├── Platform/ │ │ │ └── Windows/ │ │ ├── Rendering/ +│ │ │ ├── Passes/ │ │ │ └── Pipelines/ │ │ ├── Resources/ │ │ │ ├── AudioClip/ @@ -255,28 +297,45 @@ XCEngine/ │ └── renderdoc_parser/ ├── managed/ │ ├── CMakeLists.txt -│ ├── GameScripts/ # 示例 / 验证脚本程序集源码 -│ └── XCEngine.ScriptCore/ # 引擎托管 API +│ ├── GameScripts/ +│ └── XCEngine.ScriptCore/ ├── mvs/ -│ ├── 3DGS-Unity/ # Unity 侧 3DGS 参考与资源 +│ ├── 3DGS-Unity/ │ ├── D3D12/ │ ├── Music fluctuations/ │ ├── OpenGL/ │ ├── RenderDoc/ │ ├── Res/ -│ ├── ui/ +│ ├── ui/ # 早期 ImGui + D3D12 UI 原型,非当前正式 editor │ └── VolumeRenderer/ ├── project/ │ ├── .xceditor/ -│ │ └── imgui_layout.ini +│ │ ├── imgui_layout.ini +│ │ └── thumbs/ │ ├── Assets/ │ │ ├── Materials/ │ │ ├── Models/ │ │ │ └── backpack/ +│ │ ├── New Folder/ +│ │ ├── New Folder 1/ │ │ ├── Scenes/ +│ │ │ ├── Backpack.xc │ │ │ └── Main.xc │ │ └── Scripts/ +│ │ ├── ProjectScriptProbe.cs +│ │ └── Textures/ +│ ├── Library/ +│ │ ├── ArtifactDB/ +│ │ ├── Artifacts/ +│ │ ├── ScriptAssemblies/ +│ │ │ ├── GameScripts.dll +│ │ │ ├── mscorlib.dll +│ │ │ └── XCEngine.ScriptCore.dll +│ │ └── SourceAssetDB/ +│ ├── Assets.meta │ └── Project.xcproject +├── scripts/ +│ └── Run-RendererPhaseRegression.ps1 ├── tests/ │ ├── CMakeLists.txt │ ├── TEST_SPEC.md @@ -293,7 +352,9 @@ XCEngine/ │ ├── Memory/ │ ├── Rendering/ │ │ ├── integration/ +│ │ │ ├── backpack_lit_scene/ │ │ │ ├── backpack_scene/ +│ │ │ ├── camera_stack_scene/ │ │ │ ├── cull_material_scene/ │ │ │ ├── depth_sort_scene/ │ │ │ ├── material_state_scene/ @@ -308,66 +369,44 @@ XCEngine/ │ │ ├── Shader/ │ │ └── Texture/ │ ├── RHI/ -│ │ ├── CMakeLists.txt │ │ ├── D3D12/ │ │ │ ├── integration/ │ │ │ └── unit/ -│ │ ├── OpenGL/ -│ │ │ ├── integration/ -│ │ │ └── unit/ -│ │ ├── Vulkan/ -│ │ │ ├── integration/ -│ │ │ ├── unit/ -│ │ │ └── TEST_SPEC.md │ │ ├── integration/ │ │ │ ├── backpack/ │ │ │ ├── fixtures/ │ │ │ ├── minimal/ │ │ │ ├── quad/ │ │ │ ├── sphere/ -│ │ │ ├── triangle/ -│ │ │ ├── compare_ppm.py -│ │ │ ├── README.md -│ │ │ └── run_integration_test.py -│ │ └── unit/ +│ │ │ └── triangle/ +│ │ ├── OpenGL/ +│ │ │ ├── integration/ +│ │ │ └── unit/ +│ │ ├── unit/ +│ │ └── Vulkan/ +│ │ ├── integration/ +│ │ └── unit/ │ ├── Scene/ │ ├── Scripting/ │ └── Threading/ ├── 参考/ -│ └── TransformGizmo/ +│ ├── Fermion/ +│ ├── TransformGizmo/ +│ ├── unity editor/ +│ ├── unity-editor-icons/ +│ ├── unity-icons/ +│ └── UnityRuntimeSceneGizmo-master/ └── .vscode/ ``` -## 模块概览 - -### Engine - -- `RHI`:统一图形 API 抽象,当前三后端并行维护 -- `Rendering`:位于 Scene/Resources 与 RHI 之间,负责把场景提取成真实 draw path -- `Resources`:Mesh / Texture / Material / Shader / AudioClip 导入与加载 -- `Scene + Components`:游戏对象、相机、灯光、网格与脚本组件 -- `Scripting`:原生脚本运行时与 Mono 托管桥接 - -### Editor - -- 当前是 D3D12 桌面宿主应用 -- Scene/Game viewport 已走引擎离屏渲染链路 -- 包含 Actions、Commands、Panels、Viewport、Managers 等编辑器子系统 - -### Tests - -- `tests/RHI/`:RHI 抽象层与三后端测试 -- `tests/Rendering/`:渲染链路单元与场景级集成测试 -- `tests/Scripting/`:脚本运行时与托管程序集集成测试 -- `tests/Editor/`:编辑器动作路由与 Scene viewport 相机控制测试 - ## 关键文档入口 -- 当前工程协作入口:[AGENT.md](AGENT.md) -- RHI 核心设计文档:[docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md) -- Renderer 规划与实现说明:[docs/plan/Renderer模块设计与实现.md](docs/plan/Renderer模块设计与实现.md) +- 协作基线与 coding agent 入口:[AGENT.md](AGENT.md) +- RHI 基线设计:[docs/plan/end/RHI模块设计与实现/RHI模块总览.md](docs/plan/end/RHI模块设计与实现/RHI模块总览.md) +- 当前 Shader / Material 主线:[docs/plan/Shader与Material系统下一阶段计划.md](docs/plan/Shader与Material系统下一阶段计划.md) +- Scene viewport overlay 重构:[docs/plan/SceneViewport_Overlay_Gizmo_Rework_Plan.md](docs/plan/SceneViewport_Overlay_Gizmo_Rework_Plan.md) - 测试规范:[tests/TEST_SPEC.md](tests/TEST_SPEC.md) ## 许可证 -当前仓库根目录未看到单独的顶层许可证文件;涉及第三方库时请分别遵循对应依赖目录中的许可证说明。 +当前仓库根目录未看到独立的顶层许可证文件。涉及第三方库时,请分别遵循其所在目录中的许可证或随附说明。 diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 078ac87d..846b077e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -49,6 +49,9 @@ if(NOT TARGET XCEngine) add_subdirectory("${XCENGINE_ENGINE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/engine_dependency") endif() +file(TO_CMAKE_PATH "${XCENGINE_ROOT_DIR}" XCENGINE_ROOT_DIR_CMAKE) +file(TO_CMAKE_PATH "${XCENGINE_MONO_ROOT_DIR}" XCENGINE_MONO_ROOT_DIR_CMAKE) + set(IMGUI_SOURCES ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_demo.cpp @@ -63,6 +66,7 @@ add_executable(${PROJECT_NAME} WIN32 src/EditorApp.rc src/main.cpp src/Application.cpp + src/Scripting/EditorScriptAssemblyBuilder.cpp src/Theme.cpp src/Core/UndoManager.cpp src/Core/PlaySessionController.cpp @@ -101,6 +105,13 @@ target_include_directories(${PROJECT_NAME} PRIVATE target_compile_definitions(${PROJECT_NAME} PRIVATE UNICODE _UNICODE) target_compile_options(${PROJECT_NAME} PRIVATE /utf-8) +if(XCENGINE_ENABLE_MONO_SCRIPTING) + target_compile_definitions(${PROJECT_NAME} PRIVATE + XCENGINE_ENABLE_MONO_SCRIPTING + XCENGINE_EDITOR_REPO_ROOT="${XCENGINE_ROOT_DIR_CMAKE}" + XCENGINE_EDITOR_MONO_ROOT_DIR="${XCENGINE_MONO_ROOT_DIR_CMAKE}") +endif() + if(MSVC) target_compile_options(${PROJECT_NAME} PRIVATE /FS) set_property(TARGET ${PROJECT_NAME} PROPERTY diff --git a/editor/README.md b/editor/README.md index 55e935f7..3977c85d 100644 --- a/editor/README.md +++ b/editor/README.md @@ -1,183 +1,205 @@ -# UI Editor +# XCEditor -Unity 风格的编辑器 UI,使用 ImGui 实现,作为 XCEngine 游戏引擎编辑器的一部分。 +`editor/` 是 XCEngine 当前随仓库维护的桌面编辑器模块。它不是一套独立渲染器,而是 `D3D12` 宿主应用,用来承接引擎 `Rendering + RHI + Scene + Scripting` 主链。 -## 简介 +当前 editor 已经具备: -XCGameEngine UI 是一个仿 Unity 编辑器的桌面应用程序,提供场景管理、层级视图、属性检查器等功能。 +- Scene / Game viewport 离屏渲染接入 +- object-id picking 与选中描边 +- scene overlay / gizmo 正规化收口 +- 项目根目录解析与 `Project.xcproject` 加载 +- `Assets + .meta + Library` 风格项目目录接入 +- `ScriptComponent` 的脚本类与字段编辑入口 -## 技术栈 +## 当前定位 -- **渲染 API**: DirectX 12 -- **UI 框架**: ImGui -- **语言**: C++17 -- **构建系统**: CMake -- **依赖库**: DirectX 12 SDK +如果你想理解当前 editor,先把它当成三层: -## 项目结构 +1. `Win32 + D3D12` 宿主窗口与 ImGui backend +2. `ViewportHostService` 对引擎渲染链路的接线 +3. `panels/`、`Managers/`、`ComponentEditors/` 这些编辑器业务层 -``` -ui/ -├── src/ -│ ├── main.cpp # 程序入口 -│ ├── Application.cpp/h # 应用主类 -│ ├── Theme.cpp/h # 主题系统 -│ ├── Core/ -│ │ ├── GameObject.h # 游戏对象 -│ │ └── LogEntry.h # 日志条目 -│ ├── Managers/ -│ │ ├── LogSystem.cpp/h # 日志系统 -│ │ ├── ProjectManager.cpp/h # 项目管理 -│ │ ├── SceneManager.cpp/h # 场景管理 -│ │ └── SelectionManager.cpp/h # 选择管理 -│ └── panels/ -│ ├── Panel.cpp/h # 面板基类 -│ ├── MenuBar.cpp/h # 菜单栏 -│ ├── HierarchyPanel.cpp/h # 层级面板 -│ ├── InspectorPanel.cpp/h # 检查器面板 -│ ├── SceneViewPanel.cpp/h # 场景视图 -│ ├── GameViewPanel.cpp/h # 游戏视图 -│ ├── ProjectPanel.cpp/h # 项目面板 -│ └── ConsolePanel.cpp/h # 控制台面板 -├── bin/Release/ # 输出目录 -│ ├── XCVolumeRendererUI2.exe # 可执行文件 -│ ├── imgui.ini # ImGui 配置 -│ └── Assets/ -│ └── Models/ -│ └── Character.fbx # 示例模型 -├── build/ # 构建目录 -└── CMakeLists.txt # CMake 配置 -``` +当前不应再把 editor 视为旧式“UI sample”。它已经是引擎工作区的正式入口之一。 -## 构建方法 +## 构建 + +推荐直接在仓库根目录构建,而不是单独进入 `editor/` 目录。 ### 前置要求 - Windows 10/11 -- Visual Studio 2019 或更高版本 +- Visual Studio 2022 / MSVC v143 - CMake 3.15+ +- Vulkan SDK -### 构建步骤 +如果需要启用 Mono 脚本运行时,还需要: + +- .NET SDK +- `参考/Fermion/Fermion/external/mono` 下的 Mono 依赖 + +### 配置 ```bash -cd ui -mkdir build && cd build -cmake .. -cmake --build . --config Release +cmake -S .. -B ..\build -A x64 ``` -### 运行 +更常见的做法是直接在仓库根目录运行: ```bash -# 运行编译好的可执行文件 -.\bin\Release\XCGameEngineUI.exe +cmake -S . -B build -A x64 ``` -## 功能特性 +如果本地暂时没有 Mono,可以先关闭: -### 编辑器面板 - -#### 菜单栏(MenuBar) -- 文件菜单(新建、打开、保存等) -- 编辑菜单(撤销、重做等) -- 视图菜单(面板显示/隐藏) -- 帮助菜单 - -#### 层级面板(Hierarchy Panel) -- 显示场景中所有游戏对象 -- 树形结构展示父子关系 -- 支持对象选择 -- 对象重命名 - -#### 检查器面板(Inspector Panel) -- 显示选中对象的属性 -- 支持组件编辑 -- 变换组件(位置、旋转、缩放) -- 材质组件 - -#### 场景视图(Scene View) -- 3D 场景预览 -- 相机控制(平移、旋转、缩放) -- 对象选择 -- 辅助工具(网格、轴心) - -#### 游戏视图(Game View) -- 游戏运行时的画面预览 -- 分辨率设置 -- 宽高比选择 - -#### 项目面板(Project Panel) -- 项目文件浏览器 -- 资源组织 -- 搜索过滤 - -#### 控制台面板(Console Panel) -- 日志输出 -- 警告和错误显示 -- 日志级别过滤 -- 清空日志 - -### 管理系统 - -#### 日志系统(LogSystem) -- 分级日志(Info、Warning、Error) -- 时间戳 -- 日志持久化 - -#### 项目管理(ProjectManager) -- 项目创建/打开 -- 资源路径管理 - -#### 场景管理(SceneManager) -- 场景加载/保存 -- 对象生命周期管理 - -#### 选择管理(SelectionManager) -- 当前选中对象追踪 -- 多选支持 - -### 主题系统 - -- 深色主题(Dark Theme) -- 可自定义配色方案 - -## 窗口布局 - -默认布局采用经典的 Unity 编辑器风格: - -``` -+----------------------------------------------------------+ -| 菜单栏 | -+----------+------------------------+----------------------+ -| | | | -| 项目 | 场景视图 | 检查器 | -| 面板 | | | -| | | | -+----------+------------------------+----------------------+ -| 层级面板 | 游戏视图 | -| | | -+------------------------------------+----------------------+ -| 控制台面板 | -+----------------------------------------------------------+ +```bash +cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF ``` -## 依赖说明 +### 构建 editor -- ImGui - 跨平台 GUI 库 -- DirectX 12 - 渲染 API -- Windows SDK - 窗口管理 +```bash +cmake --build build --config Debug --target XCEditor +``` -## 扩展开发 +说明: -### 添加新面板 +- target 名称是 `XCEditor` +- 输出文件名仍然是 `XCEngine.exe` +- 输出目录是 `editor/bin//` -1. 在 `panels/` 目录下创建新的面板类 -2. 继承 `Panel` 基类 -3. 实现 `Render()` 方法 -4. 在 `Application` 中注册新面板 +## 运行 -### 添加新组件 +```bash +.\editor\bin\Debug\XCEngine.exe +``` -1. 定义组件类 -2. 在 `GameObject` 中注册组件类型 -3. 在 `InspectorPanel` 中添加属性编辑器 +默认情况下,editor 会自动把仓库内的 `project/` 识别为工程根目录。也可以显式指定工程: + +```bash +.\editor\bin\Debug\XCEngine.exe --project D:\Path\To\Project +``` + +如果需要 C# 脚本类发现与 Inspector 字段编辑,先构建: + +```bash +cmake --build build --config Debug --target xcengine_project_managed_assemblies +``` + +该 target 会把程序集放到: + +- `project/Library/ScriptAssemblies/XCEngine.ScriptCore.dll` +- `project/Library/ScriptAssemblies/GameScripts.dll` +- `project/Library/ScriptAssemblies/mscorlib.dll` + +## 当前目录结构 + +```text +editor/ +├── CMakeLists.txt +├── README.md +├── resources/ +│ └── Icons/ +├── src/ +│ ├── Actions/ # 编辑器动作路由 +│ ├── Commands/ # 命令与实体操作 +│ ├── ComponentEditors/ # Inspector 组件编辑器 +│ ├── Core/ # 应用生命周期、日志、项目根解析、撤销等 +│ ├── Layers/ # EditorLayer 等高层组装 +│ ├── Layout/ +│ ├── Managers/ # SceneManager / ProjectManager +│ ├── panels/ # Hierarchy / Scene / Game / Inspector / Project / Console +│ ├── Platform/ # Win32 host、D3D12 backend 辅助 +│ ├── UI/ # ImGui bridge 与通用 widget +│ ├── Utils/ +│ ├── Viewport/ +│ │ ├── Passes/ # editor viewport overlay pass +│ │ ├── SceneViewportOverlayBuilder.* +│ │ ├── SceneViewportPicker.* +│ │ ├── SceneViewportMoveGizmo.* +│ │ ├── SceneViewportRotateGizmo.* +│ │ ├── SceneViewportScaleGizmo.* +│ │ ├── ViewportHostRenderFlowUtils.h +│ │ └── ViewportHostService.h +│ ├── Application.cpp +│ ├── Application.h +│ ├── EditorApp.rc +│ ├── Theme.cpp +│ ├── Theme.h +│ └── main.cpp +└── bin/ +``` + +## 关键模块 + +### Application + +- `src/Application.cpp` +- `src/Application.h` + +负责: + +- editor 初始化与关闭 +- resource root 设置 +- scripting runtime 初始化 +- ImGui backend 初始化 +- `ViewportHostService` 接线 + +### Project Root + +- `src/Core/ProjectRootResolver.h` +- `src/Utils/ProjectFileUtils.h` + +负责: + +- 自动识别仓库内 `project/` +- 解析 `--project` +- 读写 `Project.xcproject` + +### Viewport + +- `src/Viewport/ViewportHostService.h` +- `src/Viewport/ViewportHostRenderFlowUtils.h` +- `src/Viewport/SceneViewportOverlayBuilder.*` +- `src/Viewport/Passes/SceneViewportEditorOverlayPass.*` + +负责: + +- 组装 scene/game viewport 渲染请求 +- 把 editor overlay 接入 `CameraRenderRequest::overlayPasses` +- object-id picking、outline、overlay pass 等 editor 视口能力 + +### Panels + +当前主要面板: + +- `HierarchyPanel` +- `SceneViewPanel` +- `GameViewPanel` +- `InspectorPanel` +- `ProjectPanel` +- `ConsolePanel` + +### Component Editors + +`ComponentEditors/` 当前不仅负责基础组件,也已经包含 `ScriptComponent` 的 Inspector 编辑入口。 + +## 开发约束 + +- editor 是宿主,不是第二套 renderer。 +- 新的世界空间 overlay / gizmo,不应继续堆到 ImGui world draw 路径。 +- viewport 相关问题优先检查 `engine/Rendering`、`RenderSurface` 与 `ViewportHostService` 的接线,而不是直接在 panel 里复制渲染逻辑。 +- 与项目资源、脚本程序集、`.meta`、`Library` 相关的问题,不要假设 editor 仍处于“无工程状态”的旧结构。 + +## 推荐验证 + +```bash +cmake --build build --config Debug --target editor_tests +cmake --build build --config Debug --target rendering_phase_regression +``` + +如果改动影响脚本类发现或 Inspector 脚本字段编辑,再补: + +```bash +cmake --build build --config Debug --target xcengine_project_managed_assemblies +cmake --build build --config Debug --target scripting_tests +``` diff --git a/editor/caidan.png b/editor/caidan.png deleted file mode 100644 index f0aa5ba9..00000000 Binary files a/editor/caidan.png and /dev/null differ diff --git a/editor/color.png b/editor/color.png deleted file mode 100644 index b466e5db..00000000 Binary files a/editor/color.png and /dev/null differ diff --git a/editor/color_xc.png b/editor/color_xc.png deleted file mode 100644 index 37295f40..00000000 Binary files a/editor/color_xc.png and /dev/null differ diff --git a/editor/project.png b/editor/project.png deleted file mode 100644 index 77a0c04f..00000000 Binary files a/editor/project.png and /dev/null differ diff --git a/editor/rename_color.py b/editor/rename_color.py deleted file mode 100644 index 9999e203..00000000 --- a/editor/rename_color.py +++ /dev/null @@ -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) diff --git a/editor/resources/Icons/camera_gizmo.png b/editor/resources/Icons/camera_gizmo.png new file mode 100644 index 00000000..48a411c1 Binary files /dev/null and b/editor/resources/Icons/camera_gizmo.png differ diff --git a/editor/resources/Icons/main_light_gizmo.png b/editor/resources/Icons/main_light_gizmo.png new file mode 100644 index 00000000..575cc1a4 Binary files /dev/null and b/editor/resources/Icons/main_light_gizmo.png differ diff --git a/editor/resources/Icons/mesh_icon.png b/editor/resources/Icons/mesh_icon.png index 7828df22..547acfd7 100644 Binary files a/editor/resources/Icons/mesh_icon.png and b/editor/resources/Icons/mesh_icon.png differ diff --git a/editor/resources/Icons/mesh_icondd.png b/editor/resources/Icons/mesh_icondd.png new file mode 100644 index 00000000..7828df22 Binary files /dev/null and b/editor/resources/Icons/mesh_icondd.png differ diff --git a/editor/resources/Icons/view_move.png b/editor/resources/Icons/view_move.png new file mode 100644 index 00000000..9ee93557 Binary files /dev/null and b/editor/resources/Icons/view_move.png differ diff --git a/editor/resources/Icons/view_orbit.png b/editor/resources/Icons/view_orbit.png new file mode 100644 index 00000000..b690df89 Binary files /dev/null and b/editor/resources/Icons/view_orbit.png differ diff --git a/editor/src/Actions/EditorActions.h b/editor/src/Actions/EditorActions.h index c4c21884..a3310fd7 100644 --- a/editor/src/Actions/EditorActions.h +++ b/editor/src/Actions/EditorActions.h @@ -28,6 +28,14 @@ inline ActionBinding MakeSaveProjectAction(bool enabled = true) { return MakeAction("Save Project", nullptr, false, enabled); } +inline ActionBinding MakeRebuildScriptsAction(bool enabled = true) { + return MakeAction("Rebuild Script Assemblies", nullptr, false, enabled); +} + +inline ActionBinding MakeMigrateSceneAssetReferencesAction(bool enabled = true) { + return MakeAction("Migrate Scene AssetRefs", nullptr, false, enabled); +} + inline ActionBinding MakeNewSceneAction(bool enabled = true) { return MakeAction("New Scene", "Ctrl+N", false, enabled, true, Shortcut(ImGuiKey_N, true)); } diff --git a/editor/src/Actions/HierarchyActionRouter.h b/editor/src/Actions/HierarchyActionRouter.h index ea99105b..b77af805 100644 --- a/editor/src/Actions/HierarchyActionRouter.h +++ b/editor/src/Actions/HierarchyActionRouter.h @@ -168,6 +168,19 @@ inline void DrawHierarchySortOptionsPopup( } inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Components::GameObject* parent) { + const auto drawPrimitiveCreateAction = [&]( + const ActionBinding& action, + ::XCEngine::Resources::BuiltinPrimitiveType primitiveType) { + const char* label = ::XCEngine::Resources::GetBuiltinPrimitiveDisplayName(primitiveType); + DrawMenuAction(action, [&]() { + TraceHierarchyPopup(std::string("Hierarchy create clicked: ") + label); + auto* created = Commands::CreatePrimitiveEntity(context, primitiveType, parent); + TraceHierarchyPopup( + std::string("Hierarchy create result: ") + label + ", createdId=" + + std::to_string(created ? created->GetID() : 0)); + }); + }; + DrawMenuAction(MakeCreateEmptyEntityAction(), [&]() { TraceHierarchyPopup("Hierarchy create clicked: Empty Object"); auto* created = Commands::CreateEmptyEntity(context, parent, "Create Entity", "GameObject"); @@ -175,6 +188,16 @@ inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Comp std::string("Hierarchy create result: Empty Object, createdId=") + std::to_string(created ? created->GetID() : 0)); }); + + UI::DrawContextSubmenu("3D Object", [&]() { + drawPrimitiveCreateAction(MakeCreateCubeEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Cube); + drawPrimitiveCreateAction(MakeCreateSphereEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Sphere); + drawPrimitiveCreateAction(MakeCreateCapsuleEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Capsule); + drawPrimitiveCreateAction(MakeCreateCylinderEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Cylinder); + drawPrimitiveCreateAction(MakeCreatePlaneEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Plane); + drawPrimitiveCreateAction(MakeCreateQuadEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Quad); + }); + DrawMenuSeparator(); DrawMenuAction(MakeCreateCameraEntityAction(), [&]() { TraceHierarchyPopup("Hierarchy create clicked: Camera"); @@ -190,28 +213,6 @@ inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Comp std::string("Hierarchy create result: Light, createdId=") + std::to_string(created ? created->GetID() : 0)); }); - DrawMenuSeparator(); - DrawMenuAction(MakeCreateCubeEntityAction(), [&]() { - TraceHierarchyPopup("Hierarchy create clicked: Cube"); - auto* created = Commands::CreateEmptyEntity(context, parent, "Create Cube", "Cube"); - TraceHierarchyPopup( - std::string("Hierarchy create result: Cube, createdId=") + - std::to_string(created ? created->GetID() : 0)); - }); - DrawMenuAction(MakeCreateSphereEntityAction(), [&]() { - TraceHierarchyPopup("Hierarchy create clicked: Sphere"); - auto* created = Commands::CreateEmptyEntity(context, parent, "Create Sphere", "Sphere"); - TraceHierarchyPopup( - std::string("Hierarchy create result: Sphere, createdId=") + - std::to_string(created ? created->GetID() : 0)); - }); - DrawMenuAction(MakeCreatePlaneEntityAction(), [&]() { - TraceHierarchyPopup("Hierarchy create clicked: Plane"); - auto* created = Commands::CreateEmptyEntity(context, parent, "Create Plane", "Plane"); - TraceHierarchyPopup( - std::string("Hierarchy create result: Plane, createdId=") + - std::to_string(created ? created->GetID() : 0)); - }); } inline void HandleHierarchyItemContextRequest( @@ -275,7 +276,7 @@ inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::Def backgroundContextMenu.ConsumeOpenRequest("HierarchyContextMenu"); static bool s_lastBackgroundPopupOpen = false; - if (!UI::BeginPopup("HierarchyContextMenu")) { + if (!UI::BeginContextMenu("HierarchyContextMenu")) { if (s_lastBackgroundPopupOpen) { TraceHierarchyPopup("Hierarchy background popup closed"); s_lastBackgroundPopupOpen = false; @@ -289,7 +290,7 @@ inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::Def } DrawHierarchyContextActions(context, nullptr, true); - UI::EndPopup(); + UI::EndContextMenu(); } inline void DrawHierarchyEntityContextPopup( @@ -298,7 +299,7 @@ inline void DrawHierarchyEntityContextPopup( itemContextMenu.ConsumeOpenRequest("HierarchyEntityContextMenu"); static bool s_lastEntityPopupOpen = false; - if (!UI::BeginPopup("HierarchyEntityContextMenu")) { + if (!UI::BeginContextMenu("HierarchyEntityContextMenu")) { if (s_lastEntityPopupOpen) { TraceHierarchyPopup("Hierarchy entity popup closed"); s_lastEntityPopupOpen = false; @@ -314,7 +315,7 @@ inline void DrawHierarchyEntityContextPopup( if (itemContextMenu.HasTarget()) { DrawHierarchyContextActions(context, itemContextMenu.TargetValue()); } - UI::EndPopup(); + UI::EndContextMenu(); if (!ImGui::IsPopupOpen("HierarchyEntityContextMenu") && !itemContextMenu.HasPendingOpenRequest()) { itemContextMenu.Clear(); diff --git a/editor/src/Actions/MainMenuActionRouter.h b/editor/src/Actions/MainMenuActionRouter.h index 18b132e7..3070528b 100644 --- a/editor/src/Actions/MainMenuActionRouter.h +++ b/editor/src/Actions/MainMenuActionRouter.h @@ -34,6 +34,14 @@ inline void ExecuteSaveProject(IEditorContext& context) { Commands::SaveProject(context); } +inline void ExecuteRebuildScriptAssemblies(IEditorContext& context) { + Commands::RebuildScriptAssemblies(context); +} + +inline void ExecuteMigrateSceneAssetReferences(IEditorContext& context) { + Commands::MigrateSceneAssetReferences(context); +} + inline void ExecuteOpenScene(IEditorContext& context) { Commands::OpenSceneWithDialog(context); } @@ -143,6 +151,10 @@ inline void DrawFileMenuActions(IEditorContext& context) { DrawMenuAction(MakeSaveSceneAction(canEditDocuments), [&]() { ExecuteSaveScene(context); }); DrawMenuAction(MakeSaveSceneAsAction(canEditDocuments), [&]() { ExecuteSaveSceneAs(context); }); DrawMenuSeparator(); + DrawMenuAction( + MakeMigrateSceneAssetReferencesAction(Commands::CanMigrateSceneAssetReferences(context)), + [&]() { ExecuteMigrateSceneAssetReferences(context); }); + DrawMenuSeparator(); DrawMenuAction(MakeExitAction(), [&]() { RequestEditorExit(context); }); } @@ -160,6 +172,12 @@ inline void DrawRunMenuActions(IEditorContext& context) { }); } +inline void DrawScriptsMenuActions(IEditorContext& context) { + DrawMenuAction( + MakeRebuildScriptsAction(Commands::CanRebuildScriptAssemblies(context)), + [&]() { ExecuteRebuildScriptAssemblies(context); }); +} + inline void DrawViewMenuActions(IEditorContext& context) { DrawMenuAction(MakeResetLayoutAction(), [&]() { RequestDockLayoutReset(context); }); } @@ -186,6 +204,9 @@ inline void DrawMainMenuBar(IEditorContext& context, UI::DeferredPopupState& abo UI::DrawMenuScope("Run", [&]() { DrawRunMenuActions(context); }); + UI::DrawMenuScope("Scripts", [&]() { + DrawScriptsMenuActions(context); + }); UI::DrawMenuScope("View", [&]() { DrawViewMenuActions(context); }); diff --git a/editor/src/Application.cpp b/editor/src/Application.cpp index ddc8ccbd..5cf8644f 100644 --- a/editor/src/Application.cpp +++ b/editor/src/Application.cpp @@ -6,12 +6,18 @@ #include "Core/EditorContext.h" #include "Core/EditorEvents.h" #include "Core/EventBus.h" +#include "Scripting/EditorScriptAssemblyBuilder.h" #include "UI/BuiltInIcons.h" #include "Platform/Win32Utf8.h" #include "Platform/WindowsProcessDiagnostics.h" #include #include +#include +#ifdef XCENGINE_ENABLE_MONO_SCRIPTING +#include +#endif #include +#include #include namespace XCEngine { @@ -22,6 +28,123 @@ Application& Application::Get() { return instance; } +void Application::InitializeScriptingRuntime(const std::string& projectPath) { + ShutdownScriptingRuntime(); + + const std::filesystem::path assemblyDirectoryPath = + std::filesystem::path(Platform::Utf8ToWide(projectPath)) / L"Library" / L"ScriptAssemblies"; + m_scriptRuntimeStatus.assemblyDirectory = Platform::WideToUtf8(assemblyDirectoryPath.wstring()); + +#ifdef XCENGINE_ENABLE_MONO_SCRIPTING + namespace fs = std::filesystem; + + auto& logger = Debug::Logger::Get(); + const fs::path assemblyDirectory = assemblyDirectoryPath; + m_scriptRuntimeStatus.backendEnabled = true; + + ::XCEngine::Scripting::MonoScriptRuntime::Settings settings; + settings.assemblyDirectory = assemblyDirectory; + settings.corlibDirectory = assemblyDirectory; + settings.coreAssemblyPath = assemblyDirectory / L"XCEngine.ScriptCore.dll"; + settings.appAssemblyPath = assemblyDirectory / L"GameScripts.dll"; + + std::error_code ec; + const bool hasCoreAssembly = fs::exists(settings.coreAssemblyPath, ec); + ec.clear(); + const bool hasAppAssembly = fs::exists(settings.appAssemblyPath, ec); + ec.clear(); + const bool hasCorlibAssembly = fs::exists(assemblyDirectory / L"mscorlib.dll", ec); + m_scriptRuntimeStatus.assembliesFound = hasCoreAssembly && hasAppAssembly && hasCorlibAssembly; + + if (!hasCoreAssembly || !hasAppAssembly || !hasCorlibAssembly) { + m_scriptRuntimeStatus.statusMessage = + "Script assemblies were not found in " + Platform::WideToUtf8(assemblyDirectory.wstring()) + + ". Script class discovery is disabled until the managed assemblies are built."; + logger.Warning(Debug::LogCategory::Scripting, m_scriptRuntimeStatus.statusMessage.c_str()); + ::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr); + return; + } + + auto runtime = std::make_unique<::XCEngine::Scripting::MonoScriptRuntime>(settings); + if (!runtime->Initialize()) { + m_scriptRuntimeStatus.statusMessage = + "Failed to initialize editor script runtime: " + runtime->GetLastError(); + logger.Warning(Debug::LogCategory::Scripting, m_scriptRuntimeStatus.statusMessage.c_str()); + ::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr); + return; + } + + ::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(runtime.get()); + m_scriptRuntimeStatus.runtimeLoaded = true; + m_scriptRuntime = std::move(runtime); + logger.Info(Debug::LogCategory::Scripting, "Editor script runtime initialized."); +#else + (void)projectPath; + m_scriptRuntimeStatus.backendEnabled = false; + m_scriptRuntimeStatus.statusMessage = "This editor build does not include Mono scripting support."; + ::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr); +#endif +} + +void Application::ShutdownScriptingRuntime() { + ::XCEngine::Scripting::ScriptEngine::Get().OnRuntimeStop(); + ::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr); + +#ifdef XCENGINE_ENABLE_MONO_SCRIPTING + m_scriptRuntime.reset(); +#endif + + m_scriptRuntimeStatus = {}; +} + +bool Application::ReloadScriptingRuntime() { + if (!m_editorContext) { + return false; + } + + const std::string& projectPath = m_editorContext->GetProjectPath(); + if (projectPath.empty()) { + return false; + } + + InitializeScriptingRuntime(projectPath); + return m_scriptRuntimeStatus.runtimeLoaded; +} + +bool Application::RebuildScriptingAssemblies() { + if (!m_editorContext) { + return false; + } + + const std::string& projectPath = m_editorContext->GetProjectPath(); + if (projectPath.empty()) { + return false; + } + +#ifdef XCENGINE_ENABLE_MONO_SCRIPTING + auto& logger = Debug::Logger::Get(); + logger.Info(Debug::LogCategory::Scripting, "Rebuilding project script assemblies..."); + + // Release the currently loaded project assembly before invoking the compiler. + // Otherwise GameScripts.dll can remain locked by the active Mono app domain. + ShutdownScriptingRuntime(); + + const ::XCEngine::Editor::Scripting::EditorScriptAssemblyBuildResult buildResult = + ::XCEngine::Editor::Scripting::EditorScriptAssemblyBuilder::RebuildProjectAssemblies(projectPath); + if (!buildResult.succeeded) { + m_scriptRuntimeStatus.statusMessage = buildResult.message; + logger.Error(Debug::LogCategory::Scripting, buildResult.message.c_str()); + return false; + } + + logger.Info(Debug::LogCategory::Scripting, buildResult.message.c_str()); + return ReloadScriptingRuntime(); +#else + m_scriptRuntimeStatus.statusMessage = "This editor build does not include Mono scripting support."; + return false; +#endif +} + bool Application::InitializeWindowRenderer(HWND hwnd) { RECT clientRect = {}; if (!GetClientRect(hwnd, &clientRect)) { @@ -150,6 +273,7 @@ bool Application::Initialize(HWND hwnd) { logger.Info(Debug::LogCategory::General, "Initializing editor context..."); InitializeEditorContext(projectRoot); + InitializeScriptingRuntime(projectRoot); logger.Info(Debug::LogCategory::General, "Initializing ImGui backend..."); InitializeImGui(hwnd); logger.Info(Debug::LogCategory::General, "Attaching editor layer..."); @@ -171,6 +295,7 @@ void Application::Shutdown() { UI::ShutdownBuiltInIcons(); m_imguiBackend.Shutdown(); m_imguiSession.Shutdown(); + ShutdownScriptingRuntime(); ShutdownEditorContext(); if (m_resourceManagerInitialized) { ::XCEngine::Resources::ResourceManager::Get().Shutdown(); @@ -230,6 +355,7 @@ bool Application::SwitchProject(const std::string& projectPath) { logger.Info(Debug::LogCategory::General, infoMessage.c_str()); ::XCEngine::Resources::ResourceManager::Get().SetResourceRoot(projectPath.c_str()); + InitializeScriptingRuntime(projectPath); m_lastWindowTitle.clear(); UpdateWindowTitle(); diff --git a/editor/src/Application.h b/editor/src/Application.h index 4747beba..4490ea92 100644 --- a/editor/src/Application.h +++ b/editor/src/Application.h @@ -1,6 +1,7 @@ #pragma once #include "Platform/D3D12WindowRenderer.h" +#include "Scripting/EditorScriptRuntimeStatus.h" #include "UI/ImGuiBackendBridge.h" #include "UI/ImGuiSession.h" #include "Viewport/ViewportHostService.h" @@ -19,6 +20,12 @@ class RHIDevice; class RHISwapChain; } // namespace RHI +namespace Scripting { +#ifdef XCENGINE_ENABLE_MONO_SCRIPTING +class MonoScriptRuntime; +#endif +} // namespace Scripting + namespace Editor { class EditorLayer; @@ -33,6 +40,8 @@ public: void Render(); void OnResize(int width, int height); bool SwitchProject(const std::string& projectPath); + bool ReloadScriptingRuntime(); + bool RebuildScriptingAssemblies(); void SaveProjectState(); Rendering::RenderContext GetMainRenderContext() const { return m_windowRenderer.GetRenderContext(); } RHI::RHIDevice* GetMainRHIDevice() const { return m_windowRenderer.GetRHIDevice(); } @@ -42,6 +51,7 @@ public: HWND GetWindowHandle() const { return m_hwnd; } IEditorContext& GetEditorContext() const { return *m_editorContext; } + const EditorScriptRuntimeStatus& GetScriptRuntimeStatus() const { return m_scriptRuntimeStatus; } private: Application() = default; @@ -52,6 +62,8 @@ private: void AttachEditorLayer(); void DetachEditorLayer(); void ShutdownEditorContext(); + void InitializeScriptingRuntime(const std::string& projectPath); + void ShutdownScriptingRuntime(); void RenderEditorFrame(); void UpdateWindowTitle(); @@ -70,6 +82,10 @@ private: bool m_hasLastFrameTime = false; bool m_renderReady = false; bool m_resourceManagerInitialized = false; + EditorScriptRuntimeStatus m_scriptRuntimeStatus; +#ifdef XCENGINE_ENABLE_MONO_SCRIPTING + std::unique_ptr<::XCEngine::Scripting::MonoScriptRuntime> m_scriptRuntime; +#endif }; } diff --git a/editor/src/Commands/EntityCommands.h b/editor/src/Commands/EntityCommands.h index a749ec40..179b648d 100644 --- a/editor/src/Commands/EntityCommands.h +++ b/editor/src/Commands/EntityCommands.h @@ -7,8 +7,11 @@ #include #include +#include +#include #include #include +#include #include @@ -80,6 +83,31 @@ inline ::XCEngine::Components::GameObject* CreateLightEntity( }); } +inline ::XCEngine::Components::GameObject* CreatePrimitiveEntity( + IEditorContext& context, + ::XCEngine::Resources::BuiltinPrimitiveType primitiveType, + ::XCEngine::Components::GameObject* parent = nullptr) { + const char* primitiveName = ::XCEngine::Resources::GetBuiltinPrimitiveDisplayName(primitiveType); + return CreateEntity( + context, + std::string("Create ") + primitiveName, + primitiveName, + parent, + [primitiveType](::XCEngine::Components::GameObject& entity, ISceneManager&) { + auto* meshFilter = entity.GetComponent<::XCEngine::Components::MeshFilterComponent>(); + if (meshFilter == nullptr) { + meshFilter = entity.AddComponent<::XCEngine::Components::MeshFilterComponent>(); + } + meshFilter->SetMeshPath(::XCEngine::Resources::GetBuiltinPrimitiveMeshPath(primitiveType).CStr()); + + auto* meshRenderer = entity.GetComponent<::XCEngine::Components::MeshRendererComponent>(); + if (meshRenderer == nullptr) { + meshRenderer = entity.AddComponent<::XCEngine::Components::MeshRendererComponent>(); + } + meshRenderer->SetMaterialPath(0, ::XCEngine::Resources::GetBuiltinDefaultPrimitiveMaterialPath().CStr()); + }); +} + inline bool RenameEntity( IEditorContext& context, ::XCEngine::Components::GameObject::ID entityId, diff --git a/editor/src/Commands/ProjectCommands.h b/editor/src/Commands/ProjectCommands.h index 55f92022..094f2c1c 100644 --- a/editor/src/Commands/ProjectCommands.h +++ b/editor/src/Commands/ProjectCommands.h @@ -314,6 +314,35 @@ inline bool SaveProject(IEditorContext& context) { return SaveProjectDescriptor(context); } +inline bool CanRebuildScriptAssemblies(const IEditorContext& context) { + return IsProjectDocumentEditingAllowed(context) && !context.GetProjectPath().empty(); +} + +inline bool RebuildScriptAssemblies(IEditorContext& context) { + if (!CanRebuildScriptAssemblies(context)) { + return false; + } + + const bool rebuilt = Application::Get().RebuildScriptingAssemblies(); + if (rebuilt) { + context.GetProjectManager().RefreshCurrentFolder(); + } + + return rebuilt; +} + +inline bool CanMigrateSceneAssetReferences(const IEditorContext& context) { + return IsProjectDocumentEditingAllowed(context) && !context.GetProjectPath().empty(); +} + +inline IProjectManager::SceneAssetReferenceMigrationReport MigrateSceneAssetReferences(IEditorContext& context) { + if (!CanMigrateSceneAssetReferences(context)) { + return {}; + } + + return context.GetProjectManager().MigrateSceneAssetReferences(); +} + inline bool SwitchProject(IEditorContext& context, const std::string& projectPath) { if (!IsProjectDocumentEditingAllowed(context)) { return false; diff --git a/editor/src/ComponentEditors/AssetReferenceEditorUtils.h b/editor/src/ComponentEditors/AssetReferenceEditorUtils.h index c2287c7b..26ccf0ee 100644 --- a/editor/src/ComponentEditors/AssetReferenceEditorUtils.h +++ b/editor/src/ComponentEditors/AssetReferenceEditorUtils.h @@ -30,7 +30,7 @@ inline AssetReferenceInteraction DrawAssetReferenceProperty( pickerOptions.assetsTabLabel = "Assets"; pickerOptions.sceneTabLabel = "Scene"; pickerOptions.showAssetsTab = true; - pickerOptions.showSceneTab = true; + pickerOptions.showSceneTab = false; pickerOptions.supportedAssetExtensions = supportedExtensions; UI::DrawPropertyRow(label, UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) { diff --git a/editor/src/ComponentEditors/ComponentEditorRegistry.cpp b/editor/src/ComponentEditors/ComponentEditorRegistry.cpp index ae0a4265..e0e15cdc 100644 --- a/editor/src/ComponentEditors/ComponentEditorRegistry.cpp +++ b/editor/src/ComponentEditors/ComponentEditorRegistry.cpp @@ -4,6 +4,7 @@ #include "ComponentEditors/LightComponentEditor.h" #include "ComponentEditors/MeshFilterComponentEditor.h" #include "ComponentEditors/MeshRendererComponentEditor.h" +#include "ComponentEditors/ScriptComponentEditor.h" #include "ComponentEditors/TransformComponentEditor.h" namespace XCEngine { @@ -20,6 +21,7 @@ ComponentEditorRegistry::ComponentEditorRegistry() { RegisterEditor(std::make_unique()); RegisterEditor(std::make_unique()); RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); } void ComponentEditorRegistry::RegisterEditor(std::unique_ptr editor) { diff --git a/editor/src/ComponentEditors/ScriptComponentEditor.h b/editor/src/ComponentEditors/ScriptComponentEditor.h new file mode 100644 index 00000000..faa378c2 --- /dev/null +++ b/editor/src/ComponentEditors/ScriptComponentEditor.h @@ -0,0 +1,515 @@ +#pragma once + +#include "Application.h" +#include "IComponentEditor.h" +#include "ScriptComponentEditorUtils.h" +#include "Core/IUndoManager.h" +#include "UI/UI.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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 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(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(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(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(field.value); + const bool widgetChanged = UI::DrawPropertyInt(field.metadata.name.c_str(), value, 1); + return widgetChanged && + ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(static_cast(value)), undoManager); + } + case ScriptFieldType::UInt64: { + uint64_t value = std::get(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(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(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> m_stringFieldStates; +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/ComponentEditors/ScriptComponentEditorUtils.h b/editor/src/ComponentEditors/ScriptComponentEditorUtils.h new file mode 100644 index 00000000..e4f39ea6 --- /dev/null +++ b/editor/src/ComponentEditors/ScriptComponentEditorUtils.h @@ -0,0 +1,125 @@ +#pragma once + +#include "Scripting/EditorScriptRuntimeStatus.h" + +#include +#include +#include +#include + +#include +#include + +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 diff --git a/editor/src/Core/EditorEvents.h b/editor/src/Core/EditorEvents.h index 597a0d6a..f1658b2d 100644 --- a/editor/src/Core/EditorEvents.h +++ b/editor/src/Core/EditorEvents.h @@ -2,6 +2,11 @@ #include "EditorRuntimeMode.h" +#include +#include + +#include +#include #include #include @@ -67,6 +72,19 @@ struct PlayModeResumeRequestedEvent { struct PlayModeStepRequestedEvent { }; +struct GameViewInputFrameEvent { + static constexpr size_t KeyStateCount = 256u; + static constexpr size_t MouseButtonStateCount = 5u; + + bool hovered = false; + bool focused = false; + Math::Vector2 mousePosition = Math::Vector2::Zero(); + Math::Vector2 mouseDelta = Math::Vector2::Zero(); + float mouseWheel = 0.0f; + std::array keyDown = {}; + std::array mouseButtonDown = {}; +}; + struct EditorModeChangedEvent { EditorRuntimeMode oldMode = EditorRuntimeMode::Edit; EditorRuntimeMode newMode = EditorRuntimeMode::Edit; diff --git a/editor/src/Core/EventBus.h b/editor/src/Core/EventBus.h index 89f7d2a4..0a5ae9bc 100644 --- a/editor/src/Core/EventBus.h +++ b/editor/src/Core/EventBus.h @@ -82,13 +82,20 @@ public: void Publish(const T& event) { static_assert(sizeof(T) > 0, "Event type must be defined"); uint32_t typeId = EventTypeId::Get(); - - std::shared_lock lock(m_mutex); - auto it = m_handlers.find(typeId); - if (it != m_handlers.end()) { - for (const auto& entry : it->second) { - entry.handler(&event); + + std::vector handlers; + { + std::shared_lock lock(m_mutex); + auto it = m_handlers.find(typeId); + if (it == m_handlers.end()) { + return; } + + handlers = it->second; + } + + for (const auto& entry : handlers) { + entry.handler(&event); } } diff --git a/editor/src/Core/IProjectManager.h b/editor/src/Core/IProjectManager.h index 7e192541..b4b63e7c 100644 --- a/editor/src/Core/IProjectManager.h +++ b/editor/src/Core/IProjectManager.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -10,6 +11,13 @@ namespace Editor { class IProjectManager { public: + struct SceneAssetReferenceMigrationReport { + size_t scannedSceneCount = 0; + size_t migratedSceneCount = 0; + size_t unchangedSceneCount = 0; + size_t failedSceneCount = 0; + }; + virtual ~IProjectManager() = default; virtual const std::vector& GetCurrentItems() const = 0; @@ -39,6 +47,7 @@ public: virtual bool DeleteItem(const std::string& fullPath) = 0; virtual bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) = 0; virtual bool RenameItem(const std::string& sourceFullPath, const std::string& newName) = 0; + virtual SceneAssetReferenceMigrationReport MigrateSceneAssetReferences() = 0; virtual const std::string& GetProjectPath() const = 0; }; diff --git a/editor/src/Core/PlaySessionController.cpp b/editor/src/Core/PlaySessionController.cpp index 7213a788..4bc97ed0 100644 --- a/editor/src/Core/PlaySessionController.cpp +++ b/editor/src/Core/PlaySessionController.cpp @@ -6,9 +6,25 @@ #include "Core/ISceneManager.h" #include "Core/IUndoManager.h" +#include +#include + namespace XCEngine { namespace Editor { +namespace { + +bool IsModifierKeyDown(const GameViewInputFrameEvent& input, XCEngine::Input::KeyCode key) { + const size_t index = static_cast(key); + return index < input.keyDown.size() && input.keyDown[index]; +} + +bool IsGameViewInputActive(const GameViewInputFrameEvent& input) { + return input.hovered || input.focused; +} + +} // namespace + void PlaySessionController::Attach(IEditorContext& context) { if (m_playStartRequestedHandlerId == 0) { m_playStartRequestedHandlerId = context.GetEventBus().Subscribe( @@ -44,6 +60,14 @@ void PlaySessionController::Attach(IEditorContext& context) { StepPlay(context); }); } + + if (m_gameViewInputFrameHandlerId == 0) { + m_gameViewInputFrameHandlerId = context.GetEventBus().Subscribe( + [this](const GameViewInputFrameEvent& event) { + m_pendingGameViewInput = event; + m_hasPendingGameViewInput = true; + }); + } } void PlaySessionController::Detach(IEditorContext& context) { @@ -73,6 +97,13 @@ void PlaySessionController::Detach(IEditorContext& context) { context.GetEventBus().Unsubscribe(m_playStepRequestedHandlerId); m_playStepRequestedHandlerId = 0; } + + if (m_gameViewInputFrameHandlerId != 0) { + context.GetEventBus().Unsubscribe(m_gameViewInputFrameHandlerId); + m_gameViewInputFrameHandlerId = 0; + } + + ResetRuntimeInputBridge(); } void PlaySessionController::Update(IEditorContext& context, float deltaTime) { @@ -81,6 +112,7 @@ void PlaySessionController::Update(IEditorContext& context, float deltaTime) { return; } + ApplyGameViewInputFrame(deltaTime); m_runtimeLoop.Tick(deltaTime); } @@ -104,6 +136,10 @@ bool PlaySessionController::StartPlay(IEditorContext& context) { } sceneManager.SetSceneDocumentDirtyTrackingEnabled(false); + XCEngine::Scripting::ScriptEngine::Get().SetRuntimeFixedDeltaTime(m_runtimeLoop.GetSettings().fixedDeltaTime); + ResetRuntimeInputBridge(); + XCEngine::Input::InputManager::Get().Shutdown(); + XCEngine::Input::InputManager::Get().Initialize(nullptr); m_runtimeLoop.Start(sceneManager.GetScene()); context.GetUndoManager().ClearHistory(); context.SetRuntimeMode(EditorRuntimeMode::Play); @@ -118,6 +154,8 @@ bool PlaySessionController::StopPlay(IEditorContext& context) { auto& sceneManager = context.GetSceneManager(); m_runtimeLoop.Stop(); + ResetRuntimeInputBridge(); + XCEngine::Input::InputManager::Get().Shutdown(); sceneManager.SetSceneDocumentDirtyTrackingEnabled(true); if (!sceneManager.RestoreSceneSnapshot(m_editorSnapshot)) { @@ -162,5 +200,83 @@ bool PlaySessionController::StepPlay(IEditorContext& context) { return true; } +void PlaySessionController::ResetRuntimeInputBridge() { + m_pendingGameViewInput = {}; + m_appliedGameViewInput = {}; + m_hasPendingGameViewInput = false; +} + +void PlaySessionController::ApplyGameViewInputFrame(float deltaTime) { + using XCEngine::Input::InputManager; + using XCEngine::Input::KeyCode; + using XCEngine::Input::MouseButton; + + InputManager& inputManager = InputManager::Get(); + inputManager.Update(deltaTime); + + const GameViewInputFrameEvent input = m_hasPendingGameViewInput + ? m_pendingGameViewInput + : GameViewInputFrameEvent{}; + m_hasPendingGameViewInput = false; + + const bool inputActive = IsGameViewInputActive(input); + const bool alt = inputActive && + (IsModifierKeyDown(input, KeyCode::LeftAlt) || IsModifierKeyDown(input, KeyCode::RightAlt)); + const bool ctrl = inputActive && + (IsModifierKeyDown(input, KeyCode::LeftCtrl) || IsModifierKeyDown(input, KeyCode::RightCtrl)); + const bool shift = inputActive && + (IsModifierKeyDown(input, KeyCode::LeftShift) || IsModifierKeyDown(input, KeyCode::RightShift)); + + for (size_t index = 0; index < input.keyDown.size(); ++index) { + const bool wasDown = m_appliedGameViewInput.keyDown[index]; + const bool isDown = inputActive && input.keyDown[index]; + if (wasDown == isDown) { + continue; + } + + const KeyCode key = static_cast(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(index), + isDown, + static_cast(input.mousePosition.x), + static_cast(input.mousePosition.y)); + } + + if (inputActive && + (input.mousePosition != m_appliedGameViewInput.mousePosition || input.mouseDelta != XCEngine::Math::Vector2::Zero())) { + inputManager.ProcessMouseMove( + static_cast(input.mousePosition.x), + static_cast(input.mousePosition.y), + static_cast(input.mouseDelta.x), + static_cast(input.mouseDelta.y)); + } + + if (inputActive && input.mouseWheel != 0.0f) { + inputManager.ProcessMouseWheel( + input.mouseWheel, + static_cast(input.mousePosition.x), + static_cast(input.mousePosition.y)); + } + + m_appliedGameViewInput = {}; + if (inputActive) { + m_appliedGameViewInput = input; + } +} + } // namespace Editor } // namespace XCEngine diff --git a/editor/src/Core/PlaySessionController.h b/editor/src/Core/PlaySessionController.h index dbf3caf1..b986b7ca 100644 --- a/editor/src/Core/PlaySessionController.h +++ b/editor/src/Core/PlaySessionController.h @@ -3,8 +3,11 @@ #include "EditorRuntimeMode.h" #include "SceneSnapshot.h" +#include "Core/EditorEvents.h" + #include +#include #include namespace XCEngine { @@ -26,12 +29,19 @@ public: bool StepPlay(IEditorContext& context); private: + void ResetRuntimeInputBridge(); + void ApplyGameViewInputFrame(float deltaTime); + uint64_t m_playStartRequestedHandlerId = 0; uint64_t m_playStopRequestedHandlerId = 0; uint64_t m_playPauseRequestedHandlerId = 0; uint64_t m_playResumeRequestedHandlerId = 0; uint64_t m_playStepRequestedHandlerId = 0; + uint64_t m_gameViewInputFrameHandlerId = 0; SceneSnapshot m_editorSnapshot = {}; + GameViewInputFrameEvent m_pendingGameViewInput = {}; + GameViewInputFrameEvent m_appliedGameViewInput = {}; + bool m_hasPendingGameViewInput = false; XCEngine::Components::RuntimeLoop m_runtimeLoop; }; diff --git a/editor/src/Managers/ProjectManager.cpp b/editor/src/Managers/ProjectManager.cpp index f96877a3..22d2da75 100644 --- a/editor/src/Managers/ProjectManager.cpp +++ b/editor/src/Managers/ProjectManager.cpp @@ -1,9 +1,14 @@ #include "ProjectManager.h" +#include +#include +#include + #include #include #include #include #include +#include #include #include #include @@ -212,6 +217,27 @@ bool CanPreviewImageAssetExtension(std::wstring_view extension) { }); } +bool IsSceneAssetFile(const fs::path& path) { + if (!fs::is_regular_file(path)) { + return false; + } + + std::wstring extension = path.extension().wstring(); + std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower); + return extension == L".xc"; +} + +std::string ReadFileText(const fs::path& path) { + std::ifstream input(path, std::ios::binary); + if (!input.is_open()) { + return {}; + } + + std::ostringstream stream; + stream << input.rdbuf(); + return stream.str(); +} + } // namespace const std::vector& ProjectManager::GetCurrentItems() const { @@ -496,6 +522,89 @@ bool ProjectManager::RenameItem(const std::string& sourceFullPath, const std::st } } +IProjectManager::SceneAssetReferenceMigrationReport ProjectManager::MigrateSceneAssetReferences() { + IProjectManager::SceneAssetReferenceMigrationReport report; + if (m_projectPath.empty()) { + return report; + } + + const fs::path assetsPath = fs::path(Utf8PathToWstring(m_projectPath)) / L"Assets"; + if (!fs::exists(assetsPath) || !fs::is_directory(assetsPath)) { + return report; + } + + auto& logger = ::XCEngine::Debug::Logger::Get(); + ::XCEngine::Resources::ResourceManager& resourceManager = ::XCEngine::Resources::ResourceManager::Get(); + resourceManager.Initialize(); + + const std::string previousRoot = resourceManager.GetResourceRoot().CStr(); + const bool restoreResourceRoot = previousRoot != m_projectPath; + if (restoreResourceRoot) { + resourceManager.SetResourceRoot(m_projectPath.c_str()); + } + + try { + for (const fs::directory_entry& entry : fs::recursive_directory_iterator(assetsPath)) { + if (!IsSceneAssetFile(entry.path())) { + continue; + } + + ++report.scannedSceneCount; + const fs::path scenePath = entry.path(); + const std::string scenePathUtf8 = WstringPathToUtf8(scenePath.wstring()); + + try { + const std::string before = ReadFileText(scenePath); + ::XCEngine::Components::Scene scene; + { + ::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope(resourceManager); + scene.Load(scenePathUtf8); + } + + scene.Save(scenePathUtf8); + const std::string after = ReadFileText(scenePath); + if (after != before) { + ++report.migratedSceneCount; + } else { + ++report.unchangedSceneCount; + } + } catch (const std::exception& exception) { + ++report.failedSceneCount; + logger.Error( + ::XCEngine::Debug::LogCategory::FileSystem, + ("Failed to migrate scene asset references: " + scenePathUtf8 + " - " + exception.what()).c_str()); + } catch (...) { + ++report.failedSceneCount; + logger.Error( + ::XCEngine::Debug::LogCategory::FileSystem, + ("Failed to migrate scene asset references: " + scenePathUtf8 + " - unknown error").c_str()); + } + } + } catch (const std::exception& exception) { + logger.Error( + ::XCEngine::Debug::LogCategory::FileSystem, + ("Scene asset reference migration aborted: " + std::string(exception.what())).c_str()); + } catch (...) { + logger.Error( + ::XCEngine::Debug::LogCategory::FileSystem, + "Scene asset reference migration aborted: unknown error"); + } + + if (restoreResourceRoot) { + resourceManager.SetResourceRoot(previousRoot.c_str()); + } + + logger.Info( + ::XCEngine::Debug::LogCategory::FileSystem, + ("Scene asset reference migration finished. scanned=" + std::to_string(report.scannedSceneCount) + + " migrated=" + std::to_string(report.migratedSceneCount) + + " unchanged=" + std::to_string(report.unchangedSceneCount) + + " failed=" + std::to_string(report.failedSceneCount)).c_str()); + + RefreshCurrentFolder(); + return report; +} + AssetItemPtr ProjectManager::FindCurrentItemByPath(const std::string& fullPath) const { const int index = FindCurrentItemIndex(fullPath); if (index < 0) { diff --git a/editor/src/Managers/ProjectManager.h b/editor/src/Managers/ProjectManager.h index 1602cd08..505013e1 100644 --- a/editor/src/Managers/ProjectManager.h +++ b/editor/src/Managers/ProjectManager.h @@ -38,6 +38,7 @@ public: bool DeleteItem(const std::string& fullPath) override; bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) override; bool RenameItem(const std::string& sourceFullPath, const std::string& newName) override; + SceneAssetReferenceMigrationReport MigrateSceneAssetReferences() override; const std::string& GetProjectPath() const override { return m_projectPath; } diff --git a/editor/src/Scripting/EditorScriptAssemblyBuilder.cpp b/editor/src/Scripting/EditorScriptAssemblyBuilder.cpp new file mode 100644 index 00000000..11b38d53 --- /dev/null +++ b/editor/src/Scripting/EditorScriptAssemblyBuilder.cpp @@ -0,0 +1,372 @@ +#include "Scripting/EditorScriptAssemblyBuilder.h" + +#include "Platform/Win32Utf8.h" +#include "Scripting/EditorScriptAssemblyBuilderUtils.h" + +#include + +#include +#include +#include + +#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(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 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& referencePaths, + const std::vector& 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& referencePaths, + const std::vector& 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 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 scriptCoreSources = CollectCSharpSourceFiles(scriptCoreSourceRoot); + if (scriptCoreSources.empty()) { + return BuildFailure("No ScriptCore C# source files were found under: " + + ScriptBuilderPathToUtf8(scriptCoreSourceRoot)); + } + + std::vector 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 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 diff --git a/editor/src/Scripting/EditorScriptAssemblyBuilder.h b/editor/src/Scripting/EditorScriptAssemblyBuilder.h new file mode 100644 index 00000000..4398e91b --- /dev/null +++ b/editor/src/Scripting/EditorScriptAssemblyBuilder.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +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 diff --git a/editor/src/Scripting/EditorScriptAssemblyBuilderUtils.h b/editor/src/Scripting/EditorScriptAssemblyBuilderUtils.h new file mode 100644 index 00000000..557ba483 --- /dev/null +++ b/editor/src/Scripting/EditorScriptAssemblyBuilderUtils.h @@ -0,0 +1,106 @@ +#pragma once + +#include "Platform/Win32Utf8.h" + +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace Scripting { + +inline std::string ScriptBuilderPathToUtf8(const std::filesystem::path& path) { + return Platform::WideToUtf8(path.wstring()); +} + +inline std::vector CollectCSharpSourceFiles(const std::filesystem::path& root) { + std::vector 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& 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 diff --git a/editor/src/Scripting/EditorScriptRuntimeStatus.h b/editor/src/Scripting/EditorScriptRuntimeStatus.h new file mode 100644 index 00000000..39152903 --- /dev/null +++ b/editor/src/Scripting/EditorScriptRuntimeStatus.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +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 diff --git a/editor/src/UI/Widgets.h b/editor/src/UI/Widgets.h index dcb5c903..77c79125 100644 --- a/editor/src/UI/Widgets.h +++ b/editor/src/UI/Widgets.h @@ -47,6 +47,20 @@ struct AssetTileOptions { bool drawLabel = true; }; +inline ImVec2 ComputeAssetTileSize( + const char* label, + const AssetTileOptions& options = AssetTileOptions()) { + const ImVec2 textSize = ImGui::CalcTextSize(label ? label : ""); + ImVec2 tileSize = options.size; + tileSize.x = std::max(tileSize.x, options.iconSize.x + AssetTileTextPadding().x * 2.0f); + tileSize.y = std::max( + tileSize.y, + options.iconOffset.y + + options.iconSize.y + + (options.drawLabel ? AssetTileIconTextGap() + textSize.y + AssetTileTextPadding().y : 0.0f)); + return tileSize; +} + enum class DialogActionResult { None, Primary, @@ -428,16 +442,8 @@ inline AssetTileResult DrawAssetTile( bool dimmed, DrawIconFn&& drawIcon, const AssetTileOptions& options = AssetTileOptions()) { - const ImVec2 textSize = ImGui::CalcTextSize(label); - ImVec2 tileSize = options.size; - tileSize.x = std::max(tileSize.x, options.iconSize.x + AssetTileTextPadding().x * 2.0f); - tileSize.y = std::max( - tileSize.y, - options.iconOffset.y + - options.iconSize.y + - AssetTileIconTextGap() + - textSize.y + - AssetTileTextPadding().y); + const ImVec2 textSize = ImGui::CalcTextSize(label ? label : ""); + const ImVec2 tileSize = ComputeAssetTileSize(label, options); ImGui::InvisibleButton("##AssetBtn", tileSize); const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); @@ -470,13 +476,13 @@ inline AssetTileResult DrawAssetTile( drawIcon(drawList, iconMin, iconMax); const ImVec2 textPadding = AssetTileTextPadding(); - const float labelHeight = ImGui::GetFrameHeight(); + const float labelHeight = (std::max)(ImGui::GetFrameHeight(), textSize.y); const ImVec2 labelMin(min.x + textPadding.x, max.y - labelHeight - textPadding.y * 0.5f); const ImVec2 labelMax(max.x - textPadding.x, labelMin.y + labelHeight); if (options.drawLabel) { const float textAreaWidth = labelMax.x - labelMin.x; const float centeredTextX = labelMin.x + std::max(0.0f, (textAreaWidth - textSize.x) * 0.5f); - const float textY = labelMin.y + (labelHeight - textSize.y) * 0.5f; + const float textY = labelMin.y + (std::max)(0.0f, (labelHeight - textSize.y) * 0.5f); ImGui::PushClipRect(labelMin, labelMax, true); drawList->AddText(ImVec2(centeredTextX, textY), ImGui::GetColorU32(AssetTileTextColor(selected)), label); ImGui::PopClipRect(); diff --git a/editor/src/Viewport/IViewportHostService.h b/editor/src/Viewport/IViewportHostService.h index ab427402..2b884a44 100644 --- a/editor/src/Viewport/IViewportHostService.h +++ b/editor/src/Viewport/IViewportHostService.h @@ -15,6 +15,8 @@ struct RenderContext; namespace Editor { class IEditorContext; +struct SceneViewportOverlayFrameData; +struct SceneViewportTransformGizmoHandleBuildInputs; enum class EditorViewportKind { Scene, @@ -83,6 +85,14 @@ public: const ImVec2& viewportMousePosition) = 0; virtual void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis axis) = 0; virtual SceneViewportOverlayData GetSceneViewOverlayData() const = 0; + virtual const SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext& context) = 0; + virtual const SceneViewportOverlayFrameData& GetSceneViewInteractionOverlayFrameData( + IEditorContext& context, + const SceneViewportOverlayData& overlay, + const SceneViewportTransformGizmoHandleBuildInputs& inputs) = 0; + virtual void SetSceneViewTransientTransformGizmoOverlayData( + const SceneViewportOverlayData& overlay, + const SceneViewportTransformGizmoHandleBuildInputs& inputs) = 0; virtual void RenderRequestedViewports( IEditorContext& context, const Rendering::RenderContext& renderContext) = 0; diff --git a/editor/src/Viewport/Passes/SceneViewportEditorOverlayPass.cpp b/editor/src/Viewport/Passes/SceneViewportEditorOverlayPass.cpp new file mode 100644 index 00000000..a6fd2465 --- /dev/null +++ b/editor/src/Viewport/Passes/SceneViewportEditorOverlayPass.cpp @@ -0,0 +1,1255 @@ +#include "Passes/SceneViewportEditorOverlayPass.h" + +#include "Platform/Win32Utf8.h" +#include "Viewport/SceneViewportMath.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace XCEngine { +namespace Editor { + +namespace { + +constexpr size_t kOverlaySpriteTextureCount = 2u; +constexpr uint64_t kMinDynamicVertexBufferBytes = 4096u; + +const char kSceneViewportEditorOverlayLineHlsl[] = R"( +cbuffer OverlayConstants : register(b0) { + float4x4 gViewProjectionMatrix; + float4 gViewportSizeAndInvSize; +}; + +struct VSInput { + float3 position : POSITION; + float4 color : COLOR0; +}; + +struct VSOutput { + float4 position : SV_POSITION; + float4 color : COLOR0; +}; + +VSOutput MainVS(VSInput input) { + VSOutput output; + output.position = mul(gViewProjectionMatrix, float4(input.position, 1.0)); + output.color = input.color; + return output; +} + +float4 MainPS(VSOutput input) : SV_TARGET0 { + return input.color; +} +)"; + +const char kSceneViewportEditorOverlayScreenTriangleHlsl[] = R"( +cbuffer OverlayConstants : register(b0) { + float4x4 gViewProjectionMatrix; + float4 gViewportSizeAndInvSize; +}; + +struct VSInput { + float2 screenPosition : POSITION; + float4 color : COLOR0; +}; + +struct VSOutput { + float4 position : SV_POSITION; + float4 color : COLOR0; +}; + +VSOutput MainVS(VSInput input) { + VSOutput output; + float2 ndc = float2( + input.screenPosition.x * gViewportSizeAndInvSize.z * 2.0 - 1.0, + 1.0 - input.screenPosition.y * gViewportSizeAndInvSize.w * 2.0); + output.position = float4(ndc, 0.0, 1.0); + output.color = input.color; + return output; +} + +float4 MainPS(VSOutput input) : SV_TARGET0 { + return input.color; +} +)"; + +const char kSceneViewportEditorOverlaySpriteHlsl[] = R"( +cbuffer OverlayConstants : register(b0) { + float4x4 gViewProjectionMatrix; + float4 gViewportSizeAndInvSize; +}; + +Texture2D gIconTexture : register(t0); +SamplerState gIconSampler : register(s0); + +struct VSInput { + float3 worldCenter : POSITION; + float2 corner : TEXCOORD0; + float2 uv : TEXCOORD1; + float2 halfSizePixels : TEXCOORD2; + float4 color : COLOR0; +}; + +struct VSOutput { + float4 position : SV_POSITION; + float2 uv : TEXCOORD0; + float4 color : COLOR0; +}; + +VSOutput MainVS(VSInput input) { + VSOutput output; + float4 clip = mul(gViewProjectionMatrix, float4(input.worldCenter, 1.0)); + float2 offsetNdc = float2( + input.corner.x * input.halfSizePixels.x * gViewportSizeAndInvSize.z * 2.0, + input.corner.y * input.halfSizePixels.y * gViewportSizeAndInvSize.w * 2.0); + clip.xy += offsetNdc * clip.w; + output.position = clip; + output.uv = input.uv; + output.color = input.color; + return output; +} + +float4 MainPS(VSOutput input) : SV_TARGET0 { + float4 sampled = gIconTexture.Sample(gIconSampler, input.uv); + float4 color = sampled * input.color; + if (color.a <= 0.001) { + discard; + } + return color; +} +)"; + +struct OverlayConstants { + Math::Matrix4x4 viewProjection = Math::Matrix4x4::Identity(); + Math::Vector4 viewportSizeAndInvSize = Math::Vector4::Zero(); +}; + +struct OverlayLineVertex { + Math::Vector3 position = Math::Vector3::Zero(); + Math::Color color = Math::Color::White(); +}; + +struct OverlayScreenTriangleVertex { + Math::Vector2 screenPosition = Math::Vector2::Zero(); + Math::Color color = Math::Color::White(); +}; + +struct OverlaySpriteVertex { + Math::Vector3 worldCenter = Math::Vector3::Zero(); + Math::Vector2 corner = Math::Vector2::Zero(); + Math::Vector2 uv = Math::Vector2::Zero(); + Math::Vector2 halfSizePixels = Math::Vector2::Zero(); + Math::Color color = Math::Color::White(); +}; + +struct OverlaySpriteBatchRange { + uint32_t firstVertex = 0; + uint32_t vertexCount = 0; + + bool HasVertices() const { + return vertexCount > 0u; + } +}; + +class SceneViewportEditorOverlayPass final : public Rendering::RenderPass { +public: + SceneViewportEditorOverlayPass( + SceneViewportEditorOverlayPassRenderer& renderer, + SceneViewportOverlayFrameData frameData) + : m_renderer(renderer) + , m_frameData(std::move(frameData)) { + } + + const char* GetName() const override { + return "SceneViewportEditorOverlay"; + } + + bool Execute(const Rendering::RenderPassContext& context) override { + return m_renderer.Render(context.renderContext, context.surface, m_frameData); + } + +private: + SceneViewportEditorOverlayPassRenderer& m_renderer; + SceneViewportOverlayFrameData m_frameData = {}; +}; + +size_t ToSpriteTextureIndex(SceneViewportOverlaySpriteTextureKind textureKind) { + switch (textureKind) { + case SceneViewportOverlaySpriteTextureKind::Camera: + return 0u; + case SceneViewportOverlaySpriteTextureKind::Light: + return 1u; + default: + return 0u; + } +} + +const char* GetSpriteTextureBaseName(SceneViewportOverlaySpriteTextureKind textureKind) { + switch (textureKind) { + case SceneViewportOverlaySpriteTextureKind::Camera: + return "camera_gizmo"; + case SceneViewportOverlaySpriteTextureKind::Light: + return "main_light_gizmo"; + default: + return ""; + } +} + +std::filesystem::path ResolveOverlaySpriteTexturePath(SceneViewportOverlaySpriteTextureKind textureKind) { + const std::filesystem::path exeDir( + Platform::Utf8ToWide(Platform::GetExecutableDirectoryUtf8())); + std::filesystem::path iconPath = + exeDir / L".." / L".." / L"resources" / L"Icons" / + std::filesystem::path(Platform::Utf8ToWide(GetSpriteTextureBaseName(textureKind))); + iconPath += L".png"; + return iconPath.lexically_normal(); +} + +bool ReadFileBytes(const std::filesystem::path& filePath, std::vector& outBytes) { + std::ifstream input(filePath, std::ios::binary | std::ios::ate); + if (!input.is_open()) { + return false; + } + + const std::ifstream::pos_type size = input.tellg(); + if (size <= 0) { + return false; + } + + outBytes.resize(static_cast(size)); + input.seekg(0, std::ios::beg); + return input.read(reinterpret_cast(outBytes.data()), size).good(); +} + +bool DecodeTextureFile( + const std::filesystem::path& filePath, + std::vector& outPixels, + int& outWidth, + int& outHeight) { + std::vector fileData = {}; + if (!ReadFileBytes(filePath, fileData)) { + return false; + } + + int channels = 0; + stbi_uc* pixels = stbi_load_from_memory( + fileData.data(), + static_cast(fileData.size()), + &outWidth, + &outHeight, + &channels, + STBI_rgb_alpha); + if (pixels == nullptr || outWidth <= 0 || outHeight <= 0) { + if (pixels != nullptr) { + stbi_image_free(pixels); + } + return false; + } + + outPixels.assign( + pixels, + pixels + static_cast(outWidth) * static_cast(outHeight) * 4u); + stbi_image_free(pixels); + return true; +} + +RHI::GraphicsPipelineDesc BuildLinePipelineDesc( + RHI::RHIPipelineLayout* pipelineLayout, + bool depthTestEnabled) { + RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Line); + pipelineDesc.renderTargetCount = 1; + pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); + pipelineDesc.depthStencilFormat = static_cast(RHI::Format::D24_UNorm_S8_UInt); + pipelineDesc.sampleCount = 1; + + pipelineDesc.inputLayout.elements = { + { "POSITION", 0, static_cast(RHI::Format::R32G32B32_Float), 0, 0, 0, 0 }, + { "COLOR", 0, static_cast(RHI::Format::R32G32B32A32_Float), 0, 12, 0, 0 } + }; + + pipelineDesc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); + pipelineDesc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); + pipelineDesc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); + pipelineDesc.rasterizerState.depthClipEnable = true; + + pipelineDesc.blendState.blendEnable = true; + pipelineDesc.blendState.srcBlend = static_cast(RHI::BlendFactor::SrcAlpha); + pipelineDesc.blendState.dstBlend = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.srcBlendAlpha = static_cast(RHI::BlendFactor::One); + pipelineDesc.blendState.dstBlendAlpha = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.blendOp = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.blendOpAlpha = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.colorWriteMask = 0xF; + + pipelineDesc.depthStencilState.depthTestEnable = depthTestEnabled; + pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::LessEqual); + + pipelineDesc.vertexShader.source.assign( + kSceneViewportEditorOverlayLineHlsl, + kSceneViewportEditorOverlayLineHlsl + std::strlen(kSceneViewportEditorOverlayLineHlsl)); + pipelineDesc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign( + kSceneViewportEditorOverlayLineHlsl, + kSceneViewportEditorOverlayLineHlsl + std::strlen(kSceneViewportEditorOverlayLineHlsl)); + pipelineDesc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + + return pipelineDesc; +} + +RHI::GraphicsPipelineDesc BuildSpritePipelineDesc( + RHI::RHIPipelineLayout* pipelineLayout, + bool depthTestEnabled) { + RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + pipelineDesc.renderTargetCount = 1; + pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); + pipelineDesc.depthStencilFormat = static_cast(RHI::Format::D24_UNorm_S8_UInt); + pipelineDesc.sampleCount = 1; + + pipelineDesc.inputLayout.elements = { + { "POSITION", 0, static_cast(RHI::Format::R32G32B32_Float), 0, 0, 0, 0 }, + { "TEXCOORD", 0, static_cast(RHI::Format::R32G32_Float), 0, 12, 0, 0 }, + { "TEXCOORD", 1, static_cast(RHI::Format::R32G32_Float), 0, 20, 0, 0 }, + { "TEXCOORD", 2, static_cast(RHI::Format::R32G32_Float), 0, 28, 0, 0 }, + { "COLOR", 0, static_cast(RHI::Format::R32G32B32A32_Float), 0, 36, 0, 0 } + }; + + pipelineDesc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); + pipelineDesc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); + pipelineDesc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); + pipelineDesc.rasterizerState.depthClipEnable = true; + + pipelineDesc.blendState.blendEnable = true; + pipelineDesc.blendState.srcBlend = static_cast(RHI::BlendFactor::SrcAlpha); + pipelineDesc.blendState.dstBlend = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.srcBlendAlpha = static_cast(RHI::BlendFactor::One); + pipelineDesc.blendState.dstBlendAlpha = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.blendOp = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.blendOpAlpha = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.colorWriteMask = 0xF; + + pipelineDesc.depthStencilState.depthTestEnable = depthTestEnabled; + pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::LessEqual); + + pipelineDesc.vertexShader.source.assign( + kSceneViewportEditorOverlaySpriteHlsl, + kSceneViewportEditorOverlaySpriteHlsl + std::strlen(kSceneViewportEditorOverlaySpriteHlsl)); + pipelineDesc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign( + kSceneViewportEditorOverlaySpriteHlsl, + kSceneViewportEditorOverlaySpriteHlsl + std::strlen(kSceneViewportEditorOverlaySpriteHlsl)); + pipelineDesc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + + return pipelineDesc; +} + +RHI::GraphicsPipelineDesc BuildScreenTrianglePipelineDesc( + RHI::RHIPipelineLayout* pipelineLayout, + bool depthTestEnabled) { + RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + pipelineDesc.renderTargetCount = 1; + pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); + pipelineDesc.depthStencilFormat = static_cast(RHI::Format::D24_UNorm_S8_UInt); + pipelineDesc.sampleCount = 1; + + pipelineDesc.inputLayout.elements = { + { "POSITION", 0, static_cast(RHI::Format::R32G32_Float), 0, 0, 0, 0 }, + { "COLOR", 0, static_cast(RHI::Format::R32G32B32A32_Float), 0, 8, 0, 0 } + }; + + pipelineDesc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); + pipelineDesc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); + pipelineDesc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); + pipelineDesc.rasterizerState.depthClipEnable = true; + + pipelineDesc.blendState.blendEnable = true; + pipelineDesc.blendState.srcBlend = static_cast(RHI::BlendFactor::SrcAlpha); + pipelineDesc.blendState.dstBlend = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.srcBlendAlpha = static_cast(RHI::BlendFactor::One); + pipelineDesc.blendState.dstBlendAlpha = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.blendOp = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.blendOpAlpha = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.colorWriteMask = 0xF; + + pipelineDesc.depthStencilState.depthTestEnable = depthTestEnabled; + pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::LessEqual); + + pipelineDesc.vertexShader.source.assign( + kSceneViewportEditorOverlayScreenTriangleHlsl, + kSceneViewportEditorOverlayScreenTriangleHlsl + std::strlen(kSceneViewportEditorOverlayScreenTriangleHlsl)); + pipelineDesc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign( + kSceneViewportEditorOverlayScreenTriangleHlsl, + kSceneViewportEditorOverlayScreenTriangleHlsl + std::strlen(kSceneViewportEditorOverlayScreenTriangleHlsl)); + pipelineDesc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + + return pipelineDesc; +} + +void AppendSpriteVertices( + std::vector& outVertices, + const SceneViewportOverlaySpritePrimitive& sprite) { + const Math::Vector2 halfSizePixels = sprite.sizePixels * 0.5f; + const OverlaySpriteVertex quadVertices[4] = { + { sprite.worldPosition, Math::Vector2(-1.0f, 1.0f), Math::Vector2(0.0f, 0.0f), halfSizePixels, sprite.tintColor }, + { sprite.worldPosition, Math::Vector2(1.0f, 1.0f), Math::Vector2(1.0f, 0.0f), halfSizePixels, sprite.tintColor }, + { sprite.worldPosition, Math::Vector2(1.0f, -1.0f), Math::Vector2(1.0f, 1.0f), halfSizePixels, sprite.tintColor }, + { sprite.worldPosition, Math::Vector2(-1.0f, -1.0f), Math::Vector2(0.0f, 1.0f), halfSizePixels, sprite.tintColor } + }; + + outVertices.push_back(quadVertices[0]); + outVertices.push_back(quadVertices[1]); + outVertices.push_back(quadVertices[2]); + outVertices.push_back(quadVertices[0]); + outVertices.push_back(quadVertices[2]); + outVertices.push_back(quadVertices[3]); +} + +void AppendScreenTriangleVertices( + std::vector& outVertices, + const SceneViewportOverlayScreenTrianglePrimitive& triangle) { + outVertices.push_back({ triangle.vertices[0].screenPosition, triangle.vertices[0].color }); + outVertices.push_back({ triangle.vertices[1].screenPosition, triangle.vertices[1].color }); + outVertices.push_back({ triangle.vertices[2].screenPosition, triangle.vertices[2].color }); +} + +} // namespace + +void SceneViewportEditorOverlayPassRenderer::Shutdown() { + DestroyResources(); +} + +bool SceneViewportEditorOverlayPassRenderer::Render( + const Rendering::RenderContext& renderContext, + const Rendering::RenderSurface& surface, + const SceneViewportOverlayFrameData& frameData) { + if (!frameData.HasOverlayPrimitives() || !renderContext.IsValid() || renderContext.commandList == nullptr) { + return true; + } + + if (!EnsureInitialized(renderContext)) { + return false; + } + + std::vector depthTestedLineVertices = {}; + std::vector alwaysOnTopLineVertices = {}; + depthTestedLineVertices.reserve(frameData.worldLines.size() * 2u); + alwaysOnTopLineVertices.reserve(frameData.worldLines.size() * 2u); + for (const SceneViewportOverlayLinePrimitive& line : frameData.worldLines) { + std::vector& targetVertices = + line.depthMode == SceneViewportOverlayDepthMode::DepthTested + ? depthTestedLineVertices + : alwaysOnTopLineVertices; + targetVertices.push_back({ line.startWorld, line.color }); + targetVertices.push_back({ line.endWorld, line.color }); + } + + std::vector depthTestedScreenTriangleVertices = {}; + std::vector alwaysOnTopScreenTriangleVertices = {}; + depthTestedScreenTriangleVertices.reserve(frameData.screenTriangles.size() * 3u); + alwaysOnTopScreenTriangleVertices.reserve(frameData.screenTriangles.size() * 3u); + for (const SceneViewportOverlayScreenTrianglePrimitive& triangle : frameData.screenTriangles) { + std::vector& targetVertices = + triangle.depthMode == SceneViewportOverlayDepthMode::DepthTested + ? depthTestedScreenTriangleVertices + : alwaysOnTopScreenTriangleVertices; + AppendScreenTriangleVertices(targetVertices, triangle); + } + + std::vector depthTestedSprites = {}; + std::vector alwaysOnTopSprites = {}; + depthTestedSprites.reserve(frameData.worldSprites.size()); + alwaysOnTopSprites.reserve(frameData.worldSprites.size()); + for (const SceneViewportOverlaySpritePrimitive& sprite : frameData.worldSprites) { + if (sprite.sizePixels.x <= Math::EPSILON || sprite.sizePixels.y <= Math::EPSILON) { + continue; + } + + if (sprite.depthMode == SceneViewportOverlayDepthMode::DepthTested) { + depthTestedSprites.push_back(&sprite); + } else { + alwaysOnTopSprites.push_back(&sprite); + } + } + + const auto sortBackToFront = + [](const SceneViewportOverlaySpritePrimitive* lhs, const SceneViewportOverlaySpritePrimitive* rhs) { + return lhs->sortDepth > rhs->sortDepth; + }; + std::sort(depthTestedSprites.begin(), depthTestedSprites.end(), sortBackToFront); + std::sort(alwaysOnTopSprites.begin(), alwaysOnTopSprites.end(), sortBackToFront); + + const size_t lineVertexCount = depthTestedLineVertices.size() + alwaysOnTopLineVertices.size(); + const size_t screenTriangleVertexCount = + depthTestedScreenTriangleVertices.size() + alwaysOnTopScreenTriangleVertices.size(); + const size_t spriteVertexCount = (depthTestedSprites.size() + alwaysOnTopSprites.size()) * 6u; + if (lineVertexCount == 0u && screenTriangleVertexCount == 0u && spriteVertexCount == 0u) { + return true; + } + + if (lineVertexCount > 0u && !EnsureLineBufferCapacity(lineVertexCount)) { + return false; + } + if (screenTriangleVertexCount > 0u && !EnsureScreenTriangleBufferCapacity(screenTriangleVertexCount)) { + return false; + } + if (spriteVertexCount > 0u) { + if (!EnsureSpriteBufferCapacity(spriteVertexCount) || !EnsureIconTexturesLoaded()) { + return false; + } + } + + if (lineVertexCount > 0u) { + std::vector lineVertices = depthTestedLineVertices; + lineVertices.insert(lineVertices.end(), alwaysOnTopLineVertices.begin(), alwaysOnTopLineVertices.end()); + m_lineVertexBuffer->SetData(lineVertices.data(), lineVertices.size() * sizeof(OverlayLineVertex)); + } + + if (screenTriangleVertexCount > 0u) { + std::vector screenTriangleVertices = depthTestedScreenTriangleVertices; + screenTriangleVertices.insert( + screenTriangleVertices.end(), + alwaysOnTopScreenTriangleVertices.begin(), + alwaysOnTopScreenTriangleVertices.end()); + m_screenTriangleVertexBuffer->SetData( + screenTriangleVertices.data(), + screenTriangleVertices.size() * sizeof(OverlayScreenTriangleVertex)); + } + + std::vector spriteVertices = {}; + std::array depthTestedSpriteBatches = {}; + std::array alwaysOnTopSpriteBatches = {}; + if (spriteVertexCount > 0u) { + spriteVertices.reserve(spriteVertexCount); + const auto appendSpriteBatches = + [&spriteVertices]( + const std::vector& sprites, + std::array& outBatches) { + for (size_t textureIndex = 0; textureIndex < kOverlaySpriteTextureCount; ++textureIndex) { + OverlaySpriteBatchRange range = {}; + range.firstVertex = static_cast(spriteVertices.size()); + for (const SceneViewportOverlaySpritePrimitive* sprite : sprites) { + if (sprite == nullptr || ToSpriteTextureIndex(sprite->textureKind) != textureIndex) { + continue; + } + + AppendSpriteVertices(spriteVertices, *sprite); + } + range.vertexCount = static_cast(spriteVertices.size()) - range.firstVertex; + outBatches[textureIndex] = range; + } + }; + appendSpriteBatches(depthTestedSprites, depthTestedSpriteBatches); + appendSpriteBatches(alwaysOnTopSprites, alwaysOnTopSpriteBatches); + m_spriteVertexBuffer->SetData(spriteVertices.data(), spriteVertices.size() * sizeof(OverlaySpriteVertex)); + } + + OverlayConstants constants = {}; + constants.viewProjection = BuildSceneViewportViewProjectionMatrix( + frameData.overlay, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight())).Transpose(); + constants.viewportSizeAndInvSize = Math::Vector4( + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()), + surface.GetWidth() > 0u ? 1.0f / static_cast(surface.GetWidth()) : 0.0f, + surface.GetHeight() > 0u ? 1.0f / static_cast(surface.GetHeight()) : 0.0f); + m_constantSet->WriteConstant(0, &constants, sizeof(constants)); + + RHI::RHICommandList* commandList = renderContext.commandList; + const std::vector& colorAttachments = surface.GetColorAttachments(); + if (colorAttachments.empty() || colorAttachments[0] == nullptr) { + return false; + } + + RHI::RHIResourceView* renderTarget = colorAttachments[0]; + commandList->TransitionBarrier( + renderTarget, + surface.GetColorStateAfter(), + RHI::ResourceStates::RenderTarget); + commandList->SetRenderTargets(1, &renderTarget, surface.GetDepthAttachment()); + + const RHI::Viewport viewport = { + 0.0f, + 0.0f, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()), + 0.0f, + 1.0f + }; + const RHI::Rect scissorRect = { + 0, + 0, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()) + }; + commandList->SetViewport(viewport); + commandList->SetScissorRect(scissorRect); + + if (lineVertexCount > 0u) { + commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::LineList); + RHI::RHIResourceView* vertexBuffers[] = { m_lineVertexBufferView }; + const uint64_t offsets[] = { 0u }; + const uint32_t strides[] = { static_cast(sizeof(OverlayLineVertex)) }; + commandList->SetVertexBuffers(0, 1, vertexBuffers, offsets, strides); + + RHI::RHIDescriptorSet* descriptorSets[] = { m_constantSet }; + commandList->SetGraphicsDescriptorSets(0, 1, descriptorSets, m_linePipelineLayout); + + const uint32_t depthTestedLineVertexCount = static_cast(depthTestedLineVertices.size()); + const uint32_t alwaysOnTopLineVertexCount = static_cast(alwaysOnTopLineVertices.size()); + if (depthTestedLineVertexCount > 0u) { + commandList->SetPipelineState(m_depthTestedLinePipelineState); + commandList->Draw(depthTestedLineVertexCount, 1, 0, 0); + } + if (alwaysOnTopLineVertexCount > 0u) { + commandList->SetPipelineState(m_alwaysOnTopLinePipelineState); + commandList->Draw(alwaysOnTopLineVertexCount, 1, depthTestedLineVertexCount, 0); + } + } + + if (spriteVertexCount > 0u) { + commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + RHI::RHIResourceView* vertexBuffers[] = { m_spriteVertexBufferView }; + const uint64_t offsets[] = { 0u }; + const uint32_t strides[] = { static_cast(sizeof(OverlaySpriteVertex)) }; + commandList->SetVertexBuffers(0, 1, vertexBuffers, offsets, strides); + + const auto drawSpriteBatchGroup = + [this, commandList]( + RHI::RHIPipelineState* pipelineState, + const std::array& batches) { + if (pipelineState == nullptr) { + return; + } + + commandList->SetPipelineState(pipelineState); + for (size_t textureIndex = 0; textureIndex < kOverlaySpriteTextureCount; ++textureIndex) { + const OverlaySpriteBatchRange& batch = batches[textureIndex]; + if (!batch.HasVertices()) { + continue; + } + + RHI::RHIDescriptorSet* descriptorSets[] = { + m_constantSet, + m_overlaySpriteTextures[textureIndex].textureSet, + m_samplerSet + }; + commandList->SetGraphicsDescriptorSets(0, 3, descriptorSets, m_spritePipelineLayout); + commandList->Draw(batch.vertexCount, 1, batch.firstVertex, 0); + } + }; + + drawSpriteBatchGroup(m_depthTestedSpritePipelineState, depthTestedSpriteBatches); + drawSpriteBatchGroup(m_alwaysOnTopSpritePipelineState, alwaysOnTopSpriteBatches); + } + + if (screenTriangleVertexCount > 0u) { + commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + RHI::RHIResourceView* vertexBuffers[] = { m_screenTriangleVertexBufferView }; + const uint64_t offsets[] = { 0u }; + const uint32_t strides[] = { static_cast(sizeof(OverlayScreenTriangleVertex)) }; + commandList->SetVertexBuffers(0, 1, vertexBuffers, offsets, strides); + + RHI::RHIDescriptorSet* descriptorSets[] = { m_constantSet }; + commandList->SetGraphicsDescriptorSets(0, 1, descriptorSets, m_linePipelineLayout); + + const uint32_t depthTestedScreenTriangleVertexCount = + static_cast(depthTestedScreenTriangleVertices.size()); + const uint32_t alwaysOnTopScreenTriangleVertexCount = + static_cast(alwaysOnTopScreenTriangleVertices.size()); + if (depthTestedScreenTriangleVertexCount > 0u) { + commandList->SetPipelineState(m_depthTestedScreenTrianglePipelineState); + commandList->Draw(depthTestedScreenTriangleVertexCount, 1, 0, 0); + } + if (alwaysOnTopScreenTriangleVertexCount > 0u) { + commandList->SetPipelineState(m_alwaysOnTopScreenTrianglePipelineState); + commandList->Draw( + alwaysOnTopScreenTriangleVertexCount, + 1, + depthTestedScreenTriangleVertexCount, + 0); + } + } + + commandList->TransitionBarrier( + renderTarget, + RHI::ResourceStates::RenderTarget, + surface.GetColorStateAfter()); + return true; +} + +bool SceneViewportEditorOverlayPassRenderer::EnsureInitialized( + const Rendering::RenderContext& renderContext) { + if (m_device == renderContext.device && + m_backendType == renderContext.backendType && + m_linePipelineLayout != nullptr && + m_spritePipelineLayout != nullptr && + m_depthTestedLinePipelineState != nullptr && + m_alwaysOnTopLinePipelineState != nullptr && + m_depthTestedScreenTrianglePipelineState != nullptr && + m_alwaysOnTopScreenTrianglePipelineState != nullptr && + m_depthTestedSpritePipelineState != nullptr && + m_alwaysOnTopSpritePipelineState != nullptr && + m_constantPool != nullptr && + m_texturePool != nullptr && + m_samplerPool != nullptr && + m_constantSet != nullptr && + m_samplerSet != nullptr && + m_sampler != nullptr) { + return true; + } + + DestroyResources(); + return CreateResources(renderContext); +} + +bool SceneViewportEditorOverlayPassRenderer::CreateResources( + const Rendering::RenderContext& renderContext) { + if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) { + return false; + } + + m_device = renderContext.device; + m_backendType = renderContext.backendType; + + RHI::DescriptorSetLayoutBinding constantBinding = {}; + constantBinding.binding = 0; + constantBinding.type = static_cast(RHI::DescriptorType::CBV); + constantBinding.count = 1; + + RHI::DescriptorSetLayoutBinding textureBinding = {}; + textureBinding.binding = 0; + textureBinding.type = static_cast(RHI::DescriptorType::SRV); + textureBinding.count = 1; + + RHI::DescriptorSetLayoutBinding samplerBinding = {}; + samplerBinding.binding = 0; + samplerBinding.type = static_cast(RHI::DescriptorType::Sampler); + samplerBinding.count = 1; + + RHI::DescriptorSetLayoutDesc constantLayout = {}; + constantLayout.bindings = &constantBinding; + constantLayout.bindingCount = 1; + + RHI::DescriptorSetLayoutDesc textureLayout = {}; + textureLayout.bindings = &textureBinding; + textureLayout.bindingCount = 1; + + RHI::DescriptorSetLayoutDesc samplerLayout = {}; + samplerLayout.bindings = &samplerBinding; + samplerLayout.bindingCount = 1; + + RHI::RHIPipelineLayoutDesc linePipelineLayoutDesc = {}; + linePipelineLayoutDesc.setLayouts = &constantLayout; + linePipelineLayoutDesc.setLayoutCount = 1; + m_linePipelineLayout = m_device->CreatePipelineLayout(linePipelineLayoutDesc); + if (m_linePipelineLayout == nullptr) { + DestroyResources(); + return false; + } + + RHI::DescriptorSetLayoutDesc spriteSetLayouts[3] = { + constantLayout, + textureLayout, + samplerLayout + }; + RHI::RHIPipelineLayoutDesc spritePipelineLayoutDesc = {}; + spritePipelineLayoutDesc.setLayouts = spriteSetLayouts; + spritePipelineLayoutDesc.setLayoutCount = 3; + m_spritePipelineLayout = m_device->CreatePipelineLayout(spritePipelineLayoutDesc); + if (m_spritePipelineLayout == nullptr) { + DestroyResources(); + return false; + } + + RHI::DescriptorPoolDesc constantPoolDesc = {}; + constantPoolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + constantPoolDesc.descriptorCount = 1; + constantPoolDesc.shaderVisible = false; + m_constantPool = m_device->CreateDescriptorPool(constantPoolDesc); + if (m_constantPool == nullptr) { + DestroyResources(); + return false; + } + + m_constantSet = m_constantPool->AllocateSet(constantLayout); + if (m_constantSet == nullptr) { + DestroyResources(); + return false; + } + + RHI::DescriptorPoolDesc texturePoolDesc = {}; + texturePoolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + texturePoolDesc.descriptorCount = static_cast(kOverlaySpriteTextureCount); + texturePoolDesc.shaderVisible = true; + m_texturePool = m_device->CreateDescriptorPool(texturePoolDesc); + if (m_texturePool == nullptr) { + DestroyResources(); + return false; + } + + RHI::DescriptorPoolDesc samplerPoolDesc = {}; + samplerPoolDesc.type = RHI::DescriptorHeapType::Sampler; + samplerPoolDesc.descriptorCount = 1; + samplerPoolDesc.shaderVisible = true; + m_samplerPool = m_device->CreateDescriptorPool(samplerPoolDesc); + if (m_samplerPool == nullptr) { + DestroyResources(); + return false; + } + + m_samplerSet = m_samplerPool->AllocateSet(samplerLayout); + if (m_samplerSet == nullptr) { + DestroyResources(); + return false; + } + + RHI::SamplerDesc samplerDesc = {}; + samplerDesc.filter = static_cast(RHI::FilterMode::Linear); + samplerDesc.addressU = static_cast(RHI::TextureAddressMode::Clamp); + samplerDesc.addressV = static_cast(RHI::TextureAddressMode::Clamp); + samplerDesc.addressW = static_cast(RHI::TextureAddressMode::Clamp); + samplerDesc.mipLodBias = 0.0f; + samplerDesc.maxAnisotropy = 1; + samplerDesc.comparisonFunc = static_cast(RHI::ComparisonFunc::Always); + samplerDesc.minLod = 0.0f; + samplerDesc.maxLod = 1000.0f; + m_sampler = m_device->CreateSampler(samplerDesc); + if (m_sampler == nullptr) { + DestroyResources(); + return false; + } + m_samplerSet->UpdateSampler(0, m_sampler); + + const RHI::GraphicsPipelineDesc depthTestedLineDesc = + BuildLinePipelineDesc(m_linePipelineLayout, true); + m_depthTestedLinePipelineState = m_device->CreatePipelineState(depthTestedLineDesc); + if (m_depthTestedLinePipelineState == nullptr || !m_depthTestedLinePipelineState->IsValid()) { + DestroyResources(); + return false; + } + + const RHI::GraphicsPipelineDesc alwaysOnTopLineDesc = + BuildLinePipelineDesc(m_linePipelineLayout, false); + m_alwaysOnTopLinePipelineState = m_device->CreatePipelineState(alwaysOnTopLineDesc); + if (m_alwaysOnTopLinePipelineState == nullptr || !m_alwaysOnTopLinePipelineState->IsValid()) { + DestroyResources(); + return false; + } + + const RHI::GraphicsPipelineDesc depthTestedScreenTriangleDesc = + BuildScreenTrianglePipelineDesc(m_linePipelineLayout, true); + m_depthTestedScreenTrianglePipelineState = m_device->CreatePipelineState(depthTestedScreenTriangleDesc); + if (m_depthTestedScreenTrianglePipelineState == nullptr || + !m_depthTestedScreenTrianglePipelineState->IsValid()) { + DestroyResources(); + return false; + } + + const RHI::GraphicsPipelineDesc alwaysOnTopScreenTriangleDesc = + BuildScreenTrianglePipelineDesc(m_linePipelineLayout, false); + m_alwaysOnTopScreenTrianglePipelineState = m_device->CreatePipelineState(alwaysOnTopScreenTriangleDesc); + if (m_alwaysOnTopScreenTrianglePipelineState == nullptr || + !m_alwaysOnTopScreenTrianglePipelineState->IsValid()) { + DestroyResources(); + return false; + } + + const RHI::GraphicsPipelineDesc depthTestedSpriteDesc = + BuildSpritePipelineDesc(m_spritePipelineLayout, true); + m_depthTestedSpritePipelineState = m_device->CreatePipelineState(depthTestedSpriteDesc); + if (m_depthTestedSpritePipelineState == nullptr || !m_depthTestedSpritePipelineState->IsValid()) { + DestroyResources(); + return false; + } + + const RHI::GraphicsPipelineDesc alwaysOnTopSpriteDesc = + BuildSpritePipelineDesc(m_spritePipelineLayout, false); + m_alwaysOnTopSpritePipelineState = m_device->CreatePipelineState(alwaysOnTopSpriteDesc); + if (m_alwaysOnTopSpritePipelineState == nullptr || !m_alwaysOnTopSpritePipelineState->IsValid()) { + DestroyResources(); + return false; + } + + return true; +} + +bool SceneViewportEditorOverlayPassRenderer::EnsureLineBufferCapacity(size_t requiredVertexCount) { + const uint64_t requiredBytes = static_cast(requiredVertexCount * sizeof(OverlayLineVertex)); + if (requiredBytes == 0u) { + return true; + } + + if (m_lineVertexBuffer != nullptr && m_lineVertexBufferCapacity >= requiredBytes) { + return true; + } + + if (m_lineVertexBufferView != nullptr) { + m_lineVertexBufferView->Shutdown(); + delete m_lineVertexBufferView; + m_lineVertexBufferView = nullptr; + } + if (m_lineVertexBuffer != nullptr) { + m_lineVertexBuffer->Shutdown(); + delete m_lineVertexBuffer; + m_lineVertexBuffer = nullptr; + } + + m_lineVertexBufferCapacity = (std::max)(requiredBytes, kMinDynamicVertexBufferBytes); + RHI::BufferDesc bufferDesc = {}; + bufferDesc.size = m_lineVertexBufferCapacity; + bufferDesc.stride = static_cast(sizeof(OverlayLineVertex)); + bufferDesc.bufferType = static_cast(RHI::BufferType::Vertex); + m_lineVertexBuffer = m_device->CreateBuffer(bufferDesc); + if (m_lineVertexBuffer == nullptr) { + m_lineVertexBufferCapacity = 0u; + return false; + } + + m_lineVertexBuffer->SetStride(bufferDesc.stride); + m_lineVertexBuffer->SetBufferType(RHI::BufferType::Vertex); + + RHI::ResourceViewDesc viewDesc = {}; + viewDesc.dimension = RHI::ResourceViewDimension::Buffer; + viewDesc.structureByteStride = bufferDesc.stride; + m_lineVertexBufferView = m_device->CreateVertexBufferView(m_lineVertexBuffer, viewDesc); + return m_lineVertexBufferView != nullptr; +} + +bool SceneViewportEditorOverlayPassRenderer::EnsureScreenTriangleBufferCapacity(size_t requiredVertexCount) { + const uint64_t requiredBytes = + static_cast(requiredVertexCount * sizeof(OverlayScreenTriangleVertex)); + if (requiredBytes == 0u) { + return true; + } + + if (m_screenTriangleVertexBuffer != nullptr && m_screenTriangleVertexBufferCapacity >= requiredBytes) { + return true; + } + + if (m_screenTriangleVertexBufferView != nullptr) { + m_screenTriangleVertexBufferView->Shutdown(); + delete m_screenTriangleVertexBufferView; + m_screenTriangleVertexBufferView = nullptr; + } + if (m_screenTriangleVertexBuffer != nullptr) { + m_screenTriangleVertexBuffer->Shutdown(); + delete m_screenTriangleVertexBuffer; + m_screenTriangleVertexBuffer = nullptr; + } + + m_screenTriangleVertexBufferCapacity = (std::max)(requiredBytes, kMinDynamicVertexBufferBytes); + RHI::BufferDesc bufferDesc = {}; + bufferDesc.size = m_screenTriangleVertexBufferCapacity; + bufferDesc.stride = static_cast(sizeof(OverlayScreenTriangleVertex)); + bufferDesc.bufferType = static_cast(RHI::BufferType::Vertex); + m_screenTriangleVertexBuffer = m_device->CreateBuffer(bufferDesc); + if (m_screenTriangleVertexBuffer == nullptr) { + m_screenTriangleVertexBufferCapacity = 0u; + return false; + } + + m_screenTriangleVertexBuffer->SetStride(bufferDesc.stride); + m_screenTriangleVertexBuffer->SetBufferType(RHI::BufferType::Vertex); + + RHI::ResourceViewDesc viewDesc = {}; + viewDesc.dimension = RHI::ResourceViewDimension::Buffer; + viewDesc.structureByteStride = bufferDesc.stride; + m_screenTriangleVertexBufferView = + m_device->CreateVertexBufferView(m_screenTriangleVertexBuffer, viewDesc); + return m_screenTriangleVertexBufferView != nullptr; +} + +bool SceneViewportEditorOverlayPassRenderer::EnsureSpriteBufferCapacity(size_t requiredVertexCount) { + const uint64_t requiredBytes = static_cast(requiredVertexCount * sizeof(OverlaySpriteVertex)); + if (requiredBytes == 0u) { + return true; + } + + if (m_spriteVertexBuffer != nullptr && m_spriteVertexBufferCapacity >= requiredBytes) { + return true; + } + + if (m_spriteVertexBufferView != nullptr) { + m_spriteVertexBufferView->Shutdown(); + delete m_spriteVertexBufferView; + m_spriteVertexBufferView = nullptr; + } + if (m_spriteVertexBuffer != nullptr) { + m_spriteVertexBuffer->Shutdown(); + delete m_spriteVertexBuffer; + m_spriteVertexBuffer = nullptr; + } + + m_spriteVertexBufferCapacity = (std::max)(requiredBytes, kMinDynamicVertexBufferBytes); + RHI::BufferDesc bufferDesc = {}; + bufferDesc.size = m_spriteVertexBufferCapacity; + bufferDesc.stride = static_cast(sizeof(OverlaySpriteVertex)); + bufferDesc.bufferType = static_cast(RHI::BufferType::Vertex); + m_spriteVertexBuffer = m_device->CreateBuffer(bufferDesc); + if (m_spriteVertexBuffer == nullptr) { + m_spriteVertexBufferCapacity = 0u; + return false; + } + + m_spriteVertexBuffer->SetStride(bufferDesc.stride); + m_spriteVertexBuffer->SetBufferType(RHI::BufferType::Vertex); + + RHI::ResourceViewDesc viewDesc = {}; + viewDesc.dimension = RHI::ResourceViewDimension::Buffer; + viewDesc.structureByteStride = bufferDesc.stride; + m_spriteVertexBufferView = m_device->CreateVertexBufferView(m_spriteVertexBuffer, viewDesc); + return m_spriteVertexBufferView != nullptr; +} + +bool SceneViewportEditorOverlayPassRenderer::EnsureIconTexturesLoaded() { + if (m_device == nullptr || m_texturePool == nullptr) { + return false; + } + + RHI::DescriptorSetLayoutBinding textureBinding = {}; + textureBinding.binding = 0; + textureBinding.type = static_cast(RHI::DescriptorType::SRV); + textureBinding.count = 1; + RHI::DescriptorSetLayoutDesc textureLayout = {}; + textureLayout.bindings = &textureBinding; + textureLayout.bindingCount = 1; + + for (size_t textureIndex = 0; textureIndex < kOverlaySpriteTextureCount; ++textureIndex) { + OverlaySpriteTextureResources& resources = m_overlaySpriteTextures[textureIndex]; + if (resources.texture != nullptr && resources.shaderView != nullptr && resources.textureSet != nullptr) { + continue; + } + + const SceneViewportOverlaySpriteTextureKind textureKind = + textureIndex == 0u + ? SceneViewportOverlaySpriteTextureKind::Camera + : SceneViewportOverlaySpriteTextureKind::Light; + + std::vector pixels = {}; + int width = 0; + int height = 0; + if (!DecodeTextureFile(ResolveOverlaySpriteTexturePath(textureKind), pixels, width, height)) { + return false; + } + + RHI::TextureDesc textureDesc = {}; + textureDesc.width = static_cast(width); + textureDesc.height = static_cast(height); + textureDesc.depth = 1; + textureDesc.mipLevels = 1; + textureDesc.arraySize = 1; + textureDesc.format = static_cast(RHI::Format::R8G8B8A8_UNorm); + textureDesc.textureType = static_cast(RHI::TextureType::Texture2D); + textureDesc.sampleCount = 1; + textureDesc.sampleQuality = 0; + textureDesc.flags = 0; + resources.texture = m_device->CreateTexture( + textureDesc, + pixels.data(), + pixels.size(), + static_cast(width * 4)); + if (resources.texture == nullptr) { + return false; + } + + RHI::ResourceViewDesc viewDesc = {}; + viewDesc.format = static_cast(RHI::Format::R8G8B8A8_UNorm); + viewDesc.dimension = RHI::ResourceViewDimension::Texture2D; + viewDesc.mipLevel = 0; + resources.shaderView = m_device->CreateShaderResourceView(resources.texture, viewDesc); + if (resources.shaderView == nullptr) { + return false; + } + + resources.textureSet = m_texturePool->AllocateSet(textureLayout); + if (resources.textureSet == nullptr) { + return false; + } + resources.textureSet->Update(0, resources.shaderView); + } + + return true; +} + +void SceneViewportEditorOverlayPassRenderer::DestroyResources() { + if (m_lineVertexBufferView != nullptr) { + m_lineVertexBufferView->Shutdown(); + delete m_lineVertexBufferView; + m_lineVertexBufferView = nullptr; + } + if (m_lineVertexBuffer != nullptr) { + m_lineVertexBuffer->Shutdown(); + delete m_lineVertexBuffer; + m_lineVertexBuffer = nullptr; + } + if (m_screenTriangleVertexBufferView != nullptr) { + m_screenTriangleVertexBufferView->Shutdown(); + delete m_screenTriangleVertexBufferView; + m_screenTriangleVertexBufferView = nullptr; + } + if (m_screenTriangleVertexBuffer != nullptr) { + m_screenTriangleVertexBuffer->Shutdown(); + delete m_screenTriangleVertexBuffer; + m_screenTriangleVertexBuffer = nullptr; + } + if (m_spriteVertexBufferView != nullptr) { + m_spriteVertexBufferView->Shutdown(); + delete m_spriteVertexBufferView; + m_spriteVertexBufferView = nullptr; + } + if (m_spriteVertexBuffer != nullptr) { + m_spriteVertexBuffer->Shutdown(); + delete m_spriteVertexBuffer; + m_spriteVertexBuffer = nullptr; + } + + for (OverlaySpriteTextureResources& resources : m_overlaySpriteTextures) { + if (resources.textureSet != nullptr) { + resources.textureSet->Shutdown(); + delete resources.textureSet; + resources.textureSet = nullptr; + } + if (resources.shaderView != nullptr) { + resources.shaderView->Shutdown(); + delete resources.shaderView; + resources.shaderView = nullptr; + } + if (resources.texture != nullptr) { + resources.texture->Shutdown(); + delete resources.texture; + resources.texture = nullptr; + } + } + + if (m_depthTestedLinePipelineState != nullptr) { + m_depthTestedLinePipelineState->Shutdown(); + delete m_depthTestedLinePipelineState; + m_depthTestedLinePipelineState = nullptr; + } + if (m_alwaysOnTopLinePipelineState != nullptr) { + m_alwaysOnTopLinePipelineState->Shutdown(); + delete m_alwaysOnTopLinePipelineState; + m_alwaysOnTopLinePipelineState = nullptr; + } + if (m_depthTestedScreenTrianglePipelineState != nullptr) { + m_depthTestedScreenTrianglePipelineState->Shutdown(); + delete m_depthTestedScreenTrianglePipelineState; + m_depthTestedScreenTrianglePipelineState = nullptr; + } + if (m_alwaysOnTopScreenTrianglePipelineState != nullptr) { + m_alwaysOnTopScreenTrianglePipelineState->Shutdown(); + delete m_alwaysOnTopScreenTrianglePipelineState; + m_alwaysOnTopScreenTrianglePipelineState = nullptr; + } + if (m_depthTestedSpritePipelineState != nullptr) { + m_depthTestedSpritePipelineState->Shutdown(); + delete m_depthTestedSpritePipelineState; + m_depthTestedSpritePipelineState = nullptr; + } + if (m_alwaysOnTopSpritePipelineState != nullptr) { + m_alwaysOnTopSpritePipelineState->Shutdown(); + delete m_alwaysOnTopSpritePipelineState; + m_alwaysOnTopSpritePipelineState = nullptr; + } + + if (m_samplerSet != nullptr) { + m_samplerSet->Shutdown(); + delete m_samplerSet; + m_samplerSet = nullptr; + } + if (m_sampler != nullptr) { + m_sampler->Shutdown(); + delete m_sampler; + m_sampler = nullptr; + } + if (m_samplerPool != nullptr) { + m_samplerPool->Shutdown(); + delete m_samplerPool; + m_samplerPool = nullptr; + } + + if (m_constantSet != nullptr) { + m_constantSet->Shutdown(); + delete m_constantSet; + m_constantSet = nullptr; + } + if (m_constantPool != nullptr) { + m_constantPool->Shutdown(); + delete m_constantPool; + m_constantPool = nullptr; + } + if (m_texturePool != nullptr) { + m_texturePool->Shutdown(); + delete m_texturePool; + m_texturePool = nullptr; + } + + if (m_linePipelineLayout != nullptr) { + m_linePipelineLayout->Shutdown(); + delete m_linePipelineLayout; + m_linePipelineLayout = nullptr; + } + if (m_spritePipelineLayout != nullptr) { + m_spritePipelineLayout->Shutdown(); + delete m_spritePipelineLayout; + m_spritePipelineLayout = nullptr; + } + + m_lineVertexBufferCapacity = 0u; + m_screenTriangleVertexBufferCapacity = 0u; + m_spriteVertexBufferCapacity = 0u; + m_device = nullptr; + m_backendType = RHI::RHIType::D3D12; +} + +std::unique_ptr CreateSceneViewportEditorOverlayPass( + SceneViewportEditorOverlayPassRenderer& renderer, + const SceneViewportOverlayFrameData& frameData) { + return std::make_unique(renderer, frameData); +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/Passes/SceneViewportEditorOverlayPass.h b/editor/src/Viewport/Passes/SceneViewportEditorOverlayPass.h new file mode 100644 index 00000000..e116bf8d --- /dev/null +++ b/editor/src/Viewport/Passes/SceneViewportEditorOverlayPass.h @@ -0,0 +1,89 @@ +#pragma once + +#include "Viewport/SceneViewportEditorOverlayData.h" + +#include +#include +#include + +#include +#include +#include +#include + +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 m_overlaySpriteTextures = {}; +}; + +std::unique_ptr CreateSceneViewportEditorOverlayPass( + SceneViewportEditorOverlayPassRenderer& renderer, + const SceneViewportOverlayFrameData& frameData); + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportCameraController.h b/editor/src/Viewport/SceneViewportCameraController.h index b490467a..55c89967 100644 --- a/editor/src/Viewport/SceneViewportCameraController.h +++ b/editor/src/Viewport/SceneViewportCameraController.h @@ -128,13 +128,21 @@ public: if (std::abs(input.lookDeltaX) > Math::EPSILON || std::abs(input.lookDeltaY) > Math::EPSILON) { - ApplyRotationDelta(input.lookDeltaX, input.lookDeltaY); + ApplyRotationDelta( + input.lookDeltaX, + input.lookDeltaY, + kLookYawSensitivity, + kLookPitchSensitivity); UpdateFocalPointFromPosition(); } if (std::abs(input.orbitDeltaX) > Math::EPSILON || std::abs(input.orbitDeltaY) > Math::EPSILON) { - ApplyRotationDelta(input.orbitDeltaX, input.orbitDeltaY); + ApplyRotationDelta( + input.orbitDeltaX, + input.orbitDeltaY, + kOrbitYawSensitivity, + kOrbitPitchSensitivity); UpdatePositionFromFocalPoint(); } @@ -200,6 +208,10 @@ public: private: static constexpr float kSnapDurationSeconds = 0.22f; + static constexpr float kLookYawSensitivity = 0.18f; + static constexpr float kLookPitchSensitivity = 0.12f; + static constexpr float kOrbitYawSensitivity = 0.30f; + static constexpr float kOrbitPitchSensitivity = 0.20f; struct OrientationAngles { float yawDegrees = 0.0f; @@ -239,9 +251,13 @@ private: return result; } - void ApplyRotationDelta(float deltaX, float deltaY) { - m_yawDegrees += deltaX * 0.30f; - m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * 0.20f, -89.0f, 89.0f); + void ApplyRotationDelta( + float deltaX, + float deltaY, + float yawSensitivity, + float pitchSensitivity) { + m_yawDegrees += deltaX * yawSensitivity; + m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * pitchSensitivity, -89.0f, 89.0f); } Math::Vector3 GetRight() const { diff --git a/editor/src/Viewport/SceneViewportEditorOverlayData.h b/editor/src/Viewport/SceneViewportEditorOverlayData.h new file mode 100644 index 00000000..80be6bdb --- /dev/null +++ b/editor/src/Viewport/SceneViewportEditorOverlayData.h @@ -0,0 +1,144 @@ +#pragma once + +#include "IViewportHostService.h" + +#include +#include +#include + +#include +#include +#include + +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 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 screenQuad = {}; + float hitThicknessPixels = 0.0f; +}; + +struct SceneViewportOverlayHandleHitResult { + SceneViewportOverlayHandleKind kind = SceneViewportOverlayHandleKind::None; + uint64_t handleId = 0; + uint64_t entityId = 0; + int priority = (std::numeric_limits::min)(); + float distanceSq = (std::numeric_limits::max)(); + float depth = (std::numeric_limits::max)(); + + bool HasHit() const { + return kind != SceneViewportOverlayHandleKind::None; + } +}; + +struct SceneViewportOverlayFrameData { + SceneViewportOverlayData overlay = {}; + std::vector worldLines = {}; + std::vector worldSprites = {}; + std::vector screenTriangles = {}; + std::vector 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 diff --git a/editor/src/Viewport/SceneViewportMoveGizmo.cpp b/editor/src/Viewport/SceneViewportMoveGizmo.cpp index 90f560ec..446f9e68 100644 --- a/editor/src/Viewport/SceneViewportMoveGizmo.cpp +++ b/editor/src/Viewport/SceneViewportMoveGizmo.cpp @@ -14,7 +14,7 @@ namespace { constexpr float kMoveGizmoHandleLengthPixels = 100.0f; constexpr float kMoveGizmoHoverThresholdPixels = 10.0f; -Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) { +Math::Vector3 GetBaseAxisVector(SceneViewportGizmoAxis axis) { switch (axis) { case SceneViewportGizmoAxis::X: return Math::Vector3::Right(); @@ -28,6 +28,16 @@ Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) { } } +Math::Vector3 GetAxisVector( + SceneViewportGizmoAxis axis, + const Math::Quaternion& orientation) { + const Math::Vector3 baseAxis = GetBaseAxisVector(axis); + const Math::Vector3 orientedAxis = orientation * baseAxis; + return orientedAxis.SqrMagnitude() <= Math::EPSILON + ? baseAxis + : orientedAxis.Normalized(); +} + Math::Color GetAxisBaseColor(SceneViewportGizmoAxis axis) { switch (axis) { case SceneViewportGizmoAxis::X: @@ -61,20 +71,21 @@ SceneViewportGizmoPlane GetPlaneForIndex(size_t index) { void GetPlaneAxes( SceneViewportGizmoPlane plane, + const Math::Quaternion& orientation, Math::Vector3& outAxisA, Math::Vector3& outAxisB) { switch (plane) { case SceneViewportGizmoPlane::XY: - outAxisA = Math::Vector3::Right(); - outAxisB = Math::Vector3::Up(); + outAxisA = GetAxisVector(SceneViewportGizmoAxis::X, orientation); + outAxisB = GetAxisVector(SceneViewportGizmoAxis::Y, orientation); return; case SceneViewportGizmoPlane::XZ: - outAxisA = Math::Vector3::Right(); - outAxisB = Math::Vector3::Forward(); + outAxisA = GetAxisVector(SceneViewportGizmoAxis::X, orientation); + outAxisB = GetAxisVector(SceneViewportGizmoAxis::Z, orientation); return; case SceneViewportGizmoPlane::YZ: - outAxisA = Math::Vector3::Up(); - outAxisB = Math::Vector3::Forward(); + outAxisA = GetAxisVector(SceneViewportGizmoAxis::Y, orientation); + outAxisB = GetAxisVector(SceneViewportGizmoAxis::Z, orientation); return; case SceneViewportGizmoPlane::None: default: @@ -84,14 +95,16 @@ void GetPlaneAxes( } } -Math::Vector3 GetPlaneNormal(SceneViewportGizmoPlane plane) { +Math::Vector3 GetPlaneNormal( + SceneViewportGizmoPlane plane, + const Math::Quaternion& orientation) { switch (plane) { case SceneViewportGizmoPlane::XY: - return Math::Vector3::Forward(); + return GetAxisVector(SceneViewportGizmoAxis::Z, orientation); case SceneViewportGizmoPlane::XZ: - return Math::Vector3::Up(); + return GetAxisVector(SceneViewportGizmoAxis::Y, orientation); case SceneViewportGizmoPlane::YZ: - return Math::Vector3::Right(); + return GetAxisVector(SceneViewportGizmoAxis::X, orientation); case SceneViewportGizmoPlane::None: default: return Math::Vector3::Zero(); @@ -185,10 +198,9 @@ float ComputeWorldUnitsPerPixel( void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) { BuildDrawData(context); if (m_dragMode == DragMode::None && IsMouseInsideViewport(context)) { - m_hoveredAxis = HitTestAxis(context.mousePosition); - m_hoveredPlane = m_hoveredAxis == SceneViewportGizmoAxis::None - ? HitTestPlane(context.mousePosition) - : SceneViewportGizmoPlane::None; + const SceneViewportMoveGizmoHitResult hitResult = EvaluateHit(context.mousePosition); + m_hoveredAxis = hitResult.axis; + m_hoveredPlane = hitResult.plane; } else if (m_dragMode == DragMode::None) { m_hoveredAxis = SceneViewportGizmoAxis::None; m_hoveredPlane = SceneViewportGizmoPlane::None; @@ -221,18 +233,17 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c return false; } - const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition(); - const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition(); + const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition; Math::Vector3 dragPlaneNormal = Math::Vector3::Zero(); Math::Vector3 worldAxis = Math::Vector3::Zero(); if (m_hoveredAxis != SceneViewportGizmoAxis::None) { - worldAxis = GetAxisVector(m_hoveredAxis); + worldAxis = GetAxisVector(m_hoveredAxis, context.axisOrientation); if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) { return false; } } else { - dragPlaneNormal = GetPlaneNormal(m_hoveredPlane); + dragPlaneNormal = GetPlaneNormal(m_hoveredPlane, context.axisOrientation); if (dragPlaneNormal.SqrMagnitude() <= Math::EPSILON) { return false; } @@ -257,10 +268,21 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c m_activeAxisDirection = worldAxis; m_activePlaneNormal = dragPlaneNormal; m_dragPlane = dragPlane; - m_dragStartObjectWorldPosition = objectWorldPosition; m_dragStartPivotWorldPosition = pivotWorldPosition; m_dragStartHitWorldPosition = hitPoint; m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - pivotWorldPosition, worldAxis); + m_dragObjects = context.selectedObjects; + if (m_dragObjects.empty()) { + m_dragObjects.push_back(context.selectedObject); + } + m_dragStartObjectWorldPositions.clear(); + m_dragStartObjectWorldPositions.reserve(m_dragObjects.size()); + for (Components::GameObject* gameObject : m_dragObjects) { + m_dragStartObjectWorldPositions.push_back( + gameObject != nullptr && gameObject->GetTransform() != nullptr + ? gameObject->GetTransform()->GetPosition() + : Math::Vector3::Zero()); + } RefreshHandleState(); return true; } @@ -268,7 +290,9 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& context) { if (m_dragMode == DragMode::None || context.selectedObject == nullptr || - context.selectedObject->GetID() != m_activeEntityId) { + context.selectedObject->GetID() != m_activeEntityId || + m_dragObjects.empty() || + m_dragObjects.size() != m_dragStartObjectWorldPositions.size()) { return; } @@ -290,17 +314,28 @@ void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& con if (m_dragMode == DragMode::Axis) { const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection); const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar; - context.selectedObject->GetTransform()->SetPosition( - m_dragStartObjectWorldPosition + m_activeAxisDirection * deltaScalar); + const Math::Vector3 worldDelta = m_activeAxisDirection * deltaScalar; + for (size_t index = 0; index < m_dragObjects.size(); ++index) { + if (m_dragObjects[index] == nullptr || m_dragObjects[index]->GetTransform() == nullptr) { + continue; + } + m_dragObjects[index]->GetTransform()->SetPosition( + m_dragStartObjectWorldPositions[index] + worldDelta); + } return; } if (m_dragMode == DragMode::Plane) { - const Math::Vector3 planeDelta = Math::Vector3::ProjectOnPlane( + const Math::Vector3 worldDelta = Math::Vector3::ProjectOnPlane( hitPoint - m_dragStartHitWorldPosition, m_activePlaneNormal); - context.selectedObject->GetTransform()->SetPosition( - m_dragStartObjectWorldPosition + planeDelta); + for (size_t index = 0; index < m_dragObjects.size(); ++index) { + if (m_dragObjects[index] == nullptr || m_dragObjects[index]->GetTransform() == nullptr) { + continue; + } + m_dragObjects[index]->GetTransform()->SetPosition( + m_dragStartObjectWorldPositions[index] + worldDelta); + } } } @@ -319,10 +354,11 @@ void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) { m_activeEntityId = 0; m_activeAxisDirection = Math::Vector3::Zero(); m_activePlaneNormal = Math::Vector3::Zero(); - m_dragStartObjectWorldPosition = Math::Vector3::Zero(); m_dragStartPivotWorldPosition = Math::Vector3::Zero(); m_dragStartHitWorldPosition = Math::Vector3::Zero(); m_dragStartAxisScalar = 0.0f; + m_dragObjects.clear(); + m_dragStartObjectWorldPositions.clear(); RefreshHandleState(); } @@ -337,10 +373,11 @@ void SceneViewportMoveGizmo::CancelDrag(IUndoManager* undoManager) { m_activeEntityId = 0; m_activeAxisDirection = Math::Vector3::Zero(); m_activePlaneNormal = Math::Vector3::Zero(); - m_dragStartObjectWorldPosition = Math::Vector3::Zero(); m_dragStartPivotWorldPosition = Math::Vector3::Zero(); m_dragStartHitWorldPosition = Math::Vector3::Zero(); m_dragStartAxisScalar = 0.0f; + m_dragObjects.clear(); + m_dragStartObjectWorldPositions.clear(); m_hoveredAxis = SceneViewportGizmoAxis::None; m_hoveredPlane = SceneViewportGizmoPlane::None; RefreshHandleState(); @@ -363,19 +400,74 @@ const SceneViewportMoveGizmoDrawData& SceneViewportMoveGizmo::GetDrawData() cons return m_drawData; } +SceneViewportMoveGizmoHitResult SceneViewportMoveGizmo::EvaluateHit(const Math::Vector2& mousePosition) const { + SceneViewportMoveGizmoHitResult result = {}; + if (!m_drawData.visible) { + return result; + } + + const float hoverThresholdSq = kMoveGizmoHoverThresholdPixels * kMoveGizmoHoverThresholdPixels; + for (const SceneViewportMoveGizmoHandleDrawData& handle : m_drawData.handles) { + if (!handle.visible) { + continue; + } + + const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end); + if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) { + continue; + } + + result.axis = handle.axis; + result.plane = SceneViewportGizmoPlane::None; + result.distanceSq = distanceSq; + } + + if (result.axis != SceneViewportGizmoAxis::None) { + return result; + } + + for (const SceneViewportMoveGizmoPlaneDrawData& plane : m_drawData.planes) { + if (!plane.visible || !PointInQuad(mousePosition, plane.corners)) { + continue; + } + + const float distanceSq = (QuadCenter(plane.corners) - mousePosition).SqrMagnitude(); + if (distanceSq >= result.distanceSq) { + continue; + } + + result.axis = SceneViewportGizmoAxis::None; + result.plane = plane.plane; + result.distanceSq = distanceSq; + } + + return result; +} + +void SceneViewportMoveGizmo::SetHoveredHandle( + SceneViewportGizmoAxis axis, + SceneViewportGizmoPlane plane) { + if (m_dragMode != DragMode::None) { + return; + } + + m_hoveredAxis = axis; + m_hoveredPlane = axis == SceneViewportGizmoAxis::None ? plane : SceneViewportGizmoPlane::None; + RefreshHandleState(); +} + void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& context) { m_drawData = {}; m_drawData.pivotRadius = 5.0f; - const Components::GameObject* selectedObject = context.selectedObject; - if (selectedObject == nullptr || + if ((context.selectedObject == nullptr && context.selectedObjects.empty()) || !context.overlay.valid || context.viewportSize.x <= 1.0f || context.viewportSize.y <= 1.0f) { return; } - const Math::Vector3 gizmoWorldOrigin = selectedObject->GetTransform()->GetPosition(); + const Math::Vector3 gizmoWorldOrigin = context.pivotWorldPosition; const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, @@ -411,7 +503,7 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& handle.start = projectedPivot.screenPosition; const Math::Vector3 axisEndWorld = - gizmoWorldOrigin + GetAxisVector(handle.axis) * axisLengthWorld; + gizmoWorldOrigin + GetAxisVector(handle.axis, context.axisOrientation) * axisLengthWorld; const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, @@ -436,7 +528,7 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& Math::Vector3 axisA = Math::Vector3::Zero(); Math::Vector3 axisB = Math::Vector3::Zero(); - GetPlaneAxes(plane.plane, axisA, axisB); + GetPlaneAxes(plane.plane, context.axisOrientation, axisA, axisB); if (axisA.SqrMagnitude() <= Math::EPSILON || axisB.SqrMagnitude() <= Math::EPSILON) { continue; } @@ -502,55 +594,5 @@ void SceneViewportMoveGizmo::RefreshHandleState() { } } -SceneViewportGizmoAxis SceneViewportMoveGizmo::HitTestAxis(const Math::Vector2& mousePosition) const { - if (!m_drawData.visible) { - return SceneViewportGizmoAxis::None; - } - - const float hoverThresholdSq = kMoveGizmoHoverThresholdPixels * kMoveGizmoHoverThresholdPixels; - SceneViewportGizmoAxis bestAxis = SceneViewportGizmoAxis::None; - float bestDistanceSq = hoverThresholdSq; - - for (const SceneViewportMoveGizmoHandleDrawData& handle : m_drawData.handles) { - if (!handle.visible) { - continue; - } - - const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end); - if (distanceSq > bestDistanceSq) { - continue; - } - - bestDistanceSq = distanceSq; - bestAxis = handle.axis; - } - - return bestAxis; -} - -SceneViewportGizmoPlane SceneViewportMoveGizmo::HitTestPlane(const Math::Vector2& mousePosition) const { - if (!m_drawData.visible) { - return SceneViewportGizmoPlane::None; - } - - SceneViewportGizmoPlane bestPlane = SceneViewportGizmoPlane::None; - float bestDistanceSq = Math::FLOAT_MAX; - for (const SceneViewportMoveGizmoPlaneDrawData& plane : m_drawData.planes) { - if (!plane.visible || !PointInQuad(mousePosition, plane.corners)) { - continue; - } - - const float distanceSq = (QuadCenter(plane.corners) - mousePosition).SqrMagnitude(); - if (distanceSq >= bestDistanceSq) { - continue; - } - - bestDistanceSq = distanceSq; - bestPlane = plane.plane; - } - - return bestPlane; -} - } // namespace Editor } // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportMoveGizmo.h b/editor/src/Viewport/SceneViewportMoveGizmo.h index 524ed170..34f49144 100644 --- a/editor/src/Viewport/SceneViewportMoveGizmo.h +++ b/editor/src/Viewport/SceneViewportMoveGizmo.h @@ -4,11 +4,13 @@ #include #include +#include #include #include #include #include +#include namespace XCEngine { namespace Components { @@ -66,6 +68,19 @@ struct SceneViewportMoveGizmoContext { Math::Vector2 viewportSize = Math::Vector2::Zero(); Math::Vector2 mousePosition = Math::Vector2::Zero(); Components::GameObject* selectedObject = nullptr; + std::vector selectedObjects = {}; + Math::Vector3 pivotWorldPosition = Math::Vector3::Zero(); + Math::Quaternion axisOrientation = Math::Quaternion::Identity(); +}; + +struct SceneViewportMoveGizmoHitResult { + SceneViewportGizmoAxis axis = SceneViewportGizmoAxis::None; + SceneViewportGizmoPlane plane = SceneViewportGizmoPlane::None; + float distanceSq = Math::FLOAT_MAX; + + bool HasHit() const { + return axis != SceneViewportGizmoAxis::None || plane != SceneViewportGizmoPlane::None; + } }; class SceneViewportMoveGizmo { @@ -80,6 +95,8 @@ public: bool IsActive() const; uint64_t GetActiveEntityId() const; const SceneViewportMoveGizmoDrawData& GetDrawData() const; + SceneViewportMoveGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const; + void SetHoveredHandle(SceneViewportGizmoAxis axis, SceneViewportGizmoPlane plane); private: enum class DragMode : uint8_t { @@ -90,8 +107,6 @@ private: void BuildDrawData(const SceneViewportMoveGizmoContext& context); void RefreshHandleState(); - SceneViewportGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const; - SceneViewportGizmoPlane HitTestPlane(const Math::Vector2& mousePosition) const; SceneViewportMoveGizmoDrawData m_drawData = {}; SceneViewportGizmoAxis m_hoveredAxis = SceneViewportGizmoAxis::None; @@ -103,10 +118,11 @@ private: Math::Vector3 m_activeAxisDirection = Math::Vector3::Zero(); Math::Vector3 m_activePlaneNormal = Math::Vector3::Zero(); Math::Plane m_dragPlane = {}; - Math::Vector3 m_dragStartObjectWorldPosition = Math::Vector3::Zero(); Math::Vector3 m_dragStartPivotWorldPosition = Math::Vector3::Zero(); Math::Vector3 m_dragStartHitWorldPosition = Math::Vector3::Zero(); float m_dragStartAxisScalar = 0.0f; + std::vector m_dragObjects = {}; + std::vector m_dragStartObjectWorldPositions = {}; }; } // namespace Editor diff --git a/editor/src/Viewport/SceneViewportOverlayBuilder.cpp b/editor/src/Viewport/SceneViewportOverlayBuilder.cpp new file mode 100644 index 00000000..3172e202 --- /dev/null +++ b/editor/src/Viewport/SceneViewportOverlayBuilder.cpp @@ -0,0 +1,431 @@ +#include "SceneViewportOverlayBuilder.h" + +#include "Core/IEditorContext.h" +#include "Core/ISceneManager.h" +#include "SceneViewportOverlayHandleBuilder.h" +#include "SceneViewportMath.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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(viewportWidth) * + (viewportRect.width > Math::EPSILON ? viewportRect.width : 1.0f); + const float resolvedHeight = static_cast(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(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(viewportWidth), + static_cast(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 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, 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 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(segmentIndex) / static_cast(kRingSegmentCount) * Math::PI * 2.0f; + const float angle1 = + static_cast(segmentIndex + 1u) / static_cast(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()) { + 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()) { + 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& 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(); + camera != nullptr && camera->IsEnabled()) { + AppendCameraFrustumOverlay(frameData, *camera, *gameObject, viewportWidth, viewportHeight); + } + + if (Components::LightComponent* light = gameObject->GetComponent(); + light != nullptr && + light->IsEnabled() && + light->GetLightType() == Components::LightType::Directional) { + AppendDirectionalLightOverlay(frameData, *gameObject, overlay, viewportHeight); + } + } + + return frameData; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportOverlayBuilder.h b/editor/src/Viewport/SceneViewportOverlayBuilder.h new file mode 100644 index 00000000..b68a1139 --- /dev/null +++ b/editor/src/Viewport/SceneViewportOverlayBuilder.h @@ -0,0 +1,25 @@ +#pragma once + +#include "IViewportHostService.h" +#include "SceneViewportEditorOverlayData.h" + +#include +#include + +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& selectedObjectIds); +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportOverlayHandleBuilder.h b/editor/src/Viewport/SceneViewportOverlayHandleBuilder.h new file mode 100644 index 00000000..80e24a8f --- /dev/null +++ b/editor/src/Viewport/SceneViewportOverlayHandleBuilder.h @@ -0,0 +1,543 @@ +#pragma once + +#include "SceneViewportEditorOverlayData.h" +#include "SceneViewportMoveGizmo.h" +#include "SceneViewportRotateGizmo.h" +#include "SceneViewportScaleGizmo.h" + +#include +#include + +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& 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 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(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(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(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(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(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(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 diff --git a/editor/src/Viewport/SceneViewportOverlayHitTester.h b/editor/src/Viewport/SceneViewportOverlayHitTester.h new file mode 100644 index 00000000..fc3df571 --- /dev/null +++ b/editor/src/Viewport/SceneViewportOverlayHitTester.h @@ -0,0 +1,186 @@ +#pragma once + +#include "SceneViewportEditorOverlayData.h" +#include "SceneViewportMath.h" + +#include +#include + +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& 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& 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 diff --git a/editor/src/Viewport/SceneViewportOverlayRenderer.cpp b/editor/src/Viewport/SceneViewportOverlayRenderer.cpp index dfa0df7f..fe6c7526 100644 --- a/editor/src/Viewport/SceneViewportOverlayRenderer.cpp +++ b/editor/src/Viewport/SceneViewportOverlayRenderer.cpp @@ -1,305 +1,16 @@ #include "SceneViewportOverlayRenderer.h" -#include "SceneViewportOrientationGizmo.h" -#include -#include +#include "SceneViewportOrientationGizmo.h" namespace XCEngine { namespace Editor { -namespace { - -constexpr float kMoveGizmoArrowLength = 14.0f; -constexpr float kMoveGizmoArrowHalfWidth = 7.0f; - -ImU32 ToImGuiColor(const Math::Color& color) { - const auto toChannel = [](float value) -> int { - return static_cast(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(angleFill.arcPointCount + 1), - fillColor); - - for (size_t index = 0; index + 1 < angleFill.arcPointCount; ++index) { - drawList->AddLine( - ImVec2(viewportMin.x + angleFill.arcPoints[index].x, viewportMin.y + angleFill.arcPoints[index].y), - ImVec2( - viewportMin.x + angleFill.arcPoints[index + 1].x, - viewportMin.y + angleFill.arcPoints[index + 1].y), - outlineColor, - 2.0f); - } - - drawList->AddLine( - pivot, - ImVec2(viewportMin.x + angleFill.arcPoints.front().x, viewportMin.y + angleFill.arcPoints.front().y), - outlineColor, - 1.6f); - drawList->AddLine( - pivot, - ImVec2( - viewportMin.x + angleFill.arcPoints[angleFill.arcPointCount - 1].x, - viewportMin.y + angleFill.arcPoints[angleFill.arcPointCount - 1].y), - outlineColor, - 1.6f); -} - -void DrawSceneScaleGizmoAxis( - ImDrawList* drawList, - const ImVec2& viewportMin, - const SceneViewportScaleGizmoAxisHandleDrawData& handle) { - if (drawList == nullptr || !handle.visible) { - return; - } - - const ImU32 color = ToImGuiColor(handle.color); - const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.2f); - const ImVec2 start(viewportMin.x + handle.start.x, viewportMin.y + handle.start.y); - const ImVec2 capCenter(viewportMin.x + handle.capCenter.x, viewportMin.y + handle.capCenter.y); - const ImVec2 direction = NormalizeImVec2(ImVec2(capCenter.x - start.x, capCenter.y - start.y)); - const ImVec2 lineEnd( - capCenter.x - direction.x * handle.capHalfSize, - capCenter.y - direction.y * handle.capHalfSize); - const ImVec2 capMin(capCenter.x - handle.capHalfSize, capCenter.y - handle.capHalfSize); - const ImVec2 capMax(capCenter.x + handle.capHalfSize, capCenter.y + handle.capHalfSize); - - drawList->AddLine(start, lineEnd, color, thickness); - drawList->AddRectFilled(capMin, capMax, color, 1.2f); - drawList->AddRect(capMin, capMax, IM_COL32(24, 24, 24, 220), 1.2f, 0, handle.active ? 2.0f : 1.0f); -} - -void DrawSceneScaleGizmoCenterHandle( - ImDrawList* drawList, - const ImVec2& viewportMin, - const SceneViewportScaleGizmoCenterHandleDrawData& handle) { - if (drawList == nullptr || !handle.visible) { - return; - } - - const ImVec2 center(viewportMin.x + handle.center.x, viewportMin.y + handle.center.y); - const ImVec2 handleMin(center.x - handle.halfSize, center.y - handle.halfSize); - const ImVec2 handleMax(center.x + handle.halfSize, center.y + handle.halfSize); - drawList->AddRectFilled(handleMin, handleMax, ToImGuiColor(handle.fillColor), 1.2f); - drawList->AddRect( - handleMin, - handleMax, - ToImGuiColor(handle.outlineColor), - 1.2f, - 0, - handle.active ? 2.0f : 1.0f); -} - -void DrawSceneMoveGizmo( - ImDrawList* drawList, - const ImVec2& viewportMin, - const SceneViewportMoveGizmoDrawData& moveGizmo) { - if (drawList == nullptr || !moveGizmo.visible) { - return; - } - - for (const SceneViewportMoveGizmoPlaneDrawData& plane : moveGizmo.planes) { - DrawSceneMoveGizmoPlane(drawList, viewportMin, plane); - } - - for (const SceneViewportMoveGizmoHandleDrawData& handle : moveGizmo.handles) { - DrawSceneMoveGizmoAxis(drawList, viewportMin, handle); - } -} - -void DrawSceneRotateGizmo( - ImDrawList* drawList, - const ImVec2& viewportMin, - const SceneViewportRotateGizmoDrawData& rotateGizmo) { - if (drawList == nullptr || !rotateGizmo.visible) { - return; - } - - for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) { - if (handle.axis == SceneViewportRotateGizmoAxis::View) { - DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, true); - } - } - - for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) { - if (handle.axis != SceneViewportRotateGizmoAxis::View) { - DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, false); - } - } - - DrawSceneRotateGizmoAngleFill(drawList, viewportMin, rotateGizmo.angleFill); - - for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) { - if (handle.axis != SceneViewportRotateGizmoAxis::View) { - DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, true); - } - } -} - -void DrawSceneScaleGizmo( - ImDrawList* drawList, - const ImVec2& viewportMin, - const SceneViewportScaleGizmoDrawData& scaleGizmo) { - if (drawList == nullptr || !scaleGizmo.visible) { - return; - } - - for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : scaleGizmo.axisHandles) { - DrawSceneScaleGizmoAxis(drawList, viewportMin, handle); - } - - DrawSceneScaleGizmoCenterHandle(drawList, viewportMin, scaleGizmo.centerHandle); -} - -} // namespace - void DrawSceneViewportOverlay( ImDrawList* drawList, const SceneViewportOverlayData& overlay, const ImVec2& viewportMin, const ImVec2& viewportMax, - const ImVec2& viewportSize, - const SceneViewportMoveGizmoDrawData* moveGizmo, - const SceneViewportRotateGizmoDrawData* rotateGizmo, - const SceneViewportScaleGizmoDrawData* scaleGizmo) { + const ImVec2& viewportSize) { if (drawList == nullptr || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) { return; } @@ -308,15 +19,6 @@ void DrawSceneViewportOverlay( if (overlay.valid) { DrawSceneViewportOrientationGizmo(drawList, overlay, viewportMin, viewportMax); } - if (moveGizmo != nullptr) { - DrawSceneMoveGizmo(drawList, viewportMin, *moveGizmo); - } - if (rotateGizmo != nullptr) { - DrawSceneRotateGizmo(drawList, viewportMin, *rotateGizmo); - } - if (scaleGizmo != nullptr) { - DrawSceneScaleGizmo(drawList, viewportMin, *scaleGizmo); - } drawList->PopClipRect(); } diff --git a/editor/src/Viewport/SceneViewportOverlayRenderer.h b/editor/src/Viewport/SceneViewportOverlayRenderer.h index efd74f2e..887d3c5a 100644 --- a/editor/src/Viewport/SceneViewportOverlayRenderer.h +++ b/editor/src/Viewport/SceneViewportOverlayRenderer.h @@ -1,9 +1,6 @@ #pragma once #include "IViewportHostService.h" -#include "SceneViewportMoveGizmo.h" -#include "SceneViewportRotateGizmo.h" -#include "SceneViewportScaleGizmo.h" #include @@ -15,10 +12,7 @@ void DrawSceneViewportOverlay( const SceneViewportOverlayData& overlay, const ImVec2& viewportMin, const ImVec2& viewportMax, - const ImVec2& viewportSize, - const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr, - const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr, - const SceneViewportScaleGizmoDrawData* scaleGizmo = nullptr); + const ImVec2& viewportSize); } // namespace Editor } // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportRotateGizmo.cpp b/editor/src/Viewport/SceneViewportRotateGizmo.cpp index 34daa0bc..8a024acd 100644 --- a/editor/src/Viewport/SceneViewportRotateGizmo.cpp +++ b/editor/src/Viewport/SceneViewportRotateGizmo.cpp @@ -19,6 +19,21 @@ constexpr float kRotateGizmoViewRadiusPixels = 106.0f; constexpr float kRotateGizmoHoverThresholdPixels = 9.0f; constexpr float kRotateGizmoAngleFillMinRadians = 0.01f; +Math::Vector3 GetBaseRotateAxisVector(SceneViewportRotateGizmoAxis axis) { + switch (axis) { + case SceneViewportRotateGizmoAxis::X: + return Math::Vector3::Right(); + case SceneViewportRotateGizmoAxis::Y: + return Math::Vector3::Up(); + case SceneViewportRotateGizmoAxis::Z: + return Math::Vector3::Forward(); + case SceneViewportRotateGizmoAxis::View: + case SceneViewportRotateGizmoAxis::None: + default: + return Math::Vector3::Zero(); + } +} + Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) { return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized(); } @@ -30,6 +45,22 @@ bool IsMouseInsideViewport(const SceneViewportRotateGizmoContext& context) { context.mousePosition.y <= context.viewportSize.y; } +Math::Quaternion ComputeStableWorldRotation(const Components::GameObject* gameObject) { + if (gameObject == nullptr || gameObject->GetTransform() == nullptr) { + return Math::Quaternion::Identity(); + } + + const Components::TransformComponent* transform = gameObject->GetTransform(); + Math::Quaternion worldRotation = transform->GetLocalRotation(); + for (const Components::TransformComponent* parent = transform->GetParent(); + parent != nullptr; + parent = parent->GetParent()) { + worldRotation = parent->GetLocalRotation() * worldRotation; + } + + return worldRotation.Normalized(); +} + Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) { switch (axis) { case SceneViewportRotateGizmoAxis::X: @@ -48,14 +79,13 @@ Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) { Math::Vector3 GetRotateAxisVector( SceneViewportRotateGizmoAxis axis, - const SceneViewportOverlayData& overlay) { + const SceneViewportOverlayData& overlay, + const Math::Quaternion& axisOrientation) { switch (axis) { case SceneViewportRotateGizmoAxis::X: - return Math::Vector3::Right(); case SceneViewportRotateGizmoAxis::Y: - return Math::Vector3::Up(); case SceneViewportRotateGizmoAxis::Z: - return Math::Vector3::Forward(); + return NormalizeVector3(axisOrientation * GetBaseRotateAxisVector(axis), GetBaseRotateAxisVector(axis)); case SceneViewportRotateGizmoAxis::View: return NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward()); case SceneViewportRotateGizmoAxis::None: @@ -67,20 +97,21 @@ Math::Vector3 GetRotateAxisVector( bool GetRotateRingBasis( SceneViewportRotateGizmoAxis axis, const SceneViewportOverlayData& overlay, + const Math::Quaternion& axisOrientation, Math::Vector3& outBasisA, Math::Vector3& outBasisB) { switch (axis) { case SceneViewportRotateGizmoAxis::X: - outBasisA = Math::Vector3::Up(); - outBasisB = Math::Vector3::Forward(); + outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up()); + outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward()); return true; case SceneViewportRotateGizmoAxis::Y: - outBasisA = Math::Vector3::Forward(); - outBasisB = Math::Vector3::Right(); + outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward()); + outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right()); return true; case SceneViewportRotateGizmoAxis::Z: - outBasisA = Math::Vector3::Right(); - outBasisB = Math::Vector3::Up(); + outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right()); + outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up()); return true; case SceneViewportRotateGizmoAxis::View: outBasisA = NormalizeVector3(overlay.cameraRight, Math::Vector3::Right()); @@ -147,11 +178,12 @@ SceneViewportRotateGizmoAxis GetRotateAxisForIndex(size_t index) { bool TryComputeRingAngleFromWorldDirection( SceneViewportRotateGizmoAxis axis, const SceneViewportOverlayData& overlay, + const Math::Quaternion& axisOrientation, const Math::Vector3& directionWorld, float& outAngle) { Math::Vector3 basisA = Math::Vector3::Zero(); Math::Vector3 basisB = Math::Vector3::Zero(); - if (!GetRotateRingBasis(axis, overlay, basisA, basisB)) { + if (!GetRotateRingBasis(axis, overlay, axisOrientation, basisA, basisB)) { return false; } @@ -171,7 +203,7 @@ bool TryComputeRingAngleFromWorldDirection( void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) { BuildDrawData(context); if (m_activeAxis == SceneViewportRotateGizmoAxis::None && IsMouseInsideViewport(context)) { - m_hoveredAxis = HitTestAxis(context.mousePosition); + m_hoveredAxis = EvaluateHit(context.mousePosition).axis; } else if (m_activeAxis == SceneViewportRotateGizmoAxis::None) { m_hoveredAxis = SceneViewportRotateGizmoAxis::None; } else { @@ -190,8 +222,8 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex return false; } - const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition(); - const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay); + const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition; + const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay, context.axisOrientation); if (worldAxis.SqrMagnitude() <= Math::EPSILON) { return false; } @@ -225,6 +257,7 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex if (!TryComputeRingAngleFromWorldDirection( m_hoveredAxis, context.overlay, + context.axisOrientation, startDirection, startRingAngle)) { return false; @@ -238,12 +271,31 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex m_activeAxis = m_hoveredAxis; m_activeEntityId = context.selectedObject->GetID(); + m_localSpace = context.localSpace && m_hoveredAxis != SceneViewportRotateGizmoAxis::View; + m_rotateAroundSharedPivot = context.rotateAroundSharedPivot; m_activeWorldAxis = worldAxis.Normalized(); m_screenSpaceDrag = useScreenSpaceDrag; m_dragPlane = dragPlane; - m_dragStartWorldRotation = context.selectedObject->GetTransform()->GetRotation(); m_dragStartRingAngle = startRingAngle; m_dragCurrentDeltaRadians = 0.0f; + m_dragStartPivotWorldPosition = pivotWorldPosition; + m_dragObjects = context.selectedObjects; + if (m_dragObjects.empty()) { + m_dragObjects.push_back(context.selectedObject); + } + m_dragStartWorldPositions.clear(); + m_dragStartWorldRotations.clear(); + m_dragStartWorldPositions.reserve(m_dragObjects.size()); + m_dragStartWorldRotations.reserve(m_dragObjects.size()); + for (Components::GameObject* gameObject : m_dragObjects) { + if (gameObject != nullptr && gameObject->GetTransform() != nullptr) { + m_dragStartWorldPositions.push_back(gameObject->GetTransform()->GetPosition()); + m_dragStartWorldRotations.push_back(gameObject->GetTransform()->GetRotation()); + } else { + m_dragStartWorldPositions.push_back(Math::Vector3::Zero()); + m_dragStartWorldRotations.push_back(Math::Quaternion::Identity()); + } + } RefreshHandleState(); return true; } @@ -251,7 +303,10 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& context) { if (m_activeAxis == SceneViewportRotateGizmoAxis::None || context.selectedObject == nullptr || - context.selectedObject->GetID() != m_activeEntityId) { + context.selectedObject->GetID() != m_activeEntityId || + m_dragObjects.empty() || + m_dragObjects.size() != m_dragStartWorldPositions.size() || + m_dragObjects.size() != m_dragStartWorldRotations.size()) { return; } @@ -275,7 +330,7 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& return; } - const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition(); + const Math::Vector3 pivotWorldPosition = m_dragStartPivotWorldPosition; const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance); const Math::Vector3 currentDirection = Math::Vector3::ProjectOnPlane(hitPoint - pivotWorldPosition, m_activeWorldAxis); if (currentDirection.SqrMagnitude() <= Math::EPSILON) { @@ -285,6 +340,7 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& if (!TryComputeRingAngleFromWorldDirection( m_activeAxis, context.overlay, + context.axisOrientation, currentDirection, currentRingAngle)) { return; @@ -293,9 +349,37 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& const float deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle); m_dragCurrentDeltaRadians = deltaRadians; - const Math::Quaternion deltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians); - context.selectedObject->GetTransform()->SetRotation(deltaRotation * m_dragStartWorldRotation); - BuildDrawData(context); + const Math::Quaternion worldDeltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians); + const Math::Vector3 localAxis = GetBaseRotateAxisVector(m_activeAxis); + const Math::Quaternion localDeltaRotation = + localAxis.SqrMagnitude() > Math::EPSILON + ? Math::Quaternion::FromAxisAngle(localAxis, deltaRadians) + : Math::Quaternion::Identity(); + for (size_t index = 0; index < m_dragObjects.size(); ++index) { + Components::GameObject* gameObject = m_dragObjects[index]; + if (gameObject == nullptr || gameObject->GetTransform() == nullptr) { + continue; + } + + if (m_rotateAroundSharedPivot) { + gameObject->GetTransform()->SetPosition( + m_dragStartPivotWorldPosition + + worldDeltaRotation * (m_dragStartWorldPositions[index] - m_dragStartPivotWorldPosition)); + } else { + gameObject->GetTransform()->SetPosition(m_dragStartWorldPositions[index]); + } + if (m_localSpace && m_activeAxis != SceneViewportRotateGizmoAxis::View) { + gameObject->GetTransform()->SetRotation(m_dragStartWorldRotations[index] * localDeltaRotation); + } else { + gameObject->GetTransform()->SetRotation(worldDeltaRotation * m_dragStartWorldRotations[index]); + } + } + SceneViewportRotateGizmoContext drawContext = context; + drawContext.pivotWorldPosition = m_dragStartPivotWorldPosition; + if (drawContext.localSpace && drawContext.selectedObject != nullptr) { + drawContext.axisOrientation = ComputeStableWorldRotation(drawContext.selectedObject); + } + BuildDrawData(drawContext); m_hoveredAxis = m_activeAxis; RefreshHandleState(); } @@ -312,10 +396,15 @@ void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) { m_activeAxis = SceneViewportRotateGizmoAxis::None; m_activeEntityId = 0; m_screenSpaceDrag = false; + m_localSpace = false; + m_rotateAroundSharedPivot = false; m_activeWorldAxis = Math::Vector3::Zero(); - m_dragStartWorldRotation = Math::Quaternion::Identity(); m_dragStartRingAngle = 0.0f; m_dragCurrentDeltaRadians = 0.0f; + m_dragStartPivotWorldPosition = Math::Vector3::Zero(); + m_dragObjects.clear(); + m_dragStartWorldPositions.clear(); + m_dragStartWorldRotations.clear(); RefreshHandleState(); } @@ -327,10 +416,15 @@ void SceneViewportRotateGizmo::CancelDrag(IUndoManager* undoManager) { m_activeAxis = SceneViewportRotateGizmoAxis::None; m_activeEntityId = 0; m_screenSpaceDrag = false; + m_localSpace = false; + m_rotateAroundSharedPivot = false; m_activeWorldAxis = Math::Vector3::Zero(); - m_dragStartWorldRotation = Math::Quaternion::Identity(); m_dragStartRingAngle = 0.0f; m_dragCurrentDeltaRadians = 0.0f; + m_dragStartPivotWorldPosition = Math::Vector3::Zero(); + m_dragObjects.clear(); + m_dragStartWorldPositions.clear(); + m_dragStartWorldRotations.clear(); m_hoveredAxis = SceneViewportRotateGizmoAxis::None; RefreshHandleState(); } @@ -351,18 +445,57 @@ const SceneViewportRotateGizmoDrawData& SceneViewportRotateGizmo::GetDrawData() return m_drawData; } +SceneViewportRotateGizmoHitResult SceneViewportRotateGizmo::EvaluateHit(const Math::Vector2& mousePosition) const { + SceneViewportRotateGizmoHitResult result = {}; + if (!m_drawData.visible) { + return result; + } + + const float hoverThresholdSq = kRotateGizmoHoverThresholdPixels * kRotateGizmoHoverThresholdPixels; + for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) { + if (!handle.visible) { + continue; + } + + for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) { + if (!segment.visible || + (handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) { + continue; + } + + const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end); + if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) { + continue; + } + + result.axis = handle.axis; + result.distanceSq = distanceSq; + } + } + + return result; +} + +void SceneViewportRotateGizmo::SetHoveredHandle(SceneViewportRotateGizmoAxis axis) { + if (m_activeAxis != SceneViewportRotateGizmoAxis::None) { + return; + } + + m_hoveredAxis = axis; + RefreshHandleState(); +} + void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoContext& context) { m_drawData = {}; - const Components::GameObject* selectedObject = context.selectedObject; - if (selectedObject == nullptr || + if ((context.selectedObject == nullptr && context.selectedObjects.empty()) || !context.overlay.valid || context.viewportSize.x <= 1.0f || context.viewportSize.y <= 1.0f) { return; } - const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition(); + const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition; const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, @@ -383,6 +516,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte m_drawData.visible = true; m_drawData.pivot = projectedPivot.screenPosition; const bool hasActiveDragFeedback = + !context.localSpace && m_activeAxis != SceneViewportRotateGizmoAxis::None && m_activeAxis != SceneViewportRotateGizmoAxis::View && std::abs(m_dragCurrentDeltaRadians) > Math::EPSILON; @@ -398,7 +532,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte Math::Vector3 basisA = Math::Vector3::Zero(); Math::Vector3 basisB = Math::Vector3::Zero(); - if (!GetRotateRingBasis(handle.axis, context.overlay, basisA, basisB)) { + if (!GetRotateRingBasis(handle.axis, context.overlay, context.axisOrientation, basisA, basisB)) { continue; } if (hasActiveDragFeedback && handle.axis != SceneViewportRotateGizmoAxis::View) { @@ -468,7 +602,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte Math::Vector3 basisA = Math::Vector3::Zero(); Math::Vector3 basisB = Math::Vector3::Zero(); - if (GetRotateRingBasis(m_activeAxis, context.overlay, basisA, basisB)) { + if (GetRotateRingBasis(m_activeAxis, context.overlay, context.axisOrientation, basisA, basisB)) { const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(m_activeAxis); const float sweepRadians = NormalizeSignedAngleRadians(m_dragCurrentDeltaRadians); const float sweepAbs = std::abs(sweepRadians); @@ -519,39 +653,6 @@ void SceneViewportRotateGizmo::RefreshHandleState() { } } -SceneViewportRotateGizmoAxis SceneViewportRotateGizmo::HitTestAxis(const Math::Vector2& mousePosition) const { - if (!m_drawData.visible) { - return SceneViewportRotateGizmoAxis::None; - } - - const float hoverThresholdSq = kRotateGizmoHoverThresholdPixels * kRotateGizmoHoverThresholdPixels; - SceneViewportRotateGizmoAxis bestAxis = SceneViewportRotateGizmoAxis::None; - float bestDistanceSq = hoverThresholdSq; - - for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) { - if (!handle.visible) { - continue; - } - - for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) { - if (!segment.visible || - (handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) { - continue; - } - - const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end); - if (distanceSq > bestDistanceSq) { - continue; - } - - bestDistanceSq = distanceSq; - bestAxis = handle.axis; - } - } - - return bestAxis; -} - bool SceneViewportRotateGizmo::TryGetClosestRingAngle( SceneViewportRotateGizmoAxis axis, const Math::Vector2& mousePosition, diff --git a/editor/src/Viewport/SceneViewportRotateGizmo.h b/editor/src/Viewport/SceneViewportRotateGizmo.h index 1bf602ac..1d6880e8 100644 --- a/editor/src/Viewport/SceneViewportRotateGizmo.h +++ b/editor/src/Viewport/SceneViewportRotateGizmo.h @@ -10,6 +10,7 @@ #include #include +#include namespace XCEngine { namespace Components { @@ -71,6 +72,20 @@ struct SceneViewportRotateGizmoContext { Math::Vector2 viewportSize = Math::Vector2::Zero(); Math::Vector2 mousePosition = Math::Vector2::Zero(); Components::GameObject* selectedObject = nullptr; + std::vector selectedObjects = {}; + Math::Vector3 pivotWorldPosition = Math::Vector3::Zero(); + Math::Quaternion axisOrientation = Math::Quaternion::Identity(); + bool localSpace = false; + bool rotateAroundSharedPivot = false; +}; + +struct SceneViewportRotateGizmoHitResult { + SceneViewportRotateGizmoAxis axis = SceneViewportRotateGizmoAxis::None; + float distanceSq = Math::FLOAT_MAX; + + bool HasHit() const { + return axis != SceneViewportRotateGizmoAxis::None; + } }; class SceneViewportRotateGizmo { @@ -85,11 +100,12 @@ public: bool IsActive() const; uint64_t GetActiveEntityId() const; const SceneViewportRotateGizmoDrawData& GetDrawData() const; + SceneViewportRotateGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const; + void SetHoveredHandle(SceneViewportRotateGizmoAxis axis); private: void BuildDrawData(const SceneViewportRotateGizmoContext& context); void RefreshHandleState(); - SceneViewportRotateGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const; bool TryGetClosestRingAngle( SceneViewportRotateGizmoAxis axis, const Math::Vector2& mousePosition, @@ -101,11 +117,16 @@ private: SceneViewportRotateGizmoAxis m_activeAxis = SceneViewportRotateGizmoAxis::None; uint64_t m_activeEntityId = 0; bool m_screenSpaceDrag = false; + bool m_localSpace = false; + bool m_rotateAroundSharedPivot = false; Math::Vector3 m_activeWorldAxis = Math::Vector3::Zero(); Math::Plane m_dragPlane = {}; - Math::Quaternion m_dragStartWorldRotation = Math::Quaternion::Identity(); float m_dragStartRingAngle = 0.0f; float m_dragCurrentDeltaRadians = 0.0f; + Math::Vector3 m_dragStartPivotWorldPosition = Math::Vector3::Zero(); + std::vector m_dragObjects = {}; + std::vector m_dragStartWorldPositions = {}; + std::vector m_dragStartWorldRotations = {}; }; } // namespace Editor diff --git a/editor/src/Viewport/SceneViewportScaleGizmo.cpp b/editor/src/Viewport/SceneViewportScaleGizmo.cpp index f4aa8a6b..637c97d7 100644 --- a/editor/src/Viewport/SceneViewportScaleGizmo.cpp +++ b/editor/src/Viewport/SceneViewportScaleGizmo.cpp @@ -130,7 +130,7 @@ float ComputeVisualScaleFactor(float current, float start) { void SceneViewportScaleGizmo::Update(const SceneViewportScaleGizmoContext& context) { BuildDrawData(context); if (m_activeHandle == SceneViewportScaleGizmoHandle::None && IsMouseInsideViewport(context)) { - m_hoveredHandle = HitTestHandle(context.mousePosition); + m_hoveredHandle = EvaluateHit(context.mousePosition).handle; } else if (m_activeHandle == SceneViewportScaleGizmoHandle::None) { m_hoveredHandle = SceneViewportScaleGizmoHandle::None; } else { @@ -158,7 +158,7 @@ bool SceneViewportScaleGizmo::TryBeginDrag(const SceneViewportScaleGizmoContext& activeScreenDirection = handle->end - handle->start; if (activeScreenDirection.SqrMagnitude() <= Math::EPSILON) { - const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition(); + const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition; if (!ProjectSceneViewportAxisDirectionAtPoint( context.overlay, context.viewportSize.x, @@ -310,6 +310,68 @@ const SceneViewportScaleGizmoDrawData& SceneViewportScaleGizmo::GetDrawData() co return m_drawData; } +SceneViewportScaleGizmoHitResult SceneViewportScaleGizmo::EvaluateHit(const Math::Vector2& mousePosition) const { + SceneViewportScaleGizmoHitResult result = {}; + if (!m_drawData.visible) { + return result; + } + + if (m_drawData.centerHandle.visible && + IsPointInsideSquare( + mousePosition, + m_drawData.centerHandle.center, + m_drawData.centerHandle.halfSize + 2.0f)) { + result.handle = SceneViewportScaleGizmoHandle::Uniform; + result.distanceSq = (m_drawData.centerHandle.center - mousePosition).SqrMagnitude(); + return result; + } + + for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) { + if (!handle.visible || + !IsPointInsideSquare(mousePosition, handle.capCenter, handle.capHalfSize + 2.0f)) { + continue; + } + + const float distanceSq = (handle.capCenter - mousePosition).SqrMagnitude(); + if (distanceSq >= result.distanceSq) { + continue; + } + + result.handle = handle.handle; + result.distanceSq = distanceSq; + } + + if (result.handle != SceneViewportScaleGizmoHandle::None) { + return result; + } + + const float hoverThresholdSq = kScaleGizmoHoverThresholdPixels * kScaleGizmoHoverThresholdPixels; + for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) { + if (!handle.visible) { + continue; + } + + const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end); + if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) { + continue; + } + + result.handle = handle.handle; + result.distanceSq = distanceSq; + } + + return result; +} + +void SceneViewportScaleGizmo::SetHoveredHandle(SceneViewportScaleGizmoHandle handle) { + if (m_activeHandle != SceneViewportScaleGizmoHandle::None) { + return; + } + + m_hoveredHandle = handle; + RefreshHandleState(); +} + void SceneViewportScaleGizmo::BuildDrawData(const SceneViewportScaleGizmoContext& context) { m_drawData = {}; @@ -321,7 +383,7 @@ void SceneViewportScaleGizmo::BuildDrawData(const SceneViewportScaleGizmoContext return; } - const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition(); + const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition; const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, @@ -435,58 +497,6 @@ void SceneViewportScaleGizmo::RefreshHandleState() { m_drawData.centerHandle.active ? 1.0f : (m_drawData.centerHandle.hovered ? 0.96f : 0.88f)); } -SceneViewportScaleGizmoHandle SceneViewportScaleGizmo::HitTestHandle(const Math::Vector2& mousePosition) const { - if (!m_drawData.visible) { - return SceneViewportScaleGizmoHandle::None; - } - - if (m_drawData.centerHandle.visible && - IsPointInsideSquare( - mousePosition, - m_drawData.centerHandle.center, - m_drawData.centerHandle.halfSize + 2.0f)) { - return SceneViewportScaleGizmoHandle::Uniform; - } - - SceneViewportScaleGizmoHandle bestHandle = SceneViewportScaleGizmoHandle::None; - float bestDistanceSq = Math::FLOAT_MAX; - for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) { - if (!handle.visible || - !IsPointInsideSquare(mousePosition, handle.capCenter, handle.capHalfSize + 2.0f)) { - continue; - } - - const float distanceSq = (handle.capCenter - mousePosition).SqrMagnitude(); - if (distanceSq >= bestDistanceSq) { - continue; - } - - bestDistanceSq = distanceSq; - bestHandle = handle.handle; - } - - if (bestHandle != SceneViewportScaleGizmoHandle::None) { - return bestHandle; - } - - bestDistanceSq = kScaleGizmoHoverThresholdPixels * kScaleGizmoHoverThresholdPixels; - for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) { - if (!handle.visible) { - continue; - } - - const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end); - if (distanceSq > bestDistanceSq) { - continue; - } - - bestDistanceSq = distanceSq; - bestHandle = handle.handle; - } - - return bestHandle; -} - const SceneViewportScaleGizmoAxisHandleDrawData* SceneViewportScaleGizmo::FindAxisHandleDrawData( SceneViewportScaleGizmoHandle handle) const { for (const SceneViewportScaleGizmoAxisHandleDrawData& drawHandle : m_drawData.axisHandles) { diff --git a/editor/src/Viewport/SceneViewportScaleGizmo.h b/editor/src/Viewport/SceneViewportScaleGizmo.h index 213a96d9..0ecdd7c7 100644 --- a/editor/src/Viewport/SceneViewportScaleGizmo.h +++ b/editor/src/Viewport/SceneViewportScaleGizmo.h @@ -3,6 +3,7 @@ #include "IViewportHostService.h" #include +#include #include #include @@ -59,9 +60,20 @@ struct SceneViewportScaleGizmoContext { Math::Vector2 viewportSize = Math::Vector2::Zero(); Math::Vector2 mousePosition = Math::Vector2::Zero(); Components::GameObject* selectedObject = nullptr; + Math::Vector3 pivotWorldPosition = Math::Vector3::Zero(); + Math::Quaternion axisOrientation = Math::Quaternion::Identity(); bool uniformOnly = false; }; +struct SceneViewportScaleGizmoHitResult { + SceneViewportScaleGizmoHandle handle = SceneViewportScaleGizmoHandle::None; + float distanceSq = Math::FLOAT_MAX; + + bool HasHit() const { + return handle != SceneViewportScaleGizmoHandle::None; + } +}; + class SceneViewportScaleGizmo { public: void Update(const SceneViewportScaleGizmoContext& context); @@ -74,11 +86,12 @@ public: bool IsActive() const; uint64_t GetActiveEntityId() const; const SceneViewportScaleGizmoDrawData& GetDrawData() const; + SceneViewportScaleGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const; + void SetHoveredHandle(SceneViewportScaleGizmoHandle handle); private: void BuildDrawData(const SceneViewportScaleGizmoContext& context); void RefreshHandleState(); - SceneViewportScaleGizmoHandle HitTestHandle(const Math::Vector2& mousePosition) const; const SceneViewportScaleGizmoAxisHandleDrawData* FindAxisHandleDrawData( SceneViewportScaleGizmoHandle handle) const; diff --git a/editor/src/Viewport/SceneViewportTransformGizmoFrameBuilder.h b/editor/src/Viewport/SceneViewportTransformGizmoFrameBuilder.h new file mode 100644 index 00000000..23bae169 --- /dev/null +++ b/editor/src/Viewport/SceneViewportTransformGizmoFrameBuilder.h @@ -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 +#include + +#include +#include + +namespace XCEngine { +namespace Editor { + +enum class SceneViewportActiveGizmoKind : uint8_t { + None = 0, + Move, + Rotate, + Scale +}; + +struct SceneViewportSelectionGizmoState { + Components::GameObject* primaryObject = nullptr; + std::vector 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()) { + 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& 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(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 diff --git a/editor/src/Viewport/ViewportHostRenderFlowUtils.h b/editor/src/Viewport/ViewportHostRenderFlowUtils.h index 6e50e6bb..751dccfd 100644 --- a/editor/src/Viewport/ViewportHostRenderFlowUtils.h +++ b/editor/src/Viewport/ViewportHostRenderFlowUtils.h @@ -166,7 +166,9 @@ inline void ApplySceneViewportRenderRequestSetup( const Rendering::BuiltinPostProcessRequest* builtinPostProcess, Rendering::RenderPassSequence* postPasses, Rendering::CameraRenderRequest& request) { + request.preScenePasses = nullptr; request.postScenePasses = nullptr; + request.overlayPasses = nullptr; request.objectId = {}; request.builtinPostProcess = {}; diff --git a/editor/src/Viewport/ViewportHostService.h b/editor/src/Viewport/ViewportHostService.h index 8ac0f8c3..7590c075 100644 --- a/editor/src/Viewport/ViewportHostService.h +++ b/editor/src/Viewport/ViewportHostService.h @@ -4,7 +4,11 @@ #include "Core/ISceneManager.h" #include "Core/ISelectionManager.h" #include "IViewportHostService.h" +#include "Passes/SceneViewportEditorOverlayPass.h" #include "SceneViewportCameraController.h" +#include "SceneViewportEditorOverlayData.h" +#include "SceneViewportOverlayHandleBuilder.h" +#include "SceneViewportOverlayBuilder.h" #include "ViewportHostRenderFlowUtils.h" #include "ViewportHostRenderTargets.h" #include "ViewportObjectIdPicker.h" @@ -12,6 +16,7 @@ #include #include +#include #include #include #include @@ -24,7 +29,9 @@ #include #include +#include #include +#include #include #include #include @@ -35,6 +42,133 @@ namespace Editor { namespace { constexpr bool kDebugSceneSelectionMask = false; +constexpr uint64_t kSceneViewportOverlaySignatureOffsetBasis = 14695981039346656037ull; +constexpr uint64_t kSceneViewportOverlaySignaturePrime = 1099511628211ull; + +void HashSceneViewportOverlayBytes(uint64_t& hash, const void* data, size_t size) { + const auto* bytes = static_cast(data); + for (size_t index = 0; index < size; ++index) { + hash ^= static_cast(bytes[index]); + hash *= kSceneViewportOverlaySignaturePrime; + } +} + +template +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& selectedObjectIds) { + uint64_t hash = kSceneViewportOverlaySignatureOffsetBasis; + + HashSceneViewportOverlayValue(hash, static_cast(selectedObjectIds.size())); + for (uint64_t entityId : selectedObjectIds) { + HashSceneViewportOverlayValue(hash, entityId); + } + + if (scene == nullptr) { + return hash; + } + + for (Components::CameraComponent* camera : scene->FindObjectsOfType()) { + Components::GameObject* gameObject = camera != nullptr ? camera->GetGameObject() : nullptr; + HashSceneViewportOverlayValue(hash, static_cast(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(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::GameObject* gameObject = light != nullptr ? light->GetGameObject() : nullptr; + HashSceneViewportOverlayValue(hash, static_cast(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(light->GetLightType())); + } + + return hash; +} + +bool AreEqualSceneViewportVector3(const Math::Vector3& lhs, const Math::Vector3& rhs) { + constexpr float kEpsilon = 1e-4f; + return std::abs(lhs.x - rhs.x) <= kEpsilon && + std::abs(lhs.y - rhs.y) <= kEpsilon && + std::abs(lhs.z - rhs.z) <= kEpsilon; +} + +bool AreEqualSceneViewportOverlayData( + const SceneViewportOverlayData& lhs, + const SceneViewportOverlayData& rhs) { + constexpr float kEpsilon = 1e-4f; + return lhs.valid == rhs.valid && + AreEqualSceneViewportVector3(lhs.cameraPosition, rhs.cameraPosition) && + AreEqualSceneViewportVector3(lhs.cameraForward, rhs.cameraForward) && + AreEqualSceneViewportVector3(lhs.cameraRight, rhs.cameraRight) && + AreEqualSceneViewportVector3(lhs.cameraUp, rhs.cameraUp) && + std::abs(lhs.verticalFovDegrees - rhs.verticalFovDegrees) <= kEpsilon && + std::abs(lhs.nearClipPlane - rhs.nearClipPlane) <= kEpsilon && + std::abs(lhs.farClipPlane - rhs.farClipPlane) <= kEpsilon && + std::abs(lhs.orbitDistance - rhs.orbitDistance) <= kEpsilon; +} Math::Vector3 GetSceneViewportOrientationAxisVector(SceneViewportOrientationAxis axis) { switch (axis) { @@ -71,7 +205,9 @@ public: entry = {}; } + m_sceneViewportEditorOverlayRenderer.Shutdown(); m_sceneViewCamera = {}; + ResetSceneViewEditorOverlayFrameData(); m_sceneViewLastRenderContext = {}; m_device = nullptr; m_backend = nullptr; @@ -84,6 +220,8 @@ public: entry.requestedWidth = 0; entry.requestedHeight = 0; } + m_sceneViewTransientTransformGizmoOverlay = {}; + m_sceneViewTransientTransformGizmoInputs = {}; } EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) override { @@ -212,6 +350,30 @@ public: return data; } + const SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext& context) override { + EnsureSceneViewEditorOverlayFrameData(context); + return m_sceneViewEditorOverlayFrameData; + } + + const SceneViewportOverlayFrameData& GetSceneViewInteractionOverlayFrameData( + IEditorContext& context, + const SceneViewportOverlayData& overlay, + const SceneViewportTransformGizmoHandleBuildInputs& inputs) override { + EnsureSceneViewEditorOverlayFrameData(context); + m_sceneViewInteractionOverlayFrameData = m_sceneViewEditorOverlayFrameData; + AppendSceneViewportOverlayFrameData( + m_sceneViewInteractionOverlayFrameData, + BuildSceneViewportTransformGizmoOverlayFrameData(overlay, inputs)); + return m_sceneViewInteractionOverlayFrameData; + } + + void SetSceneViewTransientTransformGizmoOverlayData( + const SceneViewportOverlayData& overlay, + const SceneViewportTransformGizmoHandleBuildInputs& inputs) override { + m_sceneViewTransientTransformGizmoOverlay = overlay; + m_sceneViewTransientTransformGizmoInputs = inputs; + } + void RenderRequestedViewports( IEditorContext& context, const Rendering::RenderContext& renderContext) override { @@ -256,6 +418,7 @@ private: struct SceneViewportRenderState { SceneViewportOverlayData overlay = {}; Rendering::BuiltinPostProcessRequest builtinPostProcess = {}; + SceneViewportOverlayFrameData editorOverlayFrameData = {}; std::vector selectedObjectIds; }; @@ -365,6 +528,86 @@ private: return BuildViewportColorSurface(entry.renderTargets); } + void ResetSceneViewEditorOverlayFrameData() { + m_sceneViewEditorOverlayFrameData = {}; + m_sceneViewInteractionOverlayFrameData = {}; + m_sceneViewEditorOverlayScene = nullptr; + m_sceneViewEditorOverlaySelectedObjectIds.clear(); + m_sceneViewEditorOverlayViewportWidth = 0u; + m_sceneViewEditorOverlayViewportHeight = 0u; + m_sceneViewEditorOverlayContentSignature = 0u; + m_sceneViewEditorOverlayCached = false; + } + + void ResolveSceneViewEditorOverlayViewportSize( + const ViewportEntry& entry, + uint32_t& outWidth, + uint32_t& outHeight) const { + outWidth = entry.requestedWidth > 0u ? entry.requestedWidth : entry.renderTargets.width; + outHeight = entry.requestedHeight > 0u ? entry.requestedHeight : entry.renderTargets.height; + } + + bool ShouldRebuildSceneViewEditorOverlayFrameData( + const Components::Scene* scene, + const SceneViewportOverlayData& overlay, + uint32_t viewportWidth, + uint32_t viewportHeight, + const std::vector& 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 selectedObjectIds = context.GetSelectionManager().GetSelectedEntities(); + const uint64_t contentSignature = + BuildSceneViewEditorOverlayContentSignature(scene, selectedObjectIds); + if (!ShouldRebuildSceneViewEditorOverlayFrameData( + scene, + overlay, + viewportWidth, + viewportHeight, + selectedObjectIds, + contentSignature)) { + return; + } + + m_sceneViewEditorOverlayFrameData = {}; + m_sceneViewEditorOverlayFrameData.overlay = overlay; + if (scene != nullptr && overlay.valid && viewportWidth > 0u && viewportHeight > 0u) { + m_sceneViewEditorOverlayFrameData = SceneViewportOverlayBuilder::Build( + context, + overlay, + viewportWidth, + viewportHeight, + selectedObjectIds); + } + + m_sceneViewEditorOverlayScene = scene; + m_sceneViewEditorOverlaySelectedObjectIds = selectedObjectIds; + m_sceneViewEditorOverlayViewportWidth = viewportWidth; + m_sceneViewEditorOverlayViewportHeight = viewportHeight; + m_sceneViewEditorOverlayContentSignature = contentSignature; + m_sceneViewEditorOverlayCached = true; + } + void ApplyViewportRenderFailure( ViewportEntry& entry, const Rendering::RenderContext& renderContext, @@ -399,6 +642,7 @@ private: } outState.selectedObjectIds = context.GetSelectionManager().GetSelectedEntities(); + outState.editorOverlayFrameData = GetSceneViewEditorOverlayFrameData(context); const SceneViewportBuiltinPostProcessBuildResult builtinPostProcess = BuildSceneViewportBuiltinPostProcess( outState.overlay, @@ -456,6 +700,18 @@ private: &sceneState.builtinPostProcess, nullptr, requests[0]); + SceneViewportOverlayFrameData renderOverlayFrameData = sceneState.editorOverlayFrameData; + AppendSceneViewportOverlayFrameData( + renderOverlayFrameData, + BuildSceneViewTransientTransformGizmoOverlayFrameData()); + Rendering::RenderPassSequence overlayPassSequence = {}; + if (renderOverlayFrameData.HasOverlayPrimitives()) { + overlayPassSequence.AddPass( + CreateSceneViewportEditorOverlayPass( + m_sceneViewportEditorOverlayRenderer, + renderOverlayFrameData)); + requests[0].overlayPasses = &overlayPassSequence; + } requests[0].hasClearColorOverride = true; requests[0].clearColorOverride = Math::Color(0.27f, 0.27f, 0.27f, 1.0f); @@ -600,12 +856,29 @@ private: }); } + SceneViewportOverlayFrameData BuildSceneViewTransientTransformGizmoOverlayFrameData() const { + return BuildSceneViewportTransformGizmoOverlayFrameData( + m_sceneViewTransientTransformGizmoOverlay, + m_sceneViewTransientTransformGizmoInputs); + } + UI::ImGuiBackendBridge* m_backend = nullptr; RHI::RHIDevice* m_device = nullptr; std::unique_ptr m_sceneRenderer; Rendering::RenderContext m_sceneViewLastRenderContext = {}; std::array m_entries = {}; SceneViewCameraState m_sceneViewCamera; + SceneViewportOverlayFrameData m_sceneViewEditorOverlayFrameData = {}; + SceneViewportOverlayFrameData m_sceneViewInteractionOverlayFrameData = {}; + SceneViewportOverlayData m_sceneViewTransientTransformGizmoOverlay = {}; + SceneViewportTransformGizmoHandleBuildInputs m_sceneViewTransientTransformGizmoInputs = {}; + const Components::Scene* m_sceneViewEditorOverlayScene = nullptr; + std::vector 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 diff --git a/editor/src/panels/GameViewPanel.cpp b/editor/src/panels/GameViewPanel.cpp index 0c7a12df..cbcd1114 100644 --- a/editor/src/panels/GameViewPanel.cpp +++ b/editor/src/panels/GameViewPanel.cpp @@ -1,12 +1,166 @@ #include "Actions/ActionRouting.h" +#include "Core/EventBus.h" +#include "Core/EditorEvents.h" #include "GameViewPanel.h" #include "ViewportPanelContent.h" #include "UI/UI.h" + +#include + #include namespace XCEngine { namespace Editor { +namespace { + +void SetKeyState(GameViewInputFrameEvent& event, XCEngine::Input::KeyCode key, bool isDown) { + const size_t index = static_cast(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(button); + if (index < event.mouseButtonDown.size()) { + event.mouseButtonDown[index] = isDown; + } +} + +void FillGameViewKeyboardState(const ImGuiIO& io, GameViewInputFrameEvent& event) { + using XCEngine::Input::KeyCode; + + const struct KeyMapping { + ImGuiKey imguiKey; + KeyCode keyCode; + } keyMappings[] = { + {ImGuiKey_A, KeyCode::A}, + {ImGuiKey_B, KeyCode::B}, + {ImGuiKey_C, KeyCode::C}, + {ImGuiKey_D, KeyCode::D}, + {ImGuiKey_E, KeyCode::E}, + {ImGuiKey_F, KeyCode::F}, + {ImGuiKey_G, KeyCode::G}, + {ImGuiKey_H, KeyCode::H}, + {ImGuiKey_I, KeyCode::I}, + {ImGuiKey_J, KeyCode::J}, + {ImGuiKey_K, KeyCode::K}, + {ImGuiKey_L, KeyCode::L}, + {ImGuiKey_M, KeyCode::M}, + {ImGuiKey_N, KeyCode::N}, + {ImGuiKey_O, KeyCode::O}, + {ImGuiKey_P, KeyCode::P}, + {ImGuiKey_Q, KeyCode::Q}, + {ImGuiKey_R, KeyCode::R}, + {ImGuiKey_S, KeyCode::S}, + {ImGuiKey_T, KeyCode::T}, + {ImGuiKey_U, KeyCode::U}, + {ImGuiKey_V, KeyCode::V}, + {ImGuiKey_W, KeyCode::W}, + {ImGuiKey_X, KeyCode::X}, + {ImGuiKey_Y, KeyCode::Y}, + {ImGuiKey_Z, KeyCode::Z}, + {ImGuiKey_0, KeyCode::Zero}, + {ImGuiKey_1, KeyCode::One}, + {ImGuiKey_2, KeyCode::Two}, + {ImGuiKey_3, KeyCode::Three}, + {ImGuiKey_4, KeyCode::Four}, + {ImGuiKey_5, KeyCode::Five}, + {ImGuiKey_6, KeyCode::Six}, + {ImGuiKey_7, KeyCode::Seven}, + {ImGuiKey_8, KeyCode::Eight}, + {ImGuiKey_9, KeyCode::Nine}, + {ImGuiKey_Space, KeyCode::Space}, + {ImGuiKey_Tab, KeyCode::Tab}, + {ImGuiKey_Enter, KeyCode::Enter}, + {ImGuiKey_Escape, KeyCode::Escape}, + {ImGuiKey_LeftShift, KeyCode::LeftShift}, + {ImGuiKey_RightShift, KeyCode::RightShift}, + {ImGuiKey_LeftCtrl, KeyCode::LeftCtrl}, + {ImGuiKey_RightCtrl, KeyCode::RightCtrl}, + {ImGuiKey_LeftAlt, KeyCode::LeftAlt}, + {ImGuiKey_RightAlt, KeyCode::RightAlt}, + {ImGuiKey_UpArrow, KeyCode::Up}, + {ImGuiKey_DownArrow, KeyCode::Down}, + {ImGuiKey_LeftArrow, KeyCode::Left}, + {ImGuiKey_RightArrow, KeyCode::Right}, + {ImGuiKey_Home, KeyCode::Home}, + {ImGuiKey_End, KeyCode::End}, + {ImGuiKey_PageUp, KeyCode::PageUp}, + {ImGuiKey_PageDown, KeyCode::PageDown}, + {ImGuiKey_Delete, KeyCode::Delete}, + {ImGuiKey_Backspace, KeyCode::Backspace}, + {ImGuiKey_F1, KeyCode::F1}, + {ImGuiKey_F2, KeyCode::F2}, + {ImGuiKey_F3, KeyCode::F3}, + {ImGuiKey_F4, KeyCode::F4}, + {ImGuiKey_F5, KeyCode::F5}, + {ImGuiKey_F6, KeyCode::F6}, + {ImGuiKey_F7, KeyCode::F7}, + {ImGuiKey_F8, KeyCode::F8}, + {ImGuiKey_F9, KeyCode::F9}, + {ImGuiKey_F10, KeyCode::F10}, + {ImGuiKey_F11, KeyCode::F11}, + {ImGuiKey_F12, KeyCode::F12}, + {ImGuiKey_Minus, KeyCode::Minus}, + {ImGuiKey_Equal, KeyCode::Equals}, + {ImGuiKey_LeftBracket, KeyCode::BracketLeft}, + {ImGuiKey_RightBracket, KeyCode::BracketRight}, + {ImGuiKey_Semicolon, KeyCode::Semicolon}, + {ImGuiKey_Apostrophe, KeyCode::Quote}, + {ImGuiKey_Comma, KeyCode::Comma}, + {ImGuiKey_Period, KeyCode::Period}, + {ImGuiKey_Slash, KeyCode::Slash}, + {ImGuiKey_Backslash, KeyCode::Backslash}, + {ImGuiKey_GraveAccent, KeyCode::Backtick}, + }; + + for (const KeyMapping& mapping : keyMappings) { + SetKeyState(event, mapping.keyCode, ImGui::IsKeyDown(mapping.imguiKey)); + } +} + +void FillGameViewMouseState(const ImGuiIO& io, GameViewInputFrameEvent& event) { + (void)io; + SetMouseButtonState(event, XCEngine::Input::MouseButton::Left, ImGui::IsMouseDown(ImGuiMouseButton_Left)); + SetMouseButtonState(event, XCEngine::Input::MouseButton::Right, ImGui::IsMouseDown(ImGuiMouseButton_Right)); + SetMouseButtonState(event, XCEngine::Input::MouseButton::Middle, ImGui::IsMouseDown(ImGuiMouseButton_Middle)); +} + +GameViewInputFrameEvent BuildGameViewInputFrame(const ViewportPanelContentResult& content) { + GameViewInputFrameEvent event = {}; + if (!content.hasViewportArea) { + return event; + } + + const ImGuiIO& io = ImGui::GetIO(); + event.hovered = content.hovered; + event.focused = content.focused; + event.mousePosition = XCEngine::Math::Vector2( + io.MousePos.x - content.itemMin.x, + io.MousePos.y - content.itemMin.y); + event.mouseDelta = XCEngine::Math::Vector2(io.MouseDelta.x, io.MouseDelta.y); + event.mouseWheel = content.hovered ? io.MouseWheel : 0.0f; + + if (event.hovered || event.focused) { + FillGameViewKeyboardState(io, event); + FillGameViewMouseState(io, event); + } + + return event; +} + +void PublishGameViewInputFrame(IEditorContext* context, const GameViewInputFrameEvent& event) { + if (context == nullptr) { + return; + } + + context->GetEventBus().Publish(event); +} + +} // namespace + GameViewPanel::GameViewPanel() : Panel("Game") {} void GameViewPanel::Render() { @@ -14,10 +168,12 @@ void GameViewPanel::Render() { UI::PanelWindowScope panel(m_name.c_str()); ImGui::PopStyleVar(); if (!panel.IsOpen()) { + PublishGameViewInputFrame(m_context, GameViewInputFrameEvent{}); return; } - RenderViewportPanelContent(*m_context, EditorViewportKind::Game); + const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Game); + PublishGameViewInputFrame(m_context, BuildGameViewInputFrame(content)); Actions::ObserveInactiveActionRoute(*m_context); } diff --git a/editor/src/panels/InspectorPanel.h b/editor/src/panels/InspectorPanel.h index 5c4d2e4b..3ba6dc1d 100644 --- a/editor/src/panels/InspectorPanel.h +++ b/editor/src/panels/InspectorPanel.h @@ -1,10 +1,18 @@ #pragma once #include "Panel.h" +#include "Core/AssetItem.h" +#include "Core/EditorActionRoute.h" #include "UI/PopupState.h" +#include +#include + +#include #include #include +#include +#include namespace XCEngine { namespace Components { @@ -23,7 +31,55 @@ public: void OnDetach() override; void Render() override; + enum class SubjectMode { + None, + GameObject, + MaterialAsset, + UnsupportedAsset + }; + + struct MaterialTagEditRow { + std::array name{}; + std::array value{}; + }; + + struct MaterialAssetState { + std::string assetPath; + std::string assetFullPath; + std::string assetName; + std::array shaderPath{}; + std::array shaderPass{}; + int renderQueue = static_cast(::XCEngine::Resources::MaterialRenderQueue::Geometry); + ::XCEngine::Resources::MaterialRenderState renderState{}; + std::vector 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(::XCEngine::Resources::MaterialRenderQueue::Geometry); + renderState = ::XCEngine::Resources::MaterialRenderState(); + tags.clear(); + errorMessage.clear(); + dirty = false; + loaded = false; + } + }; + private: + void SyncSubject(); + void SetSubjectMode(SubjectMode mode); + void InspectMaterialAsset(const AssetItemPtr& item); + void ClearMaterialAsset(); + void RenderMaterialAsset(); + void RenderUnsupportedAsset(); + bool SaveMaterialAsset(); + void ReloadMaterialAsset(); void RenderGameObject(::XCEngine::Components::GameObject* gameObject); void RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject); void RenderEmptyState(const char* title, const char* subtitle = nullptr); @@ -31,6 +87,11 @@ private: uint64_t m_selectionHandlerId = 0; uint64_t m_selectedEntityId = 0; + SubjectMode m_subjectMode = SubjectMode::None; + EditorActionRoute m_lastExplicitRoute = EditorActionRoute::None; + AssetItemPtr m_selectedAssetItem; + ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Material> m_selectedMaterial; + MaterialAssetState m_materialAssetState; UI::DeferredPopupState m_addComponentPopup; std::function m_deferredContextAction; }; diff --git a/editor/src/panels/MenuBar.cpp b/editor/src/panels/MenuBar.cpp index dea5beef..76ce97e9 100644 --- a/editor/src/panels/MenuBar.cpp +++ b/editor/src/panels/MenuBar.cpp @@ -21,6 +21,10 @@ constexpr float kRunToolbarHeight = 32.0f; constexpr float kRunToolbarButtonExtent = 24.0f; constexpr float kRunToolbarButtonSpacing = 8.0f; constexpr float kRunToolbarIconInset = 3.0f; +constexpr ImVec2 kMainMenuFramePadding(6.0f, 2.0f); +constexpr ImVec4 kMainMenuTextColor(0.08f, 0.08f, 0.08f, 1.0f); +constexpr ImVec4 kMainMenuItemHoveredColor(0.88f, 0.88f, 0.88f, 1.0f); +constexpr ImVec4 kMainMenuItemActiveColor(0.82f, 0.82f, 0.82f, 1.0f); constexpr ImVec4 kRunToolbarBackgroundColor(0.1f, 0.1f, 0.1f, 1.0f); std::string BuildRunToolbarIconPath(const char* fileName) { @@ -108,9 +112,14 @@ void MenuBar::RenderChrome() { } Actions::HandleMenuBarShortcuts(*m_context); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.08f, 0.08f, 0.08f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, kMainMenuFramePadding); + ImGui::PushStyleColor(ImGuiCol_Text, kMainMenuTextColor); + ImGui::PushStyleColor(ImGuiCol_Header, kMainMenuItemActiveColor); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, kMainMenuItemHoveredColor); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, kMainMenuItemActiveColor); Actions::DrawMainMenuBar(*m_context, m_aboutPopup); - ImGui::PopStyleColor(); + ImGui::PopStyleColor(4); + ImGui::PopStyleVar(); RenderRunToolbar(); } diff --git a/editor/src/panels/ProjectPanel.cpp b/editor/src/panels/ProjectPanel.cpp index f41d6191..606d934b 100644 --- a/editor/src/panels/ProjectPanel.cpp +++ b/editor/src/panels/ProjectPanel.cpp @@ -5,11 +5,16 @@ #include "Core/IEditorContext.h" #include "Core/IProjectManager.h" #include "Core/AssetItem.h" +#include "Platform/Win32Utf8.h" +#include "Utils/ProjectFileUtils.h" #include "UI/UI.h" #include #include +#include #include +#include +#include namespace XCEngine { namespace Editor { @@ -86,6 +91,52 @@ UI::AssetTileOptions MakeProjectAssetTileOptions() { return options; } +std::string BuildProjectRelativeAssetPath(const std::string& projectPath, const std::string& fullPath) { + if (projectPath.empty() || fullPath.empty()) { + return {}; + } + + return ProjectFileUtils::MakeProjectRelativePath(projectPath, fullPath); +} + +bool ShowPathInExplorer(const std::string& fullPath, bool selectTarget) { + if (fullPath.empty()) { + return false; + } + + namespace fs = std::filesystem; + + std::error_code ec; + const fs::path targetPath = fs::path(Platform::Utf8ToWide(fullPath)).lexically_normal(); + if (targetPath.empty() || !fs::exists(targetPath, ec)) { + return false; + } + + HINSTANCE result = nullptr; + if (selectTarget) { + const std::wstring parameters = L"/select,\"" + targetPath.native() + L"\""; + const std::wstring workingDirectory = targetPath.parent_path().native(); + result = ShellExecuteW( + nullptr, + L"open", + L"explorer.exe", + parameters.c_str(), + workingDirectory.empty() ? nullptr : workingDirectory.c_str(), + SW_SHOWNORMAL); + } else { + const std::wstring workingDirectory = targetPath.parent_path().native(); + result = ShellExecuteW( + nullptr, + L"open", + targetPath.c_str(), + nullptr, + workingDirectory.empty() ? nullptr : workingDirectory.c_str(), + SW_SHOWNORMAL); + } + + return reinterpret_cast(result) > 32; +} + } // namespace ProjectPanel::ProjectPanel() : Panel("Project") { @@ -186,6 +237,93 @@ void ProjectPanel::CancelRename() { m_renameState.Cancel(); } +ProjectPanel::ContextMenuTarget ProjectPanel::BuildContextMenuTarget( + IProjectManager& manager, + const AssetItemPtr& item) const { + ContextMenuTarget target; + target.item = item; + if (item) { + target.subjectPath = item->fullPath; + target.createFolderPath = item->isFolder ? item->fullPath : std::string(); + target.showInExplorerSelect = true; + return target; + } + + if (const AssetItemPtr currentFolder = manager.GetCurrentFolder()) { + target.subjectPath = currentFolder->fullPath; + target.createFolderPath = currentFolder->fullPath; + } + + return target; +} + +void ProjectPanel::DrawProjectContextMenu(IProjectManager& manager, const ContextMenuTarget& target) { + auto* managerPtr = &manager; + const bool canCreate = !target.createFolderPath.empty(); + const bool canShowInExplorer = !target.subjectPath.empty(); + const bool canOpen = target.item != nullptr && Commands::CanOpenAsset(target.item); + const bool canDelete = target.item != nullptr; + const bool canRename = target.item != nullptr; + const std::string copyPath = BuildProjectRelativeAssetPath( + m_context ? m_context->GetProjectPath() : std::string(), + target.subjectPath); + const bool canCopyPath = !copyPath.empty(); + + const auto queueCreateAsset = [this, managerPtr, target](auto createFn) { + QueueDeferredAction(m_deferredContextAction, [this, managerPtr, target, createFn]() { + if (!target.createFolderPath.empty() && target.item && target.item->isFolder) { + managerPtr->NavigateToFolder(target.item); + } + + if (AssetItemPtr createdItem = createFn(*managerPtr)) { + BeginRename(createdItem); + } + }); + }; + + UI::DrawContextSubmenu("Create", [&]() { + Actions::DrawMenuAction(Actions::MakeAction("Folder", nullptr, false, canCreate), [&]() { + queueCreateAsset([](IProjectManager& createManager) { + return Commands::CreateFolder(createManager, "New Folder"); + }); + }); + + Actions::DrawMenuAction(Actions::MakeAction("Material", nullptr, false, canCreate), [&]() { + queueCreateAsset([](IProjectManager& createManager) { + return Commands::CreateMaterial(createManager, "New Material"); + }); + }); + }, canCreate); + + Actions::DrawMenuAction(Actions::MakeAction("Show in Explore", nullptr, false, canShowInExplorer), [&]() { + QueueDeferredAction(m_deferredContextAction, [target]() { + ShowPathInExplorer(target.subjectPath, target.showInExplorerSelect); + }); + }); + + Actions::DrawMenuAction(Actions::MakeOpenAssetAction(canOpen), [&]() { + QueueDeferredAction(m_deferredContextAction, [this, target]() { + Actions::OpenProjectAsset(*m_context, target.item); + }); + }); + + Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(canDelete), [&]() { + QueueDeferredAction(m_deferredContextAction, [this, target]() { + Commands::DeleteAsset(m_context->GetProjectManager(), target.item); + }); + }); + + Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, canRename), [&]() { + QueueDeferredAction(m_deferredContextAction, [this, target]() { + BeginRename(target.item); + }); + }); + + Actions::DrawMenuAction(Actions::MakeAction("Copy Path", nullptr, false, canCopyPath), [copyPath]() { + ImGui::SetClipboardText(copyPath.c_str()); + }); +} + void ProjectPanel::Render() { UI::PanelWindowScope panel(m_name.c_str()); if (!panel.IsOpen()) { @@ -265,7 +403,6 @@ void ProjectPanel::RenderToolbar() { } void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) { - auto* managerPtr = &manager; ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor()); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding()); const bool open = ImGui::BeginChild("ProjectFolderTree", ImVec2(m_navigationWidth, 0.0f), false); @@ -287,13 +424,7 @@ void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) { } if (UI::BeginContextMenuForWindow("##ProjectFolderTreeContext")) { - Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() { - QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() { - if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) { - BeginRename(createdFolder); - } - }); - }); + DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr)); UI::EndContextMenu(); } @@ -351,7 +482,6 @@ void ProjectPanel::RenderFolderTreeNode( } void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { - auto* managerPtr = &manager; ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor()); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); const bool open = ImGui::BeginChild("ProjectBrowser", ImVec2(0.0f, 0.0f), false); @@ -393,7 +523,6 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { } const float tileWidth = UI::AssetTileSize().x; - const float tileHeight = UI::AssetTileSize().y; const float spacing = UI::AssetGridSpacing().x; const float rowSpacing = UI::AssetGridSpacing().y; const float panelWidth = ImGui::GetContentRegionAvail().x; @@ -402,19 +531,41 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { columns = 1; } + const int rowCount = visibleItems.empty() ? 0 : (static_cast(visibleItems.size()) + columns - 1) / columns; + std::vector rowHeights(static_cast(rowCount), UI::AssetTileSize().y); + AssetItemPtr pendingSelection; AssetItemPtr pendingOpenTarget; const std::string selectedItemPath = manager.GetSelectedItemPath(); const ImVec2 gridOrigin = ImGui::GetCursorPos(); + for (int visibleIndex = 0; visibleIndex < static_cast(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(row)] = (std::max)(rowHeights[static_cast(row)], tileSize.y); + } + + std::vector rowOffsets(static_cast(rowCount), gridOrigin.y); + float nextRowY = gridOrigin.y; + for (int row = 0; row < rowCount; ++row) { + rowOffsets[static_cast(row)] = nextRowY; + nextRowY += rowHeights[static_cast(row)] + rowSpacing; + } + + int renderedItemCount = 0; for (int visibleIndex = 0; visibleIndex < static_cast(visibleItems.size()); ++visibleIndex) { const int column = visibleIndex % columns; const int row = visibleIndex / columns; ImGui::SetCursorPos(ImVec2( gridOrigin.x + column * (tileWidth + spacing), - gridOrigin.y + row * (tileHeight + rowSpacing))); + rowOffsets[static_cast(row)])); const AssetItemPtr& item = visibleItems[visibleIndex]; const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath); + ++renderedItemCount; if (interaction.clicked) { pendingSelection = item; } @@ -427,9 +578,14 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { } } - if (!visibleItems.empty()) { - const int rowCount = (static_cast(visibleItems.size()) + columns - 1) / columns; - ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing); + if (renderedItemCount > 0) { + const int renderedRowCount = (renderedItemCount + columns - 1) / columns; + float contentBottom = gridOrigin.y; + for (int row = 0; row < renderedRowCount; ++row) { + contentBottom = rowOffsets[static_cast(row)] + rowHeights[static_cast(row)]; + } + ImGui::SetCursorPos(ImVec2(gridOrigin.x, contentBottom)); + ImGui::Dummy(ImVec2(0.0f, 0.0f)); } if (visibleItems.empty() && !searchQuery.Empty()) { @@ -449,21 +605,7 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { } if (UI::BeginContextMenuForWindow("##ProjectBrowserContext")) { - Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() { - QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() { - if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) { - BeginRename(createdFolder); - } - }); - }); - if (manager.CanNavigateBack()) { - Actions::DrawMenuSeparator(); - Actions::DrawMenuAction(Actions::MakeNavigateBackAction(true), [&]() { - QueueDeferredAction(m_deferredContextAction, [managerPtr]() { - managerPtr->NavigateBack(); - }); - }); - } + DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr)); UI::EndContextMenu(); } @@ -570,21 +712,7 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem Actions::BeginProjectAssetDrag(item, iconKind); if (UI::BeginContextMenuForLastItem("##ProjectItemContext")) { - Actions::DrawMenuAction(Actions::MakeOpenAssetAction(Commands::CanOpenAsset(item)), [&]() { - QueueDeferredAction(m_deferredContextAction, [this, item]() { - Actions::OpenProjectAsset(*m_context, item); - }); - }); - Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, item != nullptr), [&]() { - QueueDeferredAction(m_deferredContextAction, [this, item]() { - BeginRename(item); - }); - }); - Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(item != nullptr), [&]() { - QueueDeferredAction(m_deferredContextAction, [this, item]() { - Commands::DeleteAsset(m_context->GetProjectManager(), item); - }); - }); + DrawProjectContextMenu(m_context->GetProjectManager(), BuildContextMenuTarget(m_context->GetProjectManager(), item)); UI::EndContextMenu(); } diff --git a/editor/src/panels/ProjectPanel.h b/editor/src/panels/ProjectPanel.h index c4e78a96..5a934504 100644 --- a/editor/src/panels/ProjectPanel.h +++ b/editor/src/panels/ProjectPanel.h @@ -41,12 +41,21 @@ private: bool openRequested = false; }; + struct ContextMenuTarget { + AssetItemPtr item; + std::string subjectPath; + std::string createFolderPath; + bool showInExplorerSelect = false; + }; + void BeginAssetDragDropFrame(); void RegisterFolderDropTarget(IProjectManager& manager, const AssetItemPtr& folder); void FinalizeAssetDragDrop(IProjectManager& manager); void BeginRename(const AssetItemPtr& item); bool CommitRename(IProjectManager& manager); void CancelRename(); + ContextMenuTarget BuildContextMenuTarget(IProjectManager& manager, const AssetItemPtr& item) const; + void DrawProjectContextMenu(IProjectManager& manager, const ContextMenuTarget& target); void RenderToolbar(); void RenderFolderTreePane(IProjectManager& manager); void RenderFolderTreeNode(IProjectManager& manager, const AssetItemPtr& folder, const std::string& currentFolderPath); diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index dae0b540..5de56cd8 100644 --- a/editor/src/panels/SceneViewPanel.cpp +++ b/editor/src/panels/SceneViewPanel.cpp @@ -3,8 +3,13 @@ #include "Core/ISceneManager.h" #include "Core/ISelectionManager.h" #include "SceneViewPanel.h" +#include "Viewport/SceneViewportEditorOverlayData.h" +#include "Viewport/SceneViewportOverlayHandleBuilder.h" +#include "Viewport/SceneViewportOverlayHitTester.h" +#include "Viewport/SceneViewportMath.h" #include "Viewport/SceneViewportOrientationGizmo.h" #include "Viewport/SceneViewportOverlayRenderer.h" +#include "Viewport/SceneViewportTransformGizmoFrameBuilder.h" #include "ViewportPanelContent.h" #include "Platform/Win32Utf8.h" #include "UI/UI.h" @@ -13,9 +18,11 @@ #include +#include #include #include #include +#include namespace XCEngine { namespace Editor { @@ -28,13 +35,209 @@ struct SceneViewportToolOverlayResult { SceneViewportToolMode clickedTool = SceneViewportToolMode::Move; }; -enum class SceneViewportActiveGizmoKind : uint8_t { +enum class SceneViewportInteractionKind : uint8_t { None = 0, - Move, - Rotate, - Scale + MoveGizmo, + RotateGizmo, + ScaleGizmo, + OrientationGizmo, + SceneIcon }; +struct SceneViewportInteractionCandidate { + SceneViewportInteractionKind kind = SceneViewportInteractionKind::None; + int priority = 0; + int secondaryPriority = 0; + float distanceSq = Math::FLOAT_MAX; + float depth = Math::FLOAT_MAX; + uint64_t entityId = 0; + SceneViewportGizmoAxis moveAxis = SceneViewportGizmoAxis::None; + SceneViewportGizmoPlane movePlane = SceneViewportGizmoPlane::None; + SceneViewportRotateGizmoAxis rotateAxis = SceneViewportRotateGizmoAxis::None; + SceneViewportScaleGizmoHandle scaleHandle = SceneViewportScaleGizmoHandle::None; + SceneViewportOrientationAxis orientationAxis = SceneViewportOrientationAxis::None; + + bool HasHit() const { + return kind != SceneViewportInteractionKind::None; + } +}; + +const char* GetSceneViewportPivotModeLabel(SceneViewportPivotMode mode) { + return mode == SceneViewportPivotMode::Pivot ? "Pivot" : "Center"; +} + +const char* GetSceneViewportTransformSpaceModeLabel(SceneViewportTransformSpaceMode mode) { + return mode == SceneViewportTransformSpaceMode::Global ? "Global" : "Local"; +} + +SceneViewportActiveGizmoKind ToActiveGizmoKind(SceneViewportInteractionKind kind) { + switch (kind) { + case SceneViewportInteractionKind::MoveGizmo: + return SceneViewportActiveGizmoKind::Move; + case SceneViewportInteractionKind::RotateGizmo: + return SceneViewportActiveGizmoKind::Rotate; + case SceneViewportInteractionKind::ScaleGizmo: + return SceneViewportActiveGizmoKind::Scale; + case SceneViewportInteractionKind::OrientationGizmo: + case SceneViewportInteractionKind::SceneIcon: + case SceneViewportInteractionKind::None: + default: + return SceneViewportActiveGizmoKind::None; + } +} + +bool IsBetterSceneViewportInteractionCandidate( + const SceneViewportInteractionCandidate& candidate, + const SceneViewportInteractionCandidate& current) { + constexpr float kMetricEpsilon = 0.001f; + + if (!candidate.HasHit()) { + return false; + } + + if (!current.HasHit()) { + return true; + } + + if (candidate.priority != current.priority) { + return candidate.priority > current.priority; + } + + if (candidate.distanceSq + kMetricEpsilon < current.distanceSq) { + return true; + } + if (current.distanceSq + kMetricEpsilon < candidate.distanceSq) { + return false; + } + + if (candidate.depth + kMetricEpsilon < current.depth) { + return true; + } + if (current.depth + kMetricEpsilon < candidate.depth) { + return false; + } + + return candidate.secondaryPriority > current.secondaryPriority; +} + +void AccumulateSceneViewportInteractionCandidate( + const SceneViewportInteractionCandidate& candidate, + SceneViewportInteractionCandidate& bestCandidate) { + if (IsBetterSceneViewportInteractionCandidate(candidate, bestCandidate)) { + bestCandidate = candidate; + } +} + +SceneViewportInteractionCandidate BuildOrientationGizmoInteractionCandidate( + SceneViewportOrientationAxis axis) { + SceneViewportInteractionCandidate candidate = {}; + if (axis == SceneViewportOrientationAxis::None) { + return candidate; + } + + candidate.kind = SceneViewportInteractionKind::OrientationGizmo; + candidate.priority = 200; + candidate.distanceSq = 0.0f; + candidate.depth = 0.0f; + candidate.orientationAxis = axis; + return candidate; +} + +SceneViewportInteractionCandidate BuildOverlayHandleInteractionCandidate( + const SceneViewportOverlayHandleHitResult& hitResult) { + SceneViewportInteractionCandidate candidate = {}; + if (!hitResult.HasHit()) { + return candidate; + } + + candidate.priority = hitResult.priority; + candidate.distanceSq = hitResult.distanceSq; + candidate.depth = hitResult.depth; + candidate.entityId = hitResult.entityId; + switch (hitResult.kind) { + case SceneViewportOverlayHandleKind::SceneIcon: + candidate.kind = SceneViewportInteractionKind::SceneIcon; + return candidate; + case SceneViewportOverlayHandleKind::MoveAxis: + candidate.kind = SceneViewportInteractionKind::MoveGizmo; + candidate.moveAxis = static_cast(hitResult.handleId); + return candidate; + case SceneViewportOverlayHandleKind::MovePlane: + candidate.kind = SceneViewportInteractionKind::MoveGizmo; + candidate.movePlane = static_cast(hitResult.handleId); + return candidate; + case SceneViewportOverlayHandleKind::RotateAxis: + candidate.kind = SceneViewportInteractionKind::RotateGizmo; + candidate.rotateAxis = static_cast(hitResult.handleId); + return candidate; + case SceneViewportOverlayHandleKind::ScaleAxis: + case SceneViewportOverlayHandleKind::ScaleUniform: + candidate.kind = SceneViewportInteractionKind::ScaleGizmo; + candidate.scaleHandle = static_cast(hitResult.handleId); + return candidate; + case SceneViewportOverlayHandleKind::None: + default: + return SceneViewportInteractionCandidate{}; + } + + return candidate; +} + +float GetSceneToolbarToggleWidth(const char* firstLabel, const char* secondLabel) { + constexpr float kHorizontalPadding = 10.0f; + constexpr float kMinWidth = 68.0f; + const float maxLabelWidth = (std::max)( + ImGui::CalcTextSize(firstLabel).x, + ImGui::CalcTextSize(secondLabel).x); + return (std::max)(kMinWidth, maxLabelWidth + kHorizontalPadding * 2.0f); +} + +void RenderSceneViewportTopBar( + SceneViewportPivotMode& pivotMode, + SceneViewportTransformSpaceMode& transformSpaceMode) { + constexpr float kSceneToolbarHeight = 24.0f; + constexpr float kSceneToolbarPaddingY = 0.0f; + constexpr float kSceneToolbarButtonHeight = kSceneToolbarHeight - kSceneToolbarPaddingY * 2.0f; + constexpr ImVec2 kSceneToolbarButtonFramePadding(8.0f, 1.0f); + + UI::PanelToolbarScope toolbar( + "SceneToolbar", + kSceneToolbarHeight, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse, + true, + ImVec2(UI::ToolbarPadding().x, kSceneToolbarPaddingY), + ImVec2(0.0f, UI::ToolbarItemSpacing().y), + ImVec4(0.23f, 0.23f, 0.23f, 1.0f)); + if (!toolbar.IsOpen()) { + return; + } + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, kSceneToolbarButtonFramePadding); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); + + if (UI::ToolbarButton( + GetSceneViewportPivotModeLabel(pivotMode), + true, + ImVec2(GetSceneToolbarToggleWidth("Pivot", "Center"), kSceneToolbarButtonHeight))) { + pivotMode = pivotMode == SceneViewportPivotMode::Pivot + ? SceneViewportPivotMode::Center + : SceneViewportPivotMode::Pivot; + } + + ImGui::SameLine(0.0f, 0.0f); + + if (UI::ToolbarButton( + GetSceneViewportTransformSpaceModeLabel(transformSpaceMode), + true, + ImVec2(GetSceneToolbarToggleWidth("Global", "Local"), kSceneToolbarButtonHeight))) { + transformSpaceMode = transformSpaceMode == SceneViewportTransformSpaceMode::Global + ? SceneViewportTransformSpaceMode::Local + : SceneViewportTransformSpaceMode::Global; + } + + ImGui::PopStyleVar(2); +} + const char* GetSceneViewportToolTooltip(SceneViewportToolMode toolMode) { switch (toolMode) { case SceneViewportToolMode::ViewMove: @@ -69,6 +272,19 @@ const char* GetSceneViewportToolIconBaseName(SceneViewportToolMode toolMode) { } } +std::string BuildSceneViewportIconPath(const char* iconBaseName, bool active = false) { + const std::filesystem::path exeDir( + XCEngine::Editor::Platform::Utf8ToWide(XCEngine::Editor::Platform::GetExecutableDirectoryUtf8())); + std::filesystem::path iconPath = + exeDir / L".." / L".." / L"resources" / L"Icons" / + std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(iconBaseName)); + if (active) { + iconPath += L"_on"; + } + iconPath += L".png"; + return XCEngine::Editor::Platform::WideToUtf8(iconPath.lexically_normal().wstring()); +} + const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode, bool active) { static std::string cachedPaths[5][2] = {}; const size_t toolIndex = static_cast(toolMode); @@ -78,16 +294,7 @@ const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode, return cachedPath; } - const std::filesystem::path exeDir( - XCEngine::Editor::Platform::Utf8ToWide(XCEngine::Editor::Platform::GetExecutableDirectoryUtf8())); - std::filesystem::path iconPath = - (exeDir / L".." / L".." / L"resources" / L"Icons" / - std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(GetSceneViewportToolIconBaseName(toolMode)))); - if (active) { - iconPath += L"_on"; - } - iconPath += L".png"; - cachedPath = XCEngine::Editor::Platform::WideToUtf8(iconPath.lexically_normal().wstring()); + cachedPath = BuildSceneViewportIconPath(GetSceneViewportToolIconBaseName(toolMode), active); return cachedPath; } @@ -205,72 +412,6 @@ bool ShouldBeginSceneViewportNavigationDrag( ImGui::IsMouseClicked(button); } -SceneViewportMoveGizmoContext BuildMoveGizmoContext( - IEditorContext& context, - const SceneViewportOverlayData& overlay, - const ViewportPanelContentResult& content, - const ImVec2& mousePosition) { - SceneViewportMoveGizmoContext gizmoContext = {}; - gizmoContext.overlay = overlay; - gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y); - gizmoContext.mousePosition = Math::Vector2( - mousePosition.x - content.itemMin.x, - mousePosition.y - content.itemMin.y); - - if (context.GetSelectionManager().GetSelectionCount() == 1) { - const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity(); - if (selectedEntity != 0) { - gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity); - } - } - - return gizmoContext; -} - -SceneViewportRotateGizmoContext BuildRotateGizmoContext( - IEditorContext& context, - const SceneViewportOverlayData& overlay, - const ViewportPanelContentResult& content, - const ImVec2& mousePosition) { - SceneViewportRotateGizmoContext gizmoContext = {}; - gizmoContext.overlay = overlay; - gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y); - gizmoContext.mousePosition = Math::Vector2( - mousePosition.x - content.itemMin.x, - mousePosition.y - content.itemMin.y); - - if (context.GetSelectionManager().GetSelectionCount() == 1) { - const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity(); - if (selectedEntity != 0) { - gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity); - } - } - - return gizmoContext; -} - -SceneViewportScaleGizmoContext BuildScaleGizmoContext( - IEditorContext& context, - const SceneViewportOverlayData& overlay, - const ViewportPanelContentResult& content, - const ImVec2& mousePosition) { - SceneViewportScaleGizmoContext gizmoContext = {}; - gizmoContext.overlay = overlay; - gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y); - gizmoContext.mousePosition = Math::Vector2( - mousePosition.x - content.itemMin.x, - mousePosition.y - content.itemMin.y); - - if (context.GetSelectionManager().GetSelectionCount() == 1) { - const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity(); - if (selectedEntity != 0) { - gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity); - } - } - - return gizmoContext; -} - } // namespace SceneViewPanel::SceneViewPanel() : Panel("Scene") {} @@ -283,6 +424,7 @@ void SceneViewPanel::Render() { return; } + RenderSceneViewportTopBar(m_pivotMode, m_transformSpaceMode); const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene); if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) { const ImGuiIO& io = ImGui::GetIO(); @@ -304,8 +446,20 @@ void SceneViewPanel::Render() { m_toolMode = toolOverlay.clickedTool; } - if (content.focused && !io.WantTextInput && !m_lookDragging && !m_panDragging) { - if (ImGui::IsKeyPressed(ImGuiKey_W, false)) { + const bool allowToolShortcut = !io.WantTextInput && !m_lookDragging && !m_panDragging; + if (allowToolShortcut) { + if (ImGui::IsKeyPressed(ImGuiKey_Q, false)) { + if (m_moveGizmo.IsActive()) { + m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); + } + if (m_rotateGizmo.IsActive()) { + m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); + } + if (m_scaleGizmo.IsActive()) { + m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); + } + m_toolMode = SceneViewportToolMode::ViewMove; + } else if (ImGui::IsKeyPressed(ImGuiKey_W, false)) { if (m_rotateGizmo.IsActive()) { m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); } @@ -337,103 +491,68 @@ void SceneViewPanel::Render() { const bool showingMoveGizmo = m_toolMode == SceneViewportToolMode::Move || usingTransformTool; const bool showingRotateGizmo = m_toolMode == SceneViewportToolMode::Rotate || usingTransformTool; const bool showingScaleGizmo = m_toolMode == SceneViewportToolMode::Scale || usingTransformTool; + const bool useCenterPivot = m_pivotMode == SceneViewportPivotMode::Center; + const bool localSpace = m_transformSpaceMode == SceneViewportTransformSpaceMode::Local; + const Math::Vector2 viewportSize(content.availableSize.x, content.availableSize.y); + const Math::Vector2 localMousePosition( + io.MousePos.x - content.itemMin.x, + io.MousePos.y - content.itemMin.y); SceneViewportOverlayData overlay = {}; - SceneViewportMoveGizmoContext moveGizmoContext = {}; - SceneViewportRotateGizmoContext rotateGizmoContext = {}; - SceneViewportScaleGizmoContext scaleGizmoContext = {}; + SceneViewportTransformGizmoFrameState gizmoFrameState = {}; + SceneViewportOverlayFrameData emptySceneOverlayFrameData = {}; SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None; if (hasInteractiveViewport) { overlay = viewportHostService->GetSceneViewOverlayData(); - if (showingMoveGizmo) { - moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos); - if (m_moveGizmo.IsActive() && - (moveGizmoContext.selectedObject == nullptr || - m_context->GetSelectionManager().GetSelectedEntity() != m_moveGizmo.GetActiveEntityId())) { - m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); - } - } else if (m_moveGizmo.IsActive()) { - m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); - } - - if (showingRotateGizmo) { - rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos); - if (m_rotateGizmo.IsActive() && - (rotateGizmoContext.selectedObject == nullptr || - m_context->GetSelectionManager().GetSelectedEntity() != m_rotateGizmo.GetActiveEntityId())) { - m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); - } - } else if (m_rotateGizmo.IsActive()) { - m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); - } - - if (showingScaleGizmo) { - scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos); - scaleGizmoContext.uniformOnly = usingTransformTool; - if (m_scaleGizmo.IsActive() && - (scaleGizmoContext.selectedObject == nullptr || - m_context->GetSelectionManager().GetSelectedEntity() != m_scaleGizmo.GetActiveEntityId())) { - m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); - } - } else if (m_scaleGizmo.IsActive()) { - m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); - } - - if (m_moveGizmo.IsActive()) { - activeGizmoKind = SceneViewportActiveGizmoKind::Move; - } else if (m_rotateGizmo.IsActive()) { - activeGizmoKind = SceneViewportActiveGizmoKind::Rotate; - } else if (m_scaleGizmo.IsActive()) { - activeGizmoKind = SceneViewportActiveGizmoKind::Scale; - } - - if (showingMoveGizmo) { - SceneViewportMoveGizmoContext updateContext = moveGizmoContext; - if (activeGizmoKind != SceneViewportActiveGizmoKind::None && - activeGizmoKind != SceneViewportActiveGizmoKind::Move) { - updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); - } - m_moveGizmo.Update(updateContext); - } - if (showingRotateGizmo) { - SceneViewportRotateGizmoContext updateContext = rotateGizmoContext; - if (activeGizmoKind != SceneViewportActiveGizmoKind::None && - activeGizmoKind != SceneViewportActiveGizmoKind::Rotate) { - updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); - } - m_rotateGizmo.Update(updateContext); - } - if (showingScaleGizmo) { - SceneViewportScaleGizmoContext updateContext = scaleGizmoContext; - if (activeGizmoKind != SceneViewportActiveGizmoKind::None && - activeGizmoKind != SceneViewportActiveGizmoKind::Scale) { - updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); - } - m_scaleGizmo.Update(updateContext); - } + gizmoFrameState = RefreshSceneViewportTransformGizmos( + *m_context, + overlay, + viewportSize, + localMousePosition, + useCenterPivot, + localSpace, + usingTransformTool, + showingMoveGizmo, + m_moveGizmo, + showingRotateGizmo, + m_rotateGizmo, + showingScaleGizmo, + m_scaleGizmo); + activeGizmoKind = gizmoFrameState.activeGizmoKind; } else { - if (m_moveGizmo.IsActive()) { - m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); - } - if (m_rotateGizmo.IsActive()) { - m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); - } - if (m_scaleGizmo.IsActive()) { - m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); - } + CancelSceneViewportTransformGizmoDrags(*m_context, m_moveGizmo, m_rotateGizmo, m_scaleGizmo); } - const bool moveGizmoHovering = showingMoveGizmo && m_moveGizmo.IsHoveringHandle(); - const bool rotateGizmoHovering = showingRotateGizmo && m_rotateGizmo.IsHoveringHandle(); - const bool scaleGizmoHovering = showingScaleGizmo && m_scaleGizmo.IsHoveringHandle(); + const SceneViewportTransformGizmoHandleBuildInputs interactionGizmoInputs = + hasInteractiveViewport + ? BuildSceneViewportTransformGizmoHandleBuildInputs( + showingMoveGizmo, + m_moveGizmo, + gizmoFrameState.moveContext, + showingRotateGizmo, + m_rotateGizmo, + gizmoFrameState.rotateContext, + showingScaleGizmo, + m_scaleGizmo, + gizmoFrameState.scaleContext) + : SceneViewportTransformGizmoHandleBuildInputs{}; + const SceneViewportOverlayFrameData& interactionOverlayFrameData = + hasInteractiveViewport + ? viewportHostService->GetSceneViewInteractionOverlayFrameData( + *m_context, + overlay, + interactionGizmoInputs) + : emptySceneOverlayFrameData; + const SceneViewportOverlayHandleHitResult overlayHandleHit = + hasInteractiveViewport + ? HitTestSceneViewportOverlayHandles( + interactionOverlayFrameData, + Math::Vector2(content.availableSize.x, content.availableSize.y), + localMousePosition) + : SceneViewportOverlayHandleHitResult{}; const bool moveGizmoActive = showingMoveGizmo && m_moveGizmo.IsActive(); const bool rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive(); const bool scaleGizmoActive = showingScaleGizmo && m_scaleGizmo.IsActive(); - const SceneViewportActiveGizmoKind hoveredGizmoKind = scaleGizmoHovering - ? SceneViewportActiveGizmoKind::Scale - : (moveGizmoHovering ? SceneViewportActiveGizmoKind::Move - : (rotateGizmoHovering ? SceneViewportActiveGizmoKind::Rotate - : SceneViewportActiveGizmoKind::None)); if (moveGizmoActive) { activeGizmoKind = SceneViewportActiveGizmoKind::Move; } else if (rotateGizmoActive) { @@ -443,44 +562,83 @@ void SceneViewPanel::Render() { } else { activeGizmoKind = SceneViewportActiveGizmoKind::None; } - const bool gizmoHovering = hoveredGizmoKind != SceneViewportActiveGizmoKind::None; const bool gizmoActive = activeGizmoKind != SceneViewportActiveGizmoKind::None; - - const bool beginTransformGizmo = - hasInteractiveViewport && - content.clickedLeft && - !m_lookDragging && - !m_panDragging && - !toolOverlay.hovered && - gizmoHovering; - const SceneViewportOrientationAxis orientationAxisHit = + SceneViewportInteractionCandidate hoveredInteraction = {}; + const bool canResolveViewportInteraction = hasInteractiveViewport && viewportContentHovered && !usingViewMoveTool && !m_lookDragging && !m_panDragging && - !gizmoHovering && - !gizmoActive - ? HitTestSceneViewportOrientationGizmo( - overlay, - content.itemMin, - content.itemMax, - io.MousePos) - : SceneViewportOrientationAxis::None; + !toolOverlay.hovered && + !gizmoActive; + if (canResolveViewportInteraction) { + AccumulateSceneViewportInteractionCandidate( + BuildOverlayHandleInteractionCandidate(overlayHandleHit), + hoveredInteraction); + + AccumulateSceneViewportInteractionCandidate( + BuildOrientationGizmoInteractionCandidate( + HitTestSceneViewportOrientationGizmo( + overlay, + content.itemMin, + content.itemMax, + io.MousePos)), + hoveredInteraction); + } + + if (!gizmoActive) { + if (showingMoveGizmo) { + m_moveGizmo.SetHoveredHandle( + hoveredInteraction.kind == SceneViewportInteractionKind::MoveGizmo + ? hoveredInteraction.moveAxis + : SceneViewportGizmoAxis::None, + hoveredInteraction.kind == SceneViewportInteractionKind::MoveGizmo + ? hoveredInteraction.movePlane + : SceneViewportGizmoPlane::None); + } + if (showingRotateGizmo) { + m_rotateGizmo.SetHoveredHandle( + hoveredInteraction.kind == SceneViewportInteractionKind::RotateGizmo + ? hoveredInteraction.rotateAxis + : SceneViewportRotateGizmoAxis::None); + } + if (showingScaleGizmo) { + m_scaleGizmo.SetHoveredHandle( + hoveredInteraction.kind == SceneViewportInteractionKind::ScaleGizmo + ? hoveredInteraction.scaleHandle + : SceneViewportScaleGizmoHandle::None); + } + } + + const SceneViewportActiveGizmoKind hoveredGizmoKind = + ToActiveGizmoKind(hoveredInteraction.kind); + const bool gizmoHovering = hoveredGizmoKind != SceneViewportActiveGizmoKind::None; + const SceneViewportOrientationAxis orientationAxisHit = + hoveredInteraction.kind == SceneViewportInteractionKind::OrientationGizmo + ? hoveredInteraction.orientationAxis + : SceneViewportOrientationAxis::None; + const uint64_t clickedSceneIconEntity = + hoveredInteraction.kind == SceneViewportInteractionKind::SceneIcon + ? hoveredInteraction.entityId + : 0; + const bool beginTransformGizmo = + hasInteractiveViewport && + content.clickedLeft && + gizmoHovering; const bool orientationGizmoClick = hasInteractiveViewport && content.clickedLeft && orientationAxisHit != SceneViewportOrientationAxis::None; + const bool sceneIconClick = + hasInteractiveViewport && + content.clickedLeft && + clickedSceneIconEntity != 0; const bool selectClick = hasInteractiveViewport && content.clickedLeft && - viewportContentHovered && - !usingViewMoveTool && - !m_lookDragging && - !m_panDragging && - !orientationGizmoClick && - !gizmoHovering && - !gizmoActive; + canResolveViewportInteraction && + !hoveredInteraction.HasHit(); const bool beginLeftPanDrag = usingViewMoveTool ? ShouldBeginSceneViewportNavigationDrag( hasInteractiveViewport, @@ -525,47 +683,32 @@ void SceneViewPanel::Render() { io.MouseDelta.y); } - if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || selectClick || beginLookDrag || + if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || sceneIconClick || selectClick || beginLookDrag || beginPanDrag) { ImGui::SetWindowFocus(); } if (beginTransformGizmo) { if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) { - m_scaleGizmo.TryBeginDrag(scaleGizmoContext, m_context->GetUndoManager()); + m_scaleGizmo.TryBeginDrag(gizmoFrameState.scaleContext, m_context->GetUndoManager()); } else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) { - m_moveGizmo.TryBeginDrag(moveGizmoContext, m_context->GetUndoManager()); + m_moveGizmo.TryBeginDrag(gizmoFrameState.moveContext, m_context->GetUndoManager()); } else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) { - m_rotateGizmo.TryBeginDrag(rotateGizmoContext, m_context->GetUndoManager()); + m_rotateGizmo.TryBeginDrag(gizmoFrameState.rotateContext, m_context->GetUndoManager()); } } if (orientationGizmoClick) { viewportHostService->AlignSceneViewToOrientationAxis(orientationAxisHit); - overlay = viewportHostService->GetSceneViewOverlayData(); - if (showingMoveGizmo) { - moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos); - m_moveGizmo.Update(moveGizmoContext); - } - if (showingRotateGizmo) { - rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos); - m_rotateGizmo.Update(rotateGizmoContext); - } - if (showingScaleGizmo) { - scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos); - scaleGizmoContext.uniformOnly = usingTransformTool; - m_scaleGizmo.Update(scaleGizmoContext); - } } - if (selectClick) { - const ImVec2 localMousePosition( - io.MousePos.x - content.itemMin.x, - io.MousePos.y - content.itemMin.y); + if (sceneIconClick) { + m_context->GetSelectionManager().SetSelectedEntity(clickedSceneIconEntity); + } else if (selectClick) { const uint64_t selectedEntity = viewportHostService->PickSceneViewEntity( *m_context, content.availableSize, - localMousePosition); + ImVec2(localMousePosition.x, localMousePosition.y)); if (selectedEntity != 0) { m_context->GetSelectionManager().SetSelectedEntity(selectedEntity); } else { @@ -576,11 +719,11 @@ void SceneViewPanel::Render() { if (gizmoActive) { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) { - m_moveGizmo.UpdateDrag(moveGizmoContext); + m_moveGizmo.UpdateDrag(gizmoFrameState.moveContext); } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) { - m_rotateGizmo.UpdateDrag(rotateGizmoContext); + m_rotateGizmo.UpdateDrag(gizmoFrameState.rotateContext); } else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) { - m_scaleGizmo.UpdateDrag(scaleGizmoContext); + m_scaleGizmo.UpdateDrag(gizmoFrameState.scaleContext); } } else { if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) { @@ -707,52 +850,41 @@ void SceneViewPanel::Render() { if (content.hasViewportArea && content.frame.hasTexture) { overlay = viewportHostService->GetSceneViewOverlayData(); - SceneViewportActiveGizmoKind drawActiveGizmoKind = SceneViewportActiveGizmoKind::None; - if (m_moveGizmo.IsActive()) { - drawActiveGizmoKind = SceneViewportActiveGizmoKind::Move; - } else if (m_rotateGizmo.IsActive()) { - drawActiveGizmoKind = SceneViewportActiveGizmoKind::Rotate; - } else if (m_scaleGizmo.IsActive()) { - drawActiveGizmoKind = SceneViewportActiveGizmoKind::Scale; - } - if (showingMoveGizmo) { - moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos); - SceneViewportMoveGizmoContext updateContext = moveGizmoContext; - if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None && - drawActiveGizmoKind != SceneViewportActiveGizmoKind::Move) { - updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); - } - m_moveGizmo.Update(updateContext); - } - if (showingRotateGizmo) { - rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos); - SceneViewportRotateGizmoContext updateContext = rotateGizmoContext; - if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None && - drawActiveGizmoKind != SceneViewportActiveGizmoKind::Rotate) { - updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); - } - m_rotateGizmo.Update(updateContext); - } - if (showingScaleGizmo) { - scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos); - scaleGizmoContext.uniformOnly = usingTransformTool; - SceneViewportScaleGizmoContext updateContext = scaleGizmoContext; - if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None && - drawActiveGizmoKind != SceneViewportActiveGizmoKind::Scale) { - updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f); - } - m_scaleGizmo.Update(updateContext); - } + const SceneViewportTransformGizmoFrameState drawGizmoFrameState = + RefreshSceneViewportTransformGizmos( + *m_context, + overlay, + viewportSize, + localMousePosition, + useCenterPivot, + localSpace, + usingTransformTool, + showingMoveGizmo, + m_moveGizmo, + showingRotateGizmo, + m_rotateGizmo, + showingScaleGizmo, + m_scaleGizmo); + + viewportHostService->SetSceneViewTransientTransformGizmoOverlayData( + overlay, + BuildSceneViewportTransformGizmoHandleBuildInputs( + showingMoveGizmo, + m_moveGizmo, + drawGizmoFrameState.moveContext, + showingRotateGizmo, + m_rotateGizmo, + drawGizmoFrameState.rotateContext, + showingScaleGizmo, + m_scaleGizmo, + drawGizmoFrameState.scaleContext)); DrawSceneViewportOverlay( ImGui::GetWindowDrawList(), overlay, content.itemMin, content.itemMax, - content.availableSize, - showingMoveGizmo ? &m_moveGizmo.GetDrawData() : nullptr, - showingRotateGizmo ? &m_rotateGizmo.GetDrawData() : nullptr, - showingScaleGizmo ? &m_scaleGizmo.GetDrawData() : nullptr); + content.availableSize); } } diff --git a/editor/src/panels/SceneViewPanel.h b/editor/src/panels/SceneViewPanel.h index 51c0eab3..8170aa98 100644 --- a/editor/src/panels/SceneViewPanel.h +++ b/editor/src/panels/SceneViewPanel.h @@ -18,6 +18,16 @@ enum class SceneViewportToolMode : uint8_t { Transform }; +enum class SceneViewportPivotMode : uint8_t { + Pivot = 0, + Center +}; + +enum class SceneViewportTransformSpaceMode : uint8_t { + Global = 0, + Local +}; + class SceneViewPanel : public Panel { public: SceneViewPanel(); @@ -25,6 +35,8 @@ public: private: SceneViewportToolMode m_toolMode = SceneViewportToolMode::Move; + SceneViewportPivotMode m_pivotMode = SceneViewportPivotMode::Pivot; + SceneViewportTransformSpaceMode m_transformSpaceMode = SceneViewportTransformSpaceMode::Global; bool m_lookDragging = false; bool m_panDragging = false; int m_panDragButton = ImGuiMouseButton_Middle; diff --git a/editor/src/panels/ViewportPanelContent.h b/editor/src/panels/ViewportPanelContent.h index 412ad329..f4ea9513 100644 --- a/editor/src/panels/ViewportPanelContent.h +++ b/editor/src/panels/ViewportPanelContent.h @@ -59,6 +59,7 @@ inline void RenderViewportInteractionSurface( ViewportPanelContentResult& result, EditorViewportKind kind, const ImVec2& interactionSize) { + ImGui::SetNextItemAllowOverlap(); ImGui::InvisibleButton( GetViewportInteractionSurfaceId(kind), interactionSize, diff --git a/editor/tab.png b/editor/tab.png deleted file mode 100644 index 1becf22d..00000000 Binary files a/editor/tab.png and /dev/null differ diff --git a/editor/unity.png b/editor/unity.png deleted file mode 100644 index 32d51c66..00000000 Binary files a/editor/unity.png and /dev/null differ diff --git a/editor/unity_grid.png b/editor/unity_grid.png deleted file mode 100644 index 65b5742f..00000000 Binary files a/editor/unity_grid.png and /dev/null differ diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index f3552228..538672b5 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -246,6 +246,8 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetRef.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ArtifactFormats.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetDatabase.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetImportService.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ProjectAssetIndex.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ResourceTypes.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ImportSettings.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ResourceHandle.h @@ -255,6 +257,8 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ResourceDependencyGraph.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetGUID.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetDatabase.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetImportService.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ProjectAssetIndex.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ResourceManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ResourceCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AsyncLoader.cpp diff --git a/engine/include/XCEngine/Components/MeshFilterComponent.h b/engine/include/XCEngine/Components/MeshFilterComponent.h index e883eccf..8f97bdd4 100644 --- a/engine/include/XCEngine/Components/MeshFilterComponent.h +++ b/engine/include/XCEngine/Components/MeshFilterComponent.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -32,12 +33,14 @@ private: struct PendingMeshLoadState; void BeginAsyncMeshLoad(const std::string& meshPath); + void EnsureDeferredAsyncMeshLoadStarted(); void ResolvePendingMesh(); Resources::ResourceHandle m_mesh; std::string m_meshPath; Resources::AssetRef m_meshRef; std::shared_ptr m_pendingMeshLoad; + bool m_asyncMeshLoadRequested = false; }; } // namespace Components diff --git a/engine/include/XCEngine/Components/MeshRendererComponent.h b/engine/include/XCEngine/Components/MeshRendererComponent.h index efc0e2dc..59ad8d37 100644 --- a/engine/include/XCEngine/Components/MeshRendererComponent.h +++ b/engine/include/XCEngine/Components/MeshRendererComponent.h @@ -46,6 +46,7 @@ private: struct PendingMaterialLoadState; void BeginAsyncMaterialLoad(size_t index, const std::string& materialPath); + void EnsureDeferredAsyncMaterialLoadStarted(size_t index); void ResolvePendingMaterials(); void EnsureMaterialSlot(size_t index); static std::string MaterialPathFromHandle(const Resources::ResourceHandle& material); @@ -54,6 +55,7 @@ private: std::vector m_materialPaths; std::vector m_materialRefs; std::vector> m_pendingMaterialLoads; + std::vector m_asyncMaterialLoadRequested; bool m_castShadows = true; bool m_receiveShadows = true; uint32_t m_renderLayer = 0; diff --git a/engine/include/XCEngine/Core/Asset/AssetDatabase.h b/engine/include/XCEngine/Core/Asset/AssetDatabase.h index 1bb6db62..9b756443 100644 --- a/engine/include/XCEngine/Core/Asset/AssetDatabase.h +++ b/engine/include/XCEngine/Core/Asset/AssetDatabase.h @@ -6,12 +6,23 @@ #include #include +#include namespace XCEngine { namespace Resources { +class Mesh; +class Material; + class AssetDatabase { public: + struct ArtifactDependencyRecord { + Containers::String path; + Containers::String hash; + Core::uint64 fileSize = 0; + Core::uint64 writeTime = 0; + }; + struct SourceAssetRecord { AssetGUID guid; Containers::String relativePath; @@ -39,6 +50,7 @@ public: Core::uint64 sourceFileSize = 0; Core::uint64 sourceWriteTime = 0; LocalID mainLocalID = kMainAssetLocalID; + std::vector dependencies; }; struct ResolvedAsset { @@ -66,13 +78,15 @@ public: ResourceType requestedType, ResolvedAsset& outAsset); bool TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const; + void BuildLookupSnapshot(std::unordered_map& outPathToGuid, + std::unordered_map& outGuidToPath) const; const Containers::String& GetProjectRoot() const { return m_projectRoot; } const Containers::String& GetAssetsRoot() const { return m_assetsRoot; } const Containers::String& GetLibraryRoot() const { return m_libraryRoot; } private: - static constexpr Core::uint32 kCurrentImporterVersion = 2; + static constexpr Core::uint32 kCurrentImporterVersion = 3; void EnsureProjectLayout(); void LoadSourceAssetDB(); @@ -110,12 +124,24 @@ private: bool ImportModelAsset(const SourceAssetRecord& sourceRecord, ArtifactRecord& outRecord); - Containers::String BuildArtifactKey(const SourceAssetRecord& sourceRecord) const; + Containers::String BuildArtifactKey( + const SourceAssetRecord& sourceRecord, + const std::vector& dependencies = {}) const; Containers::String BuildArtifactDirectory(const Containers::String& artifactKey) const; static Containers::String ReadWholeFileText(const std::filesystem::path& path); static Containers::String ComputeFileHash(const std::filesystem::path& path); static Core::uint64 GetFileSizeValue(const std::filesystem::path& path); static Core::uint64 GetFileWriteTimeValue(const std::filesystem::path& path); + Containers::String NormalizeDependencyPath(const std::filesystem::path& path) const; + std::filesystem::path ResolveDependencyPath(const Containers::String& path) const; + bool CaptureDependencyRecord(const std::filesystem::path& path, + ArtifactDependencyRecord& outRecord) const; + bool AreDependenciesCurrent(const std::vector& dependencies) const; + bool CollectModelDependencies(const SourceAssetRecord& sourceRecord, + const Mesh& mesh, + std::vector& outDependencies) const; + bool CollectMaterialDependencies(const Material& material, + std::vector& outDependencies) const; Containers::String m_projectRoot; Containers::String m_assetsRoot; diff --git a/engine/include/XCEngine/Core/Asset/AssetImportService.h b/engine/include/XCEngine/Core/Asset/AssetImportService.h new file mode 100644 index 00000000..c428babe --- /dev/null +++ b/engine/include/XCEngine/Core/Asset/AssetImportService.h @@ -0,0 +1,38 @@ +#pragma once + +#include "AssetDatabase.h" + +#include +#include + +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& outPathToGuid, + std::unordered_map& outGuidToPath) const; + +private: + mutable std::recursive_mutex m_mutex; + Containers::String m_projectRoot; + AssetDatabase m_assetDatabase; +}; + +} // namespace Resources +} // namespace XCEngine diff --git a/engine/include/XCEngine/Core/Asset/ProjectAssetIndex.h b/engine/include/XCEngine/Core/Asset/ProjectAssetIndex.h new file mode 100644 index 00000000..de72bcb3 --- /dev/null +++ b/engine/include/XCEngine/Core/Asset/ProjectAssetIndex.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +#include +#include + +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 m_assetGuidByPathKey; + std::unordered_map m_assetPathByGuid; +}; + +} // namespace Resources +} // namespace XCEngine diff --git a/engine/include/XCEngine/Core/Asset/ResourceManager.h b/engine/include/XCEngine/Core/Asset/ResourceManager.h index 5159a611..fcc80f7f 100644 --- a/engine/include/XCEngine/Core/Asset/ResourceManager.h +++ b/engine/include/XCEngine/Core/Asset/ResourceManager.h @@ -1,7 +1,8 @@ #pragma once #include -#include "AssetDatabase.h" +#include "AssetImportService.h" +#include "ProjectAssetIndex.h" #include "ResourceCache.h" #include "AsyncLoader.h" #include "ResourceHandle.h" @@ -165,12 +166,12 @@ private: size_t m_memoryUsage = 0; size_t m_memoryBudget = 512 * 1024 * 1024; - AssetDatabase m_assetDatabase; + mutable AssetImportService m_assetImportService; + mutable ProjectAssetIndex m_projectAssetIndex; ResourceCache m_cache; Core::UniqueRef m_asyncLoader; Threading::Mutex m_mutex; std::mutex m_initializeMutex; - mutable std::recursive_mutex m_ioMutex; std::mutex m_inFlightLoadsMutex; std::unordered_map, InFlightLoadKeyHasher> m_inFlightLoads; std::atomic m_deferredSceneLoadDepth{0}; diff --git a/engine/include/XCEngine/Input/InputManager.h b/engine/include/XCEngine/Input/InputManager.h index 2e5aa025..624600dc 100644 --- a/engine/include/XCEngine/Input/InputManager.h +++ b/engine/include/XCEngine/Input/InputManager.h @@ -21,6 +21,7 @@ public: bool IsKeyDown(KeyCode key) const; bool IsKeyUp(KeyCode key) const; bool IsKeyPressed(KeyCode key) const; + bool IsKeyReleased(KeyCode key) const; Math::Vector2 GetMousePosition() const; Math::Vector2 GetMouseDelta() const; @@ -28,6 +29,7 @@ public: bool IsMouseButtonDown(MouseButton button) const; bool IsMouseButtonUp(MouseButton button) const; bool IsMouseButtonClicked(MouseButton button) const; + bool IsMouseButtonReleased(MouseButton button) const; int GetTouchCount() const; TouchState GetTouch(int index) const; @@ -38,6 +40,8 @@ public: bool GetButton(const Containers::String& buttonName) const; bool GetButtonDown(const Containers::String& buttonName) const; bool GetButtonUp(const Containers::String& buttonName) const; + bool IsAnyKeyDown() const; + bool IsAnyKeyPressed() const; void RegisterAxis(const InputAxis& axis); void RegisterButton(const Containers::String& name, KeyCode key); @@ -68,6 +72,7 @@ private: std::vector m_keyDownThisFrame; std::vector m_keyDownLastFrame; + std::vector m_keyUpThisFrame; std::vector m_keyDown; Math::Vector2 m_mousePosition; @@ -75,6 +80,7 @@ private: float m_mouseScrollDelta = 0.0f; std::vector m_mouseButtonDownThisFrame; std::vector m_mouseButtonDownLastFrame; + std::vector m_mouseButtonUpThisFrame; std::vector m_mouseButtonDown; std::vector m_touches; diff --git a/engine/include/XCEngine/Rendering/CameraRenderRequest.h b/engine/include/XCEngine/Rendering/CameraRenderRequest.h index 6c80e971..28a06668 100644 --- a/engine/include/XCEngine/Rendering/CameraRenderRequest.h +++ b/engine/include/XCEngine/Rendering/CameraRenderRequest.h @@ -57,6 +57,7 @@ struct CameraRenderRequest { Math::Color clearColorOverride = Math::Color::Black(); RenderPassSequence* preScenePasses = nullptr; RenderPassSequence* postScenePasses = nullptr; + RenderPassSequence* overlayPasses = nullptr; bool IsValid() const { return scene != nullptr && diff --git a/engine/include/XCEngine/Scripting/IScriptRuntime.h b/engine/include/XCEngine/Scripting/IScriptRuntime.h index 7c103c45..30deb16d 100644 --- a/engine/include/XCEngine/Scripting/IScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/IScriptRuntime.h @@ -28,6 +28,26 @@ enum class ScriptLifecycleMethod { OnDestroy }; +struct ScriptClassDescriptor { + std::string assemblyName; + std::string namespaceName; + std::string className; + + std::string GetFullName() const { + return namespaceName.empty() ? className : namespaceName + "." + className; + } + + bool operator==(const ScriptClassDescriptor& other) const { + return assemblyName == other.assemblyName + && namespaceName == other.namespaceName + && className == other.className; + } + + bool operator!=(const ScriptClassDescriptor& other) const { + return !(*this == other); + } +}; + struct ScriptRuntimeContext { Components::Scene* scene = nullptr; Components::GameObject* gameObject = nullptr; @@ -43,6 +63,9 @@ public: virtual void OnRuntimeStart(Components::Scene* scene) = 0; virtual void OnRuntimeStop(Components::Scene* scene) = 0; + virtual bool TryGetAvailableScriptClasses( + std::vector& outClasses) const = 0; + virtual bool TryGetClassFieldMetadata( const std::string& assemblyName, const std::string& namespaceName, diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index 3e25614c..bce44086 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -50,6 +50,8 @@ public: const std::string& namespaceName, const std::string& className) const; std::vector GetScriptClassNames(const std::string& assemblyName = std::string()) const; + bool TryGetAvailableScriptClasses( + std::vector& outClasses) const override; bool TryGetClassFieldMetadata( const std::string& assemblyName, const std::string& namespaceName, diff --git a/engine/include/XCEngine/Scripting/NullScriptRuntime.h b/engine/include/XCEngine/Scripting/NullScriptRuntime.h index 5c5f411a..75fc8fb0 100644 --- a/engine/include/XCEngine/Scripting/NullScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/NullScriptRuntime.h @@ -9,6 +9,8 @@ class NullScriptRuntime : public IScriptRuntime { public: void OnRuntimeStart(Components::Scene* scene) override; void OnRuntimeStop(Components::Scene* scene) override; + bool TryGetAvailableScriptClasses( + std::vector& outClasses) const override; bool TryGetClassFieldMetadata( const std::string& assemblyName, const std::string& namespaceName, diff --git a/engine/include/XCEngine/Scripting/ScriptComponent.h b/engine/include/XCEngine/Scripting/ScriptComponent.h index d1ec2f0e..e91e3125 100644 --- a/engine/include/XCEngine/Scripting/ScriptComponent.h +++ b/engine/include/XCEngine/Scripting/ScriptComponent.h @@ -28,6 +28,7 @@ public: void SetScriptClass(const std::string& namespaceName, const std::string& className); void SetScriptClass(const std::string& assemblyName, const std::string& namespaceName, const std::string& className); + void ClearScriptClass(); bool HasScriptClass() const { return !m_className.empty(); } std::string GetFullClassName() const; diff --git a/engine/include/XCEngine/Scripting/ScriptEngine.h b/engine/include/XCEngine/Scripting/ScriptEngine.h index a63fb7d8..4ed8b2be 100644 --- a/engine/include/XCEngine/Scripting/ScriptEngine.h +++ b/engine/include/XCEngine/Scripting/ScriptEngine.h @@ -19,9 +19,12 @@ class ScriptComponent; class ScriptEngine { public: static ScriptEngine& Get(); + static constexpr float DefaultFixedDeltaTime = 1.0f / 50.0f; void SetRuntime(IScriptRuntime* runtime); IScriptRuntime* GetRuntime() const { return m_runtime; } + void SetRuntimeFixedDeltaTime(float fixedDeltaTime); + float GetRuntimeFixedDeltaTime() const { return m_runtimeFixedDeltaTime; } void OnRuntimeStart(Components::Scene* scene); void OnRuntimeStop(); @@ -33,6 +36,7 @@ public: void OnScriptComponentEnabled(ScriptComponent* component); void OnScriptComponentDisabled(ScriptComponent* component); void OnScriptComponentDestroyed(ScriptComponent* component); + void OnScriptComponentClassChanged(ScriptComponent* component); bool IsRuntimeRunning() const { return m_runtimeRunning; } Components::Scene* GetRuntimeScene() const { return m_runtimeScene; } @@ -40,6 +44,9 @@ public: bool HasTrackedScriptComponent(const ScriptComponent* component) const; bool HasRuntimeInstance(const ScriptComponent* component) const; size_t GetTrackedScriptCount() const { return m_scriptOrder.size(); } + bool TryGetAvailableScriptClasses( + std::vector& outClasses, + const std::string& assemblyName = std::string()) const; bool TrySetScriptFieldValue( ScriptComponent* component, const std::string& fieldName, @@ -138,6 +145,7 @@ private: IScriptRuntime* m_runtime = &m_nullRuntime; Components::Scene* m_runtimeScene = nullptr; bool m_runtimeRunning = false; + float m_runtimeFixedDeltaTime = DefaultFixedDeltaTime; uint64_t m_runtimeSceneCreatedSubscription = 0; std::unordered_map m_scriptStates; diff --git a/engine/src/Components/MeshFilterComponent.cpp b/engine/src/Components/MeshFilterComponent.cpp index 74e0b500..9f67a341 100644 --- a/engine/src/Components/MeshFilterComponent.cpp +++ b/engine/src/Components/MeshFilterComponent.cpp @@ -20,6 +20,10 @@ bool ShouldTraceMeshPath(const std::string& path) { path.find("backpack") != std::string::npos; } +bool HasVirtualPathScheme(const std::string& path) { + return path.find("://") != std::string::npos; +} + std::string EncodeAssetRef(const Resources::AssetRef& assetRef) { if (!assetRef.IsValid()) { return std::string(); @@ -67,17 +71,20 @@ struct MeshFilterComponent::PendingMeshLoadState { }; Resources::Mesh* MeshFilterComponent::GetMesh() const { + const_cast(this)->EnsureDeferredAsyncMeshLoadStarted(); const_cast(this)->ResolvePendingMesh(); return m_mesh.Get(); } const Resources::ResourceHandle& MeshFilterComponent::GetMeshHandle() const { + const_cast(this)->EnsureDeferredAsyncMeshLoadStarted(); const_cast(this)->ResolvePendingMesh(); return m_mesh; } void MeshFilterComponent::SetMeshPath(const std::string& meshPath) { m_pendingMeshLoad.reset(); + m_asyncMeshLoadRequested = false; m_meshPath = meshPath; if (m_meshPath.empty()) { m_mesh.Reset(); @@ -101,6 +108,7 @@ void MeshFilterComponent::SetMeshPath(const std::string& meshPath) { void MeshFilterComponent::SetMesh(const Resources::ResourceHandle& mesh) { m_pendingMeshLoad.reset(); + m_asyncMeshLoadRequested = false; m_mesh = mesh; m_meshPath = mesh.Get() != nullptr ? ToStdString(mesh->GetPath()) : std::string(); if (m_meshPath.empty()) { @@ -116,18 +124,29 @@ void MeshFilterComponent::SetMesh(Resources::Mesh* mesh) { void MeshFilterComponent::ClearMesh() { m_pendingMeshLoad.reset(); + m_asyncMeshLoadRequested = false; m_mesh.Reset(); m_meshPath.clear(); m_meshRef.Reset(); } void MeshFilterComponent::Serialize(std::ostream& os) const { - os << "mesh=" << m_meshPath << ";"; - os << "meshRef=" << EncodeAssetRef(m_meshRef) << ";"; + Resources::AssetRef meshRef = m_meshRef; + if (!meshRef.IsValid() && + !m_meshPath.empty() && + !HasVirtualPathScheme(m_meshPath) && + Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, meshRef)) { + } + + os << "meshRef=" << EncodeAssetRef(meshRef) << ";"; + if (!meshRef.IsValid() && !m_meshPath.empty()) { + os << "meshPath=" << m_meshPath << ";"; + } } void MeshFilterComponent::Deserialize(std::istream& is) { m_pendingMeshLoad.reset(); + m_asyncMeshLoadRequested = false; m_mesh.Reset(); m_meshPath.clear(); m_meshRef.Reset(); @@ -148,7 +167,7 @@ void MeshFilterComponent::Deserialize(std::istream& is) { const std::string key = token.substr(0, eqPos); const std::string value = token.substr(eqPos + 1); - if (key == "mesh") { + if (key == "mesh" || key == "meshPath") { pendingMeshPath = value; } else if (key == "meshRef") { TryDecodeAssetRef(value, pendingMeshRef); @@ -172,7 +191,6 @@ void MeshFilterComponent::Deserialize(std::istream& is) { if (ShouldTraceMeshPath(m_meshPath)) { TraceMeshFilter(*this, std::string("Resolved meshRef to path=") + m_meshPath); } - BeginAsyncMeshLoad(m_meshPath); return; } @@ -191,7 +209,6 @@ void MeshFilterComponent::Deserialize(std::istream& is) { if (!Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, m_meshRef)) { m_meshRef.Reset(); } - BeginAsyncMeshLoad(m_meshPath); return; } @@ -202,10 +219,12 @@ void MeshFilterComponent::Deserialize(std::istream& is) { void MeshFilterComponent::BeginAsyncMeshLoad(const std::string& meshPath) { if (meshPath.empty()) { m_pendingMeshLoad.reset(); + m_asyncMeshLoadRequested = false; m_mesh.Reset(); return; } + m_asyncMeshLoadRequested = true; m_mesh.Reset(); m_pendingMeshLoad = std::make_shared(); if (ShouldTraceMeshPath(meshPath)) { @@ -219,7 +238,15 @@ void MeshFilterComponent::BeginAsyncMeshLoad(const std::string& meshPath) { state->result = std::move(result); state->completed = true; } - }); + }); +} + +void MeshFilterComponent::EnsureDeferredAsyncMeshLoadStarted() { + if (m_asyncMeshLoadRequested || m_mesh.Get() != nullptr || m_meshPath.empty()) { + return; + } + + BeginAsyncMeshLoad(m_meshPath); } void MeshFilterComponent::ResolvePendingMesh() { diff --git a/engine/src/Components/MeshRendererComponent.cpp b/engine/src/Components/MeshRendererComponent.cpp index bfd2018a..dc7bf817 100644 --- a/engine/src/Components/MeshRendererComponent.cpp +++ b/engine/src/Components/MeshRendererComponent.cpp @@ -4,6 +4,7 @@ #include "Core/Asset/ResourceManager.h" #include "Debug/Logger.h" +#include #include namespace XCEngine { @@ -21,6 +22,10 @@ bool ShouldTraceMaterialPath(const std::string& path) { path.find("New Material.mat") != std::string::npos; } +bool HasVirtualPathScheme(const std::string& path) { + return path.find("://") != std::string::npos; +} + std::string EncodeAssetRef(const Resources::AssetRef& assetRef) { if (!assetRef.IsValid()) { return std::string(); @@ -145,11 +150,13 @@ struct MeshRendererComponent::PendingMaterialLoadState { }; Resources::Material* MeshRendererComponent::GetMaterial(size_t index) const { + const_cast(this)->EnsureDeferredAsyncMaterialLoadStarted(index); const_cast(this)->ResolvePendingMaterials(); return index < m_materials.size() ? m_materials[index].Get() : nullptr; } const Resources::ResourceHandle& MeshRendererComponent::GetMaterialHandle(size_t index) const { + const_cast(this)->EnsureDeferredAsyncMaterialLoadStarted(index); const_cast(this)->ResolvePendingMaterials(); static const Resources::ResourceHandle kNullHandle; return index < m_materials.size() ? m_materials[index] : kNullHandle; @@ -163,6 +170,7 @@ const std::string& MeshRendererComponent::GetMaterialPath(size_t index) const { void MeshRendererComponent::SetMaterialPath(size_t index, const std::string& materialPath) { EnsureMaterialSlot(index); m_pendingMaterialLoads[index].reset(); + m_asyncMaterialLoadRequested[index] = false; m_materialPaths[index] = materialPath; if (materialPath.empty()) { m_materials[index].Reset(); @@ -188,6 +196,7 @@ void MeshRendererComponent::SetMaterialPath(size_t index, const std::string& mat void MeshRendererComponent::SetMaterial(size_t index, const Resources::ResourceHandle& material) { EnsureMaterialSlot(index); m_pendingMaterialLoads[index].reset(); + m_asyncMaterialLoadRequested[index] = false; m_materials[index] = material; m_materialPaths[index] = MaterialPathFromHandle(material); if (m_materialPaths[index].empty() || @@ -206,6 +215,8 @@ void MeshRendererComponent::SetMaterials(const std::vector(std::tolower(ch)); + }); + return key; +} + +} // namespace + +void ProjectAssetIndex::ResetProjectRoot(const Containers::String& projectRoot) { + std::unique_lock lock(m_mutex); + m_projectRoot = projectRoot; + m_assetGuidByPathKey.clear(); + m_assetPathByGuid.clear(); +} + +void ProjectAssetIndex::RefreshFrom(const AssetImportService& importService) { + std::unordered_map pathToGuid; + std::unordered_map guidToPath; + const Containers::String projectRoot = importService.GetProjectRoot(); + if (!projectRoot.Empty()) { + importService.BuildLookupSnapshot(pathToGuid, guidToPath); + } + + std::unique_lock 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 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(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(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 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(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 lock(m_mutex); + m_assetGuidByPathKey[MakeAssetLookupPathKey(relativePath)] = assetGuid; + m_assetPathByGuid[assetGuid] = relativePath; +} + +} // namespace Resources +} // namespace XCEngine diff --git a/engine/src/Core/Asset/ResourceManager.cpp b/engine/src/Core/Asset/ResourceManager.cpp index 8cc97e6c..1c4f2380 100644 --- a/engine/src/Core/Asset/ResourceManager.cpp +++ b/engine/src/Core/Asset/ResourceManager.cpp @@ -85,6 +85,7 @@ void ResourceManager::EnsureInitialized() { RegisterBuiltinLoader(*this, g_meshLoader); RegisterBuiltinLoader(*this, g_shaderLoader); RegisterBuiltinLoader(*this, g_textureLoader); + m_assetImportService.Initialize(); m_asyncLoader = std::move(asyncLoader); } @@ -96,23 +97,24 @@ void ResourceManager::Shutdown() { m_asyncLoader.reset(); } - std::lock_guard lock(m_ioMutex); - m_assetDatabase.Shutdown(); + m_assetImportService.Shutdown(); ResourceFileSystem::Get().Shutdown(); + m_projectAssetIndex.ResetProjectRoot(); std::lock_guard inFlightLock(m_inFlightLoadsMutex); m_inFlightLoads.clear(); } void ResourceManager::SetResourceRoot(const Containers::String& rootPath) { - std::lock_guard lock(m_ioMutex); m_resourceRoot = rootPath; if (!m_resourceRoot.Empty()) { ResourceFileSystem::Get().Initialize(rootPath); - m_assetDatabase.Initialize(rootPath); + m_assetImportService.SetProjectRoot(rootPath); + m_projectAssetIndex.RefreshFrom(m_assetImportService); } else { + m_assetImportService.SetProjectRoot(Containers::String()); ResourceFileSystem::Get().Shutdown(); - m_assetDatabase.Shutdown(); + m_projectAssetIndex.ResetProjectRoot(); } } @@ -360,14 +362,14 @@ void ResourceManager::UnloadGroup(const Containers::Array& guids) void ResourceManager::RefreshAssetDatabase() { if (!m_resourceRoot.Empty()) { - std::lock_guard lock(m_ioMutex); - m_assetDatabase.Refresh(); + m_assetImportService.Refresh(); + m_projectAssetIndex.RefreshFrom(m_assetImportService); } } bool ResourceManager::TryGetAssetRef(const Containers::String& path, ResourceType resourceType, AssetRef& outRef) const { - std::lock_guard lock(m_ioMutex); - const bool resolved = m_assetDatabase.TryGetAssetRef(path, resourceType, outRef); + const bool resolved = m_projectAssetIndex.TryGetAssetRef(m_assetImportService, path, resourceType, outRef); + if (ShouldTraceResourcePath(path)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, @@ -384,12 +386,8 @@ bool ResourceManager::TryGetAssetRef(const Containers::String& path, ResourceTyp } bool ResourceManager::TryResolveAssetPath(const AssetRef& assetRef, Containers::String& outPath) const { - if (!assetRef.IsValid()) { - return false; - } + const bool resolved = m_projectAssetIndex.TryResolveAssetPath(m_assetImportService, assetRef, outPath); - std::lock_guard lock(m_ioMutex); - const bool resolved = m_assetDatabase.TryGetPrimaryAssetPath(assetRef.assetGuid, outPath); if (resolved && ShouldTraceResourcePath(outPath)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, @@ -512,30 +510,27 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path, } Containers::String loadPath = path; - { - std::lock_guard ioLock(m_ioMutex); - - AssetDatabase::ResolvedAsset resolvedAsset; - if (!m_resourceRoot.Empty() && - m_assetDatabase.EnsureArtifact(path, type, resolvedAsset) && - resolvedAsset.artifactReady) { - loadPath = resolvedAsset.artifactMainPath; - if (ShouldTraceResourcePath(path)) { - Debug::Logger::Get().Info( - Debug::LogCategory::FileSystem, - Containers::String("[ResourceManager] LoadResource artifact path=") + - path + - " artifact=" + - loadPath); - } - } else if (ShouldTraceResourcePath(path)) { + AssetDatabase::ResolvedAsset resolvedAsset; + if (!m_resourceRoot.Empty() && + m_assetImportService.EnsureArtifact(path, type, resolvedAsset) && + resolvedAsset.artifactReady) { + m_projectAssetIndex.RememberResolvedPath(resolvedAsset.assetGuid, resolvedAsset.relativePath); + loadPath = resolvedAsset.artifactMainPath; + if (ShouldTraceResourcePath(path)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, - Containers::String("[ResourceManager] LoadResource direct path=") + + Containers::String("[ResourceManager] LoadResource artifact path=") + path + - " loadPath=" + + " artifact=" + loadPath); } + } else if (ShouldTraceResourcePath(path)) { + Debug::Logger::Get().Info( + Debug::LogCategory::FileSystem, + Containers::String("[ResourceManager] LoadResource direct path=") + + path + + " loadPath=" + + loadPath); } LoadResult result; diff --git a/engine/src/Input/InputManager.cpp b/engine/src/Input/InputManager.cpp index 1f0d4934..920b0244 100644 --- a/engine/src/Input/InputManager.cpp +++ b/engine/src/Input/InputManager.cpp @@ -16,10 +16,12 @@ void InputManager::Initialize(void* platformWindowHandle) { m_keyDownThisFrame.resize(256, false); m_keyDownLastFrame.resize(256, false); + m_keyUpThisFrame.resize(256, false); m_keyDown.resize(256, false); m_mouseButtonDownThisFrame.resize(5, false); m_mouseButtonDownLastFrame.resize(5, false); + m_mouseButtonUpThisFrame.resize(5, false); m_mouseButtonDown.resize(5, false); m_buttonDownThisFrame.resize(32, false); @@ -39,14 +41,21 @@ void InputManager::Initialize(void* platformWindowHandle) { } void InputManager::Shutdown() { + m_mousePosition = Math::Vector2::Zero(); + m_mouseDelta = Math::Vector2::Zero(); + m_mouseScrollDelta = 0.0f; + m_touches.clear(); + if (!m_initialized) return; m_keyDownThisFrame.clear(); m_keyDownLastFrame.clear(); + m_keyUpThisFrame.clear(); m_keyDown.clear(); m_mouseButtonDownThisFrame.clear(); m_mouseButtonDownLastFrame.clear(); + m_mouseButtonUpThisFrame.clear(); m_mouseButtonDown.clear(); m_axes.clear(); @@ -64,10 +73,14 @@ void InputManager::Update(float deltaTime) { m_keyDownLastFrame = m_keyDownThisFrame; m_keyDownThisFrame.clear(); m_keyDownThisFrame.resize(256, false); + m_keyUpThisFrame.clear(); + m_keyUpThisFrame.resize(256, false); m_mouseButtonDownLastFrame = m_mouseButtonDownThisFrame; m_mouseButtonDownThisFrame.clear(); m_mouseButtonDownThisFrame.resize(5, false); + m_mouseButtonUpThisFrame.clear(); + m_mouseButtonUpThisFrame.resize(5, false); m_buttonDownLastFrame = m_buttonDownThisFrame; m_buttonDownThisFrame.clear(); @@ -104,6 +117,13 @@ bool InputManager::IsKeyPressed(KeyCode key) const { return m_keyDownThisFrame[index] && !m_keyDownLastFrame[index]; } +bool InputManager::IsKeyReleased(KeyCode key) const { + if (!m_initialized) return false; + size_t index = GetKeyIndex(key); + if (index >= m_keyUpThisFrame.size()) return false; + return m_keyUpThisFrame[index]; +} + Math::Vector2 InputManager::GetMousePosition() const { return m_mousePosition; } @@ -135,6 +155,13 @@ bool InputManager::IsMouseButtonClicked(MouseButton button) const { return m_mouseButtonDownThisFrame[index] && !m_mouseButtonDownLastFrame[index]; } +bool InputManager::IsMouseButtonReleased(MouseButton button) const { + if (!m_initialized) return false; + size_t index = GetMouseButtonIndex(button); + if (index >= m_mouseButtonUpThisFrame.size()) return false; + return m_mouseButtonUpThisFrame[index]; +} + int InputManager::GetTouchCount() const { return static_cast(m_touches.size()); } @@ -170,10 +197,10 @@ float InputManager::GetAxisRaw(const Containers::String& axisName) const { const auto& axis = it->second; float value = 0.0f; - if (axis.GetPositiveKey() != KeyCode::None && IsKeyPressed(axis.GetPositiveKey())) { + if (axis.GetPositiveKey() != KeyCode::None && IsKeyDown(axis.GetPositiveKey())) { value += 1.0f; } - if (axis.GetNegativeKey() != KeyCode::None && IsKeyPressed(axis.GetNegativeKey())) { + if (axis.GetNegativeKey() != KeyCode::None && IsKeyDown(axis.GetNegativeKey())) { value -= 1.0f; } @@ -194,8 +221,34 @@ bool InputManager::GetButtonDown(const Containers::String& buttonName) const { bool InputManager::GetButtonUp(const Containers::String& buttonName) const { auto it = m_buttons.find(buttonName); - if (it == m_buttons.end()) return true; - return IsKeyUp(it->second); + if (it == m_buttons.end()) return false; + return IsKeyReleased(it->second); +} + +bool InputManager::IsAnyKeyDown() const { + if (!m_initialized) return false; + + return std::any_of( + m_keyDown.begin(), + m_keyDown.end(), + [](bool isDown) { return isDown; }) + || std::any_of( + m_mouseButtonDown.begin(), + m_mouseButtonDown.end(), + [](bool isDown) { return isDown; }); +} + +bool InputManager::IsAnyKeyPressed() const { + if (!m_initialized) return false; + + return std::any_of( + m_keyDownThisFrame.begin(), + m_keyDownThisFrame.end(), + [](bool isPressed) { return isPressed; }) + || std::any_of( + m_mouseButtonDownThisFrame.begin(), + m_mouseButtonDownThisFrame.end(), + [](bool isPressed) { return isPressed; }); } void InputManager::RegisterAxis(const InputAxis& axis) { @@ -238,6 +291,7 @@ void InputManager::ProcessKeyUp(KeyCode key, bool alt, bool ctrl, bool shift, bo if (index >= m_keyDown.size()) return; m_keyDown[index] = false; + m_keyUpThisFrame[index] = true; KeyEvent event; event.keyCode = key; @@ -274,6 +328,8 @@ void InputManager::ProcessMouseButton(MouseButton button, bool pressed, int x, i m_mouseButtonDown[index] = pressed; if (pressed) { m_mouseButtonDownThisFrame[index] = true; + } else { + m_mouseButtonUpThisFrame[index] = true; } MouseButtonEvent event; diff --git a/engine/src/Rendering/CameraRenderer.cpp b/engine/src/Rendering/CameraRenderer.cpp index cfdf7f6a..33d1d22f 100644 --- a/engine/src/Rendering/CameraRenderer.cpp +++ b/engine/src/Rendering/CameraRenderer.cpp @@ -231,6 +231,25 @@ bool CameraRenderer::Render( } ShutdownPassSequence(&builtinPostProcessPasses, builtinPostProcessPassesInitialized); + + bool overlayPassesInitialized = false; + if (!InitializePassSequence( + request.overlayPasses, + request.context, + overlayPassesInitialized)) { + ShutdownPassSequence(request.postScenePasses, postScenePassesInitialized); + ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized); + return false; + } + if (request.overlayPasses != nullptr && + !request.overlayPasses->Execute(passContext)) { + ShutdownPassSequence(request.overlayPasses, overlayPassesInitialized); + ShutdownPassSequence(request.postScenePasses, postScenePassesInitialized); + ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized); + return false; + } + + ShutdownPassSequence(request.overlayPasses, overlayPassesInitialized); ShutdownPassSequence(request.postScenePasses, postScenePassesInitialized); ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized); diff --git a/engine/src/Resources/Material/MaterialLoader.cpp b/engine/src/Resources/Material/MaterialLoader.cpp index a6f88964..bfaef352 100644 --- a/engine/src/Resources/Material/MaterialLoader.cpp +++ b/engine/src/Resources/Material/MaterialLoader.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -64,6 +65,56 @@ Containers::String NormalizePathString(const std::filesystem::path& path) { return Containers::String(path.lexically_normal().generic_string().c_str()); } +bool IsProjectRelativePath(const std::filesystem::path& path) { + const std::string generic = path.generic_string(); + return !generic.empty() && + generic != "." && + generic != ".." && + generic.rfind("../", 0) != 0; +} + +Containers::String ToProjectRelativeIfPossible(const std::filesystem::path& path) { + const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); + const std::filesystem::path normalizedPath = path.lexically_normal(); + if (!resourceRoot.Empty() && normalizedPath.is_absolute()) { + std::error_code ec; + const std::filesystem::path relativePath = + std::filesystem::relative(normalizedPath, std::filesystem::path(resourceRoot.CStr()), ec); + if (!ec && IsProjectRelativePath(relativePath)) { + return NormalizePathString(relativePath); + } + } + + return NormalizePathString(normalizedPath); +} + +Containers::String ResolveSourceDependencyPath(const Containers::String& dependencyPath, + const Containers::String& sourcePath) { + if (dependencyPath.Empty()) { + return dependencyPath; + } + + std::filesystem::path dependencyFsPath(dependencyPath.CStr()); + if (dependencyFsPath.is_absolute()) { + return NormalizePathString(dependencyFsPath); + } + + const std::filesystem::path sourceFsPath(sourcePath.CStr()); + if (sourceFsPath.is_absolute()) { + return ToProjectRelativeIfPossible(sourceFsPath.parent_path() / dependencyFsPath); + } + + const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); + if (!resourceRoot.Empty()) { + return ToProjectRelativeIfPossible( + std::filesystem::path(resourceRoot.CStr()) / + sourceFsPath.parent_path() / + dependencyFsPath); + } + + return NormalizePathString(sourceFsPath.parent_path() / dependencyFsPath); +} + Containers::String ResolveArtifactDependencyPath(const Containers::String& dependencyPath, const Containers::String& ownerArtifactPath) { if (dependencyPath.Empty()) { @@ -358,6 +409,125 @@ bool TryParseTagMap(const std::string& objectText, Material* material) { return false; } +bool TryParseStringMapObject( + const std::string& objectText, + const std::function& onEntry) { + if (!onEntry || objectText.empty() || objectText.front() != '{' || objectText.back() != '}') { + return false; + } + + size_t pos = 1; + while (pos < objectText.size()) { + pos = SkipWhitespace(objectText, pos); + if (pos >= objectText.size()) { + return false; + } + + if (objectText[pos] == '}') { + return true; + } + + Containers::String key; + if (!ParseQuotedString(objectText, pos, key, &pos)) { + return false; + } + + pos = SkipWhitespace(objectText, pos); + if (pos >= objectText.size() || objectText[pos] != ':') { + return false; + } + + pos = SkipWhitespace(objectText, pos + 1); + Containers::String value; + if (!ParseQuotedString(objectText, pos, value, &pos)) { + return false; + } + + onEntry(key, value); + + pos = SkipWhitespace(objectText, pos); + if (pos >= objectText.size()) { + return false; + } + + if (objectText[pos] == ',') { + ++pos; + continue; + } + + if (objectText[pos] == '}') { + return true; + } + + return false; + } + + return false; +} + +bool TryApplyTexturePath(Material* material, + const Containers::String& textureName, + const Containers::String& texturePath) { + if (material == nullptr || textureName.Empty() || texturePath.Empty()) { + return false; + } + + material->SetTexturePath( + textureName, + ResolveSourceDependencyPath(texturePath, material->GetPath())); + return true; +} + +bool TryParseMaterialTextureBindings(const std::string& jsonText, Material* material) { + if (material == nullptr) { + return false; + } + + static const char* const kKnownTextureKeys[] = { + "baseColorTexture", + "_BaseColorTexture", + "_MainTex", + "normalTexture", + "_BumpMap", + "specularTexture", + "emissiveTexture", + "metallicTexture", + "roughnessTexture", + "occlusionTexture", + "opacityTexture" + }; + + for (const char* key : kKnownTextureKeys) { + if (!HasKey(jsonText, key)) { + continue; + } + + Containers::String texturePath; + if (!TryParseStringValue(jsonText, key, texturePath)) { + return false; + } + + TryApplyTexturePath(material, Containers::String(key), texturePath); + } + + if (HasKey(jsonText, "textures")) { + std::string texturesObject; + if (!TryExtractObject(jsonText, "textures", texturesObject)) { + return false; + } + + if (!TryParseStringMapObject( + texturesObject, + [material](const Containers::String& name, const Containers::String& value) { + TryApplyTexturePath(material, name, value); + })) { + return false; + } + } + + return true; +} + bool TryParseCullMode(const Containers::String& value, MaterialCullMode& outMode) { const Containers::String normalized = value.Trim().ToLower(); if (normalized == "none" || normalized == "off") { @@ -953,6 +1123,10 @@ bool MaterialLoader::ParseMaterialData(const Containers::Array& dat } } + if (!TryParseMaterialTextureBindings(jsonText, material)) { + return false; + } + return true; } diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index 3814fc11..69ede79a 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -7,6 +7,7 @@ #include "Components/MeshRendererComponent.h" #include "Components/TransformComponent.h" #include "Debug/Logger.h" +#include "Input/InputManager.h" #include "Scene/Scene.h" #include "Scripting/ScriptComponent.h" #include "Scripting/ScriptEngine.h" @@ -356,6 +357,92 @@ float InternalCall_Time_GetDeltaTime() { return GetInternalCallDeltaTime(); } +float InternalCall_Time_GetFixedDeltaTime() { + return ScriptEngine::Get().GetRuntimeFixedDeltaTime(); +} + +mono_bool InternalCall_Input_GetKey(int32_t keyCode) { + return XCEngine::Input::InputManager::Get().IsKeyDown( + static_cast(keyCode)) ? 1 : 0; +} + +mono_bool InternalCall_Input_GetKeyDown(int32_t keyCode) { + return XCEngine::Input::InputManager::Get().IsKeyPressed( + static_cast(keyCode)) ? 1 : 0; +} + +mono_bool InternalCall_Input_GetKeyUp(int32_t keyCode) { + return XCEngine::Input::InputManager::Get().IsKeyReleased( + static_cast(keyCode)) ? 1 : 0; +} + +mono_bool InternalCall_Input_GetMouseButton(int32_t button) { + return XCEngine::Input::InputManager::Get().IsMouseButtonDown( + static_cast(button)) ? 1 : 0; +} + +mono_bool InternalCall_Input_GetMouseButtonDown(int32_t button) { + return XCEngine::Input::InputManager::Get().IsMouseButtonClicked( + static_cast(button)) ? 1 : 0; +} + +mono_bool InternalCall_Input_GetMouseButtonUp(int32_t button) { + return XCEngine::Input::InputManager::Get().IsMouseButtonReleased( + static_cast(button)) ? 1 : 0; +} + +mono_bool InternalCall_Input_GetButton(MonoString* buttonName) { + return XCEngine::Input::InputManager::Get().GetButton( + XCEngine::Containers::String(MonoStringToUtf8(buttonName).c_str())) ? 1 : 0; +} + +mono_bool InternalCall_Input_GetButtonDown(MonoString* buttonName) { + return XCEngine::Input::InputManager::Get().GetButtonDown( + XCEngine::Containers::String(MonoStringToUtf8(buttonName).c_str())) ? 1 : 0; +} + +mono_bool InternalCall_Input_GetButtonUp(MonoString* buttonName) { + return XCEngine::Input::InputManager::Get().GetButtonUp( + XCEngine::Containers::String(MonoStringToUtf8(buttonName).c_str())) ? 1 : 0; +} + +float InternalCall_Input_GetAxis(MonoString* axisName) { + return XCEngine::Input::InputManager::Get().GetAxis( + XCEngine::Containers::String(MonoStringToUtf8(axisName).c_str())); +} + +float InternalCall_Input_GetAxisRaw(MonoString* axisName) { + return XCEngine::Input::InputManager::Get().GetAxisRaw( + XCEngine::Containers::String(MonoStringToUtf8(axisName).c_str())); +} + +mono_bool InternalCall_Input_GetAnyKey() { + return XCEngine::Input::InputManager::Get().IsAnyKeyDown() ? 1 : 0; +} + +mono_bool InternalCall_Input_GetAnyKeyDown() { + return XCEngine::Input::InputManager::Get().IsAnyKeyPressed() ? 1 : 0; +} + +void InternalCall_Input_GetMousePosition(XCEngine::Math::Vector3* outPosition) { + if (!outPosition) { + return; + } + + const XCEngine::Math::Vector2 position = XCEngine::Input::InputManager::Get().GetMousePosition(); + *outPosition = XCEngine::Math::Vector3(position.x, position.y, 0.0f); +} + +void InternalCall_Input_GetMouseScrollDelta(XCEngine::Math::Vector2* outDelta) { + if (!outDelta) { + return; + } + + *outDelta = XCEngine::Math::Vector2( + 0.0f, + XCEngine::Input::InputManager::Get().GetMouseScrollDelta()); +} + MonoString* InternalCall_GameObject_GetName(uint64_t gameObjectUUID) { Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); return mono_string_new( @@ -1131,6 +1218,22 @@ void RegisterInternalCalls() { mono_add_internal_call("XCEngine.InternalCalls::Debug_LogWarning", reinterpret_cast(&InternalCall_Debug_LogWarning)); mono_add_internal_call("XCEngine.InternalCalls::Debug_LogError", reinterpret_cast(&InternalCall_Debug_LogError)); mono_add_internal_call("XCEngine.InternalCalls::Time_GetDeltaTime", reinterpret_cast(&InternalCall_Time_GetDeltaTime)); + mono_add_internal_call("XCEngine.InternalCalls::Time_GetFixedDeltaTime", reinterpret_cast(&InternalCall_Time_GetFixedDeltaTime)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetKey", reinterpret_cast(&InternalCall_Input_GetKey)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetKeyDown", reinterpret_cast(&InternalCall_Input_GetKeyDown)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetKeyUp", reinterpret_cast(&InternalCall_Input_GetKeyUp)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetMouseButton", reinterpret_cast(&InternalCall_Input_GetMouseButton)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetMouseButtonDown", reinterpret_cast(&InternalCall_Input_GetMouseButtonDown)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetMouseButtonUp", reinterpret_cast(&InternalCall_Input_GetMouseButtonUp)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetButton", reinterpret_cast(&InternalCall_Input_GetButton)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetButtonDown", reinterpret_cast(&InternalCall_Input_GetButtonDown)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetButtonUp", reinterpret_cast(&InternalCall_Input_GetButtonUp)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetAxis", reinterpret_cast(&InternalCall_Input_GetAxis)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetAxisRaw", reinterpret_cast(&InternalCall_Input_GetAxisRaw)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetAnyKey", reinterpret_cast(&InternalCall_Input_GetAnyKey)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetAnyKeyDown", reinterpret_cast(&InternalCall_Input_GetAnyKeyDown)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetMousePosition", reinterpret_cast(&InternalCall_Input_GetMousePosition)); + mono_add_internal_call("XCEngine.InternalCalls::Input_GetMouseScrollDelta", reinterpret_cast(&InternalCall_Input_GetMouseScrollDelta)); mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetName", reinterpret_cast(&InternalCall_GameObject_GetName)); mono_add_internal_call("XCEngine.InternalCalls::GameObject_SetName", reinterpret_cast(&InternalCall_GameObject_SetName)); mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetActiveSelf", reinterpret_cast(&InternalCall_GameObject_GetActiveSelf)); @@ -1281,20 +1384,57 @@ bool MonoScriptRuntime::IsClassAvailable( return FindClassMetadata(assemblyName, namespaceName, className) != nullptr; } -std::vector MonoScriptRuntime::GetScriptClassNames(const std::string& assemblyName) const { - std::vector classNames; - classNames.reserve(m_classes.size()); +bool MonoScriptRuntime::TryGetAvailableScriptClasses( + std::vector& outClasses) const { + outClasses.clear(); + if (!m_initialized) { + return false; + } + outClasses.reserve(m_classes.size()); for (const auto& [key, metadata] : m_classes) { (void)key; - if (!assemblyName.empty() && metadata.assemblyName != assemblyName) { + outClasses.push_back( + ScriptClassDescriptor{ + metadata.assemblyName, + metadata.namespaceName, + metadata.className + }); + } + + std::sort( + outClasses.begin(), + outClasses.end(), + [](const ScriptClassDescriptor& lhs, const ScriptClassDescriptor& rhs) { + if (lhs.assemblyName != rhs.assemblyName) { + return lhs.assemblyName < rhs.assemblyName; + } + if (lhs.namespaceName != rhs.namespaceName) { + return lhs.namespaceName < rhs.namespaceName; + } + return lhs.className < rhs.className; + }); + + return true; +} + +std::vector MonoScriptRuntime::GetScriptClassNames(const std::string& assemblyName) const { + std::vector classes; + if (!TryGetAvailableScriptClasses(classes)) { + return {}; + } + + std::vector classNames; + classNames.reserve(classes.size()); + + for (const ScriptClassDescriptor& descriptor : classes) { + if (!assemblyName.empty() && descriptor.assemblyName != assemblyName) { continue; } - classNames.push_back(metadata.fullName); + classNames.push_back(descriptor.GetFullName()); } - std::sort(classNames.begin(), classNames.end()); return classNames; } diff --git a/engine/src/Scripting/NullScriptRuntime.cpp b/engine/src/Scripting/NullScriptRuntime.cpp index e8266b9f..d12c98d8 100644 --- a/engine/src/Scripting/NullScriptRuntime.cpp +++ b/engine/src/Scripting/NullScriptRuntime.cpp @@ -11,6 +11,12 @@ void NullScriptRuntime::OnRuntimeStop(Components::Scene* scene) { (void)scene; } +bool NullScriptRuntime::TryGetAvailableScriptClasses( + std::vector& outClasses) const { + outClasses.clear(); + return false; +} + bool NullScriptRuntime::TryGetClassFieldMetadata( const std::string& assemblyName, const std::string& namespaceName, diff --git a/engine/src/Scripting/ScriptComponent.cpp b/engine/src/Scripting/ScriptComponent.cpp index 4a728ca2..167bf134 100644 --- a/engine/src/Scripting/ScriptComponent.cpp +++ b/engine/src/Scripting/ScriptComponent.cpp @@ -25,23 +25,36 @@ ScriptComponent::ScriptComponent() void ScriptComponent::SetScriptClass(const std::string& namespaceName, const std::string& className) { const bool hadScriptClass = HasScriptClass(); + const bool changed = m_namespaceName != namespaceName || m_className != className; m_namespaceName = namespaceName; m_className = className; if (!hadScriptClass && HasScriptClass()) { ScriptEngine::Get().OnScriptComponentEnabled(this); + } else if (hadScriptClass && changed) { + ScriptEngine::Get().OnScriptComponentClassChanged(this); } } void ScriptComponent::SetScriptClass(const std::string& assemblyName, const std::string& namespaceName, const std::string& className) { const bool hadScriptClass = HasScriptClass(); + const bool changed = + m_assemblyName != assemblyName || + m_namespaceName != namespaceName || + m_className != className; m_assemblyName = assemblyName; m_namespaceName = namespaceName; m_className = className; if (!hadScriptClass && HasScriptClass()) { ScriptEngine::Get().OnScriptComponentEnabled(this); + } else if (hadScriptClass && changed) { + ScriptEngine::Get().OnScriptComponentClassChanged(this); } } +void ScriptComponent::ClearScriptClass() { + SetScriptClass(m_assemblyName, std::string(), std::string()); +} + std::string ScriptComponent::GetFullClassName() const { if (m_className.empty()) { return std::string(); diff --git a/engine/src/Scripting/ScriptEngine.cpp b/engine/src/Scripting/ScriptEngine.cpp index 3df0c8d4..3cd18571 100644 --- a/engine/src/Scripting/ScriptEngine.cpp +++ b/engine/src/Scripting/ScriptEngine.cpp @@ -63,8 +63,19 @@ void ScriptEngine::SetRuntime(IScriptRuntime* runtime) { m_runtime = runtime ? runtime : &m_nullRuntime; } +void ScriptEngine::SetRuntimeFixedDeltaTime(float fixedDeltaTime) { + if (fixedDeltaTime > 0.0f) { + m_runtimeFixedDeltaTime = fixedDeltaTime; + return; + } + + m_runtimeFixedDeltaTime = DefaultFixedDeltaTime; +} + void ScriptEngine::OnRuntimeStart(Components::Scene* scene) { + const float configuredFixedDeltaTime = m_runtimeFixedDeltaTime; OnRuntimeStop(); + m_runtimeFixedDeltaTime = configuredFixedDeltaTime; if (!scene) { return; @@ -109,6 +120,7 @@ void ScriptEngine::OnRuntimeStop() { m_runtimeScene = nullptr; m_scriptStates.clear(); m_scriptOrder.clear(); + m_runtimeFixedDeltaTime = DefaultFixedDeltaTime; return; } @@ -125,6 +137,7 @@ void ScriptEngine::OnRuntimeStop() { m_scriptOrder.clear(); m_runtimeRunning = false; m_runtimeScene = nullptr; + m_runtimeFixedDeltaTime = DefaultFixedDeltaTime; m_runtime->OnRuntimeStop(stoppedScene); } @@ -239,6 +252,33 @@ void ScriptEngine::OnScriptComponentDestroyed(ScriptComponent* component) { StopTrackingScript(*state, false); } +void ScriptEngine::OnScriptComponentClassChanged(ScriptComponent* component) { + if (!component) { + return; + } + + if (!m_runtimeRunning) { + return; + } + + if (ScriptInstanceState* state = FindState(component)) { + StopTrackingScript(*state, false); + } + + if (!component->HasScriptClass()) { + return; + } + + ScriptInstanceState* state = TrackScriptComponent(component); + if (!state) { + return; + } + + if (ShouldScriptRun(*state)) { + EnsureScriptReady(*state, true); + } +} + bool ScriptEngine::HasTrackedScriptComponent(const ScriptComponent* component) const { return FindState(component) != nullptr; } @@ -248,6 +288,45 @@ bool ScriptEngine::HasRuntimeInstance(const ScriptComponent* component) const { return state && state->instanceCreated; } +bool ScriptEngine::TryGetAvailableScriptClasses( + std::vector& outClasses, + const std::string& assemblyName) const { + outClasses.clear(); + + std::vector runtimeClasses; + if (!m_runtime->TryGetAvailableScriptClasses(runtimeClasses)) { + return false; + } + + outClasses.reserve(runtimeClasses.size()); + for (const ScriptClassDescriptor& descriptor : runtimeClasses) { + if (!assemblyName.empty() && descriptor.assemblyName != assemblyName) { + continue; + } + + if (descriptor.className.empty()) { + continue; + } + + outClasses.push_back(descriptor); + } + + std::sort( + outClasses.begin(), + outClasses.end(), + [](const ScriptClassDescriptor& lhs, const ScriptClassDescriptor& rhs) { + if (lhs.assemblyName != rhs.assemblyName) { + return lhs.assemblyName < rhs.assemblyName; + } + if (lhs.namespaceName != rhs.namespaceName) { + return lhs.namespaceName < rhs.namespaceName; + } + return lhs.className < rhs.className; + }); + + return true; +} + bool ScriptEngine::TrySetScriptFieldValue( ScriptComponent* component, const std::string& fieldName, diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index d9b4ed88..16a2c99c 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -37,6 +37,31 @@ set(XCENGINE_MONO_MSCORLIB_PATH "${XCENGINE_MONO_CORLIB_DIR}/mscorlib.dll") set(XCENGINE_SCRIPT_CORE_DLL "${XCENGINE_MANAGED_OUTPUT_DIR}/XCEngine.ScriptCore.dll" CACHE FILEPATH "Generated XCEngine.ScriptCore assembly") set(XCENGINE_GAME_SCRIPTS_DLL "${XCENGINE_MANAGED_OUTPUT_DIR}/GameScripts.dll" CACHE FILEPATH "Generated GameScripts assembly") +set( + XCENGINE_PROJECT_ASSETS_DIR + "${CMAKE_SOURCE_DIR}/project/Assets" + CACHE PATH + "Project asset root scanned for user C# scripts") +set( + XCENGINE_PROJECT_MANAGED_OUTPUT_DIR + "${CMAKE_SOURCE_DIR}/project/Library/ScriptAssemblies" + CACHE PATH + "Output directory for project managed assemblies") +set( + XCENGINE_PROJECT_SCRIPT_CORE_DLL + "${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}/XCEngine.ScriptCore.dll" + CACHE FILEPATH + "Generated script core assembly copied into the project script assembly directory") +set( + XCENGINE_PROJECT_GAME_SCRIPTS_DLL + "${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}/GameScripts.dll" + CACHE FILEPATH + "Generated project game scripts assembly") +set( + XCENGINE_PROJECT_MONO_MSCORLIB_PATH + "${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}/mscorlib.dll" + CACHE FILEPATH + "Mono corlib copied into the project script assembly directory") foreach(XCENGINE_REQUIRED_PATH "${XCENGINE_CSC_DLL}" @@ -55,7 +80,9 @@ set(XCENGINE_SCRIPT_CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Component.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Debug.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/GameObject.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Input.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/InternalCalls.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/KeyCode.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Light.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MeshFilter.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MeshRenderer.cs @@ -76,15 +103,33 @@ set(XCENGINE_GAME_SCRIPT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ScriptComponentApiProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/RuntimeGameObjectProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/HierarchyProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/InputProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/LifecycleProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshComponentProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererEdgeCaseProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TickLogProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformConversionProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformMotionProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformOrientationProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformSpaceProbe.cs ) +file( + GLOB_RECURSE + XCENGINE_PROJECT_GAME_SCRIPT_SOURCES + CONFIGURE_DEPENDS + LIST_DIRECTORIES FALSE + "${XCENGINE_PROJECT_ASSETS_DIR}/*.cs") +list(SORT XCENGINE_PROJECT_GAME_SCRIPT_SOURCES) + +if(NOT XCENGINE_PROJECT_GAME_SCRIPT_SOURCES) + set(XCENGINE_PROJECT_SCRIPT_PLACEHOLDER "${CMAKE_CURRENT_BINARY_DIR}/Generated/EmptyProjectGameScripts.cs") + file(GENERATE + OUTPUT "${XCENGINE_PROJECT_SCRIPT_PLACEHOLDER}" + CONTENT "namespace XCEngine.Generated { public static class EmptyProjectGameScriptsMarker {} }\n") + set(XCENGINE_PROJECT_GAME_SCRIPT_SOURCES "${XCENGINE_PROJECT_SCRIPT_PLACEHOLDER}") +endif() + set(XCENGINE_MANAGED_FRAMEWORK_REFERENCES /reference:${XCENGINE_NET472_REFERENCE_DIR}/mscorlib.dll /reference:${XCENGINE_NET472_REFERENCE_DIR}/System.dll @@ -140,3 +185,47 @@ add_custom_target( ${XCENGINE_MANAGED_OUTPUT_DIR}/mscorlib.dll ) +add_custom_command( + OUTPUT ${XCENGINE_PROJECT_SCRIPT_CORE_DLL} + COMMAND ${CMAKE_COMMAND} -E make_directory ${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${XCENGINE_SCRIPT_CORE_DLL} + ${XCENGINE_PROJECT_SCRIPT_CORE_DLL} + DEPENDS ${XCENGINE_SCRIPT_CORE_DLL} + VERBATIM + COMMENT "Copying XCEngine.ScriptCore.dll into the project script assembly directory") + +add_custom_command( + OUTPUT ${XCENGINE_PROJECT_GAME_SCRIPTS_DLL} + COMMAND ${CMAKE_COMMAND} -E make_directory ${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR} + COMMAND ${XCENGINE_DOTNET_EXECUTABLE} ${XCENGINE_CSC_DLL} + /nologo + /target:library + /langversion:latest + /nostdlib+ + /out:${XCENGINE_PROJECT_GAME_SCRIPTS_DLL} + ${XCENGINE_MANAGED_FRAMEWORK_REFERENCES} + /reference:${XCENGINE_PROJECT_SCRIPT_CORE_DLL} + ${XCENGINE_PROJECT_GAME_SCRIPT_SOURCES} + DEPENDS ${XCENGINE_PROJECT_GAME_SCRIPT_SOURCES} ${XCENGINE_PROJECT_SCRIPT_CORE_DLL} + VERBATIM + COMMENT "Building project GameScripts.dll from project asset scripts") + +add_custom_command( + OUTPUT ${XCENGINE_PROJECT_MONO_MSCORLIB_PATH} + COMMAND ${CMAKE_COMMAND} -E make_directory ${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${XCENGINE_MONO_MSCORLIB_PATH} + ${XCENGINE_PROJECT_MONO_MSCORLIB_PATH} + DEPENDS ${XCENGINE_MONO_MSCORLIB_PATH} + VERBATIM + COMMENT "Copying mscorlib.dll into the project script assembly directory") + +add_custom_target( + xcengine_project_managed_assemblies ALL + DEPENDS + ${XCENGINE_PROJECT_SCRIPT_CORE_DLL} + ${XCENGINE_PROJECT_GAME_SCRIPTS_DLL} + ${XCENGINE_PROJECT_MONO_MSCORLIB_PATH} +) + diff --git a/managed/GameScripts/InputProbe.cs b/managed/GameScripts/InputProbe.cs new file mode 100644 index 00000000..50503f33 --- /dev/null +++ b/managed/GameScripts/InputProbe.cs @@ -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; + } + } +} diff --git a/managed/GameScripts/LifecycleProbe.cs b/managed/GameScripts/LifecycleProbe.cs index 0f0a59f1..c158ff3d 100644 --- a/managed/GameScripts/LifecycleProbe.cs +++ b/managed/GameScripts/LifecycleProbe.cs @@ -27,6 +27,8 @@ namespace Gameplay public float Speed; public float ObservedFixedDeltaTime; + public float ObservedConfiguredFixedDeltaTime; + public float ObservedConfiguredFixedDeltaTimeInUpdate; public float ObservedUpdateDeltaTime; public float ObservedLateDeltaTime; public string Label = string.Empty; @@ -103,6 +105,7 @@ namespace Gameplay { FixedUpdateCount += 1; ObservedFixedDeltaTime = Time.deltaTime; + ObservedConfiguredFixedDeltaTime = Time.fixedDeltaTime; } public void Update() @@ -110,6 +113,7 @@ namespace Gameplay UpdateCount += 1; Speed += 1.0f; ObservedUpdateDeltaTime = Time.deltaTime; + ObservedConfiguredFixedDeltaTimeInUpdate = Time.fixedDeltaTime; ObservedLocalPosition = transform.localPosition; Quaternion rotation = transform.localRotation; ObservedLocalRotation = new Vector4(rotation.x, rotation.y, rotation.z, rotation.w); diff --git a/managed/GameScripts/TickLogProbe.cs b/managed/GameScripts/TickLogProbe.cs new file mode 100644 index 00000000..acbf4138 --- /dev/null +++ b/managed/GameScripts/TickLogProbe.cs @@ -0,0 +1,48 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class TickLogProbe : MonoBehaviour + { + public int FixedUpdateCount; + public int UpdateCount; + public int LateUpdateCount; + + public void Awake() + { + Debug.Log("[TickLogProbe] Awake"); + } + + public void Start() + { + Debug.Log("[TickLogProbe] Start"); + } + + public void FixedUpdate() + { + FixedUpdateCount += 1; + if (FixedUpdateCount <= 3) + { + Debug.Log("[TickLogProbe] FixedUpdate " + FixedUpdateCount); + } + } + + public void Update() + { + UpdateCount += 1; + if (UpdateCount <= 3) + { + Debug.Log("[TickLogProbe] Update " + UpdateCount); + } + } + + public void LateUpdate() + { + LateUpdateCount += 1; + if (LateUpdateCount <= 3) + { + Debug.Log("[TickLogProbe] LateUpdate " + LateUpdateCount); + } + } + } +} diff --git a/managed/XCEngine.ScriptCore/Input.cs b/managed/XCEngine.ScriptCore/Input.cs new file mode 100644 index 00000000..737bf145 --- /dev/null +++ b/managed/XCEngine.ScriptCore/Input.cs @@ -0,0 +1,94 @@ +namespace XCEngine +{ + public static class Input + { + public static bool GetKey(KeyCode key) + { + return InternalCalls.Input_GetKey((int)key); + } + + public static bool GetKeyDown(KeyCode key) + { + return InternalCalls.Input_GetKeyDown((int)key); + } + + public static bool GetKeyUp(KeyCode key) + { + return InternalCalls.Input_GetKeyUp((int)key); + } + + public static bool GetMouseButton(int button) + { + return InternalCalls.Input_GetMouseButton(button); + } + + public static bool GetMouseButtonDown(int button) + { + return InternalCalls.Input_GetMouseButtonDown(button); + } + + public static bool GetMouseButtonUp(int button) + { + return InternalCalls.Input_GetMouseButtonUp(button); + } + + public static bool GetButton(string buttonName) + { + return InternalCalls.Input_GetButton(buttonName); + } + + public static bool GetButtonDown(string buttonName) + { + return InternalCalls.Input_GetButtonDown(buttonName); + } + + public static bool GetButtonUp(string buttonName) + { + return InternalCalls.Input_GetButtonUp(buttonName); + } + + public static float GetAxis(string axisName) + { + return InternalCalls.Input_GetAxis(axisName); + } + + public static float GetAxisRaw(string axisName) + { + return InternalCalls.Input_GetAxisRaw(axisName); + } + + public static bool anyKey + { + get + { + return InternalCalls.Input_GetAnyKey(); + } + } + + public static bool anyKeyDown + { + get + { + return InternalCalls.Input_GetAnyKeyDown(); + } + } + + public static Vector3 mousePosition + { + get + { + InternalCalls.Input_GetMousePosition(out Vector3 position); + return position; + } + } + + public static Vector2 mouseScrollDelta + { + get + { + InternalCalls.Input_GetMouseScrollDelta(out Vector2 delta); + return delta; + } + } + } +} diff --git a/managed/XCEngine.ScriptCore/InternalCalls.cs b/managed/XCEngine.ScriptCore/InternalCalls.cs index 9f44eab1..112e602a 100644 --- a/managed/XCEngine.ScriptCore/InternalCalls.cs +++ b/managed/XCEngine.ScriptCore/InternalCalls.cs @@ -17,6 +17,54 @@ namespace XCEngine [MethodImpl(MethodImplOptions.InternalCall)] internal static extern float Time_GetDeltaTime(); + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern float Time_GetFixedDeltaTime(); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Input_GetKey(int keyCode); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Input_GetKeyDown(int keyCode); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Input_GetKeyUp(int keyCode); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Input_GetMouseButton(int button); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Input_GetMouseButtonDown(int button); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Input_GetMouseButtonUp(int button); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Input_GetButton(string buttonName); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Input_GetButtonDown(string buttonName); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Input_GetButtonUp(string buttonName); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern float Input_GetAxis(string axisName); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern float Input_GetAxisRaw(string axisName); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Input_GetAnyKey(); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Input_GetAnyKeyDown(); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Input_GetMousePosition(out Vector3 position); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Input_GetMouseScrollDelta(out Vector2 delta); + [MethodImpl(MethodImplOptions.InternalCall)] internal static extern string GameObject_GetName(ulong gameObjectUUID); diff --git a/managed/XCEngine.ScriptCore/KeyCode.cs b/managed/XCEngine.ScriptCore/KeyCode.cs new file mode 100644 index 00000000..c4ffea73 --- /dev/null +++ b/managed/XCEngine.ScriptCore/KeyCode.cs @@ -0,0 +1,93 @@ +namespace XCEngine +{ + public enum KeyCode + { + None = 0, + + A = 4, + B = 5, + C = 6, + D = 7, + E = 8, + F = 9, + G = 10, + H = 11, + I = 12, + J = 13, + K = 14, + L = 15, + M = 16, + N = 17, + O = 18, + P = 19, + Q = 20, + R = 21, + S = 22, + T = 23, + U = 24, + V = 25, + W = 26, + X = 27, + Y = 28, + Z = 29, + + F1 = 58, + F2 = 59, + F3 = 60, + F4 = 61, + F5 = 62, + F6 = 63, + F7 = 64, + F8 = 65, + F9 = 66, + F10 = 67, + F11 = 68, + F12 = 69, + + Space = 49, + Tab = 48, + Return = 36, + Escape = 53, + LeftShift = 56, + RightShift = 60, + LeftControl = 59, + RightControl = 62, + LeftAlt = 58, + RightAlt = 61, + + UpArrow = 126, + DownArrow = 125, + LeftArrow = 123, + RightArrow = 124, + + Home = 115, + End = 119, + PageUp = 116, + PageDown = 121, + Delete = 51, + Backspace = 51, + + Alpha0 = 39, + Alpha1 = 30, + Alpha2 = 31, + Alpha3 = 32, + Alpha4 = 33, + Alpha5 = 34, + Alpha6 = 35, + Alpha7 = 37, + Alpha8 = 38, + Alpha9 = 40, + + Minus = 43, + Equals = 46, + LeftBracket = 47, + RightBracket = 54, + Semicolon = 42, + Quote = 40, + Comma = 54, + Period = 55, + Slash = 44, + Backslash = 45, + BackQuote = 41 + } +} diff --git a/managed/XCEngine.ScriptCore/Time.cs b/managed/XCEngine.ScriptCore/Time.cs index bca31a47..ea95fd20 100644 --- a/managed/XCEngine.ScriptCore/Time.cs +++ b/managed/XCEngine.ScriptCore/Time.cs @@ -3,5 +3,6 @@ namespace XCEngine public static class Time { public static float deltaTime => InternalCalls.Time_GetDeltaTime(); + public static float fixedDeltaTime => InternalCalls.Time_GetFixedDeltaTime(); } } diff --git a/tests/Components/test_mesh_render_components.cpp b/tests/Components/test_mesh_render_components.cpp index de4e4663..37d39f11 100644 --- a/tests/Components/test_mesh_render_components.cpp +++ b/tests/Components/test_mesh_render_components.cpp @@ -111,17 +111,34 @@ TEST(MeshFilterComponent_Test, SerializeAndDeserializePreservesPath) { std::stringstream stream; source.Serialize(stream); + const std::string serialized = stream.str(); + EXPECT_NE(serialized.find("meshRef="), std::string::npos); + EXPECT_NE(serialized.find("meshPath=Meshes/serialized.mesh;"), std::string::npos); + EXPECT_EQ(serialized.find("mesh=Meshes/serialized.mesh;"), std::string::npos); MeshFilterComponent target; - target.Deserialize(stream); + std::stringstream deserializeStream(serialized); + target.Deserialize(deserializeStream); EXPECT_EQ(target.GetMeshPath(), "Meshes/serialized.mesh"); EXPECT_EQ(target.GetMesh(), nullptr); + EXPECT_FALSE(target.GetMeshAssetRef().IsValid()); source.ClearMesh(); delete mesh; } +TEST(MeshFilterComponent_Test, DeserializeSupportsLegacyMeshKey) { + MeshFilterComponent target; + + std::stringstream stream("mesh=Meshes/legacy.mesh;meshRef=;"); + target.Deserialize(stream); + + EXPECT_EQ(target.GetMeshPath(), "Meshes/legacy.mesh"); + EXPECT_EQ(target.GetMesh(), nullptr); + EXPECT_FALSE(target.GetMeshAssetRef().IsValid()); +} + TEST(MeshFilterComponent_Test, SetMeshPathPreservesPathWithoutLoadedResource) { MeshFilterComponent component; @@ -144,15 +161,17 @@ TEST(MeshFilterComponent_Test, DeferredSceneDeserializeLoadsMeshAsyncByPath) { manager.RegisterLoader(&fakeLoader); MeshFilterComponent target; + const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount(); { ResourceManager::ScopedDeferredSceneLoad deferredLoadScope; EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled()); std::stringstream stream("mesh=Meshes/async.mesh;meshRef=;"); target.Deserialize(stream); - EXPECT_GT(manager.GetAsyncPendingCount(), 0u); } EXPECT_EQ(target.GetMeshPath(), "Meshes/async.mesh"); + EXPECT_EQ(target.GetMesh(), nullptr); + EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize); ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager)); ASSERT_NE(target.GetMesh(), nullptr); EXPECT_EQ(target.GetMeshPath(), "Meshes/async.mesh"); @@ -200,9 +219,16 @@ TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesMaterialPathsAn std::stringstream stream; source.Serialize(stream); + const std::string serialized = stream.str(); + EXPECT_NE( + serialized.find("materialPaths=Materials/serialized0.mat|Materials/serialized1.mat;"), + std::string::npos); + EXPECT_NE(serialized.find("materialRefs=|;"), std::string::npos); + EXPECT_EQ(serialized.find("materials="), std::string::npos); MeshRendererComponent target; - target.Deserialize(stream); + std::stringstream deserializeStream(serialized); + target.Deserialize(deserializeStream); ASSERT_EQ(target.GetMaterialCount(), 2u); EXPECT_EQ(target.GetMaterial(0), nullptr); @@ -229,9 +255,14 @@ TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesTrailingEmptyMa std::stringstream stream; source.Serialize(stream); + const std::string serialized = stream.str(); + EXPECT_NE(serialized.find("materialPaths=Materials/serialized0.mat|;"), std::string::npos); + EXPECT_NE(serialized.find("materialRefs=|;"), std::string::npos); + EXPECT_EQ(serialized.find("materials="), std::string::npos); MeshRendererComponent target; - target.Deserialize(stream); + std::stringstream deserializeStream(serialized); + target.Deserialize(deserializeStream); ASSERT_EQ(target.GetMaterialCount(), 2u); EXPECT_EQ(target.GetMaterial(0), nullptr); @@ -262,6 +293,23 @@ TEST(MeshRendererComponent_Test, SetMaterialPathPreservesPathWithoutLoadedResour EXPECT_EQ(component.GetMaterial(1), nullptr); } +TEST(MeshRendererComponent_Test, DeserializeSupportsLegacyMaterialsKey) { + MeshRendererComponent target; + + std::stringstream stream( + "materials=Materials/legacy0.mat|;materialRefs=|;castShadows=0;receiveShadows=1;renderLayer=5;"); + target.Deserialize(stream); + + ASSERT_EQ(target.GetMaterialCount(), 2u); + EXPECT_EQ(target.GetMaterialPath(0), "Materials/legacy0.mat"); + EXPECT_EQ(target.GetMaterialPath(1), ""); + EXPECT_EQ(target.GetMaterial(0), nullptr); + EXPECT_EQ(target.GetMaterial(1), nullptr); + EXPECT_FALSE(target.GetCastShadows()); + EXPECT_TRUE(target.GetReceiveShadows()); + EXPECT_EQ(target.GetRenderLayer(), 5u); +} + TEST(MeshRendererComponent_Test, SerializeAndDeserializeLoadsProjectMaterialByAssetRef) { namespace fs = std::filesystem; @@ -300,8 +348,10 @@ TEST(MeshRendererComponent_Test, SerializeAndDeserializeLoadsProjectMaterialByAs std::stringstream stream; source.Serialize(stream); const std::string serialized = stream.str(); + EXPECT_NE(serialized.find("materialPaths=;"), std::string::npos); EXPECT_NE(serialized.find("materialRefs="), std::string::npos); EXPECT_EQ(serialized.find("materialRefs=;"), std::string::npos); + EXPECT_EQ(serialized.find("materials="), std::string::npos); std::stringstream deserializeStream(serialized); MeshRendererComponent target; @@ -350,16 +400,19 @@ TEST(MeshRendererComponent_Test, DeferredSceneDeserializeLoadsProjectMaterialAsy source.Serialize(serializedStream); MeshRendererComponent target; + const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount(); { ResourceManager::ScopedDeferredSceneLoad deferredLoadScope; EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled()); std::stringstream deserializeStream(serializedStream.str()); target.Deserialize(deserializeStream); - EXPECT_GT(manager.GetAsyncPendingCount(), 0u); + EXPECT_EQ(manager.GetAsyncPendingCount(), pendingBeforeDeserialize); } ASSERT_EQ(target.GetMaterialCount(), 1u); EXPECT_EQ(target.GetMaterialPath(0), "Assets/runtime.material"); + EXPECT_EQ(target.GetMaterial(0), nullptr); + EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize); ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager)); ASSERT_NE(target.GetMaterial(0), nullptr); diff --git a/tests/Input/test_input_manager.cpp b/tests/Input/test_input_manager.cpp index 15ff9c19..d307706b 100644 --- a/tests/Input/test_input_manager.cpp +++ b/tests/Input/test_input_manager.cpp @@ -285,6 +285,47 @@ TEST(InputManager, GetButtonUp) { mgr.Shutdown(); } +TEST(InputManager, AnyKeyIncludesKeyboardAndMouseButtons) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + EXPECT_FALSE(mgr.IsAnyKeyDown()); + + mgr.ProcessKeyDown(KeyCode::A, false, false, false, false, false); + EXPECT_TRUE(mgr.IsAnyKeyDown()); + + mgr.ProcessKeyUp(KeyCode::A, false, false, false, false); + EXPECT_FALSE(mgr.IsAnyKeyDown()); + + mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200); + EXPECT_TRUE(mgr.IsAnyKeyDown()); + + mgr.ProcessMouseButton(MouseButton::Left, false, 100, 200); + EXPECT_FALSE(mgr.IsAnyKeyDown()); + + mgr.Shutdown(); +} + +TEST(InputManager, AnyKeyPressedIsFrameScoped) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + EXPECT_FALSE(mgr.IsAnyKeyPressed()); + + mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200); + EXPECT_TRUE(mgr.IsAnyKeyPressed()); + + mgr.Update(0.016f); + EXPECT_FALSE(mgr.IsAnyKeyPressed()); + EXPECT_TRUE(mgr.IsAnyKeyDown()); + + mgr.ProcessMouseButton(MouseButton::Left, false, 100, 200); + EXPECT_FALSE(mgr.IsAnyKeyPressed()); + EXPECT_FALSE(mgr.IsAnyKeyDown()); + + mgr.Shutdown(); +} + TEST(InputManager, RegisterAxis) { InputManager& mgr = InputManager::Get(); mgr.Initialize(nullptr); @@ -320,7 +361,7 @@ TEST(InputManager, GetAxisRaw) { EXPECT_EQ(mgr.GetAxisRaw("RawTest"), 1.0f); mgr.Update(0.016f); - EXPECT_EQ(mgr.GetAxisRaw("RawTest"), 0.0f); + EXPECT_EQ(mgr.GetAxisRaw("RawTest"), 1.0f); mgr.Shutdown(); } diff --git a/tests/Scene/test_scene.cpp b/tests/Scene/test_scene.cpp index 332811bd..304133ef 100644 --- a/tests/Scene/test_scene.cpp +++ b/tests/Scene/test_scene.cpp @@ -1030,12 +1030,18 @@ TEST(Scene_ProjectSample, DeferredLoadBackpackSceneEventuallyProducesVisibleRend loadedScene.Load(backpackScenePath.string()); } - ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000))); - const std::vector backpackObjects = FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj"); ASSERT_EQ(backpackObjects.size(), 2u); + XCEngine::Rendering::RenderSceneExtractor extractor; + const XCEngine::Rendering::RenderSceneData initialRenderScene = + extractor.Extract(loadedScene, nullptr, 1280u, 720u); + + ASSERT_TRUE(initialRenderScene.HasCamera()); + EXPECT_GT(resourceManager.GetAsyncPendingCount(), 0u); + ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000))); + std::vector backpackMeshFilters; for (GameObject* backpackObject : backpackObjects) { ASSERT_NE(backpackObject, nullptr); @@ -1045,7 +1051,6 @@ TEST(Scene_ProjectSample, DeferredLoadBackpackSceneEventuallyProducesVisibleRend backpackMeshFilters.push_back(meshFilter); } - XCEngine::Rendering::RenderSceneExtractor extractor; const XCEngine::Rendering::RenderSceneData renderScene = extractor.Extract(loadedScene, nullptr, 1280u, 720u); diff --git a/tests/Scene/test_scene_runtime.cpp b/tests/Scene/test_scene_runtime.cpp index 15a1790e..1198df9b 100644 --- a/tests/Scene/test_scene_runtime.cpp +++ b/tests/Scene/test_scene_runtime.cpp @@ -85,6 +85,12 @@ public: } } + bool TryGetAvailableScriptClasses( + std::vector& outClasses) const override { + outClasses.clear(); + return false; + } + bool TryGetClassFieldMetadata( const std::string& assemblyName, const std::string& namespaceName, diff --git a/tests/TEST_SPEC.md b/tests/TEST_SPEC.md index e7173001..237a379c 100644 --- a/tests/TEST_SPEC.md +++ b/tests/TEST_SPEC.md @@ -1,43 +1,61 @@ # XCEngine 测试规范 -最后更新:2026-03-27 +最后更新:`2026-04-02` ## 1. 目标 -本文档只描述当前仓库里已经存在、已经生效、并且应当长期维护的测试规范。 +本文档描述当前仓库里已经落地、已经生效、并且应当长期维护的测试基线。 重点覆盖: -- `tests/` 的整体组织方式 -- CMake / CTest 的推荐使用方式 -- RHI 模块测试的分层边界 -- RHI 抽象层集成测试的当前约束 +- `tests/` 的真实目录结构 +- 当前 CMake / CTest 入口 +- RHI 测试分层边界 +- Rendering / Editor / Scripting 相关聚合 target -如果目录结构、target 名称、后端覆盖范围或 GT 图规则发生变化,必须同步更新本文档。 +如果目录结构、target 名称、场景覆盖范围或回归入口变化,必须同步更新本文档。 ## 2. 顶层结构 -`tests/CMakeLists.txt` 当前纳入的主要测试模块如下: +`tests/CMakeLists.txt` 当前纳入的主模块如下: ```text tests/ -├─ math/ -├─ core/ -├─ containers/ -├─ memory/ -├─ threading/ -├─ debug/ -├─ components/ -├─ scene/ +├─ Components/ +├─ Core/ +│ ├─ Asset/ +│ ├─ Containers/ +│ ├─ IO/ +│ └─ Math/ +├─ Debug/ +├─ Editor/ +├─ Fixtures/ +├─ Input/ +├─ Memory/ +├─ Rendering/ +│ ├─ integration/ +│ └─ unit/ +├─ Resources/ +│ ├─ AudioClip/ +│ ├─ Material/ +│ ├─ Mesh/ +│ ├─ Shader/ +│ └─ Texture/ ├─ RHI/ -├─ resources/ -└─ input/ +│ ├─ D3D12/ +│ ├─ integration/ +│ ├─ OpenGL/ +│ ├─ unit/ +│ └─ Vulkan/ +├─ Scene/ +├─ Scripting/ +└─ Threading/ ``` 说明: -- 新增测试模块时,必须同时补充对应目录下的 `CMakeLists.txt`,并在 `tests/CMakeLists.txt` 中注册。 -- 测试目录命名、target 命名、文档命名必须和实际仓库状态一致,不保留“计划中但未落地”的旧说明。 +- 新增测试模块时,必须同时补对应目录下的 `CMakeLists.txt`,并在 `tests/CMakeLists.txt` 注册。 +- 目录名、target 名、文档说明必须与当前 checkout 一致,不保留“计划中但未落地”的旧描述。 ## 3. 基本构建方式 @@ -46,113 +64,111 @@ tests/ ### 3.1 配置 ```bash -cmake -S . -B build +cmake -S . -B build -A x64 ``` -以下场景需要重新配置: +如果当前任务不需要 Mono: + +```bash +cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF +``` + +以下情况需要重新配置: - 新增或删除源文件 - 修改任意 `CMakeLists.txt` -- 修改 target、依赖、编译定义或测试发现方式 +- 修改 target、依赖、编译定义、测试发现方式 -### 3.2 增量构建 +### 3.2 通用入口 ```bash cmake --build build --config Debug -``` - -常用 RHI 聚合 target: - -```bash -cmake --build build --config Debug --target rhi_abstraction_tests -cmake --build build --config Debug --target rhi_backend_tests -cmake --build build --config Debug --target rhi_all_tests -``` - -常用单项 target: - -```bash -cmake --build build --config Debug --target rhi_unit_tests -cmake --build build --config Debug --target rhi_integration_minimal -cmake --build build --config Debug --target rhi_integration_triangle -cmake --build build --config Debug --target rhi_integration_quad -cmake --build build --config Debug --target rhi_integration_sphere -cmake --build build --config Debug --target rhi_integration_backpack -``` - -### 3.3 运行 - -列出测试: - -```bash ctest --test-dir build -N -C Debug -``` - -运行全部测试: - -```bash ctest --test-dir build -C Debug --output-on-failure ``` -直接运行 gtest 可执行文件也是合法入口。例如: +## 4. 当前主要 target -```bash -build\tests\RHI\unit\Debug\rhi_unit_tests.exe --gtest_brief=1 -build\tests\RHI\integration\triangle\Debug\rhi_integration_triangle.exe --gtest_filter=Vulkan/TriangleTest.RenderTriangle/0 -build\tests\RHI\integration\backpack\Debug\rhi_integration_backpack.exe --gtest_filter=D3D12/BackpackTest.RenderBackpack/0 -``` +### 4.1 Engine 基础模块 -## 4. RHI 测试分层 +| 模块 | target | +| --- | --- | +| Core | `core_tests` | +| Core/Asset | `asset_tests` | +| Core/Containers | `containers_tests` | +| Core/IO | `io_tests` | +| Core/Math | `math_tests` | +| Memory | `memory_tests` | +| Threading | `threading_tests` | +| Debug | `debug_tests` | +| Components | `components_tests` | +| Scene | `scene_tests` | +| Input | `input_tests` | -RHI 当前分为四层测试: +### 4.2 Resources -| 层级 | 目录 / target | 目标 | -| --- | --- | --- | -| 抽象层单元测试 | `tests/RHI/unit/` / `rhi_unit_tests` | 验证公共 RHI 接口在 `D3D12 / OpenGL / Vulkan` 上的统一语义 | -| 抽象层集成测试 | `tests/RHI/integration/` / `rhi_integration_*` | 用同一套 RHI 抽象代码驱动三后端完成真实渲染并做 GT 图比对 | -| D3D12 后端测试 | `tests/RHI/D3D12/` | 验证 D3D12 封装本身 | -| OpenGL 后端测试 | `tests/RHI/OpenGL/` | 验证 OpenGL 封装本身 | -| Vulkan 后端测试 | `tests/RHI/Vulkan/` | 验证 Vulkan 封装本身 | +| 模块 | target | +| --- | --- | +| AudioClip | `audioclip_tests` | +| Material | `material_tests` | +| Mesh | `mesh_tests` | +| Shader | `shader_tests` | +| Texture | `texture_tests` | + +这些测试当前不只是验证简单 loader,还会覆盖: + +- `AssetDatabase` +- `.meta` +- `Library/SourceAssetDB` +- `Library/ArtifactDB` +- artifact 重导入逻辑 + +### 4.3 Editor / Scripting + +| 模块 | target | +| --- | --- | +| Editor | `editor_tests` | +| Scripting | `scripting_tests` | 补充说明: -- Vulkan 现在已经拥有独立的 `tests/RHI/Vulkan/` 子树。 -- `tests/RHI/unit/` 继续只保留三后端参数化的抽象层统一语义测试。 -- Vulkan 专属断言、原生句柄检查与直接依赖 Vulkan API 的测试,统一收敛到 `tests/RHI/Vulkan/unit/`。 -- Vulkan 现在已经建立独立的后端 integration 子树,当前已覆盖 `tests/RHI/Vulkan/integration/minimal/`、`triangle/`、`quad/`、`sphere/`。 -- Vulkan 后端后续仍可继续补更复杂的 backend integration,但不应再回流到 abstraction suite。 +- `editor_tests` 当前覆盖 action routing、play session、viewport camera controller、picker、move/rotate/scale gizmo、overlay renderer、script component editor utils 等。 +- `scripting_tests` 在启用 Mono 时会补入 `test_mono_script_runtime.cpp`。 +- 如果存在 `xcengine_project_managed_assemblies` target,`scripting_tests` 还会补入项目脚本程序集相关测试。 -设计边界: - -- `tests/RHI/unit/` 与 `tests/RHI/integration/` 只能依赖公共 RHI 抽象接口。 -- 后端私有头文件、原生句柄、后端专用 helper,只允许出现在对应后端目录。 -- 如果抽象层测试为了通过而被迫引入后端 API,优先修 RHI 本身,而不是给测试开后门。 - -## 5. 当前 RHI Target - -### 5.1 抽象层 +### 4.4 Rendering | 类别 | target | | --- | --- | -| 抽象层单元测试 | `rhi_unit_tests` | -| 抽象层集成测试 | `rhi_integration_minimal` | -| 抽象层集成测试 | `rhi_integration_triangle` | -| 抽象层集成测试 | `rhi_integration_quad` | -| 抽象层集成测试 | `rhi_integration_sphere` | -| 抽象层集成测试 | `rhi_integration_backpack` | +| 单元测试 | `rendering_unit_tests` | +| 单测聚合 | `rendering_unit_test_targets` | +| 集成聚合 | `rendering_integration_tests` | +| 全量聚合 | `rendering_all_tests` | +| 阶段回归 | `rendering_phase_regression` | -### 5.2 后端专用 +当前 rendering integration targets: -| 类别 | target | -| --- | --- | -| D3D12 后端单元测试 | `rhi_d3d12_tests` | -| OpenGL 后端单元测试 | `rhi_opengl_tests` | -| Vulkan 后端单元测试 | `rhi_vulkan_tests` | -| D3D12 后端集成测试 | `d3d12_minimal_test` `d3d12_triangle_test` `d3d12_quad_test` `d3d12_sphere_test` | -| OpenGL 后端集成测试 | `opengl_minimal_test` `opengl_triangle_test` `opengl_quad_test` `opengl_sphere_test` | -| Vulkan 后端集成测试 | `vulkan_minimal_test` `vulkan_triangle_test` `vulkan_quad_test` `vulkan_sphere_test` | +- `rendering_integration_textured_quad_scene` +- `rendering_integration_backpack_scene` +- `rendering_integration_backpack_lit_scene` +- `rendering_integration_camera_stack_scene` +- `rendering_integration_transparent_material_scene` +- `rendering_integration_cull_material_scene` +- `rendering_integration_depth_sort_scene` +- `rendering_integration_material_state_scene` +- `rendering_integration_offscreen_scene` -### 5.3 聚合 target +`rendering_phase_regression` 当前是 Windows 下的 PowerShell 回归入口,依赖: + +- `rendering_all_tests` +- `editor_tests` +- `XCEditor` + +并执行: + +- `scripts/Run-RendererPhaseRegression.ps1` + +### 4.5 RHI | 类别 | target | | --- | --- | @@ -165,6 +181,53 @@ RHI 当前分为四层测试: | 后端总聚合 | `rhi_backend_tests` | | RHI 全量聚合 | `rhi_all_tests` | +抽象层单项 target: + +- `rhi_unit_tests` +- `rhi_integration_minimal` +- `rhi_integration_triangle` +- `rhi_integration_quad` +- `rhi_integration_sphere` +- `rhi_integration_backpack` + +后端单元测试 target: + +- `rhi_d3d12_tests` +- `rhi_opengl_tests` +- `rhi_vulkan_tests` + +后端集成测试 target: + +- `d3d12_minimal_test` +- `d3d12_triangle_test` +- `d3d12_quad_test` +- `d3d12_sphere_test` +- `opengl_minimal_test` +- `opengl_triangle_test` +- `opengl_quad_test` +- `opengl_sphere_test` +- `vulkan_minimal_test` +- `vulkan_triangle_test` +- `vulkan_quad_test` +- `vulkan_sphere_test` + +## 5. RHI 测试分层 + +RHI 当前分为五块: + +- `tests/RHI/unit/` +- `tests/RHI/integration/` +- `tests/RHI/D3D12/` +- `tests/RHI/OpenGL/` +- `tests/RHI/Vulkan/` + +边界规则: + +- `tests/RHI/unit/` 与 `tests/RHI/integration/` 只能依赖公共 RHI 抽象接口。 +- 后端私有头文件、原生句柄、后端专用 helper,只允许出现在对应后端目录。 +- 如果抽象层测试为了通过而被迫引入后端 API,优先修 RHI,而不是给测试开后门。 +- Vulkan 专属断言、原生句柄检查与直接依赖 Vulkan API 的测试,应继续收敛在 `tests/RHI/Vulkan/` 子树。 + ## 6. RHI 抽象层集成测试规范 ### 6.1 当前目录 @@ -208,24 +271,10 @@ tests/RHI/integration/ - `minimal_vulkan.ppm` 5. 所有后端都必须与同一张 `GT.ppm` 做比对。 6. 新场景如果暴露抽象层缺口,应先补 RHI,再补测试。 -7. `sphere` 必须继续保留对 `firstSet != 0` 绑定路径的覆盖,避免“忽略 set 语义也误通过”的假阳性。 -8. `backpack` 是当前抽象层最接近真实资源渲染的场景,后续新增复杂渲染场景时不应削弱它的回归价值。 +7. `sphere` 必须继续保留对 `firstSet != 0` 绑定路径的覆盖。 +8. `backpack` 作为最接近真实资源路径的抽象层场景,不应随意削弱。 -### 6.4 CMake 约束 - -每个抽象层集成测试目录都应: - -- 使用独立 target -- 复用 `fixtures/RHIIntegrationFixture.cpp` -- 链接 `XCEngine`、`GTest::gtest` -- 在需要时按后端条件加入对应系统库 -- 在 `POST_BUILD` 中复制: - - `compare_ppm.py` - - 当前场景的 `GT.ppm` - - 运行所需的资源文件 -- 使用 `gtest_discover_tests(...)` - -### 6.5 推荐验证方式 +### 6.4 推荐验证方式 ```bash cmake --build build --config Debug --target rhi_abstraction_integration_tests @@ -242,23 +291,21 @@ build\tests\RHI\integration\backpack\Debug\rhi_integration_backpack.exe --gtest_ - 截图成功 - 与当前场景 `GT.ppm` 比对通过 -## 7. 当前体系的完成度与后续方向 +## 7. 推荐的验证策略 -当前状态可以认为是“高完成度、可作为正式基线”,但不是“完全封顶”。 +按改动类型选择最小有效验证: -已经完成: +- 改 `engine/RHI`:先跑 `rhi_abstraction_tests` 或 `rhi_backend_tests` +- 改 `engine/Rendering`:先跑 `rendering_unit_tests` 和最相关的 `rendering_integration_*` +- 改 `editor/Viewport`:先跑 `editor_tests`,必要时再跑 `rendering_phase_regression` +- 改脚本运行时 / managed / 项目脚本程序集:先构建 `xcengine_project_managed_assemblies`,再跑 `scripting_tests` +- 改资源导入 / `.meta` / artifact:优先跑对应 `Resources/*` tests -- 抽象层单测正式纳入 `D3D12 / OpenGL / Vulkan` -- 抽象层集成测试已经具备 5 个真实渲染场景 -- GT 图回归链路已经稳定工作 -- D3D12 与 OpenGL 仍保留独立后端测试树 +在这些最小验证通过后,再决定是否扩展到: -仍需继续完善: - -- 继续补更工程化的 Vulkan 后端 integration 场景覆盖 -- 把仍然合理存在的后端专属断言与 skip 场景继续收敛 -- 补充 `resize / swapchain 重建 / 长时间 soak / 多线程录制 / validation layer 负例` 等更工程化的测试 -- 保持文档、CMake target 与实际测试状态同步 +```bash +ctest --test-dir build -C Debug --output-on-failure +``` ## 8. 文档维护要求 @@ -266,9 +313,9 @@ build\tests\RHI\integration\backpack\Debug\rhi_integration_backpack.exe --gtest_ - 测试目录结构变化 - 新增或删除测试 target -- 抽象层集成测试场景变化 -- GT 图管理规则变化 -- CMake 聚合入口变化 -- 后端覆盖范围变化 +- rendering integration 场景变化 +- RHI 抽象层 GT 图规则变化 +- PowerShell 回归入口变化 +- 项目脚本程序集参与测试的方式变化 禁止继续保留和当前仓库状态不一致的旧说明。 diff --git a/tests/core/Asset/test_resource_manager.cpp b/tests/core/Asset/test_resource_manager.cpp index b5e30e4d..4157950d 100644 --- a/tests/core/Asset/test_resource_manager.cpp +++ b/tests/core/Asset/test_resource_manager.cpp @@ -1,5 +1,7 @@ #include +#include +#include #include #include #include @@ -7,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -155,4 +159,77 @@ TEST(ResourceManager_Test, ConcurrentAsyncLoadsCoalesceSameMeshPath) { manager.Shutdown(); } +TEST(ResourceManager_Test, AssetLookupFallbackRefreshesSnapshotForNewProjectAsset) { + namespace fs = std::filesystem; + + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + + const fs::path projectRoot = fs::temp_directory_path() / "xc_resource_manager_asset_lookup_test"; + const fs::path assetsDir = projectRoot / "Assets"; + const fs::path materialPath = assetsDir / "runtime.material"; + + fs::remove_all(projectRoot); + fs::create_directories(assetsDir); + manager.SetResourceRoot(projectRoot.string().c_str()); + + { + std::ofstream materialFile(materialPath); + ASSERT_TRUE(materialFile.is_open()); + materialFile << "{\n"; + materialFile << " \"renderQueue\": \"geometry\"\n"; + materialFile << "}\n"; + } + + AssetRef assetRef; + EXPECT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, assetRef)); + EXPECT_TRUE(assetRef.IsValid()); + + XCEngine::Containers::String resolvedPath; + EXPECT_TRUE(manager.TryResolveAssetPath(assetRef, resolvedPath)); + EXPECT_EQ(std::string(resolvedPath.CStr()), "Assets/runtime.material"); + + manager.SetResourceRoot(""); + manager.Shutdown(); + fs::remove_all(projectRoot); +} + +TEST(ProjectAssetIndex_Test, RefreshesSnapshotThroughImportServiceOnCacheMiss) { + namespace fs = std::filesystem; + + AssetImportService importService; + importService.Initialize(); + + const fs::path projectRoot = fs::temp_directory_path() / "xc_project_asset_index_refresh_test"; + const fs::path assetsDir = projectRoot / "Assets"; + const fs::path materialPath = assetsDir / "runtime.material"; + + fs::remove_all(projectRoot); + fs::create_directories(assetsDir); + + importService.SetProjectRoot(projectRoot.string().c_str()); + + ProjectAssetIndex assetIndex; + assetIndex.RefreshFrom(importService); + + { + std::ofstream materialFile(materialPath); + ASSERT_TRUE(materialFile.is_open()); + materialFile << "{\n"; + materialFile << " \"renderQueue\": \"geometry\"\n"; + materialFile << "}\n"; + } + + AssetRef assetRef; + EXPECT_TRUE(assetIndex.TryGetAssetRef(importService, "Assets/runtime.material", ResourceType::Material, assetRef)); + EXPECT_TRUE(assetRef.IsValid()); + + XCEngine::Containers::String resolvedPath; + EXPECT_TRUE(assetIndex.TryResolveAssetPath(importService, assetRef, resolvedPath)); + EXPECT_EQ(std::string(resolvedPath.CStr()), "Assets/runtime.material"); + + importService.Shutdown(); + fs::remove_all(projectRoot); +} + } // namespace diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index dc9ef0f3..e9dbfe5c 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -11,11 +11,15 @@ set(EDITOR_TEST_SOURCES test_scene_viewport_scale_gizmo.cpp test_scene_viewport_picker.cpp test_scene_viewport_overlay_renderer.cpp + test_script_component_editor_utils.cpp test_viewport_host_surface_utils.cpp test_viewport_object_id_picker.cpp test_viewport_render_targets.cpp test_viewport_render_flow_utils.cpp test_builtin_icon_layout_utils.cpp + test_editor_script_assembly_builder.cpp + test_editor_script_assembly_builder_utils.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Scripting/EditorScriptAssemblyBuilder.cpp ${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp ${CMAKE_SOURCE_DIR}/editor/src/Core/PlaySessionController.cpp ${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp @@ -26,13 +30,19 @@ set(EDITOR_TEST_SOURCES ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportScaleGizmo.cpp ) +if(XCENGINE_ENABLE_MONO_SCRIPTING AND TARGET xcengine_managed_assemblies) + list(APPEND EDITOR_TEST_SOURCES + test_play_session_controller_scripting.cpp + ) +endif() + add_executable(editor_tests ${EDITOR_TEST_SOURCES}) if(MSVC) set_target_properties(editor_tests PROPERTIES LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" ) - target_compile_options(editor_tests PRIVATE /FS) + target_compile_options(editor_tests PRIVATE /FS /utf-8) endif() target_link_libraries(editor_tests PRIVATE @@ -50,5 +60,28 @@ target_include_directories(editor_tests PRIVATE ${CMAKE_BINARY_DIR}/_deps/imgui-src/backends ) +file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCENGINE_EDITOR_TEST_REPO_ROOT_CMAKE) +file(TO_CMAKE_PATH "${XCENGINE_MONO_ROOT_DIR}" XCENGINE_EDITOR_TEST_MONO_ROOT_CMAKE) + +target_compile_definitions(editor_tests PRIVATE + XCENGINE_EDITOR_REPO_ROOT="${XCENGINE_EDITOR_TEST_REPO_ROOT_CMAKE}" + XCENGINE_EDITOR_MONO_ROOT_DIR="${XCENGINE_EDITOR_TEST_MONO_ROOT_CMAKE}" +) + +if(XCENGINE_ENABLE_MONO_SCRIPTING AND TARGET xcengine_managed_assemblies) + add_dependencies(editor_tests xcengine_managed_assemblies) + + file(TO_CMAKE_PATH "${XCENGINE_MANAGED_OUTPUT_DIR}" XCENGINE_MANAGED_OUTPUT_DIR_CMAKE) + file(TO_CMAKE_PATH "${XCENGINE_SCRIPT_CORE_DLL}" XCENGINE_SCRIPT_CORE_DLL_CMAKE) + file(TO_CMAKE_PATH "${XCENGINE_GAME_SCRIPTS_DLL}" XCENGINE_GAME_SCRIPTS_DLL_CMAKE) + + target_compile_definitions(editor_tests PRIVATE + XCENGINE_ENABLE_MONO_SCRIPTING + XCENGINE_TEST_MANAGED_OUTPUT_DIR="${XCENGINE_MANAGED_OUTPUT_DIR_CMAKE}" + XCENGINE_TEST_SCRIPT_CORE_DLL="${XCENGINE_SCRIPT_CORE_DLL_CMAKE}" + XCENGINE_TEST_GAME_SCRIPTS_DLL="${XCENGINE_GAME_SCRIPTS_DLL_CMAKE}" + ) +endif() + include(GoogleTest) gtest_discover_tests(editor_tests) diff --git a/tests/editor/test_action_routing.cpp b/tests/editor/test_action_routing.cpp index 4d5f20cc..aa0f667e 100644 --- a/tests/editor/test_action_routing.cpp +++ b/tests/editor/test_action_routing.cpp @@ -9,12 +9,14 @@ #include "Core/EditorContext.h" #include "Core/PlaySessionController.h" +#include #include #include #include #include #include +#include #include namespace fs = std::filesystem; @@ -343,6 +345,17 @@ TEST_F(EditorActionRoutingTest, MainMenuRouterRequestsPlayPauseResumeAndStepEven m_context.GetEventBus().Unsubscribe(playStepSubscription); } +TEST_F(EditorActionRoutingTest, ProjectCommandsReportWhenScriptAssembliesCanBeRebuilt) { + EXPECT_TRUE(Commands::CanRebuildScriptAssemblies(m_context)); + + m_context.SetRuntimeMode(EditorRuntimeMode::Play); + EXPECT_FALSE(Commands::CanRebuildScriptAssemblies(m_context)); + + m_context.SetRuntimeMode(EditorRuntimeMode::Edit); + m_context.SetProjectPath(std::string()); + EXPECT_FALSE(Commands::CanRebuildScriptAssemblies(m_context)); +} + TEST_F(EditorActionRoutingTest, PlayModeAllowsRuntimeSceneUndoRedoButKeepsSceneDocumentCommandsBlocked) { const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "PlayModeRuntimeEditing.xc"; ASSERT_TRUE(m_context.GetSceneManager().SaveSceneAs(savedScenePath.string())); @@ -470,6 +483,68 @@ TEST_F(EditorActionRoutingTest, ProjectCommandsRenameAssetUpdatesSelectionAndPre EXPECT_EQ(m_context.GetProjectManager().GetSelectedItemPath(), renamedItem->fullPath); } +TEST_F(EditorActionRoutingTest, ProjectCommandsMigrateSceneAssetReferencesRewritesLegacyScenePayloads) { + using ::XCEngine::Resources::ResourceManager; + + const fs::path assetsDir = m_projectRoot / "Assets"; + const fs::path scenesDir = assetsDir / "Scenes"; + const fs::path materialPath = assetsDir / "runtime.material"; + const fs::path scenePath = scenesDir / "LegacyScene.xc"; + + { + std::ofstream materialFile(materialPath.string(), std::ios::out | std::ios::trunc); + ASSERT_TRUE(materialFile.is_open()); + materialFile << "{\n"; + materialFile << " \"renderQueue\": \"geometry\",\n"; + materialFile << " \"renderState\": {\n"; + materialFile << " \"cull\": \"back\"\n"; + materialFile << " }\n"; + materialFile << "}\n"; + } + + { + std::ofstream sceneFile(scenePath.string(), std::ios::out | std::ios::trunc); + ASSERT_TRUE(sceneFile.is_open()); + sceneFile << "# XCEngine Scene File\n"; + sceneFile << "scene=Legacy Scene\n"; + sceneFile << "active=1\n\n"; + sceneFile << "gameobject_begin\n"; + sceneFile << "id=1\n"; + sceneFile << "uuid=1\n"; + sceneFile << "name=Legacy Object\n"; + sceneFile << "active=1\n"; + sceneFile << "parent=0\n"; + sceneFile << "transform=position=0,0,0;rotation=0,0,0,1;scale=1,1,1;\n"; + sceneFile << "component=MeshFilter;mesh=builtin://meshes/cube;meshRef=;\n"; + sceneFile << "component=MeshRenderer;materials=Assets/runtime.material;materialRefs=;castShadows=1;receiveShadows=1;renderLayer=0;\n"; + sceneFile << "gameobject_end\n"; + } + + ASSERT_TRUE(Commands::CanMigrateSceneAssetReferences(m_context)); + const IProjectManager::SceneAssetReferenceMigrationReport report = + Commands::MigrateSceneAssetReferences(m_context); + + EXPECT_EQ(report.scannedSceneCount, 1u); + EXPECT_EQ(report.migratedSceneCount, 1u); + EXPECT_EQ(report.unchangedSceneCount, 0u); + EXPECT_EQ(report.failedSceneCount, 0u); + + std::ifstream migratedScene(scenePath.string(), std::ios::in | std::ios::binary); + ASSERT_TRUE(migratedScene.is_open()); + std::string migratedText((std::istreambuf_iterator(migratedScene)), + std::istreambuf_iterator()); + + EXPECT_NE(migratedText.find("meshPath=builtin://meshes/cube;"), std::string::npos); + EXPECT_EQ(migratedText.find("component=MeshFilter;mesh=builtin://meshes/cube;"), std::string::npos); + EXPECT_NE(migratedText.find("materialPaths=;"), std::string::npos); + EXPECT_NE(migratedText.find("materialRefs="), std::string::npos); + EXPECT_EQ(migratedText.find("materialRefs=;"), std::string::npos); + EXPECT_EQ(migratedText.find("component=MeshRenderer;materials="), std::string::npos); + + ResourceManager::Get().SetResourceRoot(""); + ResourceManager::Get().Shutdown(); +} + TEST_F(EditorActionRoutingTest, ProjectItemContextRequestSelectsAssetAndStoresPopupTarget) { const fs::path assetsDir = m_projectRoot / "Assets"; const fs::path filePath = assetsDir / "ContextAsset.txt"; diff --git a/tests/editor/test_editor_script_assembly_builder.cpp b/tests/editor/test_editor_script_assembly_builder.cpp new file mode 100644 index 00000000..bdf3c243 --- /dev/null +++ b/tests/editor/test_editor_script_assembly_builder.cpp @@ -0,0 +1,113 @@ +#include + +#include "Scripting/EditorScriptAssemblyBuilder.h" + +#ifdef XCENGINE_ENABLE_MONO_SCRIPTING +#include +#endif + +#include +#include +#include +#include +#include + +namespace XCEngine::Editor::Scripting { +namespace { + +class EditorScriptAssemblyBuilderTest : public ::testing::Test { +protected: + static void WriteTextFile(const std::filesystem::path& path, const std::string& content) { + std::ofstream output(path, std::ios::out | std::ios::trunc); + ASSERT_TRUE(output.is_open()); + output << content; + output.close(); + ASSERT_TRUE(output.good()); + } + + void SetUp() override { + const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); + m_projectRoot = std::filesystem::temp_directory_path() / ("xc_script_builder_" + std::to_string(stamp)); + std::filesystem::create_directories(m_projectRoot / "Assets" / "Scripts"); + } + + void TearDown() override { + std::error_code ec; + std::filesystem::remove_all(m_projectRoot, ec); + } + + std::filesystem::path m_projectRoot; +}; + +TEST_F(EditorScriptAssemblyBuilderTest, RebuildsProjectScriptAssembliesIntoLibraryDirectory) { + const std::filesystem::path scriptPath = m_projectRoot / "Assets" / "Scripts" / "BuilderProbe.cs"; + WriteTextFile( + scriptPath, + "using XCEngine;\n" + "namespace BuilderTests {\n" + " public sealed class BuilderProbe : MonoBehaviour {\n" + " public float Speed = 4.0f;\n" + " }\n" + "}\n"); + + const EditorScriptAssemblyBuildResult result = + EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string()); + ASSERT_TRUE(result.succeeded) << result.message; + + EXPECT_TRUE(std::filesystem::exists(m_projectRoot / "Library" / "ScriptAssemblies" / "XCEngine.ScriptCore.dll")); + EXPECT_TRUE(std::filesystem::exists(m_projectRoot / "Library" / "ScriptAssemblies" / "GameScripts.dll")); + EXPECT_TRUE(std::filesystem::exists(m_projectRoot / "Library" / "ScriptAssemblies" / "mscorlib.dll")); +} + +#ifdef XCENGINE_ENABLE_MONO_SCRIPTING +TEST_F(EditorScriptAssemblyBuilderTest, RebuildFailsWhileLoadedAssemblyIsStillHeldByMonoRuntime) { + const std::filesystem::path initialScriptPath = m_projectRoot / "Assets" / "Scripts" / "BuilderProbe.cs"; + WriteTextFile( + initialScriptPath, + "using XCEngine;\n" + "namespace BuilderTests {\n" + " public sealed class BuilderProbe : MonoBehaviour {\n" + " public float Speed = 4.0f;\n" + " }\n" + "}\n"); + + EditorScriptAssemblyBuildResult result = + EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string()); + ASSERT_TRUE(result.succeeded) << result.message; + + XCEngine::Scripting::MonoScriptRuntime::Settings settings; + settings.assemblyDirectory = m_projectRoot / "Library" / "ScriptAssemblies"; + settings.corlibDirectory = settings.assemblyDirectory; + settings.coreAssemblyPath = settings.assemblyDirectory / "XCEngine.ScriptCore.dll"; + settings.appAssemblyPath = settings.assemblyDirectory / "GameScripts.dll"; + + auto runtime = std::make_unique(settings); + ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError(); + + const std::filesystem::path addedScriptPath = m_projectRoot / "Assets" / "Scripts" / "TickLogProbe.cs"; + WriteTextFile( + addedScriptPath, + "using XCEngine;\n" + "namespace BuilderTests {\n" + " public sealed class TickLogProbe : MonoBehaviour {\n" + " public int TickCount;\n" + " }\n" + "}\n"); + + result = EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string()); + EXPECT_FALSE(result.succeeded); + + runtime.reset(); + + result = EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string()); + ASSERT_TRUE(result.succeeded) << result.message; + + runtime = std::make_unique(settings); + ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError(); + EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "BuilderTests", "BuilderProbe")); + EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "BuilderTests", "TickLogProbe")); +} +#endif + +} // namespace +} // namespace XCEngine::Editor::Scripting diff --git a/tests/editor/test_editor_script_assembly_builder_utils.cpp b/tests/editor/test_editor_script_assembly_builder_utils.cpp new file mode 100644 index 00000000..80404c2e --- /dev/null +++ b/tests/editor/test_editor_script_assembly_builder_utils.cpp @@ -0,0 +1,74 @@ +#include + +#include "Scripting/EditorScriptAssemblyBuilderUtils.h" + +#include +#include +#include + +namespace XCEngine::Editor::Scripting { +namespace { + +class EditorScriptAssemblyBuilderUtilsTest : public ::testing::Test { +protected: + void SetUp() override { + const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); + m_root = std::filesystem::temp_directory_path() / ("xc_script_builder_utils_" + std::to_string(stamp)); + std::filesystem::create_directories(m_root); + } + + void TearDown() override { + std::error_code ec; + std::filesystem::remove_all(m_root, ec); + } + + std::filesystem::path m_root; +}; + +TEST_F(EditorScriptAssemblyBuilderUtilsTest, CollectsAndSortsCSharpSourceFilesRecursively) { + std::filesystem::create_directories(m_root / "B"); + std::filesystem::create_directories(m_root / "A" / "Nested"); + + std::ofstream(m_root / "B" / "Second.cs").put('\n'); + std::ofstream(m_root / "A" / "Nested" / "Third.cs").put('\n'); + std::ofstream(m_root / "A" / "First.cs").put('\n'); + std::ofstream(m_root / "Ignore.txt").put('\n'); + + const std::vector files = CollectCSharpSourceFiles(m_root); + ASSERT_EQ(files.size(), 3u); + EXPECT_EQ(files[0], (m_root / "A" / "First.cs").lexically_normal()); + EXPECT_EQ(files[1], (m_root / "A" / "Nested" / "Third.cs").lexically_normal()); + EXPECT_EQ(files[2], (m_root / "B" / "Second.cs").lexically_normal()); +} + +TEST(EditorScriptAssemblyBuilderUtils_StandaloneTest, ParsesLatestDotnetSdkVersionFromSdkListOutput) { + const std::string sdkListOutput = + "7.0.410 [C:\\Program Files\\dotnet\\sdk]\n" + "8.0.412 [C:\\Program Files\\dotnet\\sdk]\n" + "9.0.100 [C:\\Program Files\\dotnet\\sdk]\n"; + + EXPECT_EQ(ParseLatestDotnetSdkVersion(sdkListOutput), "9.0.100"); + EXPECT_TRUE(ParseLatestDotnetSdkVersion(std::string()).empty()); +} + +TEST_F(EditorScriptAssemblyBuilderUtilsTest, CreatesPlaceholderProjectScriptSourceWhenNoScriptsExist) { + std::vector projectSources; + std::string error; + const std::filesystem::path placeholderPath = m_root / "Generated" / "EmptyProjectGameScripts.cs"; + + ASSERT_TRUE(EnsurePlaceholderProjectScriptSource(projectSources, placeholderPath, error)) << error; + ASSERT_EQ(projectSources.size(), 1u); + EXPECT_EQ(projectSources.front(), placeholderPath.lexically_normal()); + EXPECT_TRUE(std::filesystem::exists(placeholderPath)); + + std::ifstream input(placeholderPath); + std::string content((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + EXPECT_NE(content.find("EmptyProjectGameScriptsMarker"), std::string::npos); + + error.clear(); + ASSERT_TRUE(EnsurePlaceholderProjectScriptSource(projectSources, placeholderPath, error)) << error; + EXPECT_EQ(projectSources.size(), 1u); +} + +} // namespace +} // namespace XCEngine::Editor::Scripting diff --git a/tests/editor/test_play_session_controller.cpp b/tests/editor/test_play_session_controller.cpp index 5497ff13..99d2f214 100644 --- a/tests/editor/test_play_session_controller.cpp +++ b/tests/editor/test_play_session_controller.cpp @@ -5,16 +5,55 @@ #include "Core/PlaySessionController.h" #include +#include namespace XCEngine::Editor { namespace { +GameViewInputFrameEvent CreateGameViewInputFrame( + bool focused, + bool hovered, + std::initializer_list keys = {}, + std::initializer_list mouseButtons = {}, + XCEngine::Math::Vector2 mousePosition = XCEngine::Math::Vector2::Zero(), + XCEngine::Math::Vector2 mouseDelta = XCEngine::Math::Vector2::Zero(), + float mouseWheel = 0.0f) { + GameViewInputFrameEvent event = {}; + event.focused = focused; + event.hovered = hovered; + event.mousePosition = mousePosition; + event.mouseDelta = mouseDelta; + event.mouseWheel = mouseWheel; + + for (const XCEngine::Input::KeyCode key : keys) { + const size_t index = static_cast(key); + if (index < event.keyDown.size()) { + event.keyDown[index] = true; + } + } + + for (const XCEngine::Input::MouseButton button : mouseButtons) { + const size_t index = static_cast(button); + if (index < event.mouseButtonDown.size()) { + event.mouseButtonDown[index] = true; + } + } + + return event; +} + class PlaySessionControllerTest : public ::testing::Test { protected: void SetUp() override { + XCEngine::Input::InputManager::Get().Shutdown(); m_context.GetSceneManager().NewScene("Play Session Scene"); } + void TearDown() override { + m_controller.Detach(m_context); + XCEngine::Input::InputManager::Get().Shutdown(); + } + EditorContext m_context; PlaySessionController m_controller; }; @@ -120,5 +159,82 @@ TEST_F(PlaySessionControllerTest, PauseResumeAndStepRequestsDrivePlayStateMachin m_context.GetEventBus().Unsubscribe(resumedSubscription); } +TEST_F(PlaySessionControllerTest, GameViewInputFramesDoNotAffectInputManagerOutsidePlayMode) { + m_controller.Attach(m_context); + + m_context.GetEventBus().Publish(CreateGameViewInputFrame( + true, + true, + {XCEngine::Input::KeyCode::A}, + {XCEngine::Input::MouseButton::Left}, + XCEngine::Math::Vector2(120.0f, 48.0f), + XCEngine::Math::Vector2(3.0f, -2.0f), + 1.0f)); + + m_controller.Update(m_context, 0.016f); + + auto& inputManager = XCEngine::Input::InputManager::Get(); + EXPECT_FALSE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A)); + EXPECT_FALSE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left)); + EXPECT_EQ(inputManager.GetMousePosition(), XCEngine::Math::Vector2::Zero()); + EXPECT_FLOAT_EQ(inputManager.GetMouseScrollDelta(), 0.0f); + + m_controller.Detach(m_context); +} + +TEST_F(PlaySessionControllerTest, GameViewInputFramesDriveAndReleaseRuntimeInputDuringPlayMode) { + auto* editorEntity = m_context.GetSceneManager().CreateEntity("Persistent"); + ASSERT_NE(editorEntity, nullptr); + + m_controller.Attach(m_context); + ASSERT_TRUE(m_controller.StartPlay(m_context)); + + m_context.GetEventBus().Publish(CreateGameViewInputFrame( + true, + true, + {XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space}, + {XCEngine::Input::MouseButton::Left}, + XCEngine::Math::Vector2(120.0f, 48.0f), + XCEngine::Math::Vector2(3.0f, -2.0f), + 1.0f)); + + m_controller.Update(m_context, 0.016f); + + auto& inputManager = XCEngine::Input::InputManager::Get(); + EXPECT_TRUE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A)); + EXPECT_TRUE(inputManager.IsKeyPressed(XCEngine::Input::KeyCode::A)); + EXPECT_TRUE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::Space)); + EXPECT_TRUE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left)); + EXPECT_TRUE(inputManager.IsMouseButtonClicked(XCEngine::Input::MouseButton::Left)); + EXPECT_EQ(inputManager.GetMousePosition(), XCEngine::Math::Vector2(120.0f, 48.0f)); + EXPECT_EQ(inputManager.GetMouseDelta(), XCEngine::Math::Vector2(3.0f, -2.0f)); + EXPECT_FLOAT_EQ(inputManager.GetMouseScrollDelta(), 1.0f); + + m_context.GetEventBus().Publish(CreateGameViewInputFrame( + true, + true, + {XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space}, + {XCEngine::Input::MouseButton::Left}, + XCEngine::Math::Vector2(120.0f, 48.0f))); + + m_controller.Update(m_context, 0.016f); + + EXPECT_TRUE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A)); + EXPECT_FALSE(inputManager.IsKeyPressed(XCEngine::Input::KeyCode::A)); + EXPECT_TRUE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left)); + EXPECT_FALSE(inputManager.IsMouseButtonClicked(XCEngine::Input::MouseButton::Left)); + EXPECT_EQ(inputManager.GetMouseDelta(), XCEngine::Math::Vector2::Zero()); + EXPECT_FLOAT_EQ(inputManager.GetMouseScrollDelta(), 0.0f); + + m_context.GetEventBus().Publish(GameViewInputFrameEvent{}); + m_controller.Update(m_context, 0.016f); + + EXPECT_FALSE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A)); + EXPECT_FALSE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::Space)); + EXPECT_FALSE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left)); + + m_controller.Detach(m_context); +} + } // namespace } // namespace XCEngine::Editor diff --git a/tests/editor/test_play_session_controller_scripting.cpp b/tests/editor/test_play_session_controller_scripting.cpp new file mode 100644 index 00000000..7358aea8 --- /dev/null +++ b/tests/editor/test_play_session_controller_scripting.cpp @@ -0,0 +1,641 @@ +#include + +#include "Core/EditorContext.h" +#include "Core/EditorRuntimeMode.h" +#include "Core/PlaySessionController.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace XCEngine::Components; +using namespace XCEngine::Scripting; + +namespace XCEngine::Editor { +namespace { + +MonoScriptRuntime::Settings CreateMonoSettings() { + MonoScriptRuntime::Settings settings; + settings.assemblyDirectory = XCENGINE_TEST_MANAGED_OUTPUT_DIR; + settings.corlibDirectory = XCENGINE_TEST_MANAGED_OUTPUT_DIR; + settings.coreAssemblyPath = XCENGINE_TEST_SCRIPT_CORE_DLL; + settings.appAssemblyPath = XCENGINE_TEST_GAME_SCRIPTS_DLL; + return settings; +} + +void ExpectVector3Near(const Math::Vector3& actual, const Math::Vector3& expected, float tolerance = 0.001f) { + EXPECT_NEAR(actual.x, expected.x, tolerance); + EXPECT_NEAR(actual.y, expected.y, tolerance); + EXPECT_NEAR(actual.z, expected.z, tolerance); +} + +class CapturingLogSink final : public Debug::ILogSink { +public: + void Log(const Debug::LogEntry& entry) override { + entries.push_back(entry); + } + + void Flush() override { + } + + std::vector CollectMessagesWithPrefix(const char* prefix) const { + std::vector messages; + for (const Debug::LogEntry& entry : entries) { + const std::string message = entry.message.CStr(); + if (message.rfind(prefix, 0) == 0) { + messages.push_back(message); + } + } + + return messages; + } + + std::vector entries; +}; + +ScriptComponent* FindLifecycleProbe(GameObject* gameObject) { + if (!gameObject) { + return nullptr; + } + + for (ScriptComponent* component : gameObject->GetComponents()) { + if (!component) { + continue; + } + + if (component->GetAssemblyName() == "GameScripts" + && component->GetNamespaceName() == "Gameplay" + && component->GetClassName() == "LifecycleProbe") { + return component; + } + } + + return nullptr; +} + +ScriptComponent* FindInputProbe(GameObject* gameObject) { + if (!gameObject) { + return nullptr; + } + + for (ScriptComponent* component : gameObject->GetComponents()) { + if (!component) { + continue; + } + + if (component->GetAssemblyName() == "GameScripts" + && component->GetNamespaceName() == "Gameplay" + && component->GetClassName() == "InputProbe") { + return component; + } + } + + return nullptr; +} + +ScriptComponent* FindTickLogProbe(GameObject* gameObject) { + if (!gameObject) { + return nullptr; + } + + for (ScriptComponent* component : gameObject->GetComponents()) { + if (!component) { + continue; + } + + if (component->GetAssemblyName() == "GameScripts" + && component->GetNamespaceName() == "Gameplay" + && component->GetClassName() == "TickLogProbe") { + return component; + } + } + + return nullptr; +} + +GameViewInputFrameEvent CreateGameViewInputFrame( + bool focused, + bool hovered, + std::initializer_list keys = {}, + std::initializer_list mouseButtons = {}, + XCEngine::Math::Vector2 mousePosition = XCEngine::Math::Vector2::Zero(), + XCEngine::Math::Vector2 mouseDelta = XCEngine::Math::Vector2::Zero(), + float mouseWheel = 0.0f) { + GameViewInputFrameEvent event = {}; + event.focused = focused; + event.hovered = hovered; + event.mousePosition = mousePosition; + event.mouseDelta = mouseDelta; + event.mouseWheel = mouseWheel; + + for (const XCEngine::Input::KeyCode key : keys) { + const size_t index = static_cast(key); + if (index < event.keyDown.size()) { + event.keyDown[index] = true; + } + } + + for (const XCEngine::Input::MouseButton button : mouseButtons) { + const size_t index = static_cast(button); + if (index < event.mouseButtonDown.size()) { + event.mouseButtonDown[index] = true; + } + } + + return event; +} + +class PlaySessionControllerScriptingTest : public ::testing::Test { +protected: + void SetUp() override { + engine = &ScriptEngine::Get(); + engine->OnRuntimeStop(); + + runtime = std::make_unique(CreateMonoSettings()); + ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError(); + engine->SetRuntime(runtime.get()); + + context.GetSceneManager().NewScene("Play Session Script Scene"); + controller.Attach(context); + } + + void TearDown() override { + controller.Detach(context); + engine->OnRuntimeStop(); + engine->SetRuntime(nullptr); + runtime.reset(); + } + + ScriptComponent* AddLifecycleProbe(GameObject* gameObject) { + ScriptComponent* component = gameObject->AddComponent(); + component->SetScriptClass("GameScripts", "Gameplay", "LifecycleProbe"); + return component; + } + + ScriptComponent* AddInputProbe(GameObject* gameObject) { + ScriptComponent* component = gameObject->AddComponent(); + component->SetScriptClass("GameScripts", "Gameplay", "InputProbe"); + return component; + } + + ScriptComponent* AddTickLogProbe(GameObject* gameObject) { + ScriptComponent* component = gameObject->AddComponent(); + component->SetScriptClass("GameScripts", "Gameplay", "TickLogProbe"); + return component; + } + + ScriptEngine* engine = nullptr; + std::unique_ptr runtime; + EditorContext context; + PlaySessionController controller; +}; + +TEST_F(PlaySessionControllerScriptingTest, StartPlayAndRuntimeTickDriveManagedLifecycleThroughPlayController) { + GameObject* host = context.GetSceneManager().CreateEntity("Host"); + ASSERT_NE(host, nullptr); + ScriptComponent* script = AddLifecycleProbe(host); + ASSERT_NE(script, nullptr); + + script->GetFieldStorage().SetFieldValue("Label", "EditorLabel"); + script->GetFieldStorage().SetFieldValue("Speed", 5.0f); + script->GetFieldStorage().SetFieldValue("SpawnPoint", Math::Vector3(2.0f, 4.0f, 6.0f)); + + const uint64_t hostId = host->GetID(); + + ASSERT_TRUE(controller.StartPlay(context)); + EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Play); + + GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId); + ASSERT_NE(runtimeHost, nullptr); + ScriptComponent* runtimeScript = FindLifecycleProbe(runtimeHost); + ASSERT_NE(runtimeScript, nullptr); + EXPECT_TRUE(engine->HasRuntimeInstance(runtimeScript)); + EXPECT_TRUE(runtime->HasManagedInstance(runtimeScript)); + + int32_t awakeCount = 0; + int32_t enableCount = 0; + int32_t startCount = 0; + int32_t fixedUpdateCount = 0; + int32_t updateCount = 0; + int32_t lateUpdateCount = 0; + std::string label; + + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "AwakeCount", awakeCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "EnableCount", enableCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "StartCount", startCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "Label", label)); + + EXPECT_EQ(awakeCount, 1); + EXPECT_EQ(enableCount, 1); + EXPECT_EQ(startCount, 0); + EXPECT_EQ(fixedUpdateCount, 0); + EXPECT_EQ(updateCount, 0); + EXPECT_EQ(lateUpdateCount, 0); + EXPECT_EQ(label, "EditorLabel|Awake"); + + controller.Update(context, 0.036f); + + float observedFixedDeltaTime = 0.0f; + float observedConfiguredFixedDeltaTime = 0.0f; + float observedConfiguredFixedDeltaTimeInUpdate = 0.0f; + float observedUpdateDeltaTime = 0.0f; + float observedLateDeltaTime = 0.0f; + float speed = 0.0f; + Math::Vector3 spawnPoint; + + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "StartCount", startCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFixedDeltaTime", observedFixedDeltaTime)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedUpdateDeltaTime", observedUpdateDeltaTime)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLateDeltaTime", observedLateDeltaTime)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "Speed", speed)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "SpawnPoint", spawnPoint)); + + EXPECT_EQ(startCount, 1); + EXPECT_EQ(fixedUpdateCount, 1); + EXPECT_EQ(updateCount, 1); + EXPECT_EQ(lateUpdateCount, 1); + EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.02f); + EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.02f); + EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.02f); + EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.036f); + EXPECT_FLOAT_EQ(observedLateDeltaTime, 0.036f); + EXPECT_FLOAT_EQ(speed, 6.0f); + EXPECT_EQ(runtimeHost->GetName(), "Host_Managed"); + ExpectVector3Near(runtimeHost->GetTransform()->GetLocalPosition(), Math::Vector3(8.0f, 8.0f, 9.0f)); + ExpectVector3Near(spawnPoint, Math::Vector3(3.0f, 4.0f, 6.0f)); +} + +TEST_F(PlaySessionControllerScriptingTest, PauseAndStepGateManagedUpdatesInPlayMode) { + GameObject* host = context.GetSceneManager().CreateEntity("Host"); + ASSERT_NE(host, nullptr); + ScriptComponent* script = AddLifecycleProbe(host); + ASSERT_NE(script, nullptr); + + script->GetFieldStorage().SetFieldValue("Label", "EditorLabel"); + + const uint64_t hostId = host->GetID(); + + ASSERT_TRUE(controller.StartPlay(context)); + GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId); + ASSERT_NE(runtimeHost, nullptr); + ScriptComponent* runtimeScript = FindLifecycleProbe(runtimeHost); + ASSERT_NE(runtimeScript, nullptr); + + controller.Update(context, 0.02f); + + int32_t startCount = 0; + int32_t fixedUpdateCount = 0; + int32_t updateCount = 0; + int32_t lateUpdateCount = 0; + + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "StartCount", startCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount)); + + EXPECT_EQ(startCount, 1); + EXPECT_EQ(fixedUpdateCount, 1); + EXPECT_EQ(updateCount, 1); + EXPECT_EQ(lateUpdateCount, 1); + + ASSERT_TRUE(controller.PausePlay(context)); + EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Paused); + + controller.Update(context, 0.02f); + + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount)); + + EXPECT_EQ(fixedUpdateCount, 1); + EXPECT_EQ(updateCount, 1); + EXPECT_EQ(lateUpdateCount, 1); + + ASSERT_TRUE(controller.StepPlay(context)); + controller.Update(context, 0.02f); + + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount)); + + EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Paused); + EXPECT_EQ(fixedUpdateCount, 2); + EXPECT_EQ(updateCount, 2); + EXPECT_EQ(lateUpdateCount, 2); + + ASSERT_TRUE(controller.ResumePlay(context)); + EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Play); + + controller.Update(context, 0.02f); + + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount)); + + EXPECT_EQ(fixedUpdateCount, 3); + EXPECT_EQ(updateCount, 3); + EXPECT_EQ(lateUpdateCount, 3); +} + +TEST_F(PlaySessionControllerScriptingTest, StopPlayDestroysManagedInstancesAndRestoresEditorSnapshot) { + GameObject* host = context.GetSceneManager().CreateEntity("Host"); + ASSERT_NE(host, nullptr); + host->GetTransform()->SetLocalPosition(Math::Vector3(1.0f, 2.0f, 3.0f)); + + ScriptComponent* script = AddLifecycleProbe(host); + ASSERT_NE(script, nullptr); + script->GetFieldStorage().SetFieldValue("Label", "EditorLabel"); + script->GetFieldStorage().SetFieldValue("Speed", 5.0f); + script->GetFieldStorage().SetFieldValue("SpawnPoint", Math::Vector3(2.0f, 4.0f, 6.0f)); + + const uint64_t hostId = host->GetID(); + const uint64_t scriptComponentUUID = script->GetScriptComponentUUID(); + + ASSERT_TRUE(controller.StartPlay(context)); + GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId); + ASSERT_NE(runtimeHost, nullptr); + ScriptComponent* runtimeScript = FindLifecycleProbe(runtimeHost); + ASSERT_NE(runtimeScript, nullptr); + + controller.Update(context, 0.02f); + + EXPECT_EQ(runtime->GetManagedInstanceCount(), 1u); + EXPECT_EQ(runtimeHost->GetName(), "Host_Managed"); + ExpectVector3Near(runtimeHost->GetTransform()->GetLocalPosition(), Math::Vector3(8.0f, 8.0f, 9.0f)); + + ASSERT_TRUE(controller.StopPlay(context)); + EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Edit); + EXPECT_FALSE(engine->IsRuntimeRunning()); + EXPECT_EQ(runtime->GetManagedInstanceCount(), 0u); + + GameObject* restoredHost = context.GetSceneManager().GetEntity(hostId); + ASSERT_NE(restoredHost, nullptr); + EXPECT_EQ(restoredHost->GetName(), "Host"); + ExpectVector3Near(restoredHost->GetTransform()->GetLocalPosition(), Math::Vector3(1.0f, 2.0f, 3.0f)); + + ScriptComponent* restoredScript = FindLifecycleProbe(restoredHost); + ASSERT_NE(restoredScript, nullptr); + EXPECT_EQ(restoredScript->GetScriptComponentUUID(), scriptComponentUUID); + + std::string label; + float speed = 0.0f; + Math::Vector3 spawnPoint; + int32_t awakeCount = 0; + + ASSERT_TRUE(engine->TryGetScriptFieldValue(restoredScript, "Label", label)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(restoredScript, "Speed", speed)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(restoredScript, "SpawnPoint", spawnPoint)); + + EXPECT_EQ(label, "EditorLabel"); + EXPECT_FLOAT_EQ(speed, 5.0f); + ExpectVector3Near(spawnPoint, Math::Vector3(2.0f, 4.0f, 6.0f)); + EXPECT_FALSE(engine->TryGetScriptFieldValue(restoredScript, "AwakeCount", awakeCount)); + EXPECT_FALSE(restoredScript->GetFieldStorage().Contains("AwakeCount")); +} + +TEST_F(PlaySessionControllerScriptingTest, GameViewInputBridgeFeedsManagedInputApiDuringPlayMode) { + GameObject* host = context.GetSceneManager().CreateEntity("Host"); + ASSERT_NE(host, nullptr); + ScriptComponent* script = AddInputProbe(host); + ASSERT_NE(script, nullptr); + + const uint64_t hostId = host->GetID(); + + controller.Attach(context); + ASSERT_TRUE(controller.StartPlay(context)); + GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId); + ASSERT_NE(runtimeHost, nullptr); + ScriptComponent* runtimeScript = FindInputProbe(runtimeHost); + ASSERT_NE(runtimeScript, nullptr); + + context.GetEventBus().Publish(CreateGameViewInputFrame( + true, + true, + {XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space, XCEngine::Input::KeyCode::LeftCtrl}, + {XCEngine::Input::MouseButton::Left}, + XCEngine::Math::Vector2(120.0f, 48.0f), + XCEngine::Math::Vector2(3.0f, -2.0f), + 1.0f)); + + controller.Update(context, 0.016f); + + int32_t updateCount = 0; + bool observedKeyA = false; + bool observedKeyADown = false; + bool observedKeyAUp = false; + bool observedKeySpace = false; + bool observedJump = false; + bool observedJumpDown = false; + bool observedJumpUp = false; + bool observedFire1 = false; + bool observedFire1Down = false; + bool observedFire1Up = false; + bool observedAnyKey = false; + bool observedAnyKeyDown = false; + bool observedLeftMouse = false; + bool observedLeftMouseDown = false; + bool observedLeftMouseUp = false; + float observedHorizontal = 0.0f; + float observedHorizontalRaw = 0.0f; + XCEngine::Math::Vector2 observedMouseScrollDelta; + XCEngine::Math::Vector3 observedMousePosition; + + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyA", observedKeyA)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyADown", observedKeyADown)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyAUp", observedKeyAUp)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeySpace", observedKeySpace)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpDown", observedJumpDown)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1", observedFire1)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Down", observedFire1Down)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Up", observedFire1Up)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouse", observedLeftMouse)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseDown", observedLeftMouseDown)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseUp", observedLeftMouseUp)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontal", observedHorizontal)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontalRaw", observedHorizontalRaw)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedMousePosition", observedMousePosition)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedMouseScrollDelta", observedMouseScrollDelta)); + + EXPECT_EQ(updateCount, 1); + EXPECT_TRUE(observedKeyA); + EXPECT_TRUE(observedKeyADown); + EXPECT_FALSE(observedKeyAUp); + EXPECT_TRUE(observedKeySpace); + EXPECT_TRUE(observedJump); + EXPECT_TRUE(observedJumpDown); + EXPECT_FALSE(observedJumpUp); + EXPECT_TRUE(observedFire1); + EXPECT_TRUE(observedFire1Down); + EXPECT_FALSE(observedFire1Up); + EXPECT_TRUE(observedAnyKey); + EXPECT_TRUE(observedAnyKeyDown); + EXPECT_TRUE(observedLeftMouse); + EXPECT_TRUE(observedLeftMouseDown); + EXPECT_FALSE(observedLeftMouseUp); + EXPECT_FLOAT_EQ(observedHorizontal, -1.0f); + EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f); + EXPECT_EQ(observedMousePosition, XCEngine::Math::Vector3(120.0f, 48.0f, 0.0f)); + EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 1.0f)); + + context.GetEventBus().Publish(CreateGameViewInputFrame( + true, + true, + {XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space, XCEngine::Input::KeyCode::LeftCtrl}, + {XCEngine::Input::MouseButton::Left}, + XCEngine::Math::Vector2(120.0f, 48.0f))); + + controller.Update(context, 0.016f); + + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyA", observedKeyA)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyADown", observedKeyADown)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyAUp", observedKeyAUp)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpDown", observedJumpDown)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1", observedFire1)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Down", observedFire1Down)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Up", observedFire1Up)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouse", observedLeftMouse)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseDown", observedLeftMouseDown)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseUp", observedLeftMouseUp)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontalRaw", observedHorizontalRaw)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedMouseScrollDelta", observedMouseScrollDelta)); + + EXPECT_EQ(updateCount, 2); + EXPECT_TRUE(observedKeyA); + EXPECT_FALSE(observedKeyADown); + EXPECT_FALSE(observedKeyAUp); + EXPECT_TRUE(observedJump); + EXPECT_FALSE(observedJumpDown); + EXPECT_FALSE(observedJumpUp); + EXPECT_TRUE(observedFire1); + EXPECT_FALSE(observedFire1Down); + EXPECT_FALSE(observedFire1Up); + EXPECT_TRUE(observedAnyKey); + EXPECT_FALSE(observedAnyKeyDown); + EXPECT_TRUE(observedLeftMouse); + EXPECT_FALSE(observedLeftMouseDown); + EXPECT_FALSE(observedLeftMouseUp); + EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f); + EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 0.0f)); + + context.GetEventBus().Publish(CreateGameViewInputFrame( + true, + true, + {XCEngine::Input::KeyCode::Space}, + {}, + XCEngine::Math::Vector2(120.0f, 48.0f))); + controller.Update(context, 0.016f); + + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyA", observedKeyA)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyAUp", observedKeyAUp)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeySpace", observedKeySpace)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1", observedFire1)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Up", observedFire1Up)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouse", observedLeftMouse)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseUp", observedLeftMouseUp)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontal", observedHorizontal)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontalRaw", observedHorizontalRaw)); + + EXPECT_EQ(updateCount, 3); + EXPECT_FALSE(observedKeyA); + EXPECT_TRUE(observedKeyAUp); + EXPECT_TRUE(observedKeySpace); + EXPECT_TRUE(observedJump); + EXPECT_FALSE(observedJumpUp); + EXPECT_FALSE(observedFire1); + EXPECT_TRUE(observedFire1Up); + EXPECT_TRUE(observedAnyKey); + EXPECT_FALSE(observedAnyKeyDown); + EXPECT_FALSE(observedLeftMouse); + EXPECT_TRUE(observedLeftMouseUp); + EXPECT_FLOAT_EQ(observedHorizontal, 0.0f); + EXPECT_FLOAT_EQ(observedHorizontalRaw, 0.0f); + + context.GetEventBus().Publish(CreateGameViewInputFrame( + true, + true, + {}, + {}, + XCEngine::Math::Vector2(120.0f, 48.0f))); + controller.Update(context, 0.016f); + + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeySpace", observedKeySpace)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey)); + ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown)); + + EXPECT_EQ(updateCount, 4); + EXPECT_FALSE(observedKeySpace); + EXPECT_FALSE(observedJump); + EXPECT_TRUE(observedJumpUp); + EXPECT_FALSE(observedAnyKey); + EXPECT_FALSE(observedAnyKeyDown); +} + +TEST_F(PlaySessionControllerScriptingTest, PlayModeTickWritesManagedDebugLogsToNativeLogger) { + auto sink = std::make_unique(); + CapturingLogSink* sinkPtr = sink.get(); + Debug::Logger::Get().AddSink(std::move(sink)); + + GameObject* host = context.GetSceneManager().CreateEntity("Host"); + ASSERT_NE(host, nullptr); + ScriptComponent* script = AddTickLogProbe(host); + ASSERT_NE(script, nullptr); + + const uint64_t hostId = host->GetID(); + + ASSERT_TRUE(controller.StartPlay(context)); + GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId); + ASSERT_NE(runtimeHost, nullptr); + ASSERT_NE(FindTickLogProbe(runtimeHost), nullptr); + + controller.Update(context, 0.036f); + + const std::vector messages = sinkPtr->CollectMessagesWithPrefix("[TickLogProbe]"); + const std::vector expected = { + "[TickLogProbe] Awake", + "[TickLogProbe] FixedUpdate 1", + "[TickLogProbe] Start", + "[TickLogProbe] Update 1", + "[TickLogProbe] LateUpdate 1", + }; + + EXPECT_EQ(messages, expected); + + Debug::Logger::Get().RemoveSink(sinkPtr); +} + +} // namespace +} // namespace XCEngine::Editor diff --git a/tests/editor/test_scene_viewport_camera_controller.cpp b/tests/editor/test_scene_viewport_camera_controller.cpp index f22d3abc..a5dfc792 100644 --- a/tests/editor/test_scene_viewport_camera_controller.cpp +++ b/tests/editor/test_scene_viewport_camera_controller.cpp @@ -159,16 +159,16 @@ TEST(SceneViewportCameraController_Test, FlyInputMovesCameraAndFocalPointTogethe TEST(SceneViewportCameraController_Test, ZoomDoesNotChangeFlySpeed) { SceneViewportCameraController zoomedController; zoomedController.Reset(); - const Vector3 zoomedInitialPosition = zoomedController.GetPosition(); SceneViewportCameraController baselineController; baselineController.Reset(); - const Vector3 baselineInitialPosition = baselineController.GetPosition(); SceneViewportCameraInputState zoomInput = {}; zoomInput.viewportHeight = 720.0f; zoomInput.zoomDelta = 8.0f; zoomedController.ApplyInput(zoomInput); + const Vector3 zoomedPositionBeforeMove = zoomedController.GetPosition(); + const Vector3 baselinePositionBeforeMove = baselineController.GetPosition(); SceneViewportCameraInputState moveInput = {}; moveInput.viewportHeight = 720.0f; @@ -178,8 +178,8 @@ TEST(SceneViewportCameraController_Test, ZoomDoesNotChangeFlySpeed) { baselineController.ApplyInput(moveInput); EXPECT_FLOAT_EQ(zoomedController.GetFlySpeed(), baselineController.GetFlySpeed()); - const float zoomedTravel = (zoomedController.GetPosition() - zoomedInitialPosition).Magnitude(); - const float baselineTravel = (baselineController.GetPosition() - baselineInitialPosition).Magnitude(); + const float zoomedTravel = (zoomedController.GetPosition() - zoomedPositionBeforeMove).Magnitude(); + const float baselineTravel = (baselineController.GetPosition() - baselinePositionBeforeMove).Magnitude(); EXPECT_NEAR(zoomedTravel, baselineTravel, 1e-3f); } diff --git a/tests/editor/test_script_component_editor_utils.cpp b/tests/editor/test_script_component_editor_utils.cpp new file mode 100644 index 00000000..b98f4917 --- /dev/null +++ b/tests/editor/test_script_component_editor_utils.cpp @@ -0,0 +1,135 @@ +#include + +#include "ComponentEditors/ScriptComponentEditorUtils.h" + +#include + +namespace XCEngine::Editor { +namespace { + +TEST(ScriptComponentEditorUtils_Test, BuildsReadableScriptClassDisplayNames) { + const ::XCEngine::Scripting::ScriptClassDescriptor gameScript{ + "GameScripts", + "Gameplay", + "PlayerController" + }; + const ::XCEngine::Scripting::ScriptClassDescriptor toolScript{ + "EditorTools", + "Gameplay", + "PlayerController" + }; + + EXPECT_EQ( + ScriptComponentEditorUtils::BuildScriptClassDisplayName(gameScript), + "Gameplay.PlayerController"); + EXPECT_EQ( + ScriptComponentEditorUtils::BuildScriptClassDisplayName(toolScript), + "Gameplay.PlayerController (EditorTools)"); + + ::XCEngine::Scripting::ScriptComponent component; + component.SetScriptClass("GameScripts", "ProjectScripts", "Mover"); + EXPECT_EQ( + ScriptComponentEditorUtils::BuildScriptClassDisplayName(component), + "ProjectScripts.Mover"); +} + +TEST(ScriptComponentEditorUtils_Test, FindsGameObjectIdsByUuidAcrossHierarchy) { + ::XCEngine::Components::GameObject root("Root"); + ::XCEngine::Components::GameObject child("Child"); + ::XCEngine::Components::GameObject grandChild("GrandChild"); + + child.SetParent(&root); + grandChild.SetParent(&child); + + const std::vector<::XCEngine::Components::GameObject*> roots = { &root }; + EXPECT_EQ( + ScriptComponentEditorUtils::FindGameObjectIdByUuid(roots, child.GetUUID()), + child.GetID()); + EXPECT_EQ( + ScriptComponentEditorUtils::FindGameObjectIdByUuid(roots, grandChild.GetUUID()), + grandChild.GetID()); + EXPECT_EQ( + ScriptComponentEditorUtils::FindGameObjectIdByUuid(roots, 0), + ::XCEngine::Components::GameObject::INVALID_ID); +} + +TEST(ScriptComponentEditorUtils_Test, ReportsFieldEditabilityClearabilityAndIssues) { + ::XCEngine::Scripting::ScriptFieldSnapshot declaredField; + declaredField.metadata = { "Health", ::XCEngine::Scripting::ScriptFieldType::Int32 }; + declaredField.declaredInClass = true; + + ::XCEngine::Scripting::ScriptFieldSnapshot storedOnlyField; + storedOnlyField.metadata = { "LegacyValue", ::XCEngine::Scripting::ScriptFieldType::String }; + storedOnlyField.hasValue = true; + storedOnlyField.value = std::string("legacy"); + storedOnlyField.valueSource = ::XCEngine::Scripting::ScriptFieldValueSource::StoredValue; + storedOnlyField.issue = ::XCEngine::Scripting::ScriptFieldIssue::StoredOnly; + storedOnlyField.hasStoredValue = true; + storedOnlyField.storedType = ::XCEngine::Scripting::ScriptFieldType::String; + storedOnlyField.storedValue = std::string("legacy"); + + ::XCEngine::Scripting::ScriptFieldSnapshot mismatchedField; + mismatchedField.metadata = { "Speed", ::XCEngine::Scripting::ScriptFieldType::Float }; + mismatchedField.declaredInClass = true; + mismatchedField.issue = ::XCEngine::Scripting::ScriptFieldIssue::TypeMismatch; + mismatchedField.hasStoredValue = true; + mismatchedField.storedType = ::XCEngine::Scripting::ScriptFieldType::UInt64; + mismatchedField.storedValue = uint64_t(5); + + EXPECT_TRUE(ScriptComponentEditorUtils::CanEditScriptField( + ::XCEngine::Scripting::ScriptFieldClassStatus::Available, + declaredField)); + EXPECT_FALSE(ScriptComponentEditorUtils::CanEditScriptField( + ::XCEngine::Scripting::ScriptFieldClassStatus::Available, + storedOnlyField)); + EXPECT_TRUE(ScriptComponentEditorUtils::CanEditScriptField( + ::XCEngine::Scripting::ScriptFieldClassStatus::Missing, + storedOnlyField)); + + EXPECT_FALSE(ScriptComponentEditorUtils::CanClearScriptFieldOverride(declaredField)); + EXPECT_TRUE(ScriptComponentEditorUtils::CanClearScriptFieldOverride(storedOnlyField)); + EXPECT_TRUE(ScriptComponentEditorUtils::CanClearScriptFieldOverride(mismatchedField)); + + EXPECT_EQ( + ScriptComponentEditorUtils::BuildScriptFieldIssueText(storedOnlyField), + "Stored override is not declared by the selected script."); + EXPECT_EQ( + ScriptComponentEditorUtils::BuildScriptFieldIssueText(mismatchedField), + "Stored override type is UInt64, but the script field expects Float."); +} + +TEST(ScriptComponentEditorUtils_Test, BuildsRuntimeUnavailableHintsAndReloadAvailability) { + EditorScriptRuntimeStatus missingAssembliesStatus; + missingAssembliesStatus.backendEnabled = true; + missingAssembliesStatus.assemblyDirectory = "D:/Project/Library/ScriptAssemblies"; + missingAssembliesStatus.statusMessage = + "Script assemblies were not found in D:/Project/Library/ScriptAssemblies. " + "Script class discovery is disabled until the managed assemblies are built."; + + EXPECT_EQ( + ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(missingAssembliesStatus), + missingAssembliesStatus.statusMessage); + EXPECT_TRUE(ScriptComponentEditorUtils::CanReloadScriptRuntime(missingAssembliesStatus)); + EXPECT_TRUE(ScriptComponentEditorUtils::CanRebuildScriptAssemblies(missingAssembliesStatus)); + + EditorScriptRuntimeStatus fallbackStatus; + fallbackStatus.backendEnabled = true; + fallbackStatus.assemblyDirectory = "D:/Project/Library/ScriptAssemblies"; + + EXPECT_EQ( + ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(fallbackStatus), + "No script assemblies are currently loaded from D:/Project/Library/ScriptAssemblies."); + EXPECT_TRUE(ScriptComponentEditorUtils::CanReloadScriptRuntime(fallbackStatus)); + EXPECT_TRUE(ScriptComponentEditorUtils::CanRebuildScriptAssemblies(fallbackStatus)); + + EditorScriptRuntimeStatus backendDisabledStatus; + + EXPECT_EQ( + ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(backendDisabledStatus), + "This editor build does not include Mono scripting support."); + EXPECT_FALSE(ScriptComponentEditorUtils::CanReloadScriptRuntime(backendDisabledStatus)); + EXPECT_FALSE(ScriptComponentEditorUtils::CanRebuildScriptAssemblies(backendDisabledStatus)); +} + +} // namespace +} // namespace XCEngine::Editor diff --git a/tests/scripting/CMakeLists.txt b/tests/scripting/CMakeLists.txt index 75f8dcde..a519147a 100644 --- a/tests/scripting/CMakeLists.txt +++ b/tests/scripting/CMakeLists.txt @@ -12,6 +12,12 @@ if(XCENGINE_ENABLE_MONO_SCRIPTING) list(APPEND SCRIPTING_TEST_SOURCES test_mono_script_runtime.cpp ) + + if(TARGET xcengine_project_managed_assemblies) + list(APPEND SCRIPTING_TEST_SOURCES + test_project_script_assembly.cpp + ) + endif() endif() add_executable(scripting_tests ${SCRIPTING_TEST_SOURCES}) @@ -46,6 +52,20 @@ if(TARGET xcengine_managed_assemblies) ) endif() +if(TARGET xcengine_project_managed_assemblies) + add_dependencies(scripting_tests xcengine_project_managed_assemblies) + + file(TO_CMAKE_PATH "${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}" XCENGINE_PROJECT_MANAGED_OUTPUT_DIR_CMAKE) + file(TO_CMAKE_PATH "${XCENGINE_PROJECT_SCRIPT_CORE_DLL}" XCENGINE_PROJECT_SCRIPT_CORE_DLL_CMAKE) + file(TO_CMAKE_PATH "${XCENGINE_PROJECT_GAME_SCRIPTS_DLL}" XCENGINE_PROJECT_GAME_SCRIPTS_DLL_CMAKE) + + target_compile_definitions(scripting_tests PRIVATE + XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR=\"${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR_CMAKE}\" + XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL=\"${XCENGINE_PROJECT_SCRIPT_CORE_DLL_CMAKE}\" + XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL=\"${XCENGINE_PROJECT_GAME_SCRIPTS_DLL_CMAKE}\" + ) +endif() + if(WIN32 AND EXISTS "${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll") add_custom_command(TARGET scripting_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 3fc21797..43db1513 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -4,8 +4,13 @@ #include #include #include +#include +#include +#include #include #include +#include +#include #include #include #include @@ -28,6 +33,30 @@ void ExpectVector3Near(const XCEngine::Math::Vector3& actual, const XCEngine::Ma EXPECT_NEAR(actual.z, expected.z, tolerance); } +class CapturingLogSink final : public XCEngine::Debug::ILogSink { +public: + void Log(const XCEngine::Debug::LogEntry& entry) override { + entries.push_back(entry); + } + + void Flush() override { + } + + std::vector CollectMessagesWithPrefix(const char* prefix) const { + std::vector messages; + for (const XCEngine::Debug::LogEntry& entry : entries) { + const std::string message = entry.message.CStr(); + if (message.rfind(prefix, 0) == 0) { + messages.push_back(message); + } + } + + return messages; + } + + std::vector entries; +}; + ScriptComponent* FindScriptComponentByClass(GameObject* gameObject, const std::string& namespaceName, const std::string& className) { if (!gameObject) { return nullptr; @@ -60,6 +89,8 @@ protected: void SetUp() override { engine = &ScriptEngine::Get(); engine->OnRuntimeStop(); + engine->SetRuntimeFixedDeltaTime(ScriptEngine::DefaultFixedDeltaTime); + XCEngine::Input::InputManager::Get().Shutdown(); runtime = std::make_unique(CreateMonoSettings()); ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError(); @@ -70,6 +101,7 @@ protected: void TearDown() override { engine->OnRuntimeStop(); engine->SetRuntime(nullptr); + XCEngine::Input::InputManager::Get().Shutdown(); runtime.reset(); scene.reset(); } @@ -100,6 +132,44 @@ TEST_F(MonoScriptRuntimeTest, InitializesAndDiscoversConcreteMonoBehaviourClasse EXPECT_EQ(std::find(classNames.begin(), classNames.end(), "Gameplay.UtilityHelper"), classNames.end()); } +TEST_F(MonoScriptRuntimeTest, ScriptClassDescriptorApiReturnsConcreteManagedTypes) { + std::vector classes; + ASSERT_TRUE(runtime->TryGetAvailableScriptClasses(classes)); + ASSERT_FALSE(classes.empty()); + + EXPECT_TRUE(std::is_sorted( + classes.begin(), + classes.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; + })); + + EXPECT_NE( + std::find( + classes.begin(), + classes.end(), + ScriptClassDescriptor{"GameScripts", "Gameplay", "LifecycleProbe"}), + classes.end()); + EXPECT_EQ( + std::find( + classes.begin(), + classes.end(), + ScriptClassDescriptor{"GameScripts", "Gameplay", "AbstractLifecycleProbe"}), + classes.end()); + EXPECT_EQ( + std::find( + classes.begin(), + classes.end(), + ScriptClassDescriptor{"GameScripts", "Gameplay", "UtilityHelper"}), + classes.end()); +} + TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataListsSupportedPublicInstanceFields) { std::vector fields; @@ -184,6 +254,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle bool observedIsActiveAndEnabled = false; float speed = 0.0f; float observedFixedDeltaTime = 0.0f; + float observedConfiguredFixedDeltaTime = 0.0f; + float observedConfiguredFixedDeltaTimeInUpdate = 0.0f; float observedUpdateDeltaTime = 0.0f; float observedLateDeltaTime = 0.0f; std::string label; @@ -219,6 +291,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIsActiveAndEnabled", observedIsActiveAndEnabled)); EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", speed)); EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFixedDeltaTime", observedFixedDeltaTime)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate)); EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdateDeltaTime", observedUpdateDeltaTime)); EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLateDeltaTime", observedLateDeltaTime)); EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", label)); @@ -254,6 +328,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle EXPECT_TRUE(observedIsActiveAndEnabled); EXPECT_FLOAT_EQ(speed, 6.0f); EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.02f); + EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.02f); + EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.02f); EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f); EXPECT_FLOAT_EQ(observedLateDeltaTime, 0.016f); EXPECT_EQ(label, "Configured|Awake"); @@ -276,6 +352,233 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle EXPECT_FLOAT_EQ(localRotation.w, 0.8660254f); } +TEST_F(MonoScriptRuntimeTest, TimeFixedDeltaTimeUsesConfiguredRuntimeStepAcrossFixedAndVariableUpdates) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe"); + + engine->SetRuntimeFixedDeltaTime(0.05f); + engine->OnRuntimeStart(runtimeScene); + engine->OnFixedUpdate(0.05f); + engine->OnUpdate(0.016f); + + float observedFixedDeltaTime = 0.0f; + float observedConfiguredFixedDeltaTime = 0.0f; + float observedConfiguredFixedDeltaTimeInUpdate = 0.0f; + float observedUpdateDeltaTime = 0.0f; + + ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedFixedDeltaTime", observedFixedDeltaTime)); + ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime)); + ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate)); + ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdateDeltaTime", observedUpdateDeltaTime)); + + EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.05f); + EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.05f); + EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.05f); + EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f); +} + +TEST_F(MonoScriptRuntimeTest, ManagedInputApiReadsCurrentNativeInputManagerState) { + XCEngine::Input::InputManager& inputManager = XCEngine::Input::InputManager::Get(); + inputManager.Initialize(nullptr); + + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScript(host, "Gameplay", "InputProbe"); + + engine->OnRuntimeStart(runtimeScene); + + inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::A, false, false, false, false, false); + inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::Space, false, false, false, false, false); + inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::LeftCtrl, false, false, false, false, false); + inputManager.ProcessMouseButton(XCEngine::Input::MouseButton::Left, true, 120, 48); + inputManager.ProcessMouseMove(120, 48, 3, -2); + inputManager.ProcessMouseWheel(1.0f, 120, 48); + engine->OnUpdate(0.016f); + + int32_t updateCount = 0; + bool observedKeyA = false; + bool observedKeyADown = false; + bool observedKeyAUp = false; + bool observedKeySpace = false; + bool observedJump = false; + bool observedJumpDown = false; + bool observedJumpUp = false; + bool observedFire1 = false; + bool observedFire1Down = false; + bool observedFire1Up = false; + bool observedAnyKey = false; + bool observedAnyKeyDown = false; + bool observedLeftMouse = false; + bool observedLeftMouseDown = false; + bool observedLeftMouseUp = false; + float observedHorizontal = 0.0f; + float observedHorizontalRaw = 0.0f; + XCEngine::Math::Vector2 observedMouseScrollDelta; + XCEngine::Math::Vector3 observedMousePosition; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyADown", observedKeyADown)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeySpace", observedKeySpace)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpDown", observedJumpDown)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Down", observedFire1Down)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseDown", observedLeftMouseDown)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontal", observedHorizontal)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMousePosition", observedMousePosition)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMouseScrollDelta", observedMouseScrollDelta)); + + EXPECT_EQ(updateCount, 1); + EXPECT_TRUE(observedKeyA); + EXPECT_TRUE(observedKeyADown); + EXPECT_FALSE(observedKeyAUp); + EXPECT_TRUE(observedKeySpace); + EXPECT_TRUE(observedJump); + EXPECT_TRUE(observedJumpDown); + EXPECT_FALSE(observedJumpUp); + EXPECT_TRUE(observedFire1); + EXPECT_TRUE(observedFire1Down); + EXPECT_FALSE(observedFire1Up); + EXPECT_TRUE(observedAnyKey); + EXPECT_TRUE(observedAnyKeyDown); + EXPECT_TRUE(observedLeftMouse); + EXPECT_TRUE(observedLeftMouseDown); + EXPECT_FALSE(observedLeftMouseUp); + EXPECT_FLOAT_EQ(observedHorizontal, -1.0f); + EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f); + EXPECT_EQ(observedMousePosition, XCEngine::Math::Vector3(120.0f, 48.0f, 0.0f)); + EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 1.0f)); + + inputManager.Update(0.016f); + engine->OnUpdate(0.016f); + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyADown", observedKeyADown)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpDown", observedJumpDown)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Down", observedFire1Down)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseDown", observedLeftMouseDown)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMouseScrollDelta", observedMouseScrollDelta)); + + EXPECT_EQ(updateCount, 2); + EXPECT_TRUE(observedKeyA); + EXPECT_FALSE(observedKeyADown); + EXPECT_FALSE(observedKeyAUp); + EXPECT_TRUE(observedJump); + EXPECT_FALSE(observedJumpDown); + EXPECT_FALSE(observedJumpUp); + EXPECT_TRUE(observedFire1); + EXPECT_FALSE(observedFire1Down); + EXPECT_FALSE(observedFire1Up); + EXPECT_TRUE(observedAnyKey); + EXPECT_FALSE(observedAnyKeyDown); + EXPECT_TRUE(observedLeftMouse); + EXPECT_FALSE(observedLeftMouseDown); + EXPECT_FALSE(observedLeftMouseUp); + EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f); + EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 0.0f)); + + inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::A, false, false, false, false); + inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::LeftCtrl, false, false, false, false); + inputManager.ProcessMouseButton(XCEngine::Input::MouseButton::Left, false, 120, 48); + engine->OnUpdate(0.016f); + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontal", observedHorizontal)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw)); + + EXPECT_EQ(updateCount, 3); + EXPECT_FALSE(observedKeyA); + EXPECT_TRUE(observedKeyAUp); + EXPECT_TRUE(observedJump); + EXPECT_FALSE(observedJumpUp); + EXPECT_FALSE(observedFire1); + EXPECT_TRUE(observedFire1Up); + EXPECT_TRUE(observedAnyKey); + EXPECT_FALSE(observedAnyKeyDown); + EXPECT_FALSE(observedLeftMouse); + EXPECT_TRUE(observedLeftMouseUp); + EXPECT_FLOAT_EQ(observedHorizontal, 0.0f); + EXPECT_FLOAT_EQ(observedHorizontalRaw, 0.0f); + + inputManager.Update(0.016f); + inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::Space, false, false, false, false); + engine->OnUpdate(0.016f); + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeySpace", observedKeySpace)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown)); + + EXPECT_EQ(updateCount, 4); + EXPECT_FALSE(observedKeySpace); + EXPECT_FALSE(observedJump); + EXPECT_TRUE(observedJumpUp); + EXPECT_FALSE(observedAnyKey); + EXPECT_FALSE(observedAnyKeyDown); +} + +TEST_F(MonoScriptRuntimeTest, ManagedDebugLogBridgeWritesLifecycleTickMessagesToNativeLogger) { + auto sink = std::make_unique(); + CapturingLogSink* sinkPtr = sink.get(); + XCEngine::Debug::Logger::Get().AddSink(std::move(sink)); + + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScript(host, "Gameplay", "TickLogProbe"); + ASSERT_NE(component, nullptr); + + engine->OnRuntimeStart(runtimeScene); + engine->OnFixedUpdate(0.02f); + engine->OnUpdate(0.016f); + engine->OnLateUpdate(0.016f); + + const std::vector messages = sinkPtr->CollectMessagesWithPrefix("[TickLogProbe]"); + const std::vector expected = { + "[TickLogProbe] Awake", + "[TickLogProbe] FixedUpdate 1", + "[TickLogProbe] Start", + "[TickLogProbe] Update 1", + "[TickLogProbe] LateUpdate 1", + }; + + EXPECT_EQ(messages, expected); + + XCEngine::Debug::Logger::Get().RemoveSink(sinkPtr); +} + TEST_F(MonoScriptRuntimeTest, DeserializedSceneRebindsManagedScriptsAndRestoresStoredFields) { Scene originalScene("SerializedMonoScene"); GameObject* hostA = originalScene.CreateGameObject("HostA"); diff --git a/tests/scripting/test_project_script_assembly.cpp b/tests/scripting/test_project_script_assembly.cpp new file mode 100644 index 00000000..3a1a6233 --- /dev/null +++ b/tests/scripting/test_project_script_assembly.cpp @@ -0,0 +1,87 @@ +#include + +#include + +#include +#include +#include +#include +#include + +using namespace XCEngine::Scripting; + +namespace { + +MonoScriptRuntime::Settings CreateProjectMonoSettings() { + MonoScriptRuntime::Settings settings; + settings.assemblyDirectory = XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR; + settings.corlibDirectory = XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR; + settings.coreAssemblyPath = XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL; + settings.appAssemblyPath = XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL; + return settings; +} + +class ProjectScriptAssemblyTest : public ::testing::Test { +protected: + void SetUp() override { + ASSERT_TRUE(std::filesystem::exists(XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL)); + ASSERT_TRUE(std::filesystem::exists(XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL)); + + runtime = std::make_unique(CreateProjectMonoSettings()); + ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError(); + } + + std::unique_ptr runtime; +}; + +TEST_F(ProjectScriptAssemblyTest, InitializesFromProjectScriptAssemblyDirectory) { + EXPECT_TRUE(runtime->IsInitialized()); + EXPECT_EQ(runtime->GetSettings().assemblyDirectory, std::filesystem::path(XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR)); + EXPECT_EQ(runtime->GetSettings().appAssemblyPath, std::filesystem::path(XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL)); +} + +TEST_F(ProjectScriptAssemblyTest, DiscoversProjectAssetMonoBehaviourClassesAndFieldMetadata) { + const std::vector classNames = runtime->GetScriptClassNames("GameScripts"); + std::vector classDescriptors; + ASSERT_TRUE(runtime->TryGetAvailableScriptClasses(classDescriptors)); + + EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "ProjectScripts", "ProjectScriptProbe")); + EXPECT_NE( + std::find(classNames.begin(), classNames.end(), "ProjectScripts.ProjectScriptProbe"), + classNames.end()); + EXPECT_NE( + std::find( + classDescriptors.begin(), + classDescriptors.end(), + ScriptClassDescriptor{"GameScripts", "ProjectScripts", "ProjectScriptProbe"}), + classDescriptors.end()); + + std::vector fields; + ASSERT_TRUE(runtime->TryGetClassFieldMetadata("GameScripts", "ProjectScripts", "ProjectScriptProbe", fields)); + + const std::vector expectedFields = { + {"EnabledOnBoot", ScriptFieldType::Bool}, + {"Label", ScriptFieldType::String}, + {"Speed", ScriptFieldType::Float}, + }; + + EXPECT_EQ(fields, expectedFields); + + std::vector defaultValues; + ASSERT_TRUE(runtime->TryGetClassFieldDefaultValues("GameScripts", "ProjectScripts", "ProjectScriptProbe", defaultValues)); + ASSERT_EQ(defaultValues.size(), 3u); + + EXPECT_EQ(defaultValues[0].fieldName, "EnabledOnBoot"); + EXPECT_EQ(defaultValues[0].type, ScriptFieldType::Bool); + EXPECT_EQ(std::get(defaultValues[0].value), true); + + EXPECT_EQ(defaultValues[1].fieldName, "Label"); + EXPECT_EQ(defaultValues[1].type, ScriptFieldType::String); + EXPECT_EQ(std::get(defaultValues[1].value), "ProjectScriptProbe"); + + EXPECT_EQ(defaultValues[2].fieldName, "Speed"); + EXPECT_EQ(defaultValues[2].type, ScriptFieldType::Float); + EXPECT_FLOAT_EQ(std::get(defaultValues[2].value), 2.5f); +} + +} // namespace diff --git a/tests/scripting/test_script_engine.cpp b/tests/scripting/test_script_engine.cpp index 012ceb27..d0582013 100644 --- a/tests/scripting/test_script_engine.cpp +++ b/tests/scripting/test_script_engine.cpp @@ -42,6 +42,12 @@ public: events.push_back("RuntimeStop:" + (scene ? scene->GetName() : std::string("null"))); } + bool TryGetAvailableScriptClasses( + std::vector& outClasses) const override { + outClasses = scriptClasses; + return true; + } + bool TryGetClassFieldMetadata( const std::string& assemblyName, const std::string& namespaceName, @@ -117,6 +123,7 @@ public: } std::vector events; + std::vector scriptClasses; std::vector fieldMetadata; std::vector fieldDefaultValues; std::unordered_map managedFieldValues; @@ -325,6 +332,83 @@ TEST_F(ScriptEngineTest, RuntimeCreatedScriptComponentIsTrackedImmediatelyAndSta EXPECT_EQ(runtime.events, expectedAfterUpdate); } +TEST_F(ScriptEngineTest, ScriptClassDiscoveryApiReturnsSortedDescriptorsAndSupportsAssemblyFilter) { + runtime.scriptClasses = { + {"Tools", "", "UtilityProbe"}, + {"GameScripts", "Gameplay", "Zombie"}, + {"GameScripts", "", "Bootstrap"}, + {"GameScripts", "Gameplay", "Actor"}, + {"Broken", "", ""} + }; + + std::vector allClasses; + ASSERT_TRUE(engine->TryGetAvailableScriptClasses(allClasses)); + + const std::vector expectedAllClasses = { + {"GameScripts", "", "Bootstrap"}, + {"GameScripts", "Gameplay", "Actor"}, + {"GameScripts", "Gameplay", "Zombie"}, + {"Tools", "", "UtilityProbe"} + }; + EXPECT_EQ(allClasses, expectedAllClasses); + EXPECT_EQ(allClasses[0].GetFullName(), "Bootstrap"); + EXPECT_EQ(allClasses[1].GetFullName(), "Gameplay.Actor"); + + std::vector gameScriptClasses; + ASSERT_TRUE(engine->TryGetAvailableScriptClasses(gameScriptClasses, "GameScripts")); + + const std::vector expectedGameScriptClasses = { + {"GameScripts", "", "Bootstrap"}, + {"GameScripts", "Gameplay", "Actor"}, + {"GameScripts", "Gameplay", "Zombie"} + }; + EXPECT_EQ(gameScriptClasses, expectedGameScriptClasses); +} + +TEST_F(ScriptEngineTest, ChangingScriptClassWhileRuntimeRunningRecreatesTrackedInstance) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScriptComponent(host, "Gameplay", "OldClass"); + + engine->OnRuntimeStart(runtimeScene); + runtime.Clear(); + + component->SetScriptClass("GameScripts", "Gameplay", "NewClass"); + + const std::vector expected = { + "OnDisable:Host:Gameplay.NewClass", + "OnDestroy:Host:Gameplay.NewClass", + "Destroy:Host:Gameplay.NewClass", + "Create:Host:Gameplay.NewClass", + "Awake:Host:Gameplay.NewClass", + "OnEnable:Host:Gameplay.NewClass" + }; + EXPECT_EQ(runtime.events, expected); + EXPECT_TRUE(engine->HasTrackedScriptComponent(component)); + EXPECT_TRUE(engine->HasRuntimeInstance(component)); +} + +TEST_F(ScriptEngineTest, ClearingScriptClassWhileRuntimeRunningDestroysTrackedInstance) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScriptComponent(host, "Gameplay", "OldClass"); + + engine->OnRuntimeStart(runtimeScene); + runtime.Clear(); + + component->ClearScriptClass(); + + const std::vector expected = { + "OnDisable:Host:", + "OnDestroy:Host:", + "Destroy:Host:" + }; + EXPECT_EQ(runtime.events, expected); + EXPECT_FALSE(engine->HasTrackedScriptComponent(component)); + EXPECT_FALSE(engine->HasRuntimeInstance(component)); + EXPECT_FALSE(component->HasScriptClass()); +} + TEST_F(ScriptEngineTest, FieldReadApiPrefersLiveManagedValueAndFallsBackToStoredValue) { Scene* runtimeScene = CreateScene("RuntimeScene"); GameObject* host = runtimeScene->CreateGameObject("Host");