feat: expand editor scripting asset and viewport flow
@@ -49,6 +49,9 @@ if(NOT TARGET XCEngine)
|
||||
add_subdirectory("${XCENGINE_ENGINE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/engine_dependency")
|
||||
endif()
|
||||
|
||||
file(TO_CMAKE_PATH "${XCENGINE_ROOT_DIR}" XCENGINE_ROOT_DIR_CMAKE)
|
||||
file(TO_CMAKE_PATH "${XCENGINE_MONO_ROOT_DIR}" XCENGINE_MONO_ROOT_DIR_CMAKE)
|
||||
|
||||
set(IMGUI_SOURCES
|
||||
${imgui_SOURCE_DIR}/imgui.cpp
|
||||
${imgui_SOURCE_DIR}/imgui_demo.cpp
|
||||
@@ -63,6 +66,7 @@ add_executable(${PROJECT_NAME} WIN32
|
||||
src/EditorApp.rc
|
||||
src/main.cpp
|
||||
src/Application.cpp
|
||||
src/Scripting/EditorScriptAssemblyBuilder.cpp
|
||||
src/Theme.cpp
|
||||
src/Core/UndoManager.cpp
|
||||
src/Core/PlaySessionController.cpp
|
||||
@@ -101,6 +105,13 @@ target_include_directories(${PROJECT_NAME} PRIVATE
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE UNICODE _UNICODE)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8)
|
||||
|
||||
if(XCENGINE_ENABLE_MONO_SCRIPTING)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||
XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
XCENGINE_EDITOR_REPO_ROOT="${XCENGINE_ROOT_DIR_CMAKE}"
|
||||
XCENGINE_EDITOR_MONO_ROOT_DIR="${XCENGINE_MONO_ROOT_DIR_CMAKE}")
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /FS)
|
||||
set_property(TARGET ${PROJECT_NAME} PROPERTY
|
||||
|
||||
326
editor/README.md
@@ -1,183 +1,205 @@
|
||||
# UI Editor
|
||||
# XCEditor
|
||||
|
||||
Unity 风格的编辑器 UI,使用 ImGui 实现,作为 XCEngine 游戏引擎编辑器的一部分。
|
||||
`editor/` 是 XCEngine 当前随仓库维护的桌面编辑器模块。它不是一套独立渲染器,而是 `D3D12` 宿主应用,用来承接引擎 `Rendering + RHI + Scene + Scripting` 主链。
|
||||
|
||||
## 简介
|
||||
当前 editor 已经具备:
|
||||
|
||||
XCGameEngine UI 是一个仿 Unity 编辑器的桌面应用程序,提供场景管理、层级视图、属性检查器等功能。
|
||||
- Scene / Game viewport 离屏渲染接入
|
||||
- object-id picking 与选中描边
|
||||
- scene overlay / gizmo 正规化收口
|
||||
- 项目根目录解析与 `Project.xcproject` 加载
|
||||
- `Assets + .meta + Library` 风格项目目录接入
|
||||
- `ScriptComponent` 的脚本类与字段编辑入口
|
||||
|
||||
## 技术栈
|
||||
## 当前定位
|
||||
|
||||
- **渲染 API**: DirectX 12
|
||||
- **UI 框架**: ImGui
|
||||
- **语言**: C++17
|
||||
- **构建系统**: CMake
|
||||
- **依赖库**: DirectX 12 SDK
|
||||
如果你想理解当前 editor,先把它当成三层:
|
||||
|
||||
## 项目结构
|
||||
1. `Win32 + D3D12` 宿主窗口与 ImGui backend
|
||||
2. `ViewportHostService` 对引擎渲染链路的接线
|
||||
3. `panels/`、`Managers/`、`ComponentEditors/` 这些编辑器业务层
|
||||
|
||||
```
|
||||
ui/
|
||||
├── src/
|
||||
│ ├── main.cpp # 程序入口
|
||||
│ ├── Application.cpp/h # 应用主类
|
||||
│ ├── Theme.cpp/h # 主题系统
|
||||
│ ├── Core/
|
||||
│ │ ├── GameObject.h # 游戏对象
|
||||
│ │ └── LogEntry.h # 日志条目
|
||||
│ ├── Managers/
|
||||
│ │ ├── LogSystem.cpp/h # 日志系统
|
||||
│ │ ├── ProjectManager.cpp/h # 项目管理
|
||||
│ │ ├── SceneManager.cpp/h # 场景管理
|
||||
│ │ └── SelectionManager.cpp/h # 选择管理
|
||||
│ └── panels/
|
||||
│ ├── Panel.cpp/h # 面板基类
|
||||
│ ├── MenuBar.cpp/h # 菜单栏
|
||||
│ ├── HierarchyPanel.cpp/h # 层级面板
|
||||
│ ├── InspectorPanel.cpp/h # 检查器面板
|
||||
│ ├── SceneViewPanel.cpp/h # 场景视图
|
||||
│ ├── GameViewPanel.cpp/h # 游戏视图
|
||||
│ ├── ProjectPanel.cpp/h # 项目面板
|
||||
│ └── ConsolePanel.cpp/h # 控制台面板
|
||||
├── bin/Release/ # 输出目录
|
||||
│ ├── XCVolumeRendererUI2.exe # 可执行文件
|
||||
│ ├── imgui.ini # ImGui 配置
|
||||
│ └── Assets/
|
||||
│ └── Models/
|
||||
│ └── Character.fbx # 示例模型
|
||||
├── build/ # 构建目录
|
||||
└── CMakeLists.txt # CMake 配置
|
||||
```
|
||||
当前不应再把 editor 视为旧式“UI sample”。它已经是引擎工作区的正式入口之一。
|
||||
|
||||
## 构建方法
|
||||
## 构建
|
||||
|
||||
推荐直接在仓库根目录构建,而不是单独进入 `editor/` 目录。
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Windows 10/11
|
||||
- Visual Studio 2019 或更高版本
|
||||
- Visual Studio 2022 / MSVC v143
|
||||
- CMake 3.15+
|
||||
- Vulkan SDK
|
||||
|
||||
### 构建步骤
|
||||
如果需要启用 Mono 脚本运行时,还需要:
|
||||
|
||||
- .NET SDK
|
||||
- `参考/Fermion/Fermion/external/mono` 下的 Mono 依赖
|
||||
|
||||
### 配置
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
cmake --build . --config Release
|
||||
cmake -S .. -B ..\build -A x64
|
||||
```
|
||||
|
||||
### 运行
|
||||
更常见的做法是直接在仓库根目录运行:
|
||||
|
||||
```bash
|
||||
# 运行编译好的可执行文件
|
||||
.\bin\Release\XCGameEngineUI.exe
|
||||
cmake -S . -B build -A x64
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
如果本地暂时没有 Mono,可以先关闭:
|
||||
|
||||
### 编辑器面板
|
||||
|
||||
#### 菜单栏(MenuBar)
|
||||
- 文件菜单(新建、打开、保存等)
|
||||
- 编辑菜单(撤销、重做等)
|
||||
- 视图菜单(面板显示/隐藏)
|
||||
- 帮助菜单
|
||||
|
||||
#### 层级面板(Hierarchy Panel)
|
||||
- 显示场景中所有游戏对象
|
||||
- 树形结构展示父子关系
|
||||
- 支持对象选择
|
||||
- 对象重命名
|
||||
|
||||
#### 检查器面板(Inspector Panel)
|
||||
- 显示选中对象的属性
|
||||
- 支持组件编辑
|
||||
- 变换组件(位置、旋转、缩放)
|
||||
- 材质组件
|
||||
|
||||
#### 场景视图(Scene View)
|
||||
- 3D 场景预览
|
||||
- 相机控制(平移、旋转、缩放)
|
||||
- 对象选择
|
||||
- 辅助工具(网格、轴心)
|
||||
|
||||
#### 游戏视图(Game View)
|
||||
- 游戏运行时的画面预览
|
||||
- 分辨率设置
|
||||
- 宽高比选择
|
||||
|
||||
#### 项目面板(Project Panel)
|
||||
- 项目文件浏览器
|
||||
- 资源组织
|
||||
- 搜索过滤
|
||||
|
||||
#### 控制台面板(Console Panel)
|
||||
- 日志输出
|
||||
- 警告和错误显示
|
||||
- 日志级别过滤
|
||||
- 清空日志
|
||||
|
||||
### 管理系统
|
||||
|
||||
#### 日志系统(LogSystem)
|
||||
- 分级日志(Info、Warning、Error)
|
||||
- 时间戳
|
||||
- 日志持久化
|
||||
|
||||
#### 项目管理(ProjectManager)
|
||||
- 项目创建/打开
|
||||
- 资源路径管理
|
||||
|
||||
#### 场景管理(SceneManager)
|
||||
- 场景加载/保存
|
||||
- 对象生命周期管理
|
||||
|
||||
#### 选择管理(SelectionManager)
|
||||
- 当前选中对象追踪
|
||||
- 多选支持
|
||||
|
||||
### 主题系统
|
||||
|
||||
- 深色主题(Dark Theme)
|
||||
- 可自定义配色方案
|
||||
|
||||
## 窗口布局
|
||||
|
||||
默认布局采用经典的 Unity 编辑器风格:
|
||||
|
||||
```
|
||||
+----------------------------------------------------------+
|
||||
| 菜单栏 |
|
||||
+----------+------------------------+----------------------+
|
||||
| | | |
|
||||
| 项目 | 场景视图 | 检查器 |
|
||||
| 面板 | | |
|
||||
| | | |
|
||||
+----------+------------------------+----------------------+
|
||||
| 层级面板 | 游戏视图 |
|
||||
| | |
|
||||
+------------------------------------+----------------------+
|
||||
| 控制台面板 |
|
||||
+----------------------------------------------------------+
|
||||
```bash
|
||||
cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF
|
||||
```
|
||||
|
||||
## 依赖说明
|
||||
### 构建 editor
|
||||
|
||||
- ImGui - 跨平台 GUI 库
|
||||
- DirectX 12 - 渲染 API
|
||||
- Windows SDK - 窗口管理
|
||||
```bash
|
||||
cmake --build build --config Debug --target XCEditor
|
||||
```
|
||||
|
||||
## 扩展开发
|
||||
说明:
|
||||
|
||||
### 添加新面板
|
||||
- target 名称是 `XCEditor`
|
||||
- 输出文件名仍然是 `XCEngine.exe`
|
||||
- 输出目录是 `editor/bin/<Config>/`
|
||||
|
||||
1. 在 `panels/` 目录下创建新的面板类
|
||||
2. 继承 `Panel` 基类
|
||||
3. 实现 `Render()` 方法
|
||||
4. 在 `Application` 中注册新面板
|
||||
## 运行
|
||||
|
||||
### 添加新组件
|
||||
```bash
|
||||
.\editor\bin\Debug\XCEngine.exe
|
||||
```
|
||||
|
||||
1. 定义组件类
|
||||
2. 在 `GameObject` 中注册组件类型
|
||||
3. 在 `InspectorPanel` 中添加属性编辑器
|
||||
默认情况下,editor 会自动把仓库内的 `project/` 识别为工程根目录。也可以显式指定工程:
|
||||
|
||||
```bash
|
||||
.\editor\bin\Debug\XCEngine.exe --project D:\Path\To\Project
|
||||
```
|
||||
|
||||
如果需要 C# 脚本类发现与 Inspector 字段编辑,先构建:
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target xcengine_project_managed_assemblies
|
||||
```
|
||||
|
||||
该 target 会把程序集放到:
|
||||
|
||||
- `project/Library/ScriptAssemblies/XCEngine.ScriptCore.dll`
|
||||
- `project/Library/ScriptAssemblies/GameScripts.dll`
|
||||
- `project/Library/ScriptAssemblies/mscorlib.dll`
|
||||
|
||||
## 当前目录结构
|
||||
|
||||
```text
|
||||
editor/
|
||||
├── CMakeLists.txt
|
||||
├── README.md
|
||||
├── resources/
|
||||
│ └── Icons/
|
||||
├── src/
|
||||
│ ├── Actions/ # 编辑器动作路由
|
||||
│ ├── Commands/ # 命令与实体操作
|
||||
│ ├── ComponentEditors/ # Inspector 组件编辑器
|
||||
│ ├── Core/ # 应用生命周期、日志、项目根解析、撤销等
|
||||
│ ├── Layers/ # EditorLayer 等高层组装
|
||||
│ ├── Layout/
|
||||
│ ├── Managers/ # SceneManager / ProjectManager
|
||||
│ ├── panels/ # Hierarchy / Scene / Game / Inspector / Project / Console
|
||||
│ ├── Platform/ # Win32 host、D3D12 backend 辅助
|
||||
│ ├── UI/ # ImGui bridge 与通用 widget
|
||||
│ ├── Utils/
|
||||
│ ├── Viewport/
|
||||
│ │ ├── Passes/ # editor viewport overlay pass
|
||||
│ │ ├── SceneViewportOverlayBuilder.*
|
||||
│ │ ├── SceneViewportPicker.*
|
||||
│ │ ├── SceneViewportMoveGizmo.*
|
||||
│ │ ├── SceneViewportRotateGizmo.*
|
||||
│ │ ├── SceneViewportScaleGizmo.*
|
||||
│ │ ├── ViewportHostRenderFlowUtils.h
|
||||
│ │ └── ViewportHostService.h
|
||||
│ ├── Application.cpp
|
||||
│ ├── Application.h
|
||||
│ ├── EditorApp.rc
|
||||
│ ├── Theme.cpp
|
||||
│ ├── Theme.h
|
||||
│ └── main.cpp
|
||||
└── bin/
|
||||
```
|
||||
|
||||
## 关键模块
|
||||
|
||||
### Application
|
||||
|
||||
- `src/Application.cpp`
|
||||
- `src/Application.h`
|
||||
|
||||
负责:
|
||||
|
||||
- editor 初始化与关闭
|
||||
- resource root 设置
|
||||
- scripting runtime 初始化
|
||||
- ImGui backend 初始化
|
||||
- `ViewportHostService` 接线
|
||||
|
||||
### Project Root
|
||||
|
||||
- `src/Core/ProjectRootResolver.h`
|
||||
- `src/Utils/ProjectFileUtils.h`
|
||||
|
||||
负责:
|
||||
|
||||
- 自动识别仓库内 `project/`
|
||||
- 解析 `--project`
|
||||
- 读写 `Project.xcproject`
|
||||
|
||||
### Viewport
|
||||
|
||||
- `src/Viewport/ViewportHostService.h`
|
||||
- `src/Viewport/ViewportHostRenderFlowUtils.h`
|
||||
- `src/Viewport/SceneViewportOverlayBuilder.*`
|
||||
- `src/Viewport/Passes/SceneViewportEditorOverlayPass.*`
|
||||
|
||||
负责:
|
||||
|
||||
- 组装 scene/game viewport 渲染请求
|
||||
- 把 editor overlay 接入 `CameraRenderRequest::overlayPasses`
|
||||
- object-id picking、outline、overlay pass 等 editor 视口能力
|
||||
|
||||
### Panels
|
||||
|
||||
当前主要面板:
|
||||
|
||||
- `HierarchyPanel`
|
||||
- `SceneViewPanel`
|
||||
- `GameViewPanel`
|
||||
- `InspectorPanel`
|
||||
- `ProjectPanel`
|
||||
- `ConsolePanel`
|
||||
|
||||
### Component Editors
|
||||
|
||||
`ComponentEditors/` 当前不仅负责基础组件,也已经包含 `ScriptComponent` 的 Inspector 编辑入口。
|
||||
|
||||
## 开发约束
|
||||
|
||||
- editor 是宿主,不是第二套 renderer。
|
||||
- 新的世界空间 overlay / gizmo,不应继续堆到 ImGui world draw 路径。
|
||||
- viewport 相关问题优先检查 `engine/Rendering`、`RenderSurface` 与 `ViewportHostService` 的接线,而不是直接在 panel 里复制渲染逻辑。
|
||||
- 与项目资源、脚本程序集、`.meta`、`Library` 相关的问题,不要假设 editor 仍处于“无工程状态”的旧结构。
|
||||
|
||||
## 推荐验证
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target editor_tests
|
||||
cmake --build build --config Debug --target rendering_phase_regression
|
||||
```
|
||||
|
||||
如果改动影响脚本类发现或 Inspector 脚本字段编辑,再补:
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target xcengine_project_managed_assemblies
|
||||
cmake --build build --config Debug --target scripting_tests
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 180 B |
BIN
editor/color.png
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 29 KiB |
@@ -1,28 +0,0 @@
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def get_dominant_color(image_path):
|
||||
img = Image.open(image_path).convert("RGB")
|
||||
img = img.resize((1, 1), Image.Resampling.LANCZOS)
|
||||
r, g, b = img.getpixel((0, 0))
|
||||
return r, g, b
|
||||
|
||||
|
||||
def rename_with_color(base_path):
|
||||
files = ["color.png", "color2.png"]
|
||||
for f in files:
|
||||
old_path = os.path.join(base_path, f)
|
||||
if os.path.exists(old_path):
|
||||
r, g, b = get_dominant_color(old_path)
|
||||
new_name = f"color-({r},{g},{b}).png"
|
||||
new_path = os.path.join(base_path, new_name)
|
||||
os.rename(old_path, new_path)
|
||||
print(f"Renamed: {f} -> {new_name}")
|
||||
else:
|
||||
print(f"File not found: {old_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
base = r"D:\Xuanchi\Main\XCEngine\editor"
|
||||
rename_with_color(base)
|
||||
BIN
editor/resources/Icons/camera_gizmo.png
Normal file
|
After Width: | Height: | Size: 672 B |
BIN
editor/resources/Icons/main_light_gizmo.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 232 B |
BIN
editor/resources/Icons/mesh_icondd.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
editor/resources/Icons/view_move.png
Normal file
|
After Width: | Height: | Size: 657 B |
BIN
editor/resources/Icons/view_orbit.png
Normal file
|
After Width: | Height: | Size: 381 B |
@@ -28,6 +28,14 @@ inline ActionBinding MakeSaveProjectAction(bool enabled = true) {
|
||||
return MakeAction("Save Project", nullptr, false, enabled);
|
||||
}
|
||||
|
||||
inline ActionBinding MakeRebuildScriptsAction(bool enabled = true) {
|
||||
return MakeAction("Rebuild Script Assemblies", nullptr, false, enabled);
|
||||
}
|
||||
|
||||
inline ActionBinding MakeMigrateSceneAssetReferencesAction(bool enabled = true) {
|
||||
return MakeAction("Migrate Scene AssetRefs", nullptr, false, enabled);
|
||||
}
|
||||
|
||||
inline ActionBinding MakeNewSceneAction(bool enabled = true) {
|
||||
return MakeAction("New Scene", "Ctrl+N", false, enabled, true, Shortcut(ImGuiKey_N, true));
|
||||
}
|
||||
|
||||
@@ -168,6 +168,19 @@ inline void DrawHierarchySortOptionsPopup(
|
||||
}
|
||||
|
||||
inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Components::GameObject* parent) {
|
||||
const auto drawPrimitiveCreateAction = [&](
|
||||
const ActionBinding& action,
|
||||
::XCEngine::Resources::BuiltinPrimitiveType primitiveType) {
|
||||
const char* label = ::XCEngine::Resources::GetBuiltinPrimitiveDisplayName(primitiveType);
|
||||
DrawMenuAction(action, [&]() {
|
||||
TraceHierarchyPopup(std::string("Hierarchy create clicked: ") + label);
|
||||
auto* created = Commands::CreatePrimitiveEntity(context, primitiveType, parent);
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: ") + label + ", createdId=" +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
};
|
||||
|
||||
DrawMenuAction(MakeCreateEmptyEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Empty Object");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Entity", "GameObject");
|
||||
@@ -175,6 +188,16 @@ inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Comp
|
||||
std::string("Hierarchy create result: Empty Object, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
|
||||
UI::DrawContextSubmenu("3D Object", [&]() {
|
||||
drawPrimitiveCreateAction(MakeCreateCubeEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Cube);
|
||||
drawPrimitiveCreateAction(MakeCreateSphereEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Sphere);
|
||||
drawPrimitiveCreateAction(MakeCreateCapsuleEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Capsule);
|
||||
drawPrimitiveCreateAction(MakeCreateCylinderEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Cylinder);
|
||||
drawPrimitiveCreateAction(MakeCreatePlaneEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Plane);
|
||||
drawPrimitiveCreateAction(MakeCreateQuadEntityAction(), ::XCEngine::Resources::BuiltinPrimitiveType::Quad);
|
||||
});
|
||||
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeCreateCameraEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Camera");
|
||||
@@ -190,28 +213,6 @@ inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Comp
|
||||
std::string("Hierarchy create result: Light, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeCreateCubeEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Cube");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Cube", "Cube");
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Cube, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
DrawMenuAction(MakeCreateSphereEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Sphere");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Sphere", "Sphere");
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Sphere, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
DrawMenuAction(MakeCreatePlaneEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Plane");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Plane", "Plane");
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Plane, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
}
|
||||
|
||||
inline void HandleHierarchyItemContextRequest(
|
||||
@@ -275,7 +276,7 @@ inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::Def
|
||||
backgroundContextMenu.ConsumeOpenRequest("HierarchyContextMenu");
|
||||
static bool s_lastBackgroundPopupOpen = false;
|
||||
|
||||
if (!UI::BeginPopup("HierarchyContextMenu")) {
|
||||
if (!UI::BeginContextMenu("HierarchyContextMenu")) {
|
||||
if (s_lastBackgroundPopupOpen) {
|
||||
TraceHierarchyPopup("Hierarchy background popup closed");
|
||||
s_lastBackgroundPopupOpen = false;
|
||||
@@ -289,7 +290,7 @@ inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::Def
|
||||
}
|
||||
|
||||
DrawHierarchyContextActions(context, nullptr, true);
|
||||
UI::EndPopup();
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
inline void DrawHierarchyEntityContextPopup(
|
||||
@@ -298,7 +299,7 @@ inline void DrawHierarchyEntityContextPopup(
|
||||
itemContextMenu.ConsumeOpenRequest("HierarchyEntityContextMenu");
|
||||
static bool s_lastEntityPopupOpen = false;
|
||||
|
||||
if (!UI::BeginPopup("HierarchyEntityContextMenu")) {
|
||||
if (!UI::BeginContextMenu("HierarchyEntityContextMenu")) {
|
||||
if (s_lastEntityPopupOpen) {
|
||||
TraceHierarchyPopup("Hierarchy entity popup closed");
|
||||
s_lastEntityPopupOpen = false;
|
||||
@@ -314,7 +315,7 @@ inline void DrawHierarchyEntityContextPopup(
|
||||
if (itemContextMenu.HasTarget()) {
|
||||
DrawHierarchyContextActions(context, itemContextMenu.TargetValue());
|
||||
}
|
||||
UI::EndPopup();
|
||||
UI::EndContextMenu();
|
||||
|
||||
if (!ImGui::IsPopupOpen("HierarchyEntityContextMenu") && !itemContextMenu.HasPendingOpenRequest()) {
|
||||
itemContextMenu.Clear();
|
||||
|
||||
@@ -34,6 +34,14 @@ inline void ExecuteSaveProject(IEditorContext& context) {
|
||||
Commands::SaveProject(context);
|
||||
}
|
||||
|
||||
inline void ExecuteRebuildScriptAssemblies(IEditorContext& context) {
|
||||
Commands::RebuildScriptAssemblies(context);
|
||||
}
|
||||
|
||||
inline void ExecuteMigrateSceneAssetReferences(IEditorContext& context) {
|
||||
Commands::MigrateSceneAssetReferences(context);
|
||||
}
|
||||
|
||||
inline void ExecuteOpenScene(IEditorContext& context) {
|
||||
Commands::OpenSceneWithDialog(context);
|
||||
}
|
||||
@@ -143,6 +151,10 @@ inline void DrawFileMenuActions(IEditorContext& context) {
|
||||
DrawMenuAction(MakeSaveSceneAction(canEditDocuments), [&]() { ExecuteSaveScene(context); });
|
||||
DrawMenuAction(MakeSaveSceneAsAction(canEditDocuments), [&]() { ExecuteSaveSceneAs(context); });
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(
|
||||
MakeMigrateSceneAssetReferencesAction(Commands::CanMigrateSceneAssetReferences(context)),
|
||||
[&]() { ExecuteMigrateSceneAssetReferences(context); });
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeExitAction(), [&]() { RequestEditorExit(context); });
|
||||
}
|
||||
|
||||
@@ -160,6 +172,12 @@ inline void DrawRunMenuActions(IEditorContext& context) {
|
||||
});
|
||||
}
|
||||
|
||||
inline void DrawScriptsMenuActions(IEditorContext& context) {
|
||||
DrawMenuAction(
|
||||
MakeRebuildScriptsAction(Commands::CanRebuildScriptAssemblies(context)),
|
||||
[&]() { ExecuteRebuildScriptAssemblies(context); });
|
||||
}
|
||||
|
||||
inline void DrawViewMenuActions(IEditorContext& context) {
|
||||
DrawMenuAction(MakeResetLayoutAction(), [&]() { RequestDockLayoutReset(context); });
|
||||
}
|
||||
@@ -186,6 +204,9 @@ inline void DrawMainMenuBar(IEditorContext& context, UI::DeferredPopupState& abo
|
||||
UI::DrawMenuScope("Run", [&]() {
|
||||
DrawRunMenuActions(context);
|
||||
});
|
||||
UI::DrawMenuScope("Scripts", [&]() {
|
||||
DrawScriptsMenuActions(context);
|
||||
});
|
||||
UI::DrawMenuScope("View", [&]() {
|
||||
DrawViewMenuActions(context);
|
||||
});
|
||||
|
||||
@@ -6,12 +6,18 @@
|
||||
#include "Core/EditorContext.h"
|
||||
#include "Core/EditorEvents.h"
|
||||
#include "Core/EventBus.h"
|
||||
#include "Scripting/EditorScriptAssemblyBuilder.h"
|
||||
#include "UI/BuiltInIcons.h"
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "Platform/WindowsProcessDiagnostics.h"
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <XCEngine/Scripting/ScriptEngine.h>
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
|
||||
#endif
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <windows.h>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -22,6 +28,123 @@ Application& Application::Get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
void Application::InitializeScriptingRuntime(const std::string& projectPath) {
|
||||
ShutdownScriptingRuntime();
|
||||
|
||||
const std::filesystem::path assemblyDirectoryPath =
|
||||
std::filesystem::path(Platform::Utf8ToWide(projectPath)) / L"Library" / L"ScriptAssemblies";
|
||||
m_scriptRuntimeStatus.assemblyDirectory = Platform::WideToUtf8(assemblyDirectoryPath.wstring());
|
||||
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
auto& logger = Debug::Logger::Get();
|
||||
const fs::path assemblyDirectory = assemblyDirectoryPath;
|
||||
m_scriptRuntimeStatus.backendEnabled = true;
|
||||
|
||||
::XCEngine::Scripting::MonoScriptRuntime::Settings settings;
|
||||
settings.assemblyDirectory = assemblyDirectory;
|
||||
settings.corlibDirectory = assemblyDirectory;
|
||||
settings.coreAssemblyPath = assemblyDirectory / L"XCEngine.ScriptCore.dll";
|
||||
settings.appAssemblyPath = assemblyDirectory / L"GameScripts.dll";
|
||||
|
||||
std::error_code ec;
|
||||
const bool hasCoreAssembly = fs::exists(settings.coreAssemblyPath, ec);
|
||||
ec.clear();
|
||||
const bool hasAppAssembly = fs::exists(settings.appAssemblyPath, ec);
|
||||
ec.clear();
|
||||
const bool hasCorlibAssembly = fs::exists(assemblyDirectory / L"mscorlib.dll", ec);
|
||||
m_scriptRuntimeStatus.assembliesFound = hasCoreAssembly && hasAppAssembly && hasCorlibAssembly;
|
||||
|
||||
if (!hasCoreAssembly || !hasAppAssembly || !hasCorlibAssembly) {
|
||||
m_scriptRuntimeStatus.statusMessage =
|
||||
"Script assemblies were not found in " + Platform::WideToUtf8(assemblyDirectory.wstring()) +
|
||||
". Script class discovery is disabled until the managed assemblies are built.";
|
||||
logger.Warning(Debug::LogCategory::Scripting, m_scriptRuntimeStatus.statusMessage.c_str());
|
||||
::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
auto runtime = std::make_unique<::XCEngine::Scripting::MonoScriptRuntime>(settings);
|
||||
if (!runtime->Initialize()) {
|
||||
m_scriptRuntimeStatus.statusMessage =
|
||||
"Failed to initialize editor script runtime: " + runtime->GetLastError();
|
||||
logger.Warning(Debug::LogCategory::Scripting, m_scriptRuntimeStatus.statusMessage.c_str());
|
||||
::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(runtime.get());
|
||||
m_scriptRuntimeStatus.runtimeLoaded = true;
|
||||
m_scriptRuntime = std::move(runtime);
|
||||
logger.Info(Debug::LogCategory::Scripting, "Editor script runtime initialized.");
|
||||
#else
|
||||
(void)projectPath;
|
||||
m_scriptRuntimeStatus.backendEnabled = false;
|
||||
m_scriptRuntimeStatus.statusMessage = "This editor build does not include Mono scripting support.";
|
||||
::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Application::ShutdownScriptingRuntime() {
|
||||
::XCEngine::Scripting::ScriptEngine::Get().OnRuntimeStop();
|
||||
::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr);
|
||||
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
m_scriptRuntime.reset();
|
||||
#endif
|
||||
|
||||
m_scriptRuntimeStatus = {};
|
||||
}
|
||||
|
||||
bool Application::ReloadScriptingRuntime() {
|
||||
if (!m_editorContext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string& projectPath = m_editorContext->GetProjectPath();
|
||||
if (projectPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
InitializeScriptingRuntime(projectPath);
|
||||
return m_scriptRuntimeStatus.runtimeLoaded;
|
||||
}
|
||||
|
||||
bool Application::RebuildScriptingAssemblies() {
|
||||
if (!m_editorContext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string& projectPath = m_editorContext->GetProjectPath();
|
||||
if (projectPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
auto& logger = Debug::Logger::Get();
|
||||
logger.Info(Debug::LogCategory::Scripting, "Rebuilding project script assemblies...");
|
||||
|
||||
// Release the currently loaded project assembly before invoking the compiler.
|
||||
// Otherwise GameScripts.dll can remain locked by the active Mono app domain.
|
||||
ShutdownScriptingRuntime();
|
||||
|
||||
const ::XCEngine::Editor::Scripting::EditorScriptAssemblyBuildResult buildResult =
|
||||
::XCEngine::Editor::Scripting::EditorScriptAssemblyBuilder::RebuildProjectAssemblies(projectPath);
|
||||
if (!buildResult.succeeded) {
|
||||
m_scriptRuntimeStatus.statusMessage = buildResult.message;
|
||||
logger.Error(Debug::LogCategory::Scripting, buildResult.message.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.Info(Debug::LogCategory::Scripting, buildResult.message.c_str());
|
||||
return ReloadScriptingRuntime();
|
||||
#else
|
||||
m_scriptRuntimeStatus.statusMessage = "This editor build does not include Mono scripting support.";
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Application::InitializeWindowRenderer(HWND hwnd) {
|
||||
RECT clientRect = {};
|
||||
if (!GetClientRect(hwnd, &clientRect)) {
|
||||
@@ -150,6 +273,7 @@ bool Application::Initialize(HWND hwnd) {
|
||||
|
||||
logger.Info(Debug::LogCategory::General, "Initializing editor context...");
|
||||
InitializeEditorContext(projectRoot);
|
||||
InitializeScriptingRuntime(projectRoot);
|
||||
logger.Info(Debug::LogCategory::General, "Initializing ImGui backend...");
|
||||
InitializeImGui(hwnd);
|
||||
logger.Info(Debug::LogCategory::General, "Attaching editor layer...");
|
||||
@@ -171,6 +295,7 @@ void Application::Shutdown() {
|
||||
UI::ShutdownBuiltInIcons();
|
||||
m_imguiBackend.Shutdown();
|
||||
m_imguiSession.Shutdown();
|
||||
ShutdownScriptingRuntime();
|
||||
ShutdownEditorContext();
|
||||
if (m_resourceManagerInitialized) {
|
||||
::XCEngine::Resources::ResourceManager::Get().Shutdown();
|
||||
@@ -230,6 +355,7 @@ bool Application::SwitchProject(const std::string& projectPath) {
|
||||
logger.Info(Debug::LogCategory::General, infoMessage.c_str());
|
||||
|
||||
::XCEngine::Resources::ResourceManager::Get().SetResourceRoot(projectPath.c_str());
|
||||
InitializeScriptingRuntime(projectPath);
|
||||
|
||||
m_lastWindowTitle.clear();
|
||||
UpdateWindowTitle();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Platform/D3D12WindowRenderer.h"
|
||||
#include "Scripting/EditorScriptRuntimeStatus.h"
|
||||
#include "UI/ImGuiBackendBridge.h"
|
||||
#include "UI/ImGuiSession.h"
|
||||
#include "Viewport/ViewportHostService.h"
|
||||
@@ -19,6 +20,12 @@ class RHIDevice;
|
||||
class RHISwapChain;
|
||||
} // namespace RHI
|
||||
|
||||
namespace Scripting {
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
class MonoScriptRuntime;
|
||||
#endif
|
||||
} // namespace Scripting
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class EditorLayer;
|
||||
@@ -33,6 +40,8 @@ public:
|
||||
void Render();
|
||||
void OnResize(int width, int height);
|
||||
bool SwitchProject(const std::string& projectPath);
|
||||
bool ReloadScriptingRuntime();
|
||||
bool RebuildScriptingAssemblies();
|
||||
void SaveProjectState();
|
||||
Rendering::RenderContext GetMainRenderContext() const { return m_windowRenderer.GetRenderContext(); }
|
||||
RHI::RHIDevice* GetMainRHIDevice() const { return m_windowRenderer.GetRHIDevice(); }
|
||||
@@ -42,6 +51,7 @@ public:
|
||||
HWND GetWindowHandle() const { return m_hwnd; }
|
||||
|
||||
IEditorContext& GetEditorContext() const { return *m_editorContext; }
|
||||
const EditorScriptRuntimeStatus& GetScriptRuntimeStatus() const { return m_scriptRuntimeStatus; }
|
||||
|
||||
private:
|
||||
Application() = default;
|
||||
@@ -52,6 +62,8 @@ private:
|
||||
void AttachEditorLayer();
|
||||
void DetachEditorLayer();
|
||||
void ShutdownEditorContext();
|
||||
void InitializeScriptingRuntime(const std::string& projectPath);
|
||||
void ShutdownScriptingRuntime();
|
||||
void RenderEditorFrame();
|
||||
void UpdateWindowTitle();
|
||||
|
||||
@@ -70,6 +82,10 @@ private:
|
||||
bool m_hasLastFrameTime = false;
|
||||
bool m_renderReady = false;
|
||||
bool m_resourceManagerInitialized = false;
|
||||
EditorScriptRuntimeStatus m_scriptRuntimeStatus;
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
std::unique_ptr<::XCEngine::Scripting::MonoScriptRuntime> m_scriptRuntime;
|
||||
#endif
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/LightComponent.h>
|
||||
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
#include <XCEngine/Resources/BuiltinResources.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
@@ -80,6 +83,31 @@ inline ::XCEngine::Components::GameObject* CreateLightEntity(
|
||||
});
|
||||
}
|
||||
|
||||
inline ::XCEngine::Components::GameObject* CreatePrimitiveEntity(
|
||||
IEditorContext& context,
|
||||
::XCEngine::Resources::BuiltinPrimitiveType primitiveType,
|
||||
::XCEngine::Components::GameObject* parent = nullptr) {
|
||||
const char* primitiveName = ::XCEngine::Resources::GetBuiltinPrimitiveDisplayName(primitiveType);
|
||||
return CreateEntity(
|
||||
context,
|
||||
std::string("Create ") + primitiveName,
|
||||
primitiveName,
|
||||
parent,
|
||||
[primitiveType](::XCEngine::Components::GameObject& entity, ISceneManager&) {
|
||||
auto* meshFilter = entity.GetComponent<::XCEngine::Components::MeshFilterComponent>();
|
||||
if (meshFilter == nullptr) {
|
||||
meshFilter = entity.AddComponent<::XCEngine::Components::MeshFilterComponent>();
|
||||
}
|
||||
meshFilter->SetMeshPath(::XCEngine::Resources::GetBuiltinPrimitiveMeshPath(primitiveType).CStr());
|
||||
|
||||
auto* meshRenderer = entity.GetComponent<::XCEngine::Components::MeshRendererComponent>();
|
||||
if (meshRenderer == nullptr) {
|
||||
meshRenderer = entity.AddComponent<::XCEngine::Components::MeshRendererComponent>();
|
||||
}
|
||||
meshRenderer->SetMaterialPath(0, ::XCEngine::Resources::GetBuiltinDefaultPrimitiveMaterialPath().CStr());
|
||||
});
|
||||
}
|
||||
|
||||
inline bool RenameEntity(
|
||||
IEditorContext& context,
|
||||
::XCEngine::Components::GameObject::ID entityId,
|
||||
|
||||
@@ -314,6 +314,35 @@ inline bool SaveProject(IEditorContext& context) {
|
||||
return SaveProjectDescriptor(context);
|
||||
}
|
||||
|
||||
inline bool CanRebuildScriptAssemblies(const IEditorContext& context) {
|
||||
return IsProjectDocumentEditingAllowed(context) && !context.GetProjectPath().empty();
|
||||
}
|
||||
|
||||
inline bool RebuildScriptAssemblies(IEditorContext& context) {
|
||||
if (!CanRebuildScriptAssemblies(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool rebuilt = Application::Get().RebuildScriptingAssemblies();
|
||||
if (rebuilt) {
|
||||
context.GetProjectManager().RefreshCurrentFolder();
|
||||
}
|
||||
|
||||
return rebuilt;
|
||||
}
|
||||
|
||||
inline bool CanMigrateSceneAssetReferences(const IEditorContext& context) {
|
||||
return IsProjectDocumentEditingAllowed(context) && !context.GetProjectPath().empty();
|
||||
}
|
||||
|
||||
inline IProjectManager::SceneAssetReferenceMigrationReport MigrateSceneAssetReferences(IEditorContext& context) {
|
||||
if (!CanMigrateSceneAssetReferences(context)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return context.GetProjectManager().MigrateSceneAssetReferences();
|
||||
}
|
||||
|
||||
inline bool SwitchProject(IEditorContext& context, const std::string& projectPath) {
|
||||
if (!IsProjectDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
|
||||
@@ -30,7 +30,7 @@ inline AssetReferenceInteraction DrawAssetReferenceProperty(
|
||||
pickerOptions.assetsTabLabel = "Assets";
|
||||
pickerOptions.sceneTabLabel = "Scene";
|
||||
pickerOptions.showAssetsTab = true;
|
||||
pickerOptions.showSceneTab = true;
|
||||
pickerOptions.showSceneTab = false;
|
||||
pickerOptions.supportedAssetExtensions = supportedExtensions;
|
||||
|
||||
UI::DrawPropertyRow(label, UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "ComponentEditors/LightComponentEditor.h"
|
||||
#include "ComponentEditors/MeshFilterComponentEditor.h"
|
||||
#include "ComponentEditors/MeshRendererComponentEditor.h"
|
||||
#include "ComponentEditors/ScriptComponentEditor.h"
|
||||
#include "ComponentEditors/TransformComponentEditor.h"
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -20,6 +21,7 @@ ComponentEditorRegistry::ComponentEditorRegistry() {
|
||||
RegisterEditor(std::make_unique<LightComponentEditor>());
|
||||
RegisterEditor(std::make_unique<MeshFilterComponentEditor>());
|
||||
RegisterEditor(std::make_unique<MeshRendererComponentEditor>());
|
||||
RegisterEditor(std::make_unique<ScriptComponentEditor>());
|
||||
}
|
||||
|
||||
void ComponentEditorRegistry::RegisterEditor(std::unique_ptr<IComponentEditor> editor) {
|
||||
|
||||
515
editor/src/ComponentEditors/ScriptComponentEditor.h
Normal file
@@ -0,0 +1,515 @@
|
||||
#pragma once
|
||||
|
||||
#include "Application.h"
|
||||
#include "IComponentEditor.h"
|
||||
#include "ScriptComponentEditorUtils.h"
|
||||
#include "Core/IUndoManager.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <XCEngine/Scripting/ScriptComponent.h>
|
||||
#include <XCEngine/Scripting/ScriptEngine.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
class ScriptComponentEditor : public IComponentEditor {
|
||||
public:
|
||||
const char* GetComponentTypeName() const override {
|
||||
return "ScriptComponent";
|
||||
}
|
||||
|
||||
const char* GetDisplayName() const override {
|
||||
return "Script";
|
||||
}
|
||||
|
||||
bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override {
|
||||
auto* scriptComponent = dynamic_cast<::XCEngine::Scripting::ScriptComponent*>(component);
|
||||
if (!scriptComponent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
changed |= RenderScriptClassSelector(*scriptComponent, undoManager);
|
||||
|
||||
::XCEngine::Scripting::ScriptFieldModel model;
|
||||
if (!::XCEngine::Scripting::ScriptEngine::Get().TryGetScriptFieldModel(scriptComponent, model)) {
|
||||
UI::DrawHintText("Failed to query script field metadata.");
|
||||
return changed;
|
||||
}
|
||||
|
||||
switch (model.classStatus) {
|
||||
case ::XCEngine::Scripting::ScriptFieldClassStatus::Unassigned:
|
||||
UI::DrawHintText("Assign a C# script to edit serialized fields.");
|
||||
return changed;
|
||||
case ::XCEngine::Scripting::ScriptFieldClassStatus::Missing:
|
||||
UI::DrawHintText("Assigned script class is not available in the loaded script assemblies.");
|
||||
break;
|
||||
case ::XCEngine::Scripting::ScriptFieldClassStatus::Available:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (model.fields.empty()) {
|
||||
UI::DrawHintText(
|
||||
model.classStatus == ::XCEngine::Scripting::ScriptFieldClassStatus::Available
|
||||
? "Selected script exposes no supported public instance fields."
|
||||
: "No serialized script fields are currently available.");
|
||||
return changed;
|
||||
}
|
||||
|
||||
for (const ::XCEngine::Scripting::ScriptFieldSnapshot& field : model.fields) {
|
||||
changed |= RenderScriptField(*scriptComponent, model.classStatus, field, undoManager);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override {
|
||||
return gameObject != nullptr;
|
||||
}
|
||||
|
||||
const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override {
|
||||
return gameObject ? nullptr : "Invalid";
|
||||
}
|
||||
|
||||
bool CanRemove(::XCEngine::Components::Component* component) const override {
|
||||
return CanEdit(component);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr size_t kStringBufferSize = 512;
|
||||
|
||||
struct StringFieldEditState {
|
||||
std::array<char, kStringBufferSize> buffer{};
|
||||
std::string lastSyncedValue;
|
||||
bool initialized = false;
|
||||
bool editing = false;
|
||||
};
|
||||
|
||||
bool RenderScriptClassSelector(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
IUndoManager* undoManager) {
|
||||
std::vector<::XCEngine::Scripting::ScriptClassDescriptor> scriptClasses;
|
||||
const bool hasLoadedClasses =
|
||||
::XCEngine::Scripting::ScriptEngine::Get().TryGetAvailableScriptClasses(scriptClasses);
|
||||
|
||||
const ::XCEngine::Scripting::ScriptClassDescriptor currentDescriptor{
|
||||
scriptComponent.GetAssemblyName(),
|
||||
scriptComponent.GetNamespaceName(),
|
||||
scriptComponent.GetClassName()
|
||||
};
|
||||
|
||||
std::string currentLabel = "None";
|
||||
if (scriptComponent.HasScriptClass()) {
|
||||
const auto currentIt = std::find(scriptClasses.begin(), scriptClasses.end(), currentDescriptor);
|
||||
currentLabel = currentIt != scriptClasses.end()
|
||||
? ScriptComponentEditorUtils::BuildScriptClassDisplayName(*currentIt)
|
||||
: ScriptComponentEditorUtils::BuildScriptClassDisplayName(scriptComponent) + " (Missing)";
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
UI::DrawPropertyRow("Script", UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
ImGui::SetNextItemWidth(layout.controlWidth);
|
||||
if (!ImGui::BeginCombo("##ScriptClass", currentLabel.c_str())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ImGui::Selectable("None", !scriptComponent.HasScriptClass())) {
|
||||
changed |= ApplyScriptClassSelection(scriptComponent, nullptr, undoManager);
|
||||
}
|
||||
|
||||
if (!scriptClasses.empty()) {
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
for (const ::XCEngine::Scripting::ScriptClassDescriptor& descriptor : scriptClasses) {
|
||||
const bool selected = descriptor == currentDescriptor;
|
||||
const std::string label =
|
||||
ScriptComponentEditorUtils::BuildScriptClassDisplayName(descriptor);
|
||||
if (ImGui::Selectable(label.c_str(), selected)) {
|
||||
changed |= ApplyScriptClassSelection(scriptComponent, &descriptor, undoManager);
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasLoadedClasses) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("No script assemblies are currently loaded.");
|
||||
}
|
||||
|
||||
ImGui::EndCombo();
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!hasLoadedClasses) {
|
||||
const EditorScriptRuntimeStatus& runtimeStatus = Application::Get().GetScriptRuntimeStatus();
|
||||
const std::string hintText =
|
||||
ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(runtimeStatus);
|
||||
UI::DrawHintText(hintText.c_str());
|
||||
|
||||
if (ScriptComponentEditorUtils::CanRebuildScriptAssemblies(runtimeStatus)) {
|
||||
if (UI::InspectorActionButton("Rebuild Scripts", ImVec2(120.0f, 0.0f))) {
|
||||
Application::Get().RebuildScriptingAssemblies();
|
||||
}
|
||||
}
|
||||
|
||||
if (ScriptComponentEditorUtils::CanReloadScriptRuntime(runtimeStatus)) {
|
||||
if (ScriptComponentEditorUtils::CanRebuildScriptAssemblies(runtimeStatus)) {
|
||||
ImGui::SameLine();
|
||||
}
|
||||
if (UI::InspectorActionButton("Reload Scripts", ImVec2(120.0f, 0.0f))) {
|
||||
Application::Get().ReloadScriptingRuntime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool ApplyScriptClassSelection(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptClassDescriptor* descriptor,
|
||||
IUndoManager* undoManager) const {
|
||||
if (!descriptor) {
|
||||
if (!scriptComponent.HasScriptClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (undoManager) {
|
||||
undoManager->BeginInteractiveChange("Modify Script Component");
|
||||
}
|
||||
scriptComponent.ClearScriptClass();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (scriptComponent.GetAssemblyName() == descriptor->assemblyName &&
|
||||
scriptComponent.GetNamespaceName() == descriptor->namespaceName &&
|
||||
scriptComponent.GetClassName() == descriptor->className) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (undoManager) {
|
||||
undoManager->BeginInteractiveChange("Modify Script Component");
|
||||
}
|
||||
scriptComponent.SetScriptClass(
|
||||
descriptor->assemblyName,
|
||||
descriptor->namespaceName,
|
||||
descriptor->className);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderScriptField(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
::XCEngine::Scripting::ScriptFieldClassStatus classStatus,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
IUndoManager* undoManager) {
|
||||
bool changed = false;
|
||||
const bool canEdit = ScriptComponentEditorUtils::CanEditScriptField(classStatus, field);
|
||||
|
||||
if (canEdit) {
|
||||
changed |= RenderScriptFieldEditor(scriptComponent, field, undoManager);
|
||||
} else {
|
||||
RenderReadOnlyScriptField(field);
|
||||
}
|
||||
|
||||
const std::string issueText = ScriptComponentEditorUtils::BuildScriptFieldIssueText(field);
|
||||
if (!issueText.empty()) {
|
||||
UI::DrawHintText(issueText.c_str());
|
||||
}
|
||||
|
||||
if (ScriptComponentEditorUtils::CanClearScriptFieldOverride(field)) {
|
||||
ImGui::PushID((field.metadata.name + "##Reset").c_str());
|
||||
if (UI::InspectorActionButton("Reset", ImVec2(72.0f, 0.0f))) {
|
||||
changed |= ClearScriptFieldOverride(scriptComponent, field, undoManager);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool RenderScriptFieldEditor(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
IUndoManager* undoManager) {
|
||||
using namespace ::XCEngine::Scripting;
|
||||
|
||||
switch (field.metadata.type) {
|
||||
case ScriptFieldType::Float: {
|
||||
float value = std::get<float>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyFloat(field.metadata.name.c_str(), value, 0.1f);
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::Double: {
|
||||
double value = std::get<double>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
ImGui::SetNextItemWidth(layout.controlWidth);
|
||||
return ImGui::InputDouble("##Value", &value, 0.0, 0.0, "%.3f");
|
||||
});
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::Bool: {
|
||||
bool value = std::get<bool>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyBool(field.metadata.name.c_str(), value);
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::Int32: {
|
||||
int value = std::get<int32_t>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyInt(field.metadata.name.c_str(), value, 1);
|
||||
return widgetChanged &&
|
||||
ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(static_cast<int32_t>(value)), undoManager);
|
||||
}
|
||||
case ScriptFieldType::UInt64: {
|
||||
uint64_t value = std::get<uint64_t>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
ImGui::SetNextItemWidth(layout.controlWidth);
|
||||
return ImGui::InputScalar("##Value", ImGuiDataType_U64, &value);
|
||||
});
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::String:
|
||||
return RenderStringScriptFieldEditor(scriptComponent, field, undoManager);
|
||||
case ScriptFieldType::Vector2: {
|
||||
::XCEngine::Math::Vector2 value = std::get<::XCEngine::Math::Vector2>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyVec2(field.metadata.name.c_str(), value, 0.0f, 0.1f);
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::Vector3: {
|
||||
::XCEngine::Math::Vector3 value = std::get<::XCEngine::Math::Vector3>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyVec3Input(field.metadata.name.c_str(), value, 0.1f);
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::Vector4: {
|
||||
::XCEngine::Math::Vector4 value = std::get<::XCEngine::Math::Vector4>(field.value);
|
||||
const bool widgetChanged = UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
ImGui::SetNextItemWidth(layout.controlWidth);
|
||||
return ImGui::DragFloat4("##Value", &value.x, 0.1f);
|
||||
});
|
||||
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
||||
}
|
||||
case ScriptFieldType::GameObject:
|
||||
return RenderGameObjectScriptFieldEditor(scriptComponent, field, undoManager);
|
||||
case ScriptFieldType::None:
|
||||
default:
|
||||
RenderReadOnlyScriptField(field);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderStringScriptFieldEditor(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
IUndoManager* undoManager) {
|
||||
StringFieldEditState& editState =
|
||||
m_stringFieldStates[scriptComponent.GetScriptComponentUUID()][field.metadata.name];
|
||||
const std::string currentValue = std::get<std::string>(field.value);
|
||||
|
||||
if (!editState.initialized || (!editState.editing && editState.lastSyncedValue != currentValue)) {
|
||||
SyncStringFieldEditState(editState, currentValue);
|
||||
}
|
||||
|
||||
bool isEditing = false;
|
||||
const bool widgetChanged = UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
ImGui::SetNextItemWidth(layout.controlWidth);
|
||||
const bool changed = ImGui::InputText("##Value", editState.buffer.data(), editState.buffer.size());
|
||||
isEditing = ImGui::IsItemActive();
|
||||
return changed;
|
||||
});
|
||||
editState.editing = isEditing;
|
||||
|
||||
if (!widgetChanged) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string editedValue(editState.buffer.data());
|
||||
if (!ApplyScriptFieldWrite(
|
||||
scriptComponent,
|
||||
field,
|
||||
::XCEngine::Scripting::ScriptFieldValue(editedValue),
|
||||
undoManager)) {
|
||||
SyncStringFieldEditState(editState, currentValue);
|
||||
return false;
|
||||
}
|
||||
|
||||
editState.lastSyncedValue = editedValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderGameObjectScriptFieldEditor(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
IUndoManager* undoManager) {
|
||||
using namespace ::XCEngine::Scripting;
|
||||
|
||||
const GameObjectReference currentReference = std::get<GameObjectReference>(field.value);
|
||||
const auto& sceneRoots = Application::Get().GetEditorContext().GetSceneManager().GetRootEntities();
|
||||
const ::XCEngine::Components::GameObject::ID currentGameObjectId =
|
||||
ScriptComponentEditorUtils::FindGameObjectIdByUuid(sceneRoots, currentReference.gameObjectUUID);
|
||||
|
||||
UI::ReferencePickerOptions pickerOptions;
|
||||
pickerOptions.popupTitle = "Select GameObject";
|
||||
pickerOptions.emptyHint = "None";
|
||||
pickerOptions.searchHint = "Search";
|
||||
pickerOptions.noSceneText = "No scene objects.";
|
||||
pickerOptions.showAssetsTab = false;
|
||||
pickerOptions.showSceneTab = true;
|
||||
|
||||
GameObjectReference pendingReference = currentReference;
|
||||
const bool widgetChanged = UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
const UI::ReferencePickerInteraction interaction =
|
||||
UI::DrawReferencePickerControl(
|
||||
std::string(),
|
||||
currentGameObjectId,
|
||||
pickerOptions,
|
||||
layout.controlWidth);
|
||||
|
||||
if (interaction.clearRequested) {
|
||||
pendingReference = GameObjectReference{};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (interaction.assignedSceneObjectId == ::XCEngine::Components::GameObject::INVALID_ID ||
|
||||
interaction.assignedSceneObjectId == currentGameObjectId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
::XCEngine::Components::GameObject* assignedGameObject =
|
||||
Application::Get().GetEditorContext().GetSceneManager().GetEntity(interaction.assignedSceneObjectId);
|
||||
if (!assignedGameObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pendingReference = GameObjectReference{assignedGameObject->GetUUID()};
|
||||
return true;
|
||||
});
|
||||
|
||||
return widgetChanged &&
|
||||
ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(pendingReference), undoManager);
|
||||
}
|
||||
|
||||
void RenderReadOnlyScriptField(const ::XCEngine::Scripting::ScriptFieldSnapshot& field) const {
|
||||
const std::string valueText = DescribeScriptFieldValue(field.metadata.type, field.value);
|
||||
UI::DrawPropertyRow(
|
||||
field.metadata.name.c_str(),
|
||||
UI::InspectorPropertyLayout(),
|
||||
[&](const UI::PropertyLayoutMetrics& layout) {
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetTextLineHeight());
|
||||
ImGui::TextDisabled("%s", valueText.c_str());
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
bool ApplyScriptFieldWrite(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
const ::XCEngine::Scripting::ScriptFieldValue& value,
|
||||
IUndoManager* undoManager) const {
|
||||
std::vector<::XCEngine::Scripting::ScriptFieldWriteResult> results;
|
||||
if (undoManager) {
|
||||
undoManager->BeginInteractiveChange("Modify Script Field");
|
||||
}
|
||||
|
||||
const bool applied = ::XCEngine::Scripting::ScriptEngine::Get().ApplyScriptFieldWrites(
|
||||
&scriptComponent,
|
||||
{ ::XCEngine::Scripting::ScriptFieldWriteRequest{field.metadata.name, field.metadata.type, value} },
|
||||
results);
|
||||
|
||||
if (!applied || results.empty() ||
|
||||
results.front().status != ::XCEngine::Scripting::ScriptFieldWriteStatus::Applied) {
|
||||
if (undoManager && undoManager->HasPendingInteractiveChange()) {
|
||||
undoManager->CancelInteractiveChange();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClearScriptFieldOverride(
|
||||
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
||||
IUndoManager* undoManager) const {
|
||||
std::vector<::XCEngine::Scripting::ScriptFieldClearResult> results;
|
||||
if (undoManager) {
|
||||
undoManager->BeginInteractiveChange("Clear Script Field Override");
|
||||
}
|
||||
|
||||
const bool cleared = ::XCEngine::Scripting::ScriptEngine::Get().ClearScriptFieldOverrides(
|
||||
&scriptComponent,
|
||||
{ ::XCEngine::Scripting::ScriptFieldClearRequest{field.metadata.name} },
|
||||
results);
|
||||
|
||||
if (!cleared || results.empty() ||
|
||||
results.front().status != ::XCEngine::Scripting::ScriptFieldClearStatus::Applied) {
|
||||
if (undoManager && undoManager->HasPendingInteractiveChange()) {
|
||||
undoManager->CancelInteractiveChange();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void SyncStringFieldEditState(StringFieldEditState& editState, const std::string& value) {
|
||||
editState.buffer.fill('\0');
|
||||
const size_t copyLength = (std::min)(value.size(), editState.buffer.size() - 1);
|
||||
if (copyLength > 0) {
|
||||
std::memcpy(editState.buffer.data(), value.data(), copyLength);
|
||||
}
|
||||
editState.buffer[copyLength] = '\0';
|
||||
editState.lastSyncedValue = value;
|
||||
editState.initialized = true;
|
||||
}
|
||||
|
||||
static std::string DescribeScriptFieldValue(
|
||||
::XCEngine::Scripting::ScriptFieldType type,
|
||||
const ::XCEngine::Scripting::ScriptFieldValue& value) {
|
||||
if (type == ::XCEngine::Scripting::ScriptFieldType::GameObject) {
|
||||
const auto reference = std::get<::XCEngine::Scripting::GameObjectReference>(value);
|
||||
if (reference.gameObjectUUID == 0) {
|
||||
return "None";
|
||||
}
|
||||
|
||||
return "GameObject (" + std::to_string(reference.gameObjectUUID) + ")";
|
||||
}
|
||||
|
||||
return ::XCEngine::Scripting::SerializeScriptFieldValue(type, value);
|
||||
}
|
||||
|
||||
std::unordered_map<uint64_t, std::unordered_map<std::string, StringFieldEditState>> m_stringFieldStates;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
125
editor/src/ComponentEditors/ScriptComponentEditorUtils.h
Normal file
@@ -0,0 +1,125 @@
|
||||
#pragma once
|
||||
|
||||
#include "Scripting/EditorScriptRuntimeStatus.h"
|
||||
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Scripting/IScriptRuntime.h>
|
||||
#include <XCEngine/Scripting/ScriptComponent.h>
|
||||
#include <XCEngine/Scripting/ScriptField.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace ScriptComponentEditorUtils {
|
||||
|
||||
inline std::string BuildScriptClassDisplayName(const ::XCEngine::Scripting::ScriptClassDescriptor& descriptor) {
|
||||
const std::string fullName = descriptor.GetFullName();
|
||||
if (descriptor.assemblyName.empty() || descriptor.assemblyName == "GameScripts") {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
return fullName + " (" + descriptor.assemblyName + ")";
|
||||
}
|
||||
|
||||
inline std::string BuildScriptClassDisplayName(const ::XCEngine::Scripting::ScriptComponent& component) {
|
||||
if (!component.HasScriptClass()) {
|
||||
return "None";
|
||||
}
|
||||
|
||||
return BuildScriptClassDisplayName(::XCEngine::Scripting::ScriptClassDescriptor{
|
||||
component.GetAssemblyName(),
|
||||
component.GetNamespaceName(),
|
||||
component.GetClassName()
|
||||
});
|
||||
}
|
||||
|
||||
inline ::XCEngine::Components::GameObject::ID FindGameObjectIdByUuidRecursive(
|
||||
::XCEngine::Components::GameObject* gameObject,
|
||||
uint64_t uuid) {
|
||||
if (!gameObject || uuid == 0) {
|
||||
return ::XCEngine::Components::GameObject::INVALID_ID;
|
||||
}
|
||||
|
||||
if (gameObject->GetUUID() == uuid) {
|
||||
return gameObject->GetID();
|
||||
}
|
||||
|
||||
for (::XCEngine::Components::GameObject* child : gameObject->GetChildren()) {
|
||||
const ::XCEngine::Components::GameObject::ID childId =
|
||||
FindGameObjectIdByUuidRecursive(child, uuid);
|
||||
if (childId != ::XCEngine::Components::GameObject::INVALID_ID) {
|
||||
return childId;
|
||||
}
|
||||
}
|
||||
|
||||
return ::XCEngine::Components::GameObject::INVALID_ID;
|
||||
}
|
||||
|
||||
inline ::XCEngine::Components::GameObject::ID FindGameObjectIdByUuid(
|
||||
const std::vector<::XCEngine::Components::GameObject*>& roots,
|
||||
uint64_t uuid) {
|
||||
for (::XCEngine::Components::GameObject* root : roots) {
|
||||
const ::XCEngine::Components::GameObject::ID gameObjectId =
|
||||
FindGameObjectIdByUuidRecursive(root, uuid);
|
||||
if (gameObjectId != ::XCEngine::Components::GameObject::INVALID_ID) {
|
||||
return gameObjectId;
|
||||
}
|
||||
}
|
||||
|
||||
return ::XCEngine::Components::GameObject::INVALID_ID;
|
||||
}
|
||||
|
||||
inline bool CanEditScriptField(
|
||||
::XCEngine::Scripting::ScriptFieldClassStatus classStatus,
|
||||
const ::XCEngine::Scripting::ScriptFieldSnapshot& field) {
|
||||
return classStatus != ::XCEngine::Scripting::ScriptFieldClassStatus::Available || field.declaredInClass;
|
||||
}
|
||||
|
||||
inline bool CanClearScriptFieldOverride(const ::XCEngine::Scripting::ScriptFieldSnapshot& field) {
|
||||
return field.hasStoredValue || field.valueSource == ::XCEngine::Scripting::ScriptFieldValueSource::ManagedValue;
|
||||
}
|
||||
|
||||
inline std::string BuildScriptFieldIssueText(const ::XCEngine::Scripting::ScriptFieldSnapshot& field) {
|
||||
switch (field.issue) {
|
||||
case ::XCEngine::Scripting::ScriptFieldIssue::StoredOnly:
|
||||
return "Stored override is not declared by the selected script.";
|
||||
case ::XCEngine::Scripting::ScriptFieldIssue::TypeMismatch:
|
||||
return "Stored override type is " +
|
||||
::XCEngine::Scripting::ScriptFieldTypeToString(field.storedType) +
|
||||
", but the script field expects " +
|
||||
::XCEngine::Scripting::ScriptFieldTypeToString(field.metadata.type) + ".";
|
||||
case ::XCEngine::Scripting::ScriptFieldIssue::None:
|
||||
default:
|
||||
return std::string();
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string BuildScriptRuntimeUnavailableHint(const EditorScriptRuntimeStatus& status) {
|
||||
if (!status.statusMessage.empty()) {
|
||||
return status.statusMessage;
|
||||
}
|
||||
|
||||
if (!status.backendEnabled) {
|
||||
return "This editor build does not include Mono scripting support.";
|
||||
}
|
||||
|
||||
if (!status.assemblyDirectory.empty()) {
|
||||
return "No script assemblies are currently loaded from " + status.assemblyDirectory + ".";
|
||||
}
|
||||
|
||||
return "No script assemblies are currently loaded.";
|
||||
}
|
||||
|
||||
inline bool CanReloadScriptRuntime(const EditorScriptRuntimeStatus& status) {
|
||||
return status.backendEnabled && !status.assemblyDirectory.empty();
|
||||
}
|
||||
|
||||
inline bool CanRebuildScriptAssemblies(const EditorScriptRuntimeStatus& status) {
|
||||
return status.backendEnabled && !status.assemblyDirectory.empty();
|
||||
}
|
||||
|
||||
} // namespace ScriptComponentEditorUtils
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
#include "EditorRuntimeMode.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
@@ -67,6 +72,19 @@ struct PlayModeResumeRequestedEvent {
|
||||
struct PlayModeStepRequestedEvent {
|
||||
};
|
||||
|
||||
struct GameViewInputFrameEvent {
|
||||
static constexpr size_t KeyStateCount = 256u;
|
||||
static constexpr size_t MouseButtonStateCount = 5u;
|
||||
|
||||
bool hovered = false;
|
||||
bool focused = false;
|
||||
Math::Vector2 mousePosition = Math::Vector2::Zero();
|
||||
Math::Vector2 mouseDelta = Math::Vector2::Zero();
|
||||
float mouseWheel = 0.0f;
|
||||
std::array<bool, KeyStateCount> keyDown = {};
|
||||
std::array<bool, MouseButtonStateCount> mouseButtonDown = {};
|
||||
};
|
||||
|
||||
struct EditorModeChangedEvent {
|
||||
EditorRuntimeMode oldMode = EditorRuntimeMode::Edit;
|
||||
EditorRuntimeMode newMode = EditorRuntimeMode::Edit;
|
||||
|
||||
@@ -82,13 +82,20 @@ public:
|
||||
void Publish(const T& event) {
|
||||
static_assert(sizeof(T) > 0, "Event type must be defined");
|
||||
uint32_t typeId = EventTypeId<T>::Get();
|
||||
|
||||
std::shared_lock<std::shared_mutex> lock(m_mutex);
|
||||
auto it = m_handlers.find(typeId);
|
||||
if (it != m_handlers.end()) {
|
||||
for (const auto& entry : it->second) {
|
||||
entry.handler(&event);
|
||||
|
||||
std::vector<HandlerEntry> handlers;
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(m_mutex);
|
||||
auto it = m_handlers.find(typeId);
|
||||
if (it == m_handlers.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
handlers = it->second;
|
||||
}
|
||||
|
||||
for (const auto& entry : handlers) {
|
||||
entry.handler(&event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
@@ -10,6 +11,13 @@ namespace Editor {
|
||||
|
||||
class IProjectManager {
|
||||
public:
|
||||
struct SceneAssetReferenceMigrationReport {
|
||||
size_t scannedSceneCount = 0;
|
||||
size_t migratedSceneCount = 0;
|
||||
size_t unchangedSceneCount = 0;
|
||||
size_t failedSceneCount = 0;
|
||||
};
|
||||
|
||||
virtual ~IProjectManager() = default;
|
||||
|
||||
virtual const std::vector<AssetItemPtr>& GetCurrentItems() const = 0;
|
||||
@@ -39,6 +47,7 @@ public:
|
||||
virtual bool DeleteItem(const std::string& fullPath) = 0;
|
||||
virtual bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) = 0;
|
||||
virtual bool RenameItem(const std::string& sourceFullPath, const std::string& newName) = 0;
|
||||
virtual SceneAssetReferenceMigrationReport MigrateSceneAssetReferences() = 0;
|
||||
|
||||
virtual const std::string& GetProjectPath() const = 0;
|
||||
};
|
||||
|
||||
@@ -6,9 +6,25 @@
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "Core/IUndoManager.h"
|
||||
|
||||
#include <XCEngine/Input/InputManager.h>
|
||||
#include <XCEngine/Scripting/ScriptEngine.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsModifierKeyDown(const GameViewInputFrameEvent& input, XCEngine::Input::KeyCode key) {
|
||||
const size_t index = static_cast<size_t>(key);
|
||||
return index < input.keyDown.size() && input.keyDown[index];
|
||||
}
|
||||
|
||||
bool IsGameViewInputActive(const GameViewInputFrameEvent& input) {
|
||||
return input.hovered || input.focused;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PlaySessionController::Attach(IEditorContext& context) {
|
||||
if (m_playStartRequestedHandlerId == 0) {
|
||||
m_playStartRequestedHandlerId = context.GetEventBus().Subscribe<PlayModeStartRequestedEvent>(
|
||||
@@ -44,6 +60,14 @@ void PlaySessionController::Attach(IEditorContext& context) {
|
||||
StepPlay(context);
|
||||
});
|
||||
}
|
||||
|
||||
if (m_gameViewInputFrameHandlerId == 0) {
|
||||
m_gameViewInputFrameHandlerId = context.GetEventBus().Subscribe<GameViewInputFrameEvent>(
|
||||
[this](const GameViewInputFrameEvent& event) {
|
||||
m_pendingGameViewInput = event;
|
||||
m_hasPendingGameViewInput = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void PlaySessionController::Detach(IEditorContext& context) {
|
||||
@@ -73,6 +97,13 @@ void PlaySessionController::Detach(IEditorContext& context) {
|
||||
context.GetEventBus().Unsubscribe<PlayModeStepRequestedEvent>(m_playStepRequestedHandlerId);
|
||||
m_playStepRequestedHandlerId = 0;
|
||||
}
|
||||
|
||||
if (m_gameViewInputFrameHandlerId != 0) {
|
||||
context.GetEventBus().Unsubscribe<GameViewInputFrameEvent>(m_gameViewInputFrameHandlerId);
|
||||
m_gameViewInputFrameHandlerId = 0;
|
||||
}
|
||||
|
||||
ResetRuntimeInputBridge();
|
||||
}
|
||||
|
||||
void PlaySessionController::Update(IEditorContext& context, float deltaTime) {
|
||||
@@ -81,6 +112,7 @@ void PlaySessionController::Update(IEditorContext& context, float deltaTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyGameViewInputFrame(deltaTime);
|
||||
m_runtimeLoop.Tick(deltaTime);
|
||||
}
|
||||
|
||||
@@ -104,6 +136,10 @@ bool PlaySessionController::StartPlay(IEditorContext& context) {
|
||||
}
|
||||
|
||||
sceneManager.SetSceneDocumentDirtyTrackingEnabled(false);
|
||||
XCEngine::Scripting::ScriptEngine::Get().SetRuntimeFixedDeltaTime(m_runtimeLoop.GetSettings().fixedDeltaTime);
|
||||
ResetRuntimeInputBridge();
|
||||
XCEngine::Input::InputManager::Get().Shutdown();
|
||||
XCEngine::Input::InputManager::Get().Initialize(nullptr);
|
||||
m_runtimeLoop.Start(sceneManager.GetScene());
|
||||
context.GetUndoManager().ClearHistory();
|
||||
context.SetRuntimeMode(EditorRuntimeMode::Play);
|
||||
@@ -118,6 +154,8 @@ bool PlaySessionController::StopPlay(IEditorContext& context) {
|
||||
|
||||
auto& sceneManager = context.GetSceneManager();
|
||||
m_runtimeLoop.Stop();
|
||||
ResetRuntimeInputBridge();
|
||||
XCEngine::Input::InputManager::Get().Shutdown();
|
||||
sceneManager.SetSceneDocumentDirtyTrackingEnabled(true);
|
||||
|
||||
if (!sceneManager.RestoreSceneSnapshot(m_editorSnapshot)) {
|
||||
@@ -162,5 +200,83 @@ bool PlaySessionController::StepPlay(IEditorContext& context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlaySessionController::ResetRuntimeInputBridge() {
|
||||
m_pendingGameViewInput = {};
|
||||
m_appliedGameViewInput = {};
|
||||
m_hasPendingGameViewInput = false;
|
||||
}
|
||||
|
||||
void PlaySessionController::ApplyGameViewInputFrame(float deltaTime) {
|
||||
using XCEngine::Input::InputManager;
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::Input::MouseButton;
|
||||
|
||||
InputManager& inputManager = InputManager::Get();
|
||||
inputManager.Update(deltaTime);
|
||||
|
||||
const GameViewInputFrameEvent input = m_hasPendingGameViewInput
|
||||
? m_pendingGameViewInput
|
||||
: GameViewInputFrameEvent{};
|
||||
m_hasPendingGameViewInput = false;
|
||||
|
||||
const bool inputActive = IsGameViewInputActive(input);
|
||||
const bool alt = inputActive &&
|
||||
(IsModifierKeyDown(input, KeyCode::LeftAlt) || IsModifierKeyDown(input, KeyCode::RightAlt));
|
||||
const bool ctrl = inputActive &&
|
||||
(IsModifierKeyDown(input, KeyCode::LeftCtrl) || IsModifierKeyDown(input, KeyCode::RightCtrl));
|
||||
const bool shift = inputActive &&
|
||||
(IsModifierKeyDown(input, KeyCode::LeftShift) || IsModifierKeyDown(input, KeyCode::RightShift));
|
||||
|
||||
for (size_t index = 0; index < input.keyDown.size(); ++index) {
|
||||
const bool wasDown = m_appliedGameViewInput.keyDown[index];
|
||||
const bool isDown = inputActive && input.keyDown[index];
|
||||
if (wasDown == isDown) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const KeyCode key = static_cast<KeyCode>(index);
|
||||
if (isDown) {
|
||||
inputManager.ProcessKeyDown(key, false, alt, ctrl, shift, false);
|
||||
} else {
|
||||
inputManager.ProcessKeyUp(key, alt, ctrl, shift, false);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < input.mouseButtonDown.size(); ++index) {
|
||||
const bool wasDown = m_appliedGameViewInput.mouseButtonDown[index];
|
||||
const bool isDown = inputActive && input.mouseButtonDown[index];
|
||||
if (wasDown == isDown) {
|
||||
continue;
|
||||
}
|
||||
|
||||
inputManager.ProcessMouseButton(
|
||||
static_cast<MouseButton>(index),
|
||||
isDown,
|
||||
static_cast<int>(input.mousePosition.x),
|
||||
static_cast<int>(input.mousePosition.y));
|
||||
}
|
||||
|
||||
if (inputActive &&
|
||||
(input.mousePosition != m_appliedGameViewInput.mousePosition || input.mouseDelta != XCEngine::Math::Vector2::Zero())) {
|
||||
inputManager.ProcessMouseMove(
|
||||
static_cast<int>(input.mousePosition.x),
|
||||
static_cast<int>(input.mousePosition.y),
|
||||
static_cast<int>(input.mouseDelta.x),
|
||||
static_cast<int>(input.mouseDelta.y));
|
||||
}
|
||||
|
||||
if (inputActive && input.mouseWheel != 0.0f) {
|
||||
inputManager.ProcessMouseWheel(
|
||||
input.mouseWheel,
|
||||
static_cast<int>(input.mousePosition.x),
|
||||
static_cast<int>(input.mousePosition.y));
|
||||
}
|
||||
|
||||
m_appliedGameViewInput = {};
|
||||
if (inputActive) {
|
||||
m_appliedGameViewInput = input;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
#include "EditorRuntimeMode.h"
|
||||
#include "SceneSnapshot.h"
|
||||
|
||||
#include "Core/EditorEvents.h"
|
||||
|
||||
#include <XCEngine/Scene/RuntimeLoop.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -26,12 +29,19 @@ public:
|
||||
bool StepPlay(IEditorContext& context);
|
||||
|
||||
private:
|
||||
void ResetRuntimeInputBridge();
|
||||
void ApplyGameViewInputFrame(float deltaTime);
|
||||
|
||||
uint64_t m_playStartRequestedHandlerId = 0;
|
||||
uint64_t m_playStopRequestedHandlerId = 0;
|
||||
uint64_t m_playPauseRequestedHandlerId = 0;
|
||||
uint64_t m_playResumeRequestedHandlerId = 0;
|
||||
uint64_t m_playStepRequestedHandlerId = 0;
|
||||
uint64_t m_gameViewInputFrameHandlerId = 0;
|
||||
SceneSnapshot m_editorSnapshot = {};
|
||||
GameViewInputFrameEvent m_pendingGameViewInput = {};
|
||||
GameViewInputFrameEvent m_appliedGameViewInput = {};
|
||||
bool m_hasPendingGameViewInput = false;
|
||||
XCEngine::Components::RuntimeLoop m_runtimeLoop;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
#include "ProjectManager.h"
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cwctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <initializer_list>
|
||||
#include <string_view>
|
||||
#include <windows.h>
|
||||
@@ -212,6 +217,27 @@ bool CanPreviewImageAssetExtension(std::wstring_view extension) {
|
||||
});
|
||||
}
|
||||
|
||||
bool IsSceneAssetFile(const fs::path& path) {
|
||||
if (!fs::is_regular_file(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring extension = path.extension().wstring();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower);
|
||||
return extension == L".xc";
|
||||
}
|
||||
|
||||
std::string ReadFileText(const fs::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << input.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<AssetItemPtr>& ProjectManager::GetCurrentItems() const {
|
||||
@@ -496,6 +522,89 @@ bool ProjectManager::RenameItem(const std::string& sourceFullPath, const std::st
|
||||
}
|
||||
}
|
||||
|
||||
IProjectManager::SceneAssetReferenceMigrationReport ProjectManager::MigrateSceneAssetReferences() {
|
||||
IProjectManager::SceneAssetReferenceMigrationReport report;
|
||||
if (m_projectPath.empty()) {
|
||||
return report;
|
||||
}
|
||||
|
||||
const fs::path assetsPath = fs::path(Utf8PathToWstring(m_projectPath)) / L"Assets";
|
||||
if (!fs::exists(assetsPath) || !fs::is_directory(assetsPath)) {
|
||||
return report;
|
||||
}
|
||||
|
||||
auto& logger = ::XCEngine::Debug::Logger::Get();
|
||||
::XCEngine::Resources::ResourceManager& resourceManager = ::XCEngine::Resources::ResourceManager::Get();
|
||||
resourceManager.Initialize();
|
||||
|
||||
const std::string previousRoot = resourceManager.GetResourceRoot().CStr();
|
||||
const bool restoreResourceRoot = previousRoot != m_projectPath;
|
||||
if (restoreResourceRoot) {
|
||||
resourceManager.SetResourceRoot(m_projectPath.c_str());
|
||||
}
|
||||
|
||||
try {
|
||||
for (const fs::directory_entry& entry : fs::recursive_directory_iterator(assetsPath)) {
|
||||
if (!IsSceneAssetFile(entry.path())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
++report.scannedSceneCount;
|
||||
const fs::path scenePath = entry.path();
|
||||
const std::string scenePathUtf8 = WstringPathToUtf8(scenePath.wstring());
|
||||
|
||||
try {
|
||||
const std::string before = ReadFileText(scenePath);
|
||||
::XCEngine::Components::Scene scene;
|
||||
{
|
||||
::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope(resourceManager);
|
||||
scene.Load(scenePathUtf8);
|
||||
}
|
||||
|
||||
scene.Save(scenePathUtf8);
|
||||
const std::string after = ReadFileText(scenePath);
|
||||
if (after != before) {
|
||||
++report.migratedSceneCount;
|
||||
} else {
|
||||
++report.unchangedSceneCount;
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
++report.failedSceneCount;
|
||||
logger.Error(
|
||||
::XCEngine::Debug::LogCategory::FileSystem,
|
||||
("Failed to migrate scene asset references: " + scenePathUtf8 + " - " + exception.what()).c_str());
|
||||
} catch (...) {
|
||||
++report.failedSceneCount;
|
||||
logger.Error(
|
||||
::XCEngine::Debug::LogCategory::FileSystem,
|
||||
("Failed to migrate scene asset references: " + scenePathUtf8 + " - unknown error").c_str());
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
logger.Error(
|
||||
::XCEngine::Debug::LogCategory::FileSystem,
|
||||
("Scene asset reference migration aborted: " + std::string(exception.what())).c_str());
|
||||
} catch (...) {
|
||||
logger.Error(
|
||||
::XCEngine::Debug::LogCategory::FileSystem,
|
||||
"Scene asset reference migration aborted: unknown error");
|
||||
}
|
||||
|
||||
if (restoreResourceRoot) {
|
||||
resourceManager.SetResourceRoot(previousRoot.c_str());
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
::XCEngine::Debug::LogCategory::FileSystem,
|
||||
("Scene asset reference migration finished. scanned=" + std::to_string(report.scannedSceneCount) +
|
||||
" migrated=" + std::to_string(report.migratedSceneCount) +
|
||||
" unchanged=" + std::to_string(report.unchangedSceneCount) +
|
||||
" failed=" + std::to_string(report.failedSceneCount)).c_str());
|
||||
|
||||
RefreshCurrentFolder();
|
||||
return report;
|
||||
}
|
||||
|
||||
AssetItemPtr ProjectManager::FindCurrentItemByPath(const std::string& fullPath) const {
|
||||
const int index = FindCurrentItemIndex(fullPath);
|
||||
if (index < 0) {
|
||||
|
||||
@@ -38,6 +38,7 @@ public:
|
||||
bool DeleteItem(const std::string& fullPath) override;
|
||||
bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) override;
|
||||
bool RenameItem(const std::string& sourceFullPath, const std::string& newName) override;
|
||||
SceneAssetReferenceMigrationReport MigrateSceneAssetReferences() override;
|
||||
|
||||
const std::string& GetProjectPath() const override { return m_projectPath; }
|
||||
|
||||
|
||||
372
editor/src/Scripting/EditorScriptAssemblyBuilder.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
#include "Scripting/EditorScriptAssemblyBuilder.h"
|
||||
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "Scripting/EditorScriptAssemblyBuilderUtils.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef XCENGINE_EDITOR_REPO_ROOT
|
||||
#define XCENGINE_EDITOR_REPO_ROOT ""
|
||||
#endif
|
||||
|
||||
#ifndef XCENGINE_EDITOR_MONO_ROOT_DIR
|
||||
#define XCENGINE_EDITOR_MONO_ROOT_DIR ""
|
||||
#endif
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Scripting {
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path GetFallbackRepositoryRoot() {
|
||||
std::filesystem::path repoRoot = std::filesystem::path(Platform::Utf8ToWide(Platform::GetExecutableDirectoryUtf8()));
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (repoRoot.has_parent_path()) {
|
||||
repoRoot = repoRoot.parent_path();
|
||||
}
|
||||
}
|
||||
return repoRoot.lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path GetRepositoryRoot() {
|
||||
const std::string configuredRoot = XCENGINE_EDITOR_REPO_ROOT;
|
||||
if (!configuredRoot.empty()) {
|
||||
return std::filesystem::path(Platform::Utf8ToWide(configuredRoot)).lexically_normal();
|
||||
}
|
||||
|
||||
return GetFallbackRepositoryRoot();
|
||||
}
|
||||
|
||||
std::filesystem::path FindBundledMonoRootDirectory(const std::filesystem::path& repositoryRoot) {
|
||||
std::error_code ec;
|
||||
if (repositoryRoot.empty() || !std::filesystem::exists(repositoryRoot, ec)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
for (std::filesystem::directory_iterator it(repositoryRoot, ec), end; it != end && !ec; it.increment(ec)) {
|
||||
if (ec || !it->is_directory(ec)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::filesystem::path candidate =
|
||||
it->path() / "Fermion" / "Fermion" / "external" / "mono";
|
||||
if (std::filesystem::exists(candidate / "binary" / "mscorlib.dll", ec)) {
|
||||
return candidate.lexically_normal();
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path GetMonoRootDirectory() {
|
||||
const std::filesystem::path repositoryRoot = GetRepositoryRoot();
|
||||
const std::filesystem::path bundledMonoRoot = FindBundledMonoRootDirectory(repositoryRoot);
|
||||
if (!bundledMonoRoot.empty()) {
|
||||
return bundledMonoRoot;
|
||||
}
|
||||
|
||||
const std::string configuredRoot = XCENGINE_EDITOR_MONO_ROOT_DIR;
|
||||
if (!configuredRoot.empty()) {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path configuredPath =
|
||||
std::filesystem::path(Platform::Utf8ToWide(configuredRoot)).lexically_normal();
|
||||
if (std::filesystem::exists(configuredPath / "binary" / "mscorlib.dll", ec)) {
|
||||
return configuredPath;
|
||||
}
|
||||
}
|
||||
|
||||
return (repositoryRoot / "managed" / "mono").lexically_normal();
|
||||
}
|
||||
|
||||
std::wstring QuotePath(const std::filesystem::path& path) {
|
||||
return L"\"" + path.wstring() + L"\"";
|
||||
}
|
||||
|
||||
bool FindExecutableOnPath(const wchar_t* executableName, std::filesystem::path& outPath) {
|
||||
DWORD requiredLength = SearchPathW(nullptr, executableName, nullptr, 0, nullptr, nullptr);
|
||||
if (requiredLength == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring buffer(requiredLength, L'\0');
|
||||
const DWORD resolvedLength = SearchPathW(
|
||||
nullptr,
|
||||
executableName,
|
||||
nullptr,
|
||||
static_cast<DWORD>(buffer.size()),
|
||||
buffer.data(),
|
||||
nullptr);
|
||||
if (resolvedLength == 0 || resolvedLength >= buffer.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer.resize(resolvedLength);
|
||||
outPath = std::filesystem::path(buffer).lexically_normal();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunProcessAndCapture(
|
||||
const std::filesystem::path& executablePath,
|
||||
const std::wstring& arguments,
|
||||
const std::filesystem::path& workingDirectory,
|
||||
DWORD& outExitCode,
|
||||
std::string& outOutput) {
|
||||
SECURITY_ATTRIBUTES securityAttributes = {};
|
||||
securityAttributes.nLength = sizeof(securityAttributes);
|
||||
securityAttributes.bInheritHandle = TRUE;
|
||||
|
||||
HANDLE readPipe = nullptr;
|
||||
HANDLE writePipe = nullptr;
|
||||
if (!CreatePipe(&readPipe, &writePipe, &securityAttributes, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0);
|
||||
|
||||
STARTUPINFOW startupInfo = {};
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
startupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||
startupInfo.hStdOutput = writePipe;
|
||||
startupInfo.hStdError = writePipe;
|
||||
|
||||
PROCESS_INFORMATION processInfo = {};
|
||||
std::wstring commandLine = QuotePath(executablePath) + L" " + arguments;
|
||||
std::vector<wchar_t> commandLineBuffer(commandLine.begin(), commandLine.end());
|
||||
commandLineBuffer.push_back(L'\0');
|
||||
|
||||
const wchar_t* currentDirectory = workingDirectory.empty() ? nullptr : workingDirectory.c_str();
|
||||
const BOOL created = CreateProcessW(
|
||||
nullptr,
|
||||
commandLineBuffer.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
TRUE,
|
||||
CREATE_NO_WINDOW,
|
||||
nullptr,
|
||||
currentDirectory,
|
||||
&startupInfo,
|
||||
&processInfo);
|
||||
|
||||
CloseHandle(writePipe);
|
||||
writePipe = nullptr;
|
||||
|
||||
if (!created) {
|
||||
CloseHandle(readPipe);
|
||||
return false;
|
||||
}
|
||||
|
||||
char buffer[4096] = {};
|
||||
DWORD bytesRead = 0;
|
||||
while (ReadFile(readPipe, buffer, sizeof(buffer), &bytesRead, nullptr) && bytesRead > 0) {
|
||||
outOutput.append(buffer, bytesRead);
|
||||
}
|
||||
|
||||
WaitForSingleObject(processInfo.hProcess, INFINITE);
|
||||
GetExitCodeProcess(processInfo.hProcess, &outExitCode);
|
||||
|
||||
CloseHandle(processInfo.hThread);
|
||||
CloseHandle(processInfo.hProcess);
|
||||
CloseHandle(readPipe);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::wstring BuildCompilerArguments(
|
||||
const std::filesystem::path& outputPath,
|
||||
const std::vector<std::filesystem::path>& referencePaths,
|
||||
const std::vector<std::filesystem::path>& sourcePaths) {
|
||||
std::wstring arguments = L"/nologo /target:library /langversion:latest /nostdlib+ ";
|
||||
arguments += L"/out:" + QuotePath(outputPath);
|
||||
|
||||
for (const std::filesystem::path& referencePath : referencePaths) {
|
||||
arguments += L" /reference:" + QuotePath(referencePath);
|
||||
}
|
||||
|
||||
for (const std::filesystem::path& sourcePath : sourcePaths) {
|
||||
arguments += L" " + QuotePath(sourcePath);
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
EditorScriptAssemblyBuildResult BuildFailure(const std::string& message) {
|
||||
return EditorScriptAssemblyBuildResult{false, message};
|
||||
}
|
||||
|
||||
bool RunCSharpCompiler(
|
||||
const std::filesystem::path& dotnetExecutable,
|
||||
const std::filesystem::path& cscDllPath,
|
||||
const std::filesystem::path& workingDirectory,
|
||||
const std::filesystem::path& outputPath,
|
||||
const std::vector<std::filesystem::path>& referencePaths,
|
||||
const std::vector<std::filesystem::path>& sourcePaths,
|
||||
std::string& outError) {
|
||||
std::wstring arguments = QuotePath(cscDllPath);
|
||||
arguments += L" ";
|
||||
arguments += BuildCompilerArguments(outputPath, referencePaths, sourcePaths);
|
||||
|
||||
DWORD exitCode = 0;
|
||||
std::string processOutput;
|
||||
if (!RunProcessAndCapture(dotnetExecutable, arguments, workingDirectory, exitCode, processOutput)) {
|
||||
outError = "Failed to launch dotnet to compile managed script assemblies.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exitCode != 0) {
|
||||
outError = processOutput.empty()
|
||||
? "The C# compiler failed to build managed script assemblies."
|
||||
: processOutput;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorScriptAssemblyBuildResult EditorScriptAssemblyBuilder::RebuildProjectAssemblies(const std::string& projectPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
try {
|
||||
if (projectPath.empty()) {
|
||||
return BuildFailure("Cannot rebuild script assemblies without a loaded project.");
|
||||
}
|
||||
|
||||
const fs::path projectRoot = fs::path(Platform::Utf8ToWide(projectPath)).lexically_normal();
|
||||
const fs::path repositoryRoot = GetRepositoryRoot();
|
||||
const fs::path monoRoot = GetMonoRootDirectory();
|
||||
const fs::path managedRoot = repositoryRoot / "managed";
|
||||
const fs::path scriptCoreSourceRoot = managedRoot / "XCEngine.ScriptCore";
|
||||
const fs::path outputDirectory = projectRoot / "Library" / "ScriptAssemblies";
|
||||
const fs::path generatedDirectory = outputDirectory / "Generated";
|
||||
const fs::path scriptCoreOutputPath = outputDirectory / "XCEngine.ScriptCore.dll";
|
||||
const fs::path gameScriptsOutputPath = outputDirectory / "GameScripts.dll";
|
||||
const fs::path corlibOutputPath = outputDirectory / "mscorlib.dll";
|
||||
const fs::path monoCorlibSourcePath = monoRoot / "binary" / "mscorlib.dll";
|
||||
const fs::path frameworkReferenceDirectory =
|
||||
L"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.7.2";
|
||||
|
||||
std::error_code ec;
|
||||
fs::create_directories(outputDirectory, ec);
|
||||
if (ec) {
|
||||
return BuildFailure("Failed to create the project script assembly directory: " +
|
||||
ScriptBuilderPathToUtf8(outputDirectory));
|
||||
}
|
||||
|
||||
fs::path dotnetExecutable;
|
||||
if (!FindExecutableOnPath(L"dotnet.exe", dotnetExecutable)) {
|
||||
return BuildFailure("dotnet.exe was not found on PATH.");
|
||||
}
|
||||
|
||||
std::string sdkListOutput;
|
||||
DWORD sdkListExitCode = 0;
|
||||
if (!RunProcessAndCapture(dotnetExecutable, L"--list-sdks", projectRoot, sdkListExitCode, sdkListOutput) ||
|
||||
sdkListExitCode != 0) {
|
||||
return BuildFailure("Failed to query installed .NET SDKs with dotnet --list-sdks.");
|
||||
}
|
||||
|
||||
const std::string sdkVersion = ParseLatestDotnetSdkVersion(sdkListOutput);
|
||||
if (sdkVersion.empty()) {
|
||||
return BuildFailure("Failed to resolve a usable .NET SDK version from dotnet --list-sdks.");
|
||||
}
|
||||
|
||||
const fs::path cscDllPath =
|
||||
fs::path(L"C:\\Program Files\\dotnet\\sdk") /
|
||||
fs::path(Platform::Utf8ToWide(sdkVersion)) /
|
||||
"Roslyn" /
|
||||
"bincore" /
|
||||
"csc.dll";
|
||||
if (!fs::exists(cscDllPath, ec)) {
|
||||
return BuildFailure("Roslyn csc.dll was not found: " + ScriptBuilderPathToUtf8(cscDllPath));
|
||||
}
|
||||
|
||||
const std::vector<fs::path> frameworkReferences = {
|
||||
frameworkReferenceDirectory / "mscorlib.dll",
|
||||
frameworkReferenceDirectory / "System.dll",
|
||||
frameworkReferenceDirectory / "System.Core.dll"
|
||||
};
|
||||
for (const fs::path& referencePath : frameworkReferences) {
|
||||
if (!fs::exists(referencePath, ec)) {
|
||||
return BuildFailure("Required .NET Framework reference is missing: " +
|
||||
ScriptBuilderPathToUtf8(referencePath));
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs::exists(monoCorlibSourcePath, ec)) {
|
||||
return BuildFailure("Mono corlib was not found: " + ScriptBuilderPathToUtf8(monoCorlibSourcePath));
|
||||
}
|
||||
|
||||
std::vector<fs::path> scriptCoreSources = CollectCSharpSourceFiles(scriptCoreSourceRoot);
|
||||
if (scriptCoreSources.empty()) {
|
||||
return BuildFailure("No ScriptCore C# source files were found under: " +
|
||||
ScriptBuilderPathToUtf8(scriptCoreSourceRoot));
|
||||
}
|
||||
|
||||
std::vector<fs::path> projectScriptSources = CollectCSharpSourceFiles(projectRoot / "Assets");
|
||||
std::string placeholderError;
|
||||
if (!EnsurePlaceholderProjectScriptSource(
|
||||
projectScriptSources,
|
||||
generatedDirectory / "EmptyProjectGameScripts.cs",
|
||||
placeholderError)) {
|
||||
return BuildFailure(placeholderError);
|
||||
}
|
||||
|
||||
std::string compileError;
|
||||
if (!RunCSharpCompiler(
|
||||
dotnetExecutable,
|
||||
cscDllPath,
|
||||
projectRoot,
|
||||
scriptCoreOutputPath,
|
||||
frameworkReferences,
|
||||
scriptCoreSources,
|
||||
compileError)) {
|
||||
return BuildFailure("Failed to build XCEngine.ScriptCore.dll: " + compileError);
|
||||
}
|
||||
|
||||
// Mono can keep the project-local corlib mapped for the lifetime of the process.
|
||||
// Once it exists in the output folder, reuse it across incremental rebuilds.
|
||||
ec.clear();
|
||||
const bool hasProjectCorlib = fs::exists(corlibOutputPath, ec);
|
||||
if (ec) {
|
||||
return BuildFailure("Failed to inspect the project script assembly corlib path.");
|
||||
}
|
||||
|
||||
if (!hasProjectCorlib) {
|
||||
fs::copy_file(monoCorlibSourcePath, corlibOutputPath, fs::copy_options::overwrite_existing, ec);
|
||||
if (ec) {
|
||||
return BuildFailure("Failed to copy mscorlib.dll into the project script assembly directory.");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<fs::path> gameScriptReferences = frameworkReferences;
|
||||
gameScriptReferences.push_back(scriptCoreOutputPath);
|
||||
if (!RunCSharpCompiler(
|
||||
dotnetExecutable,
|
||||
cscDllPath,
|
||||
projectRoot,
|
||||
gameScriptsOutputPath,
|
||||
gameScriptReferences,
|
||||
projectScriptSources,
|
||||
compileError)) {
|
||||
return BuildFailure("Failed to build GameScripts.dll: " + compileError);
|
||||
}
|
||||
|
||||
return EditorScriptAssemblyBuildResult{
|
||||
true,
|
||||
"Rebuilt script assemblies in " + ScriptBuilderPathToUtf8(outputDirectory)
|
||||
};
|
||||
} catch (const std::exception& exception) {
|
||||
return BuildFailure("Script assembly rebuild threw an exception: " + std::string(exception.what()));
|
||||
} catch (...) {
|
||||
return BuildFailure("Script assembly rebuild threw an unknown exception.");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Scripting
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
21
editor/src/Scripting/EditorScriptAssemblyBuilder.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Scripting {
|
||||
|
||||
struct EditorScriptAssemblyBuildResult {
|
||||
bool succeeded = false;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
class EditorScriptAssemblyBuilder {
|
||||
public:
|
||||
static EditorScriptAssemblyBuildResult RebuildProjectAssemblies(const std::string& projectPath);
|
||||
};
|
||||
|
||||
} // namespace Scripting
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
106
editor/src/Scripting/EditorScriptAssemblyBuilderUtils.h
Normal file
@@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
|
||||
#include "Platform/Win32Utf8.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Scripting {
|
||||
|
||||
inline std::string ScriptBuilderPathToUtf8(const std::filesystem::path& path) {
|
||||
return Platform::WideToUtf8(path.wstring());
|
||||
}
|
||||
|
||||
inline std::vector<std::filesystem::path> CollectCSharpSourceFiles(const std::filesystem::path& root) {
|
||||
std::vector<std::filesystem::path> sourceFiles;
|
||||
std::error_code ec;
|
||||
if (root.empty() || !std::filesystem::exists(root, ec)) {
|
||||
return sourceFiles;
|
||||
}
|
||||
|
||||
for (std::filesystem::recursive_directory_iterator it(root, ec), end; it != end && !ec; it.increment(ec)) {
|
||||
if (ec || !it->is_regular_file(ec)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::filesystem::path path = it->path();
|
||||
if (path.extension() == ".cs") {
|
||||
sourceFiles.push_back(path.lexically_normal());
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(sourceFiles.begin(), sourceFiles.end());
|
||||
return sourceFiles;
|
||||
}
|
||||
|
||||
inline std::string ParseLatestDotnetSdkVersion(const std::string& sdkListOutput) {
|
||||
std::string latestVersion;
|
||||
size_t lineStart = 0;
|
||||
while (lineStart < sdkListOutput.size()) {
|
||||
const size_t lineEnd = sdkListOutput.find_first_of("\r\n", lineStart);
|
||||
const std::string line = sdkListOutput.substr(
|
||||
lineStart,
|
||||
lineEnd == std::string::npos ? std::string::npos : lineEnd - lineStart);
|
||||
const size_t delimiter = line.find(" [");
|
||||
if (delimiter != std::string::npos) {
|
||||
latestVersion = line.substr(0, delimiter);
|
||||
}
|
||||
|
||||
if (lineEnd == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
|
||||
lineStart = lineEnd + 1;
|
||||
if (lineStart < sdkListOutput.size() &&
|
||||
sdkListOutput[lineEnd] == '\r' &&
|
||||
sdkListOutput[lineStart] == '\n') {
|
||||
++lineStart;
|
||||
}
|
||||
}
|
||||
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
inline bool EnsurePlaceholderProjectScriptSource(
|
||||
std::vector<std::filesystem::path>& ioSourceFiles,
|
||||
const std::filesystem::path& placeholderPath,
|
||||
std::string& outError) {
|
||||
if (!ioSourceFiles.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(placeholderPath.parent_path(), ec);
|
||||
if (ec) {
|
||||
outError = "Failed to create the placeholder script directory: " +
|
||||
ScriptBuilderPathToUtf8(placeholderPath.parent_path());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream output(placeholderPath, std::ios::out | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
outError = "Failed to create the placeholder project script source: " +
|
||||
ScriptBuilderPathToUtf8(placeholderPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
output << "namespace XCEngine.Generated { public static class EmptyProjectGameScriptsMarker {} }\n";
|
||||
output.close();
|
||||
if (!output.good()) {
|
||||
outError = "Failed to write the placeholder project script source: " +
|
||||
ScriptBuilderPathToUtf8(placeholderPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
ioSourceFiles.push_back(placeholderPath.lexically_normal());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Scripting
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
17
editor/src/Scripting/EditorScriptRuntimeStatus.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
struct EditorScriptRuntimeStatus {
|
||||
bool backendEnabled = false;
|
||||
bool assembliesFound = false;
|
||||
bool runtimeLoaded = false;
|
||||
std::string assemblyDirectory;
|
||||
std::string statusMessage;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -47,6 +47,20 @@ struct AssetTileOptions {
|
||||
bool drawLabel = true;
|
||||
};
|
||||
|
||||
inline ImVec2 ComputeAssetTileSize(
|
||||
const char* label,
|
||||
const AssetTileOptions& options = AssetTileOptions()) {
|
||||
const ImVec2 textSize = ImGui::CalcTextSize(label ? label : "");
|
||||
ImVec2 tileSize = options.size;
|
||||
tileSize.x = std::max(tileSize.x, options.iconSize.x + AssetTileTextPadding().x * 2.0f);
|
||||
tileSize.y = std::max(
|
||||
tileSize.y,
|
||||
options.iconOffset.y +
|
||||
options.iconSize.y +
|
||||
(options.drawLabel ? AssetTileIconTextGap() + textSize.y + AssetTileTextPadding().y : 0.0f));
|
||||
return tileSize;
|
||||
}
|
||||
|
||||
enum class DialogActionResult {
|
||||
None,
|
||||
Primary,
|
||||
@@ -428,16 +442,8 @@ inline AssetTileResult DrawAssetTile(
|
||||
bool dimmed,
|
||||
DrawIconFn&& drawIcon,
|
||||
const AssetTileOptions& options = AssetTileOptions()) {
|
||||
const ImVec2 textSize = ImGui::CalcTextSize(label);
|
||||
ImVec2 tileSize = options.size;
|
||||
tileSize.x = std::max(tileSize.x, options.iconSize.x + AssetTileTextPadding().x * 2.0f);
|
||||
tileSize.y = std::max(
|
||||
tileSize.y,
|
||||
options.iconOffset.y +
|
||||
options.iconSize.y +
|
||||
AssetTileIconTextGap() +
|
||||
textSize.y +
|
||||
AssetTileTextPadding().y);
|
||||
const ImVec2 textSize = ImGui::CalcTextSize(label ? label : "");
|
||||
const ImVec2 tileSize = ComputeAssetTileSize(label, options);
|
||||
ImGui::InvisibleButton("##AssetBtn", tileSize);
|
||||
|
||||
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||
@@ -470,13 +476,13 @@ inline AssetTileResult DrawAssetTile(
|
||||
drawIcon(drawList, iconMin, iconMax);
|
||||
|
||||
const ImVec2 textPadding = AssetTileTextPadding();
|
||||
const float labelHeight = ImGui::GetFrameHeight();
|
||||
const float labelHeight = (std::max)(ImGui::GetFrameHeight(), textSize.y);
|
||||
const ImVec2 labelMin(min.x + textPadding.x, max.y - labelHeight - textPadding.y * 0.5f);
|
||||
const ImVec2 labelMax(max.x - textPadding.x, labelMin.y + labelHeight);
|
||||
if (options.drawLabel) {
|
||||
const float textAreaWidth = labelMax.x - labelMin.x;
|
||||
const float centeredTextX = labelMin.x + std::max(0.0f, (textAreaWidth - textSize.x) * 0.5f);
|
||||
const float textY = labelMin.y + (labelHeight - textSize.y) * 0.5f;
|
||||
const float textY = labelMin.y + (std::max)(0.0f, (labelHeight - textSize.y) * 0.5f);
|
||||
ImGui::PushClipRect(labelMin, labelMax, true);
|
||||
drawList->AddText(ImVec2(centeredTextX, textY), ImGui::GetColorU32(AssetTileTextColor(selected)), label);
|
||||
ImGui::PopClipRect();
|
||||
|
||||
@@ -15,6 +15,8 @@ struct RenderContext;
|
||||
namespace Editor {
|
||||
|
||||
class IEditorContext;
|
||||
struct SceneViewportOverlayFrameData;
|
||||
struct SceneViewportTransformGizmoHandleBuildInputs;
|
||||
|
||||
enum class EditorViewportKind {
|
||||
Scene,
|
||||
@@ -83,6 +85,14 @@ public:
|
||||
const ImVec2& viewportMousePosition) = 0;
|
||||
virtual void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis axis) = 0;
|
||||
virtual SceneViewportOverlayData GetSceneViewOverlayData() const = 0;
|
||||
virtual const SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext& context) = 0;
|
||||
virtual const SceneViewportOverlayFrameData& GetSceneViewInteractionOverlayFrameData(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) = 0;
|
||||
virtual void SetSceneViewTransientTransformGizmoOverlayData(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) = 0;
|
||||
virtual void RenderRequestedViewports(
|
||||
IEditorContext& context,
|
||||
const Rendering::RenderContext& renderContext) = 0;
|
||||
|
||||
1255
editor/src/Viewport/Passes/SceneViewportEditorOverlayPass.cpp
Normal file
89
editor/src/Viewport/Passes/SceneViewportEditorOverlayPass.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include "Viewport/SceneViewportEditorOverlayData.h"
|
||||
|
||||
#include <XCEngine/Rendering/RenderContext.h>
|
||||
#include <XCEngine/Rendering/RenderPass.h>
|
||||
#include <XCEngine/Rendering/RenderSurface.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace RHI {
|
||||
class RHIBuffer;
|
||||
class RHIDescriptorPool;
|
||||
class RHIDescriptorSet;
|
||||
class RHIDevice;
|
||||
class RHIPipelineLayout;
|
||||
class RHIPipelineState;
|
||||
class RHIResourceView;
|
||||
class RHISampler;
|
||||
class RHITexture;
|
||||
enum class RHIType : uint8_t;
|
||||
} // namespace RHI
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class SceneViewportEditorOverlayPassRenderer {
|
||||
public:
|
||||
~SceneViewportEditorOverlayPassRenderer() = default;
|
||||
|
||||
void Shutdown();
|
||||
|
||||
bool Render(
|
||||
const Rendering::RenderContext& renderContext,
|
||||
const Rendering::RenderSurface& surface,
|
||||
const SceneViewportOverlayFrameData& frameData);
|
||||
|
||||
private:
|
||||
struct OverlaySpriteTextureResources {
|
||||
RHI::RHITexture* texture = nullptr;
|
||||
RHI::RHIResourceView* shaderView = nullptr;
|
||||
RHI::RHIDescriptorSet* textureSet = nullptr;
|
||||
};
|
||||
|
||||
bool EnsureInitialized(const Rendering::RenderContext& renderContext);
|
||||
bool CreateResources(const Rendering::RenderContext& renderContext);
|
||||
bool EnsureLineBufferCapacity(size_t requiredVertexCount);
|
||||
bool EnsureScreenTriangleBufferCapacity(size_t requiredVertexCount);
|
||||
bool EnsureSpriteBufferCapacity(size_t requiredVertexCount);
|
||||
bool EnsureIconTexturesLoaded();
|
||||
void DestroyResources();
|
||||
|
||||
RHI::RHIDevice* m_device = nullptr;
|
||||
RHI::RHIType m_backendType = RHI::RHIType::D3D12;
|
||||
RHI::RHIPipelineLayout* m_linePipelineLayout = nullptr;
|
||||
RHI::RHIPipelineLayout* m_spritePipelineLayout = nullptr;
|
||||
RHI::RHIPipelineState* m_depthTestedLinePipelineState = nullptr;
|
||||
RHI::RHIPipelineState* m_alwaysOnTopLinePipelineState = nullptr;
|
||||
RHI::RHIPipelineState* m_depthTestedScreenTrianglePipelineState = nullptr;
|
||||
RHI::RHIPipelineState* m_alwaysOnTopScreenTrianglePipelineState = nullptr;
|
||||
RHI::RHIPipelineState* m_depthTestedSpritePipelineState = nullptr;
|
||||
RHI::RHIPipelineState* m_alwaysOnTopSpritePipelineState = nullptr;
|
||||
RHI::RHIDescriptorPool* m_constantPool = nullptr;
|
||||
RHI::RHIDescriptorPool* m_texturePool = nullptr;
|
||||
RHI::RHIDescriptorPool* m_samplerPool = nullptr;
|
||||
RHI::RHIDescriptorSet* m_constantSet = nullptr;
|
||||
RHI::RHIDescriptorSet* m_samplerSet = nullptr;
|
||||
RHI::RHISampler* m_sampler = nullptr;
|
||||
RHI::RHIBuffer* m_lineVertexBuffer = nullptr;
|
||||
RHI::RHIResourceView* m_lineVertexBufferView = nullptr;
|
||||
RHI::RHIBuffer* m_screenTriangleVertexBuffer = nullptr;
|
||||
RHI::RHIResourceView* m_screenTriangleVertexBufferView = nullptr;
|
||||
RHI::RHIBuffer* m_spriteVertexBuffer = nullptr;
|
||||
RHI::RHIResourceView* m_spriteVertexBufferView = nullptr;
|
||||
uint64_t m_lineVertexBufferCapacity = 0;
|
||||
uint64_t m_screenTriangleVertexBufferCapacity = 0;
|
||||
uint64_t m_spriteVertexBufferCapacity = 0;
|
||||
std::array<OverlaySpriteTextureResources, 2> m_overlaySpriteTextures = {};
|
||||
};
|
||||
|
||||
std::unique_ptr<Rendering::RenderPass> CreateSceneViewportEditorOverlayPass(
|
||||
SceneViewportEditorOverlayPassRenderer& renderer,
|
||||
const SceneViewportOverlayFrameData& frameData);
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -128,13 +128,21 @@ public:
|
||||
|
||||
if (std::abs(input.lookDeltaX) > Math::EPSILON ||
|
||||
std::abs(input.lookDeltaY) > Math::EPSILON) {
|
||||
ApplyRotationDelta(input.lookDeltaX, input.lookDeltaY);
|
||||
ApplyRotationDelta(
|
||||
input.lookDeltaX,
|
||||
input.lookDeltaY,
|
||||
kLookYawSensitivity,
|
||||
kLookPitchSensitivity);
|
||||
UpdateFocalPointFromPosition();
|
||||
}
|
||||
|
||||
if (std::abs(input.orbitDeltaX) > Math::EPSILON ||
|
||||
std::abs(input.orbitDeltaY) > Math::EPSILON) {
|
||||
ApplyRotationDelta(input.orbitDeltaX, input.orbitDeltaY);
|
||||
ApplyRotationDelta(
|
||||
input.orbitDeltaX,
|
||||
input.orbitDeltaY,
|
||||
kOrbitYawSensitivity,
|
||||
kOrbitPitchSensitivity);
|
||||
UpdatePositionFromFocalPoint();
|
||||
}
|
||||
|
||||
@@ -200,6 +208,10 @@ public:
|
||||
|
||||
private:
|
||||
static constexpr float kSnapDurationSeconds = 0.22f;
|
||||
static constexpr float kLookYawSensitivity = 0.18f;
|
||||
static constexpr float kLookPitchSensitivity = 0.12f;
|
||||
static constexpr float kOrbitYawSensitivity = 0.30f;
|
||||
static constexpr float kOrbitPitchSensitivity = 0.20f;
|
||||
|
||||
struct OrientationAngles {
|
||||
float yawDegrees = 0.0f;
|
||||
@@ -239,9 +251,13 @@ private:
|
||||
return result;
|
||||
}
|
||||
|
||||
void ApplyRotationDelta(float deltaX, float deltaY) {
|
||||
m_yawDegrees += deltaX * 0.30f;
|
||||
m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * 0.20f, -89.0f, 89.0f);
|
||||
void ApplyRotationDelta(
|
||||
float deltaX,
|
||||
float deltaY,
|
||||
float yawSensitivity,
|
||||
float pitchSensitivity) {
|
||||
m_yawDegrees += deltaX * yawSensitivity;
|
||||
m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * pitchSensitivity, -89.0f, 89.0f);
|
||||
}
|
||||
|
||||
Math::Vector3 GetRight() const {
|
||||
|
||||
144
editor/src/Viewport/SceneViewportEditorOverlayData.h
Normal file
@@ -0,0 +1,144 @@
|
||||
#pragma once
|
||||
|
||||
#include "IViewportHostService.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
#include <array>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
enum class SceneViewportOverlayDepthMode : uint8_t {
|
||||
DepthTested = 0,
|
||||
AlwaysOnTop
|
||||
};
|
||||
|
||||
enum class SceneViewportOverlaySpriteTextureKind : uint8_t {
|
||||
Camera = 0,
|
||||
Light = 1
|
||||
};
|
||||
|
||||
enum class SceneViewportOverlayHandleKind : uint8_t {
|
||||
None = 0,
|
||||
SceneIcon,
|
||||
MoveAxis,
|
||||
MovePlane,
|
||||
RotateAxis,
|
||||
ScaleAxis,
|
||||
ScaleUniform
|
||||
};
|
||||
|
||||
enum class SceneViewportOverlayHandleShape : uint8_t {
|
||||
None = 0,
|
||||
WorldRect,
|
||||
ScreenSegment,
|
||||
ScreenRect,
|
||||
ScreenQuad
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayLinePrimitive {
|
||||
Math::Vector3 startWorld = Math::Vector3::Zero();
|
||||
Math::Vector3 endWorld = Math::Vector3::Zero();
|
||||
Math::Color color = Math::Color::White();
|
||||
float thicknessPixels = 1.0f;
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop;
|
||||
};
|
||||
|
||||
struct SceneViewportOverlaySpritePrimitive {
|
||||
Math::Vector3 worldPosition = Math::Vector3::Zero();
|
||||
Math::Vector2 sizePixels = Math::Vector2::Zero();
|
||||
Math::Color tintColor = Math::Color::White();
|
||||
float sortDepth = 0.0f;
|
||||
uint64_t entityId = 0;
|
||||
SceneViewportOverlaySpriteTextureKind textureKind = SceneViewportOverlaySpriteTextureKind::Camera;
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop;
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayScreenTriangleVertex {
|
||||
Math::Vector2 screenPosition = Math::Vector2::Zero();
|
||||
Math::Color color = Math::Color::White();
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayScreenTrianglePrimitive {
|
||||
std::array<SceneViewportOverlayScreenTriangleVertex, 3> vertices = {};
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop;
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayHandleRecord {
|
||||
SceneViewportOverlayHandleKind kind = SceneViewportOverlayHandleKind::None;
|
||||
uint64_t handleId = 0;
|
||||
uint64_t entityId = 0;
|
||||
SceneViewportOverlayHandleShape shape = SceneViewportOverlayHandleShape::None;
|
||||
int priority = 0;
|
||||
Math::Vector3 worldPosition = Math::Vector3::Zero();
|
||||
Math::Vector2 sizePixels = Math::Vector2::Zero();
|
||||
float sortDepth = 0.0f;
|
||||
Math::Vector2 screenStart = Math::Vector2::Zero();
|
||||
Math::Vector2 screenEnd = Math::Vector2::Zero();
|
||||
Math::Vector2 screenCenter = Math::Vector2::Zero();
|
||||
Math::Vector2 screenHalfSize = Math::Vector2::Zero();
|
||||
std::array<Math::Vector2, 4> screenQuad = {};
|
||||
float hitThicknessPixels = 0.0f;
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayHandleHitResult {
|
||||
SceneViewportOverlayHandleKind kind = SceneViewportOverlayHandleKind::None;
|
||||
uint64_t handleId = 0;
|
||||
uint64_t entityId = 0;
|
||||
int priority = (std::numeric_limits<int>::min)();
|
||||
float distanceSq = (std::numeric_limits<float>::max)();
|
||||
float depth = (std::numeric_limits<float>::max)();
|
||||
|
||||
bool HasHit() const {
|
||||
return kind != SceneViewportOverlayHandleKind::None;
|
||||
}
|
||||
};
|
||||
|
||||
struct SceneViewportOverlayFrameData {
|
||||
SceneViewportOverlayData overlay = {};
|
||||
std::vector<SceneViewportOverlayLinePrimitive> worldLines = {};
|
||||
std::vector<SceneViewportOverlaySpritePrimitive> worldSprites = {};
|
||||
std::vector<SceneViewportOverlayScreenTrianglePrimitive> screenTriangles = {};
|
||||
std::vector<SceneViewportOverlayHandleRecord> handleRecords = {};
|
||||
|
||||
bool HasOverlayPrimitives() const {
|
||||
return overlay.valid && (!worldLines.empty() || !worldSprites.empty() || !screenTriangles.empty());
|
||||
}
|
||||
|
||||
bool HasWorldOverlay() const {
|
||||
return HasOverlayPrimitives();
|
||||
}
|
||||
};
|
||||
|
||||
inline void AppendSceneViewportOverlayFrameData(
|
||||
SceneViewportOverlayFrameData& target,
|
||||
const SceneViewportOverlayFrameData& source) {
|
||||
if (!target.overlay.valid && source.overlay.valid) {
|
||||
target.overlay = source.overlay;
|
||||
}
|
||||
|
||||
target.worldLines.insert(
|
||||
target.worldLines.end(),
|
||||
source.worldLines.begin(),
|
||||
source.worldLines.end());
|
||||
target.worldSprites.insert(
|
||||
target.worldSprites.end(),
|
||||
source.worldSprites.begin(),
|
||||
source.worldSprites.end());
|
||||
target.screenTriangles.insert(
|
||||
target.screenTriangles.end(),
|
||||
source.screenTriangles.begin(),
|
||||
source.screenTriangles.end());
|
||||
target.handleRecords.insert(
|
||||
target.handleRecords.end(),
|
||||
source.handleRecords.begin(),
|
||||
source.handleRecords.end());
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -14,7 +14,7 @@ namespace {
|
||||
constexpr float kMoveGizmoHandleLengthPixels = 100.0f;
|
||||
constexpr float kMoveGizmoHoverThresholdPixels = 10.0f;
|
||||
|
||||
Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) {
|
||||
Math::Vector3 GetBaseAxisVector(SceneViewportGizmoAxis axis) {
|
||||
switch (axis) {
|
||||
case SceneViewportGizmoAxis::X:
|
||||
return Math::Vector3::Right();
|
||||
@@ -28,6 +28,16 @@ Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) {
|
||||
}
|
||||
}
|
||||
|
||||
Math::Vector3 GetAxisVector(
|
||||
SceneViewportGizmoAxis axis,
|
||||
const Math::Quaternion& orientation) {
|
||||
const Math::Vector3 baseAxis = GetBaseAxisVector(axis);
|
||||
const Math::Vector3 orientedAxis = orientation * baseAxis;
|
||||
return orientedAxis.SqrMagnitude() <= Math::EPSILON
|
||||
? baseAxis
|
||||
: orientedAxis.Normalized();
|
||||
}
|
||||
|
||||
Math::Color GetAxisBaseColor(SceneViewportGizmoAxis axis) {
|
||||
switch (axis) {
|
||||
case SceneViewportGizmoAxis::X:
|
||||
@@ -61,20 +71,21 @@ SceneViewportGizmoPlane GetPlaneForIndex(size_t index) {
|
||||
|
||||
void GetPlaneAxes(
|
||||
SceneViewportGizmoPlane plane,
|
||||
const Math::Quaternion& orientation,
|
||||
Math::Vector3& outAxisA,
|
||||
Math::Vector3& outAxisB) {
|
||||
switch (plane) {
|
||||
case SceneViewportGizmoPlane::XY:
|
||||
outAxisA = Math::Vector3::Right();
|
||||
outAxisB = Math::Vector3::Up();
|
||||
outAxisA = GetAxisVector(SceneViewportGizmoAxis::X, orientation);
|
||||
outAxisB = GetAxisVector(SceneViewportGizmoAxis::Y, orientation);
|
||||
return;
|
||||
case SceneViewportGizmoPlane::XZ:
|
||||
outAxisA = Math::Vector3::Right();
|
||||
outAxisB = Math::Vector3::Forward();
|
||||
outAxisA = GetAxisVector(SceneViewportGizmoAxis::X, orientation);
|
||||
outAxisB = GetAxisVector(SceneViewportGizmoAxis::Z, orientation);
|
||||
return;
|
||||
case SceneViewportGizmoPlane::YZ:
|
||||
outAxisA = Math::Vector3::Up();
|
||||
outAxisB = Math::Vector3::Forward();
|
||||
outAxisA = GetAxisVector(SceneViewportGizmoAxis::Y, orientation);
|
||||
outAxisB = GetAxisVector(SceneViewportGizmoAxis::Z, orientation);
|
||||
return;
|
||||
case SceneViewportGizmoPlane::None:
|
||||
default:
|
||||
@@ -84,14 +95,16 @@ void GetPlaneAxes(
|
||||
}
|
||||
}
|
||||
|
||||
Math::Vector3 GetPlaneNormal(SceneViewportGizmoPlane plane) {
|
||||
Math::Vector3 GetPlaneNormal(
|
||||
SceneViewportGizmoPlane plane,
|
||||
const Math::Quaternion& orientation) {
|
||||
switch (plane) {
|
||||
case SceneViewportGizmoPlane::XY:
|
||||
return Math::Vector3::Forward();
|
||||
return GetAxisVector(SceneViewportGizmoAxis::Z, orientation);
|
||||
case SceneViewportGizmoPlane::XZ:
|
||||
return Math::Vector3::Up();
|
||||
return GetAxisVector(SceneViewportGizmoAxis::Y, orientation);
|
||||
case SceneViewportGizmoPlane::YZ:
|
||||
return Math::Vector3::Right();
|
||||
return GetAxisVector(SceneViewportGizmoAxis::X, orientation);
|
||||
case SceneViewportGizmoPlane::None:
|
||||
default:
|
||||
return Math::Vector3::Zero();
|
||||
@@ -185,10 +198,9 @@ float ComputeWorldUnitsPerPixel(
|
||||
void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) {
|
||||
BuildDrawData(context);
|
||||
if (m_dragMode == DragMode::None && IsMouseInsideViewport(context)) {
|
||||
m_hoveredAxis = HitTestAxis(context.mousePosition);
|
||||
m_hoveredPlane = m_hoveredAxis == SceneViewportGizmoAxis::None
|
||||
? HitTestPlane(context.mousePosition)
|
||||
: SceneViewportGizmoPlane::None;
|
||||
const SceneViewportMoveGizmoHitResult hitResult = EvaluateHit(context.mousePosition);
|
||||
m_hoveredAxis = hitResult.axis;
|
||||
m_hoveredPlane = hitResult.plane;
|
||||
} else if (m_dragMode == DragMode::None) {
|
||||
m_hoveredAxis = SceneViewportGizmoAxis::None;
|
||||
m_hoveredPlane = SceneViewportGizmoPlane::None;
|
||||
@@ -221,18 +233,17 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
|
||||
Math::Vector3 dragPlaneNormal = Math::Vector3::Zero();
|
||||
Math::Vector3 worldAxis = Math::Vector3::Zero();
|
||||
|
||||
if (m_hoveredAxis != SceneViewportGizmoAxis::None) {
|
||||
worldAxis = GetAxisVector(m_hoveredAxis);
|
||||
worldAxis = GetAxisVector(m_hoveredAxis, context.axisOrientation);
|
||||
if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
dragPlaneNormal = GetPlaneNormal(m_hoveredPlane);
|
||||
dragPlaneNormal = GetPlaneNormal(m_hoveredPlane, context.axisOrientation);
|
||||
if (dragPlaneNormal.SqrMagnitude() <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
@@ -257,10 +268,21 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
|
||||
m_activeAxisDirection = worldAxis;
|
||||
m_activePlaneNormal = dragPlaneNormal;
|
||||
m_dragPlane = dragPlane;
|
||||
m_dragStartObjectWorldPosition = objectWorldPosition;
|
||||
m_dragStartPivotWorldPosition = pivotWorldPosition;
|
||||
m_dragStartHitWorldPosition = hitPoint;
|
||||
m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - pivotWorldPosition, worldAxis);
|
||||
m_dragObjects = context.selectedObjects;
|
||||
if (m_dragObjects.empty()) {
|
||||
m_dragObjects.push_back(context.selectedObject);
|
||||
}
|
||||
m_dragStartObjectWorldPositions.clear();
|
||||
m_dragStartObjectWorldPositions.reserve(m_dragObjects.size());
|
||||
for (Components::GameObject* gameObject : m_dragObjects) {
|
||||
m_dragStartObjectWorldPositions.push_back(
|
||||
gameObject != nullptr && gameObject->GetTransform() != nullptr
|
||||
? gameObject->GetTransform()->GetPosition()
|
||||
: Math::Vector3::Zero());
|
||||
}
|
||||
RefreshHandleState();
|
||||
return true;
|
||||
}
|
||||
@@ -268,7 +290,9 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
|
||||
void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& context) {
|
||||
if (m_dragMode == DragMode::None ||
|
||||
context.selectedObject == nullptr ||
|
||||
context.selectedObject->GetID() != m_activeEntityId) {
|
||||
context.selectedObject->GetID() != m_activeEntityId ||
|
||||
m_dragObjects.empty() ||
|
||||
m_dragObjects.size() != m_dragStartObjectWorldPositions.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -290,17 +314,28 @@ void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& con
|
||||
if (m_dragMode == DragMode::Axis) {
|
||||
const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection);
|
||||
const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar;
|
||||
context.selectedObject->GetTransform()->SetPosition(
|
||||
m_dragStartObjectWorldPosition + m_activeAxisDirection * deltaScalar);
|
||||
const Math::Vector3 worldDelta = m_activeAxisDirection * deltaScalar;
|
||||
for (size_t index = 0; index < m_dragObjects.size(); ++index) {
|
||||
if (m_dragObjects[index] == nullptr || m_dragObjects[index]->GetTransform() == nullptr) {
|
||||
continue;
|
||||
}
|
||||
m_dragObjects[index]->GetTransform()->SetPosition(
|
||||
m_dragStartObjectWorldPositions[index] + worldDelta);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_dragMode == DragMode::Plane) {
|
||||
const Math::Vector3 planeDelta = Math::Vector3::ProjectOnPlane(
|
||||
const Math::Vector3 worldDelta = Math::Vector3::ProjectOnPlane(
|
||||
hitPoint - m_dragStartHitWorldPosition,
|
||||
m_activePlaneNormal);
|
||||
context.selectedObject->GetTransform()->SetPosition(
|
||||
m_dragStartObjectWorldPosition + planeDelta);
|
||||
for (size_t index = 0; index < m_dragObjects.size(); ++index) {
|
||||
if (m_dragObjects[index] == nullptr || m_dragObjects[index]->GetTransform() == nullptr) {
|
||||
continue;
|
||||
}
|
||||
m_dragObjects[index]->GetTransform()->SetPosition(
|
||||
m_dragStartObjectWorldPositions[index] + worldDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,10 +354,11 @@ void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) {
|
||||
m_activeEntityId = 0;
|
||||
m_activeAxisDirection = Math::Vector3::Zero();
|
||||
m_activePlaneNormal = Math::Vector3::Zero();
|
||||
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartHitWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartAxisScalar = 0.0f;
|
||||
m_dragObjects.clear();
|
||||
m_dragStartObjectWorldPositions.clear();
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
@@ -337,10 +373,11 @@ void SceneViewportMoveGizmo::CancelDrag(IUndoManager* undoManager) {
|
||||
m_activeEntityId = 0;
|
||||
m_activeAxisDirection = Math::Vector3::Zero();
|
||||
m_activePlaneNormal = Math::Vector3::Zero();
|
||||
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartHitWorldPosition = Math::Vector3::Zero();
|
||||
m_dragStartAxisScalar = 0.0f;
|
||||
m_dragObjects.clear();
|
||||
m_dragStartObjectWorldPositions.clear();
|
||||
m_hoveredAxis = SceneViewportGizmoAxis::None;
|
||||
m_hoveredPlane = SceneViewportGizmoPlane::None;
|
||||
RefreshHandleState();
|
||||
@@ -363,19 +400,74 @@ const SceneViewportMoveGizmoDrawData& SceneViewportMoveGizmo::GetDrawData() cons
|
||||
return m_drawData;
|
||||
}
|
||||
|
||||
SceneViewportMoveGizmoHitResult SceneViewportMoveGizmo::EvaluateHit(const Math::Vector2& mousePosition) const {
|
||||
SceneViewportMoveGizmoHitResult result = {};
|
||||
if (!m_drawData.visible) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const float hoverThresholdSq = kMoveGizmoHoverThresholdPixels * kMoveGizmoHoverThresholdPixels;
|
||||
for (const SceneViewportMoveGizmoHandleDrawData& handle : m_drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end);
|
||||
if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.axis = handle.axis;
|
||||
result.plane = SceneViewportGizmoPlane::None;
|
||||
result.distanceSq = distanceSq;
|
||||
}
|
||||
|
||||
if (result.axis != SceneViewportGizmoAxis::None) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoPlaneDrawData& plane : m_drawData.planes) {
|
||||
if (!plane.visible || !PointInQuad(mousePosition, plane.corners)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = (QuadCenter(plane.corners) - mousePosition).SqrMagnitude();
|
||||
if (distanceSq >= result.distanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.axis = SceneViewportGizmoAxis::None;
|
||||
result.plane = plane.plane;
|
||||
result.distanceSq = distanceSq;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void SceneViewportMoveGizmo::SetHoveredHandle(
|
||||
SceneViewportGizmoAxis axis,
|
||||
SceneViewportGizmoPlane plane) {
|
||||
if (m_dragMode != DragMode::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_hoveredAxis = axis;
|
||||
m_hoveredPlane = axis == SceneViewportGizmoAxis::None ? plane : SceneViewportGizmoPlane::None;
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& context) {
|
||||
m_drawData = {};
|
||||
m_drawData.pivotRadius = 5.0f;
|
||||
|
||||
const Components::GameObject* selectedObject = context.selectedObject;
|
||||
if (selectedObject == nullptr ||
|
||||
if ((context.selectedObject == nullptr && context.selectedObjects.empty()) ||
|
||||
!context.overlay.valid ||
|
||||
context.viewportSize.x <= 1.0f ||
|
||||
context.viewportSize.y <= 1.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 gizmoWorldOrigin = selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 gizmoWorldOrigin = context.pivotWorldPosition;
|
||||
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
@@ -411,7 +503,7 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
|
||||
handle.start = projectedPivot.screenPosition;
|
||||
|
||||
const Math::Vector3 axisEndWorld =
|
||||
gizmoWorldOrigin + GetAxisVector(handle.axis) * axisLengthWorld;
|
||||
gizmoWorldOrigin + GetAxisVector(handle.axis, context.axisOrientation) * axisLengthWorld;
|
||||
const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
@@ -436,7 +528,7 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
|
||||
|
||||
Math::Vector3 axisA = Math::Vector3::Zero();
|
||||
Math::Vector3 axisB = Math::Vector3::Zero();
|
||||
GetPlaneAxes(plane.plane, axisA, axisB);
|
||||
GetPlaneAxes(plane.plane, context.axisOrientation, axisA, axisB);
|
||||
if (axisA.SqrMagnitude() <= Math::EPSILON || axisB.SqrMagnitude() <= Math::EPSILON) {
|
||||
continue;
|
||||
}
|
||||
@@ -502,55 +594,5 @@ void SceneViewportMoveGizmo::RefreshHandleState() {
|
||||
}
|
||||
}
|
||||
|
||||
SceneViewportGizmoAxis SceneViewportMoveGizmo::HitTestAxis(const Math::Vector2& mousePosition) const {
|
||||
if (!m_drawData.visible) {
|
||||
return SceneViewportGizmoAxis::None;
|
||||
}
|
||||
|
||||
const float hoverThresholdSq = kMoveGizmoHoverThresholdPixels * kMoveGizmoHoverThresholdPixels;
|
||||
SceneViewportGizmoAxis bestAxis = SceneViewportGizmoAxis::None;
|
||||
float bestDistanceSq = hoverThresholdSq;
|
||||
|
||||
for (const SceneViewportMoveGizmoHandleDrawData& handle : m_drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end);
|
||||
if (distanceSq > bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestAxis = handle.axis;
|
||||
}
|
||||
|
||||
return bestAxis;
|
||||
}
|
||||
|
||||
SceneViewportGizmoPlane SceneViewportMoveGizmo::HitTestPlane(const Math::Vector2& mousePosition) const {
|
||||
if (!m_drawData.visible) {
|
||||
return SceneViewportGizmoPlane::None;
|
||||
}
|
||||
|
||||
SceneViewportGizmoPlane bestPlane = SceneViewportGizmoPlane::None;
|
||||
float bestDistanceSq = Math::FLOAT_MAX;
|
||||
for (const SceneViewportMoveGizmoPlaneDrawData& plane : m_drawData.planes) {
|
||||
if (!plane.visible || !PointInQuad(mousePosition, plane.corners)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = (QuadCenter(plane.corners) - mousePosition).SqrMagnitude();
|
||||
if (distanceSq >= bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestPlane = plane.plane;
|
||||
}
|
||||
|
||||
return bestPlane;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/Core/Math/Plane.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Components {
|
||||
@@ -66,6 +68,19 @@ struct SceneViewportMoveGizmoContext {
|
||||
Math::Vector2 viewportSize = Math::Vector2::Zero();
|
||||
Math::Vector2 mousePosition = Math::Vector2::Zero();
|
||||
Components::GameObject* selectedObject = nullptr;
|
||||
std::vector<Components::GameObject*> selectedObjects = {};
|
||||
Math::Vector3 pivotWorldPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion axisOrientation = Math::Quaternion::Identity();
|
||||
};
|
||||
|
||||
struct SceneViewportMoveGizmoHitResult {
|
||||
SceneViewportGizmoAxis axis = SceneViewportGizmoAxis::None;
|
||||
SceneViewportGizmoPlane plane = SceneViewportGizmoPlane::None;
|
||||
float distanceSq = Math::FLOAT_MAX;
|
||||
|
||||
bool HasHit() const {
|
||||
return axis != SceneViewportGizmoAxis::None || plane != SceneViewportGizmoPlane::None;
|
||||
}
|
||||
};
|
||||
|
||||
class SceneViewportMoveGizmo {
|
||||
@@ -80,6 +95,8 @@ public:
|
||||
bool IsActive() const;
|
||||
uint64_t GetActiveEntityId() const;
|
||||
const SceneViewportMoveGizmoDrawData& GetDrawData() const;
|
||||
SceneViewportMoveGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const;
|
||||
void SetHoveredHandle(SceneViewportGizmoAxis axis, SceneViewportGizmoPlane plane);
|
||||
|
||||
private:
|
||||
enum class DragMode : uint8_t {
|
||||
@@ -90,8 +107,6 @@ private:
|
||||
|
||||
void BuildDrawData(const SceneViewportMoveGizmoContext& context);
|
||||
void RefreshHandleState();
|
||||
SceneViewportGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const;
|
||||
SceneViewportGizmoPlane HitTestPlane(const Math::Vector2& mousePosition) const;
|
||||
|
||||
SceneViewportMoveGizmoDrawData m_drawData = {};
|
||||
SceneViewportGizmoAxis m_hoveredAxis = SceneViewportGizmoAxis::None;
|
||||
@@ -103,10 +118,11 @@ private:
|
||||
Math::Vector3 m_activeAxisDirection = Math::Vector3::Zero();
|
||||
Math::Vector3 m_activePlaneNormal = Math::Vector3::Zero();
|
||||
Math::Plane m_dragPlane = {};
|
||||
Math::Vector3 m_dragStartObjectWorldPosition = Math::Vector3::Zero();
|
||||
Math::Vector3 m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
Math::Vector3 m_dragStartHitWorldPosition = Math::Vector3::Zero();
|
||||
float m_dragStartAxisScalar = 0.0f;
|
||||
std::vector<Components::GameObject*> m_dragObjects = {};
|
||||
std::vector<Math::Vector3> m_dragStartObjectWorldPositions = {};
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
|
||||
431
editor/src/Viewport/SceneViewportOverlayBuilder.cpp
Normal file
@@ -0,0 +1,431 @@
|
||||
#include "SceneViewportOverlayBuilder.h"
|
||||
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "SceneViewportOverlayHandleBuilder.h"
|
||||
#include "SceneViewportMath.h"
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Components/LightComponent.h>
|
||||
#include <XCEngine/Components/TransformComponent.h>
|
||||
#include <XCEngine/Core/Math/Rect.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool CanBuildOverlayForGameObject(const Components::GameObject* gameObject) {
|
||||
return gameObject != nullptr &&
|
||||
gameObject->GetTransform() != nullptr &&
|
||||
gameObject->IsActiveInHierarchy();
|
||||
}
|
||||
|
||||
float ResolveCameraAspect(
|
||||
const Components::CameraComponent& camera,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight) {
|
||||
const Math::Rect viewportRect = camera.GetViewportRect();
|
||||
const float resolvedWidth = static_cast<float>(viewportWidth) *
|
||||
(viewportRect.width > Math::EPSILON ? viewportRect.width : 1.0f);
|
||||
const float resolvedHeight = static_cast<float>(viewportHeight) *
|
||||
(viewportRect.height > Math::EPSILON ? viewportRect.height : 1.0f);
|
||||
return resolvedHeight > Math::EPSILON
|
||||
? resolvedWidth / resolvedHeight
|
||||
: 1.0f;
|
||||
}
|
||||
|
||||
float ComputeWorldUnitsPerPixel(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector3& worldPoint,
|
||||
uint32_t viewportHeight) {
|
||||
if (!overlay.valid || viewportHeight <= 1u) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const Math::Vector3 cameraForward = overlay.cameraForward.Normalized();
|
||||
const float depth = Math::Vector3::Dot(worldPoint - overlay.cameraPosition, cameraForward);
|
||||
if (depth <= Math::EPSILON) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) /
|
||||
static_cast<float>(viewportHeight);
|
||||
}
|
||||
|
||||
void AppendWorldLine(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector3& startWorld,
|
||||
const Math::Vector3& endWorld,
|
||||
const Math::Color& color,
|
||||
float thicknessPixels,
|
||||
SceneViewportOverlayDepthMode depthMode) {
|
||||
SceneViewportOverlayLinePrimitive& line = frameData.worldLines.emplace_back();
|
||||
line.startWorld = startWorld;
|
||||
line.endWorld = endWorld;
|
||||
line.color = color;
|
||||
line.thicknessPixels = thicknessPixels;
|
||||
line.depthMode = depthMode;
|
||||
}
|
||||
|
||||
void AppendWorldSprite(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector3& worldPosition,
|
||||
const Math::Vector2& sizePixels,
|
||||
const Math::Color& tintColor,
|
||||
float sortDepth,
|
||||
uint64_t entityId,
|
||||
SceneViewportOverlaySpriteTextureKind textureKind,
|
||||
SceneViewportOverlayDepthMode depthMode) {
|
||||
if (entityId == 0 || sizePixels.x <= Math::EPSILON || sizePixels.y <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
SceneViewportOverlaySpritePrimitive& sprite = frameData.worldSprites.emplace_back();
|
||||
sprite.worldPosition = worldPosition;
|
||||
sprite.sizePixels = sizePixels;
|
||||
sprite.tintColor = tintColor;
|
||||
sprite.sortDepth = sortDepth;
|
||||
sprite.entityId = entityId;
|
||||
sprite.textureKind = textureKind;
|
||||
sprite.depthMode = depthMode;
|
||||
}
|
||||
|
||||
void AppendHandleRecord(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
SceneViewportOverlayHandleKind kind,
|
||||
uint64_t handleId,
|
||||
uint64_t entityId,
|
||||
const Math::Vector3& worldPosition,
|
||||
const Math::Vector2& sizePixels,
|
||||
float sortDepth) {
|
||||
if (kind == SceneViewportOverlayHandleKind::None ||
|
||||
handleId == 0 ||
|
||||
entityId == 0 ||
|
||||
sizePixels.x <= Math::EPSILON ||
|
||||
sizePixels.y <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
SceneViewportOverlayHandleRecord& handleRecord = frameData.handleRecords.emplace_back();
|
||||
handleRecord.kind = kind;
|
||||
handleRecord.handleId = handleId;
|
||||
handleRecord.entityId = entityId;
|
||||
handleRecord.shape = SceneViewportOverlayHandleShape::WorldRect;
|
||||
handleRecord.priority = Detail::kSceneViewportHandlePrioritySceneIcon;
|
||||
handleRecord.worldPosition = worldPosition;
|
||||
handleRecord.sizePixels = sizePixels;
|
||||
handleRecord.sortDepth = sortDepth;
|
||||
}
|
||||
|
||||
void AppendSceneIconOverlay(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight,
|
||||
const Components::GameObject& gameObject,
|
||||
const Math::Vector2& sizePixels,
|
||||
SceneViewportOverlaySpriteTextureKind textureKind) {
|
||||
const Components::TransformComponent* transform = gameObject.GetTransform();
|
||||
if (transform == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint(
|
||||
overlay,
|
||||
static_cast<float>(viewportWidth),
|
||||
static_cast<float>(viewportHeight),
|
||||
transform->GetPosition());
|
||||
if (!projectedPoint.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppendWorldSprite(
|
||||
frameData,
|
||||
transform->GetPosition(),
|
||||
sizePixels,
|
||||
Math::Color::White(),
|
||||
projectedPoint.ndcDepth,
|
||||
gameObject.GetID(),
|
||||
textureKind,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
AppendHandleRecord(
|
||||
frameData,
|
||||
SceneViewportOverlayHandleKind::SceneIcon,
|
||||
gameObject.GetID(),
|
||||
gameObject.GetID(),
|
||||
transform->GetPosition(),
|
||||
sizePixels,
|
||||
projectedPoint.ndcDepth);
|
||||
}
|
||||
|
||||
void AppendCameraFrustumOverlay(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Components::CameraComponent& camera,
|
||||
const Components::GameObject& gameObject,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight) {
|
||||
const Components::TransformComponent* transform = gameObject.GetTransform();
|
||||
if (transform == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 position = transform->GetPosition();
|
||||
const Math::Vector3 forward = transform->GetForward().Normalized();
|
||||
const Math::Vector3 right = transform->GetRight().Normalized();
|
||||
const Math::Vector3 up = transform->GetUp().Normalized();
|
||||
if (forward.SqrMagnitude() <= Math::EPSILON ||
|
||||
right.SqrMagnitude() <= Math::EPSILON ||
|
||||
up.SqrMagnitude() <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float nearClip = (std::max)(camera.GetNearClipPlane(), 0.01f);
|
||||
const float farClip = (std::max)(camera.GetFarClipPlane(), nearClip + 0.01f);
|
||||
const float aspect = ResolveCameraAspect(camera, viewportWidth, viewportHeight);
|
||||
|
||||
float nearHalfHeight = 0.0f;
|
||||
float nearHalfWidth = 0.0f;
|
||||
float farHalfHeight = 0.0f;
|
||||
float farHalfWidth = 0.0f;
|
||||
if (camera.GetProjectionType() == Components::CameraProjectionType::Perspective) {
|
||||
const float halfFovRadians =
|
||||
std::clamp(camera.GetFieldOfView(), 1.0f, 179.0f) * Math::DEG_TO_RAD * 0.5f;
|
||||
nearHalfHeight = std::tan(halfFovRadians) * nearClip;
|
||||
nearHalfWidth = nearHalfHeight * aspect;
|
||||
farHalfHeight = std::tan(halfFovRadians) * farClip;
|
||||
farHalfWidth = farHalfHeight * aspect;
|
||||
} else {
|
||||
const float halfHeight = (std::max)(camera.GetOrthographicSize(), 0.01f);
|
||||
const float halfWidth = halfHeight * aspect;
|
||||
nearHalfHeight = halfHeight;
|
||||
nearHalfWidth = halfWidth;
|
||||
farHalfHeight = halfHeight;
|
||||
farHalfWidth = halfWidth;
|
||||
}
|
||||
|
||||
const Math::Vector3 nearCenter = position + forward * nearClip;
|
||||
const Math::Vector3 farCenter = position + forward * farClip;
|
||||
const std::array<Math::Vector3, 8> corners = {{
|
||||
nearCenter + up * nearHalfHeight - right * nearHalfWidth,
|
||||
nearCenter + up * nearHalfHeight + right * nearHalfWidth,
|
||||
nearCenter - up * nearHalfHeight + right * nearHalfWidth,
|
||||
nearCenter - up * nearHalfHeight - right * nearHalfWidth,
|
||||
farCenter + up * farHalfHeight - right * farHalfWidth,
|
||||
farCenter + up * farHalfHeight + right * farHalfWidth,
|
||||
farCenter - up * farHalfHeight + right * farHalfWidth,
|
||||
farCenter - up * farHalfHeight - right * farHalfWidth
|
||||
}};
|
||||
|
||||
static constexpr std::array<std::pair<size_t, size_t>, 12> kFrustumEdges = {{
|
||||
{ 0u, 1u }, { 1u, 2u }, { 2u, 3u }, { 3u, 0u },
|
||||
{ 4u, 5u }, { 5u, 6u }, { 6u, 7u }, { 7u, 4u },
|
||||
{ 0u, 4u }, { 1u, 5u }, { 2u, 6u }, { 3u, 7u }
|
||||
}};
|
||||
constexpr Math::Color kFrustumColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
for (const auto& edge : kFrustumEdges) {
|
||||
AppendWorldLine(
|
||||
frameData,
|
||||
corners[edge.first],
|
||||
corners[edge.second],
|
||||
kFrustumColor,
|
||||
1.6f,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendDirectionalLightOverlay(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Components::GameObject& gameObject,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportHeight) {
|
||||
const Components::TransformComponent* transform = gameObject.GetTransform();
|
||||
if (transform == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 position = transform->GetPosition();
|
||||
const Math::Vector3 lightDirection = (transform->GetForward() * -1.0f).Normalized();
|
||||
const Math::Vector3 right = transform->GetRight().Normalized();
|
||||
const Math::Vector3 up = transform->GetUp().Normalized();
|
||||
if (lightDirection.SqrMagnitude() <= Math::EPSILON ||
|
||||
right.SqrMagnitude() <= Math::EPSILON ||
|
||||
up.SqrMagnitude() <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(overlay, position, viewportHeight);
|
||||
if (worldUnitsPerPixel <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr Math::Color kDirectionalLightColor(1.0f, 0.92f, 0.24f, 1.0f);
|
||||
constexpr float kLineThickness = 1.8f;
|
||||
constexpr size_t kRingSegmentCount = 32;
|
||||
constexpr std::array<float, 6> kRayAngles = {{
|
||||
0.0f,
|
||||
Math::PI / 3.0f,
|
||||
Math::PI * 2.0f / 3.0f,
|
||||
Math::PI,
|
||||
Math::PI * 4.0f / 3.0f,
|
||||
Math::PI * 5.0f / 3.0f
|
||||
}};
|
||||
|
||||
const float ringRadius = worldUnitsPerPixel * 26.0f;
|
||||
const float ringOffset = worldUnitsPerPixel * 54.0f;
|
||||
const float innerRayRadius = ringRadius * 0.52f;
|
||||
const float rayLength = worldUnitsPerPixel * 96.0f;
|
||||
const Math::Vector3 ringCenter = position + lightDirection * ringOffset;
|
||||
|
||||
for (size_t segmentIndex = 0; segmentIndex < kRingSegmentCount; ++segmentIndex) {
|
||||
const float angle0 =
|
||||
static_cast<float>(segmentIndex) / static_cast<float>(kRingSegmentCount) * Math::PI * 2.0f;
|
||||
const float angle1 =
|
||||
static_cast<float>(segmentIndex + 1u) / static_cast<float>(kRingSegmentCount) * Math::PI * 2.0f;
|
||||
const Math::Vector3 p0 =
|
||||
ringCenter + right * std::cos(angle0) * ringRadius + up * std::sin(angle0) * ringRadius;
|
||||
const Math::Vector3 p1 =
|
||||
ringCenter + right * std::cos(angle1) * ringRadius + up * std::sin(angle1) * ringRadius;
|
||||
AppendWorldLine(
|
||||
frameData,
|
||||
p0,
|
||||
p1,
|
||||
kDirectionalLightColor,
|
||||
kLineThickness,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
}
|
||||
|
||||
AppendWorldLine(
|
||||
frameData,
|
||||
position,
|
||||
ringCenter,
|
||||
kDirectionalLightColor,
|
||||
kLineThickness,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
AppendWorldLine(
|
||||
frameData,
|
||||
ringCenter,
|
||||
ringCenter + lightDirection * rayLength,
|
||||
kDirectionalLightColor,
|
||||
kLineThickness,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
|
||||
for (float angle : kRayAngles) {
|
||||
const Math::Vector3 rayStart =
|
||||
ringCenter + right * std::cos(angle) * innerRayRadius + up * std::sin(angle) * innerRayRadius;
|
||||
AppendWorldLine(
|
||||
frameData,
|
||||
rayStart,
|
||||
rayStart + lightDirection * rayLength,
|
||||
kDirectionalLightColor,
|
||||
kLineThickness,
|
||||
SceneViewportOverlayDepthMode::AlwaysOnTop);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendSceneObjectIconOverlays(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Components::Scene& scene,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight) {
|
||||
constexpr Math::Vector2 kCameraIconSize(90.0f, 90.0f);
|
||||
constexpr Math::Vector2 kLightIconSize(100.0f, 100.0f);
|
||||
|
||||
for (Components::CameraComponent* camera : scene.FindObjectsOfType<Components::CameraComponent>()) {
|
||||
if (camera == nullptr || !camera->IsEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Components::GameObject* gameObject = camera->GetGameObject();
|
||||
if (!CanBuildOverlayForGameObject(gameObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AppendSceneIconOverlay(
|
||||
frameData,
|
||||
overlay,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
*gameObject,
|
||||
kCameraIconSize,
|
||||
SceneViewportOverlaySpriteTextureKind::Camera);
|
||||
}
|
||||
|
||||
for (Components::LightComponent* light : scene.FindObjectsOfType<Components::LightComponent>()) {
|
||||
if (light == nullptr || !light->IsEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Components::GameObject* gameObject = light->GetGameObject();
|
||||
if (!CanBuildOverlayForGameObject(gameObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AppendSceneIconOverlay(
|
||||
frameData,
|
||||
overlay,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
*gameObject,
|
||||
kLightIconSize,
|
||||
SceneViewportOverlaySpriteTextureKind::Light);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SceneViewportOverlayFrameData SceneViewportOverlayBuilder::Build(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight,
|
||||
const std::vector<uint64_t>& selectedObjectIds) {
|
||||
SceneViewportOverlayFrameData frameData = {};
|
||||
frameData.overlay = overlay;
|
||||
if (!overlay.valid || viewportWidth == 0u || viewportHeight == 0u) {
|
||||
return frameData;
|
||||
}
|
||||
|
||||
const Components::Scene* scene = context.GetSceneManager().GetScene();
|
||||
if (scene == nullptr) {
|
||||
return frameData;
|
||||
}
|
||||
|
||||
AppendSceneObjectIconOverlays(frameData, *scene, overlay, viewportWidth, viewportHeight);
|
||||
|
||||
for (uint64_t entityId : selectedObjectIds) {
|
||||
if (entityId == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Components::GameObject* gameObject = context.GetSceneManager().GetEntity(entityId);
|
||||
if (!CanBuildOverlayForGameObject(gameObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Components::CameraComponent* camera = gameObject->GetComponent<Components::CameraComponent>();
|
||||
camera != nullptr && camera->IsEnabled()) {
|
||||
AppendCameraFrustumOverlay(frameData, *camera, *gameObject, viewportWidth, viewportHeight);
|
||||
}
|
||||
|
||||
if (Components::LightComponent* light = gameObject->GetComponent<Components::LightComponent>();
|
||||
light != nullptr &&
|
||||
light->IsEnabled() &&
|
||||
light->GetLightType() == Components::LightType::Directional) {
|
||||
AppendDirectionalLightOverlay(frameData, *gameObject, overlay, viewportHeight);
|
||||
}
|
||||
}
|
||||
|
||||
return frameData;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
25
editor/src/Viewport/SceneViewportOverlayBuilder.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "IViewportHostService.h"
|
||||
#include "SceneViewportEditorOverlayData.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
class IEditorContext;
|
||||
|
||||
class SceneViewportOverlayBuilder {
|
||||
public:
|
||||
static SceneViewportOverlayFrameData Build(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight,
|
||||
const std::vector<uint64_t>& selectedObjectIds);
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
543
editor/src/Viewport/SceneViewportOverlayHandleBuilder.h
Normal file
@@ -0,0 +1,543 @@
|
||||
#pragma once
|
||||
|
||||
#include "SceneViewportEditorOverlayData.h"
|
||||
#include "SceneViewportMoveGizmo.h"
|
||||
#include "SceneViewportRotateGizmo.h"
|
||||
#include "SceneViewportScaleGizmo.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
struct SceneViewportTransformGizmoHandleBuildInputs {
|
||||
const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr;
|
||||
uint64_t moveEntityId = 0;
|
||||
const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr;
|
||||
uint64_t rotateEntityId = 0;
|
||||
const SceneViewportScaleGizmoDrawData* scaleGizmo = nullptr;
|
||||
uint64_t scaleEntityId = 0;
|
||||
};
|
||||
|
||||
inline SceneViewportTransformGizmoHandleBuildInputs BuildSceneViewportTransformGizmoHandleBuildInputs(
|
||||
bool showingMoveGizmo,
|
||||
const SceneViewportMoveGizmo& moveGizmo,
|
||||
const SceneViewportMoveGizmoContext& moveGizmoContext,
|
||||
bool showingRotateGizmo,
|
||||
const SceneViewportRotateGizmo& rotateGizmo,
|
||||
const SceneViewportRotateGizmoContext& rotateGizmoContext,
|
||||
bool showingScaleGizmo,
|
||||
const SceneViewportScaleGizmo& scaleGizmo,
|
||||
const SceneViewportScaleGizmoContext& scaleGizmoContext) {
|
||||
SceneViewportTransformGizmoHandleBuildInputs inputs = {};
|
||||
if (showingMoveGizmo && moveGizmoContext.selectedObject != nullptr) {
|
||||
inputs.moveGizmo = &moveGizmo.GetDrawData();
|
||||
inputs.moveEntityId = moveGizmoContext.selectedObject->GetID();
|
||||
}
|
||||
if (showingRotateGizmo && rotateGizmoContext.selectedObject != nullptr) {
|
||||
inputs.rotateGizmo = &rotateGizmo.GetDrawData();
|
||||
inputs.rotateEntityId = rotateGizmoContext.selectedObject->GetID();
|
||||
}
|
||||
if (showingScaleGizmo && scaleGizmoContext.selectedObject != nullptr) {
|
||||
inputs.scaleGizmo = &scaleGizmo.GetDrawData();
|
||||
inputs.scaleEntityId = scaleGizmoContext.selectedObject->GetID();
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
namespace Detail {
|
||||
|
||||
inline constexpr int kSceneViewportHandlePrioritySceneIcon = 100;
|
||||
inline constexpr int kSceneViewportHandlePriorityRotateAxis = 311;
|
||||
inline constexpr int kSceneViewportHandlePriorityMovePlane = 321;
|
||||
inline constexpr int kSceneViewportHandlePriorityMoveAxis = 322;
|
||||
inline constexpr int kSceneViewportHandlePriorityScaleAxisLine = 331;
|
||||
inline constexpr int kSceneViewportHandlePriorityScaleAxisCap = 332;
|
||||
inline constexpr int kSceneViewportHandlePriorityScaleUniform = 333;
|
||||
|
||||
inline constexpr float kSceneViewportMoveAxisHitThicknessPixels = 10.0f;
|
||||
inline constexpr float kSceneViewportRotateAxisHitThicknessPixels = 9.0f;
|
||||
inline constexpr float kSceneViewportScaleAxisHitThicknessPixels = 10.0f;
|
||||
inline constexpr float kSceneViewportScaleCapHitPaddingPixels = 2.0f;
|
||||
|
||||
inline constexpr float kSceneViewportMoveArrowLengthPixels = 14.0f;
|
||||
inline constexpr float kSceneViewportMoveArrowHalfWidthPixels = 7.0f;
|
||||
|
||||
inline Math::Color WithAlpha(const Math::Color& color, float alpha) {
|
||||
return Math::Color(color.r, color.g, color.b, alpha);
|
||||
}
|
||||
|
||||
inline Math::Color LerpColor(const Math::Color& a, const Math::Color& b, float t) {
|
||||
return Math::Color(
|
||||
a.r + (b.r - a.r) * t,
|
||||
a.g + (b.g - a.g) * t,
|
||||
a.b + (b.b - a.b) * t,
|
||||
a.a + (b.a - a.a) * t);
|
||||
}
|
||||
|
||||
inline Math::Vector2 NormalizeVector2(
|
||||
const Math::Vector2& value,
|
||||
const Math::Vector2& fallback = Math::Vector2(1.0f, 0.0f)) {
|
||||
const float lengthSq = value.SqrMagnitude();
|
||||
if (lengthSq <= Math::EPSILON) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return value / std::sqrt(lengthSq);
|
||||
}
|
||||
|
||||
inline void AppendScreenTriangle(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& a,
|
||||
const Math::Vector2& b,
|
||||
const Math::Vector2& c,
|
||||
const Math::Color& color,
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
|
||||
SceneViewportOverlayScreenTrianglePrimitive& triangle = frameData.screenTriangles.emplace_back();
|
||||
triangle.vertices[0].screenPosition = a;
|
||||
triangle.vertices[0].color = color;
|
||||
triangle.vertices[1].screenPosition = b;
|
||||
triangle.vertices[1].color = color;
|
||||
triangle.vertices[2].screenPosition = c;
|
||||
triangle.vertices[2].color = color;
|
||||
triangle.depthMode = depthMode;
|
||||
}
|
||||
|
||||
inline void AppendScreenQuad(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& a,
|
||||
const Math::Vector2& b,
|
||||
const Math::Vector2& c,
|
||||
const Math::Vector2& d,
|
||||
const Math::Color& color,
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
|
||||
AppendScreenTriangle(frameData, a, b, c, color, depthMode);
|
||||
AppendScreenTriangle(frameData, a, c, d, color, depthMode);
|
||||
}
|
||||
|
||||
inline void AppendScreenRect(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& center,
|
||||
const Math::Vector2& halfSize,
|
||||
const Math::Color& color,
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
|
||||
AppendScreenQuad(
|
||||
frameData,
|
||||
Math::Vector2(center.x - halfSize.x, center.y - halfSize.y),
|
||||
Math::Vector2(center.x + halfSize.x, center.y - halfSize.y),
|
||||
Math::Vector2(center.x + halfSize.x, center.y + halfSize.y),
|
||||
Math::Vector2(center.x - halfSize.x, center.y + halfSize.y),
|
||||
color,
|
||||
depthMode);
|
||||
}
|
||||
|
||||
inline void AppendScreenSegmentQuad(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& start,
|
||||
const Math::Vector2& end,
|
||||
float thicknessPixels,
|
||||
const Math::Color& color,
|
||||
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
|
||||
const Math::Vector2 delta = end - start;
|
||||
if (delta.SqrMagnitude() <= Math::EPSILON || thicknessPixels <= Math::EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector2 direction = NormalizeVector2(delta);
|
||||
const Math::Vector2 normal(-direction.y, direction.x);
|
||||
const Math::Vector2 offset = normal * (thicknessPixels * 0.5f);
|
||||
AppendScreenQuad(
|
||||
frameData,
|
||||
start + offset,
|
||||
start - offset,
|
||||
end - offset,
|
||||
end + offset,
|
||||
color,
|
||||
depthMode);
|
||||
}
|
||||
|
||||
inline void AppendScreenQuadOutline(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const std::array<Math::Vector2, 4>& corners,
|
||||
float thicknessPixels,
|
||||
const Math::Color& color) {
|
||||
for (size_t index = 0; index < corners.size(); ++index) {
|
||||
AppendScreenSegmentQuad(
|
||||
frameData,
|
||||
corners[index],
|
||||
corners[(index + 1u) % corners.size()],
|
||||
thicknessPixels,
|
||||
color);
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendScreenRectOutline(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& center,
|
||||
const Math::Vector2& halfSize,
|
||||
float thicknessPixels,
|
||||
const Math::Color& color) {
|
||||
const std::array<Math::Vector2, 4> corners = {{
|
||||
Math::Vector2(center.x - halfSize.x, center.y - halfSize.y),
|
||||
Math::Vector2(center.x + halfSize.x, center.y - halfSize.y),
|
||||
Math::Vector2(center.x + halfSize.x, center.y + halfSize.y),
|
||||
Math::Vector2(center.x - halfSize.x, center.y + halfSize.y)
|
||||
}};
|
||||
AppendScreenQuadOutline(frameData, corners, thicknessPixels, color);
|
||||
}
|
||||
|
||||
inline void AppendMoveGizmoHandleRecords(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportMoveGizmoDrawData& drawData,
|
||||
uint64_t entityId) {
|
||||
if (!drawData.visible || entityId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (!handle.visible || handle.axis == SceneViewportGizmoAxis::None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SceneViewportOverlayHandleRecord& record = frameData.handleRecords.emplace_back();
|
||||
record.kind = SceneViewportOverlayHandleKind::MoveAxis;
|
||||
record.handleId = static_cast<uint64_t>(handle.axis);
|
||||
record.entityId = entityId;
|
||||
record.shape = SceneViewportOverlayHandleShape::ScreenSegment;
|
||||
record.priority = kSceneViewportHandlePriorityMoveAxis;
|
||||
record.screenStart = handle.start;
|
||||
record.screenEnd = handle.end;
|
||||
record.hitThicknessPixels = kSceneViewportMoveAxisHitThicknessPixels;
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoPlaneDrawData& plane : drawData.planes) {
|
||||
if (!plane.visible || plane.plane == SceneViewportGizmoPlane::None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SceneViewportOverlayHandleRecord& record = frameData.handleRecords.emplace_back();
|
||||
record.kind = SceneViewportOverlayHandleKind::MovePlane;
|
||||
record.handleId = static_cast<uint64_t>(plane.plane);
|
||||
record.entityId = entityId;
|
||||
record.shape = SceneViewportOverlayHandleShape::ScreenQuad;
|
||||
record.priority = kSceneViewportHandlePriorityMovePlane;
|
||||
record.screenQuad = plane.corners;
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendRotateGizmoHandleRecords(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportRotateGizmoDrawData& drawData,
|
||||
uint64_t entityId) {
|
||||
if (!drawData.visible || entityId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (!handle.visible || handle.axis == SceneViewportRotateGizmoAxis::None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible ||
|
||||
(handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SceneViewportOverlayHandleRecord& record = frameData.handleRecords.emplace_back();
|
||||
record.kind = SceneViewportOverlayHandleKind::RotateAxis;
|
||||
record.handleId = static_cast<uint64_t>(handle.axis);
|
||||
record.entityId = entityId;
|
||||
record.shape = SceneViewportOverlayHandleShape::ScreenSegment;
|
||||
record.priority = kSceneViewportHandlePriorityRotateAxis;
|
||||
record.screenStart = segment.start;
|
||||
record.screenEnd = segment.end;
|
||||
record.hitThicknessPixels = kSceneViewportRotateAxisHitThicknessPixels;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendScaleGizmoHandleRecords(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportScaleGizmoDrawData& drawData,
|
||||
uint64_t entityId) {
|
||||
if (!drawData.visible || entityId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (drawData.centerHandle.visible) {
|
||||
SceneViewportOverlayHandleRecord& uniformRecord = frameData.handleRecords.emplace_back();
|
||||
uniformRecord.kind = SceneViewportOverlayHandleKind::ScaleUniform;
|
||||
uniformRecord.handleId = static_cast<uint64_t>(SceneViewportScaleGizmoHandle::Uniform);
|
||||
uniformRecord.entityId = entityId;
|
||||
uniformRecord.shape = SceneViewportOverlayHandleShape::ScreenRect;
|
||||
uniformRecord.priority = kSceneViewportHandlePriorityScaleUniform;
|
||||
uniformRecord.screenCenter = drawData.centerHandle.center;
|
||||
uniformRecord.screenHalfSize = Math::Vector2(
|
||||
drawData.centerHandle.halfSize + kSceneViewportScaleCapHitPaddingPixels,
|
||||
drawData.centerHandle.halfSize + kSceneViewportScaleCapHitPaddingPixels);
|
||||
}
|
||||
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : drawData.axisHandles) {
|
||||
if (!handle.visible || handle.handle == SceneViewportScaleGizmoHandle::None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SceneViewportOverlayHandleRecord& capRecord = frameData.handleRecords.emplace_back();
|
||||
capRecord.kind = SceneViewportOverlayHandleKind::ScaleAxis;
|
||||
capRecord.handleId = static_cast<uint64_t>(handle.handle);
|
||||
capRecord.entityId = entityId;
|
||||
capRecord.shape = SceneViewportOverlayHandleShape::ScreenRect;
|
||||
capRecord.priority = kSceneViewportHandlePriorityScaleAxisCap;
|
||||
capRecord.screenCenter = handle.capCenter;
|
||||
capRecord.screenHalfSize = Math::Vector2(
|
||||
handle.capHalfSize + kSceneViewportScaleCapHitPaddingPixels,
|
||||
handle.capHalfSize + kSceneViewportScaleCapHitPaddingPixels);
|
||||
|
||||
SceneViewportOverlayHandleRecord& lineRecord = frameData.handleRecords.emplace_back();
|
||||
lineRecord.kind = SceneViewportOverlayHandleKind::ScaleAxis;
|
||||
lineRecord.handleId = static_cast<uint64_t>(handle.handle);
|
||||
lineRecord.entityId = entityId;
|
||||
lineRecord.shape = SceneViewportOverlayHandleShape::ScreenSegment;
|
||||
lineRecord.priority = kSceneViewportHandlePriorityScaleAxisLine;
|
||||
lineRecord.screenStart = handle.start;
|
||||
lineRecord.screenEnd = handle.end;
|
||||
lineRecord.hitThicknessPixels = kSceneViewportScaleAxisHitThicknessPixels;
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendMoveGizmoScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportMoveGizmoDrawData& drawData) {
|
||||
if (!drawData.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoPlaneDrawData& plane : drawData.planes) {
|
||||
if (!plane.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AppendScreenQuad(
|
||||
frameData,
|
||||
plane.corners[0],
|
||||
plane.corners[1],
|
||||
plane.corners[2],
|
||||
plane.corners[3],
|
||||
plane.fillColor);
|
||||
AppendScreenQuadOutline(
|
||||
frameData,
|
||||
plane.corners,
|
||||
plane.active ? 2.6f : (plane.hovered ? 2.0f : 1.4f),
|
||||
plane.outlineColor);
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f);
|
||||
const Math::Vector2 direction = NormalizeVector2(handle.end - handle.start);
|
||||
const float arrowLength =
|
||||
(std::min)(kSceneViewportMoveArrowLengthPixels, (handle.end - handle.start).Magnitude());
|
||||
const Math::Vector2 normal(-direction.y, direction.x);
|
||||
const Math::Vector2 arrowBase = handle.end - direction * arrowLength;
|
||||
const Math::Vector2 arrowLeft = arrowBase + normal * kSceneViewportMoveArrowHalfWidthPixels;
|
||||
const Math::Vector2 arrowRight = arrowBase - normal * kSceneViewportMoveArrowHalfWidthPixels;
|
||||
|
||||
AppendScreenSegmentQuad(frameData, handle.start, arrowBase, thickness, handle.color);
|
||||
AppendScreenTriangle(frameData, handle.end, arrowLeft, arrowRight, handle.color);
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendRotateGizmoHandleScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportRotateGizmoHandleDrawData& handle,
|
||||
bool frontPass) {
|
||||
if (!handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool isViewHandle = handle.axis == SceneViewportRotateGizmoAxis::View;
|
||||
if (isViewHandle && !frontPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float thickness = handle.active ? 3.6f : (handle.hovered ? 3.0f : 2.1f);
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible || (!isViewHandle && segment.frontFacing != frontPass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Math::Color drawColor = handle.color;
|
||||
if (!isViewHandle && !frontPass) {
|
||||
drawColor = LerpColor(handle.color, Math::Color(0.72f, 0.72f, 0.72f, 1.0f), 0.78f);
|
||||
drawColor = WithAlpha(drawColor, handle.active ? 0.55f : 0.38f);
|
||||
} else if (isViewHandle) {
|
||||
drawColor = WithAlpha(drawColor, handle.active ? 0.95f : (handle.hovered ? 0.88f : 0.78f));
|
||||
}
|
||||
|
||||
AppendScreenSegmentQuad(frameData, segment.start, segment.end, thickness, drawColor);
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendRotateGizmoAngleFillScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportRotateGizmoAngleFillDrawData& angleFill) {
|
||||
if (!angleFill.visible || angleFill.arcPointCount < 2u) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t index = 0; index + 1u < angleFill.arcPointCount; ++index) {
|
||||
AppendScreenTriangle(
|
||||
frameData,
|
||||
angleFill.pivot,
|
||||
angleFill.arcPoints[index],
|
||||
angleFill.arcPoints[index + 1u],
|
||||
angleFill.fillColor);
|
||||
}
|
||||
|
||||
for (size_t index = 0; index + 1u < angleFill.arcPointCount; ++index) {
|
||||
AppendScreenSegmentQuad(
|
||||
frameData,
|
||||
angleFill.arcPoints[index],
|
||||
angleFill.arcPoints[index + 1u],
|
||||
2.0f,
|
||||
angleFill.outlineColor);
|
||||
}
|
||||
|
||||
AppendScreenSegmentQuad(
|
||||
frameData,
|
||||
angleFill.pivot,
|
||||
angleFill.arcPoints[0],
|
||||
1.6f,
|
||||
angleFill.outlineColor);
|
||||
AppendScreenSegmentQuad(
|
||||
frameData,
|
||||
angleFill.pivot,
|
||||
angleFill.arcPoints[angleFill.arcPointCount - 1u],
|
||||
1.6f,
|
||||
angleFill.outlineColor);
|
||||
}
|
||||
|
||||
inline void AppendRotateGizmoScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportRotateGizmoDrawData& drawData) {
|
||||
if (!drawData.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (handle.axis == SceneViewportRotateGizmoAxis::View) {
|
||||
AppendRotateGizmoHandleScreenTriangles(frameData, handle, true);
|
||||
}
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
AppendRotateGizmoHandleScreenTriangles(frameData, handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
AppendRotateGizmoAngleFillScreenTriangles(frameData, drawData.angleFill);
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
|
||||
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
AppendRotateGizmoHandleScreenTriangles(frameData, handle, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendScaleGizmoScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportScaleGizmoDrawData& drawData) {
|
||||
if (!drawData.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr Math::Color kScaleCapOutlineColor(24.0f / 255.0f, 24.0f / 255.0f, 24.0f / 255.0f, 220.0f / 255.0f);
|
||||
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : drawData.axisHandles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.2f);
|
||||
const Math::Vector2 direction = NormalizeVector2(handle.capCenter - handle.start);
|
||||
const Math::Vector2 lineEnd = handle.capCenter - direction * handle.capHalfSize;
|
||||
const Math::Vector2 capHalfSize(handle.capHalfSize, handle.capHalfSize);
|
||||
AppendScreenSegmentQuad(frameData, handle.start, lineEnd, thickness, handle.color);
|
||||
AppendScreenRect(frameData, handle.capCenter, capHalfSize, handle.color);
|
||||
AppendScreenRectOutline(
|
||||
frameData,
|
||||
handle.capCenter,
|
||||
capHalfSize,
|
||||
handle.active ? 2.0f : 1.0f,
|
||||
kScaleCapOutlineColor);
|
||||
}
|
||||
|
||||
if (drawData.centerHandle.visible) {
|
||||
const Math::Vector2 halfSize(drawData.centerHandle.halfSize, drawData.centerHandle.halfSize);
|
||||
AppendScreenRect(
|
||||
frameData,
|
||||
drawData.centerHandle.center,
|
||||
halfSize,
|
||||
drawData.centerHandle.fillColor);
|
||||
AppendScreenRectOutline(
|
||||
frameData,
|
||||
drawData.centerHandle.center,
|
||||
halfSize,
|
||||
drawData.centerHandle.active ? 2.0f : 1.0f,
|
||||
drawData.centerHandle.outlineColor);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
inline void AppendTransformGizmoHandleRecords(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) {
|
||||
if (inputs.moveGizmo != nullptr) {
|
||||
Detail::AppendMoveGizmoHandleRecords(frameData, *inputs.moveGizmo, inputs.moveEntityId);
|
||||
}
|
||||
|
||||
if (inputs.rotateGizmo != nullptr) {
|
||||
Detail::AppendRotateGizmoHandleRecords(frameData, *inputs.rotateGizmo, inputs.rotateEntityId);
|
||||
}
|
||||
|
||||
if (inputs.scaleGizmo != nullptr) {
|
||||
Detail::AppendScaleGizmoHandleRecords(frameData, *inputs.scaleGizmo, inputs.scaleEntityId);
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendTransformGizmoScreenTriangles(
|
||||
SceneViewportOverlayFrameData& frameData,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) {
|
||||
if (inputs.moveGizmo != nullptr) {
|
||||
Detail::AppendMoveGizmoScreenTriangles(frameData, *inputs.moveGizmo);
|
||||
}
|
||||
|
||||
if (inputs.rotateGizmo != nullptr) {
|
||||
Detail::AppendRotateGizmoScreenTriangles(frameData, *inputs.rotateGizmo);
|
||||
}
|
||||
|
||||
if (inputs.scaleGizmo != nullptr) {
|
||||
Detail::AppendScaleGizmoScreenTriangles(frameData, *inputs.scaleGizmo);
|
||||
}
|
||||
}
|
||||
|
||||
inline SceneViewportOverlayFrameData BuildSceneViewportTransformGizmoOverlayFrameData(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) {
|
||||
SceneViewportOverlayFrameData frameData = {};
|
||||
frameData.overlay = overlay;
|
||||
AppendTransformGizmoScreenTriangles(frameData, inputs);
|
||||
AppendTransformGizmoHandleRecords(frameData, inputs);
|
||||
return frameData;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
186
editor/src/Viewport/SceneViewportOverlayHitTester.h
Normal file
@@ -0,0 +1,186 @@
|
||||
#pragma once
|
||||
|
||||
#include "SceneViewportEditorOverlayData.h"
|
||||
#include "SceneViewportMath.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace Detail {
|
||||
|
||||
inline bool IsPointInsideSceneViewportScreenRect(
|
||||
const Math::Vector2& point,
|
||||
const Math::Vector2& center,
|
||||
const Math::Vector2& halfSize) {
|
||||
return std::abs(point.x - center.x) <= halfSize.x &&
|
||||
std::abs(point.y - center.y) <= halfSize.y;
|
||||
}
|
||||
|
||||
inline Math::Vector2 ComputeSceneViewportScreenQuadCenter(
|
||||
const std::array<Math::Vector2, 4>& corners) {
|
||||
Math::Vector2 center = Math::Vector2::Zero();
|
||||
for (const Math::Vector2& corner : corners) {
|
||||
center += corner;
|
||||
}
|
||||
return center * 0.25f;
|
||||
}
|
||||
|
||||
inline bool IsPointInsideSceneViewportScreenQuad(
|
||||
const Math::Vector2& point,
|
||||
const std::array<Math::Vector2, 4>& corners) {
|
||||
float previousCross = 0.0f;
|
||||
for (size_t index = 0; index < corners.size(); ++index) {
|
||||
const Math::Vector2 edgeStart = corners[index];
|
||||
const Math::Vector2 edgeEnd = corners[(index + 1u) % corners.size()];
|
||||
const Math::Vector2 edge = edgeEnd - edgeStart;
|
||||
const Math::Vector2 toPoint = point - edgeStart;
|
||||
const float cross = Math::Vector2::Cross(edge, toPoint);
|
||||
if (std::abs(cross) <= Math::EPSILON) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousCross != 0.0f && cross * previousCross < 0.0f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
previousCross = cross;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool TryBuildSceneViewportOverlayHandleHitMetrics(
|
||||
const SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& viewportSize,
|
||||
const SceneViewportOverlayHandleRecord& handleRecord,
|
||||
const Math::Vector2& viewportMousePosition,
|
||||
float& outDistanceSq,
|
||||
float& outDepth) {
|
||||
if (!frameData.overlay.valid ||
|
||||
viewportSize.x <= 1.0f ||
|
||||
viewportSize.y <= 1.0f ||
|
||||
handleRecord.kind == SceneViewportOverlayHandleKind::None) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (handleRecord.shape) {
|
||||
case SceneViewportOverlayHandleShape::WorldRect: {
|
||||
if (handleRecord.sizePixels.x <= Math::EPSILON ||
|
||||
handleRecord.sizePixels.y <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint(
|
||||
frameData.overlay,
|
||||
viewportSize.x,
|
||||
viewportSize.y,
|
||||
handleRecord.worldPosition);
|
||||
if (!projectedPoint.visible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector2 center = projectedPoint.screenPosition;
|
||||
const Math::Vector2 halfSize = handleRecord.sizePixels * 0.5f;
|
||||
if (!IsPointInsideSceneViewportScreenRect(viewportMousePosition, center, halfSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outDistanceSq = (center - viewportMousePosition).SqrMagnitude();
|
||||
outDepth = projectedPoint.ndcDepth;
|
||||
return true;
|
||||
}
|
||||
case SceneViewportOverlayHandleShape::ScreenSegment: {
|
||||
if (handleRecord.hitThicknessPixels <= Math::EPSILON ||
|
||||
(handleRecord.screenEnd - handleRecord.screenStart).SqrMagnitude() <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float maxDistanceSq = handleRecord.hitThicknessPixels * handleRecord.hitThicknessPixels;
|
||||
const float distanceSq = DistanceToSegmentSquared(
|
||||
viewportMousePosition,
|
||||
handleRecord.screenStart,
|
||||
handleRecord.screenEnd);
|
||||
if (distanceSq > maxDistanceSq) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outDistanceSq = distanceSq;
|
||||
outDepth = handleRecord.sortDepth;
|
||||
return true;
|
||||
}
|
||||
case SceneViewportOverlayHandleShape::ScreenRect: {
|
||||
if (handleRecord.screenHalfSize.x <= Math::EPSILON ||
|
||||
handleRecord.screenHalfSize.y <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsPointInsideSceneViewportScreenRect(
|
||||
viewportMousePosition,
|
||||
handleRecord.screenCenter,
|
||||
handleRecord.screenHalfSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outDistanceSq = (handleRecord.screenCenter - viewportMousePosition).SqrMagnitude();
|
||||
outDepth = handleRecord.sortDepth;
|
||||
return true;
|
||||
}
|
||||
case SceneViewportOverlayHandleShape::ScreenQuad: {
|
||||
if (!IsPointInsideSceneViewportScreenQuad(viewportMousePosition, handleRecord.screenQuad)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector2 center = ComputeSceneViewportScreenQuadCenter(handleRecord.screenQuad);
|
||||
outDistanceSq = (center - viewportMousePosition).SqrMagnitude();
|
||||
outDepth = handleRecord.sortDepth;
|
||||
return true;
|
||||
}
|
||||
case SceneViewportOverlayHandleShape::None:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
inline SceneViewportOverlayHandleHitResult HitTestSceneViewportOverlayHandles(
|
||||
const SceneViewportOverlayFrameData& frameData,
|
||||
const Math::Vector2& viewportSize,
|
||||
const Math::Vector2& viewportMousePosition) {
|
||||
constexpr float kMetricEpsilon = 0.001f;
|
||||
|
||||
SceneViewportOverlayHandleHitResult result = {};
|
||||
for (const SceneViewportOverlayHandleRecord& handleRecord : frameData.handleRecords) {
|
||||
float distanceSq = 0.0f;
|
||||
float depth = 0.0f;
|
||||
if (!Detail::TryBuildSceneViewportOverlayHandleHitMetrics(
|
||||
frameData,
|
||||
viewportSize,
|
||||
handleRecord,
|
||||
viewportMousePosition,
|
||||
distanceSq,
|
||||
depth)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handleRecord.priority > result.priority ||
|
||||
(handleRecord.priority == result.priority &&
|
||||
(depth + kMetricEpsilon < result.depth ||
|
||||
(std::abs(depth - result.depth) <= kMetricEpsilon && distanceSq < result.distanceSq)))) {
|
||||
result.kind = handleRecord.kind;
|
||||
result.handleId = handleRecord.handleId;
|
||||
result.entityId = handleRecord.entityId;
|
||||
result.priority = handleRecord.priority;
|
||||
result.distanceSq = distanceSq;
|
||||
result.depth = depth;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -1,305 +1,16 @@
|
||||
#include "SceneViewportOverlayRenderer.h"
|
||||
#include "SceneViewportOrientationGizmo.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include "SceneViewportOrientationGizmo.h"
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kMoveGizmoArrowLength = 14.0f;
|
||||
constexpr float kMoveGizmoArrowHalfWidth = 7.0f;
|
||||
|
||||
ImU32 ToImGuiColor(const Math::Color& color) {
|
||||
const auto toChannel = [](float value) -> int {
|
||||
return static_cast<int>(std::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
};
|
||||
|
||||
return IM_COL32(
|
||||
toChannel(color.r),
|
||||
toChannel(color.g),
|
||||
toChannel(color.b),
|
||||
toChannel(color.a));
|
||||
}
|
||||
|
||||
Math::Color WithAlpha(const Math::Color& color, float alpha) {
|
||||
return Math::Color(color.r, color.g, color.b, alpha);
|
||||
}
|
||||
|
||||
Math::Color LerpColor(const Math::Color& a, const Math::Color& b, float t) {
|
||||
return Math::Color(
|
||||
a.r + (b.r - a.r) * t,
|
||||
a.g + (b.g - a.g) * t,
|
||||
a.b + (b.b - a.b) * t,
|
||||
a.a + (b.a - a.a) * t);
|
||||
}
|
||||
|
||||
ImVec2 NormalizeImVec2(const ImVec2& value, const ImVec2& fallback = ImVec2(1.0f, 0.0f)) {
|
||||
const float lengthSq = value.x * value.x + value.y * value.y;
|
||||
if (lengthSq <= 1e-6f) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const float inverseLength = 1.0f / std::sqrt(lengthSq);
|
||||
return ImVec2(value.x * inverseLength, value.y * inverseLength);
|
||||
}
|
||||
|
||||
void DrawSceneMoveGizmoPlane(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportMoveGizmoPlaneDrawData& plane) {
|
||||
if (drawList == nullptr || !plane.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImVec2 points[4] = {};
|
||||
for (size_t index = 0; index < plane.corners.size(); ++index) {
|
||||
points[index] = ImVec2(
|
||||
viewportMin.x + plane.corners[index].x,
|
||||
viewportMin.y + plane.corners[index].y);
|
||||
}
|
||||
|
||||
drawList->AddConvexPolyFilled(points, 4, ToImGuiColor(plane.fillColor));
|
||||
drawList->AddPolyline(
|
||||
points,
|
||||
4,
|
||||
ToImGuiColor(plane.outlineColor),
|
||||
true,
|
||||
plane.active ? 2.6f : (plane.hovered ? 2.0f : 1.4f));
|
||||
}
|
||||
|
||||
void DrawSceneMoveGizmoAxis(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportMoveGizmoHandleDrawData& handle) {
|
||||
if (drawList == nullptr || !handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImU32 color = ToImGuiColor(handle.color);
|
||||
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f);
|
||||
const ImVec2 start(viewportMin.x + handle.start.x, viewportMin.y + handle.start.y);
|
||||
const ImVec2 end(viewportMin.x + handle.end.x, viewportMin.y + handle.end.y);
|
||||
const ImVec2 direction = NormalizeImVec2(ImVec2(end.x - start.x, end.y - start.y));
|
||||
const ImVec2 normal(-direction.y, direction.x);
|
||||
const ImVec2 arrowBase(
|
||||
end.x - direction.x * kMoveGizmoArrowLength,
|
||||
end.y - direction.y * kMoveGizmoArrowLength);
|
||||
const ImVec2 arrowLeft(
|
||||
arrowBase.x + normal.x * kMoveGizmoArrowHalfWidth,
|
||||
arrowBase.y + normal.y * kMoveGizmoArrowHalfWidth);
|
||||
const ImVec2 arrowRight(
|
||||
arrowBase.x - normal.x * kMoveGizmoArrowHalfWidth,
|
||||
arrowBase.y - normal.y * kMoveGizmoArrowHalfWidth);
|
||||
|
||||
drawList->AddLine(start, arrowBase, color, thickness);
|
||||
const ImVec2 triangle[3] = { end, arrowLeft, arrowRight };
|
||||
drawList->AddConvexPolyFilled(triangle, 3, color);
|
||||
}
|
||||
|
||||
void DrawSceneRotateGizmoHandle(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportRotateGizmoHandleDrawData& handle,
|
||||
bool frontPass) {
|
||||
if (drawList == nullptr || !handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool isViewHandle = handle.axis == SceneViewportRotateGizmoAxis::View;
|
||||
if (isViewHandle && !frontPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float thickness = handle.active ? 3.6f : (handle.hovered ? 3.0f : 2.1f);
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible || (!isViewHandle && segment.frontFacing != frontPass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Math::Color drawColor = handle.color;
|
||||
if (!isViewHandle && !frontPass) {
|
||||
drawColor = LerpColor(handle.color, Math::Color(0.72f, 0.72f, 0.72f, 1.0f), 0.78f);
|
||||
drawColor = WithAlpha(drawColor, handle.active ? 0.55f : 0.38f);
|
||||
} else if (isViewHandle) {
|
||||
drawColor = WithAlpha(drawColor, handle.active ? 0.95f : (handle.hovered ? 0.88f : 0.78f));
|
||||
}
|
||||
|
||||
drawList->AddLine(
|
||||
ImVec2(viewportMin.x + segment.start.x, viewportMin.y + segment.start.y),
|
||||
ImVec2(viewportMin.x + segment.end.x, viewportMin.y + segment.end.y),
|
||||
ToImGuiColor(drawColor),
|
||||
thickness);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSceneRotateGizmoAngleFill(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportRotateGizmoAngleFillDrawData& angleFill) {
|
||||
if (drawList == nullptr || !angleFill.visible || angleFill.arcPointCount < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImVec2 pivot(viewportMin.x + angleFill.pivot.x, viewportMin.y + angleFill.pivot.y);
|
||||
const ImU32 fillColor = ToImGuiColor(angleFill.fillColor);
|
||||
const ImU32 outlineColor = ToImGuiColor(angleFill.outlineColor);
|
||||
|
||||
ImVec2 fillPoints[kSceneViewportRotateGizmoAngleFillPointCount + 1] = {};
|
||||
fillPoints[0] = pivot;
|
||||
for (size_t index = 0; index < angleFill.arcPointCount; ++index) {
|
||||
fillPoints[index + 1] = ImVec2(
|
||||
viewportMin.x + angleFill.arcPoints[index].x,
|
||||
viewportMin.y + angleFill.arcPoints[index].y);
|
||||
}
|
||||
drawList->AddConvexPolyFilled(
|
||||
fillPoints,
|
||||
static_cast<int>(angleFill.arcPointCount + 1),
|
||||
fillColor);
|
||||
|
||||
for (size_t index = 0; index + 1 < angleFill.arcPointCount; ++index) {
|
||||
drawList->AddLine(
|
||||
ImVec2(viewportMin.x + angleFill.arcPoints[index].x, viewportMin.y + angleFill.arcPoints[index].y),
|
||||
ImVec2(
|
||||
viewportMin.x + angleFill.arcPoints[index + 1].x,
|
||||
viewportMin.y + angleFill.arcPoints[index + 1].y),
|
||||
outlineColor,
|
||||
2.0f);
|
||||
}
|
||||
|
||||
drawList->AddLine(
|
||||
pivot,
|
||||
ImVec2(viewportMin.x + angleFill.arcPoints.front().x, viewportMin.y + angleFill.arcPoints.front().y),
|
||||
outlineColor,
|
||||
1.6f);
|
||||
drawList->AddLine(
|
||||
pivot,
|
||||
ImVec2(
|
||||
viewportMin.x + angleFill.arcPoints[angleFill.arcPointCount - 1].x,
|
||||
viewportMin.y + angleFill.arcPoints[angleFill.arcPointCount - 1].y),
|
||||
outlineColor,
|
||||
1.6f);
|
||||
}
|
||||
|
||||
void DrawSceneScaleGizmoAxis(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportScaleGizmoAxisHandleDrawData& handle) {
|
||||
if (drawList == nullptr || !handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImU32 color = ToImGuiColor(handle.color);
|
||||
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.2f);
|
||||
const ImVec2 start(viewportMin.x + handle.start.x, viewportMin.y + handle.start.y);
|
||||
const ImVec2 capCenter(viewportMin.x + handle.capCenter.x, viewportMin.y + handle.capCenter.y);
|
||||
const ImVec2 direction = NormalizeImVec2(ImVec2(capCenter.x - start.x, capCenter.y - start.y));
|
||||
const ImVec2 lineEnd(
|
||||
capCenter.x - direction.x * handle.capHalfSize,
|
||||
capCenter.y - direction.y * handle.capHalfSize);
|
||||
const ImVec2 capMin(capCenter.x - handle.capHalfSize, capCenter.y - handle.capHalfSize);
|
||||
const ImVec2 capMax(capCenter.x + handle.capHalfSize, capCenter.y + handle.capHalfSize);
|
||||
|
||||
drawList->AddLine(start, lineEnd, color, thickness);
|
||||
drawList->AddRectFilled(capMin, capMax, color, 1.2f);
|
||||
drawList->AddRect(capMin, capMax, IM_COL32(24, 24, 24, 220), 1.2f, 0, handle.active ? 2.0f : 1.0f);
|
||||
}
|
||||
|
||||
void DrawSceneScaleGizmoCenterHandle(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportScaleGizmoCenterHandleDrawData& handle) {
|
||||
if (drawList == nullptr || !handle.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImVec2 center(viewportMin.x + handle.center.x, viewportMin.y + handle.center.y);
|
||||
const ImVec2 handleMin(center.x - handle.halfSize, center.y - handle.halfSize);
|
||||
const ImVec2 handleMax(center.x + handle.halfSize, center.y + handle.halfSize);
|
||||
drawList->AddRectFilled(handleMin, handleMax, ToImGuiColor(handle.fillColor), 1.2f);
|
||||
drawList->AddRect(
|
||||
handleMin,
|
||||
handleMax,
|
||||
ToImGuiColor(handle.outlineColor),
|
||||
1.2f,
|
||||
0,
|
||||
handle.active ? 2.0f : 1.0f);
|
||||
}
|
||||
|
||||
void DrawSceneMoveGizmo(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportMoveGizmoDrawData& moveGizmo) {
|
||||
if (drawList == nullptr || !moveGizmo.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoPlaneDrawData& plane : moveGizmo.planes) {
|
||||
DrawSceneMoveGizmoPlane(drawList, viewportMin, plane);
|
||||
}
|
||||
|
||||
for (const SceneViewportMoveGizmoHandleDrawData& handle : moveGizmo.handles) {
|
||||
DrawSceneMoveGizmoAxis(drawList, viewportMin, handle);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSceneRotateGizmo(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportRotateGizmoDrawData& rotateGizmo) {
|
||||
if (drawList == nullptr || !rotateGizmo.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) {
|
||||
if (handle.axis == SceneViewportRotateGizmoAxis::View) {
|
||||
DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, true);
|
||||
}
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) {
|
||||
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
DrawSceneRotateGizmoAngleFill(drawList, viewportMin, rotateGizmo.angleFill);
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : rotateGizmo.handles) {
|
||||
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
DrawSceneRotateGizmoHandle(drawList, viewportMin, handle, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSceneScaleGizmo(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& viewportMin,
|
||||
const SceneViewportScaleGizmoDrawData& scaleGizmo) {
|
||||
if (drawList == nullptr || !scaleGizmo.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : scaleGizmo.axisHandles) {
|
||||
DrawSceneScaleGizmoAxis(drawList, viewportMin, handle);
|
||||
}
|
||||
|
||||
DrawSceneScaleGizmoCenterHandle(drawList, viewportMin, scaleGizmo.centerHandle);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DrawSceneViewportOverlay(
|
||||
ImDrawList* drawList,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportMax,
|
||||
const ImVec2& viewportSize,
|
||||
const SceneViewportMoveGizmoDrawData* moveGizmo,
|
||||
const SceneViewportRotateGizmoDrawData* rotateGizmo,
|
||||
const SceneViewportScaleGizmoDrawData* scaleGizmo) {
|
||||
const ImVec2& viewportSize) {
|
||||
if (drawList == nullptr || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) {
|
||||
return;
|
||||
}
|
||||
@@ -308,15 +19,6 @@ void DrawSceneViewportOverlay(
|
||||
if (overlay.valid) {
|
||||
DrawSceneViewportOrientationGizmo(drawList, overlay, viewportMin, viewportMax);
|
||||
}
|
||||
if (moveGizmo != nullptr) {
|
||||
DrawSceneMoveGizmo(drawList, viewportMin, *moveGizmo);
|
||||
}
|
||||
if (rotateGizmo != nullptr) {
|
||||
DrawSceneRotateGizmo(drawList, viewportMin, *rotateGizmo);
|
||||
}
|
||||
if (scaleGizmo != nullptr) {
|
||||
DrawSceneScaleGizmo(drawList, viewportMin, *scaleGizmo);
|
||||
}
|
||||
drawList->PopClipRect();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "IViewportHostService.h"
|
||||
#include "SceneViewportMoveGizmo.h"
|
||||
#include "SceneViewportRotateGizmo.h"
|
||||
#include "SceneViewportScaleGizmo.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
@@ -15,10 +12,7 @@ void DrawSceneViewportOverlay(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportMax,
|
||||
const ImVec2& viewportSize,
|
||||
const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr,
|
||||
const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr,
|
||||
const SceneViewportScaleGizmoDrawData* scaleGizmo = nullptr);
|
||||
const ImVec2& viewportSize);
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -19,6 +19,21 @@ constexpr float kRotateGizmoViewRadiusPixels = 106.0f;
|
||||
constexpr float kRotateGizmoHoverThresholdPixels = 9.0f;
|
||||
constexpr float kRotateGizmoAngleFillMinRadians = 0.01f;
|
||||
|
||||
Math::Vector3 GetBaseRotateAxisVector(SceneViewportRotateGizmoAxis axis) {
|
||||
switch (axis) {
|
||||
case SceneViewportRotateGizmoAxis::X:
|
||||
return Math::Vector3::Right();
|
||||
case SceneViewportRotateGizmoAxis::Y:
|
||||
return Math::Vector3::Up();
|
||||
case SceneViewportRotateGizmoAxis::Z:
|
||||
return Math::Vector3::Forward();
|
||||
case SceneViewportRotateGizmoAxis::View:
|
||||
case SceneViewportRotateGizmoAxis::None:
|
||||
default:
|
||||
return Math::Vector3::Zero();
|
||||
}
|
||||
}
|
||||
|
||||
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
|
||||
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
|
||||
}
|
||||
@@ -30,6 +45,22 @@ bool IsMouseInsideViewport(const SceneViewportRotateGizmoContext& context) {
|
||||
context.mousePosition.y <= context.viewportSize.y;
|
||||
}
|
||||
|
||||
Math::Quaternion ComputeStableWorldRotation(const Components::GameObject* gameObject) {
|
||||
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
|
||||
return Math::Quaternion::Identity();
|
||||
}
|
||||
|
||||
const Components::TransformComponent* transform = gameObject->GetTransform();
|
||||
Math::Quaternion worldRotation = transform->GetLocalRotation();
|
||||
for (const Components::TransformComponent* parent = transform->GetParent();
|
||||
parent != nullptr;
|
||||
parent = parent->GetParent()) {
|
||||
worldRotation = parent->GetLocalRotation() * worldRotation;
|
||||
}
|
||||
|
||||
return worldRotation.Normalized();
|
||||
}
|
||||
|
||||
Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) {
|
||||
switch (axis) {
|
||||
case SceneViewportRotateGizmoAxis::X:
|
||||
@@ -48,14 +79,13 @@ Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) {
|
||||
|
||||
Math::Vector3 GetRotateAxisVector(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const SceneViewportOverlayData& overlay) {
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Quaternion& axisOrientation) {
|
||||
switch (axis) {
|
||||
case SceneViewportRotateGizmoAxis::X:
|
||||
return Math::Vector3::Right();
|
||||
case SceneViewportRotateGizmoAxis::Y:
|
||||
return Math::Vector3::Up();
|
||||
case SceneViewportRotateGizmoAxis::Z:
|
||||
return Math::Vector3::Forward();
|
||||
return NormalizeVector3(axisOrientation * GetBaseRotateAxisVector(axis), GetBaseRotateAxisVector(axis));
|
||||
case SceneViewportRotateGizmoAxis::View:
|
||||
return NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward());
|
||||
case SceneViewportRotateGizmoAxis::None:
|
||||
@@ -67,20 +97,21 @@ Math::Vector3 GetRotateAxisVector(
|
||||
bool GetRotateRingBasis(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Quaternion& axisOrientation,
|
||||
Math::Vector3& outBasisA,
|
||||
Math::Vector3& outBasisB) {
|
||||
switch (axis) {
|
||||
case SceneViewportRotateGizmoAxis::X:
|
||||
outBasisA = Math::Vector3::Up();
|
||||
outBasisB = Math::Vector3::Forward();
|
||||
outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up());
|
||||
outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward());
|
||||
return true;
|
||||
case SceneViewportRotateGizmoAxis::Y:
|
||||
outBasisA = Math::Vector3::Forward();
|
||||
outBasisB = Math::Vector3::Right();
|
||||
outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward());
|
||||
outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right());
|
||||
return true;
|
||||
case SceneViewportRotateGizmoAxis::Z:
|
||||
outBasisA = Math::Vector3::Right();
|
||||
outBasisB = Math::Vector3::Up();
|
||||
outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right());
|
||||
outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up());
|
||||
return true;
|
||||
case SceneViewportRotateGizmoAxis::View:
|
||||
outBasisA = NormalizeVector3(overlay.cameraRight, Math::Vector3::Right());
|
||||
@@ -147,11 +178,12 @@ SceneViewportRotateGizmoAxis GetRotateAxisForIndex(size_t index) {
|
||||
bool TryComputeRingAngleFromWorldDirection(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Quaternion& axisOrientation,
|
||||
const Math::Vector3& directionWorld,
|
||||
float& outAngle) {
|
||||
Math::Vector3 basisA = Math::Vector3::Zero();
|
||||
Math::Vector3 basisB = Math::Vector3::Zero();
|
||||
if (!GetRotateRingBasis(axis, overlay, basisA, basisB)) {
|
||||
if (!GetRotateRingBasis(axis, overlay, axisOrientation, basisA, basisB)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -171,7 +203,7 @@ bool TryComputeRingAngleFromWorldDirection(
|
||||
void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) {
|
||||
BuildDrawData(context);
|
||||
if (m_activeAxis == SceneViewportRotateGizmoAxis::None && IsMouseInsideViewport(context)) {
|
||||
m_hoveredAxis = HitTestAxis(context.mousePosition);
|
||||
m_hoveredAxis = EvaluateHit(context.mousePosition).axis;
|
||||
} else if (m_activeAxis == SceneViewportRotateGizmoAxis::None) {
|
||||
m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
|
||||
} else {
|
||||
@@ -190,8 +222,8 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay);
|
||||
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
|
||||
const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay, context.axisOrientation);
|
||||
if (worldAxis.SqrMagnitude() <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
@@ -225,6 +257,7 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
|
||||
if (!TryComputeRingAngleFromWorldDirection(
|
||||
m_hoveredAxis,
|
||||
context.overlay,
|
||||
context.axisOrientation,
|
||||
startDirection,
|
||||
startRingAngle)) {
|
||||
return false;
|
||||
@@ -238,12 +271,31 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
|
||||
|
||||
m_activeAxis = m_hoveredAxis;
|
||||
m_activeEntityId = context.selectedObject->GetID();
|
||||
m_localSpace = context.localSpace && m_hoveredAxis != SceneViewportRotateGizmoAxis::View;
|
||||
m_rotateAroundSharedPivot = context.rotateAroundSharedPivot;
|
||||
m_activeWorldAxis = worldAxis.Normalized();
|
||||
m_screenSpaceDrag = useScreenSpaceDrag;
|
||||
m_dragPlane = dragPlane;
|
||||
m_dragStartWorldRotation = context.selectedObject->GetTransform()->GetRotation();
|
||||
m_dragStartRingAngle = startRingAngle;
|
||||
m_dragCurrentDeltaRadians = 0.0f;
|
||||
m_dragStartPivotWorldPosition = pivotWorldPosition;
|
||||
m_dragObjects = context.selectedObjects;
|
||||
if (m_dragObjects.empty()) {
|
||||
m_dragObjects.push_back(context.selectedObject);
|
||||
}
|
||||
m_dragStartWorldPositions.clear();
|
||||
m_dragStartWorldRotations.clear();
|
||||
m_dragStartWorldPositions.reserve(m_dragObjects.size());
|
||||
m_dragStartWorldRotations.reserve(m_dragObjects.size());
|
||||
for (Components::GameObject* gameObject : m_dragObjects) {
|
||||
if (gameObject != nullptr && gameObject->GetTransform() != nullptr) {
|
||||
m_dragStartWorldPositions.push_back(gameObject->GetTransform()->GetPosition());
|
||||
m_dragStartWorldRotations.push_back(gameObject->GetTransform()->GetRotation());
|
||||
} else {
|
||||
m_dragStartWorldPositions.push_back(Math::Vector3::Zero());
|
||||
m_dragStartWorldRotations.push_back(Math::Quaternion::Identity());
|
||||
}
|
||||
}
|
||||
RefreshHandleState();
|
||||
return true;
|
||||
}
|
||||
@@ -251,7 +303,10 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
|
||||
void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& context) {
|
||||
if (m_activeAxis == SceneViewportRotateGizmoAxis::None ||
|
||||
context.selectedObject == nullptr ||
|
||||
context.selectedObject->GetID() != m_activeEntityId) {
|
||||
context.selectedObject->GetID() != m_activeEntityId ||
|
||||
m_dragObjects.empty() ||
|
||||
m_dragObjects.size() != m_dragStartWorldPositions.size() ||
|
||||
m_dragObjects.size() != m_dragStartWorldRotations.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -275,7 +330,7 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = m_dragStartPivotWorldPosition;
|
||||
const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance);
|
||||
const Math::Vector3 currentDirection = Math::Vector3::ProjectOnPlane(hitPoint - pivotWorldPosition, m_activeWorldAxis);
|
||||
if (currentDirection.SqrMagnitude() <= Math::EPSILON) {
|
||||
@@ -285,6 +340,7 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
|
||||
if (!TryComputeRingAngleFromWorldDirection(
|
||||
m_activeAxis,
|
||||
context.overlay,
|
||||
context.axisOrientation,
|
||||
currentDirection,
|
||||
currentRingAngle)) {
|
||||
return;
|
||||
@@ -293,9 +349,37 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
|
||||
|
||||
const float deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle);
|
||||
m_dragCurrentDeltaRadians = deltaRadians;
|
||||
const Math::Quaternion deltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians);
|
||||
context.selectedObject->GetTransform()->SetRotation(deltaRotation * m_dragStartWorldRotation);
|
||||
BuildDrawData(context);
|
||||
const Math::Quaternion worldDeltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians);
|
||||
const Math::Vector3 localAxis = GetBaseRotateAxisVector(m_activeAxis);
|
||||
const Math::Quaternion localDeltaRotation =
|
||||
localAxis.SqrMagnitude() > Math::EPSILON
|
||||
? Math::Quaternion::FromAxisAngle(localAxis, deltaRadians)
|
||||
: Math::Quaternion::Identity();
|
||||
for (size_t index = 0; index < m_dragObjects.size(); ++index) {
|
||||
Components::GameObject* gameObject = m_dragObjects[index];
|
||||
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m_rotateAroundSharedPivot) {
|
||||
gameObject->GetTransform()->SetPosition(
|
||||
m_dragStartPivotWorldPosition +
|
||||
worldDeltaRotation * (m_dragStartWorldPositions[index] - m_dragStartPivotWorldPosition));
|
||||
} else {
|
||||
gameObject->GetTransform()->SetPosition(m_dragStartWorldPositions[index]);
|
||||
}
|
||||
if (m_localSpace && m_activeAxis != SceneViewportRotateGizmoAxis::View) {
|
||||
gameObject->GetTransform()->SetRotation(m_dragStartWorldRotations[index] * localDeltaRotation);
|
||||
} else {
|
||||
gameObject->GetTransform()->SetRotation(worldDeltaRotation * m_dragStartWorldRotations[index]);
|
||||
}
|
||||
}
|
||||
SceneViewportRotateGizmoContext drawContext = context;
|
||||
drawContext.pivotWorldPosition = m_dragStartPivotWorldPosition;
|
||||
if (drawContext.localSpace && drawContext.selectedObject != nullptr) {
|
||||
drawContext.axisOrientation = ComputeStableWorldRotation(drawContext.selectedObject);
|
||||
}
|
||||
BuildDrawData(drawContext);
|
||||
m_hoveredAxis = m_activeAxis;
|
||||
RefreshHandleState();
|
||||
}
|
||||
@@ -312,10 +396,15 @@ void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) {
|
||||
m_activeAxis = SceneViewportRotateGizmoAxis::None;
|
||||
m_activeEntityId = 0;
|
||||
m_screenSpaceDrag = false;
|
||||
m_localSpace = false;
|
||||
m_rotateAroundSharedPivot = false;
|
||||
m_activeWorldAxis = Math::Vector3::Zero();
|
||||
m_dragStartWorldRotation = Math::Quaternion::Identity();
|
||||
m_dragStartRingAngle = 0.0f;
|
||||
m_dragCurrentDeltaRadians = 0.0f;
|
||||
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
m_dragObjects.clear();
|
||||
m_dragStartWorldPositions.clear();
|
||||
m_dragStartWorldRotations.clear();
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
@@ -327,10 +416,15 @@ void SceneViewportRotateGizmo::CancelDrag(IUndoManager* undoManager) {
|
||||
m_activeAxis = SceneViewportRotateGizmoAxis::None;
|
||||
m_activeEntityId = 0;
|
||||
m_screenSpaceDrag = false;
|
||||
m_localSpace = false;
|
||||
m_rotateAroundSharedPivot = false;
|
||||
m_activeWorldAxis = Math::Vector3::Zero();
|
||||
m_dragStartWorldRotation = Math::Quaternion::Identity();
|
||||
m_dragStartRingAngle = 0.0f;
|
||||
m_dragCurrentDeltaRadians = 0.0f;
|
||||
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
m_dragObjects.clear();
|
||||
m_dragStartWorldPositions.clear();
|
||||
m_dragStartWorldRotations.clear();
|
||||
m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
|
||||
RefreshHandleState();
|
||||
}
|
||||
@@ -351,18 +445,57 @@ const SceneViewportRotateGizmoDrawData& SceneViewportRotateGizmo::GetDrawData()
|
||||
return m_drawData;
|
||||
}
|
||||
|
||||
SceneViewportRotateGizmoHitResult SceneViewportRotateGizmo::EvaluateHit(const Math::Vector2& mousePosition) const {
|
||||
SceneViewportRotateGizmoHitResult result = {};
|
||||
if (!m_drawData.visible) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const float hoverThresholdSq = kRotateGizmoHoverThresholdPixels * kRotateGizmoHoverThresholdPixels;
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible ||
|
||||
(handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end);
|
||||
if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.axis = handle.axis;
|
||||
result.distanceSq = distanceSq;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void SceneViewportRotateGizmo::SetHoveredHandle(SceneViewportRotateGizmoAxis axis) {
|
||||
if (m_activeAxis != SceneViewportRotateGizmoAxis::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_hoveredAxis = axis;
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoContext& context) {
|
||||
m_drawData = {};
|
||||
|
||||
const Components::GameObject* selectedObject = context.selectedObject;
|
||||
if (selectedObject == nullptr ||
|
||||
if ((context.selectedObject == nullptr && context.selectedObjects.empty()) ||
|
||||
!context.overlay.valid ||
|
||||
context.viewportSize.x <= 1.0f ||
|
||||
context.viewportSize.y <= 1.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
|
||||
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
@@ -383,6 +516,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
|
||||
m_drawData.visible = true;
|
||||
m_drawData.pivot = projectedPivot.screenPosition;
|
||||
const bool hasActiveDragFeedback =
|
||||
!context.localSpace &&
|
||||
m_activeAxis != SceneViewportRotateGizmoAxis::None &&
|
||||
m_activeAxis != SceneViewportRotateGizmoAxis::View &&
|
||||
std::abs(m_dragCurrentDeltaRadians) > Math::EPSILON;
|
||||
@@ -398,7 +532,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
|
||||
|
||||
Math::Vector3 basisA = Math::Vector3::Zero();
|
||||
Math::Vector3 basisB = Math::Vector3::Zero();
|
||||
if (!GetRotateRingBasis(handle.axis, context.overlay, basisA, basisB)) {
|
||||
if (!GetRotateRingBasis(handle.axis, context.overlay, context.axisOrientation, basisA, basisB)) {
|
||||
continue;
|
||||
}
|
||||
if (hasActiveDragFeedback && handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
@@ -468,7 +602,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
|
||||
|
||||
Math::Vector3 basisA = Math::Vector3::Zero();
|
||||
Math::Vector3 basisB = Math::Vector3::Zero();
|
||||
if (GetRotateRingBasis(m_activeAxis, context.overlay, basisA, basisB)) {
|
||||
if (GetRotateRingBasis(m_activeAxis, context.overlay, context.axisOrientation, basisA, basisB)) {
|
||||
const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(m_activeAxis);
|
||||
const float sweepRadians = NormalizeSignedAngleRadians(m_dragCurrentDeltaRadians);
|
||||
const float sweepAbs = std::abs(sweepRadians);
|
||||
@@ -519,39 +653,6 @@ void SceneViewportRotateGizmo::RefreshHandleState() {
|
||||
}
|
||||
}
|
||||
|
||||
SceneViewportRotateGizmoAxis SceneViewportRotateGizmo::HitTestAxis(const Math::Vector2& mousePosition) const {
|
||||
if (!m_drawData.visible) {
|
||||
return SceneViewportRotateGizmoAxis::None;
|
||||
}
|
||||
|
||||
const float hoverThresholdSq = kRotateGizmoHoverThresholdPixels * kRotateGizmoHoverThresholdPixels;
|
||||
SceneViewportRotateGizmoAxis bestAxis = SceneViewportRotateGizmoAxis::None;
|
||||
float bestDistanceSq = hoverThresholdSq;
|
||||
|
||||
for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
|
||||
if (!segment.visible ||
|
||||
(handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end);
|
||||
if (distanceSq > bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestAxis = handle.axis;
|
||||
}
|
||||
}
|
||||
|
||||
return bestAxis;
|
||||
}
|
||||
|
||||
bool SceneViewportRotateGizmo::TryGetClosestRingAngle(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const Math::Vector2& mousePosition,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Components {
|
||||
@@ -71,6 +72,20 @@ struct SceneViewportRotateGizmoContext {
|
||||
Math::Vector2 viewportSize = Math::Vector2::Zero();
|
||||
Math::Vector2 mousePosition = Math::Vector2::Zero();
|
||||
Components::GameObject* selectedObject = nullptr;
|
||||
std::vector<Components::GameObject*> selectedObjects = {};
|
||||
Math::Vector3 pivotWorldPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion axisOrientation = Math::Quaternion::Identity();
|
||||
bool localSpace = false;
|
||||
bool rotateAroundSharedPivot = false;
|
||||
};
|
||||
|
||||
struct SceneViewportRotateGizmoHitResult {
|
||||
SceneViewportRotateGizmoAxis axis = SceneViewportRotateGizmoAxis::None;
|
||||
float distanceSq = Math::FLOAT_MAX;
|
||||
|
||||
bool HasHit() const {
|
||||
return axis != SceneViewportRotateGizmoAxis::None;
|
||||
}
|
||||
};
|
||||
|
||||
class SceneViewportRotateGizmo {
|
||||
@@ -85,11 +100,12 @@ public:
|
||||
bool IsActive() const;
|
||||
uint64_t GetActiveEntityId() const;
|
||||
const SceneViewportRotateGizmoDrawData& GetDrawData() const;
|
||||
SceneViewportRotateGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const;
|
||||
void SetHoveredHandle(SceneViewportRotateGizmoAxis axis);
|
||||
|
||||
private:
|
||||
void BuildDrawData(const SceneViewportRotateGizmoContext& context);
|
||||
void RefreshHandleState();
|
||||
SceneViewportRotateGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const;
|
||||
bool TryGetClosestRingAngle(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const Math::Vector2& mousePosition,
|
||||
@@ -101,11 +117,16 @@ private:
|
||||
SceneViewportRotateGizmoAxis m_activeAxis = SceneViewportRotateGizmoAxis::None;
|
||||
uint64_t m_activeEntityId = 0;
|
||||
bool m_screenSpaceDrag = false;
|
||||
bool m_localSpace = false;
|
||||
bool m_rotateAroundSharedPivot = false;
|
||||
Math::Vector3 m_activeWorldAxis = Math::Vector3::Zero();
|
||||
Math::Plane m_dragPlane = {};
|
||||
Math::Quaternion m_dragStartWorldRotation = Math::Quaternion::Identity();
|
||||
float m_dragStartRingAngle = 0.0f;
|
||||
float m_dragCurrentDeltaRadians = 0.0f;
|
||||
Math::Vector3 m_dragStartPivotWorldPosition = Math::Vector3::Zero();
|
||||
std::vector<Components::GameObject*> m_dragObjects = {};
|
||||
std::vector<Math::Vector3> m_dragStartWorldPositions = {};
|
||||
std::vector<Math::Quaternion> m_dragStartWorldRotations = {};
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
|
||||
@@ -130,7 +130,7 @@ float ComputeVisualScaleFactor(float current, float start) {
|
||||
void SceneViewportScaleGizmo::Update(const SceneViewportScaleGizmoContext& context) {
|
||||
BuildDrawData(context);
|
||||
if (m_activeHandle == SceneViewportScaleGizmoHandle::None && IsMouseInsideViewport(context)) {
|
||||
m_hoveredHandle = HitTestHandle(context.mousePosition);
|
||||
m_hoveredHandle = EvaluateHit(context.mousePosition).handle;
|
||||
} else if (m_activeHandle == SceneViewportScaleGizmoHandle::None) {
|
||||
m_hoveredHandle = SceneViewportScaleGizmoHandle::None;
|
||||
} else {
|
||||
@@ -158,7 +158,7 @@ bool SceneViewportScaleGizmo::TryBeginDrag(const SceneViewportScaleGizmoContext&
|
||||
|
||||
activeScreenDirection = handle->end - handle->start;
|
||||
if (activeScreenDirection.SqrMagnitude() <= Math::EPSILON) {
|
||||
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
|
||||
if (!ProjectSceneViewportAxisDirectionAtPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
@@ -310,6 +310,68 @@ const SceneViewportScaleGizmoDrawData& SceneViewportScaleGizmo::GetDrawData() co
|
||||
return m_drawData;
|
||||
}
|
||||
|
||||
SceneViewportScaleGizmoHitResult SceneViewportScaleGizmo::EvaluateHit(const Math::Vector2& mousePosition) const {
|
||||
SceneViewportScaleGizmoHitResult result = {};
|
||||
if (!m_drawData.visible) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (m_drawData.centerHandle.visible &&
|
||||
IsPointInsideSquare(
|
||||
mousePosition,
|
||||
m_drawData.centerHandle.center,
|
||||
m_drawData.centerHandle.halfSize + 2.0f)) {
|
||||
result.handle = SceneViewportScaleGizmoHandle::Uniform;
|
||||
result.distanceSq = (m_drawData.centerHandle.center - mousePosition).SqrMagnitude();
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) {
|
||||
if (!handle.visible ||
|
||||
!IsPointInsideSquare(mousePosition, handle.capCenter, handle.capHalfSize + 2.0f)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = (handle.capCenter - mousePosition).SqrMagnitude();
|
||||
if (distanceSq >= result.distanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.handle = handle.handle;
|
||||
result.distanceSq = distanceSq;
|
||||
}
|
||||
|
||||
if (result.handle != SceneViewportScaleGizmoHandle::None) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const float hoverThresholdSq = kScaleGizmoHoverThresholdPixels * kScaleGizmoHoverThresholdPixels;
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end);
|
||||
if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.handle = handle.handle;
|
||||
result.distanceSq = distanceSq;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void SceneViewportScaleGizmo::SetHoveredHandle(SceneViewportScaleGizmoHandle handle) {
|
||||
if (m_activeHandle != SceneViewportScaleGizmoHandle::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_hoveredHandle = handle;
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
void SceneViewportScaleGizmo::BuildDrawData(const SceneViewportScaleGizmoContext& context) {
|
||||
m_drawData = {};
|
||||
|
||||
@@ -321,7 +383,7 @@ void SceneViewportScaleGizmo::BuildDrawData(const SceneViewportScaleGizmoContext
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition();
|
||||
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
|
||||
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
@@ -435,58 +497,6 @@ void SceneViewportScaleGizmo::RefreshHandleState() {
|
||||
m_drawData.centerHandle.active ? 1.0f : (m_drawData.centerHandle.hovered ? 0.96f : 0.88f));
|
||||
}
|
||||
|
||||
SceneViewportScaleGizmoHandle SceneViewportScaleGizmo::HitTestHandle(const Math::Vector2& mousePosition) const {
|
||||
if (!m_drawData.visible) {
|
||||
return SceneViewportScaleGizmoHandle::None;
|
||||
}
|
||||
|
||||
if (m_drawData.centerHandle.visible &&
|
||||
IsPointInsideSquare(
|
||||
mousePosition,
|
||||
m_drawData.centerHandle.center,
|
||||
m_drawData.centerHandle.halfSize + 2.0f)) {
|
||||
return SceneViewportScaleGizmoHandle::Uniform;
|
||||
}
|
||||
|
||||
SceneViewportScaleGizmoHandle bestHandle = SceneViewportScaleGizmoHandle::None;
|
||||
float bestDistanceSq = Math::FLOAT_MAX;
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) {
|
||||
if (!handle.visible ||
|
||||
!IsPointInsideSquare(mousePosition, handle.capCenter, handle.capHalfSize + 2.0f)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = (handle.capCenter - mousePosition).SqrMagnitude();
|
||||
if (distanceSq >= bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestHandle = handle.handle;
|
||||
}
|
||||
|
||||
if (bestHandle != SceneViewportScaleGizmoHandle::None) {
|
||||
return bestHandle;
|
||||
}
|
||||
|
||||
bestDistanceSq = kScaleGizmoHoverThresholdPixels * kScaleGizmoHoverThresholdPixels;
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : m_drawData.axisHandles) {
|
||||
if (!handle.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float distanceSq = DistanceToSegmentSquared(mousePosition, handle.start, handle.end);
|
||||
if (distanceSq > bestDistanceSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestHandle = handle.handle;
|
||||
}
|
||||
|
||||
return bestHandle;
|
||||
}
|
||||
|
||||
const SceneViewportScaleGizmoAxisHandleDrawData* SceneViewportScaleGizmo::FindAxisHandleDrawData(
|
||||
SceneViewportScaleGizmoHandle handle) const {
|
||||
for (const SceneViewportScaleGizmoAxisHandleDrawData& drawHandle : m_drawData.axisHandles) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "IViewportHostService.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
@@ -59,9 +60,20 @@ struct SceneViewportScaleGizmoContext {
|
||||
Math::Vector2 viewportSize = Math::Vector2::Zero();
|
||||
Math::Vector2 mousePosition = Math::Vector2::Zero();
|
||||
Components::GameObject* selectedObject = nullptr;
|
||||
Math::Vector3 pivotWorldPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion axisOrientation = Math::Quaternion::Identity();
|
||||
bool uniformOnly = false;
|
||||
};
|
||||
|
||||
struct SceneViewportScaleGizmoHitResult {
|
||||
SceneViewportScaleGizmoHandle handle = SceneViewportScaleGizmoHandle::None;
|
||||
float distanceSq = Math::FLOAT_MAX;
|
||||
|
||||
bool HasHit() const {
|
||||
return handle != SceneViewportScaleGizmoHandle::None;
|
||||
}
|
||||
};
|
||||
|
||||
class SceneViewportScaleGizmo {
|
||||
public:
|
||||
void Update(const SceneViewportScaleGizmoContext& context);
|
||||
@@ -74,11 +86,12 @@ public:
|
||||
bool IsActive() const;
|
||||
uint64_t GetActiveEntityId() const;
|
||||
const SceneViewportScaleGizmoDrawData& GetDrawData() const;
|
||||
SceneViewportScaleGizmoHitResult EvaluateHit(const Math::Vector2& mousePosition) const;
|
||||
void SetHoveredHandle(SceneViewportScaleGizmoHandle handle);
|
||||
|
||||
private:
|
||||
void BuildDrawData(const SceneViewportScaleGizmoContext& context);
|
||||
void RefreshHandleState();
|
||||
SceneViewportScaleGizmoHandle HitTestHandle(const Math::Vector2& mousePosition) const;
|
||||
const SceneViewportScaleGizmoAxisHandleDrawData* FindAxisHandleDrawData(
|
||||
SceneViewportScaleGizmoHandle handle) const;
|
||||
|
||||
|
||||
323
editor/src/Viewport/SceneViewportTransformGizmoFrameBuilder.h
Normal file
@@ -0,0 +1,323 @@
|
||||
#pragma once
|
||||
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "Core/ISelectionManager.h"
|
||||
#include "SceneViewportEditorOverlayData.h"
|
||||
#include "SceneViewportMoveGizmo.h"
|
||||
#include "SceneViewportRotateGizmo.h"
|
||||
#include "SceneViewportScaleGizmo.h"
|
||||
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
enum class SceneViewportActiveGizmoKind : uint8_t {
|
||||
None = 0,
|
||||
Move,
|
||||
Rotate,
|
||||
Scale
|
||||
};
|
||||
|
||||
struct SceneViewportSelectionGizmoState {
|
||||
Components::GameObject* primaryObject = nullptr;
|
||||
std::vector<Components::GameObject*> selectedObjects = {};
|
||||
Math::Vector3 pivotWorldPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion primaryWorldRotation = Math::Quaternion::Identity();
|
||||
};
|
||||
|
||||
struct SceneViewportTransformGizmoFrameState {
|
||||
SceneViewportOverlayData overlay = {};
|
||||
SceneViewportSelectionGizmoState selectionState = {};
|
||||
SceneViewportMoveGizmoContext moveContext = {};
|
||||
SceneViewportRotateGizmoContext rotateContext = {};
|
||||
SceneViewportScaleGizmoContext scaleContext = {};
|
||||
SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None;
|
||||
};
|
||||
|
||||
inline SceneViewportActiveGizmoKind GetActiveSceneViewportGizmoKind(
|
||||
const SceneViewportMoveGizmo& moveGizmo,
|
||||
const SceneViewportRotateGizmo& rotateGizmo,
|
||||
const SceneViewportScaleGizmo& scaleGizmo) {
|
||||
if (moveGizmo.IsActive()) {
|
||||
return SceneViewportActiveGizmoKind::Move;
|
||||
}
|
||||
if (rotateGizmo.IsActive()) {
|
||||
return SceneViewportActiveGizmoKind::Rotate;
|
||||
}
|
||||
if (scaleGizmo.IsActive()) {
|
||||
return SceneViewportActiveGizmoKind::Scale;
|
||||
}
|
||||
|
||||
return SceneViewportActiveGizmoKind::None;
|
||||
}
|
||||
|
||||
inline Math::Quaternion ComputeStableWorldRotation(const Components::GameObject* gameObject) {
|
||||
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
|
||||
return Math::Quaternion::Identity();
|
||||
}
|
||||
|
||||
const auto* transform = gameObject->GetTransform();
|
||||
Math::Quaternion worldRotation = transform->GetLocalRotation();
|
||||
for (const auto* parent = transform->GetParent();
|
||||
parent != nullptr;
|
||||
parent = parent->GetParent()) {
|
||||
worldRotation = parent->GetLocalRotation() * worldRotation;
|
||||
}
|
||||
|
||||
return worldRotation.Normalized();
|
||||
}
|
||||
|
||||
inline Math::Vector3 GetGameObjectPivotWorldPosition(const Components::GameObject* gameObject) {
|
||||
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
|
||||
return Math::Vector3::Zero();
|
||||
}
|
||||
|
||||
return gameObject->GetTransform()->GetPosition();
|
||||
}
|
||||
|
||||
inline Math::Vector3 GetGameObjectCenterWorldPosition(const Components::GameObject* gameObject) {
|
||||
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
|
||||
return Math::Vector3::Zero();
|
||||
}
|
||||
|
||||
if (auto* meshFilter = gameObject->GetComponent<Components::MeshFilterComponent>()) {
|
||||
if (Resources::Mesh* mesh = meshFilter->GetMesh();
|
||||
mesh != nullptr && mesh->IsValid()) {
|
||||
return gameObject->GetTransform()->TransformPoint(mesh->GetBounds().center);
|
||||
}
|
||||
}
|
||||
|
||||
return gameObject->GetTransform()->GetPosition();
|
||||
}
|
||||
|
||||
inline SceneViewportSelectionGizmoState BuildSceneViewportSelectionGizmoState(
|
||||
IEditorContext& context,
|
||||
bool useCenterPivot) {
|
||||
SceneViewportSelectionGizmoState state = {};
|
||||
const uint64_t primaryEntityId = context.GetSelectionManager().GetSelectedEntity();
|
||||
if (primaryEntityId != 0) {
|
||||
state.primaryObject = context.GetSceneManager().GetEntity(primaryEntityId);
|
||||
}
|
||||
|
||||
const std::vector<uint64_t>& selectedEntities = context.GetSelectionManager().GetSelectedEntities();
|
||||
state.selectedObjects.reserve(selectedEntities.size());
|
||||
for (uint64_t entityId : selectedEntities) {
|
||||
if (entityId == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (auto* gameObject = context.GetSceneManager().GetEntity(entityId)) {
|
||||
state.selectedObjects.push_back(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.primaryObject == nullptr && !state.selectedObjects.empty()) {
|
||||
state.primaryObject = state.selectedObjects.back();
|
||||
}
|
||||
if (state.primaryObject != nullptr && state.selectedObjects.empty()) {
|
||||
state.selectedObjects.push_back(state.primaryObject);
|
||||
}
|
||||
if (state.primaryObject != nullptr) {
|
||||
state.primaryWorldRotation = ComputeStableWorldRotation(state.primaryObject);
|
||||
}
|
||||
if (state.selectedObjects.empty()) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (useCenterPivot) {
|
||||
Math::Vector3 centerSum = Math::Vector3::Zero();
|
||||
for (const auto* gameObject : state.selectedObjects) {
|
||||
centerSum += GetGameObjectCenterWorldPosition(gameObject);
|
||||
}
|
||||
state.pivotWorldPosition = centerSum / static_cast<float>(state.selectedObjects.size());
|
||||
} else {
|
||||
state.pivotWorldPosition = GetGameObjectPivotWorldPosition(state.primaryObject);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
inline SceneViewportMoveGizmoContext BuildMoveGizmoContext(
|
||||
const SceneViewportSelectionGizmoState& selectionState,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector2& viewportSize,
|
||||
const Math::Vector2& mousePosition,
|
||||
bool localSpace) {
|
||||
SceneViewportMoveGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = viewportSize;
|
||||
gizmoContext.mousePosition = mousePosition;
|
||||
gizmoContext.selectedObject = selectionState.primaryObject;
|
||||
gizmoContext.selectedObjects = selectionState.selectedObjects;
|
||||
gizmoContext.pivotWorldPosition = selectionState.pivotWorldPosition;
|
||||
gizmoContext.axisOrientation = localSpace
|
||||
? selectionState.primaryWorldRotation
|
||||
: Math::Quaternion::Identity();
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
inline SceneViewportRotateGizmoContext BuildRotateGizmoContext(
|
||||
const SceneViewportSelectionGizmoState& selectionState,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector2& viewportSize,
|
||||
const Math::Vector2& mousePosition,
|
||||
bool localSpace,
|
||||
bool rotateAroundSharedPivot) {
|
||||
SceneViewportRotateGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = viewportSize;
|
||||
gizmoContext.mousePosition = mousePosition;
|
||||
gizmoContext.selectedObject = selectionState.primaryObject;
|
||||
gizmoContext.selectedObjects = selectionState.selectedObjects;
|
||||
gizmoContext.pivotWorldPosition = selectionState.pivotWorldPosition;
|
||||
gizmoContext.axisOrientation = localSpace
|
||||
? selectionState.primaryWorldRotation
|
||||
: Math::Quaternion::Identity();
|
||||
gizmoContext.localSpace = localSpace;
|
||||
gizmoContext.rotateAroundSharedPivot = rotateAroundSharedPivot;
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
inline SceneViewportScaleGizmoContext BuildScaleGizmoContext(
|
||||
const SceneViewportSelectionGizmoState& selectionState,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector2& viewportSize,
|
||||
const Math::Vector2& mousePosition,
|
||||
bool localSpace) {
|
||||
SceneViewportScaleGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = viewportSize;
|
||||
gizmoContext.mousePosition = mousePosition;
|
||||
gizmoContext.selectedObject = selectionState.primaryObject;
|
||||
gizmoContext.pivotWorldPosition = selectionState.pivotWorldPosition;
|
||||
gizmoContext.axisOrientation = localSpace
|
||||
? selectionState.primaryWorldRotation
|
||||
: Math::Quaternion::Identity();
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
inline void CancelSceneViewportTransformGizmoDrags(
|
||||
IEditorContext& context,
|
||||
SceneViewportMoveGizmo& moveGizmo,
|
||||
SceneViewportRotateGizmo& rotateGizmo,
|
||||
SceneViewportScaleGizmo& scaleGizmo) {
|
||||
if (moveGizmo.IsActive()) {
|
||||
moveGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
if (rotateGizmo.IsActive()) {
|
||||
rotateGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
if (scaleGizmo.IsActive()) {
|
||||
scaleGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
}
|
||||
|
||||
inline SceneViewportTransformGizmoFrameState RefreshSceneViewportTransformGizmos(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector2& viewportSize,
|
||||
const Math::Vector2& mousePosition,
|
||||
bool useCenterPivot,
|
||||
bool localSpace,
|
||||
bool usingTransformTool,
|
||||
bool showingMoveGizmo,
|
||||
SceneViewportMoveGizmo& moveGizmo,
|
||||
bool showingRotateGizmo,
|
||||
SceneViewportRotateGizmo& rotateGizmo,
|
||||
bool showingScaleGizmo,
|
||||
SceneViewportScaleGizmo& scaleGizmo) {
|
||||
SceneViewportTransformGizmoFrameState state = {};
|
||||
state.overlay = overlay;
|
||||
state.selectionState = BuildSceneViewportSelectionGizmoState(context, useCenterPivot);
|
||||
|
||||
if (showingMoveGizmo) {
|
||||
state.moveContext = BuildMoveGizmoContext(
|
||||
state.selectionState,
|
||||
overlay,
|
||||
viewportSize,
|
||||
mousePosition,
|
||||
localSpace);
|
||||
if (moveGizmo.IsActive() &&
|
||||
(state.moveContext.selectedObject == nullptr ||
|
||||
context.GetSelectionManager().GetSelectedEntity() != moveGizmo.GetActiveEntityId())) {
|
||||
moveGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
} else if (moveGizmo.IsActive()) {
|
||||
moveGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
|
||||
if (showingRotateGizmo) {
|
||||
state.rotateContext = BuildRotateGizmoContext(
|
||||
state.selectionState,
|
||||
overlay,
|
||||
viewportSize,
|
||||
mousePosition,
|
||||
localSpace,
|
||||
useCenterPivot);
|
||||
if (rotateGizmo.IsActive() &&
|
||||
(state.rotateContext.selectedObject == nullptr ||
|
||||
context.GetSelectionManager().GetSelectedEntity() != rotateGizmo.GetActiveEntityId())) {
|
||||
rotateGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
} else if (rotateGizmo.IsActive()) {
|
||||
rotateGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
|
||||
if (showingScaleGizmo) {
|
||||
state.scaleContext = BuildScaleGizmoContext(
|
||||
state.selectionState,
|
||||
overlay,
|
||||
viewportSize,
|
||||
mousePosition,
|
||||
localSpace);
|
||||
state.scaleContext.uniformOnly = usingTransformTool;
|
||||
if (scaleGizmo.IsActive() &&
|
||||
(state.scaleContext.selectedObject == nullptr ||
|
||||
context.GetSelectionManager().GetSelectedEntity() != scaleGizmo.GetActiveEntityId())) {
|
||||
scaleGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
} else if (scaleGizmo.IsActive()) {
|
||||
scaleGizmo.CancelDrag(&context.GetUndoManager());
|
||||
}
|
||||
|
||||
state.activeGizmoKind = GetActiveSceneViewportGizmoKind(moveGizmo, rotateGizmo, scaleGizmo);
|
||||
|
||||
if (showingMoveGizmo) {
|
||||
SceneViewportMoveGizmoContext updateContext = state.moveContext;
|
||||
if (state.activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
state.activeGizmoKind != SceneViewportActiveGizmoKind::Move) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
moveGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingRotateGizmo) {
|
||||
SceneViewportRotateGizmoContext updateContext = state.rotateContext;
|
||||
if (state.activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
state.activeGizmoKind != SceneViewportActiveGizmoKind::Rotate) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
rotateGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingScaleGizmo) {
|
||||
SceneViewportScaleGizmoContext updateContext = state.scaleContext;
|
||||
if (state.activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
state.activeGizmoKind != SceneViewportActiveGizmoKind::Scale) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
scaleGizmo.Update(updateContext);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -166,7 +166,9 @@ inline void ApplySceneViewportRenderRequestSetup(
|
||||
const Rendering::BuiltinPostProcessRequest* builtinPostProcess,
|
||||
Rendering::RenderPassSequence* postPasses,
|
||||
Rendering::CameraRenderRequest& request) {
|
||||
request.preScenePasses = nullptr;
|
||||
request.postScenePasses = nullptr;
|
||||
request.overlayPasses = nullptr;
|
||||
request.objectId = {};
|
||||
request.builtinPostProcess = {};
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "Core/ISelectionManager.h"
|
||||
#include "IViewportHostService.h"
|
||||
#include "Passes/SceneViewportEditorOverlayPass.h"
|
||||
#include "SceneViewportCameraController.h"
|
||||
#include "SceneViewportEditorOverlayData.h"
|
||||
#include "SceneViewportOverlayHandleBuilder.h"
|
||||
#include "SceneViewportOverlayBuilder.h"
|
||||
#include "ViewportHostRenderFlowUtils.h"
|
||||
#include "ViewportHostRenderTargets.h"
|
||||
#include "ViewportObjectIdPicker.h"
|
||||
@@ -12,6 +16,7 @@
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Components/LightComponent.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/RHI/RHIDevice.h>
|
||||
#include <XCEngine/RHI/RHIEnums.h>
|
||||
@@ -24,7 +29,9 @@
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -35,6 +42,133 @@ namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr bool kDebugSceneSelectionMask = false;
|
||||
constexpr uint64_t kSceneViewportOverlaySignatureOffsetBasis = 14695981039346656037ull;
|
||||
constexpr uint64_t kSceneViewportOverlaySignaturePrime = 1099511628211ull;
|
||||
|
||||
void HashSceneViewportOverlayBytes(uint64_t& hash, const void* data, size_t size) {
|
||||
const auto* bytes = static_cast<const uint8_t*>(data);
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
hash ^= static_cast<uint64_t>(bytes[index]);
|
||||
hash *= kSceneViewportOverlaySignaturePrime;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename TValue>
|
||||
void HashSceneViewportOverlayValue(uint64_t& hash, const TValue& value) {
|
||||
HashSceneViewportOverlayBytes(hash, &value, sizeof(TValue));
|
||||
}
|
||||
|
||||
void HashSceneViewportOverlayFloat(uint64_t& hash, float value) {
|
||||
uint32_t bits = 0u;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
HashSceneViewportOverlayValue(hash, bits);
|
||||
}
|
||||
|
||||
void HashSceneViewportOverlayVector3(uint64_t& hash, const Math::Vector3& value) {
|
||||
HashSceneViewportOverlayFloat(hash, value.x);
|
||||
HashSceneViewportOverlayFloat(hash, value.y);
|
||||
HashSceneViewportOverlayFloat(hash, value.z);
|
||||
}
|
||||
|
||||
void HashSceneViewportOverlayQuaternion(uint64_t& hash, const Math::Quaternion& value) {
|
||||
HashSceneViewportOverlayFloat(hash, value.x);
|
||||
HashSceneViewportOverlayFloat(hash, value.y);
|
||||
HashSceneViewportOverlayFloat(hash, value.z);
|
||||
HashSceneViewportOverlayFloat(hash, value.w);
|
||||
}
|
||||
|
||||
void HashSceneViewportOverlayRect(uint64_t& hash, const Math::Rect& value) {
|
||||
HashSceneViewportOverlayFloat(hash, value.x);
|
||||
HashSceneViewportOverlayFloat(hash, value.y);
|
||||
HashSceneViewportOverlayFloat(hash, value.width);
|
||||
HashSceneViewportOverlayFloat(hash, value.height);
|
||||
}
|
||||
|
||||
void HashSceneViewportOverlayTransform(uint64_t& hash, const Components::TransformComponent& transform) {
|
||||
HashSceneViewportOverlayVector3(hash, transform.GetPosition());
|
||||
HashSceneViewportOverlayQuaternion(hash, transform.GetRotation());
|
||||
HashSceneViewportOverlayVector3(hash, transform.GetScale());
|
||||
}
|
||||
|
||||
uint64_t BuildSceneViewEditorOverlayContentSignature(
|
||||
const Components::Scene* scene,
|
||||
const std::vector<uint64_t>& selectedObjectIds) {
|
||||
uint64_t hash = kSceneViewportOverlaySignatureOffsetBasis;
|
||||
|
||||
HashSceneViewportOverlayValue(hash, static_cast<uint64_t>(selectedObjectIds.size()));
|
||||
for (uint64_t entityId : selectedObjectIds) {
|
||||
HashSceneViewportOverlayValue(hash, entityId);
|
||||
}
|
||||
|
||||
if (scene == nullptr) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
for (Components::CameraComponent* camera : scene->FindObjectsOfType<Components::CameraComponent>()) {
|
||||
Components::GameObject* gameObject = camera != nullptr ? camera->GetGameObject() : nullptr;
|
||||
HashSceneViewportOverlayValue(hash, static_cast<uint8_t>(1u));
|
||||
HashSceneViewportOverlayValue(hash, gameObject != nullptr ? gameObject->GetID() : 0ull);
|
||||
HashSceneViewportOverlayValue(hash, camera != nullptr && camera->IsEnabled());
|
||||
HashSceneViewportOverlayValue(hash, gameObject != nullptr && gameObject->IsActiveInHierarchy());
|
||||
if (camera == nullptr ||
|
||||
gameObject == nullptr ||
|
||||
!camera->IsEnabled() ||
|
||||
!gameObject->IsActiveInHierarchy() ||
|
||||
gameObject->GetTransform() == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSceneViewportOverlayTransform(hash, *gameObject->GetTransform());
|
||||
HashSceneViewportOverlayValue(hash, static_cast<uint32_t>(camera->GetProjectionType()));
|
||||
HashSceneViewportOverlayFloat(hash, camera->GetFieldOfView());
|
||||
HashSceneViewportOverlayFloat(hash, camera->GetOrthographicSize());
|
||||
HashSceneViewportOverlayFloat(hash, camera->GetNearClipPlane());
|
||||
HashSceneViewportOverlayFloat(hash, camera->GetFarClipPlane());
|
||||
HashSceneViewportOverlayRect(hash, camera->GetViewportRect());
|
||||
}
|
||||
|
||||
for (Components::LightComponent* light : scene->FindObjectsOfType<Components::LightComponent>()) {
|
||||
Components::GameObject* gameObject = light != nullptr ? light->GetGameObject() : nullptr;
|
||||
HashSceneViewportOverlayValue(hash, static_cast<uint8_t>(2u));
|
||||
HashSceneViewportOverlayValue(hash, gameObject != nullptr ? gameObject->GetID() : 0ull);
|
||||
HashSceneViewportOverlayValue(hash, light != nullptr && light->IsEnabled());
|
||||
HashSceneViewportOverlayValue(hash, gameObject != nullptr && gameObject->IsActiveInHierarchy());
|
||||
if (light == nullptr ||
|
||||
gameObject == nullptr ||
|
||||
!light->IsEnabled() ||
|
||||
!gameObject->IsActiveInHierarchy() ||
|
||||
gameObject->GetTransform() == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSceneViewportOverlayTransform(hash, *gameObject->GetTransform());
|
||||
HashSceneViewportOverlayValue(hash, static_cast<uint32_t>(light->GetLightType()));
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool AreEqualSceneViewportVector3(const Math::Vector3& lhs, const Math::Vector3& rhs) {
|
||||
constexpr float kEpsilon = 1e-4f;
|
||||
return std::abs(lhs.x - rhs.x) <= kEpsilon &&
|
||||
std::abs(lhs.y - rhs.y) <= kEpsilon &&
|
||||
std::abs(lhs.z - rhs.z) <= kEpsilon;
|
||||
}
|
||||
|
||||
bool AreEqualSceneViewportOverlayData(
|
||||
const SceneViewportOverlayData& lhs,
|
||||
const SceneViewportOverlayData& rhs) {
|
||||
constexpr float kEpsilon = 1e-4f;
|
||||
return lhs.valid == rhs.valid &&
|
||||
AreEqualSceneViewportVector3(lhs.cameraPosition, rhs.cameraPosition) &&
|
||||
AreEqualSceneViewportVector3(lhs.cameraForward, rhs.cameraForward) &&
|
||||
AreEqualSceneViewportVector3(lhs.cameraRight, rhs.cameraRight) &&
|
||||
AreEqualSceneViewportVector3(lhs.cameraUp, rhs.cameraUp) &&
|
||||
std::abs(lhs.verticalFovDegrees - rhs.verticalFovDegrees) <= kEpsilon &&
|
||||
std::abs(lhs.nearClipPlane - rhs.nearClipPlane) <= kEpsilon &&
|
||||
std::abs(lhs.farClipPlane - rhs.farClipPlane) <= kEpsilon &&
|
||||
std::abs(lhs.orbitDistance - rhs.orbitDistance) <= kEpsilon;
|
||||
}
|
||||
|
||||
Math::Vector3 GetSceneViewportOrientationAxisVector(SceneViewportOrientationAxis axis) {
|
||||
switch (axis) {
|
||||
@@ -71,7 +205,9 @@ public:
|
||||
entry = {};
|
||||
}
|
||||
|
||||
m_sceneViewportEditorOverlayRenderer.Shutdown();
|
||||
m_sceneViewCamera = {};
|
||||
ResetSceneViewEditorOverlayFrameData();
|
||||
m_sceneViewLastRenderContext = {};
|
||||
m_device = nullptr;
|
||||
m_backend = nullptr;
|
||||
@@ -84,6 +220,8 @@ public:
|
||||
entry.requestedWidth = 0;
|
||||
entry.requestedHeight = 0;
|
||||
}
|
||||
m_sceneViewTransientTransformGizmoOverlay = {};
|
||||
m_sceneViewTransientTransformGizmoInputs = {};
|
||||
}
|
||||
|
||||
EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) override {
|
||||
@@ -212,6 +350,30 @@ public:
|
||||
return data;
|
||||
}
|
||||
|
||||
const SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext& context) override {
|
||||
EnsureSceneViewEditorOverlayFrameData(context);
|
||||
return m_sceneViewEditorOverlayFrameData;
|
||||
}
|
||||
|
||||
const SceneViewportOverlayFrameData& GetSceneViewInteractionOverlayFrameData(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) override {
|
||||
EnsureSceneViewEditorOverlayFrameData(context);
|
||||
m_sceneViewInteractionOverlayFrameData = m_sceneViewEditorOverlayFrameData;
|
||||
AppendSceneViewportOverlayFrameData(
|
||||
m_sceneViewInteractionOverlayFrameData,
|
||||
BuildSceneViewportTransformGizmoOverlayFrameData(overlay, inputs));
|
||||
return m_sceneViewInteractionOverlayFrameData;
|
||||
}
|
||||
|
||||
void SetSceneViewTransientTransformGizmoOverlayData(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const SceneViewportTransformGizmoHandleBuildInputs& inputs) override {
|
||||
m_sceneViewTransientTransformGizmoOverlay = overlay;
|
||||
m_sceneViewTransientTransformGizmoInputs = inputs;
|
||||
}
|
||||
|
||||
void RenderRequestedViewports(
|
||||
IEditorContext& context,
|
||||
const Rendering::RenderContext& renderContext) override {
|
||||
@@ -256,6 +418,7 @@ private:
|
||||
struct SceneViewportRenderState {
|
||||
SceneViewportOverlayData overlay = {};
|
||||
Rendering::BuiltinPostProcessRequest builtinPostProcess = {};
|
||||
SceneViewportOverlayFrameData editorOverlayFrameData = {};
|
||||
std::vector<uint64_t> selectedObjectIds;
|
||||
};
|
||||
|
||||
@@ -365,6 +528,86 @@ private:
|
||||
return BuildViewportColorSurface(entry.renderTargets);
|
||||
}
|
||||
|
||||
void ResetSceneViewEditorOverlayFrameData() {
|
||||
m_sceneViewEditorOverlayFrameData = {};
|
||||
m_sceneViewInteractionOverlayFrameData = {};
|
||||
m_sceneViewEditorOverlayScene = nullptr;
|
||||
m_sceneViewEditorOverlaySelectedObjectIds.clear();
|
||||
m_sceneViewEditorOverlayViewportWidth = 0u;
|
||||
m_sceneViewEditorOverlayViewportHeight = 0u;
|
||||
m_sceneViewEditorOverlayContentSignature = 0u;
|
||||
m_sceneViewEditorOverlayCached = false;
|
||||
}
|
||||
|
||||
void ResolveSceneViewEditorOverlayViewportSize(
|
||||
const ViewportEntry& entry,
|
||||
uint32_t& outWidth,
|
||||
uint32_t& outHeight) const {
|
||||
outWidth = entry.requestedWidth > 0u ? entry.requestedWidth : entry.renderTargets.width;
|
||||
outHeight = entry.requestedHeight > 0u ? entry.requestedHeight : entry.renderTargets.height;
|
||||
}
|
||||
|
||||
bool ShouldRebuildSceneViewEditorOverlayFrameData(
|
||||
const Components::Scene* scene,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
uint32_t viewportWidth,
|
||||
uint32_t viewportHeight,
|
||||
const std::vector<uint64_t>& selectedObjectIds,
|
||||
uint64_t contentSignature) const {
|
||||
return !m_sceneViewEditorOverlayCached ||
|
||||
m_sceneViewEditorOverlayScene != scene ||
|
||||
m_sceneViewEditorOverlayViewportWidth != viewportWidth ||
|
||||
m_sceneViewEditorOverlayViewportHeight != viewportHeight ||
|
||||
m_sceneViewEditorOverlaySelectedObjectIds != selectedObjectIds ||
|
||||
m_sceneViewEditorOverlayContentSignature != contentSignature ||
|
||||
!AreEqualSceneViewportOverlayData(m_sceneViewEditorOverlayFrameData.overlay, overlay);
|
||||
}
|
||||
|
||||
void EnsureSceneViewEditorOverlayFrameData(IEditorContext& context) {
|
||||
if (!EnsureSceneViewCamera()) {
|
||||
ResetSceneViewEditorOverlayFrameData();
|
||||
return;
|
||||
}
|
||||
|
||||
const ViewportEntry& entry = GetEntry(EditorViewportKind::Scene);
|
||||
uint32_t viewportWidth = 0u;
|
||||
uint32_t viewportHeight = 0u;
|
||||
ResolveSceneViewEditorOverlayViewportSize(entry, viewportWidth, viewportHeight);
|
||||
|
||||
const Components::Scene* scene = context.GetSceneManager().GetScene();
|
||||
const SceneViewportOverlayData overlay = GetSceneViewOverlayData();
|
||||
const std::vector<uint64_t> selectedObjectIds = context.GetSelectionManager().GetSelectedEntities();
|
||||
const uint64_t contentSignature =
|
||||
BuildSceneViewEditorOverlayContentSignature(scene, selectedObjectIds);
|
||||
if (!ShouldRebuildSceneViewEditorOverlayFrameData(
|
||||
scene,
|
||||
overlay,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
selectedObjectIds,
|
||||
contentSignature)) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sceneViewEditorOverlayFrameData = {};
|
||||
m_sceneViewEditorOverlayFrameData.overlay = overlay;
|
||||
if (scene != nullptr && overlay.valid && viewportWidth > 0u && viewportHeight > 0u) {
|
||||
m_sceneViewEditorOverlayFrameData = SceneViewportOverlayBuilder::Build(
|
||||
context,
|
||||
overlay,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
selectedObjectIds);
|
||||
}
|
||||
|
||||
m_sceneViewEditorOverlayScene = scene;
|
||||
m_sceneViewEditorOverlaySelectedObjectIds = selectedObjectIds;
|
||||
m_sceneViewEditorOverlayViewportWidth = viewportWidth;
|
||||
m_sceneViewEditorOverlayViewportHeight = viewportHeight;
|
||||
m_sceneViewEditorOverlayContentSignature = contentSignature;
|
||||
m_sceneViewEditorOverlayCached = true;
|
||||
}
|
||||
|
||||
void ApplyViewportRenderFailure(
|
||||
ViewportEntry& entry,
|
||||
const Rendering::RenderContext& renderContext,
|
||||
@@ -399,6 +642,7 @@ private:
|
||||
}
|
||||
|
||||
outState.selectedObjectIds = context.GetSelectionManager().GetSelectedEntities();
|
||||
outState.editorOverlayFrameData = GetSceneViewEditorOverlayFrameData(context);
|
||||
const SceneViewportBuiltinPostProcessBuildResult builtinPostProcess =
|
||||
BuildSceneViewportBuiltinPostProcess(
|
||||
outState.overlay,
|
||||
@@ -456,6 +700,18 @@ private:
|
||||
&sceneState.builtinPostProcess,
|
||||
nullptr,
|
||||
requests[0]);
|
||||
SceneViewportOverlayFrameData renderOverlayFrameData = sceneState.editorOverlayFrameData;
|
||||
AppendSceneViewportOverlayFrameData(
|
||||
renderOverlayFrameData,
|
||||
BuildSceneViewTransientTransformGizmoOverlayFrameData());
|
||||
Rendering::RenderPassSequence overlayPassSequence = {};
|
||||
if (renderOverlayFrameData.HasOverlayPrimitives()) {
|
||||
overlayPassSequence.AddPass(
|
||||
CreateSceneViewportEditorOverlayPass(
|
||||
m_sceneViewportEditorOverlayRenderer,
|
||||
renderOverlayFrameData));
|
||||
requests[0].overlayPasses = &overlayPassSequence;
|
||||
}
|
||||
requests[0].hasClearColorOverride = true;
|
||||
requests[0].clearColorOverride = Math::Color(0.27f, 0.27f, 0.27f, 1.0f);
|
||||
|
||||
@@ -600,12 +856,29 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
SceneViewportOverlayFrameData BuildSceneViewTransientTransformGizmoOverlayFrameData() const {
|
||||
return BuildSceneViewportTransformGizmoOverlayFrameData(
|
||||
m_sceneViewTransientTransformGizmoOverlay,
|
||||
m_sceneViewTransientTransformGizmoInputs);
|
||||
}
|
||||
|
||||
UI::ImGuiBackendBridge* m_backend = nullptr;
|
||||
RHI::RHIDevice* m_device = nullptr;
|
||||
std::unique_ptr<Rendering::SceneRenderer> m_sceneRenderer;
|
||||
Rendering::RenderContext m_sceneViewLastRenderContext = {};
|
||||
std::array<ViewportEntry, 2> m_entries = {};
|
||||
SceneViewCameraState m_sceneViewCamera;
|
||||
SceneViewportOverlayFrameData m_sceneViewEditorOverlayFrameData = {};
|
||||
SceneViewportOverlayFrameData m_sceneViewInteractionOverlayFrameData = {};
|
||||
SceneViewportOverlayData m_sceneViewTransientTransformGizmoOverlay = {};
|
||||
SceneViewportTransformGizmoHandleBuildInputs m_sceneViewTransientTransformGizmoInputs = {};
|
||||
const Components::Scene* m_sceneViewEditorOverlayScene = nullptr;
|
||||
std::vector<uint64_t> m_sceneViewEditorOverlaySelectedObjectIds = {};
|
||||
uint32_t m_sceneViewEditorOverlayViewportWidth = 0u;
|
||||
uint32_t m_sceneViewEditorOverlayViewportHeight = 0u;
|
||||
uint64_t m_sceneViewEditorOverlayContentSignature = 0u;
|
||||
bool m_sceneViewEditorOverlayCached = false;
|
||||
SceneViewportEditorOverlayPassRenderer m_sceneViewportEditorOverlayRenderer;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
|
||||
@@ -1,12 +1,166 @@
|
||||
#include "Actions/ActionRouting.h"
|
||||
#include "Core/EventBus.h"
|
||||
#include "Core/EditorEvents.h"
|
||||
#include "GameViewPanel.h"
|
||||
#include "ViewportPanelContent.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
void SetKeyState(GameViewInputFrameEvent& event, XCEngine::Input::KeyCode key, bool isDown) {
|
||||
const size_t index = static_cast<size_t>(key);
|
||||
if (index < event.keyDown.size()) {
|
||||
event.keyDown[index] = isDown;
|
||||
}
|
||||
}
|
||||
|
||||
void SetMouseButtonState(GameViewInputFrameEvent& event, XCEngine::Input::MouseButton button, bool isDown) {
|
||||
const size_t index = static_cast<size_t>(button);
|
||||
if (index < event.mouseButtonDown.size()) {
|
||||
event.mouseButtonDown[index] = isDown;
|
||||
}
|
||||
}
|
||||
|
||||
void FillGameViewKeyboardState(const ImGuiIO& io, GameViewInputFrameEvent& event) {
|
||||
using XCEngine::Input::KeyCode;
|
||||
|
||||
const struct KeyMapping {
|
||||
ImGuiKey imguiKey;
|
||||
KeyCode keyCode;
|
||||
} keyMappings[] = {
|
||||
{ImGuiKey_A, KeyCode::A},
|
||||
{ImGuiKey_B, KeyCode::B},
|
||||
{ImGuiKey_C, KeyCode::C},
|
||||
{ImGuiKey_D, KeyCode::D},
|
||||
{ImGuiKey_E, KeyCode::E},
|
||||
{ImGuiKey_F, KeyCode::F},
|
||||
{ImGuiKey_G, KeyCode::G},
|
||||
{ImGuiKey_H, KeyCode::H},
|
||||
{ImGuiKey_I, KeyCode::I},
|
||||
{ImGuiKey_J, KeyCode::J},
|
||||
{ImGuiKey_K, KeyCode::K},
|
||||
{ImGuiKey_L, KeyCode::L},
|
||||
{ImGuiKey_M, KeyCode::M},
|
||||
{ImGuiKey_N, KeyCode::N},
|
||||
{ImGuiKey_O, KeyCode::O},
|
||||
{ImGuiKey_P, KeyCode::P},
|
||||
{ImGuiKey_Q, KeyCode::Q},
|
||||
{ImGuiKey_R, KeyCode::R},
|
||||
{ImGuiKey_S, KeyCode::S},
|
||||
{ImGuiKey_T, KeyCode::T},
|
||||
{ImGuiKey_U, KeyCode::U},
|
||||
{ImGuiKey_V, KeyCode::V},
|
||||
{ImGuiKey_W, KeyCode::W},
|
||||
{ImGuiKey_X, KeyCode::X},
|
||||
{ImGuiKey_Y, KeyCode::Y},
|
||||
{ImGuiKey_Z, KeyCode::Z},
|
||||
{ImGuiKey_0, KeyCode::Zero},
|
||||
{ImGuiKey_1, KeyCode::One},
|
||||
{ImGuiKey_2, KeyCode::Two},
|
||||
{ImGuiKey_3, KeyCode::Three},
|
||||
{ImGuiKey_4, KeyCode::Four},
|
||||
{ImGuiKey_5, KeyCode::Five},
|
||||
{ImGuiKey_6, KeyCode::Six},
|
||||
{ImGuiKey_7, KeyCode::Seven},
|
||||
{ImGuiKey_8, KeyCode::Eight},
|
||||
{ImGuiKey_9, KeyCode::Nine},
|
||||
{ImGuiKey_Space, KeyCode::Space},
|
||||
{ImGuiKey_Tab, KeyCode::Tab},
|
||||
{ImGuiKey_Enter, KeyCode::Enter},
|
||||
{ImGuiKey_Escape, KeyCode::Escape},
|
||||
{ImGuiKey_LeftShift, KeyCode::LeftShift},
|
||||
{ImGuiKey_RightShift, KeyCode::RightShift},
|
||||
{ImGuiKey_LeftCtrl, KeyCode::LeftCtrl},
|
||||
{ImGuiKey_RightCtrl, KeyCode::RightCtrl},
|
||||
{ImGuiKey_LeftAlt, KeyCode::LeftAlt},
|
||||
{ImGuiKey_RightAlt, KeyCode::RightAlt},
|
||||
{ImGuiKey_UpArrow, KeyCode::Up},
|
||||
{ImGuiKey_DownArrow, KeyCode::Down},
|
||||
{ImGuiKey_LeftArrow, KeyCode::Left},
|
||||
{ImGuiKey_RightArrow, KeyCode::Right},
|
||||
{ImGuiKey_Home, KeyCode::Home},
|
||||
{ImGuiKey_End, KeyCode::End},
|
||||
{ImGuiKey_PageUp, KeyCode::PageUp},
|
||||
{ImGuiKey_PageDown, KeyCode::PageDown},
|
||||
{ImGuiKey_Delete, KeyCode::Delete},
|
||||
{ImGuiKey_Backspace, KeyCode::Backspace},
|
||||
{ImGuiKey_F1, KeyCode::F1},
|
||||
{ImGuiKey_F2, KeyCode::F2},
|
||||
{ImGuiKey_F3, KeyCode::F3},
|
||||
{ImGuiKey_F4, KeyCode::F4},
|
||||
{ImGuiKey_F5, KeyCode::F5},
|
||||
{ImGuiKey_F6, KeyCode::F6},
|
||||
{ImGuiKey_F7, KeyCode::F7},
|
||||
{ImGuiKey_F8, KeyCode::F8},
|
||||
{ImGuiKey_F9, KeyCode::F9},
|
||||
{ImGuiKey_F10, KeyCode::F10},
|
||||
{ImGuiKey_F11, KeyCode::F11},
|
||||
{ImGuiKey_F12, KeyCode::F12},
|
||||
{ImGuiKey_Minus, KeyCode::Minus},
|
||||
{ImGuiKey_Equal, KeyCode::Equals},
|
||||
{ImGuiKey_LeftBracket, KeyCode::BracketLeft},
|
||||
{ImGuiKey_RightBracket, KeyCode::BracketRight},
|
||||
{ImGuiKey_Semicolon, KeyCode::Semicolon},
|
||||
{ImGuiKey_Apostrophe, KeyCode::Quote},
|
||||
{ImGuiKey_Comma, KeyCode::Comma},
|
||||
{ImGuiKey_Period, KeyCode::Period},
|
||||
{ImGuiKey_Slash, KeyCode::Slash},
|
||||
{ImGuiKey_Backslash, KeyCode::Backslash},
|
||||
{ImGuiKey_GraveAccent, KeyCode::Backtick},
|
||||
};
|
||||
|
||||
for (const KeyMapping& mapping : keyMappings) {
|
||||
SetKeyState(event, mapping.keyCode, ImGui::IsKeyDown(mapping.imguiKey));
|
||||
}
|
||||
}
|
||||
|
||||
void FillGameViewMouseState(const ImGuiIO& io, GameViewInputFrameEvent& event) {
|
||||
(void)io;
|
||||
SetMouseButtonState(event, XCEngine::Input::MouseButton::Left, ImGui::IsMouseDown(ImGuiMouseButton_Left));
|
||||
SetMouseButtonState(event, XCEngine::Input::MouseButton::Right, ImGui::IsMouseDown(ImGuiMouseButton_Right));
|
||||
SetMouseButtonState(event, XCEngine::Input::MouseButton::Middle, ImGui::IsMouseDown(ImGuiMouseButton_Middle));
|
||||
}
|
||||
|
||||
GameViewInputFrameEvent BuildGameViewInputFrame(const ViewportPanelContentResult& content) {
|
||||
GameViewInputFrameEvent event = {};
|
||||
if (!content.hasViewportArea) {
|
||||
return event;
|
||||
}
|
||||
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
event.hovered = content.hovered;
|
||||
event.focused = content.focused;
|
||||
event.mousePosition = XCEngine::Math::Vector2(
|
||||
io.MousePos.x - content.itemMin.x,
|
||||
io.MousePos.y - content.itemMin.y);
|
||||
event.mouseDelta = XCEngine::Math::Vector2(io.MouseDelta.x, io.MouseDelta.y);
|
||||
event.mouseWheel = content.hovered ? io.MouseWheel : 0.0f;
|
||||
|
||||
if (event.hovered || event.focused) {
|
||||
FillGameViewKeyboardState(io, event);
|
||||
FillGameViewMouseState(io, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
void PublishGameViewInputFrame(IEditorContext* context, const GameViewInputFrameEvent& event) {
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
context->GetEventBus().Publish(event);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GameViewPanel::GameViewPanel() : Panel("Game") {}
|
||||
|
||||
void GameViewPanel::Render() {
|
||||
@@ -14,10 +168,12 @@ void GameViewPanel::Render() {
|
||||
UI::PanelWindowScope panel(m_name.c_str());
|
||||
ImGui::PopStyleVar();
|
||||
if (!panel.IsOpen()) {
|
||||
PublishGameViewInputFrame(m_context, GameViewInputFrameEvent{});
|
||||
return;
|
||||
}
|
||||
|
||||
RenderViewportPanelContent(*m_context, EditorViewportKind::Game);
|
||||
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Game);
|
||||
PublishGameViewInputFrame(m_context, BuildGameViewInputFrame(content));
|
||||
Actions::ObserveInactiveActionRoute(*m_context);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "Panel.h"
|
||||
#include "Core/AssetItem.h"
|
||||
#include "Core/EditorActionRoute.h"
|
||||
#include "UI/PopupState.h"
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceHandle.h>
|
||||
#include <XCEngine/Resources/Material/Material.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Components {
|
||||
@@ -23,7 +31,55 @@ public:
|
||||
void OnDetach() override;
|
||||
void Render() override;
|
||||
|
||||
enum class SubjectMode {
|
||||
None,
|
||||
GameObject,
|
||||
MaterialAsset,
|
||||
UnsupportedAsset
|
||||
};
|
||||
|
||||
struct MaterialTagEditRow {
|
||||
std::array<char, 64> name{};
|
||||
std::array<char, 128> value{};
|
||||
};
|
||||
|
||||
struct MaterialAssetState {
|
||||
std::string assetPath;
|
||||
std::string assetFullPath;
|
||||
std::string assetName;
|
||||
std::array<char, 260> shaderPath{};
|
||||
std::array<char, 128> shaderPass{};
|
||||
int renderQueue = static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Geometry);
|
||||
::XCEngine::Resources::MaterialRenderState renderState{};
|
||||
std::vector<MaterialTagEditRow> tags;
|
||||
std::string errorMessage;
|
||||
bool dirty = false;
|
||||
bool loaded = false;
|
||||
|
||||
void Reset() {
|
||||
assetPath.clear();
|
||||
assetFullPath.clear();
|
||||
assetName.clear();
|
||||
shaderPath.fill('\0');
|
||||
shaderPass.fill('\0');
|
||||
renderQueue = static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Geometry);
|
||||
renderState = ::XCEngine::Resources::MaterialRenderState();
|
||||
tags.clear();
|
||||
errorMessage.clear();
|
||||
dirty = false;
|
||||
loaded = false;
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
void SyncSubject();
|
||||
void SetSubjectMode(SubjectMode mode);
|
||||
void InspectMaterialAsset(const AssetItemPtr& item);
|
||||
void ClearMaterialAsset();
|
||||
void RenderMaterialAsset();
|
||||
void RenderUnsupportedAsset();
|
||||
bool SaveMaterialAsset();
|
||||
void ReloadMaterialAsset();
|
||||
void RenderGameObject(::XCEngine::Components::GameObject* gameObject);
|
||||
void RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject);
|
||||
void RenderEmptyState(const char* title, const char* subtitle = nullptr);
|
||||
@@ -31,6 +87,11 @@ private:
|
||||
|
||||
uint64_t m_selectionHandlerId = 0;
|
||||
uint64_t m_selectedEntityId = 0;
|
||||
SubjectMode m_subjectMode = SubjectMode::None;
|
||||
EditorActionRoute m_lastExplicitRoute = EditorActionRoute::None;
|
||||
AssetItemPtr m_selectedAssetItem;
|
||||
::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Material> m_selectedMaterial;
|
||||
MaterialAssetState m_materialAssetState;
|
||||
UI::DeferredPopupState m_addComponentPopup;
|
||||
std::function<void()> m_deferredContextAction;
|
||||
};
|
||||
|
||||
@@ -21,6 +21,10 @@ constexpr float kRunToolbarHeight = 32.0f;
|
||||
constexpr float kRunToolbarButtonExtent = 24.0f;
|
||||
constexpr float kRunToolbarButtonSpacing = 8.0f;
|
||||
constexpr float kRunToolbarIconInset = 3.0f;
|
||||
constexpr ImVec2 kMainMenuFramePadding(6.0f, 2.0f);
|
||||
constexpr ImVec4 kMainMenuTextColor(0.08f, 0.08f, 0.08f, 1.0f);
|
||||
constexpr ImVec4 kMainMenuItemHoveredColor(0.88f, 0.88f, 0.88f, 1.0f);
|
||||
constexpr ImVec4 kMainMenuItemActiveColor(0.82f, 0.82f, 0.82f, 1.0f);
|
||||
constexpr ImVec4 kRunToolbarBackgroundColor(0.1f, 0.1f, 0.1f, 1.0f);
|
||||
|
||||
std::string BuildRunToolbarIconPath(const char* fileName) {
|
||||
@@ -108,9 +112,14 @@ void MenuBar::RenderChrome() {
|
||||
}
|
||||
|
||||
Actions::HandleMenuBarShortcuts(*m_context);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.08f, 0.08f, 0.08f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, kMainMenuFramePadding);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, kMainMenuTextColor);
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, kMainMenuItemActiveColor);
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, kMainMenuItemHoveredColor);
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, kMainMenuItemActiveColor);
|
||||
Actions::DrawMainMenuBar(*m_context, m_aboutPopup);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar();
|
||||
RenderRunToolbar();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/IProjectManager.h"
|
||||
#include "Core/AssetItem.h"
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "Utils/ProjectFileUtils.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <imgui.h>
|
||||
#include <shellapi.h>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
@@ -86,6 +91,52 @@ UI::AssetTileOptions MakeProjectAssetTileOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
std::string BuildProjectRelativeAssetPath(const std::string& projectPath, const std::string& fullPath) {
|
||||
if (projectPath.empty() || fullPath.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return ProjectFileUtils::MakeProjectRelativePath(projectPath, fullPath);
|
||||
}
|
||||
|
||||
bool ShowPathInExplorer(const std::string& fullPath, bool selectTarget) {
|
||||
if (fullPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::error_code ec;
|
||||
const fs::path targetPath = fs::path(Platform::Utf8ToWide(fullPath)).lexically_normal();
|
||||
if (targetPath.empty() || !fs::exists(targetPath, ec)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HINSTANCE result = nullptr;
|
||||
if (selectTarget) {
|
||||
const std::wstring parameters = L"/select,\"" + targetPath.native() + L"\"";
|
||||
const std::wstring workingDirectory = targetPath.parent_path().native();
|
||||
result = ShellExecuteW(
|
||||
nullptr,
|
||||
L"open",
|
||||
L"explorer.exe",
|
||||
parameters.c_str(),
|
||||
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
||||
SW_SHOWNORMAL);
|
||||
} else {
|
||||
const std::wstring workingDirectory = targetPath.parent_path().native();
|
||||
result = ShellExecuteW(
|
||||
nullptr,
|
||||
L"open",
|
||||
targetPath.c_str(),
|
||||
nullptr,
|
||||
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
||||
SW_SHOWNORMAL);
|
||||
}
|
||||
|
||||
return reinterpret_cast<INT_PTR>(result) > 32;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProjectPanel::ProjectPanel() : Panel("Project") {
|
||||
@@ -186,6 +237,93 @@ void ProjectPanel::CancelRename() {
|
||||
m_renameState.Cancel();
|
||||
}
|
||||
|
||||
ProjectPanel::ContextMenuTarget ProjectPanel::BuildContextMenuTarget(
|
||||
IProjectManager& manager,
|
||||
const AssetItemPtr& item) const {
|
||||
ContextMenuTarget target;
|
||||
target.item = item;
|
||||
if (item) {
|
||||
target.subjectPath = item->fullPath;
|
||||
target.createFolderPath = item->isFolder ? item->fullPath : std::string();
|
||||
target.showInExplorerSelect = true;
|
||||
return target;
|
||||
}
|
||||
|
||||
if (const AssetItemPtr currentFolder = manager.GetCurrentFolder()) {
|
||||
target.subjectPath = currentFolder->fullPath;
|
||||
target.createFolderPath = currentFolder->fullPath;
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
void ProjectPanel::DrawProjectContextMenu(IProjectManager& manager, const ContextMenuTarget& target) {
|
||||
auto* managerPtr = &manager;
|
||||
const bool canCreate = !target.createFolderPath.empty();
|
||||
const bool canShowInExplorer = !target.subjectPath.empty();
|
||||
const bool canOpen = target.item != nullptr && Commands::CanOpenAsset(target.item);
|
||||
const bool canDelete = target.item != nullptr;
|
||||
const bool canRename = target.item != nullptr;
|
||||
const std::string copyPath = BuildProjectRelativeAssetPath(
|
||||
m_context ? m_context->GetProjectPath() : std::string(),
|
||||
target.subjectPath);
|
||||
const bool canCopyPath = !copyPath.empty();
|
||||
|
||||
const auto queueCreateAsset = [this, managerPtr, target](auto createFn) {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, managerPtr, target, createFn]() {
|
||||
if (!target.createFolderPath.empty() && target.item && target.item->isFolder) {
|
||||
managerPtr->NavigateToFolder(target.item);
|
||||
}
|
||||
|
||||
if (AssetItemPtr createdItem = createFn(*managerPtr)) {
|
||||
BeginRename(createdItem);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
UI::DrawContextSubmenu("Create", [&]() {
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Folder", nullptr, false, canCreate), [&]() {
|
||||
queueCreateAsset([](IProjectManager& createManager) {
|
||||
return Commands::CreateFolder(createManager, "New Folder");
|
||||
});
|
||||
});
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Material", nullptr, false, canCreate), [&]() {
|
||||
queueCreateAsset([](IProjectManager& createManager) {
|
||||
return Commands::CreateMaterial(createManager, "New Material");
|
||||
});
|
||||
});
|
||||
}, canCreate);
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Show in Explore", nullptr, false, canShowInExplorer), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [target]() {
|
||||
ShowPathInExplorer(target.subjectPath, target.showInExplorerSelect);
|
||||
});
|
||||
});
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeOpenAssetAction(canOpen), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, target]() {
|
||||
Actions::OpenProjectAsset(*m_context, target.item);
|
||||
});
|
||||
});
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(canDelete), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, target]() {
|
||||
Commands::DeleteAsset(m_context->GetProjectManager(), target.item);
|
||||
});
|
||||
});
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, canRename), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, target]() {
|
||||
BeginRename(target.item);
|
||||
});
|
||||
});
|
||||
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Copy Path", nullptr, false, canCopyPath), [copyPath]() {
|
||||
ImGui::SetClipboardText(copyPath.c_str());
|
||||
});
|
||||
}
|
||||
|
||||
void ProjectPanel::Render() {
|
||||
UI::PanelWindowScope panel(m_name.c_str());
|
||||
if (!panel.IsOpen()) {
|
||||
@@ -265,7 +403,6 @@ void ProjectPanel::RenderToolbar() {
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
|
||||
auto* managerPtr = &manager;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding());
|
||||
const bool open = ImGui::BeginChild("ProjectFolderTree", ImVec2(m_navigationWidth, 0.0f), false);
|
||||
@@ -287,13 +424,7 @@ void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
|
||||
}
|
||||
|
||||
if (UI::BeginContextMenuForWindow("##ProjectFolderTreeContext")) {
|
||||
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
|
||||
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
|
||||
BeginRename(createdFolder);
|
||||
}
|
||||
});
|
||||
});
|
||||
DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr));
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
@@ -351,7 +482,6 @@ void ProjectPanel::RenderFolderTreeNode(
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
auto* managerPtr = &manager;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
const bool open = ImGui::BeginChild("ProjectBrowser", ImVec2(0.0f, 0.0f), false);
|
||||
@@ -393,7 +523,6 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
}
|
||||
|
||||
const float tileWidth = UI::AssetTileSize().x;
|
||||
const float tileHeight = UI::AssetTileSize().y;
|
||||
const float spacing = UI::AssetGridSpacing().x;
|
||||
const float rowSpacing = UI::AssetGridSpacing().y;
|
||||
const float panelWidth = ImGui::GetContentRegionAvail().x;
|
||||
@@ -402,19 +531,41 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
columns = 1;
|
||||
}
|
||||
|
||||
const int rowCount = visibleItems.empty() ? 0 : (static_cast<int>(visibleItems.size()) + columns - 1) / columns;
|
||||
std::vector<float> rowHeights(static_cast<size_t>(rowCount), UI::AssetTileSize().y);
|
||||
|
||||
AssetItemPtr pendingSelection;
|
||||
AssetItemPtr pendingOpenTarget;
|
||||
const std::string selectedItemPath = manager.GetSelectedItemPath();
|
||||
const ImVec2 gridOrigin = ImGui::GetCursorPos();
|
||||
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
|
||||
const AssetItemPtr& item = visibleItems[visibleIndex];
|
||||
const bool isRenaming = item && m_renameState.IsEditing(item->fullPath);
|
||||
UI::AssetTileOptions tileOptions = MakeProjectAssetTileOptions();
|
||||
tileOptions.drawLabel = !isRenaming;
|
||||
const ImVec2 tileSize = UI::ComputeAssetTileSize(GetProjectAssetDisplayName(item).c_str(), tileOptions);
|
||||
const int row = visibleIndex / columns;
|
||||
rowHeights[static_cast<size_t>(row)] = (std::max)(rowHeights[static_cast<size_t>(row)], tileSize.y);
|
||||
}
|
||||
|
||||
std::vector<float> rowOffsets(static_cast<size_t>(rowCount), gridOrigin.y);
|
||||
float nextRowY = gridOrigin.y;
|
||||
for (int row = 0; row < rowCount; ++row) {
|
||||
rowOffsets[static_cast<size_t>(row)] = nextRowY;
|
||||
nextRowY += rowHeights[static_cast<size_t>(row)] + rowSpacing;
|
||||
}
|
||||
|
||||
int renderedItemCount = 0;
|
||||
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
|
||||
const int column = visibleIndex % columns;
|
||||
const int row = visibleIndex / columns;
|
||||
ImGui::SetCursorPos(ImVec2(
|
||||
gridOrigin.x + column * (tileWidth + spacing),
|
||||
gridOrigin.y + row * (tileHeight + rowSpacing)));
|
||||
rowOffsets[static_cast<size_t>(row)]));
|
||||
|
||||
const AssetItemPtr& item = visibleItems[visibleIndex];
|
||||
const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath);
|
||||
++renderedItemCount;
|
||||
if (interaction.clicked) {
|
||||
pendingSelection = item;
|
||||
}
|
||||
@@ -427,9 +578,14 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!visibleItems.empty()) {
|
||||
const int rowCount = (static_cast<int>(visibleItems.size()) + columns - 1) / columns;
|
||||
ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing);
|
||||
if (renderedItemCount > 0) {
|
||||
const int renderedRowCount = (renderedItemCount + columns - 1) / columns;
|
||||
float contentBottom = gridOrigin.y;
|
||||
for (int row = 0; row < renderedRowCount; ++row) {
|
||||
contentBottom = rowOffsets[static_cast<size_t>(row)] + rowHeights[static_cast<size_t>(row)];
|
||||
}
|
||||
ImGui::SetCursorPos(ImVec2(gridOrigin.x, contentBottom));
|
||||
ImGui::Dummy(ImVec2(0.0f, 0.0f));
|
||||
}
|
||||
|
||||
if (visibleItems.empty() && !searchQuery.Empty()) {
|
||||
@@ -449,21 +605,7 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
}
|
||||
|
||||
if (UI::BeginContextMenuForWindow("##ProjectBrowserContext")) {
|
||||
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
|
||||
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
|
||||
BeginRename(createdFolder);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (manager.CanNavigateBack()) {
|
||||
Actions::DrawMenuSeparator();
|
||||
Actions::DrawMenuAction(Actions::MakeNavigateBackAction(true), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [managerPtr]() {
|
||||
managerPtr->NavigateBack();
|
||||
});
|
||||
});
|
||||
}
|
||||
DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr));
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
@@ -570,21 +712,7 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem
|
||||
Actions::BeginProjectAssetDrag(item, iconKind);
|
||||
|
||||
if (UI::BeginContextMenuForLastItem("##ProjectItemContext")) {
|
||||
Actions::DrawMenuAction(Actions::MakeOpenAssetAction(Commands::CanOpenAsset(item)), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, item]() {
|
||||
Actions::OpenProjectAsset(*m_context, item);
|
||||
});
|
||||
});
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, item != nullptr), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, item]() {
|
||||
BeginRename(item);
|
||||
});
|
||||
});
|
||||
Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(item != nullptr), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, item]() {
|
||||
Commands::DeleteAsset(m_context->GetProjectManager(), item);
|
||||
});
|
||||
});
|
||||
DrawProjectContextMenu(m_context->GetProjectManager(), BuildContextMenuTarget(m_context->GetProjectManager(), item));
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,12 +41,21 @@ private:
|
||||
bool openRequested = false;
|
||||
};
|
||||
|
||||
struct ContextMenuTarget {
|
||||
AssetItemPtr item;
|
||||
std::string subjectPath;
|
||||
std::string createFolderPath;
|
||||
bool showInExplorerSelect = false;
|
||||
};
|
||||
|
||||
void BeginAssetDragDropFrame();
|
||||
void RegisterFolderDropTarget(IProjectManager& manager, const AssetItemPtr& folder);
|
||||
void FinalizeAssetDragDrop(IProjectManager& manager);
|
||||
void BeginRename(const AssetItemPtr& item);
|
||||
bool CommitRename(IProjectManager& manager);
|
||||
void CancelRename();
|
||||
ContextMenuTarget BuildContextMenuTarget(IProjectManager& manager, const AssetItemPtr& item) const;
|
||||
void DrawProjectContextMenu(IProjectManager& manager, const ContextMenuTarget& target);
|
||||
void RenderToolbar();
|
||||
void RenderFolderTreePane(IProjectManager& manager);
|
||||
void RenderFolderTreeNode(IProjectManager& manager, const AssetItemPtr& folder, const std::string& currentFolderPath);
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "Core/ISelectionManager.h"
|
||||
#include "SceneViewPanel.h"
|
||||
#include "Viewport/SceneViewportEditorOverlayData.h"
|
||||
#include "Viewport/SceneViewportOverlayHandleBuilder.h"
|
||||
#include "Viewport/SceneViewportOverlayHitTester.h"
|
||||
#include "Viewport/SceneViewportMath.h"
|
||||
#include "Viewport/SceneViewportOrientationGizmo.h"
|
||||
#include "Viewport/SceneViewportOverlayRenderer.h"
|
||||
#include "Viewport/SceneViewportTransformGizmoFrameBuilder.h"
|
||||
#include "ViewportPanelContent.h"
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "UI/UI.h"
|
||||
@@ -13,9 +18,11 @@
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
@@ -28,13 +35,209 @@ struct SceneViewportToolOverlayResult {
|
||||
SceneViewportToolMode clickedTool = SceneViewportToolMode::Move;
|
||||
};
|
||||
|
||||
enum class SceneViewportActiveGizmoKind : uint8_t {
|
||||
enum class SceneViewportInteractionKind : uint8_t {
|
||||
None = 0,
|
||||
Move,
|
||||
Rotate,
|
||||
Scale
|
||||
MoveGizmo,
|
||||
RotateGizmo,
|
||||
ScaleGizmo,
|
||||
OrientationGizmo,
|
||||
SceneIcon
|
||||
};
|
||||
|
||||
struct SceneViewportInteractionCandidate {
|
||||
SceneViewportInteractionKind kind = SceneViewportInteractionKind::None;
|
||||
int priority = 0;
|
||||
int secondaryPriority = 0;
|
||||
float distanceSq = Math::FLOAT_MAX;
|
||||
float depth = Math::FLOAT_MAX;
|
||||
uint64_t entityId = 0;
|
||||
SceneViewportGizmoAxis moveAxis = SceneViewportGizmoAxis::None;
|
||||
SceneViewportGizmoPlane movePlane = SceneViewportGizmoPlane::None;
|
||||
SceneViewportRotateGizmoAxis rotateAxis = SceneViewportRotateGizmoAxis::None;
|
||||
SceneViewportScaleGizmoHandle scaleHandle = SceneViewportScaleGizmoHandle::None;
|
||||
SceneViewportOrientationAxis orientationAxis = SceneViewportOrientationAxis::None;
|
||||
|
||||
bool HasHit() const {
|
||||
return kind != SceneViewportInteractionKind::None;
|
||||
}
|
||||
};
|
||||
|
||||
const char* GetSceneViewportPivotModeLabel(SceneViewportPivotMode mode) {
|
||||
return mode == SceneViewportPivotMode::Pivot ? "Pivot" : "Center";
|
||||
}
|
||||
|
||||
const char* GetSceneViewportTransformSpaceModeLabel(SceneViewportTransformSpaceMode mode) {
|
||||
return mode == SceneViewportTransformSpaceMode::Global ? "Global" : "Local";
|
||||
}
|
||||
|
||||
SceneViewportActiveGizmoKind ToActiveGizmoKind(SceneViewportInteractionKind kind) {
|
||||
switch (kind) {
|
||||
case SceneViewportInteractionKind::MoveGizmo:
|
||||
return SceneViewportActiveGizmoKind::Move;
|
||||
case SceneViewportInteractionKind::RotateGizmo:
|
||||
return SceneViewportActiveGizmoKind::Rotate;
|
||||
case SceneViewportInteractionKind::ScaleGizmo:
|
||||
return SceneViewportActiveGizmoKind::Scale;
|
||||
case SceneViewportInteractionKind::OrientationGizmo:
|
||||
case SceneViewportInteractionKind::SceneIcon:
|
||||
case SceneViewportInteractionKind::None:
|
||||
default:
|
||||
return SceneViewportActiveGizmoKind::None;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsBetterSceneViewportInteractionCandidate(
|
||||
const SceneViewportInteractionCandidate& candidate,
|
||||
const SceneViewportInteractionCandidate& current) {
|
||||
constexpr float kMetricEpsilon = 0.001f;
|
||||
|
||||
if (!candidate.HasHit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!current.HasHit()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (candidate.priority != current.priority) {
|
||||
return candidate.priority > current.priority;
|
||||
}
|
||||
|
||||
if (candidate.distanceSq + kMetricEpsilon < current.distanceSq) {
|
||||
return true;
|
||||
}
|
||||
if (current.distanceSq + kMetricEpsilon < candidate.distanceSq) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (candidate.depth + kMetricEpsilon < current.depth) {
|
||||
return true;
|
||||
}
|
||||
if (current.depth + kMetricEpsilon < candidate.depth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return candidate.secondaryPriority > current.secondaryPriority;
|
||||
}
|
||||
|
||||
void AccumulateSceneViewportInteractionCandidate(
|
||||
const SceneViewportInteractionCandidate& candidate,
|
||||
SceneViewportInteractionCandidate& bestCandidate) {
|
||||
if (IsBetterSceneViewportInteractionCandidate(candidate, bestCandidate)) {
|
||||
bestCandidate = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
SceneViewportInteractionCandidate BuildOrientationGizmoInteractionCandidate(
|
||||
SceneViewportOrientationAxis axis) {
|
||||
SceneViewportInteractionCandidate candidate = {};
|
||||
if (axis == SceneViewportOrientationAxis::None) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
candidate.kind = SceneViewportInteractionKind::OrientationGizmo;
|
||||
candidate.priority = 200;
|
||||
candidate.distanceSq = 0.0f;
|
||||
candidate.depth = 0.0f;
|
||||
candidate.orientationAxis = axis;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
SceneViewportInteractionCandidate BuildOverlayHandleInteractionCandidate(
|
||||
const SceneViewportOverlayHandleHitResult& hitResult) {
|
||||
SceneViewportInteractionCandidate candidate = {};
|
||||
if (!hitResult.HasHit()) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
candidate.priority = hitResult.priority;
|
||||
candidate.distanceSq = hitResult.distanceSq;
|
||||
candidate.depth = hitResult.depth;
|
||||
candidate.entityId = hitResult.entityId;
|
||||
switch (hitResult.kind) {
|
||||
case SceneViewportOverlayHandleKind::SceneIcon:
|
||||
candidate.kind = SceneViewportInteractionKind::SceneIcon;
|
||||
return candidate;
|
||||
case SceneViewportOverlayHandleKind::MoveAxis:
|
||||
candidate.kind = SceneViewportInteractionKind::MoveGizmo;
|
||||
candidate.moveAxis = static_cast<SceneViewportGizmoAxis>(hitResult.handleId);
|
||||
return candidate;
|
||||
case SceneViewportOverlayHandleKind::MovePlane:
|
||||
candidate.kind = SceneViewportInteractionKind::MoveGizmo;
|
||||
candidate.movePlane = static_cast<SceneViewportGizmoPlane>(hitResult.handleId);
|
||||
return candidate;
|
||||
case SceneViewportOverlayHandleKind::RotateAxis:
|
||||
candidate.kind = SceneViewportInteractionKind::RotateGizmo;
|
||||
candidate.rotateAxis = static_cast<SceneViewportRotateGizmoAxis>(hitResult.handleId);
|
||||
return candidate;
|
||||
case SceneViewportOverlayHandleKind::ScaleAxis:
|
||||
case SceneViewportOverlayHandleKind::ScaleUniform:
|
||||
candidate.kind = SceneViewportInteractionKind::ScaleGizmo;
|
||||
candidate.scaleHandle = static_cast<SceneViewportScaleGizmoHandle>(hitResult.handleId);
|
||||
return candidate;
|
||||
case SceneViewportOverlayHandleKind::None:
|
||||
default:
|
||||
return SceneViewportInteractionCandidate{};
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
float GetSceneToolbarToggleWidth(const char* firstLabel, const char* secondLabel) {
|
||||
constexpr float kHorizontalPadding = 10.0f;
|
||||
constexpr float kMinWidth = 68.0f;
|
||||
const float maxLabelWidth = (std::max)(
|
||||
ImGui::CalcTextSize(firstLabel).x,
|
||||
ImGui::CalcTextSize(secondLabel).x);
|
||||
return (std::max)(kMinWidth, maxLabelWidth + kHorizontalPadding * 2.0f);
|
||||
}
|
||||
|
||||
void RenderSceneViewportTopBar(
|
||||
SceneViewportPivotMode& pivotMode,
|
||||
SceneViewportTransformSpaceMode& transformSpaceMode) {
|
||||
constexpr float kSceneToolbarHeight = 24.0f;
|
||||
constexpr float kSceneToolbarPaddingY = 0.0f;
|
||||
constexpr float kSceneToolbarButtonHeight = kSceneToolbarHeight - kSceneToolbarPaddingY * 2.0f;
|
||||
constexpr ImVec2 kSceneToolbarButtonFramePadding(8.0f, 1.0f);
|
||||
|
||||
UI::PanelToolbarScope toolbar(
|
||||
"SceneToolbar",
|
||||
kSceneToolbarHeight,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse,
|
||||
true,
|
||||
ImVec2(UI::ToolbarPadding().x, kSceneToolbarPaddingY),
|
||||
ImVec2(0.0f, UI::ToolbarItemSpacing().y),
|
||||
ImVec4(0.23f, 0.23f, 0.23f, 1.0f));
|
||||
if (!toolbar.IsOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, kSceneToolbarButtonFramePadding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
|
||||
|
||||
if (UI::ToolbarButton(
|
||||
GetSceneViewportPivotModeLabel(pivotMode),
|
||||
true,
|
||||
ImVec2(GetSceneToolbarToggleWidth("Pivot", "Center"), kSceneToolbarButtonHeight))) {
|
||||
pivotMode = pivotMode == SceneViewportPivotMode::Pivot
|
||||
? SceneViewportPivotMode::Center
|
||||
: SceneViewportPivotMode::Pivot;
|
||||
}
|
||||
|
||||
ImGui::SameLine(0.0f, 0.0f);
|
||||
|
||||
if (UI::ToolbarButton(
|
||||
GetSceneViewportTransformSpaceModeLabel(transformSpaceMode),
|
||||
true,
|
||||
ImVec2(GetSceneToolbarToggleWidth("Global", "Local"), kSceneToolbarButtonHeight))) {
|
||||
transformSpaceMode = transformSpaceMode == SceneViewportTransformSpaceMode::Global
|
||||
? SceneViewportTransformSpaceMode::Local
|
||||
: SceneViewportTransformSpaceMode::Global;
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
const char* GetSceneViewportToolTooltip(SceneViewportToolMode toolMode) {
|
||||
switch (toolMode) {
|
||||
case SceneViewportToolMode::ViewMove:
|
||||
@@ -69,6 +272,19 @@ const char* GetSceneViewportToolIconBaseName(SceneViewportToolMode toolMode) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string BuildSceneViewportIconPath(const char* iconBaseName, bool active = false) {
|
||||
const std::filesystem::path exeDir(
|
||||
XCEngine::Editor::Platform::Utf8ToWide(XCEngine::Editor::Platform::GetExecutableDirectoryUtf8()));
|
||||
std::filesystem::path iconPath =
|
||||
exeDir / L".." / L".." / L"resources" / L"Icons" /
|
||||
std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(iconBaseName));
|
||||
if (active) {
|
||||
iconPath += L"_on";
|
||||
}
|
||||
iconPath += L".png";
|
||||
return XCEngine::Editor::Platform::WideToUtf8(iconPath.lexically_normal().wstring());
|
||||
}
|
||||
|
||||
const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode, bool active) {
|
||||
static std::string cachedPaths[5][2] = {};
|
||||
const size_t toolIndex = static_cast<size_t>(toolMode);
|
||||
@@ -78,16 +294,7 @@ const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode,
|
||||
return cachedPath;
|
||||
}
|
||||
|
||||
const std::filesystem::path exeDir(
|
||||
XCEngine::Editor::Platform::Utf8ToWide(XCEngine::Editor::Platform::GetExecutableDirectoryUtf8()));
|
||||
std::filesystem::path iconPath =
|
||||
(exeDir / L".." / L".." / L"resources" / L"Icons" /
|
||||
std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(GetSceneViewportToolIconBaseName(toolMode))));
|
||||
if (active) {
|
||||
iconPath += L"_on";
|
||||
}
|
||||
iconPath += L".png";
|
||||
cachedPath = XCEngine::Editor::Platform::WideToUtf8(iconPath.lexically_normal().wstring());
|
||||
cachedPath = BuildSceneViewportIconPath(GetSceneViewportToolIconBaseName(toolMode), active);
|
||||
return cachedPath;
|
||||
}
|
||||
|
||||
@@ -205,72 +412,6 @@ bool ShouldBeginSceneViewportNavigationDrag(
|
||||
ImGui::IsMouseClicked(button);
|
||||
}
|
||||
|
||||
SceneViewportMoveGizmoContext BuildMoveGizmoContext(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ViewportPanelContentResult& content,
|
||||
const ImVec2& mousePosition) {
|
||||
SceneViewportMoveGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y);
|
||||
gizmoContext.mousePosition = Math::Vector2(
|
||||
mousePosition.x - content.itemMin.x,
|
||||
mousePosition.y - content.itemMin.y);
|
||||
|
||||
if (context.GetSelectionManager().GetSelectionCount() == 1) {
|
||||
const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity();
|
||||
if (selectedEntity != 0) {
|
||||
gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
SceneViewportRotateGizmoContext BuildRotateGizmoContext(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ViewportPanelContentResult& content,
|
||||
const ImVec2& mousePosition) {
|
||||
SceneViewportRotateGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y);
|
||||
gizmoContext.mousePosition = Math::Vector2(
|
||||
mousePosition.x - content.itemMin.x,
|
||||
mousePosition.y - content.itemMin.y);
|
||||
|
||||
if (context.GetSelectionManager().GetSelectionCount() == 1) {
|
||||
const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity();
|
||||
if (selectedEntity != 0) {
|
||||
gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
SceneViewportScaleGizmoContext BuildScaleGizmoContext(
|
||||
IEditorContext& context,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ViewportPanelContentResult& content,
|
||||
const ImVec2& mousePosition) {
|
||||
SceneViewportScaleGizmoContext gizmoContext = {};
|
||||
gizmoContext.overlay = overlay;
|
||||
gizmoContext.viewportSize = Math::Vector2(content.availableSize.x, content.availableSize.y);
|
||||
gizmoContext.mousePosition = Math::Vector2(
|
||||
mousePosition.x - content.itemMin.x,
|
||||
mousePosition.y - content.itemMin.y);
|
||||
|
||||
if (context.GetSelectionManager().GetSelectionCount() == 1) {
|
||||
const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity();
|
||||
if (selectedEntity != 0) {
|
||||
gizmoContext.selectedObject = context.GetSceneManager().GetEntity(selectedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
return gizmoContext;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SceneViewPanel::SceneViewPanel() : Panel("Scene") {}
|
||||
@@ -283,6 +424,7 @@ void SceneViewPanel::Render() {
|
||||
return;
|
||||
}
|
||||
|
||||
RenderSceneViewportTopBar(m_pivotMode, m_transformSpaceMode);
|
||||
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene);
|
||||
if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) {
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
@@ -304,8 +446,20 @@ void SceneViewPanel::Render() {
|
||||
m_toolMode = toolOverlay.clickedTool;
|
||||
}
|
||||
|
||||
if (content.focused && !io.WantTextInput && !m_lookDragging && !m_panDragging) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_W, false)) {
|
||||
const bool allowToolShortcut = !io.WantTextInput && !m_lookDragging && !m_panDragging;
|
||||
if (allowToolShortcut) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Q, false)) {
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
if (m_rotateGizmo.IsActive()) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
if (m_scaleGizmo.IsActive()) {
|
||||
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
m_toolMode = SceneViewportToolMode::ViewMove;
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_W, false)) {
|
||||
if (m_rotateGizmo.IsActive()) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
@@ -337,103 +491,68 @@ void SceneViewPanel::Render() {
|
||||
const bool showingMoveGizmo = m_toolMode == SceneViewportToolMode::Move || usingTransformTool;
|
||||
const bool showingRotateGizmo = m_toolMode == SceneViewportToolMode::Rotate || usingTransformTool;
|
||||
const bool showingScaleGizmo = m_toolMode == SceneViewportToolMode::Scale || usingTransformTool;
|
||||
const bool useCenterPivot = m_pivotMode == SceneViewportPivotMode::Center;
|
||||
const bool localSpace = m_transformSpaceMode == SceneViewportTransformSpaceMode::Local;
|
||||
const Math::Vector2 viewportSize(content.availableSize.x, content.availableSize.y);
|
||||
const Math::Vector2 localMousePosition(
|
||||
io.MousePos.x - content.itemMin.x,
|
||||
io.MousePos.y - content.itemMin.y);
|
||||
SceneViewportOverlayData overlay = {};
|
||||
SceneViewportMoveGizmoContext moveGizmoContext = {};
|
||||
SceneViewportRotateGizmoContext rotateGizmoContext = {};
|
||||
SceneViewportScaleGizmoContext scaleGizmoContext = {};
|
||||
SceneViewportTransformGizmoFrameState gizmoFrameState = {};
|
||||
SceneViewportOverlayFrameData emptySceneOverlayFrameData = {};
|
||||
SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None;
|
||||
|
||||
if (hasInteractiveViewport) {
|
||||
overlay = viewportHostService->GetSceneViewOverlayData();
|
||||
if (showingMoveGizmo) {
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
if (m_moveGizmo.IsActive() &&
|
||||
(moveGizmoContext.selectedObject == nullptr ||
|
||||
m_context->GetSelectionManager().GetSelectedEntity() != m_moveGizmo.GetActiveEntityId())) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
} else if (m_moveGizmo.IsActive()) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
|
||||
if (showingRotateGizmo) {
|
||||
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
if (m_rotateGizmo.IsActive() &&
|
||||
(rotateGizmoContext.selectedObject == nullptr ||
|
||||
m_context->GetSelectionManager().GetSelectedEntity() != m_rotateGizmo.GetActiveEntityId())) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
} else if (m_rotateGizmo.IsActive()) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
|
||||
if (showingScaleGizmo) {
|
||||
scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
scaleGizmoContext.uniformOnly = usingTransformTool;
|
||||
if (m_scaleGizmo.IsActive() &&
|
||||
(scaleGizmoContext.selectedObject == nullptr ||
|
||||
m_context->GetSelectionManager().GetSelectedEntity() != m_scaleGizmo.GetActiveEntityId())) {
|
||||
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
} else if (m_scaleGizmo.IsActive()) {
|
||||
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
activeGizmoKind = SceneViewportActiveGizmoKind::Move;
|
||||
} else if (m_rotateGizmo.IsActive()) {
|
||||
activeGizmoKind = SceneViewportActiveGizmoKind::Rotate;
|
||||
} else if (m_scaleGizmo.IsActive()) {
|
||||
activeGizmoKind = SceneViewportActiveGizmoKind::Scale;
|
||||
}
|
||||
|
||||
if (showingMoveGizmo) {
|
||||
SceneViewportMoveGizmoContext updateContext = moveGizmoContext;
|
||||
if (activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
activeGizmoKind != SceneViewportActiveGizmoKind::Move) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_moveGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingRotateGizmo) {
|
||||
SceneViewportRotateGizmoContext updateContext = rotateGizmoContext;
|
||||
if (activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
activeGizmoKind != SceneViewportActiveGizmoKind::Rotate) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_rotateGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingScaleGizmo) {
|
||||
SceneViewportScaleGizmoContext updateContext = scaleGizmoContext;
|
||||
if (activeGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
activeGizmoKind != SceneViewportActiveGizmoKind::Scale) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_scaleGizmo.Update(updateContext);
|
||||
}
|
||||
gizmoFrameState = RefreshSceneViewportTransformGizmos(
|
||||
*m_context,
|
||||
overlay,
|
||||
viewportSize,
|
||||
localMousePosition,
|
||||
useCenterPivot,
|
||||
localSpace,
|
||||
usingTransformTool,
|
||||
showingMoveGizmo,
|
||||
m_moveGizmo,
|
||||
showingRotateGizmo,
|
||||
m_rotateGizmo,
|
||||
showingScaleGizmo,
|
||||
m_scaleGizmo);
|
||||
activeGizmoKind = gizmoFrameState.activeGizmoKind;
|
||||
} else {
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
if (m_rotateGizmo.IsActive()) {
|
||||
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
if (m_scaleGizmo.IsActive()) {
|
||||
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
|
||||
}
|
||||
CancelSceneViewportTransformGizmoDrags(*m_context, m_moveGizmo, m_rotateGizmo, m_scaleGizmo);
|
||||
}
|
||||
|
||||
const bool moveGizmoHovering = showingMoveGizmo && m_moveGizmo.IsHoveringHandle();
|
||||
const bool rotateGizmoHovering = showingRotateGizmo && m_rotateGizmo.IsHoveringHandle();
|
||||
const bool scaleGizmoHovering = showingScaleGizmo && m_scaleGizmo.IsHoveringHandle();
|
||||
const SceneViewportTransformGizmoHandleBuildInputs interactionGizmoInputs =
|
||||
hasInteractiveViewport
|
||||
? BuildSceneViewportTransformGizmoHandleBuildInputs(
|
||||
showingMoveGizmo,
|
||||
m_moveGizmo,
|
||||
gizmoFrameState.moveContext,
|
||||
showingRotateGizmo,
|
||||
m_rotateGizmo,
|
||||
gizmoFrameState.rotateContext,
|
||||
showingScaleGizmo,
|
||||
m_scaleGizmo,
|
||||
gizmoFrameState.scaleContext)
|
||||
: SceneViewportTransformGizmoHandleBuildInputs{};
|
||||
const SceneViewportOverlayFrameData& interactionOverlayFrameData =
|
||||
hasInteractiveViewport
|
||||
? viewportHostService->GetSceneViewInteractionOverlayFrameData(
|
||||
*m_context,
|
||||
overlay,
|
||||
interactionGizmoInputs)
|
||||
: emptySceneOverlayFrameData;
|
||||
const SceneViewportOverlayHandleHitResult overlayHandleHit =
|
||||
hasInteractiveViewport
|
||||
? HitTestSceneViewportOverlayHandles(
|
||||
interactionOverlayFrameData,
|
||||
Math::Vector2(content.availableSize.x, content.availableSize.y),
|
||||
localMousePosition)
|
||||
: SceneViewportOverlayHandleHitResult{};
|
||||
const bool moveGizmoActive = showingMoveGizmo && m_moveGizmo.IsActive();
|
||||
const bool rotateGizmoActive = showingRotateGizmo && m_rotateGizmo.IsActive();
|
||||
const bool scaleGizmoActive = showingScaleGizmo && m_scaleGizmo.IsActive();
|
||||
const SceneViewportActiveGizmoKind hoveredGizmoKind = scaleGizmoHovering
|
||||
? SceneViewportActiveGizmoKind::Scale
|
||||
: (moveGizmoHovering ? SceneViewportActiveGizmoKind::Move
|
||||
: (rotateGizmoHovering ? SceneViewportActiveGizmoKind::Rotate
|
||||
: SceneViewportActiveGizmoKind::None));
|
||||
if (moveGizmoActive) {
|
||||
activeGizmoKind = SceneViewportActiveGizmoKind::Move;
|
||||
} else if (rotateGizmoActive) {
|
||||
@@ -443,44 +562,83 @@ void SceneViewPanel::Render() {
|
||||
} else {
|
||||
activeGizmoKind = SceneViewportActiveGizmoKind::None;
|
||||
}
|
||||
const bool gizmoHovering = hoveredGizmoKind != SceneViewportActiveGizmoKind::None;
|
||||
const bool gizmoActive = activeGizmoKind != SceneViewportActiveGizmoKind::None;
|
||||
|
||||
const bool beginTransformGizmo =
|
||||
hasInteractiveViewport &&
|
||||
content.clickedLeft &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
!toolOverlay.hovered &&
|
||||
gizmoHovering;
|
||||
const SceneViewportOrientationAxis orientationAxisHit =
|
||||
SceneViewportInteractionCandidate hoveredInteraction = {};
|
||||
const bool canResolveViewportInteraction =
|
||||
hasInteractiveViewport &&
|
||||
viewportContentHovered &&
|
||||
!usingViewMoveTool &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
!gizmoHovering &&
|
||||
!gizmoActive
|
||||
? HitTestSceneViewportOrientationGizmo(
|
||||
overlay,
|
||||
content.itemMin,
|
||||
content.itemMax,
|
||||
io.MousePos)
|
||||
: SceneViewportOrientationAxis::None;
|
||||
!toolOverlay.hovered &&
|
||||
!gizmoActive;
|
||||
if (canResolveViewportInteraction) {
|
||||
AccumulateSceneViewportInteractionCandidate(
|
||||
BuildOverlayHandleInteractionCandidate(overlayHandleHit),
|
||||
hoveredInteraction);
|
||||
|
||||
AccumulateSceneViewportInteractionCandidate(
|
||||
BuildOrientationGizmoInteractionCandidate(
|
||||
HitTestSceneViewportOrientationGizmo(
|
||||
overlay,
|
||||
content.itemMin,
|
||||
content.itemMax,
|
||||
io.MousePos)),
|
||||
hoveredInteraction);
|
||||
}
|
||||
|
||||
if (!gizmoActive) {
|
||||
if (showingMoveGizmo) {
|
||||
m_moveGizmo.SetHoveredHandle(
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::MoveGizmo
|
||||
? hoveredInteraction.moveAxis
|
||||
: SceneViewportGizmoAxis::None,
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::MoveGizmo
|
||||
? hoveredInteraction.movePlane
|
||||
: SceneViewportGizmoPlane::None);
|
||||
}
|
||||
if (showingRotateGizmo) {
|
||||
m_rotateGizmo.SetHoveredHandle(
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::RotateGizmo
|
||||
? hoveredInteraction.rotateAxis
|
||||
: SceneViewportRotateGizmoAxis::None);
|
||||
}
|
||||
if (showingScaleGizmo) {
|
||||
m_scaleGizmo.SetHoveredHandle(
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::ScaleGizmo
|
||||
? hoveredInteraction.scaleHandle
|
||||
: SceneViewportScaleGizmoHandle::None);
|
||||
}
|
||||
}
|
||||
|
||||
const SceneViewportActiveGizmoKind hoveredGizmoKind =
|
||||
ToActiveGizmoKind(hoveredInteraction.kind);
|
||||
const bool gizmoHovering = hoveredGizmoKind != SceneViewportActiveGizmoKind::None;
|
||||
const SceneViewportOrientationAxis orientationAxisHit =
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::OrientationGizmo
|
||||
? hoveredInteraction.orientationAxis
|
||||
: SceneViewportOrientationAxis::None;
|
||||
const uint64_t clickedSceneIconEntity =
|
||||
hoveredInteraction.kind == SceneViewportInteractionKind::SceneIcon
|
||||
? hoveredInteraction.entityId
|
||||
: 0;
|
||||
const bool beginTransformGizmo =
|
||||
hasInteractiveViewport &&
|
||||
content.clickedLeft &&
|
||||
gizmoHovering;
|
||||
const bool orientationGizmoClick =
|
||||
hasInteractiveViewport &&
|
||||
content.clickedLeft &&
|
||||
orientationAxisHit != SceneViewportOrientationAxis::None;
|
||||
const bool sceneIconClick =
|
||||
hasInteractiveViewport &&
|
||||
content.clickedLeft &&
|
||||
clickedSceneIconEntity != 0;
|
||||
const bool selectClick =
|
||||
hasInteractiveViewport &&
|
||||
content.clickedLeft &&
|
||||
viewportContentHovered &&
|
||||
!usingViewMoveTool &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging &&
|
||||
!orientationGizmoClick &&
|
||||
!gizmoHovering &&
|
||||
!gizmoActive;
|
||||
canResolveViewportInteraction &&
|
||||
!hoveredInteraction.HasHit();
|
||||
const bool beginLeftPanDrag = usingViewMoveTool
|
||||
? ShouldBeginSceneViewportNavigationDrag(
|
||||
hasInteractiveViewport,
|
||||
@@ -525,47 +683,32 @@ void SceneViewPanel::Render() {
|
||||
io.MouseDelta.y);
|
||||
}
|
||||
|
||||
if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || selectClick || beginLookDrag ||
|
||||
if (toolOverlay.clicked || beginTransformGizmo || orientationGizmoClick || sceneIconClick || selectClick || beginLookDrag ||
|
||||
beginPanDrag) {
|
||||
ImGui::SetWindowFocus();
|
||||
}
|
||||
|
||||
if (beginTransformGizmo) {
|
||||
if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Scale) {
|
||||
m_scaleGizmo.TryBeginDrag(scaleGizmoContext, m_context->GetUndoManager());
|
||||
m_scaleGizmo.TryBeginDrag(gizmoFrameState.scaleContext, m_context->GetUndoManager());
|
||||
} else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Move) {
|
||||
m_moveGizmo.TryBeginDrag(moveGizmoContext, m_context->GetUndoManager());
|
||||
m_moveGizmo.TryBeginDrag(gizmoFrameState.moveContext, m_context->GetUndoManager());
|
||||
} else if (hoveredGizmoKind == SceneViewportActiveGizmoKind::Rotate) {
|
||||
m_rotateGizmo.TryBeginDrag(rotateGizmoContext, m_context->GetUndoManager());
|
||||
m_rotateGizmo.TryBeginDrag(gizmoFrameState.rotateContext, m_context->GetUndoManager());
|
||||
}
|
||||
}
|
||||
|
||||
if (orientationGizmoClick) {
|
||||
viewportHostService->AlignSceneViewToOrientationAxis(orientationAxisHit);
|
||||
overlay = viewportHostService->GetSceneViewOverlayData();
|
||||
if (showingMoveGizmo) {
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
m_moveGizmo.Update(moveGizmoContext);
|
||||
}
|
||||
if (showingRotateGizmo) {
|
||||
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
m_rotateGizmo.Update(rotateGizmoContext);
|
||||
}
|
||||
if (showingScaleGizmo) {
|
||||
scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
scaleGizmoContext.uniformOnly = usingTransformTool;
|
||||
m_scaleGizmo.Update(scaleGizmoContext);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectClick) {
|
||||
const ImVec2 localMousePosition(
|
||||
io.MousePos.x - content.itemMin.x,
|
||||
io.MousePos.y - content.itemMin.y);
|
||||
if (sceneIconClick) {
|
||||
m_context->GetSelectionManager().SetSelectedEntity(clickedSceneIconEntity);
|
||||
} else if (selectClick) {
|
||||
const uint64_t selectedEntity = viewportHostService->PickSceneViewEntity(
|
||||
*m_context,
|
||||
content.availableSize,
|
||||
localMousePosition);
|
||||
ImVec2(localMousePosition.x, localMousePosition.y));
|
||||
if (selectedEntity != 0) {
|
||||
m_context->GetSelectionManager().SetSelectedEntity(selectedEntity);
|
||||
} else {
|
||||
@@ -576,11 +719,11 @@ void SceneViewPanel::Render() {
|
||||
if (gizmoActive) {
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) {
|
||||
m_moveGizmo.UpdateDrag(moveGizmoContext);
|
||||
m_moveGizmo.UpdateDrag(gizmoFrameState.moveContext);
|
||||
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Rotate) {
|
||||
m_rotateGizmo.UpdateDrag(rotateGizmoContext);
|
||||
m_rotateGizmo.UpdateDrag(gizmoFrameState.rotateContext);
|
||||
} else if (activeGizmoKind == SceneViewportActiveGizmoKind::Scale) {
|
||||
m_scaleGizmo.UpdateDrag(scaleGizmoContext);
|
||||
m_scaleGizmo.UpdateDrag(gizmoFrameState.scaleContext);
|
||||
}
|
||||
} else {
|
||||
if (activeGizmoKind == SceneViewportActiveGizmoKind::Move) {
|
||||
@@ -707,52 +850,41 @@ void SceneViewPanel::Render() {
|
||||
|
||||
if (content.hasViewportArea && content.frame.hasTexture) {
|
||||
overlay = viewportHostService->GetSceneViewOverlayData();
|
||||
SceneViewportActiveGizmoKind drawActiveGizmoKind = SceneViewportActiveGizmoKind::None;
|
||||
if (m_moveGizmo.IsActive()) {
|
||||
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Move;
|
||||
} else if (m_rotateGizmo.IsActive()) {
|
||||
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Rotate;
|
||||
} else if (m_scaleGizmo.IsActive()) {
|
||||
drawActiveGizmoKind = SceneViewportActiveGizmoKind::Scale;
|
||||
}
|
||||
if (showingMoveGizmo) {
|
||||
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
SceneViewportMoveGizmoContext updateContext = moveGizmoContext;
|
||||
if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
drawActiveGizmoKind != SceneViewportActiveGizmoKind::Move) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_moveGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingRotateGizmo) {
|
||||
rotateGizmoContext = BuildRotateGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
SceneViewportRotateGizmoContext updateContext = rotateGizmoContext;
|
||||
if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
drawActiveGizmoKind != SceneViewportActiveGizmoKind::Rotate) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_rotateGizmo.Update(updateContext);
|
||||
}
|
||||
if (showingScaleGizmo) {
|
||||
scaleGizmoContext = BuildScaleGizmoContext(*m_context, overlay, content, io.MousePos);
|
||||
scaleGizmoContext.uniformOnly = usingTransformTool;
|
||||
SceneViewportScaleGizmoContext updateContext = scaleGizmoContext;
|
||||
if (drawActiveGizmoKind != SceneViewportActiveGizmoKind::None &&
|
||||
drawActiveGizmoKind != SceneViewportActiveGizmoKind::Scale) {
|
||||
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
|
||||
}
|
||||
m_scaleGizmo.Update(updateContext);
|
||||
}
|
||||
const SceneViewportTransformGizmoFrameState drawGizmoFrameState =
|
||||
RefreshSceneViewportTransformGizmos(
|
||||
*m_context,
|
||||
overlay,
|
||||
viewportSize,
|
||||
localMousePosition,
|
||||
useCenterPivot,
|
||||
localSpace,
|
||||
usingTransformTool,
|
||||
showingMoveGizmo,
|
||||
m_moveGizmo,
|
||||
showingRotateGizmo,
|
||||
m_rotateGizmo,
|
||||
showingScaleGizmo,
|
||||
m_scaleGizmo);
|
||||
|
||||
viewportHostService->SetSceneViewTransientTransformGizmoOverlayData(
|
||||
overlay,
|
||||
BuildSceneViewportTransformGizmoHandleBuildInputs(
|
||||
showingMoveGizmo,
|
||||
m_moveGizmo,
|
||||
drawGizmoFrameState.moveContext,
|
||||
showingRotateGizmo,
|
||||
m_rotateGizmo,
|
||||
drawGizmoFrameState.rotateContext,
|
||||
showingScaleGizmo,
|
||||
m_scaleGizmo,
|
||||
drawGizmoFrameState.scaleContext));
|
||||
|
||||
DrawSceneViewportOverlay(
|
||||
ImGui::GetWindowDrawList(),
|
||||
overlay,
|
||||
content.itemMin,
|
||||
content.itemMax,
|
||||
content.availableSize,
|
||||
showingMoveGizmo ? &m_moveGizmo.GetDrawData() : nullptr,
|
||||
showingRotateGizmo ? &m_rotateGizmo.GetDrawData() : nullptr,
|
||||
showingScaleGizmo ? &m_scaleGizmo.GetDrawData() : nullptr);
|
||||
content.availableSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,16 @@ enum class SceneViewportToolMode : uint8_t {
|
||||
Transform
|
||||
};
|
||||
|
||||
enum class SceneViewportPivotMode : uint8_t {
|
||||
Pivot = 0,
|
||||
Center
|
||||
};
|
||||
|
||||
enum class SceneViewportTransformSpaceMode : uint8_t {
|
||||
Global = 0,
|
||||
Local
|
||||
};
|
||||
|
||||
class SceneViewPanel : public Panel {
|
||||
public:
|
||||
SceneViewPanel();
|
||||
@@ -25,6 +35,8 @@ public:
|
||||
|
||||
private:
|
||||
SceneViewportToolMode m_toolMode = SceneViewportToolMode::Move;
|
||||
SceneViewportPivotMode m_pivotMode = SceneViewportPivotMode::Pivot;
|
||||
SceneViewportTransformSpaceMode m_transformSpaceMode = SceneViewportTransformSpaceMode::Global;
|
||||
bool m_lookDragging = false;
|
||||
bool m_panDragging = false;
|
||||
int m_panDragButton = ImGuiMouseButton_Middle;
|
||||
|
||||
@@ -59,6 +59,7 @@ inline void RenderViewportInteractionSurface(
|
||||
ViewportPanelContentResult& result,
|
||||
EditorViewportKind kind,
|
||||
const ImVec2& interactionSize) {
|
||||
ImGui::SetNextItemAllowOverlap();
|
||||
ImGui::InvisibleButton(
|
||||
GetViewportInteractionSurfaceId(kind),
|
||||
interactionSize,
|
||||
|
||||
BIN
editor/tab.png
|
Before Width: | Height: | Size: 2.4 KiB |
BIN
editor/unity.png
|
Before Width: | Height: | Size: 786 KiB |
|
Before Width: | Height: | Size: 117 KiB |