feat: expand editor scripting asset and viewport flow

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

View File

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

View File

@@ -1,183 +1,205 @@
# UI Editor
# XCEditor
Unity 风格的编辑器 UI使用 ImGui 实现,作为 XCEngine 游戏引擎编辑器的一部分
`editor/` 是 XCEngine 当前随仓库维护的桌面编辑器模块。它不是一套独立渲染器,而是 `D3D12` 宿主应用,用来承接引擎 `Rendering + RHI + Scene + Scripting` 主链
## 简介
当前 editor 已经具备:
XCGameEngine UI 是一个仿 Unity 编辑器的桌面应用程序,提供场景管理、层级视图、属性检查器等功能。
- Scene / Game viewport 离屏渲染接入
- object-id picking 与选中描边
- scene overlay / gizmo 正规化收口
- 项目根目录解析与 `Project.xcproject` 加载
- `Assets + .meta + Library` 风格项目目录接入
- `ScriptComponent` 的脚本类与字段编辑入口
## 技术栈
## 当前定位
- **渲染 API**: DirectX 12
- **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
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1,28 +0,0 @@
import os
from PIL import Image
def get_dominant_color(image_path):
img = Image.open(image_path).convert("RGB")
img = img.resize((1, 1), Image.Resampling.LANCZOS)
r, g, b = img.getpixel((0, 0))
return r, g, b
def rename_with_color(base_path):
files = ["color.png", "color2.png"]
for f in files:
old_path = os.path.join(base_path, f)
if os.path.exists(old_path):
r, g, b = get_dominant_color(old_path)
new_name = f"color-({r},{g},{b}).png"
new_path = os.path.join(base_path, new_name)
os.rename(old_path, new_path)
print(f"Renamed: {f} -> {new_name}")
else:
print(f"File not found: {old_path}")
if __name__ == "__main__":
base = r"D:\Xuanchi\Main\XCEngine\editor"
rename_with_color(base)

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,515 @@
#pragma once
#include "Application.h"
#include "IComponentEditor.h"
#include "ScriptComponentEditorUtils.h"
#include "Core/IUndoManager.h"
#include "UI/UI.h"
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
namespace XCEngine {
namespace Editor {
class ScriptComponentEditor : public IComponentEditor {
public:
const char* GetComponentTypeName() const override {
return "ScriptComponent";
}
const char* GetDisplayName() const override {
return "Script";
}
bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override {
auto* scriptComponent = dynamic_cast<::XCEngine::Scripting::ScriptComponent*>(component);
if (!scriptComponent) {
return false;
}
bool changed = false;
changed |= RenderScriptClassSelector(*scriptComponent, undoManager);
::XCEngine::Scripting::ScriptFieldModel model;
if (!::XCEngine::Scripting::ScriptEngine::Get().TryGetScriptFieldModel(scriptComponent, model)) {
UI::DrawHintText("Failed to query script field metadata.");
return changed;
}
switch (model.classStatus) {
case ::XCEngine::Scripting::ScriptFieldClassStatus::Unassigned:
UI::DrawHintText("Assign a C# script to edit serialized fields.");
return changed;
case ::XCEngine::Scripting::ScriptFieldClassStatus::Missing:
UI::DrawHintText("Assigned script class is not available in the loaded script assemblies.");
break;
case ::XCEngine::Scripting::ScriptFieldClassStatus::Available:
default:
break;
}
if (model.fields.empty()) {
UI::DrawHintText(
model.classStatus == ::XCEngine::Scripting::ScriptFieldClassStatus::Available
? "Selected script exposes no supported public instance fields."
: "No serialized script fields are currently available.");
return changed;
}
for (const ::XCEngine::Scripting::ScriptFieldSnapshot& field : model.fields) {
changed |= RenderScriptField(*scriptComponent, model.classStatus, field, undoManager);
}
return changed;
}
bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override {
return gameObject != nullptr;
}
const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override {
return gameObject ? nullptr : "Invalid";
}
bool CanRemove(::XCEngine::Components::Component* component) const override {
return CanEdit(component);
}
private:
static constexpr size_t kStringBufferSize = 512;
struct StringFieldEditState {
std::array<char, kStringBufferSize> buffer{};
std::string lastSyncedValue;
bool initialized = false;
bool editing = false;
};
bool RenderScriptClassSelector(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
IUndoManager* undoManager) {
std::vector<::XCEngine::Scripting::ScriptClassDescriptor> scriptClasses;
const bool hasLoadedClasses =
::XCEngine::Scripting::ScriptEngine::Get().TryGetAvailableScriptClasses(scriptClasses);
const ::XCEngine::Scripting::ScriptClassDescriptor currentDescriptor{
scriptComponent.GetAssemblyName(),
scriptComponent.GetNamespaceName(),
scriptComponent.GetClassName()
};
std::string currentLabel = "None";
if (scriptComponent.HasScriptClass()) {
const auto currentIt = std::find(scriptClasses.begin(), scriptClasses.end(), currentDescriptor);
currentLabel = currentIt != scriptClasses.end()
? ScriptComponentEditorUtils::BuildScriptClassDisplayName(*currentIt)
: ScriptComponentEditorUtils::BuildScriptClassDisplayName(scriptComponent) + " (Missing)";
}
bool changed = false;
UI::DrawPropertyRow("Script", UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::SetNextItemWidth(layout.controlWidth);
if (!ImGui::BeginCombo("##ScriptClass", currentLabel.c_str())) {
return false;
}
if (ImGui::Selectable("None", !scriptComponent.HasScriptClass())) {
changed |= ApplyScriptClassSelection(scriptComponent, nullptr, undoManager);
}
if (!scriptClasses.empty()) {
ImGui::Separator();
}
for (const ::XCEngine::Scripting::ScriptClassDescriptor& descriptor : scriptClasses) {
const bool selected = descriptor == currentDescriptor;
const std::string label =
ScriptComponentEditorUtils::BuildScriptClassDisplayName(descriptor);
if (ImGui::Selectable(label.c_str(), selected)) {
changed |= ApplyScriptClassSelection(scriptComponent, &descriptor, undoManager);
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
if (!hasLoadedClasses) {
ImGui::Separator();
ImGui::TextDisabled("No script assemblies are currently loaded.");
}
ImGui::EndCombo();
return false;
});
if (!hasLoadedClasses) {
const EditorScriptRuntimeStatus& runtimeStatus = Application::Get().GetScriptRuntimeStatus();
const std::string hintText =
ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(runtimeStatus);
UI::DrawHintText(hintText.c_str());
if (ScriptComponentEditorUtils::CanRebuildScriptAssemblies(runtimeStatus)) {
if (UI::InspectorActionButton("Rebuild Scripts", ImVec2(120.0f, 0.0f))) {
Application::Get().RebuildScriptingAssemblies();
}
}
if (ScriptComponentEditorUtils::CanReloadScriptRuntime(runtimeStatus)) {
if (ScriptComponentEditorUtils::CanRebuildScriptAssemblies(runtimeStatus)) {
ImGui::SameLine();
}
if (UI::InspectorActionButton("Reload Scripts", ImVec2(120.0f, 0.0f))) {
Application::Get().ReloadScriptingRuntime();
}
}
}
return changed;
}
bool ApplyScriptClassSelection(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptClassDescriptor* descriptor,
IUndoManager* undoManager) const {
if (!descriptor) {
if (!scriptComponent.HasScriptClass()) {
return false;
}
if (undoManager) {
undoManager->BeginInteractiveChange("Modify Script Component");
}
scriptComponent.ClearScriptClass();
return true;
}
if (scriptComponent.GetAssemblyName() == descriptor->assemblyName &&
scriptComponent.GetNamespaceName() == descriptor->namespaceName &&
scriptComponent.GetClassName() == descriptor->className) {
return false;
}
if (undoManager) {
undoManager->BeginInteractiveChange("Modify Script Component");
}
scriptComponent.SetScriptClass(
descriptor->assemblyName,
descriptor->namespaceName,
descriptor->className);
return true;
}
bool RenderScriptField(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
::XCEngine::Scripting::ScriptFieldClassStatus classStatus,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
IUndoManager* undoManager) {
bool changed = false;
const bool canEdit = ScriptComponentEditorUtils::CanEditScriptField(classStatus, field);
if (canEdit) {
changed |= RenderScriptFieldEditor(scriptComponent, field, undoManager);
} else {
RenderReadOnlyScriptField(field);
}
const std::string issueText = ScriptComponentEditorUtils::BuildScriptFieldIssueText(field);
if (!issueText.empty()) {
UI::DrawHintText(issueText.c_str());
}
if (ScriptComponentEditorUtils::CanClearScriptFieldOverride(field)) {
ImGui::PushID((field.metadata.name + "##Reset").c_str());
if (UI::InspectorActionButton("Reset", ImVec2(72.0f, 0.0f))) {
changed |= ClearScriptFieldOverride(scriptComponent, field, undoManager);
}
ImGui::PopID();
}
return changed;
}
bool RenderScriptFieldEditor(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
IUndoManager* undoManager) {
using namespace ::XCEngine::Scripting;
switch (field.metadata.type) {
case ScriptFieldType::Float: {
float value = std::get<float>(field.value);
const bool widgetChanged = UI::DrawPropertyFloat(field.metadata.name.c_str(), value, 0.1f);
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::Double: {
double value = std::get<double>(field.value);
const bool widgetChanged = UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::SetNextItemWidth(layout.controlWidth);
return ImGui::InputDouble("##Value", &value, 0.0, 0.0, "%.3f");
});
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::Bool: {
bool value = std::get<bool>(field.value);
const bool widgetChanged = UI::DrawPropertyBool(field.metadata.name.c_str(), value);
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::Int32: {
int value = std::get<int32_t>(field.value);
const bool widgetChanged = UI::DrawPropertyInt(field.metadata.name.c_str(), value, 1);
return widgetChanged &&
ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(static_cast<int32_t>(value)), undoManager);
}
case ScriptFieldType::UInt64: {
uint64_t value = std::get<uint64_t>(field.value);
const bool widgetChanged = UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::SetNextItemWidth(layout.controlWidth);
return ImGui::InputScalar("##Value", ImGuiDataType_U64, &value);
});
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::String:
return RenderStringScriptFieldEditor(scriptComponent, field, undoManager);
case ScriptFieldType::Vector2: {
::XCEngine::Math::Vector2 value = std::get<::XCEngine::Math::Vector2>(field.value);
const bool widgetChanged = UI::DrawPropertyVec2(field.metadata.name.c_str(), value, 0.0f, 0.1f);
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::Vector3: {
::XCEngine::Math::Vector3 value = std::get<::XCEngine::Math::Vector3>(field.value);
const bool widgetChanged = UI::DrawPropertyVec3Input(field.metadata.name.c_str(), value, 0.1f);
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::Vector4: {
::XCEngine::Math::Vector4 value = std::get<::XCEngine::Math::Vector4>(field.value);
const bool widgetChanged = UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::SetNextItemWidth(layout.controlWidth);
return ImGui::DragFloat4("##Value", &value.x, 0.1f);
});
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::GameObject:
return RenderGameObjectScriptFieldEditor(scriptComponent, field, undoManager);
case ScriptFieldType::None:
default:
RenderReadOnlyScriptField(field);
return false;
}
}
bool RenderStringScriptFieldEditor(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
IUndoManager* undoManager) {
StringFieldEditState& editState =
m_stringFieldStates[scriptComponent.GetScriptComponentUUID()][field.metadata.name];
const std::string currentValue = std::get<std::string>(field.value);
if (!editState.initialized || (!editState.editing && editState.lastSyncedValue != currentValue)) {
SyncStringFieldEditState(editState, currentValue);
}
bool isEditing = false;
const bool widgetChanged = UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::SetNextItemWidth(layout.controlWidth);
const bool changed = ImGui::InputText("##Value", editState.buffer.data(), editState.buffer.size());
isEditing = ImGui::IsItemActive();
return changed;
});
editState.editing = isEditing;
if (!widgetChanged) {
return false;
}
const std::string editedValue(editState.buffer.data());
if (!ApplyScriptFieldWrite(
scriptComponent,
field,
::XCEngine::Scripting::ScriptFieldValue(editedValue),
undoManager)) {
SyncStringFieldEditState(editState, currentValue);
return false;
}
editState.lastSyncedValue = editedValue;
return true;
}
bool RenderGameObjectScriptFieldEditor(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
IUndoManager* undoManager) {
using namespace ::XCEngine::Scripting;
const GameObjectReference currentReference = std::get<GameObjectReference>(field.value);
const auto& sceneRoots = Application::Get().GetEditorContext().GetSceneManager().GetRootEntities();
const ::XCEngine::Components::GameObject::ID currentGameObjectId =
ScriptComponentEditorUtils::FindGameObjectIdByUuid(sceneRoots, currentReference.gameObjectUUID);
UI::ReferencePickerOptions pickerOptions;
pickerOptions.popupTitle = "Select GameObject";
pickerOptions.emptyHint = "None";
pickerOptions.searchHint = "Search";
pickerOptions.noSceneText = "No scene objects.";
pickerOptions.showAssetsTab = false;
pickerOptions.showSceneTab = true;
GameObjectReference pendingReference = currentReference;
const bool widgetChanged = UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
const UI::ReferencePickerInteraction interaction =
UI::DrawReferencePickerControl(
std::string(),
currentGameObjectId,
pickerOptions,
layout.controlWidth);
if (interaction.clearRequested) {
pendingReference = GameObjectReference{};
return true;
}
if (interaction.assignedSceneObjectId == ::XCEngine::Components::GameObject::INVALID_ID ||
interaction.assignedSceneObjectId == currentGameObjectId) {
return false;
}
::XCEngine::Components::GameObject* assignedGameObject =
Application::Get().GetEditorContext().GetSceneManager().GetEntity(interaction.assignedSceneObjectId);
if (!assignedGameObject) {
return false;
}
pendingReference = GameObjectReference{assignedGameObject->GetUUID()};
return true;
});
return widgetChanged &&
ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(pendingReference), undoManager);
}
void RenderReadOnlyScriptField(const ::XCEngine::Scripting::ScriptFieldSnapshot& field) const {
const std::string valueText = DescribeScriptFieldValue(field.metadata.type, field.value);
UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetTextLineHeight());
ImGui::TextDisabled("%s", valueText.c_str());
return false;
});
}
bool ApplyScriptFieldWrite(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
const ::XCEngine::Scripting::ScriptFieldValue& value,
IUndoManager* undoManager) const {
std::vector<::XCEngine::Scripting::ScriptFieldWriteResult> results;
if (undoManager) {
undoManager->BeginInteractiveChange("Modify Script Field");
}
const bool applied = ::XCEngine::Scripting::ScriptEngine::Get().ApplyScriptFieldWrites(
&scriptComponent,
{ ::XCEngine::Scripting::ScriptFieldWriteRequest{field.metadata.name, field.metadata.type, value} },
results);
if (!applied || results.empty() ||
results.front().status != ::XCEngine::Scripting::ScriptFieldWriteStatus::Applied) {
if (undoManager && undoManager->HasPendingInteractiveChange()) {
undoManager->CancelInteractiveChange();
}
return false;
}
return true;
}
bool ClearScriptFieldOverride(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
IUndoManager* undoManager) const {
std::vector<::XCEngine::Scripting::ScriptFieldClearResult> results;
if (undoManager) {
undoManager->BeginInteractiveChange("Clear Script Field Override");
}
const bool cleared = ::XCEngine::Scripting::ScriptEngine::Get().ClearScriptFieldOverrides(
&scriptComponent,
{ ::XCEngine::Scripting::ScriptFieldClearRequest{field.metadata.name} },
results);
if (!cleared || results.empty() ||
results.front().status != ::XCEngine::Scripting::ScriptFieldClearStatus::Applied) {
if (undoManager && undoManager->HasPendingInteractiveChange()) {
undoManager->CancelInteractiveChange();
}
return false;
}
return true;
}
static void SyncStringFieldEditState(StringFieldEditState& editState, const std::string& value) {
editState.buffer.fill('\0');
const size_t copyLength = (std::min)(value.size(), editState.buffer.size() - 1);
if (copyLength > 0) {
std::memcpy(editState.buffer.data(), value.data(), copyLength);
}
editState.buffer[copyLength] = '\0';
editState.lastSyncedValue = value;
editState.initialized = true;
}
static std::string DescribeScriptFieldValue(
::XCEngine::Scripting::ScriptFieldType type,
const ::XCEngine::Scripting::ScriptFieldValue& value) {
if (type == ::XCEngine::Scripting::ScriptFieldType::GameObject) {
const auto reference = std::get<::XCEngine::Scripting::GameObjectReference>(value);
if (reference.gameObjectUUID == 0) {
return "None";
}
return "GameObject (" + std::to_string(reference.gameObjectUUID) + ")";
}
return ::XCEngine::Scripting::SerializeScriptFieldValue(type, value);
}
std::unordered_map<uint64_t, std::unordered_map<std::string, StringFieldEditState>> m_stringFieldStates;
};
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,125 @@
#pragma once
#include "Scripting/EditorScriptRuntimeStatus.h"
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Scripting/IScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptField.h>
#include <string>
#include <vector>
namespace XCEngine {
namespace Editor {
namespace ScriptComponentEditorUtils {
inline std::string BuildScriptClassDisplayName(const ::XCEngine::Scripting::ScriptClassDescriptor& descriptor) {
const std::string fullName = descriptor.GetFullName();
if (descriptor.assemblyName.empty() || descriptor.assemblyName == "GameScripts") {
return fullName;
}
return fullName + " (" + descriptor.assemblyName + ")";
}
inline std::string BuildScriptClassDisplayName(const ::XCEngine::Scripting::ScriptComponent& component) {
if (!component.HasScriptClass()) {
return "None";
}
return BuildScriptClassDisplayName(::XCEngine::Scripting::ScriptClassDescriptor{
component.GetAssemblyName(),
component.GetNamespaceName(),
component.GetClassName()
});
}
inline ::XCEngine::Components::GameObject::ID FindGameObjectIdByUuidRecursive(
::XCEngine::Components::GameObject* gameObject,
uint64_t uuid) {
if (!gameObject || uuid == 0) {
return ::XCEngine::Components::GameObject::INVALID_ID;
}
if (gameObject->GetUUID() == uuid) {
return gameObject->GetID();
}
for (::XCEngine::Components::GameObject* child : gameObject->GetChildren()) {
const ::XCEngine::Components::GameObject::ID childId =
FindGameObjectIdByUuidRecursive(child, uuid);
if (childId != ::XCEngine::Components::GameObject::INVALID_ID) {
return childId;
}
}
return ::XCEngine::Components::GameObject::INVALID_ID;
}
inline ::XCEngine::Components::GameObject::ID FindGameObjectIdByUuid(
const std::vector<::XCEngine::Components::GameObject*>& roots,
uint64_t uuid) {
for (::XCEngine::Components::GameObject* root : roots) {
const ::XCEngine::Components::GameObject::ID gameObjectId =
FindGameObjectIdByUuidRecursive(root, uuid);
if (gameObjectId != ::XCEngine::Components::GameObject::INVALID_ID) {
return gameObjectId;
}
}
return ::XCEngine::Components::GameObject::INVALID_ID;
}
inline bool CanEditScriptField(
::XCEngine::Scripting::ScriptFieldClassStatus classStatus,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field) {
return classStatus != ::XCEngine::Scripting::ScriptFieldClassStatus::Available || field.declaredInClass;
}
inline bool CanClearScriptFieldOverride(const ::XCEngine::Scripting::ScriptFieldSnapshot& field) {
return field.hasStoredValue || field.valueSource == ::XCEngine::Scripting::ScriptFieldValueSource::ManagedValue;
}
inline std::string BuildScriptFieldIssueText(const ::XCEngine::Scripting::ScriptFieldSnapshot& field) {
switch (field.issue) {
case ::XCEngine::Scripting::ScriptFieldIssue::StoredOnly:
return "Stored override is not declared by the selected script.";
case ::XCEngine::Scripting::ScriptFieldIssue::TypeMismatch:
return "Stored override type is " +
::XCEngine::Scripting::ScriptFieldTypeToString(field.storedType) +
", but the script field expects " +
::XCEngine::Scripting::ScriptFieldTypeToString(field.metadata.type) + ".";
case ::XCEngine::Scripting::ScriptFieldIssue::None:
default:
return std::string();
}
}
inline std::string BuildScriptRuntimeUnavailableHint(const EditorScriptRuntimeStatus& status) {
if (!status.statusMessage.empty()) {
return status.statusMessage;
}
if (!status.backendEnabled) {
return "This editor build does not include Mono scripting support.";
}
if (!status.assemblyDirectory.empty()) {
return "No script assemblies are currently loaded from " + status.assemblyDirectory + ".";
}
return "No script assemblies are currently loaded.";
}
inline bool CanReloadScriptRuntime(const EditorScriptRuntimeStatus& status) {
return status.backendEnabled && !status.assemblyDirectory.empty();
}
inline bool CanRebuildScriptAssemblies(const EditorScriptRuntimeStatus& status) {
return status.backendEnabled && !status.assemblyDirectory.empty();
}
} // namespace ScriptComponentEditorUtils
} // namespace Editor
} // namespace XCEngine

View File

@@ -2,6 +2,11 @@
#include "EditorRuntimeMode.h"
#include <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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,372 @@
#include "Scripting/EditorScriptAssemblyBuilder.h"
#include "Platform/Win32Utf8.h"
#include "Scripting/EditorScriptAssemblyBuilderUtils.h"
#include <windows.h>
#include <filesystem>
#include <string>
#include <vector>
#ifndef XCENGINE_EDITOR_REPO_ROOT
#define XCENGINE_EDITOR_REPO_ROOT ""
#endif
#ifndef XCENGINE_EDITOR_MONO_ROOT_DIR
#define XCENGINE_EDITOR_MONO_ROOT_DIR ""
#endif
namespace XCEngine {
namespace Editor {
namespace Scripting {
namespace {
std::filesystem::path GetFallbackRepositoryRoot() {
std::filesystem::path repoRoot = std::filesystem::path(Platform::Utf8ToWide(Platform::GetExecutableDirectoryUtf8()));
for (int i = 0; i < 3; ++i) {
if (repoRoot.has_parent_path()) {
repoRoot = repoRoot.parent_path();
}
}
return repoRoot.lexically_normal();
}
std::filesystem::path GetRepositoryRoot() {
const std::string configuredRoot = XCENGINE_EDITOR_REPO_ROOT;
if (!configuredRoot.empty()) {
return std::filesystem::path(Platform::Utf8ToWide(configuredRoot)).lexically_normal();
}
return GetFallbackRepositoryRoot();
}
std::filesystem::path FindBundledMonoRootDirectory(const std::filesystem::path& repositoryRoot) {
std::error_code ec;
if (repositoryRoot.empty() || !std::filesystem::exists(repositoryRoot, ec)) {
return {};
}
for (std::filesystem::directory_iterator it(repositoryRoot, ec), end; it != end && !ec; it.increment(ec)) {
if (ec || !it->is_directory(ec)) {
continue;
}
const std::filesystem::path candidate =
it->path() / "Fermion" / "Fermion" / "external" / "mono";
if (std::filesystem::exists(candidate / "binary" / "mscorlib.dll", ec)) {
return candidate.lexically_normal();
}
}
return {};
}
std::filesystem::path GetMonoRootDirectory() {
const std::filesystem::path repositoryRoot = GetRepositoryRoot();
const std::filesystem::path bundledMonoRoot = FindBundledMonoRootDirectory(repositoryRoot);
if (!bundledMonoRoot.empty()) {
return bundledMonoRoot;
}
const std::string configuredRoot = XCENGINE_EDITOR_MONO_ROOT_DIR;
if (!configuredRoot.empty()) {
std::error_code ec;
const std::filesystem::path configuredPath =
std::filesystem::path(Platform::Utf8ToWide(configuredRoot)).lexically_normal();
if (std::filesystem::exists(configuredPath / "binary" / "mscorlib.dll", ec)) {
return configuredPath;
}
}
return (repositoryRoot / "managed" / "mono").lexically_normal();
}
std::wstring QuotePath(const std::filesystem::path& path) {
return L"\"" + path.wstring() + L"\"";
}
bool FindExecutableOnPath(const wchar_t* executableName, std::filesystem::path& outPath) {
DWORD requiredLength = SearchPathW(nullptr, executableName, nullptr, 0, nullptr, nullptr);
if (requiredLength == 0) {
return false;
}
std::wstring buffer(requiredLength, L'\0');
const DWORD resolvedLength = SearchPathW(
nullptr,
executableName,
nullptr,
static_cast<DWORD>(buffer.size()),
buffer.data(),
nullptr);
if (resolvedLength == 0 || resolvedLength >= buffer.size()) {
return false;
}
buffer.resize(resolvedLength);
outPath = std::filesystem::path(buffer).lexically_normal();
return true;
}
bool RunProcessAndCapture(
const std::filesystem::path& executablePath,
const std::wstring& arguments,
const std::filesystem::path& workingDirectory,
DWORD& outExitCode,
std::string& outOutput) {
SECURITY_ATTRIBUTES securityAttributes = {};
securityAttributes.nLength = sizeof(securityAttributes);
securityAttributes.bInheritHandle = TRUE;
HANDLE readPipe = nullptr;
HANDLE writePipe = nullptr;
if (!CreatePipe(&readPipe, &writePipe, &securityAttributes, 0)) {
return false;
}
SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOW startupInfo = {};
startupInfo.cb = sizeof(startupInfo);
startupInfo.dwFlags = STARTF_USESTDHANDLES;
startupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
startupInfo.hStdOutput = writePipe;
startupInfo.hStdError = writePipe;
PROCESS_INFORMATION processInfo = {};
std::wstring commandLine = QuotePath(executablePath) + L" " + arguments;
std::vector<wchar_t> commandLineBuffer(commandLine.begin(), commandLine.end());
commandLineBuffer.push_back(L'\0');
const wchar_t* currentDirectory = workingDirectory.empty() ? nullptr : workingDirectory.c_str();
const BOOL created = CreateProcessW(
nullptr,
commandLineBuffer.data(),
nullptr,
nullptr,
TRUE,
CREATE_NO_WINDOW,
nullptr,
currentDirectory,
&startupInfo,
&processInfo);
CloseHandle(writePipe);
writePipe = nullptr;
if (!created) {
CloseHandle(readPipe);
return false;
}
char buffer[4096] = {};
DWORD bytesRead = 0;
while (ReadFile(readPipe, buffer, sizeof(buffer), &bytesRead, nullptr) && bytesRead > 0) {
outOutput.append(buffer, bytesRead);
}
WaitForSingleObject(processInfo.hProcess, INFINITE);
GetExitCodeProcess(processInfo.hProcess, &outExitCode);
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
CloseHandle(readPipe);
return true;
}
std::wstring BuildCompilerArguments(
const std::filesystem::path& outputPath,
const std::vector<std::filesystem::path>& referencePaths,
const std::vector<std::filesystem::path>& sourcePaths) {
std::wstring arguments = L"/nologo /target:library /langversion:latest /nostdlib+ ";
arguments += L"/out:" + QuotePath(outputPath);
for (const std::filesystem::path& referencePath : referencePaths) {
arguments += L" /reference:" + QuotePath(referencePath);
}
for (const std::filesystem::path& sourcePath : sourcePaths) {
arguments += L" " + QuotePath(sourcePath);
}
return arguments;
}
EditorScriptAssemblyBuildResult BuildFailure(const std::string& message) {
return EditorScriptAssemblyBuildResult{false, message};
}
bool RunCSharpCompiler(
const std::filesystem::path& dotnetExecutable,
const std::filesystem::path& cscDllPath,
const std::filesystem::path& workingDirectory,
const std::filesystem::path& outputPath,
const std::vector<std::filesystem::path>& referencePaths,
const std::vector<std::filesystem::path>& sourcePaths,
std::string& outError) {
std::wstring arguments = QuotePath(cscDllPath);
arguments += L" ";
arguments += BuildCompilerArguments(outputPath, referencePaths, sourcePaths);
DWORD exitCode = 0;
std::string processOutput;
if (!RunProcessAndCapture(dotnetExecutable, arguments, workingDirectory, exitCode, processOutput)) {
outError = "Failed to launch dotnet to compile managed script assemblies.";
return false;
}
if (exitCode != 0) {
outError = processOutput.empty()
? "The C# compiler failed to build managed script assemblies."
: processOutput;
return false;
}
return true;
}
} // namespace
EditorScriptAssemblyBuildResult EditorScriptAssemblyBuilder::RebuildProjectAssemblies(const std::string& projectPath) {
namespace fs = std::filesystem;
try {
if (projectPath.empty()) {
return BuildFailure("Cannot rebuild script assemblies without a loaded project.");
}
const fs::path projectRoot = fs::path(Platform::Utf8ToWide(projectPath)).lexically_normal();
const fs::path repositoryRoot = GetRepositoryRoot();
const fs::path monoRoot = GetMonoRootDirectory();
const fs::path managedRoot = repositoryRoot / "managed";
const fs::path scriptCoreSourceRoot = managedRoot / "XCEngine.ScriptCore";
const fs::path outputDirectory = projectRoot / "Library" / "ScriptAssemblies";
const fs::path generatedDirectory = outputDirectory / "Generated";
const fs::path scriptCoreOutputPath = outputDirectory / "XCEngine.ScriptCore.dll";
const fs::path gameScriptsOutputPath = outputDirectory / "GameScripts.dll";
const fs::path corlibOutputPath = outputDirectory / "mscorlib.dll";
const fs::path monoCorlibSourcePath = monoRoot / "binary" / "mscorlib.dll";
const fs::path frameworkReferenceDirectory =
L"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.7.2";
std::error_code ec;
fs::create_directories(outputDirectory, ec);
if (ec) {
return BuildFailure("Failed to create the project script assembly directory: " +
ScriptBuilderPathToUtf8(outputDirectory));
}
fs::path dotnetExecutable;
if (!FindExecutableOnPath(L"dotnet.exe", dotnetExecutable)) {
return BuildFailure("dotnet.exe was not found on PATH.");
}
std::string sdkListOutput;
DWORD sdkListExitCode = 0;
if (!RunProcessAndCapture(dotnetExecutable, L"--list-sdks", projectRoot, sdkListExitCode, sdkListOutput) ||
sdkListExitCode != 0) {
return BuildFailure("Failed to query installed .NET SDKs with dotnet --list-sdks.");
}
const std::string sdkVersion = ParseLatestDotnetSdkVersion(sdkListOutput);
if (sdkVersion.empty()) {
return BuildFailure("Failed to resolve a usable .NET SDK version from dotnet --list-sdks.");
}
const fs::path cscDllPath =
fs::path(L"C:\\Program Files\\dotnet\\sdk") /
fs::path(Platform::Utf8ToWide(sdkVersion)) /
"Roslyn" /
"bincore" /
"csc.dll";
if (!fs::exists(cscDllPath, ec)) {
return BuildFailure("Roslyn csc.dll was not found: " + ScriptBuilderPathToUtf8(cscDllPath));
}
const std::vector<fs::path> frameworkReferences = {
frameworkReferenceDirectory / "mscorlib.dll",
frameworkReferenceDirectory / "System.dll",
frameworkReferenceDirectory / "System.Core.dll"
};
for (const fs::path& referencePath : frameworkReferences) {
if (!fs::exists(referencePath, ec)) {
return BuildFailure("Required .NET Framework reference is missing: " +
ScriptBuilderPathToUtf8(referencePath));
}
}
if (!fs::exists(monoCorlibSourcePath, ec)) {
return BuildFailure("Mono corlib was not found: " + ScriptBuilderPathToUtf8(monoCorlibSourcePath));
}
std::vector<fs::path> scriptCoreSources = CollectCSharpSourceFiles(scriptCoreSourceRoot);
if (scriptCoreSources.empty()) {
return BuildFailure("No ScriptCore C# source files were found under: " +
ScriptBuilderPathToUtf8(scriptCoreSourceRoot));
}
std::vector<fs::path> projectScriptSources = CollectCSharpSourceFiles(projectRoot / "Assets");
std::string placeholderError;
if (!EnsurePlaceholderProjectScriptSource(
projectScriptSources,
generatedDirectory / "EmptyProjectGameScripts.cs",
placeholderError)) {
return BuildFailure(placeholderError);
}
std::string compileError;
if (!RunCSharpCompiler(
dotnetExecutable,
cscDllPath,
projectRoot,
scriptCoreOutputPath,
frameworkReferences,
scriptCoreSources,
compileError)) {
return BuildFailure("Failed to build XCEngine.ScriptCore.dll: " + compileError);
}
// Mono can keep the project-local corlib mapped for the lifetime of the process.
// Once it exists in the output folder, reuse it across incremental rebuilds.
ec.clear();
const bool hasProjectCorlib = fs::exists(corlibOutputPath, ec);
if (ec) {
return BuildFailure("Failed to inspect the project script assembly corlib path.");
}
if (!hasProjectCorlib) {
fs::copy_file(monoCorlibSourcePath, corlibOutputPath, fs::copy_options::overwrite_existing, ec);
if (ec) {
return BuildFailure("Failed to copy mscorlib.dll into the project script assembly directory.");
}
}
std::vector<fs::path> gameScriptReferences = frameworkReferences;
gameScriptReferences.push_back(scriptCoreOutputPath);
if (!RunCSharpCompiler(
dotnetExecutable,
cscDllPath,
projectRoot,
gameScriptsOutputPath,
gameScriptReferences,
projectScriptSources,
compileError)) {
return BuildFailure("Failed to build GameScripts.dll: " + compileError);
}
return EditorScriptAssemblyBuildResult{
true,
"Rebuilt script assemblies in " + ScriptBuilderPathToUtf8(outputDirectory)
};
} catch (const std::exception& exception) {
return BuildFailure("Script assembly rebuild threw an exception: " + std::string(exception.what()));
} catch (...) {
return BuildFailure("Script assembly rebuild threw an unknown exception.");
}
}
} // namespace Scripting
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,21 @@
#pragma once
#include <string>
namespace XCEngine {
namespace Editor {
namespace Scripting {
struct EditorScriptAssemblyBuildResult {
bool succeeded = false;
std::string message;
};
class EditorScriptAssemblyBuilder {
public:
static EditorScriptAssemblyBuildResult RebuildProjectAssemblies(const std::string& projectPath);
};
} // namespace Scripting
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,106 @@
#pragma once
#include "Platform/Win32Utf8.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
namespace XCEngine {
namespace Editor {
namespace Scripting {
inline std::string ScriptBuilderPathToUtf8(const std::filesystem::path& path) {
return Platform::WideToUtf8(path.wstring());
}
inline std::vector<std::filesystem::path> CollectCSharpSourceFiles(const std::filesystem::path& root) {
std::vector<std::filesystem::path> sourceFiles;
std::error_code ec;
if (root.empty() || !std::filesystem::exists(root, ec)) {
return sourceFiles;
}
for (std::filesystem::recursive_directory_iterator it(root, ec), end; it != end && !ec; it.increment(ec)) {
if (ec || !it->is_regular_file(ec)) {
continue;
}
const std::filesystem::path path = it->path();
if (path.extension() == ".cs") {
sourceFiles.push_back(path.lexically_normal());
}
}
std::sort(sourceFiles.begin(), sourceFiles.end());
return sourceFiles;
}
inline std::string ParseLatestDotnetSdkVersion(const std::string& sdkListOutput) {
std::string latestVersion;
size_t lineStart = 0;
while (lineStart < sdkListOutput.size()) {
const size_t lineEnd = sdkListOutput.find_first_of("\r\n", lineStart);
const std::string line = sdkListOutput.substr(
lineStart,
lineEnd == std::string::npos ? std::string::npos : lineEnd - lineStart);
const size_t delimiter = line.find(" [");
if (delimiter != std::string::npos) {
latestVersion = line.substr(0, delimiter);
}
if (lineEnd == std::string::npos) {
break;
}
lineStart = lineEnd + 1;
if (lineStart < sdkListOutput.size() &&
sdkListOutput[lineEnd] == '\r' &&
sdkListOutput[lineStart] == '\n') {
++lineStart;
}
}
return latestVersion;
}
inline bool EnsurePlaceholderProjectScriptSource(
std::vector<std::filesystem::path>& ioSourceFiles,
const std::filesystem::path& placeholderPath,
std::string& outError) {
if (!ioSourceFiles.empty()) {
return true;
}
std::error_code ec;
std::filesystem::create_directories(placeholderPath.parent_path(), ec);
if (ec) {
outError = "Failed to create the placeholder script directory: " +
ScriptBuilderPathToUtf8(placeholderPath.parent_path());
return false;
}
std::ofstream output(placeholderPath, std::ios::out | std::ios::trunc);
if (!output.is_open()) {
outError = "Failed to create the placeholder project script source: " +
ScriptBuilderPathToUtf8(placeholderPath);
return false;
}
output << "namespace XCEngine.Generated { public static class EmptyProjectGameScriptsMarker {} }\n";
output.close();
if (!output.good()) {
outError = "Failed to write the placeholder project script source: " +
ScriptBuilderPathToUtf8(placeholderPath);
return false;
}
ioSourceFiles.push_back(placeholderPath.lexically_normal());
return true;
}
} // namespace Scripting
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,17 @@
#pragma once
#include <string>
namespace XCEngine {
namespace Editor {
struct EditorScriptRuntimeStatus {
bool backendEnabled = false;
bool assembliesFound = false;
bool runtimeLoaded = false;
std::string assemblyDirectory;
std::string statusMessage;
};
} // namespace Editor
} // namespace XCEngine

View File

@@ -47,6 +47,20 @@ struct AssetTileOptions {
bool drawLabel = true;
};
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();

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,89 @@
#pragma once
#include "Viewport/SceneViewportEditorOverlayData.h"
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderPass.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
namespace XCEngine {
namespace RHI {
class RHIBuffer;
class RHIDescriptorPool;
class RHIDescriptorSet;
class RHIDevice;
class RHIPipelineLayout;
class RHIPipelineState;
class RHIResourceView;
class RHISampler;
class RHITexture;
enum class RHIType : uint8_t;
} // namespace RHI
namespace Editor {
class SceneViewportEditorOverlayPassRenderer {
public:
~SceneViewportEditorOverlayPassRenderer() = default;
void Shutdown();
bool Render(
const Rendering::RenderContext& renderContext,
const Rendering::RenderSurface& surface,
const SceneViewportOverlayFrameData& frameData);
private:
struct OverlaySpriteTextureResources {
RHI::RHITexture* texture = nullptr;
RHI::RHIResourceView* shaderView = nullptr;
RHI::RHIDescriptorSet* textureSet = nullptr;
};
bool EnsureInitialized(const Rendering::RenderContext& renderContext);
bool CreateResources(const Rendering::RenderContext& renderContext);
bool EnsureLineBufferCapacity(size_t requiredVertexCount);
bool EnsureScreenTriangleBufferCapacity(size_t requiredVertexCount);
bool EnsureSpriteBufferCapacity(size_t requiredVertexCount);
bool EnsureIconTexturesLoaded();
void DestroyResources();
RHI::RHIDevice* m_device = nullptr;
RHI::RHIType m_backendType = RHI::RHIType::D3D12;
RHI::RHIPipelineLayout* m_linePipelineLayout = nullptr;
RHI::RHIPipelineLayout* m_spritePipelineLayout = nullptr;
RHI::RHIPipelineState* m_depthTestedLinePipelineState = nullptr;
RHI::RHIPipelineState* m_alwaysOnTopLinePipelineState = nullptr;
RHI::RHIPipelineState* m_depthTestedScreenTrianglePipelineState = nullptr;
RHI::RHIPipelineState* m_alwaysOnTopScreenTrianglePipelineState = nullptr;
RHI::RHIPipelineState* m_depthTestedSpritePipelineState = nullptr;
RHI::RHIPipelineState* m_alwaysOnTopSpritePipelineState = nullptr;
RHI::RHIDescriptorPool* m_constantPool = nullptr;
RHI::RHIDescriptorPool* m_texturePool = nullptr;
RHI::RHIDescriptorPool* m_samplerPool = nullptr;
RHI::RHIDescriptorSet* m_constantSet = nullptr;
RHI::RHIDescriptorSet* m_samplerSet = nullptr;
RHI::RHISampler* m_sampler = nullptr;
RHI::RHIBuffer* m_lineVertexBuffer = nullptr;
RHI::RHIResourceView* m_lineVertexBufferView = nullptr;
RHI::RHIBuffer* m_screenTriangleVertexBuffer = nullptr;
RHI::RHIResourceView* m_screenTriangleVertexBufferView = nullptr;
RHI::RHIBuffer* m_spriteVertexBuffer = nullptr;
RHI::RHIResourceView* m_spriteVertexBufferView = nullptr;
uint64_t m_lineVertexBufferCapacity = 0;
uint64_t m_screenTriangleVertexBufferCapacity = 0;
uint64_t m_spriteVertexBufferCapacity = 0;
std::array<OverlaySpriteTextureResources, 2> m_overlaySpriteTextures = {};
};
std::unique_ptr<Rendering::RenderPass> CreateSceneViewportEditorOverlayPass(
SceneViewportEditorOverlayPassRenderer& renderer,
const SceneViewportOverlayFrameData& frameData);
} // namespace Editor
} // namespace XCEngine

View File

@@ -128,13 +128,21 @@ public:
if (std::abs(input.lookDeltaX) > Math::EPSILON ||
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 {

View File

@@ -0,0 +1,144 @@
#pragma once
#include "IViewportHostService.h"
#include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <array>
#include <limits>
#include <vector>
namespace XCEngine {
namespace Editor {
enum class SceneViewportOverlayDepthMode : uint8_t {
DepthTested = 0,
AlwaysOnTop
};
enum class SceneViewportOverlaySpriteTextureKind : uint8_t {
Camera = 0,
Light = 1
};
enum class SceneViewportOverlayHandleKind : uint8_t {
None = 0,
SceneIcon,
MoveAxis,
MovePlane,
RotateAxis,
ScaleAxis,
ScaleUniform
};
enum class SceneViewportOverlayHandleShape : uint8_t {
None = 0,
WorldRect,
ScreenSegment,
ScreenRect,
ScreenQuad
};
struct SceneViewportOverlayLinePrimitive {
Math::Vector3 startWorld = Math::Vector3::Zero();
Math::Vector3 endWorld = Math::Vector3::Zero();
Math::Color color = Math::Color::White();
float thicknessPixels = 1.0f;
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop;
};
struct SceneViewportOverlaySpritePrimitive {
Math::Vector3 worldPosition = Math::Vector3::Zero();
Math::Vector2 sizePixels = Math::Vector2::Zero();
Math::Color tintColor = Math::Color::White();
float sortDepth = 0.0f;
uint64_t entityId = 0;
SceneViewportOverlaySpriteTextureKind textureKind = SceneViewportOverlaySpriteTextureKind::Camera;
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop;
};
struct SceneViewportOverlayScreenTriangleVertex {
Math::Vector2 screenPosition = Math::Vector2::Zero();
Math::Color color = Math::Color::White();
};
struct SceneViewportOverlayScreenTrianglePrimitive {
std::array<SceneViewportOverlayScreenTriangleVertex, 3> vertices = {};
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop;
};
struct SceneViewportOverlayHandleRecord {
SceneViewportOverlayHandleKind kind = SceneViewportOverlayHandleKind::None;
uint64_t handleId = 0;
uint64_t entityId = 0;
SceneViewportOverlayHandleShape shape = SceneViewportOverlayHandleShape::None;
int priority = 0;
Math::Vector3 worldPosition = Math::Vector3::Zero();
Math::Vector2 sizePixels = Math::Vector2::Zero();
float sortDepth = 0.0f;
Math::Vector2 screenStart = Math::Vector2::Zero();
Math::Vector2 screenEnd = Math::Vector2::Zero();
Math::Vector2 screenCenter = Math::Vector2::Zero();
Math::Vector2 screenHalfSize = Math::Vector2::Zero();
std::array<Math::Vector2, 4> screenQuad = {};
float hitThicknessPixels = 0.0f;
};
struct SceneViewportOverlayHandleHitResult {
SceneViewportOverlayHandleKind kind = SceneViewportOverlayHandleKind::None;
uint64_t handleId = 0;
uint64_t entityId = 0;
int priority = (std::numeric_limits<int>::min)();
float distanceSq = (std::numeric_limits<float>::max)();
float depth = (std::numeric_limits<float>::max)();
bool HasHit() const {
return kind != SceneViewportOverlayHandleKind::None;
}
};
struct SceneViewportOverlayFrameData {
SceneViewportOverlayData overlay = {};
std::vector<SceneViewportOverlayLinePrimitive> worldLines = {};
std::vector<SceneViewportOverlaySpritePrimitive> worldSprites = {};
std::vector<SceneViewportOverlayScreenTrianglePrimitive> screenTriangles = {};
std::vector<SceneViewportOverlayHandleRecord> handleRecords = {};
bool HasOverlayPrimitives() const {
return overlay.valid && (!worldLines.empty() || !worldSprites.empty() || !screenTriangles.empty());
}
bool HasWorldOverlay() const {
return HasOverlayPrimitives();
}
};
inline void AppendSceneViewportOverlayFrameData(
SceneViewportOverlayFrameData& target,
const SceneViewportOverlayFrameData& source) {
if (!target.overlay.valid && source.overlay.valid) {
target.overlay = source.overlay;
}
target.worldLines.insert(
target.worldLines.end(),
source.worldLines.begin(),
source.worldLines.end());
target.worldSprites.insert(
target.worldSprites.end(),
source.worldSprites.begin(),
source.worldSprites.end());
target.screenTriangles.insert(
target.screenTriangles.end(),
source.screenTriangles.begin(),
source.screenTriangles.end());
target.handleRecords.insert(
target.handleRecords.end(),
source.handleRecords.begin(),
source.handleRecords.end());
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -14,7 +14,7 @@ namespace {
constexpr float kMoveGizmoHandleLengthPixels = 100.0f;
constexpr float 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

View File

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

View File

@@ -0,0 +1,431 @@
#include "SceneViewportOverlayBuilder.h"
#include "Core/IEditorContext.h"
#include "Core/ISceneManager.h"
#include "SceneViewportOverlayHandleBuilder.h"
#include "SceneViewportMath.h"
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/LightComponent.h>
#include <XCEngine/Components/TransformComponent.h>
#include <XCEngine/Core/Math/Rect.h>
#include <XCEngine/Scene/Scene.h>
#include <algorithm>
#include <array>
#include <cmath>
namespace XCEngine {
namespace Editor {
namespace {
bool CanBuildOverlayForGameObject(const Components::GameObject* gameObject) {
return gameObject != nullptr &&
gameObject->GetTransform() != nullptr &&
gameObject->IsActiveInHierarchy();
}
float ResolveCameraAspect(
const Components::CameraComponent& camera,
uint32_t viewportWidth,
uint32_t viewportHeight) {
const Math::Rect viewportRect = camera.GetViewportRect();
const float resolvedWidth = static_cast<float>(viewportWidth) *
(viewportRect.width > Math::EPSILON ? viewportRect.width : 1.0f);
const float resolvedHeight = static_cast<float>(viewportHeight) *
(viewportRect.height > Math::EPSILON ? viewportRect.height : 1.0f);
return resolvedHeight > Math::EPSILON
? resolvedWidth / resolvedHeight
: 1.0f;
}
float ComputeWorldUnitsPerPixel(
const SceneViewportOverlayData& overlay,
const Math::Vector3& worldPoint,
uint32_t viewportHeight) {
if (!overlay.valid || viewportHeight <= 1u) {
return 0.0f;
}
const Math::Vector3 cameraForward = overlay.cameraForward.Normalized();
const float depth = Math::Vector3::Dot(worldPoint - overlay.cameraPosition, cameraForward);
if (depth <= Math::EPSILON) {
return 0.0f;
}
return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) /
static_cast<float>(viewportHeight);
}
void AppendWorldLine(
SceneViewportOverlayFrameData& frameData,
const Math::Vector3& startWorld,
const Math::Vector3& endWorld,
const Math::Color& color,
float thicknessPixels,
SceneViewportOverlayDepthMode depthMode) {
SceneViewportOverlayLinePrimitive& line = frameData.worldLines.emplace_back();
line.startWorld = startWorld;
line.endWorld = endWorld;
line.color = color;
line.thicknessPixels = thicknessPixels;
line.depthMode = depthMode;
}
void AppendWorldSprite(
SceneViewportOverlayFrameData& frameData,
const Math::Vector3& worldPosition,
const Math::Vector2& sizePixels,
const Math::Color& tintColor,
float sortDepth,
uint64_t entityId,
SceneViewportOverlaySpriteTextureKind textureKind,
SceneViewportOverlayDepthMode depthMode) {
if (entityId == 0 || sizePixels.x <= Math::EPSILON || sizePixels.y <= Math::EPSILON) {
return;
}
SceneViewportOverlaySpritePrimitive& sprite = frameData.worldSprites.emplace_back();
sprite.worldPosition = worldPosition;
sprite.sizePixels = sizePixels;
sprite.tintColor = tintColor;
sprite.sortDepth = sortDepth;
sprite.entityId = entityId;
sprite.textureKind = textureKind;
sprite.depthMode = depthMode;
}
void AppendHandleRecord(
SceneViewportOverlayFrameData& frameData,
SceneViewportOverlayHandleKind kind,
uint64_t handleId,
uint64_t entityId,
const Math::Vector3& worldPosition,
const Math::Vector2& sizePixels,
float sortDepth) {
if (kind == SceneViewportOverlayHandleKind::None ||
handleId == 0 ||
entityId == 0 ||
sizePixels.x <= Math::EPSILON ||
sizePixels.y <= Math::EPSILON) {
return;
}
SceneViewportOverlayHandleRecord& handleRecord = frameData.handleRecords.emplace_back();
handleRecord.kind = kind;
handleRecord.handleId = handleId;
handleRecord.entityId = entityId;
handleRecord.shape = SceneViewportOverlayHandleShape::WorldRect;
handleRecord.priority = Detail::kSceneViewportHandlePrioritySceneIcon;
handleRecord.worldPosition = worldPosition;
handleRecord.sizePixels = sizePixels;
handleRecord.sortDepth = sortDepth;
}
void AppendSceneIconOverlay(
SceneViewportOverlayFrameData& frameData,
const SceneViewportOverlayData& overlay,
uint32_t viewportWidth,
uint32_t viewportHeight,
const Components::GameObject& gameObject,
const Math::Vector2& sizePixels,
SceneViewportOverlaySpriteTextureKind textureKind) {
const Components::TransformComponent* transform = gameObject.GetTransform();
if (transform == nullptr) {
return;
}
const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint(
overlay,
static_cast<float>(viewportWidth),
static_cast<float>(viewportHeight),
transform->GetPosition());
if (!projectedPoint.visible) {
return;
}
AppendWorldSprite(
frameData,
transform->GetPosition(),
sizePixels,
Math::Color::White(),
projectedPoint.ndcDepth,
gameObject.GetID(),
textureKind,
SceneViewportOverlayDepthMode::AlwaysOnTop);
AppendHandleRecord(
frameData,
SceneViewportOverlayHandleKind::SceneIcon,
gameObject.GetID(),
gameObject.GetID(),
transform->GetPosition(),
sizePixels,
projectedPoint.ndcDepth);
}
void AppendCameraFrustumOverlay(
SceneViewportOverlayFrameData& frameData,
const Components::CameraComponent& camera,
const Components::GameObject& gameObject,
uint32_t viewportWidth,
uint32_t viewportHeight) {
const Components::TransformComponent* transform = gameObject.GetTransform();
if (transform == nullptr) {
return;
}
const Math::Vector3 position = transform->GetPosition();
const Math::Vector3 forward = transform->GetForward().Normalized();
const Math::Vector3 right = transform->GetRight().Normalized();
const Math::Vector3 up = transform->GetUp().Normalized();
if (forward.SqrMagnitude() <= Math::EPSILON ||
right.SqrMagnitude() <= Math::EPSILON ||
up.SqrMagnitude() <= Math::EPSILON) {
return;
}
const float nearClip = (std::max)(camera.GetNearClipPlane(), 0.01f);
const float farClip = (std::max)(camera.GetFarClipPlane(), nearClip + 0.01f);
const float aspect = ResolveCameraAspect(camera, viewportWidth, viewportHeight);
float nearHalfHeight = 0.0f;
float nearHalfWidth = 0.0f;
float farHalfHeight = 0.0f;
float farHalfWidth = 0.0f;
if (camera.GetProjectionType() == Components::CameraProjectionType::Perspective) {
const float halfFovRadians =
std::clamp(camera.GetFieldOfView(), 1.0f, 179.0f) * Math::DEG_TO_RAD * 0.5f;
nearHalfHeight = std::tan(halfFovRadians) * nearClip;
nearHalfWidth = nearHalfHeight * aspect;
farHalfHeight = std::tan(halfFovRadians) * farClip;
farHalfWidth = farHalfHeight * aspect;
} else {
const float halfHeight = (std::max)(camera.GetOrthographicSize(), 0.01f);
const float halfWidth = halfHeight * aspect;
nearHalfHeight = halfHeight;
nearHalfWidth = halfWidth;
farHalfHeight = halfHeight;
farHalfWidth = halfWidth;
}
const Math::Vector3 nearCenter = position + forward * nearClip;
const Math::Vector3 farCenter = position + forward * farClip;
const std::array<Math::Vector3, 8> corners = {{
nearCenter + up * nearHalfHeight - right * nearHalfWidth,
nearCenter + up * nearHalfHeight + right * nearHalfWidth,
nearCenter - up * nearHalfHeight + right * nearHalfWidth,
nearCenter - up * nearHalfHeight - right * nearHalfWidth,
farCenter + up * farHalfHeight - right * farHalfWidth,
farCenter + up * farHalfHeight + right * farHalfWidth,
farCenter - up * farHalfHeight + right * farHalfWidth,
farCenter - up * farHalfHeight - right * farHalfWidth
}};
static constexpr std::array<std::pair<size_t, size_t>, 12> kFrustumEdges = {{
{ 0u, 1u }, { 1u, 2u }, { 2u, 3u }, { 3u, 0u },
{ 4u, 5u }, { 5u, 6u }, { 6u, 7u }, { 7u, 4u },
{ 0u, 4u }, { 1u, 5u }, { 2u, 6u }, { 3u, 7u }
}};
constexpr Math::Color kFrustumColor(1.0f, 1.0f, 1.0f, 1.0f);
for (const auto& edge : kFrustumEdges) {
AppendWorldLine(
frameData,
corners[edge.first],
corners[edge.second],
kFrustumColor,
1.6f,
SceneViewportOverlayDepthMode::AlwaysOnTop);
}
}
void AppendDirectionalLightOverlay(
SceneViewportOverlayFrameData& frameData,
const Components::GameObject& gameObject,
const SceneViewportOverlayData& overlay,
uint32_t viewportHeight) {
const Components::TransformComponent* transform = gameObject.GetTransform();
if (transform == nullptr) {
return;
}
const Math::Vector3 position = transform->GetPosition();
const Math::Vector3 lightDirection = (transform->GetForward() * -1.0f).Normalized();
const Math::Vector3 right = transform->GetRight().Normalized();
const Math::Vector3 up = transform->GetUp().Normalized();
if (lightDirection.SqrMagnitude() <= Math::EPSILON ||
right.SqrMagnitude() <= Math::EPSILON ||
up.SqrMagnitude() <= Math::EPSILON) {
return;
}
const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(overlay, position, viewportHeight);
if (worldUnitsPerPixel <= Math::EPSILON) {
return;
}
constexpr Math::Color kDirectionalLightColor(1.0f, 0.92f, 0.24f, 1.0f);
constexpr float kLineThickness = 1.8f;
constexpr size_t kRingSegmentCount = 32;
constexpr std::array<float, 6> kRayAngles = {{
0.0f,
Math::PI / 3.0f,
Math::PI * 2.0f / 3.0f,
Math::PI,
Math::PI * 4.0f / 3.0f,
Math::PI * 5.0f / 3.0f
}};
const float ringRadius = worldUnitsPerPixel * 26.0f;
const float ringOffset = worldUnitsPerPixel * 54.0f;
const float innerRayRadius = ringRadius * 0.52f;
const float rayLength = worldUnitsPerPixel * 96.0f;
const Math::Vector3 ringCenter = position + lightDirection * ringOffset;
for (size_t segmentIndex = 0; segmentIndex < kRingSegmentCount; ++segmentIndex) {
const float angle0 =
static_cast<float>(segmentIndex) / static_cast<float>(kRingSegmentCount) * Math::PI * 2.0f;
const float angle1 =
static_cast<float>(segmentIndex + 1u) / static_cast<float>(kRingSegmentCount) * Math::PI * 2.0f;
const Math::Vector3 p0 =
ringCenter + right * std::cos(angle0) * ringRadius + up * std::sin(angle0) * ringRadius;
const Math::Vector3 p1 =
ringCenter + right * std::cos(angle1) * ringRadius + up * std::sin(angle1) * ringRadius;
AppendWorldLine(
frameData,
p0,
p1,
kDirectionalLightColor,
kLineThickness,
SceneViewportOverlayDepthMode::AlwaysOnTop);
}
AppendWorldLine(
frameData,
position,
ringCenter,
kDirectionalLightColor,
kLineThickness,
SceneViewportOverlayDepthMode::AlwaysOnTop);
AppendWorldLine(
frameData,
ringCenter,
ringCenter + lightDirection * rayLength,
kDirectionalLightColor,
kLineThickness,
SceneViewportOverlayDepthMode::AlwaysOnTop);
for (float angle : kRayAngles) {
const Math::Vector3 rayStart =
ringCenter + right * std::cos(angle) * innerRayRadius + up * std::sin(angle) * innerRayRadius;
AppendWorldLine(
frameData,
rayStart,
rayStart + lightDirection * rayLength,
kDirectionalLightColor,
kLineThickness,
SceneViewportOverlayDepthMode::AlwaysOnTop);
}
}
void AppendSceneObjectIconOverlays(
SceneViewportOverlayFrameData& frameData,
const Components::Scene& scene,
const SceneViewportOverlayData& overlay,
uint32_t viewportWidth,
uint32_t viewportHeight) {
constexpr Math::Vector2 kCameraIconSize(90.0f, 90.0f);
constexpr Math::Vector2 kLightIconSize(100.0f, 100.0f);
for (Components::CameraComponent* camera : scene.FindObjectsOfType<Components::CameraComponent>()) {
if (camera == nullptr || !camera->IsEnabled()) {
continue;
}
Components::GameObject* gameObject = camera->GetGameObject();
if (!CanBuildOverlayForGameObject(gameObject)) {
continue;
}
AppendSceneIconOverlay(
frameData,
overlay,
viewportWidth,
viewportHeight,
*gameObject,
kCameraIconSize,
SceneViewportOverlaySpriteTextureKind::Camera);
}
for (Components::LightComponent* light : scene.FindObjectsOfType<Components::LightComponent>()) {
if (light == nullptr || !light->IsEnabled()) {
continue;
}
Components::GameObject* gameObject = light->GetGameObject();
if (!CanBuildOverlayForGameObject(gameObject)) {
continue;
}
AppendSceneIconOverlay(
frameData,
overlay,
viewportWidth,
viewportHeight,
*gameObject,
kLightIconSize,
SceneViewportOverlaySpriteTextureKind::Light);
}
}
} // namespace
SceneViewportOverlayFrameData SceneViewportOverlayBuilder::Build(
IEditorContext& context,
const SceneViewportOverlayData& overlay,
uint32_t viewportWidth,
uint32_t viewportHeight,
const std::vector<uint64_t>& selectedObjectIds) {
SceneViewportOverlayFrameData frameData = {};
frameData.overlay = overlay;
if (!overlay.valid || viewportWidth == 0u || viewportHeight == 0u) {
return frameData;
}
const Components::Scene* scene = context.GetSceneManager().GetScene();
if (scene == nullptr) {
return frameData;
}
AppendSceneObjectIconOverlays(frameData, *scene, overlay, viewportWidth, viewportHeight);
for (uint64_t entityId : selectedObjectIds) {
if (entityId == 0) {
continue;
}
Components::GameObject* gameObject = context.GetSceneManager().GetEntity(entityId);
if (!CanBuildOverlayForGameObject(gameObject)) {
continue;
}
if (Components::CameraComponent* camera = gameObject->GetComponent<Components::CameraComponent>();
camera != nullptr && camera->IsEnabled()) {
AppendCameraFrustumOverlay(frameData, *camera, *gameObject, viewportWidth, viewportHeight);
}
if (Components::LightComponent* light = gameObject->GetComponent<Components::LightComponent>();
light != nullptr &&
light->IsEnabled() &&
light->GetLightType() == Components::LightType::Directional) {
AppendDirectionalLightOverlay(frameData, *gameObject, overlay, viewportHeight);
}
}
return frameData;
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,25 @@
#pragma once
#include "IViewportHostService.h"
#include "SceneViewportEditorOverlayData.h"
#include <cstdint>
#include <vector>
namespace XCEngine {
namespace Editor {
class IEditorContext;
class SceneViewportOverlayBuilder {
public:
static SceneViewportOverlayFrameData Build(
IEditorContext& context,
const SceneViewportOverlayData& overlay,
uint32_t viewportWidth,
uint32_t viewportHeight,
const std::vector<uint64_t>& selectedObjectIds);
};
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,543 @@
#pragma once
#include "SceneViewportEditorOverlayData.h"
#include "SceneViewportMoveGizmo.h"
#include "SceneViewportRotateGizmo.h"
#include "SceneViewportScaleGizmo.h"
#include <algorithm>
#include <cmath>
namespace XCEngine {
namespace Editor {
struct SceneViewportTransformGizmoHandleBuildInputs {
const SceneViewportMoveGizmoDrawData* moveGizmo = nullptr;
uint64_t moveEntityId = 0;
const SceneViewportRotateGizmoDrawData* rotateGizmo = nullptr;
uint64_t rotateEntityId = 0;
const SceneViewportScaleGizmoDrawData* scaleGizmo = nullptr;
uint64_t scaleEntityId = 0;
};
inline SceneViewportTransformGizmoHandleBuildInputs BuildSceneViewportTransformGizmoHandleBuildInputs(
bool showingMoveGizmo,
const SceneViewportMoveGizmo& moveGizmo,
const SceneViewportMoveGizmoContext& moveGizmoContext,
bool showingRotateGizmo,
const SceneViewportRotateGizmo& rotateGizmo,
const SceneViewportRotateGizmoContext& rotateGizmoContext,
bool showingScaleGizmo,
const SceneViewportScaleGizmo& scaleGizmo,
const SceneViewportScaleGizmoContext& scaleGizmoContext) {
SceneViewportTransformGizmoHandleBuildInputs inputs = {};
if (showingMoveGizmo && moveGizmoContext.selectedObject != nullptr) {
inputs.moveGizmo = &moveGizmo.GetDrawData();
inputs.moveEntityId = moveGizmoContext.selectedObject->GetID();
}
if (showingRotateGizmo && rotateGizmoContext.selectedObject != nullptr) {
inputs.rotateGizmo = &rotateGizmo.GetDrawData();
inputs.rotateEntityId = rotateGizmoContext.selectedObject->GetID();
}
if (showingScaleGizmo && scaleGizmoContext.selectedObject != nullptr) {
inputs.scaleGizmo = &scaleGizmo.GetDrawData();
inputs.scaleEntityId = scaleGizmoContext.selectedObject->GetID();
}
return inputs;
}
namespace Detail {
inline constexpr int kSceneViewportHandlePrioritySceneIcon = 100;
inline constexpr int kSceneViewportHandlePriorityRotateAxis = 311;
inline constexpr int kSceneViewportHandlePriorityMovePlane = 321;
inline constexpr int kSceneViewportHandlePriorityMoveAxis = 322;
inline constexpr int kSceneViewportHandlePriorityScaleAxisLine = 331;
inline constexpr int kSceneViewportHandlePriorityScaleAxisCap = 332;
inline constexpr int kSceneViewportHandlePriorityScaleUniform = 333;
inline constexpr float kSceneViewportMoveAxisHitThicknessPixels = 10.0f;
inline constexpr float kSceneViewportRotateAxisHitThicknessPixels = 9.0f;
inline constexpr float kSceneViewportScaleAxisHitThicknessPixels = 10.0f;
inline constexpr float kSceneViewportScaleCapHitPaddingPixels = 2.0f;
inline constexpr float kSceneViewportMoveArrowLengthPixels = 14.0f;
inline constexpr float kSceneViewportMoveArrowHalfWidthPixels = 7.0f;
inline Math::Color WithAlpha(const Math::Color& color, float alpha) {
return Math::Color(color.r, color.g, color.b, alpha);
}
inline Math::Color LerpColor(const Math::Color& a, const Math::Color& b, float t) {
return Math::Color(
a.r + (b.r - a.r) * t,
a.g + (b.g - a.g) * t,
a.b + (b.b - a.b) * t,
a.a + (b.a - a.a) * t);
}
inline Math::Vector2 NormalizeVector2(
const Math::Vector2& value,
const Math::Vector2& fallback = Math::Vector2(1.0f, 0.0f)) {
const float lengthSq = value.SqrMagnitude();
if (lengthSq <= Math::EPSILON) {
return fallback;
}
return value / std::sqrt(lengthSq);
}
inline void AppendScreenTriangle(
SceneViewportOverlayFrameData& frameData,
const Math::Vector2& a,
const Math::Vector2& b,
const Math::Vector2& c,
const Math::Color& color,
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
SceneViewportOverlayScreenTrianglePrimitive& triangle = frameData.screenTriangles.emplace_back();
triangle.vertices[0].screenPosition = a;
triangle.vertices[0].color = color;
triangle.vertices[1].screenPosition = b;
triangle.vertices[1].color = color;
triangle.vertices[2].screenPosition = c;
triangle.vertices[2].color = color;
triangle.depthMode = depthMode;
}
inline void AppendScreenQuad(
SceneViewportOverlayFrameData& frameData,
const Math::Vector2& a,
const Math::Vector2& b,
const Math::Vector2& c,
const Math::Vector2& d,
const Math::Color& color,
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
AppendScreenTriangle(frameData, a, b, c, color, depthMode);
AppendScreenTriangle(frameData, a, c, d, color, depthMode);
}
inline void AppendScreenRect(
SceneViewportOverlayFrameData& frameData,
const Math::Vector2& center,
const Math::Vector2& halfSize,
const Math::Color& color,
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
AppendScreenQuad(
frameData,
Math::Vector2(center.x - halfSize.x, center.y - halfSize.y),
Math::Vector2(center.x + halfSize.x, center.y - halfSize.y),
Math::Vector2(center.x + halfSize.x, center.y + halfSize.y),
Math::Vector2(center.x - halfSize.x, center.y + halfSize.y),
color,
depthMode);
}
inline void AppendScreenSegmentQuad(
SceneViewportOverlayFrameData& frameData,
const Math::Vector2& start,
const Math::Vector2& end,
float thicknessPixels,
const Math::Color& color,
SceneViewportOverlayDepthMode depthMode = SceneViewportOverlayDepthMode::AlwaysOnTop) {
const Math::Vector2 delta = end - start;
if (delta.SqrMagnitude() <= Math::EPSILON || thicknessPixels <= Math::EPSILON) {
return;
}
const Math::Vector2 direction = NormalizeVector2(delta);
const Math::Vector2 normal(-direction.y, direction.x);
const Math::Vector2 offset = normal * (thicknessPixels * 0.5f);
AppendScreenQuad(
frameData,
start + offset,
start - offset,
end - offset,
end + offset,
color,
depthMode);
}
inline void AppendScreenQuadOutline(
SceneViewportOverlayFrameData& frameData,
const std::array<Math::Vector2, 4>& corners,
float thicknessPixels,
const Math::Color& color) {
for (size_t index = 0; index < corners.size(); ++index) {
AppendScreenSegmentQuad(
frameData,
corners[index],
corners[(index + 1u) % corners.size()],
thicknessPixels,
color);
}
}
inline void AppendScreenRectOutline(
SceneViewportOverlayFrameData& frameData,
const Math::Vector2& center,
const Math::Vector2& halfSize,
float thicknessPixels,
const Math::Color& color) {
const std::array<Math::Vector2, 4> corners = {{
Math::Vector2(center.x - halfSize.x, center.y - halfSize.y),
Math::Vector2(center.x + halfSize.x, center.y - halfSize.y),
Math::Vector2(center.x + halfSize.x, center.y + halfSize.y),
Math::Vector2(center.x - halfSize.x, center.y + halfSize.y)
}};
AppendScreenQuadOutline(frameData, corners, thicknessPixels, color);
}
inline void AppendMoveGizmoHandleRecords(
SceneViewportOverlayFrameData& frameData,
const SceneViewportMoveGizmoDrawData& drawData,
uint64_t entityId) {
if (!drawData.visible || entityId == 0) {
return;
}
for (const SceneViewportMoveGizmoHandleDrawData& handle : drawData.handles) {
if (!handle.visible || handle.axis == SceneViewportGizmoAxis::None) {
continue;
}
SceneViewportOverlayHandleRecord& record = frameData.handleRecords.emplace_back();
record.kind = SceneViewportOverlayHandleKind::MoveAxis;
record.handleId = static_cast<uint64_t>(handle.axis);
record.entityId = entityId;
record.shape = SceneViewportOverlayHandleShape::ScreenSegment;
record.priority = kSceneViewportHandlePriorityMoveAxis;
record.screenStart = handle.start;
record.screenEnd = handle.end;
record.hitThicknessPixels = kSceneViewportMoveAxisHitThicknessPixels;
}
for (const SceneViewportMoveGizmoPlaneDrawData& plane : drawData.planes) {
if (!plane.visible || plane.plane == SceneViewportGizmoPlane::None) {
continue;
}
SceneViewportOverlayHandleRecord& record = frameData.handleRecords.emplace_back();
record.kind = SceneViewportOverlayHandleKind::MovePlane;
record.handleId = static_cast<uint64_t>(plane.plane);
record.entityId = entityId;
record.shape = SceneViewportOverlayHandleShape::ScreenQuad;
record.priority = kSceneViewportHandlePriorityMovePlane;
record.screenQuad = plane.corners;
}
}
inline void AppendRotateGizmoHandleRecords(
SceneViewportOverlayFrameData& frameData,
const SceneViewportRotateGizmoDrawData& drawData,
uint64_t entityId) {
if (!drawData.visible || entityId == 0) {
return;
}
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
if (!handle.visible || handle.axis == SceneViewportRotateGizmoAxis::None) {
continue;
}
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
if (!segment.visible ||
(handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) {
continue;
}
SceneViewportOverlayHandleRecord& record = frameData.handleRecords.emplace_back();
record.kind = SceneViewportOverlayHandleKind::RotateAxis;
record.handleId = static_cast<uint64_t>(handle.axis);
record.entityId = entityId;
record.shape = SceneViewportOverlayHandleShape::ScreenSegment;
record.priority = kSceneViewportHandlePriorityRotateAxis;
record.screenStart = segment.start;
record.screenEnd = segment.end;
record.hitThicknessPixels = kSceneViewportRotateAxisHitThicknessPixels;
}
}
}
inline void AppendScaleGizmoHandleRecords(
SceneViewportOverlayFrameData& frameData,
const SceneViewportScaleGizmoDrawData& drawData,
uint64_t entityId) {
if (!drawData.visible || entityId == 0) {
return;
}
if (drawData.centerHandle.visible) {
SceneViewportOverlayHandleRecord& uniformRecord = frameData.handleRecords.emplace_back();
uniformRecord.kind = SceneViewportOverlayHandleKind::ScaleUniform;
uniformRecord.handleId = static_cast<uint64_t>(SceneViewportScaleGizmoHandle::Uniform);
uniformRecord.entityId = entityId;
uniformRecord.shape = SceneViewportOverlayHandleShape::ScreenRect;
uniformRecord.priority = kSceneViewportHandlePriorityScaleUniform;
uniformRecord.screenCenter = drawData.centerHandle.center;
uniformRecord.screenHalfSize = Math::Vector2(
drawData.centerHandle.halfSize + kSceneViewportScaleCapHitPaddingPixels,
drawData.centerHandle.halfSize + kSceneViewportScaleCapHitPaddingPixels);
}
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : drawData.axisHandles) {
if (!handle.visible || handle.handle == SceneViewportScaleGizmoHandle::None) {
continue;
}
SceneViewportOverlayHandleRecord& capRecord = frameData.handleRecords.emplace_back();
capRecord.kind = SceneViewportOverlayHandleKind::ScaleAxis;
capRecord.handleId = static_cast<uint64_t>(handle.handle);
capRecord.entityId = entityId;
capRecord.shape = SceneViewportOverlayHandleShape::ScreenRect;
capRecord.priority = kSceneViewportHandlePriorityScaleAxisCap;
capRecord.screenCenter = handle.capCenter;
capRecord.screenHalfSize = Math::Vector2(
handle.capHalfSize + kSceneViewportScaleCapHitPaddingPixels,
handle.capHalfSize + kSceneViewportScaleCapHitPaddingPixels);
SceneViewportOverlayHandleRecord& lineRecord = frameData.handleRecords.emplace_back();
lineRecord.kind = SceneViewportOverlayHandleKind::ScaleAxis;
lineRecord.handleId = static_cast<uint64_t>(handle.handle);
lineRecord.entityId = entityId;
lineRecord.shape = SceneViewportOverlayHandleShape::ScreenSegment;
lineRecord.priority = kSceneViewportHandlePriorityScaleAxisLine;
lineRecord.screenStart = handle.start;
lineRecord.screenEnd = handle.end;
lineRecord.hitThicknessPixels = kSceneViewportScaleAxisHitThicknessPixels;
}
}
inline void AppendMoveGizmoScreenTriangles(
SceneViewportOverlayFrameData& frameData,
const SceneViewportMoveGizmoDrawData& drawData) {
if (!drawData.visible) {
return;
}
for (const SceneViewportMoveGizmoPlaneDrawData& plane : drawData.planes) {
if (!plane.visible) {
continue;
}
AppendScreenQuad(
frameData,
plane.corners[0],
plane.corners[1],
plane.corners[2],
plane.corners[3],
plane.fillColor);
AppendScreenQuadOutline(
frameData,
plane.corners,
plane.active ? 2.6f : (plane.hovered ? 2.0f : 1.4f),
plane.outlineColor);
}
for (const SceneViewportMoveGizmoHandleDrawData& handle : drawData.handles) {
if (!handle.visible) {
continue;
}
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f);
const Math::Vector2 direction = NormalizeVector2(handle.end - handle.start);
const float arrowLength =
(std::min)(kSceneViewportMoveArrowLengthPixels, (handle.end - handle.start).Magnitude());
const Math::Vector2 normal(-direction.y, direction.x);
const Math::Vector2 arrowBase = handle.end - direction * arrowLength;
const Math::Vector2 arrowLeft = arrowBase + normal * kSceneViewportMoveArrowHalfWidthPixels;
const Math::Vector2 arrowRight = arrowBase - normal * kSceneViewportMoveArrowHalfWidthPixels;
AppendScreenSegmentQuad(frameData, handle.start, arrowBase, thickness, handle.color);
AppendScreenTriangle(frameData, handle.end, arrowLeft, arrowRight, handle.color);
}
}
inline void AppendRotateGizmoHandleScreenTriangles(
SceneViewportOverlayFrameData& frameData,
const SceneViewportRotateGizmoHandleDrawData& handle,
bool frontPass) {
if (!handle.visible) {
return;
}
const bool isViewHandle = handle.axis == SceneViewportRotateGizmoAxis::View;
if (isViewHandle && !frontPass) {
return;
}
const float thickness = handle.active ? 3.6f : (handle.hovered ? 3.0f : 2.1f);
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
if (!segment.visible || (!isViewHandle && segment.frontFacing != frontPass)) {
continue;
}
Math::Color drawColor = handle.color;
if (!isViewHandle && !frontPass) {
drawColor = LerpColor(handle.color, Math::Color(0.72f, 0.72f, 0.72f, 1.0f), 0.78f);
drawColor = WithAlpha(drawColor, handle.active ? 0.55f : 0.38f);
} else if (isViewHandle) {
drawColor = WithAlpha(drawColor, handle.active ? 0.95f : (handle.hovered ? 0.88f : 0.78f));
}
AppendScreenSegmentQuad(frameData, segment.start, segment.end, thickness, drawColor);
}
}
inline void AppendRotateGizmoAngleFillScreenTriangles(
SceneViewportOverlayFrameData& frameData,
const SceneViewportRotateGizmoAngleFillDrawData& angleFill) {
if (!angleFill.visible || angleFill.arcPointCount < 2u) {
return;
}
for (size_t index = 0; index + 1u < angleFill.arcPointCount; ++index) {
AppendScreenTriangle(
frameData,
angleFill.pivot,
angleFill.arcPoints[index],
angleFill.arcPoints[index + 1u],
angleFill.fillColor);
}
for (size_t index = 0; index + 1u < angleFill.arcPointCount; ++index) {
AppendScreenSegmentQuad(
frameData,
angleFill.arcPoints[index],
angleFill.arcPoints[index + 1u],
2.0f,
angleFill.outlineColor);
}
AppendScreenSegmentQuad(
frameData,
angleFill.pivot,
angleFill.arcPoints[0],
1.6f,
angleFill.outlineColor);
AppendScreenSegmentQuad(
frameData,
angleFill.pivot,
angleFill.arcPoints[angleFill.arcPointCount - 1u],
1.6f,
angleFill.outlineColor);
}
inline void AppendRotateGizmoScreenTriangles(
SceneViewportOverlayFrameData& frameData,
const SceneViewportRotateGizmoDrawData& drawData) {
if (!drawData.visible) {
return;
}
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
if (handle.axis == SceneViewportRotateGizmoAxis::View) {
AppendRotateGizmoHandleScreenTriangles(frameData, handle, true);
}
}
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
AppendRotateGizmoHandleScreenTriangles(frameData, handle, false);
}
}
AppendRotateGizmoAngleFillScreenTriangles(frameData, drawData.angleFill);
for (const SceneViewportRotateGizmoHandleDrawData& handle : drawData.handles) {
if (handle.axis != SceneViewportRotateGizmoAxis::View) {
AppendRotateGizmoHandleScreenTriangles(frameData, handle, true);
}
}
}
inline void AppendScaleGizmoScreenTriangles(
SceneViewportOverlayFrameData& frameData,
const SceneViewportScaleGizmoDrawData& drawData) {
if (!drawData.visible) {
return;
}
constexpr Math::Color kScaleCapOutlineColor(24.0f / 255.0f, 24.0f / 255.0f, 24.0f / 255.0f, 220.0f / 255.0f);
for (const SceneViewportScaleGizmoAxisHandleDrawData& handle : drawData.axisHandles) {
if (!handle.visible) {
continue;
}
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.2f);
const Math::Vector2 direction = NormalizeVector2(handle.capCenter - handle.start);
const Math::Vector2 lineEnd = handle.capCenter - direction * handle.capHalfSize;
const Math::Vector2 capHalfSize(handle.capHalfSize, handle.capHalfSize);
AppendScreenSegmentQuad(frameData, handle.start, lineEnd, thickness, handle.color);
AppendScreenRect(frameData, handle.capCenter, capHalfSize, handle.color);
AppendScreenRectOutline(
frameData,
handle.capCenter,
capHalfSize,
handle.active ? 2.0f : 1.0f,
kScaleCapOutlineColor);
}
if (drawData.centerHandle.visible) {
const Math::Vector2 halfSize(drawData.centerHandle.halfSize, drawData.centerHandle.halfSize);
AppendScreenRect(
frameData,
drawData.centerHandle.center,
halfSize,
drawData.centerHandle.fillColor);
AppendScreenRectOutline(
frameData,
drawData.centerHandle.center,
halfSize,
drawData.centerHandle.active ? 2.0f : 1.0f,
drawData.centerHandle.outlineColor);
}
}
} // namespace Detail
inline void AppendTransformGizmoHandleRecords(
SceneViewportOverlayFrameData& frameData,
const SceneViewportTransformGizmoHandleBuildInputs& inputs) {
if (inputs.moveGizmo != nullptr) {
Detail::AppendMoveGizmoHandleRecords(frameData, *inputs.moveGizmo, inputs.moveEntityId);
}
if (inputs.rotateGizmo != nullptr) {
Detail::AppendRotateGizmoHandleRecords(frameData, *inputs.rotateGizmo, inputs.rotateEntityId);
}
if (inputs.scaleGizmo != nullptr) {
Detail::AppendScaleGizmoHandleRecords(frameData, *inputs.scaleGizmo, inputs.scaleEntityId);
}
}
inline void AppendTransformGizmoScreenTriangles(
SceneViewportOverlayFrameData& frameData,
const SceneViewportTransformGizmoHandleBuildInputs& inputs) {
if (inputs.moveGizmo != nullptr) {
Detail::AppendMoveGizmoScreenTriangles(frameData, *inputs.moveGizmo);
}
if (inputs.rotateGizmo != nullptr) {
Detail::AppendRotateGizmoScreenTriangles(frameData, *inputs.rotateGizmo);
}
if (inputs.scaleGizmo != nullptr) {
Detail::AppendScaleGizmoScreenTriangles(frameData, *inputs.scaleGizmo);
}
}
inline SceneViewportOverlayFrameData BuildSceneViewportTransformGizmoOverlayFrameData(
const SceneViewportOverlayData& overlay,
const SceneViewportTransformGizmoHandleBuildInputs& inputs) {
SceneViewportOverlayFrameData frameData = {};
frameData.overlay = overlay;
AppendTransformGizmoScreenTriangles(frameData, inputs);
AppendTransformGizmoHandleRecords(frameData, inputs);
return frameData;
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,186 @@
#pragma once
#include "SceneViewportEditorOverlayData.h"
#include "SceneViewportMath.h"
#include <algorithm>
#include <cmath>
namespace XCEngine {
namespace Editor {
namespace Detail {
inline bool IsPointInsideSceneViewportScreenRect(
const Math::Vector2& point,
const Math::Vector2& center,
const Math::Vector2& halfSize) {
return std::abs(point.x - center.x) <= halfSize.x &&
std::abs(point.y - center.y) <= halfSize.y;
}
inline Math::Vector2 ComputeSceneViewportScreenQuadCenter(
const std::array<Math::Vector2, 4>& corners) {
Math::Vector2 center = Math::Vector2::Zero();
for (const Math::Vector2& corner : corners) {
center += corner;
}
return center * 0.25f;
}
inline bool IsPointInsideSceneViewportScreenQuad(
const Math::Vector2& point,
const std::array<Math::Vector2, 4>& corners) {
float previousCross = 0.0f;
for (size_t index = 0; index < corners.size(); ++index) {
const Math::Vector2 edgeStart = corners[index];
const Math::Vector2 edgeEnd = corners[(index + 1u) % corners.size()];
const Math::Vector2 edge = edgeEnd - edgeStart;
const Math::Vector2 toPoint = point - edgeStart;
const float cross = Math::Vector2::Cross(edge, toPoint);
if (std::abs(cross) <= Math::EPSILON) {
continue;
}
if (previousCross != 0.0f && cross * previousCross < 0.0f) {
return false;
}
previousCross = cross;
}
return true;
}
inline bool TryBuildSceneViewportOverlayHandleHitMetrics(
const SceneViewportOverlayFrameData& frameData,
const Math::Vector2& viewportSize,
const SceneViewportOverlayHandleRecord& handleRecord,
const Math::Vector2& viewportMousePosition,
float& outDistanceSq,
float& outDepth) {
if (!frameData.overlay.valid ||
viewportSize.x <= 1.0f ||
viewportSize.y <= 1.0f ||
handleRecord.kind == SceneViewportOverlayHandleKind::None) {
return false;
}
switch (handleRecord.shape) {
case SceneViewportOverlayHandleShape::WorldRect: {
if (handleRecord.sizePixels.x <= Math::EPSILON ||
handleRecord.sizePixels.y <= Math::EPSILON) {
return false;
}
const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint(
frameData.overlay,
viewportSize.x,
viewportSize.y,
handleRecord.worldPosition);
if (!projectedPoint.visible) {
return false;
}
const Math::Vector2 center = projectedPoint.screenPosition;
const Math::Vector2 halfSize = handleRecord.sizePixels * 0.5f;
if (!IsPointInsideSceneViewportScreenRect(viewportMousePosition, center, halfSize)) {
return false;
}
outDistanceSq = (center - viewportMousePosition).SqrMagnitude();
outDepth = projectedPoint.ndcDepth;
return true;
}
case SceneViewportOverlayHandleShape::ScreenSegment: {
if (handleRecord.hitThicknessPixels <= Math::EPSILON ||
(handleRecord.screenEnd - handleRecord.screenStart).SqrMagnitude() <= Math::EPSILON) {
return false;
}
const float maxDistanceSq = handleRecord.hitThicknessPixels * handleRecord.hitThicknessPixels;
const float distanceSq = DistanceToSegmentSquared(
viewportMousePosition,
handleRecord.screenStart,
handleRecord.screenEnd);
if (distanceSq > maxDistanceSq) {
return false;
}
outDistanceSq = distanceSq;
outDepth = handleRecord.sortDepth;
return true;
}
case SceneViewportOverlayHandleShape::ScreenRect: {
if (handleRecord.screenHalfSize.x <= Math::EPSILON ||
handleRecord.screenHalfSize.y <= Math::EPSILON) {
return false;
}
if (!IsPointInsideSceneViewportScreenRect(
viewportMousePosition,
handleRecord.screenCenter,
handleRecord.screenHalfSize)) {
return false;
}
outDistanceSq = (handleRecord.screenCenter - viewportMousePosition).SqrMagnitude();
outDepth = handleRecord.sortDepth;
return true;
}
case SceneViewportOverlayHandleShape::ScreenQuad: {
if (!IsPointInsideSceneViewportScreenQuad(viewportMousePosition, handleRecord.screenQuad)) {
return false;
}
const Math::Vector2 center = ComputeSceneViewportScreenQuadCenter(handleRecord.screenQuad);
outDistanceSq = (center - viewportMousePosition).SqrMagnitude();
outDepth = handleRecord.sortDepth;
return true;
}
case SceneViewportOverlayHandleShape::None:
default:
return false;
}
}
} // namespace Detail
inline SceneViewportOverlayHandleHitResult HitTestSceneViewportOverlayHandles(
const SceneViewportOverlayFrameData& frameData,
const Math::Vector2& viewportSize,
const Math::Vector2& viewportMousePosition) {
constexpr float kMetricEpsilon = 0.001f;
SceneViewportOverlayHandleHitResult result = {};
for (const SceneViewportOverlayHandleRecord& handleRecord : frameData.handleRecords) {
float distanceSq = 0.0f;
float depth = 0.0f;
if (!Detail::TryBuildSceneViewportOverlayHandleHitMetrics(
frameData,
viewportSize,
handleRecord,
viewportMousePosition,
distanceSq,
depth)) {
continue;
}
if (handleRecord.priority > result.priority ||
(handleRecord.priority == result.priority &&
(depth + kMetricEpsilon < result.depth ||
(std::abs(depth - result.depth) <= kMetricEpsilon && distanceSq < result.distanceSq)))) {
result.kind = handleRecord.kind;
result.handleId = handleRecord.handleId;
result.entityId = handleRecord.entityId;
result.priority = handleRecord.priority;
result.distanceSq = distanceSq;
result.depth = depth;
}
}
return result;
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -1,305 +1,16 @@
#include "SceneViewportOverlayRenderer.h"
#include "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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,323 @@
#pragma once
#include "Core/IEditorContext.h"
#include "Core/ISceneManager.h"
#include "Core/ISelectionManager.h"
#include "SceneViewportEditorOverlayData.h"
#include "SceneViewportMoveGizmo.h"
#include "SceneViewportRotateGizmo.h"
#include "SceneViewportScaleGizmo.h"
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/MeshFilterComponent.h>
#include <cstdint>
#include <vector>
namespace XCEngine {
namespace Editor {
enum class SceneViewportActiveGizmoKind : uint8_t {
None = 0,
Move,
Rotate,
Scale
};
struct SceneViewportSelectionGizmoState {
Components::GameObject* primaryObject = nullptr;
std::vector<Components::GameObject*> selectedObjects = {};
Math::Vector3 pivotWorldPosition = Math::Vector3::Zero();
Math::Quaternion primaryWorldRotation = Math::Quaternion::Identity();
};
struct SceneViewportTransformGizmoFrameState {
SceneViewportOverlayData overlay = {};
SceneViewportSelectionGizmoState selectionState = {};
SceneViewportMoveGizmoContext moveContext = {};
SceneViewportRotateGizmoContext rotateContext = {};
SceneViewportScaleGizmoContext scaleContext = {};
SceneViewportActiveGizmoKind activeGizmoKind = SceneViewportActiveGizmoKind::None;
};
inline SceneViewportActiveGizmoKind GetActiveSceneViewportGizmoKind(
const SceneViewportMoveGizmo& moveGizmo,
const SceneViewportRotateGizmo& rotateGizmo,
const SceneViewportScaleGizmo& scaleGizmo) {
if (moveGizmo.IsActive()) {
return SceneViewportActiveGizmoKind::Move;
}
if (rotateGizmo.IsActive()) {
return SceneViewportActiveGizmoKind::Rotate;
}
if (scaleGizmo.IsActive()) {
return SceneViewportActiveGizmoKind::Scale;
}
return SceneViewportActiveGizmoKind::None;
}
inline Math::Quaternion ComputeStableWorldRotation(const Components::GameObject* gameObject) {
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
return Math::Quaternion::Identity();
}
const auto* transform = gameObject->GetTransform();
Math::Quaternion worldRotation = transform->GetLocalRotation();
for (const auto* parent = transform->GetParent();
parent != nullptr;
parent = parent->GetParent()) {
worldRotation = parent->GetLocalRotation() * worldRotation;
}
return worldRotation.Normalized();
}
inline Math::Vector3 GetGameObjectPivotWorldPosition(const Components::GameObject* gameObject) {
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
return Math::Vector3::Zero();
}
return gameObject->GetTransform()->GetPosition();
}
inline Math::Vector3 GetGameObjectCenterWorldPosition(const Components::GameObject* gameObject) {
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
return Math::Vector3::Zero();
}
if (auto* meshFilter = gameObject->GetComponent<Components::MeshFilterComponent>()) {
if (Resources::Mesh* mesh = meshFilter->GetMesh();
mesh != nullptr && mesh->IsValid()) {
return gameObject->GetTransform()->TransformPoint(mesh->GetBounds().center);
}
}
return gameObject->GetTransform()->GetPosition();
}
inline SceneViewportSelectionGizmoState BuildSceneViewportSelectionGizmoState(
IEditorContext& context,
bool useCenterPivot) {
SceneViewportSelectionGizmoState state = {};
const uint64_t primaryEntityId = context.GetSelectionManager().GetSelectedEntity();
if (primaryEntityId != 0) {
state.primaryObject = context.GetSceneManager().GetEntity(primaryEntityId);
}
const std::vector<uint64_t>& selectedEntities = context.GetSelectionManager().GetSelectedEntities();
state.selectedObjects.reserve(selectedEntities.size());
for (uint64_t entityId : selectedEntities) {
if (entityId == 0) {
continue;
}
if (auto* gameObject = context.GetSceneManager().GetEntity(entityId)) {
state.selectedObjects.push_back(gameObject);
}
}
if (state.primaryObject == nullptr && !state.selectedObjects.empty()) {
state.primaryObject = state.selectedObjects.back();
}
if (state.primaryObject != nullptr && state.selectedObjects.empty()) {
state.selectedObjects.push_back(state.primaryObject);
}
if (state.primaryObject != nullptr) {
state.primaryWorldRotation = ComputeStableWorldRotation(state.primaryObject);
}
if (state.selectedObjects.empty()) {
return state;
}
if (useCenterPivot) {
Math::Vector3 centerSum = Math::Vector3::Zero();
for (const auto* gameObject : state.selectedObjects) {
centerSum += GetGameObjectCenterWorldPosition(gameObject);
}
state.pivotWorldPosition = centerSum / static_cast<float>(state.selectedObjects.size());
} else {
state.pivotWorldPosition = GetGameObjectPivotWorldPosition(state.primaryObject);
}
return state;
}
inline SceneViewportMoveGizmoContext BuildMoveGizmoContext(
const SceneViewportSelectionGizmoState& selectionState,
const SceneViewportOverlayData& overlay,
const Math::Vector2& viewportSize,
const Math::Vector2& mousePosition,
bool localSpace) {
SceneViewportMoveGizmoContext gizmoContext = {};
gizmoContext.overlay = overlay;
gizmoContext.viewportSize = viewportSize;
gizmoContext.mousePosition = mousePosition;
gizmoContext.selectedObject = selectionState.primaryObject;
gizmoContext.selectedObjects = selectionState.selectedObjects;
gizmoContext.pivotWorldPosition = selectionState.pivotWorldPosition;
gizmoContext.axisOrientation = localSpace
? selectionState.primaryWorldRotation
: Math::Quaternion::Identity();
return gizmoContext;
}
inline SceneViewportRotateGizmoContext BuildRotateGizmoContext(
const SceneViewportSelectionGizmoState& selectionState,
const SceneViewportOverlayData& overlay,
const Math::Vector2& viewportSize,
const Math::Vector2& mousePosition,
bool localSpace,
bool rotateAroundSharedPivot) {
SceneViewportRotateGizmoContext gizmoContext = {};
gizmoContext.overlay = overlay;
gizmoContext.viewportSize = viewportSize;
gizmoContext.mousePosition = mousePosition;
gizmoContext.selectedObject = selectionState.primaryObject;
gizmoContext.selectedObjects = selectionState.selectedObjects;
gizmoContext.pivotWorldPosition = selectionState.pivotWorldPosition;
gizmoContext.axisOrientation = localSpace
? selectionState.primaryWorldRotation
: Math::Quaternion::Identity();
gizmoContext.localSpace = localSpace;
gizmoContext.rotateAroundSharedPivot = rotateAroundSharedPivot;
return gizmoContext;
}
inline SceneViewportScaleGizmoContext BuildScaleGizmoContext(
const SceneViewportSelectionGizmoState& selectionState,
const SceneViewportOverlayData& overlay,
const Math::Vector2& viewportSize,
const Math::Vector2& mousePosition,
bool localSpace) {
SceneViewportScaleGizmoContext gizmoContext = {};
gizmoContext.overlay = overlay;
gizmoContext.viewportSize = viewportSize;
gizmoContext.mousePosition = mousePosition;
gizmoContext.selectedObject = selectionState.primaryObject;
gizmoContext.pivotWorldPosition = selectionState.pivotWorldPosition;
gizmoContext.axisOrientation = localSpace
? selectionState.primaryWorldRotation
: Math::Quaternion::Identity();
return gizmoContext;
}
inline void CancelSceneViewportTransformGizmoDrags(
IEditorContext& context,
SceneViewportMoveGizmo& moveGizmo,
SceneViewportRotateGizmo& rotateGizmo,
SceneViewportScaleGizmo& scaleGizmo) {
if (moveGizmo.IsActive()) {
moveGizmo.CancelDrag(&context.GetUndoManager());
}
if (rotateGizmo.IsActive()) {
rotateGizmo.CancelDrag(&context.GetUndoManager());
}
if (scaleGizmo.IsActive()) {
scaleGizmo.CancelDrag(&context.GetUndoManager());
}
}
inline SceneViewportTransformGizmoFrameState RefreshSceneViewportTransformGizmos(
IEditorContext& context,
const SceneViewportOverlayData& overlay,
const Math::Vector2& viewportSize,
const Math::Vector2& mousePosition,
bool useCenterPivot,
bool localSpace,
bool usingTransformTool,
bool showingMoveGizmo,
SceneViewportMoveGizmo& moveGizmo,
bool showingRotateGizmo,
SceneViewportRotateGizmo& rotateGizmo,
bool showingScaleGizmo,
SceneViewportScaleGizmo& scaleGizmo) {
SceneViewportTransformGizmoFrameState state = {};
state.overlay = overlay;
state.selectionState = BuildSceneViewportSelectionGizmoState(context, useCenterPivot);
if (showingMoveGizmo) {
state.moveContext = BuildMoveGizmoContext(
state.selectionState,
overlay,
viewportSize,
mousePosition,
localSpace);
if (moveGizmo.IsActive() &&
(state.moveContext.selectedObject == nullptr ||
context.GetSelectionManager().GetSelectedEntity() != moveGizmo.GetActiveEntityId())) {
moveGizmo.CancelDrag(&context.GetUndoManager());
}
} else if (moveGizmo.IsActive()) {
moveGizmo.CancelDrag(&context.GetUndoManager());
}
if (showingRotateGizmo) {
state.rotateContext = BuildRotateGizmoContext(
state.selectionState,
overlay,
viewportSize,
mousePosition,
localSpace,
useCenterPivot);
if (rotateGizmo.IsActive() &&
(state.rotateContext.selectedObject == nullptr ||
context.GetSelectionManager().GetSelectedEntity() != rotateGizmo.GetActiveEntityId())) {
rotateGizmo.CancelDrag(&context.GetUndoManager());
}
} else if (rotateGizmo.IsActive()) {
rotateGizmo.CancelDrag(&context.GetUndoManager());
}
if (showingScaleGizmo) {
state.scaleContext = BuildScaleGizmoContext(
state.selectionState,
overlay,
viewportSize,
mousePosition,
localSpace);
state.scaleContext.uniformOnly = usingTransformTool;
if (scaleGizmo.IsActive() &&
(state.scaleContext.selectedObject == nullptr ||
context.GetSelectionManager().GetSelectedEntity() != scaleGizmo.GetActiveEntityId())) {
scaleGizmo.CancelDrag(&context.GetUndoManager());
}
} else if (scaleGizmo.IsActive()) {
scaleGizmo.CancelDrag(&context.GetUndoManager());
}
state.activeGizmoKind = GetActiveSceneViewportGizmoKind(moveGizmo, rotateGizmo, scaleGizmo);
if (showingMoveGizmo) {
SceneViewportMoveGizmoContext updateContext = state.moveContext;
if (state.activeGizmoKind != SceneViewportActiveGizmoKind::None &&
state.activeGizmoKind != SceneViewportActiveGizmoKind::Move) {
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
}
moveGizmo.Update(updateContext);
}
if (showingRotateGizmo) {
SceneViewportRotateGizmoContext updateContext = state.rotateContext;
if (state.activeGizmoKind != SceneViewportActiveGizmoKind::None &&
state.activeGizmoKind != SceneViewportActiveGizmoKind::Rotate) {
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
}
rotateGizmo.Update(updateContext);
}
if (showingScaleGizmo) {
SceneViewportScaleGizmoContext updateContext = state.scaleContext;
if (state.activeGizmoKind != SceneViewportActiveGizmoKind::None &&
state.activeGizmoKind != SceneViewportActiveGizmoKind::Scale) {
updateContext.mousePosition = Math::Vector2(-1.0f, -1.0f);
}
scaleGizmo.Update(updateContext);
}
return state;
}
} // namespace Editor
} // namespace XCEngine

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}

View File

@@ -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();
}

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB