diff --git a/docs/plan/editor-engine-services-refactor-plan.md b/docs/plan/editor-engine-services-refactor-plan.md new file mode 100644 index 00000000..1e98b387 --- /dev/null +++ b/docs/plan/editor-engine-services-refactor-plan.md @@ -0,0 +1,267 @@ +# EditorEngineServices Refactor Plan + +## Problem + +The root problem is no longer that `EngineEditorServices.cpp` used to be too large. + +That implementation has already been split. The real architectural problem is +still `EditorEngineServices` itself: + +- it is a god interface +- it mixes scene backend creation, viewport bridge logic, shader loading, + object-id resolving, and engine lifecycle +- editor subsystems still depend on one shared service-locator style entrypoint +- new bridge needs will keep accumulating in `editor/app/Services/Engine` + unless that dependency surface is actively reduced + +This refactor must therefore optimize for dependency reduction, not file +splitting. + +## Target State + +The goal is not "no bridge code". + +The goal is "distributed, narrow bridges at clear boundaries instead of one +central editor-to-engine gateway". + +The target editor-side interfaces are: + +- `EditorSceneBackendFactory` + - only `CreateSceneBackend` +- `SceneViewportEngineBridge` + - only scene viewport frame-plan build, render, and object-id resolve +- `GameViewportEngineBridge` + - only game viewport frame-plan build and render +- `EditorShaderProvider` + - only shader or resource acquisition +- `EditorEngineLifecycle` + - only `UpdateAsyncLoads` and `Shutdown` + +If `EngineEditorServices.cpp` still exists at the end, it may only exist as an +internal composition-shell implementation. It must not remain a shared runtime +dependency across editor features. + +## Freeze Rule + +`EditorEngineServices` is now a compatibility shell under freeze: + +- no new public methods +- no new feature bridges added to it +- no new panels, tools, passes, or runtimes depending on it +- only subtraction is allowed: move capabilities out of it + +Without this rule, the refactor will fail even if some files get cleaner. + +## Current Consumer Map + +Refactoring must follow actual consumers, not folder aesthetics. + +### `EditorContext` + +Current usage: + +- `CreateSceneBackend` + +Conclusion: + +- it does not need viewport, shader, game, or lifecycle capabilities +- it should depend only on `EditorSceneBackendFactory` + +File: + +- `editor/app/Composition/EditorContext.cpp` + +### `SceneViewportRenderService` + +Current usage: + +- `BuildSceneViewportFramePlan` +- `RenderSceneViewportFramePlan` +- `TryResolveActiveSceneRenderObjectId` + +Conclusion: + +- it should depend only on `SceneViewportEngineBridge` + +File: + +- `editor/app/Rendering/Viewport/SceneViewportRenderService.cpp` + +### `GameViewportRenderService` + +Current usage: + +- `BuildGameViewportFramePlans` +- `RenderGameViewportFramePlans` + +Conclusion: + +- it should depend only on `GameViewportEngineBridge` + +File: + +- `editor/app/Rendering/Viewport/GameViewportRenderService.cpp` + +### Scene Viewport Passes + +Current usage: + +- `LoadShader` + +Affected code: + +- `SceneViewportGridPass` +- `SceneViewportSelectionOutlinePass` + +Conclusion: + +- low-level render passes are taking a full engine facade just to load shaders +- this is the wrong dependency direction +- they should depend only on `EditorShaderProvider` + +Files: + +- `editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.cpp` +- `editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.cpp` + +### `Application` + +Current usage: + +- `UpdateAsyncLoads` +- `Shutdown` + +Conclusion: + +- it should depend only on `EditorEngineLifecycle` + +File: + +- `editor/app/Bootstrap/Application.cpp` + +## Execution Order + +This work should proceed in dependency order, not in arbitrary file order. + +### Phase A. Freeze the Interface + +Goals: + +- document `EditorEngineServices` as a temporary compatibility shell +- enforce the "no new methods" rule in code review and follow-up work + +Acceptance: + +- `EditorEngineServices.h` clearly states the compatibility-shell role +- no new consumers are introduced + +### Phase B. Split by Consumer + +Goals: + +- remove the easy, high-signal consumers from the god interface first + +Execution order: + +1. `EditorContext` -> `EditorSceneBackendFactory` +2. `SceneViewportRenderService` -> `SceneViewportEngineBridge` +3. `GameViewportRenderService` -> `GameViewportEngineBridge` +4. `Application` -> `EditorEngineLifecycle` + +Why this order: + +- low behavior risk +- mostly editor-side injection cleanup +- removes the largest legitimate consumers first + +Acceptance: + +- those four consumers no longer include or store `EditorEngineServices` +- each one is injected with a narrow dependency + +### Phase C. Remove Low-Level Resource Leakage + +Goals: + +- remove `LoadShader` reach from low-level viewport passes + +Execution order: + +1. introduce `EditorShaderProvider` +2. migrate `SceneViewportGridPass` +3. migrate `SceneViewportSelectionOutlinePass` + +Acceptance: + +- scene viewport passes no longer include `EditorEngineServices.h` +- shader access flows through a narrow provider + +### Phase D. Collapse the Compatibility Shell + +Goals: + +- remove the public architectural role of `EditorEngineServices` + +Allowed end states: + +1. delete `EditorEngineServices` +2. keep only an internal composition object that does not appear in feature, + runtime, or pass dependencies + +Disallowed fake end states: + +- split implementation files but keep the same shared public facade forever +- rename the god interface without shrinking its surface + +## Non-Goals + +This plan intentionally does not include: + +- mixing this work with `EditorSceneComponentMutation` typed-command refactors +- copying Unreal Engine module structure mechanically +- removing all bridge code as a principle +- large behavior rewrites when dependency shrinkage is sufficient + +## Code Movement Rules + +All follow-up implementation should obey these rules: + +- interfaces are named by consumer boundary, not by generic service language +- each interface exposes only the methods that one consumer actually uses +- render passes must not depend on scene backend factory or lifecycle code +- composition owns assembly; features and passes must not locate services on + their own +- bridge code is acceptable only when it stays narrow, local, and scoped to a + concrete boundary + +## Completion Criteria + +This refactor is complete only when all of the following are true: + +- `EditorContext` no longer depends on `EditorEngineServices` +- `SceneViewportRenderService` no longer depends on `EditorEngineServices` +- `GameViewportRenderService` no longer depends on `EditorEngineServices` +- `Application` no longer depends on `EditorEngineServices` +- scene viewport passes no longer depend on `EditorEngineServices` +- `EditorEngineServices` has gained no new methods +- `editor/app/Services/Engine` has stopped being the default dumping ground for + every editor-engine bridge + +## Immediate Next Cut + +The next cut is not "split `EngineEditorServices.cpp` again". + +The correct next cut is: + +1. define `EditorSceneBackendFactory` +2. make `EditorContext` depend only on that interface +3. remove scene backend creation from the `EditorEngineServices` consumer + surface + +Why this is next: + +- it is the safest cut +- it has the narrowest dependency +- it starts emptying `EditorEngineServices` from the top of the editor stack +- it creates the migration template for viewport, game, lifecycle, and shader + provider splits diff --git a/docs/plan/editor-next-stage-runtime-plan.md b/docs/plan/editor-next-stage-runtime-plan.md new file mode 100644 index 00000000..394be1d9 --- /dev/null +++ b/docs/plan/editor-next-stage-runtime-plan.md @@ -0,0 +1,143 @@ +# Editor Next Stage Plan + +## Goal + +在不再发起新一轮大规模架构重构的前提下,把新 `editor` 从“结构已经完整的编辑器壳”推进成“可运行、可保存、可调试的编辑器产品”。 + +下一阶段只围绕一个主目标展开: + +- 补齐 `Edit -> Play/Pause/Step -> Script Rebuild/Reload -> Game View -> Runtime Feedback -> Scene Save/Open` 的产品闭环 + +## Why This Plan Now + +- `editor` 现有分层已经基本成型,`XCUIEditor / XCEditorCore / XCEditorRendering / XCEditorHost / XCEditor` 的边界已经足够支撑产品化推进 +- `EditorRuntimeMode` 已经定义了 `Edit / Play / Paused`,但当前还没有真正的 runtime owner 驱动这条状态机 +- `Game` 面板仍然是 placeholder,产品清单明确写着 `Game view runtime is not implemented` +- 顶层命令壳已经存在,`run.play / run.pause / run.step / scripts.rebuild / file.new_scene / file.open_scene / file.save_scene` 已经出现在 shell asset 中,但新 app 层还没有把它们接成真实工作流 +- Scene 运行链、场景打开能力、视口渲染链都已经存在,说明当前短板不在底层能力,而在 editor app 层没有形成完整 owner 和文档流 +- Project、Inspector、Console 也都已经有初步形态,但如果 runtime 闭环没建好,这些能力无法转化成稳定的日常工作流 + +## Current Facts + +- 架构事实: + - `editor` 当前是多层结构,不再适合继续把主要精力投入到“继续拆模块”上 +- 运行态事实: + - `EditorRuntimeMode` 已存在 + - `Game` 面板仍为占位实现 + - `run.play / run.pause / run.step` 仍缺少真实 owner +- 文档流事实: + - `file.new_scene / file.open_scene / file.save_scene / file.save_scene_as / file.save_project` 已出现在命令系统里 + - 但 HostCommandBridge 仍对大量命令走 unsupported 路径 +- 场景编辑事实: + - Scene viewport、选择、变换、组件增删改、transform undo/redo 已经可用 + - 但更完整的 scene/document 事务系统还没有闭合 +- 资产工作流事实: + - Project 面板已经支持 folder/scene 等基础操作 + - `duplicate / cut / copy / paste / reimport / clear library` 等关键命令仍未完成 +- 调试诊断事实: + - Console 目前主要展示 `session.consoleEntries` + - 它更像 session/status console,而不是运行时日志和脚本日志前端 + +## Phase 1: Close the runtime product loop + +目标: + +- 给 `EditorRuntimeMode` 建立真正的 runtime/session owner +- 把 `run.play / run.pause / run.step` 变成真实可执行命令 +- 把 `Game` 面板从 placeholder 变成真实 runtime-backed viewport +- 建立 `scripts.rebuild` 的最小闭环,至少做到 rebuild + reload + 反馈 +- 把 `new/open/save scene` 接成真正的文档工作流 + +工作项: + +- 新建或收敛一个统一的 editor runtime coordinator +- 明确 Edit、Play、Paused 三态的状态迁移、资源生命周期和失败回滚 +- 在 HostCommandBridge 中为 `run.*` 和 `file.*` 的核心命令接入 owner +- 把 `Game` 视口接入真实 runtime render source,而不是 placeholder renderer +- 为脚本重建建立最小执行链和结果反馈链 +- 让 Console 至少能显示最基本的 runtime/script 结果反馈,而不只是菜单和 workspace 状态 +- 为 scene 文档引入最基本的 dirty state、save、open、replace current scene 规则 + +验收标准: + +- 可以从当前 scene 进入 Play,并在 `Game` 面板看到真实运行画面 +- `Pause` 和 `Step` 有确定行为,而不是只改变按钮或状态文本 +- 脚本修改后可以触发 rebuild,并能看到成功/失败反馈 +- `New/Open/Save Scene` 可以在新 editor app 流程中稳定工作 +- Play 进出不会破坏 Scene 视口、Selection、Inspector 的基本一致性 + +阶段收益: + +- 新 editor 第一次具备“可以完整跑一轮内容制作和运行验证”的产品意义 +- 现有菜单、工具栏、Game 面板、Console、脚本系统开始从壳变成真正可用功能 + +## Phase 2: Complete the asset workflow + +目标: + +- 补齐 Project 面板的核心缺口 +- 让资产浏览从“能看能点”升级成“能生产内容” +- 把 Inspector 从 scene-object 强、asset 弱的状态推进到更均衡 + +工作项: + +- 完成 `edit.duplicate` +- 完成 `edit.cut / edit.copy / edit.paste` +- 完成 `assets.reimport_selected / assets.reimport_all / assets.clear_library` +- 扩展 Project item open/preview/import ownership +- 优先补 `Material` 资产 Inspector,再补 `Scene` 和 `Texture` 相关编辑能力 +- 梳理 project runtime、asset database、preview path 的 ownership 边界 + +验收标准: + +- 常见资产整理动作不再需要回退到外部文件管理器 +- `Material` 至少具备可编辑的 Inspector 入口 +- 资产重导入和预览行为具有明确反馈,而不是静默失败 + +阶段收益: + +- 一旦 runtime 闭环建立,内容生产效率不会立刻被 Project/Asset 流程卡死 + +## Phase 3: Transactions, diagnostics, and hardening + +目标: + +- 把 editor 从“能用”推进到“可持续使用” +- 提升编辑事务一致性、运行时诊断能力和多窗口稳定性 + +工作项: + +- 把 undo/redo 从 transform 历史扩展到更多 scene mutation +- 梳理 selection、hierarchy、inspector、scene backend 之间的统一事务边界 +- 把 Console 升级为真正的 runtime/script diagnostic front-end +- 加强 runtime 切换、viewport 生命周期、多窗口同步的稳定性 +- 围绕 runtime/document/asset flow 补齐更多回归测试 + +验收标准: + +- 常见 scene 修改可以撤销/重做,不再只覆盖少数编辑动作 +- 运行时和脚本错误能在 Console 中定位,而不是依赖外部日志或缺失反馈 +- 多窗口和 runtime 切换不再频繁出现状态错位 + +阶段收益: + +- editor 开始具备日常开发环境应有的可靠性,而不是一次性演示工具 + +## Non-goals For This Stage + +- 不把“继续拆模块”作为下一阶段主线 +- 不把新增更多独立面板作为优先方向 +- 不把大规模 UI 美化和交互抛光放在 runtime 闭环之前 +- 不把多窗口能力扩展当成当前最高优先级 + +## Current Decision + +当前正式进入 `Phase 1`。 + +原因很直接: + +- 架构基础已经足够 +- 底层 scene/runtime/rendering 能力已经不是主要瓶颈 +- 真正限制新 editor 价值的,是 runtime owner、Game 视口、脚本重建、scene 文档命令这几条链路没有闭合 + +只有先补齐这条主线,后续的 asset workflow、diagnostics、hardening 才有明确落点。 diff --git a/editor/AGENTS.md b/editor/AGENTS.md index a380fb43..2fae3c9a 100644 --- a/editor/AGENTS.md +++ b/editor/AGENTS.md @@ -64,6 +64,11 @@ Rules: - `EditorHostCommandBridge` delegates `file.*`, `run.*`, and `scripts.*` to a bound runtime owner. If no owner is bound, it must continue exposing honest disabled messages. +- host-command context is workspace-shell local, not editor-global. The active + panel fallback and command-focus resolution must come from the current + `UIEditorWorkspaceController` plus that shell's + `EditorCommandFocusService`. Do not put window-local command routing back on + `EditorSession`, `EditorContext`, or any app-global bridge/shortcut manager. - `EditorSceneRuntime` owns raw scene editing behavior and scene mutations. It may return transient startup/open/new results, but it must not own the live scene document state. Scene viewport private state now lives in @@ -88,7 +93,10 @@ Rules: passed through the workspace-panel seam or the utility-window panel seam as a generic dependency source; workspace panels and utility panels should receive explicit panel-local bindings or typed callbacks instead. Runtime time-step - ownership does not belong on this interface. + ownership does not belong on this interface. Command routing ownership also + does not belong on this interface: do not add host-command bridge bindings, + command-focus syncing, or active-route/session transport back to + `EditorFrameServices`. ## Working Rules @@ -101,6 +109,11 @@ Rules: future utility panels must be constructed with the exact runtime/tool state they use; do not make utility panel update paths depend on `EditorFrameServices` +- keep command routing shell-local. `UIEditorShortcutManager`, + `EditorHostCommandBridge`, and `EditorCommandFocusService` must be owned by + the workspace shell/runtime set that serves one window. If a command needs + active-panel context, pass the current `UIEditorWorkspaceController`; do not + mirror that context into `EditorSession` - keep viewport-local state local. Camera/tool mode/pivot/space state for Scene belongs in `SceneViewportSession` on the feature/controller side; do not push it back into shared runtime/context/frame-service layers for convenience @@ -125,6 +138,10 @@ Rules: dependency bundle - no `EditorFrameServices` tunneling into workspace-panel update/prepare paths - no `EditorFrameServices` tunneling into utility-window panel update paths +- no global host-command bridge or global shortcut manager shared across editor + windows +- no `EditorSession.activePanelId`, `EditorSession.activeRoute`, or equivalent + session mirrors for per-window command context - no per-window or per-shell runtime ticking path; runtime must not advance once per workspace window - no new live scene document metadata on `EditorSceneRuntime`; coordinator diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index eb263d24..540fdb1d 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -17,6 +17,9 @@ function(xcui_editor_apply_common_target_settings target visibility) endif() endfunction() +file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCENGINE_ROOT_DIR_CMAKE) +file(TO_CMAKE_PATH "${XCENGINE_MONO_ROOT_DIR}" XCENGINE_MONO_ROOT_DIR_CMAKE) + if(XCENGINE_BUILD_XCUI_EDITOR_APP AND NOT XCENGINE_BUILD_XCUI_EDITOR_CORE) message(FATAL_ERROR @@ -255,6 +258,7 @@ if(XCENGINE_BUILD_XCUI_EDITOR_CORE) app/Features/Inspector/InspectorSubject.cpp app/Features/Inspector/Components/InspectorBindingComponentEditor.cpp app/Features/Inspector/Components/InspectorComponentEditorRegistry.cpp + app/Features/Inspector/Components/ScriptComponentInspectorComponentEditor.cpp app/Features/Inspector/Components/TransformInspectorComponentEditor.cpp app/Features/Project/ProjectPanel.cpp app/Features/Scene/SceneViewportTransformGizmo.cpp @@ -289,10 +293,17 @@ if(XCENGINE_BUILD_XCUI_EDITOR_CORE) app/Services/Scene/EngineEditorSceneBackend.cpp app/Services/Scene/EditorSceneRuntime.cpp app/Services/Runtime/EditorRuntimeCoordinator.cpp + app/Services/Runtime/EditorScriptingRuntimeService.cpp app/Services/Project/EditorProjectRuntime.cpp app/Services/Project/ProjectBrowserModel.cpp ) + if(XCENGINE_ENABLE_MONO_SCRIPTING) + list(APPEND XCUI_EDITOR_APP_SUPPORT_SOURCES + ${CMAKE_SOURCE_DIR}/mvs/editor/src/Scripting/EditorScriptAssemblyBuilder.cpp + ) + endif() + set(XCUI_EDITOR_APP_CORE_SOURCES ${XCUI_EDITOR_APP_CORE_CONTRACT_SOURCES} ${XCUI_EDITOR_APP_STATE_SOURCES} @@ -324,6 +335,7 @@ if(XCENGINE_BUILD_XCUI_EDITOR_CORE) ${CMAKE_CURRENT_SOURCE_DIR}/app/Services ${CMAKE_CURRENT_SOURCE_DIR}/app/Support ${CMAKE_CURRENT_SOURCE_DIR}/app/Windowing + ${CMAKE_SOURCE_DIR}/mvs/editor/src ) xcui_editor_apply_common_target_settings(XCEditorCore PUBLIC) @@ -334,6 +346,21 @@ if(XCENGINE_BUILD_XCUI_EDITOR_CORE) XCEngineRenderingEditorSupport ) + target_compile_definitions(XCEditorCore PRIVATE + XCENGINE_EDITOR_REPO_ROOT="${XCENGINE_ROOT_DIR_CMAKE}" + ) + + if(XCENGINE_ENABLE_MONO_SCRIPTING) + target_compile_definitions(XCEditorCore PRIVATE + XCENGINE_ENABLE_MONO_SCRIPTING + XCENGINE_EDITOR_MONO_ROOT_DIR="${XCENGINE_MONO_ROOT_DIR_CMAKE}" + ) + + if(TARGET xcengine_project_managed_assemblies) + add_dependencies(XCEditorCore xcengine_project_managed_assemblies) + endif() + endif() + add_library(XCEditorRendering STATIC ${XCUI_EDITOR_APP_RENDERING_SOURCES} ) diff --git a/editor/app/Bootstrap/Application.cpp b/editor/app/Bootstrap/Application.cpp index a82612cb..a7a1d99c 100644 --- a/editor/app/Bootstrap/Application.cpp +++ b/editor/app/Bootstrap/Application.cpp @@ -19,6 +19,7 @@ #include "D3D12EditorWindowRenderRuntime.h" #include "EnvironmentFlags.h" +#include #include #include #include @@ -223,7 +224,6 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { return App::CreateEditorWorkspaceShellRuntime( App::CreateEditorWorkspacePanelRuntimeSet( m_editorContext->GetSession(), - m_editorContext->GetCommandFocusService(), m_editorContext->GetProjectRuntime(), m_editorContext->GetSceneRuntime(), m_editorContext->GetColorPickerToolState(), @@ -235,7 +235,17 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { return m_editorContext->RequestOpenSceneAsset(scenePath); }), App::CreateEditorIconService(), - App::CreateEditorViewportRuntimeServices()); + App::CreateEditorViewportRuntimeServices(), + ::XCEngine::UI::Editor::BuildEditorShellShortcutManager( + m_editorContext->GetShellAsset()), + m_editorContext->GetRuntimeCoordinator(), + [this]() { + if (m_windowManager == nullptr) { + return; + } + + m_windowManager->RequestPrimaryWindowClose(); + }); }; m_windowManager = std::make_unique( *m_editorContext, @@ -254,13 +264,6 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { m_editorContext->GetSceneRuntime()); }); - m_editorContext->SetExitRequestHandler([this]() { - if (m_windowManager == nullptr) { - return; - } - - m_windowManager->RequestPrimaryWindowClose(); - }); m_editorContext->SetReadyStatus(); App::EditorWindowCreateParams createParams = {}; @@ -422,6 +425,9 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) { break; } + if (m_editorContext != nullptr) { + m_editorContext->TickEditorRuntime(); + } m_windowManager->RenderAllWindows(); if (m_smokeTestEnabled && !m_smokeTestCloseRequested) { ++m_smokeTestRenderedFrameCount; diff --git a/editor/app/Composition/EditorContext.cpp b/editor/app/Composition/EditorContext.cpp index b412ddde..77f708c9 100644 --- a/editor/app/Composition/EditorContext.cpp +++ b/editor/app/Composition/EditorContext.cpp @@ -12,7 +12,6 @@ namespace XCEngine::UI::Editor::App { namespace { -using ::XCEngine::UI::Editor::BuildEditorShellShortcutManager; using ::XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel; using ::XCEngine::UI::Editor::AppendUIEditorRuntimeTrace; @@ -65,7 +64,6 @@ bool EditorContext::Initialize( m_session = {}; m_session.workspaceRoot = runtimePaths.workspaceRoot; m_session.projectRoot = runtimePaths.projectRoot; - m_commandFocusService = {}; m_selectionService = {}; m_projectRuntime.Reset(); AppendUIEditorRuntimeTrace("startup", "EditorProjectRuntime::Initialize begin"); @@ -92,15 +90,7 @@ bool EditorContext::Initialize( ResetEditorColorPickerToolState(m_colorPickerToolState); ResetEditorUtilityWindowRequestState(m_utilityWindowRequestState); SyncSessionFromSelectionService(); - m_hostCommandBridge.BindSession(m_session); - m_hostCommandBridge.BindCommandFocusService(m_commandFocusService); - m_hostCommandBridge.BindRuntimeCommandOwner(&m_runtimeCoordinator); - SyncSessionFromCommandFocusService(); - m_shortcutManager = BuildEditorShellShortcutManager(m_shellAsset); - m_shortcutManager.SetHostCommandHandler(&m_hostCommandBridge); - m_shellServices = {}; - m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher(); - m_shellServices.shortcutManager = &m_shortcutManager; + m_textMeasurer = nullptr; m_lastStatus.clear(); m_lastMessage.clear(); SetReadyStatus(); @@ -111,7 +101,7 @@ bool EditorContext::Initialize( void EditorContext::AttachTextMeasurer( const UIEditorTextMeasurer& textMeasurer) { - m_shellServices.textMeasurer = &textMeasurer; + m_textMeasurer = &textMeasurer; } void EditorContext::SetSystemInteractionHost( @@ -119,28 +109,6 @@ void EditorContext::SetSystemInteractionHost( m_systemInteractionHost = systemInteractionHost; } -void EditorContext::BindEditCommandRoutes( - EditorEditCommandRoute* hierarchyRoute, - EditorEditCommandRoute* projectRoute, - EditorEditCommandRoute* sceneRoute, - EditorEditCommandRoute* inspectorRoute) { - m_hostCommandBridge.BindEditCommandRoutes( - hierarchyRoute, - projectRoute, - sceneRoute, - inspectorRoute); -} - -void EditorContext::SetExitRequestHandler(std::function handler) { - m_hostCommandBridge.SetExitRequestHandler(std::move(handler)); -} - -void EditorContext::SyncSessionFromWorkspace( - const UIEditorWorkspaceController& workspaceController) { - SyncEditorSessionFromWorkspace(m_session, workspaceController); - SyncSessionFromCommandFocusService(); -} - void EditorContext::TickEditorRuntime() { m_runtimeCoordinator.TickFrame(); } @@ -161,14 +129,6 @@ const EditorSession& EditorContext::GetSession() const { return m_session; } -EditorCommandFocusService& EditorContext::GetCommandFocusService() { - return m_commandFocusService; -} - -const EditorCommandFocusService& EditorContext::GetCommandFocusService() const { - return m_commandFocusService; -} - EditorProjectRuntime& EditorContext::GetProjectRuntime() { return m_projectRuntime; } @@ -223,11 +183,6 @@ void EditorContext::SyncSessionFromSelectionService() { m_session.selection = m_selectionService.GetSelection(); } -void EditorContext::SyncSessionFromCommandFocusService() { - m_session.activeRoute = m_commandFocusService.ResolveRoute( - ResolveEditorActionRoute(m_session.activePanelId)); -} - UIEditorWorkspaceController EditorContext::BuildWorkspaceController() const { return UIEditorWorkspaceController( m_shellAsset.panelRegistry, @@ -235,12 +190,8 @@ UIEditorWorkspaceController EditorContext::BuildWorkspaceController() const { m_shellAsset.workspaceSession); } -const UIEditorShellInteractionServices& EditorContext::GetShellServices() const { - return m_shellServices; -} - const UIEditorTextMeasurer* EditorContext::GetTextMeasurer() const { - return m_shellServices.textMeasurer; + return m_textMeasurer; } bool EditorContext::RequestOpenSceneAsset(const std::filesystem::path& scenePath) { diff --git a/editor/app/Composition/EditorContext.h b/editor/app/Composition/EditorContext.h index add8fc5c..771ecb1f 100644 --- a/editor/app/Composition/EditorContext.h +++ b/editor/app/Composition/EditorContext.h @@ -7,15 +7,11 @@ #include "Runtime/EditorRuntimeCoordinator.h" #include "UtilityWindows/EditorUtilityWindowRuntime.h" -#include "Commands/EditorHostCommandBridge.h" #include "State/EditorColorPickerToolState.h" -#include "State/EditorCommandFocusService.h" #include "State/EditorSelectionService.h" #include "State/EditorSession.h" #include "State/EditorUtilityWindowRequestState.h" -#include #include -#include #include #include @@ -41,15 +37,7 @@ public: void AttachTextMeasurer(const UIEditorTextMeasurer& textMeasurer) override; void SetSystemInteractionHost( ::XCEngine::UI::Editor::System::SystemInteractionService* systemInteractionHost); - void BindEditCommandRoutes( - EditorEditCommandRoute* hierarchyRoute, - EditorEditCommandRoute* projectRoute, - EditorEditCommandRoute* sceneRoute, - EditorEditCommandRoute* inspectorRoute = nullptr) override; - void SetExitRequestHandler(std::function handler); const EditorSession& GetSession() const; - EditorCommandFocusService& GetCommandFocusService(); - const EditorCommandFocusService& GetCommandFocusService() const; EditorProjectRuntime& GetProjectRuntime(); const EditorProjectRuntime& GetProjectRuntime() const; EditorSceneRuntime& GetSceneRuntime(); @@ -59,9 +47,7 @@ public: void RequestOpenUtilityWindow(EditorUtilityWindowKind kind); const UIEditorTextMeasurer* GetTextMeasurer() const override; bool RequestOpenSceneAsset(const std::filesystem::path& scenePath); - void SyncSessionFromWorkspace( - const UIEditorWorkspaceController& workspaceController) override; - void TickEditorRuntime() override; + void TickEditorRuntime(); bool IsValid() const override; const std::string& GetValidationMessage() const override; @@ -72,10 +58,8 @@ public: void SetSelection(EditorSelectionState selection); void ClearSelection(); void SyncSessionFromSelectionService(); - void SyncSessionFromCommandFocusService() override; UIEditorWorkspaceController BuildWorkspaceController() const; - const UIEditorShellInteractionServices& GetShellServices() const override; UIEditorShellInteractionDefinition BuildShellDefinition( const UIEditorWorkspaceController& workspaceController, @@ -102,17 +86,14 @@ private: EditorShellAsset m_shellAsset = {}; EditorShellAssetValidationResult m_shellValidation = {}; - UIEditorShortcutManager m_shortcutManager = {}; - UIEditorShellInteractionServices m_shellServices = {}; EditorSession m_session = {}; - EditorCommandFocusService m_commandFocusService = {}; EditorSelectionService m_selectionService = {}; EditorProjectRuntime m_projectRuntime = {}; EditorSceneRuntime m_sceneRuntime = {}; EditorRuntimeCoordinator m_runtimeCoordinator = {}; EditorColorPickerToolState m_colorPickerToolState = {}; EditorUtilityWindowRequestState m_utilityWindowRequestState = {}; - EditorHostCommandBridge m_hostCommandBridge = {}; + const UIEditorTextMeasurer* m_textMeasurer = nullptr; ::XCEngine::UI::Editor::System::SystemInteractionService* m_systemInteractionHost = nullptr; bool m_valid = false; std::string m_validationMessage = {}; diff --git a/editor/app/Composition/EditorShellHostedPanelCoordinator.cpp b/editor/app/Composition/EditorShellHostedPanelCoordinator.cpp index ff51f970..3750ef87 100644 --- a/editor/app/Composition/EditorShellHostedPanelCoordinator.cpp +++ b/editor/app/Composition/EditorShellHostedPanelCoordinator.cpp @@ -29,7 +29,6 @@ void EditorShellHostedPanelCoordinator::Update( context.workspacePanels.UpdatePhase( updateContext, EditorWorkspacePanelUpdatePhase::Main); - context.frameServices.SyncSessionFromCommandFocusService(); context.workspacePanels.UpdatePhase( updateContext, EditorWorkspacePanelUpdatePhase::AfterCommandFocusSync); diff --git a/editor/app/Composition/EditorShellRuntime.cpp b/editor/app/Composition/EditorShellRuntime.cpp index 5414c6cf..d9a929e3 100644 --- a/editor/app/Composition/EditorShellRuntime.cpp +++ b/editor/app/Composition/EditorShellRuntime.cpp @@ -12,10 +12,20 @@ namespace XCEngine::UI::Editor::App { EditorShellRuntime::EditorShellRuntime( EditorWorkspacePanelRuntimeSet workspacePanels, std::unique_ptr iconService, - std::unique_ptr viewportRuntimeServices) + std::unique_ptr viewportRuntimeServices, + UIEditorShortcutManager shortcutManager, + EditorHostCommandBridge::RuntimeCommandOwner& runtimeCommandOwner, + std::function requestExit) : m_iconService(std::move(iconService)) , m_viewportRuntimeServices(std::move(viewportRuntimeServices)) - , m_workspacePanels(std::move(workspacePanels)) {} + , m_workspacePanels(std::move(workspacePanels)) + , m_shortcutManager(std::move(shortcutManager)) { + m_hostCommandBridge.BindRuntimeCommandOwner(&runtimeCommandOwner); + m_hostCommandBridge.SetExitRequestHandler(std::move(requestExit)); + m_shortcutManager.SetHostCommandHandler(&m_hostCommandBridge); + m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher(); + m_shellServices.shortcutManager = &m_shortcutManager; +} void EditorShellRuntime::Initialize( const EditorRuntimePaths& runtimePaths, @@ -37,6 +47,7 @@ void EditorShellRuntime::Initialize( sceneViewportEngineBridge, gameViewportEngineBridge, shaderProvider); + m_shellServices.textMeasurer = &textMeasurer; m_workspacePanels.Initialize( EditorWorkspacePanelInitializationContext{ .runtimePaths = runtimePaths, @@ -71,6 +82,7 @@ void EditorShellRuntime::Shutdown() { m_traceEntries.clear(); m_workspacePanels.Shutdown(EditorWorkspacePanelShutdownContext{}); m_workspacePanels = {}; + m_shellServices.textMeasurer = nullptr; if (m_iconService != nullptr) { m_iconService->Shutdown(); } @@ -165,11 +177,17 @@ bool EditorShellRuntime::HasInteractiveCapture() const { std::unique_ptr CreateEditorWorkspaceShellRuntime( EditorWorkspacePanelRuntimeSet workspacePanels, std::unique_ptr iconService, - std::unique_ptr viewportRuntimeServices) { + std::unique_ptr viewportRuntimeServices, + UIEditorShortcutManager shortcutManager, + EditorHostCommandBridge::RuntimeCommandOwner& runtimeCommandOwner, + std::function requestExit) { return std::make_unique( std::move(workspacePanels), std::move(iconService), - std::move(viewportRuntimeServices)); + std::move(viewportRuntimeServices), + std::move(shortcutManager), + runtimeCommandOwner, + std::move(requestExit)); } } // namespace XCEngine::UI::Editor::App @@ -219,6 +237,14 @@ void EditorShellRuntime::Update( return; } + m_hostCommandBridge.BindCommandFocusService( + m_workspacePanels.GetCommandFocusService()); + m_hostCommandBridge.BindEditCommandRoutes( + m_workspacePanels.FindCommandRoute(EditorActionRoute::Hierarchy), + m_workspacePanels.FindCommandRoute(EditorActionRoute::Project), + m_workspacePanels.FindCommandRoute(EditorActionRoute::Scene), + m_workspacePanels.FindCommandRoute(EditorActionRoute::Inspector)); + const auto buildDefinition = [&]() { return m_sessionCoordinator.PrepareShellDefinition( EditorShellSessionCoordinatorContext{ @@ -237,7 +263,7 @@ void EditorShellRuntime::Update( .workspaceController = workspaceController, .bounds = bounds, .inputEvents = inputEvents, - .shellServices = frameServices.GetShellServices(), + .shellServices = m_shellServices, .buildDefinition = buildDefinition, .hostedContentCaptureActive = HasHostedContentCapture(), .useDetachedTitleBarTabStrip = useDetachedTitleBarTabStrip, @@ -258,7 +284,6 @@ void EditorShellRuntime::Update( }); m_traceEntries = frameServices.SyncWorkspacePanelFrameEvents( m_workspacePanels.CollectFrameEvents()); - frameServices.TickEditorRuntime(); } } // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Composition/EditorShellRuntime.h b/editor/app/Composition/EditorShellRuntime.h index d75cf726..bc9967d3 100644 --- a/editor/app/Composition/EditorShellRuntime.h +++ b/editor/app/Composition/EditorShellRuntime.h @@ -5,10 +5,12 @@ #include "EditorShellInteractionEngine.h" #include "EditorShellSessionCoordinator.h" #include "Assets/EditorIconService.h" +#include "Commands/EditorHostCommandBridge.h" #include "Viewport/EditorViewportRuntimeServices.h" #include "Windowing/EditorWorkspaceShellRuntime.h" #include "WorkspacePanels/EditorWorkspacePanelRuntime.h" +#include #include #include #include @@ -18,6 +20,7 @@ #include #include +#include #include #include #include @@ -48,7 +51,10 @@ public: EditorShellRuntime( EditorWorkspacePanelRuntimeSet workspacePanels, std::unique_ptr iconService, - std::unique_ptr viewportRuntimeServices); + std::unique_ptr viewportRuntimeServices, + UIEditorShortcutManager shortcutManager, + EditorHostCommandBridge::RuntimeCommandOwner& runtimeCommandOwner, + std::function requestExit); void Initialize( const EditorRuntimePaths& runtimePaths, @@ -104,6 +110,9 @@ private: std::unique_ptr m_iconService = {}; std::unique_ptr m_viewportRuntimeServices = {}; EditorWorkspacePanelRuntimeSet m_workspacePanels = {}; + UIEditorShortcutManager m_shortcutManager = {}; + EditorHostCommandBridge m_hostCommandBridge = {}; + UIEditorShellInteractionServices m_shellServices = {}; UIEditorShellInteractionState m_shellInteractionState = {}; UIEditorShellInteractionFrame m_shellFrame = {}; std::vector m_traceEntries = {}; @@ -117,7 +126,10 @@ private: std::unique_ptr CreateEditorWorkspaceShellRuntime( EditorWorkspacePanelRuntimeSet workspacePanels, std::unique_ptr iconService, - std::unique_ptr viewportRuntimeServices); + std::unique_ptr viewportRuntimeServices, + UIEditorShortcutManager shortcutManager, + EditorHostCommandBridge::RuntimeCommandOwner& runtimeCommandOwner, + std::function requestExit); } // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Composition/EditorShellSessionCoordinator.cpp b/editor/app/Composition/EditorShellSessionCoordinator.cpp index 52dc8221..efcb8529 100644 --- a/editor/app/Composition/EditorShellSessionCoordinator.cpp +++ b/editor/app/Composition/EditorShellSessionCoordinator.cpp @@ -7,12 +7,6 @@ namespace XCEngine::UI::Editor::App { UIEditorShellInteractionDefinition EditorShellSessionCoordinator::PrepareShellDefinition( const EditorShellSessionCoordinatorContext& context) const { context.workspacePanels.PrepareForShellDefinition(context.workspaceController); - context.frameServices.BindEditCommandRoutes( - context.workspacePanels.FindCommandRoute(EditorActionRoute::Hierarchy), - context.workspacePanels.FindCommandRoute(EditorActionRoute::Project), - context.workspacePanels.FindCommandRoute(EditorActionRoute::Scene), - context.workspacePanels.FindCommandRoute(EditorActionRoute::Inspector)); - context.frameServices.SyncSessionFromWorkspace(context.workspaceController); return context.frameServices.BuildShellDefinition( context.workspaceController, context.captureText, @@ -23,7 +17,7 @@ void EditorShellSessionCoordinator::FinalizeFrame( EditorFrameServices& frameServices, UIEditorWorkspaceController& workspaceController, const UIEditorShellInteractionResult& shellResult) const { - frameServices.SyncSessionFromWorkspace(workspaceController); + (void)workspaceController; frameServices.UpdateStatusFromShellResult(workspaceController, shellResult); } diff --git a/editor/app/Core/Commands/EditorHostCommandBridge.cpp b/editor/app/Core/Commands/EditorHostCommandBridge.cpp index a794b67f..1db2248f 100644 --- a/editor/app/Core/Commands/EditorHostCommandBridge.cpp +++ b/editor/app/Core/Commands/EditorHostCommandBridge.cpp @@ -5,10 +5,6 @@ namespace XCEngine::UI::Editor::App { -void EditorHostCommandBridge::BindSession(EditorSession& session) { - m_session = &session; -} - void EditorHostCommandBridge::BindCommandFocusService( const EditorCommandFocusService& commandFocusService) { m_commandFocusService = &commandFocusService; @@ -35,7 +31,8 @@ void EditorHostCommandBridge::SetExitRequestHandler(std::function handle } UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateHostCommand( - std::string_view commandId) const { + std::string_view commandId, + const UIEditorWorkspaceController& controller) const { if (commandId == "file.exit") { return BuildExecutableResult("Exit editor."); } @@ -57,18 +54,19 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateHostCommand } if (commandId.rfind("edit.", 0u) == 0u) { - return EvaluateEditCommand(commandId); + return EvaluateEditCommand(commandId, controller); } if (commandId.rfind("assets.", 0u) == 0u) { return EvaluateAssetCommand(commandId); } - return EvaluateUnsupportedHostCommand(commandId); + return EvaluateUnsupportedHostCommand(commandId, controller); } UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchHostCommand( - std::string_view commandId) { + std::string_view commandId, + UIEditorWorkspaceController& controller) { UIEditorHostCommandDispatchResult result = {}; if (commandId == "file.exit") { @@ -81,7 +79,7 @@ UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchHostCommand( } if (commandId == "help.about") { - result.message = EvaluateHostCommand(commandId).message; + result.message = EvaluateHostCommand(commandId, controller).message; return result; } @@ -89,7 +87,7 @@ UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchHostCommand( if (m_runtimeCommandOwner != nullptr) { return m_runtimeCommandOwner->DispatchFileCommand(commandId); } - result.message = EvaluateHostCommand(commandId).message; + result.message = EvaluateHostCommand(commandId, controller).message; return result; } @@ -97,7 +95,7 @@ UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchHostCommand( if (m_runtimeCommandOwner != nullptr) { return m_runtimeCommandOwner->DispatchRunCommand(commandId); } - result.message = EvaluateHostCommand(commandId).message; + result.message = EvaluateHostCommand(commandId, controller).message; return result; } @@ -105,19 +103,19 @@ UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchHostCommand( if (m_runtimeCommandOwner != nullptr) { return m_runtimeCommandOwner->DispatchScriptCommand(commandId); } - result.message = EvaluateHostCommand(commandId).message; + result.message = EvaluateHostCommand(commandId, controller).message; return result; } if (commandId.rfind("edit.", 0u) == 0u) { - return DispatchEditCommand(commandId); + return DispatchEditCommand(commandId, controller); } if (commandId.rfind("assets.", 0u) == 0u) { return DispatchAssetCommand(commandId); } - result.message = EvaluateHostCommand(commandId).message; + result.message = EvaluateHostCommand(commandId, controller).message; return result; } @@ -181,12 +179,9 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateScriptComma } UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateEditCommand( - std::string_view commandId) const { - if (m_session == nullptr) { - return BuildDisabledResult("Editor session is unavailable."); - } - - const EditorActionRoute route = ResolveActiveEditRoute(); + std::string_view commandId, + const UIEditorWorkspaceController& controller) const { + const EditorActionRoute route = ResolveActiveEditRoute(controller); if (route == EditorActionRoute::None) { return BuildDisabledResult("No active edit route."); } @@ -216,15 +211,18 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateEditCommand } UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchEditCommand( - std::string_view commandId) { + std::string_view commandId, + const UIEditorWorkspaceController& controller) { UIEditorHostCommandDispatchResult result = {}; - const UIEditorHostCommandEvaluationResult evaluation = EvaluateEditCommand(commandId); + const UIEditorHostCommandEvaluationResult evaluation = + EvaluateEditCommand(commandId, controller); if (!evaluation.executable) { result.message = evaluation.message; return result; } - EditorEditCommandRoute* route = ResolveEditCommandRoute(ResolveActiveEditRoute()); + EditorEditCommandRoute* route = + ResolveEditCommandRoute(ResolveActiveEditRoute(controller)); if (route == nullptr) { result.message = "Edit command route is unavailable."; return result; @@ -251,7 +249,8 @@ UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchAssetCommand( } UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateUnsupportedHostCommand( - std::string_view commandId) const { + std::string_view commandId, + const UIEditorWorkspaceController& controller) const { if (commandId.rfind("file.", 0u) == 0u) { return EvaluateFileCommand(commandId); } @@ -268,23 +267,19 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateUnsupported return EvaluateScriptCommand(commandId); } + (void)controller; return BuildDisabledResult("Host command has no owner in the current shell."); } -EditorActionRoute EditorHostCommandBridge::ResolveActiveEditRoute() const { - if (m_session == nullptr) { - return EditorActionRoute::None; - } - +EditorActionRoute EditorHostCommandBridge::ResolveActiveEditRoute( + const UIEditorWorkspaceController& controller) const { const EditorActionRoute fallbackRoute = - ResolveEditorActionRoute(m_session->activePanelId); + ResolveEditorActionRoute(controller.GetWorkspace().activePanelId); if (m_commandFocusService != nullptr) { return m_commandFocusService->ResolveRoute(fallbackRoute); } - return m_session->activeRoute != EditorActionRoute::None - ? m_session->activeRoute - : fallbackRoute; + return fallbackRoute; } EditorEditCommandRoute* EditorHostCommandBridge::ResolveEditCommandRoute( diff --git a/editor/app/Core/Commands/EditorHostCommandBridge.h b/editor/app/Core/Commands/EditorHostCommandBridge.h index 7bbc0801..ca00002f 100644 --- a/editor/app/Core/Commands/EditorHostCommandBridge.h +++ b/editor/app/Core/Commands/EditorHostCommandBridge.h @@ -2,7 +2,6 @@ #include "Commands/EditorEditCommandRoute.h" #include "State/EditorCommandFocusService.h" -#include "State/EditorSession.h" #include @@ -31,7 +30,6 @@ public: std::string_view commandId) = 0; }; - void BindSession(EditorSession& session); void BindCommandFocusService(const EditorCommandFocusService& commandFocusService); void BindRuntimeCommandOwner(RuntimeCommandOwner* runtimeCommandOwner); void BindEditCommandRoutes( @@ -42,9 +40,11 @@ public: void SetExitRequestHandler(std::function handler); UIEditorHostCommandEvaluationResult EvaluateHostCommand( - std::string_view commandId) const override; + std::string_view commandId, + const UIEditorWorkspaceController& controller) const override; UIEditorHostCommandDispatchResult DispatchHostCommand( - std::string_view commandId) override; + std::string_view commandId, + UIEditorWorkspaceController& controller) override; private: UIEditorHostCommandEvaluationResult BuildDisabledResult( @@ -60,17 +60,20 @@ private: UIEditorHostCommandEvaluationResult EvaluateScriptCommand( std::string_view commandId) const; UIEditorHostCommandEvaluationResult EvaluateEditCommand( - std::string_view commandId) const; + std::string_view commandId, + const UIEditorWorkspaceController& controller) const; UIEditorHostCommandDispatchResult DispatchAssetCommand( std::string_view commandId); UIEditorHostCommandDispatchResult DispatchEditCommand( - std::string_view commandId); + std::string_view commandId, + const UIEditorWorkspaceController& controller); UIEditorHostCommandEvaluationResult EvaluateUnsupportedHostCommand( - std::string_view commandId) const; - EditorActionRoute ResolveActiveEditRoute() const; + std::string_view commandId, + const UIEditorWorkspaceController& controller) const; + EditorActionRoute ResolveActiveEditRoute( + const UIEditorWorkspaceController& controller) const; EditorEditCommandRoute* ResolveEditCommandRoute(EditorActionRoute route) const; - EditorSession* m_session = nullptr; const EditorCommandFocusService* m_commandFocusService = nullptr; EditorEditCommandRoute* m_hierarchyRoute = nullptr; EditorEditCommandRoute* m_projectRoute = nullptr; diff --git a/editor/app/Core/Scene/EditorSceneBackend.h b/editor/app/Core/Scene/EditorSceneBackend.h index 7167b3c0..dcce0aad 100644 --- a/editor/app/Core/Scene/EditorSceneBackend.h +++ b/editor/app/Core/Scene/EditorSceneBackend.h @@ -3,7 +3,10 @@ #include #include #include +#include #include +#include +#include #include #include @@ -276,6 +279,18 @@ public: [[nodiscard]] virtual bool GetReceiveShadows() const = 0; }; +class EditorSceneScriptComponentView : public EditorSceneComponentView { +public: + [[nodiscard]] virtual std::uint64_t GetScriptComponentUUID() const = 0; + [[nodiscard]] virtual bool HasScriptClass() const = 0; + [[nodiscard]] virtual std::string GetAssemblyName() const = 0; + [[nodiscard]] virtual std::string GetNamespaceName() const = 0; + [[nodiscard]] virtual std::string GetClassName() const = 0; + [[nodiscard]] virtual std::string GetFullClassName() const = 0; + [[nodiscard]] virtual bool TryGetFieldModel( + ::XCEngine::Scripting::ScriptFieldModel& outModel) const = 0; +}; + struct EditorSceneComponentDescriptor { std::string componentId = {}; std::string typeName = {}; @@ -295,8 +310,14 @@ using EditorSceneComponentMutationValue = std::variant< std::int32_t, std::uint32_t, float, + double, + std::uint64_t, std::string, + ::XCEngine::Math::Vector2, ::XCEngine::Math::Vector3, + ::XCEngine::Math::Vector4, + ::XCEngine::Scripting::GameObjectReference, + ::XCEngine::Scripting::ComponentReference, ::XCEngine::Math::Color, EditorSceneCameraProjectionType, EditorSceneLightType, diff --git a/editor/app/Core/State/EditorSession.cpp b/editor/app/Core/State/EditorSession.cpp index 83c4ea9f..e72cf3df 100644 --- a/editor/app/Core/State/EditorSession.cpp +++ b/editor/app/Core/State/EditorSession.cpp @@ -60,10 +60,4 @@ EditorActionRoute ResolveEditorActionRoute(std::string_view panelId) { return EditorActionRoute::None; } -void SyncEditorSessionFromWorkspace( - EditorSession& session, - const UIEditorWorkspaceController& controller) { - session.activePanelId = controller.GetWorkspace().activePanelId; -} - } // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Core/State/EditorSession.h b/editor/app/Core/State/EditorSession.h index c4e579da..f308224c 100644 --- a/editor/app/Core/State/EditorSession.h +++ b/editor/app/Core/State/EditorSession.h @@ -55,9 +55,7 @@ struct EditorSession { std::filesystem::path projectRoot = {}; std::filesystem::path currentScenePath = {}; std::string currentSceneName = {}; - std::string activePanelId = {}; EditorRuntimeMode runtimeMode = EditorRuntimeMode::Edit; - EditorActionRoute activeRoute = EditorActionRoute::None; EditorSelectionState selection = {}; std::vector consoleEntries = {}; bool sceneDocumentDirty = false; @@ -69,8 +67,4 @@ std::string_view GetEditorSelectionKindName(EditorSelectionKind kind); EditorActionRoute ResolveEditorActionRoute(std::string_view panelId); -void SyncEditorSessionFromWorkspace( - EditorSession& session, - const UIEditorWorkspaceController& controller); - } // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Core/Windowing/EditorFrameServices.h b/editor/app/Core/Windowing/EditorFrameServices.h index d540d02f..6a290bb0 100644 --- a/editor/app/Core/Windowing/EditorFrameServices.h +++ b/editor/app/Core/Windowing/EditorFrameServices.h @@ -37,19 +37,7 @@ public: virtual void AttachTextMeasurer(const UIEditorTextMeasurer& textMeasurer) = 0; virtual bool IsValid() const = 0; virtual const std::string& GetValidationMessage() const = 0; - - virtual void BindEditCommandRoutes( - EditorEditCommandRoute* hierarchyRoute, - EditorEditCommandRoute* projectRoute, - EditorEditCommandRoute* sceneRoute, - EditorEditCommandRoute* inspectorRoute) = 0; virtual const UIEditorTextMeasurer* GetTextMeasurer() const = 0; - virtual void SyncSessionFromWorkspace( - const UIEditorWorkspaceController& workspaceController) = 0; - virtual void SyncSessionFromCommandFocusService() = 0; - virtual void TickEditorRuntime() = 0; - - virtual const UIEditorShellInteractionServices& GetShellServices() const = 0; virtual UIEditorShellInteractionDefinition BuildShellDefinition( const UIEditorWorkspaceController& workspaceController, std::string_view captureText, diff --git a/editor/app/Core/WorkspacePanels/EditorWorkspacePanelRuntime.cpp b/editor/app/Core/WorkspacePanels/EditorWorkspacePanelRuntime.cpp index 52df68ff..77622197 100644 --- a/editor/app/Core/WorkspacePanels/EditorWorkspacePanelRuntime.cpp +++ b/editor/app/Core/WorkspacePanels/EditorWorkspacePanelRuntime.cpp @@ -30,6 +30,15 @@ void EditorWorkspacePanelRuntimeSet::AddPanel( } } +EditorCommandFocusService& EditorWorkspacePanelRuntimeSet::GetCommandFocusService() { + return m_commandFocusService; +} + +const EditorCommandFocusService& +EditorWorkspacePanelRuntimeSet::GetCommandFocusService() const { + return m_commandFocusService; +} + void EditorWorkspacePanelRuntimeSet::Initialize( const EditorWorkspacePanelInitializationContext& context) { for (const std::unique_ptr& panel : m_panels) { @@ -45,6 +54,7 @@ void EditorWorkspacePanelRuntimeSet::Shutdown( } void EditorWorkspacePanelRuntimeSet::ResetInteractionState() { + m_commandFocusService.ClearFocus(); for (const std::unique_ptr& panel : m_panels) { panel->ResetInteractionState(); } diff --git a/editor/app/Core/WorkspacePanels/EditorWorkspacePanelRuntime.h b/editor/app/Core/WorkspacePanels/EditorWorkspacePanelRuntime.h index 5a54af70..aaf91c81 100644 --- a/editor/app/Core/WorkspacePanels/EditorWorkspacePanelRuntime.h +++ b/editor/app/Core/WorkspacePanels/EditorWorkspacePanelRuntime.h @@ -3,9 +3,10 @@ #include "Assets/EditorIconService.h" #include "Commands/EditorEditCommandRoute.h" #include "Environment/EditorRuntimePaths.h" -#include "Viewport/EditorViewportRuntimeServices.h" -#include "State/EditorSession.h" #include "State/EditorColorPickerToolState.h" +#include "State/EditorCommandFocusService.h" +#include "State/EditorSession.h" +#include "Viewport/EditorViewportRuntimeServices.h" #include #include @@ -121,6 +122,8 @@ public: EditorWorkspacePanelRuntimeSet& operator=(EditorWorkspacePanelRuntimeSet&&) noexcept = default; void AddPanel(std::unique_ptr panel); + EditorCommandFocusService& GetCommandFocusService(); + const EditorCommandFocusService& GetCommandFocusService() const; void Initialize(const EditorWorkspacePanelInitializationContext& context); void Shutdown(const EditorWorkspacePanelShutdownContext& context); void ResetInteractionState(); @@ -139,6 +142,7 @@ private: std::vector BuildUpdateOrder( EditorWorkspacePanelUpdatePhase phase) const; + EditorCommandFocusService m_commandFocusService = {}; std::vector> m_panels = {}; }; diff --git a/editor/app/Features/EditorWorkspacePanelRegistry.cpp b/editor/app/Features/EditorWorkspacePanelRegistry.cpp index b3258097..4570f957 100644 --- a/editor/app/Features/EditorWorkspacePanelRegistry.cpp +++ b/editor/app/Features/EditorWorkspacePanelRegistry.cpp @@ -613,7 +613,6 @@ std::unique_ptr CreateWorkspacePanelRuntime( EditorWorkspacePanelRuntimeSet CreateEditorWorkspacePanelRuntimeSet( const EditorSession& session, - EditorCommandFocusService& commandFocusService, EditorProjectRuntime& projectRuntime, EditorSceneRuntime& sceneRuntime, EditorColorPickerToolState& colorPickerToolState, @@ -621,6 +620,8 @@ EditorWorkspacePanelRuntimeSet CreateEditorWorkspacePanelRuntimeSet( RequestOpenUtilityWindowCallback requestOpenUtilityWindow, RequestOpenSceneAssetCallback requestOpenSceneAsset) { EditorWorkspacePanelRuntimeSet panels = {}; + EditorCommandFocusService& commandFocusService = + panels.GetCommandFocusService(); for (const EditorProductPanelDescriptor& panel : GetEditorProductPanels()) { if (std::unique_ptr runtime = CreateWorkspacePanelRuntime( diff --git a/editor/app/Features/EditorWorkspacePanelRegistry.h b/editor/app/Features/EditorWorkspacePanelRegistry.h index dc05511e..8d0e33e5 100644 --- a/editor/app/Features/EditorWorkspacePanelRegistry.h +++ b/editor/app/Features/EditorWorkspacePanelRegistry.h @@ -6,7 +6,6 @@ namespace XCEngine::UI::Editor::App { EditorWorkspacePanelRuntimeSet CreateEditorWorkspacePanelRuntimeSet( const EditorSession& session, - EditorCommandFocusService& commandFocusService, EditorProjectRuntime& projectRuntime, EditorSceneRuntime& sceneRuntime, EditorColorPickerToolState& colorPickerToolState, diff --git a/editor/app/Features/Inspector/Components/IInspectorComponentEditor.h b/editor/app/Features/Inspector/Components/IInspectorComponentEditor.h index b3438901..a86c4629 100644 --- a/editor/app/Features/Inspector/Components/IInspectorComponentEditor.h +++ b/editor/app/Features/Inspector/Components/IInspectorComponentEditor.h @@ -38,6 +38,16 @@ public: const InspectorComponentEditorContext& context, const Widgets::UIEditorPropertyGridField& field) const = 0; + virtual bool HandleFieldActivation( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const { + (void)sceneRuntime; + (void)context; + (void)field; + return false; + } + virtual bool ShowInAddComponentMenu() const { return true; } diff --git a/editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.cpp b/editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.cpp index da3c2c7b..f50c38a2 100644 --- a/editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.cpp +++ b/editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.cpp @@ -10,6 +10,7 @@ #include "Inspector/Components/MeshFilterInspectorComponentEditor.h" #include "Inspector/Components/MeshRendererInspectorComponentEditor.h" #include "Inspector/Components/RigidbodyInspectorComponentEditor.h" +#include "Inspector/Components/ScriptComponentInspectorComponentEditor.h" #include "Inspector/Components/SphereColliderInspectorComponentEditor.h" #include "Inspector/Components/TransformInspectorComponentEditor.h" #include "Inspector/Components/VolumeRendererInspectorComponentEditor.h" @@ -34,6 +35,7 @@ InspectorComponentEditorRegistry::InspectorComponentEditorRegistry() { RegisterEditor(std::make_unique()); RegisterEditor(std::make_unique()); RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); } void InspectorComponentEditorRegistry::RegisterEditor( diff --git a/editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h b/editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h index 743a9138..8b3f8e98 100644 --- a/editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h +++ b/editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h @@ -3,7 +3,9 @@ #include #include +#include #include +#include #include #include @@ -267,6 +269,54 @@ inline ::XCEngine::Math::Vector3 ToMathVector3( static_cast(field.vector3Value.values[2])); } +inline std::array ToInspectorVector2Values( + const ::XCEngine::Math::Vector2& value) { + return { + static_cast(value.x), + static_cast(value.y) + }; +} + +inline ::XCEngine::Math::Vector2 ToMathVector2( + const Widgets::UIEditorPropertyGridField& field) { + return ::XCEngine::Math::Vector2( + static_cast(field.vector2Value.values[0]), + static_cast(field.vector2Value.values[1])); +} + +inline Widgets::UIEditorPropertyGridField BuildInspectorVector2Field( + std::string fieldId, + std::string label, + const ::XCEngine::Math::Vector2& value, + double step, + float controlMinWidth = kDefaultInspectorFieldControlMinWidth, + bool integerMode = false, + double minValue = -1000000.0, + double maxValue = 1000000.0) { + Widgets::UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = Widgets::UIEditorPropertyGridFieldKind::Vector2; + field.vector2Value.values = ToInspectorVector2Values(value); + field.vector2Value.step = step; + field.vector2Value.minValue = minValue; + field.vector2Value.maxValue = maxValue; + field.vector2Value.integerMode = integerMode; + field.controlMinWidth = controlMinWidth; + return field; +} + +inline bool SyncInspectorVector2FieldValue( + Widgets::UIEditorPropertyGridField& field, + const ::XCEngine::Math::Vector2& value) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Vector2) { + return false; + } + + field.vector2Value.values = ToInspectorVector2Values(value); + return true; +} + inline Widgets::UIEditorPropertyGridField BuildInspectorVector3Field( std::string fieldId, std::string label, @@ -300,6 +350,58 @@ inline bool SyncInspectorVector3FieldValue( return true; } +inline std::array ToInspectorVector4Values( + const ::XCEngine::Math::Vector4& value) { + return { + static_cast(value.x), + static_cast(value.y), + static_cast(value.z), + static_cast(value.w) + }; +} + +inline ::XCEngine::Math::Vector4 ToMathVector4( + const Widgets::UIEditorPropertyGridField& field) { + return ::XCEngine::Math::Vector4( + static_cast(field.vector4Value.values[0]), + static_cast(field.vector4Value.values[1]), + static_cast(field.vector4Value.values[2]), + static_cast(field.vector4Value.values[3])); +} + +inline Widgets::UIEditorPropertyGridField BuildInspectorVector4Field( + std::string fieldId, + std::string label, + const ::XCEngine::Math::Vector4& value, + double step, + float controlMinWidth = kDefaultInspectorFieldControlMinWidth, + bool integerMode = false, + double minValue = -1000000.0, + double maxValue = 1000000.0) { + Widgets::UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = Widgets::UIEditorPropertyGridFieldKind::Vector4; + field.vector4Value.values = ToInspectorVector4Values(value); + field.vector4Value.step = step; + field.vector4Value.minValue = minValue; + field.vector4Value.maxValue = maxValue; + field.vector4Value.integerMode = integerMode; + field.controlMinWidth = controlMinWidth; + return field; +} + +inline bool SyncInspectorVector4FieldValue( + Widgets::UIEditorPropertyGridField& field, + const ::XCEngine::Math::Vector4& value) { + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Vector4) { + return false; + } + + field.vector4Value.values = ToInspectorVector4Values(value); + return true; +} + inline void AppendInspectorStructureToken( std::string& signature, std::string_view token) { diff --git a/editor/app/Features/Inspector/Components/ScriptComponentInspectorComponentEditor.cpp b/editor/app/Features/Inspector/Components/ScriptComponentInspectorComponentEditor.cpp new file mode 100644 index 00000000..31dfe53c --- /dev/null +++ b/editor/app/Features/Inspector/Components/ScriptComponentInspectorComponentEditor.cpp @@ -0,0 +1,1037 @@ +#include "Inspector/Components/ScriptComponentInspectorComponentEditor.h" + +#include "Inspector/Components/InspectorComponentEditorUtils.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::Components::GameObject; +using ::XCEngine::Components::Scene; +using ::XCEngine::Components::SceneManager; +using ::XCEngine::Scripting::ComponentReference; +using ::XCEngine::Scripting::GameObjectReference; +using ::XCEngine::Scripting::ScriptClassDescriptor; +using ::XCEngine::Scripting::ScriptComponent; +using ::XCEngine::Scripting::ScriptEngine; +using ::XCEngine::Scripting::ScriptFieldClassStatus; +using ::XCEngine::Scripting::ScriptFieldIssue; +using ::XCEngine::Scripting::ScriptFieldModel; +using ::XCEngine::Scripting::ScriptFieldSnapshot; +using ::XCEngine::Scripting::ScriptFieldType; +using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridFieldKind; +using Widgets::UIEditorPropertyGridSection; + +constexpr std::string_view kScriptClassFieldSuffix = "script_class"; +constexpr std::string_view kRuntimeStatusFieldSuffix = "runtime_status"; +constexpr std::string_view kModelStatusFieldSuffix = "model_status"; +constexpr std::string_view kFieldsSectionSuffix = "fields"; +constexpr std::string_view kScriptClassPropertyPath = "script_class"; +constexpr std::string_view kScriptFieldPropertyPrefix = "script_field."; + +struct ScriptClassOption { + std::string label = {}; + std::string encodedValue = {}; +}; + +struct ScriptPresentationData { + std::vector classOptions = {}; + std::size_t selectedClassIndex = 0u; + bool classesLoaded = false; + bool fieldModelReady = false; + ScriptFieldModel fieldModel = {}; + std::string runtimeStatusText = {}; + std::string modelStatusText = {}; +}; + +struct ScriptReferenceFieldPresentation { + std::string encodedValue = {}; + std::string displayName = {}; + std::string statusText = {}; +}; + +std::string EncodeScriptClassSelection( + std::string_view assemblyName, + std::string_view namespaceName, + std::string_view className) { + if (className.empty()) { + return {}; + } + + std::string encoded = std::string(assemblyName); + encoded.push_back('|'); + encoded += namespaceName; + encoded.push_back('|'); + encoded += className; + return encoded; +} + +std::string BuildScriptClassDisplayName(const ScriptClassDescriptor& descriptor) { + const std::string fullName = descriptor.GetFullName(); + if (descriptor.assemblyName.empty() || descriptor.assemblyName == "GameScripts") { + return fullName; + } + + return fullName + " (" + descriptor.assemblyName + ")"; +} + +std::string BuildScriptClassDisplayName( + const EditorSceneScriptComponentView& view) { + if (!view.HasScriptClass()) { + return "None"; + } + + return BuildScriptClassDisplayName(ScriptClassDescriptor{ + view.GetAssemblyName(), + view.GetNamespaceName(), + view.GetClassName() + }); +} + +std::string ResolveRuntimeStatusText(bool classesLoaded) { + if (classesLoaded) { + return {}; + } + + const auto* nullRuntime = + dynamic_cast( + ScriptEngine::Get().GetRuntime()); + if (nullRuntime != nullptr) { + return "No script assemblies are currently loaded."; + } + + return "Failed to query the loaded script classes."; +} + +bool CanEditScriptField( + ScriptFieldClassStatus classStatus, + const ScriptFieldSnapshot& field) { + return classStatus != ScriptFieldClassStatus::Available || field.declaredInClass; +} + +bool SupportsEditableScriptFieldType(ScriptFieldType type) { + switch (type) { + case ScriptFieldType::Float: + case ScriptFieldType::Double: + case ScriptFieldType::Bool: + case ScriptFieldType::Int32: + case ScriptFieldType::UInt64: + case ScriptFieldType::String: + case ScriptFieldType::Vector2: + case ScriptFieldType::Vector3: + case ScriptFieldType::Vector4: + case ScriptFieldType::GameObject: + case ScriptFieldType::Component: + return true; + case ScriptFieldType::None: + default: + return false; + } +} + +std::string BuildScriptFieldIssueText(const ScriptFieldSnapshot& field) { + switch (field.issue) { + case ScriptFieldIssue::StoredOnly: + return "Stored override is not declared by the selected script."; + case ScriptFieldIssue::TypeMismatch: + return "Stored override type does not match the current script field type."; + case ScriptFieldIssue::None: + default: + return {}; + } +} + +std::string BuildScriptFieldReferenceText(const ScriptFieldSnapshot& field) { + if (field.metadata.type == ScriptFieldType::GameObject) { + const GameObjectReference* reference = + std::get_if(&field.value); + return reference == nullptr || reference->gameObjectUUID == 0u + ? std::string("None") + : std::string("GameObject #") + + std::to_string(reference->gameObjectUUID); + } + + if (field.metadata.type == ScriptFieldType::Component) { + const ComponentReference* reference = + std::get_if(&field.value); + return reference == nullptr || reference->gameObjectUUID == 0u + ? std::string("None") + : std::string("GameObject #") + + std::to_string(reference->gameObjectUUID) + + " / Script #" + + std::to_string(reference->scriptComponentUUID); + } + + return {}; +} + +std::string EncodeGameObjectReference(const GameObjectReference& reference) { + return reference.gameObjectUUID == 0u + ? std::string() + : std::to_string(reference.gameObjectUUID); +} + +bool DecodeGameObjectReference( + std::string_view encodedValue, + GameObjectReference& outReference) { + outReference = {}; + if (encodedValue.empty()) { + return true; + } + + const char* first = encodedValue.data(); + const char* last = encodedValue.data() + encodedValue.size(); + std::uint64_t gameObjectUUID = 0u; + const std::from_chars_result result = + std::from_chars(first, last, gameObjectUUID); + if (result.ec != std::errc() || result.ptr != last) { + return false; + } + + outReference.gameObjectUUID = gameObjectUUID; + return true; +} + +std::string EncodeComponentReference(const ComponentReference& reference) { + if (reference.gameObjectUUID == 0u || reference.scriptComponentUUID == 0u) { + return {}; + } + + return std::to_string(reference.gameObjectUUID) + + "|" + + std::to_string(reference.scriptComponentUUID); +} + +bool DecodeComponentReference( + std::string_view encodedValue, + ComponentReference& outReference) { + outReference = {}; + if (encodedValue.empty()) { + return true; + } + + const std::size_t separatorIndex = encodedValue.find('|'); + if (separatorIndex == std::string_view::npos) { + return false; + } + + const char* first = encodedValue.data(); + const char* middle = encodedValue.data() + separatorIndex; + const char* second = middle + 1u; + const char* last = encodedValue.data() + encodedValue.size(); + std::uint64_t gameObjectUUID = 0u; + std::uint64_t scriptComponentUUID = 0u; + const std::from_chars_result firstResult = + std::from_chars(first, middle, gameObjectUUID); + const std::from_chars_result secondResult = + std::from_chars(second, last, scriptComponentUUID); + if (firstResult.ec != std::errc() || + firstResult.ptr != middle || + secondResult.ec != std::errc() || + secondResult.ptr != last) { + return false; + } + + outReference.gameObjectUUID = gameObjectUUID; + outReference.scriptComponentUUID = scriptComponentUUID; + return true; +} + +const GameObject* FindGameObjectByUUIDRecursive( + const GameObject* gameObject, + std::uint64_t gameObjectUUID) { + if (gameObject == nullptr) { + return nullptr; + } + + if (gameObject->GetUUID() == gameObjectUUID) { + return gameObject; + } + + for (GameObject* child : gameObject->GetChildren()) { + if (const GameObject* found = + FindGameObjectByUUIDRecursive(child, gameObjectUUID); + found != nullptr) { + return found; + } + } + + return nullptr; +} + +const GameObject* FindGameObjectByUUID( + const Scene* scene, + std::uint64_t gameObjectUUID) { + if (scene == nullptr || gameObjectUUID == 0u) { + return nullptr; + } + + for (GameObject* root : scene->GetRootGameObjects()) { + if (const GameObject* found = + FindGameObjectByUUIDRecursive(root, gameObjectUUID); + found != nullptr) { + return found; + } + } + + return nullptr; +} + +const ScriptComponent* FindScriptComponentByUUIDRecursive( + const GameObject* gameObject, + std::uint64_t scriptComponentUUID) { + if (gameObject == nullptr) { + return nullptr; + } + + for (const ScriptComponent* component : + gameObject->GetComponents()) { + if (component != nullptr && + component->GetScriptComponentUUID() == scriptComponentUUID) { + return component; + } + } + + for (GameObject* child : gameObject->GetChildren()) { + if (const ScriptComponent* found = + FindScriptComponentByUUIDRecursive(child, scriptComponentUUID); + found != nullptr) { + return found; + } + } + + return nullptr; +} + +const ScriptComponent* FindScriptComponentByUUID( + const Scene* scene, + std::uint64_t scriptComponentUUID) { + if (scene == nullptr || scriptComponentUUID == 0u) { + return nullptr; + } + + for (GameObject* root : scene->GetRootGameObjects()) { + if (const ScriptComponent* found = + FindScriptComponentByUUIDRecursive(root, scriptComponentUUID); + found != nullptr) { + return found; + } + } + + return nullptr; +} + +UIEditorPropertyGridField BuildScriptReferenceFieldEditor( + std::string fieldId, + std::string label, + const ScriptReferenceFieldPresentation& presentation, + bool readOnly) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Asset; + field.readOnly = readOnly; + field.controlMinWidth = kDefaultInspectorFieldControlMinWidth; + field.assetValue.assetId = presentation.encodedValue; + field.assetValue.displayName = presentation.displayName; + field.assetValue.statusText = presentation.statusText; + field.assetValue.emptyText = "None"; + field.assetValue.showPickerButton = false; + field.assetValue.allowClear = !readOnly; + field.assetValue.showStatusBadge = !presentation.statusText.empty(); + return field; +} + +ScriptReferenceFieldPresentation BuildScriptReferenceFieldPresentation( + const ScriptFieldSnapshot& field) { + ScriptReferenceFieldPresentation presentation = {}; + const Scene* const scene = SceneManager::Get().GetActiveScene(); + + if (field.metadata.type == ScriptFieldType::GameObject) { + const GameObjectReference* reference = + std::get_if(&field.value); + if (reference == nullptr) { + presentation.displayName = BuildScriptFieldReferenceText(field); + presentation.statusText = "Invalid"; + return presentation; + } + + presentation.encodedValue = EncodeGameObjectReference(*reference); + if (reference->gameObjectUUID == 0u) { + return presentation; + } + + if (const GameObject* target = + FindGameObjectByUUID(scene, reference->gameObjectUUID); + target != nullptr) { + presentation.displayName = target->GetName(); + presentation.statusText = "GameObject"; + } else { + presentation.displayName = + "Missing GameObject #" + std::to_string(reference->gameObjectUUID); + presentation.statusText = "Missing"; + } + return presentation; + } + + if (field.metadata.type == ScriptFieldType::Component) { + const ComponentReference* reference = + std::get_if(&field.value); + if (reference == nullptr) { + presentation.displayName = BuildScriptFieldReferenceText(field); + presentation.statusText = "Invalid"; + return presentation; + } + + presentation.encodedValue = EncodeComponentReference(*reference); + if (reference->gameObjectUUID == 0u || + reference->scriptComponentUUID == 0u) { + return presentation; + } + + const GameObject* targetObject = + FindGameObjectByUUID(scene, reference->gameObjectUUID); + const ScriptComponent* targetComponent = + FindScriptComponentByUUID(scene, reference->scriptComponentUUID); + if (targetObject != nullptr && + targetComponent != nullptr && + targetComponent->GetGameObject() != nullptr && + targetComponent->GetGameObject()->GetUUID() == + reference->gameObjectUUID) { + presentation.displayName = targetObject->GetName(); + presentation.statusText = targetComponent->GetClassName().empty() + ? std::string("Script") + : targetComponent->GetClassName(); + } else if (targetObject != nullptr) { + presentation.displayName = targetObject->GetName(); + presentation.statusText = "Missing Script"; + } else { + presentation.displayName = + "Missing GameObject #" + std::to_string(reference->gameObjectUUID); + presentation.statusText = "Missing"; + } + return presentation; + } + + presentation.displayName = BuildScriptFieldReferenceText(field); + return presentation; +} + +std::string BuildScriptModelStatusText( + const ScriptFieldModel& fieldModel) { + switch (fieldModel.classStatus) { + case ScriptFieldClassStatus::Unassigned: + return "Assign a C# script to edit serialized fields."; + case ScriptFieldClassStatus::Missing: + return "Assigned script class is not available in the loaded script assemblies."; + case ScriptFieldClassStatus::Available: + default: + if (fieldModel.fields.empty()) { + return "Selected script exposes no supported serialized fields."; + } + return {}; + } +} + +const EditorSceneScriptComponentView* ResolveScriptView( + const InspectorComponentEditorContext& context) { + return ResolveInspectorComponent(context); +} + +std::optional BuildScriptPresentationData( + const InspectorComponentEditorContext& context) { + const EditorSceneScriptComponentView* view = ResolveScriptView(context); + if (view == nullptr) { + return std::nullopt; + } + + ScriptPresentationData data = {}; + data.classOptions.push_back(ScriptClassOption{ + .label = "None", + .encodedValue = {} + }); + + std::vector classes = {}; + data.classesLoaded = ScriptEngine::Get().TryGetAvailableScriptClasses(classes); + data.runtimeStatusText = ResolveRuntimeStatusText(data.classesLoaded); + + const ScriptClassDescriptor currentClass{ + view->GetAssemblyName(), + view->GetNamespaceName(), + view->GetClassName() + }; + + bool foundCurrentClass = !view->HasScriptClass(); + for (const ScriptClassDescriptor& descriptor : classes) { + const std::string encodedValue = EncodeScriptClassSelection( + descriptor.assemblyName, + descriptor.namespaceName, + descriptor.className); + if (!foundCurrentClass && descriptor == currentClass) { + data.selectedClassIndex = data.classOptions.size(); + foundCurrentClass = true; + } + + data.classOptions.push_back(ScriptClassOption{ + .label = BuildScriptClassDisplayName(descriptor), + .encodedValue = std::move(encodedValue) + }); + } + + if (!foundCurrentClass && view->HasScriptClass()) { + data.selectedClassIndex = data.classOptions.size(); + data.classOptions.push_back(ScriptClassOption{ + .label = BuildScriptClassDisplayName(*view) + " (Missing)", + .encodedValue = EncodeScriptClassSelection( + view->GetAssemblyName(), + view->GetNamespaceName(), + view->GetClassName()) + }); + } + + data.fieldModelReady = view->TryGetFieldModel(data.fieldModel); + data.modelStatusText = data.fieldModelReady + ? BuildScriptModelStatusText(data.fieldModel) + : std::string("Failed to query script field metadata."); + return data; +} + +std::string BuildFieldSuffix(std::string_view fieldName) { + std::string suffix = "field."; + suffix += fieldName; + return suffix; +} + +std::string BuildIssueFieldSuffix(std::string_view fieldName) { + std::string suffix = "field_issue."; + suffix += fieldName; + return suffix; +} + +bool TryParseFieldNameFromFieldId( + const InspectorComponentEditorContext& context, + std::string_view fieldId, + std::string& outFieldName) { + const std::string prefix = BuildInspectorComponentFieldId(context.componentId, "field."); + if (fieldId.size() <= prefix.size() || + fieldId.substr(0u, prefix.size()) != prefix) { + return false; + } + + outFieldName = std::string(fieldId.substr(prefix.size())); + return !outFieldName.empty(); +} + +const ScriptFieldSnapshot* FindScriptField( + const ScriptFieldModel& model, + std::string_view fieldName) { + for (const ScriptFieldSnapshot& field : model.fields) { + if (field.metadata.name == fieldName) { + return &field; + } + } + + return nullptr; +} + +UIEditorPropertyGridField BuildScriptFieldEditor( + std::string fieldId, + const ScriptFieldSnapshot& field, + bool readOnly) { + switch (field.metadata.type) { + case ScriptFieldType::Float: { + const float value = std::get(field.value); + UIEditorPropertyGridField result = BuildInspectorNumberField( + std::move(fieldId), + field.metadata.name, + static_cast(value), + 0.1, + -1000000.0, + 1000000.0, + false); + result.readOnly = readOnly; + return result; + } + case ScriptFieldType::Double: { + const double value = std::get(field.value); + UIEditorPropertyGridField result = BuildInspectorNumberField( + std::move(fieldId), + field.metadata.name, + value, + 0.1, + -1000000.0, + 1000000.0, + false); + result.readOnly = readOnly; + return result; + } + case ScriptFieldType::Bool: { + UIEditorPropertyGridField result = + BuildInspectorBoolField( + std::move(fieldId), + field.metadata.name, + std::get(field.value)); + result.readOnly = readOnly; + return result; + } + case ScriptFieldType::Int32: { + UIEditorPropertyGridField result = BuildInspectorNumberField( + std::move(fieldId), + field.metadata.name, + static_cast(std::get(field.value)), + 1.0, + -2147483648.0, + 2147483647.0, + true); + result.readOnly = readOnly; + return result; + } + case ScriptFieldType::UInt64: { + return BuildInspectorTextField( + std::move(fieldId), + field.metadata.name, + std::to_string(std::get(field.value)), + readOnly); + } + case ScriptFieldType::String: + return BuildInspectorTextField( + std::move(fieldId), + field.metadata.name, + std::get(field.value), + readOnly); + case ScriptFieldType::Vector2: { + UIEditorPropertyGridField result = BuildInspectorVector2Field( + std::move(fieldId), + field.metadata.name, + std::get<::XCEngine::Math::Vector2>(field.value), + 0.1); + result.readOnly = readOnly; + return result; + } + case ScriptFieldType::Vector3: { + UIEditorPropertyGridField result = BuildInspectorVector3Field( + std::move(fieldId), + field.metadata.name, + std::get<::XCEngine::Math::Vector3>(field.value), + 0.1); + result.readOnly = readOnly; + return result; + } + case ScriptFieldType::Vector4: { + UIEditorPropertyGridField result = BuildInspectorVector4Field( + std::move(fieldId), + field.metadata.name, + std::get<::XCEngine::Math::Vector4>(field.value), + 0.1); + result.readOnly = readOnly; + return result; + } + case ScriptFieldType::GameObject: + case ScriptFieldType::Component: + return BuildScriptReferenceFieldEditor( + std::move(fieldId), + field.metadata.name, + BuildScriptReferenceFieldPresentation(field), + readOnly); + case ScriptFieldType::None: + default: + return BuildInspectorReadOnlyTextField( + std::move(fieldId), + field.metadata.name, + "Unsupported"); + } +} + +const UIEditorPropertyGridField* FindFieldById( + const std::vector& sections, + std::string_view fieldId) { + for (const UIEditorPropertyGridSection& section : sections) { + for (const UIEditorPropertyGridField& field : section.fields) { + if (field.fieldId == fieldId) { + return &field; + } + } + } + + return nullptr; +} + +} // namespace + +std::string_view ScriptComponentInspectorComponentEditor::GetComponentTypeName() const { + return "ScriptComponent"; +} + +std::string_view ScriptComponentInspectorComponentEditor::GetDisplayName() const { + return "Script"; +} + +void ScriptComponentInspectorComponentEditor::BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const { + const std::optional data = + BuildScriptPresentationData(context); + if (!data.has_value()) { + return; + } + + UIEditorPropertyGridSection scriptSection = {}; + scriptSection.sectionId = + BuildInspectorComponentSectionId(context.componentId); + scriptSection.title = std::string(GetDisplayName()); + scriptSection.fields.push_back(BuildInspectorEnumField( + BuildInspectorComponentFieldId(context.componentId, kScriptClassFieldSuffix), + "Script", + [&data]() { + std::vector labels = {}; + labels.reserve(data->classOptions.size()); + for (const ScriptClassOption& option : data->classOptions) { + labels.push_back(option.label); + } + return labels; + }(), + data->selectedClassIndex)); + + if (!data->runtimeStatusText.empty()) { + scriptSection.fields.push_back(BuildInspectorReadOnlyTextField( + BuildInspectorComponentFieldId(context.componentId, kRuntimeStatusFieldSuffix), + "Runtime", + data->runtimeStatusText)); + } + if (!data->modelStatusText.empty()) { + scriptSection.fields.push_back(BuildInspectorReadOnlyTextField( + BuildInspectorComponentFieldId(context.componentId, kModelStatusFieldSuffix), + "Status", + data->modelStatusText)); + } + outSections.push_back(std::move(scriptSection)); + + if (!data->fieldModelReady || data->fieldModel.fields.empty()) { + return; + } + + UIEditorPropertyGridSection fieldsSection = {}; + fieldsSection.sectionId = + BuildInspectorComponentSectionId(context.componentId, kFieldsSectionSuffix); + fieldsSection.title = "Script Fields"; + for (const ScriptFieldSnapshot& field : data->fieldModel.fields) { + const bool readOnly = + !CanEditScriptField(data->fieldModel.classStatus, field) || + !SupportsEditableScriptFieldType(field.metadata.type); + fieldsSection.fields.push_back(BuildScriptFieldEditor( + BuildInspectorComponentFieldId( + context.componentId, + BuildFieldSuffix(field.metadata.name)), + field, + readOnly)); + + const std::string issueText = BuildScriptFieldIssueText(field); + if (!issueText.empty()) { + fieldsSection.fields.push_back(BuildInspectorReadOnlyTextField( + BuildInspectorComponentFieldId( + context.componentId, + BuildIssueFieldSuffix(field.metadata.name)), + field.metadata.name + " Status", + issueText)); + } + } + outSections.push_back(std::move(fieldsSection)); +} + +bool ScriptComponentInspectorComponentEditor::SyncFieldValue( + const InspectorComponentEditorContext& context, + UIEditorPropertyGridField& field) const { + std::vector sections = {}; + BuildSections(context, sections); + const UIEditorPropertyGridField* source = FindFieldById(sections, field.fieldId); + if (source == nullptr) { + return false; + } + + field = *source; + return true; +} + +bool ScriptComponentInspectorComponentEditor::ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const UIEditorPropertyGridField& field) const { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, kScriptClassFieldSuffix)) { + if (field.kind != UIEditorPropertyGridFieldKind::Enum) { + return false; + } + + const std::optional data = + BuildScriptPresentationData(context); + if (!data.has_value() || + field.enumValue.selectedIndex >= data->classOptions.size()) { + return false; + } + + return ApplyInspectorComponentMutation( + sceneRuntime, + context, + kScriptClassPropertyPath, + data->classOptions[field.enumValue.selectedIndex].encodedValue); + } + + std::string fieldName = {}; + if (!TryParseFieldNameFromFieldId(context, field.fieldId, fieldName)) { + return false; + } + + const std::optional data = + BuildScriptPresentationData(context); + if (!data.has_value() || !data->fieldModelReady) { + return false; + } + + const ScriptFieldSnapshot* scriptField = + FindScriptField(data->fieldModel, fieldName); + if (scriptField == nullptr || + !CanEditScriptField(data->fieldModel.classStatus, *scriptField) || + !SupportsEditableScriptFieldType(scriptField->metadata.type)) { + return false; + } + + const std::string propertyPath = + std::string(kScriptFieldPropertyPrefix) + fieldName; + switch (scriptField->metadata.type) { + case ScriptFieldType::Float: + return field.kind == UIEditorPropertyGridFieldKind::Number && + ApplyInspectorComponentMutation( + sceneRuntime, + context, + propertyPath, + static_cast(field.numberValue.value)); + case ScriptFieldType::Double: + return field.kind == UIEditorPropertyGridFieldKind::Number && + ApplyInspectorComponentMutation( + sceneRuntime, + context, + propertyPath, + field.numberValue.value); + case ScriptFieldType::Bool: + return field.kind == UIEditorPropertyGridFieldKind::Bool && + ApplyInspectorComponentMutation( + sceneRuntime, + context, + propertyPath, + field.boolValue); + case ScriptFieldType::Int32: + return field.kind == UIEditorPropertyGridFieldKind::Number && + ApplyInspectorComponentMutation( + sceneRuntime, + context, + propertyPath, + static_cast(field.numberValue.value)); + case ScriptFieldType::UInt64: { + if (field.kind != UIEditorPropertyGridFieldKind::Text) { + return false; + } + + std::uint64_t value = 0u; + const char* first = field.valueText.data(); + const char* last = field.valueText.data() + field.valueText.size(); + const std::from_chars_result result = + std::from_chars(first, last, value); + if (result.ec != std::errc() || result.ptr != last) { + return false; + } + + return ApplyInspectorComponentMutation( + sceneRuntime, + context, + propertyPath, + value); + } + case ScriptFieldType::String: + return field.kind == UIEditorPropertyGridFieldKind::Text && + ApplyInspectorComponentMutation( + sceneRuntime, + context, + propertyPath, + field.valueText); + case ScriptFieldType::Vector2: + return field.kind == UIEditorPropertyGridFieldKind::Vector2 && + ApplyInspectorComponentMutation( + sceneRuntime, + context, + propertyPath, + ToMathVector2(field)); + case ScriptFieldType::Vector3: + return field.kind == UIEditorPropertyGridFieldKind::Vector3 && + ApplyInspectorComponentMutation( + sceneRuntime, + context, + propertyPath, + ToMathVector3(field)); + case ScriptFieldType::Vector4: + return field.kind == UIEditorPropertyGridFieldKind::Vector4 && + ApplyInspectorComponentMutation( + sceneRuntime, + context, + propertyPath, + ToMathVector4(field)); + case ScriptFieldType::GameObject: { + if (field.kind != UIEditorPropertyGridFieldKind::Asset) { + return false; + } + + GameObjectReference reference = {}; + if (!DecodeGameObjectReference(field.assetValue.assetId, reference)) { + return false; + } + + return ApplyInspectorComponentMutation( + sceneRuntime, + context, + propertyPath, + reference); + } + case ScriptFieldType::Component: { + if (field.kind != UIEditorPropertyGridFieldKind::Asset) { + return false; + } + + ComponentReference reference = {}; + if (!DecodeComponentReference(field.assetValue.assetId, reference)) { + return false; + } + + return ApplyInspectorComponentMutation( + sceneRuntime, + context, + propertyPath, + reference); + } + case ScriptFieldType::None: + default: + return false; + } +} + +bool ScriptComponentInspectorComponentEditor::HandleFieldActivation( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const UIEditorPropertyGridField& field) const { + std::string fieldName = {}; + if (!TryParseFieldNameFromFieldId(context, field.fieldId, fieldName)) { + return false; + } + + const std::optional data = + BuildScriptPresentationData(context); + if (!data.has_value() || !data->fieldModelReady) { + return false; + } + + const ScriptFieldSnapshot* scriptField = + FindScriptField(data->fieldModel, fieldName); + if (scriptField == nullptr || + field.kind != UIEditorPropertyGridFieldKind::Asset) { + return false; + } + + std::uint64_t targetGameObjectUUID = 0u; + switch (scriptField->metadata.type) { + case ScriptFieldType::GameObject: { + GameObjectReference reference = {}; + if (!DecodeGameObjectReference(field.assetValue.assetId, reference)) { + return false; + } + targetGameObjectUUID = reference.gameObjectUUID; + break; + } + case ScriptFieldType::Component: { + ComponentReference reference = {}; + if (!DecodeComponentReference(field.assetValue.assetId, reference)) { + return false; + } + targetGameObjectUUID = reference.gameObjectUUID; + break; + } + case ScriptFieldType::Float: + case ScriptFieldType::Double: + case ScriptFieldType::Bool: + case ScriptFieldType::Int32: + case ScriptFieldType::UInt64: + case ScriptFieldType::String: + case ScriptFieldType::Vector2: + case ScriptFieldType::Vector3: + case ScriptFieldType::Vector4: + case ScriptFieldType::None: + default: + return false; + } + + const Scene* const scene = SceneManager::Get().GetActiveScene(); + const GameObject* const target = + FindGameObjectByUUID(scene, targetGameObjectUUID); + if (target == nullptr) { + return false; + } + + return sceneRuntime.SetSelection(target->GetID()); +} + +bool ScriptComponentInspectorComponentEditor::CanAddTo( + const EditorSceneObjectSnapshot* gameObject) const { + return CanAddMultipleInspectorComponentsToGameObject(gameObject); +} + +std::string_view ScriptComponentInspectorComponentEditor::GetAddDisabledReason( + const EditorSceneObjectSnapshot* gameObject) const { + return GetInvalidInspectorAddDisabledReason(gameObject); +} + +void ScriptComponentInspectorComponentEditor::AppendStructureSignature( + const InspectorComponentEditorContext& context, + std::string& signature) const { + const std::optional data = + BuildScriptPresentationData(context); + AppendInspectorStructureToken(signature, std::string_view(GetComponentTypeName())); + if (!data.has_value()) { + AppendInspectorStructureToken(signature, "missing-view"); + return; + } + + AppendInspectorStructureToken(signature, data->classesLoaded); + for (const ScriptClassOption& option : data->classOptions) { + AppendInspectorStructureToken(signature, option.encodedValue); + AppendInspectorStructureToken(signature, option.label); + } + + AppendInspectorStructureToken(signature, data->fieldModelReady); + AppendInspectorStructureToken(signature, data->fieldModel.classStatus == ScriptFieldClassStatus::Available); + AppendInspectorStructureToken(signature, data->fieldModel.classStatus == ScriptFieldClassStatus::Missing); + AppendInspectorStructureToken(signature, data->runtimeStatusText); + AppendInspectorStructureToken(signature, data->modelStatusText); + for (const ScriptFieldSnapshot& field : data->fieldModel.fields) { + AppendInspectorStructureToken(signature, field.metadata.name); + AppendInspectorStructureToken( + signature, + static_cast(field.metadata.type)); + AppendInspectorStructureToken(signature, field.declaredInClass); + AppendInspectorStructureToken( + signature, + CanEditScriptField(data->fieldModel.classStatus, field)); + AppendInspectorStructureToken(signature, SupportsEditableScriptFieldType(field.metadata.type)); + AppendInspectorStructureToken(signature, BuildScriptFieldIssueText(field)); + } +} + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Features/Inspector/Components/ScriptComponentInspectorComponentEditor.h b/editor/app/Features/Inspector/Components/ScriptComponentInspectorComponentEditor.h new file mode 100644 index 00000000..607cabc9 --- /dev/null +++ b/editor/app/Features/Inspector/Components/ScriptComponentInspectorComponentEditor.h @@ -0,0 +1,36 @@ +#pragma once + +#include "Inspector/Components/IInspectorComponentEditor.h" + +namespace XCEngine::UI::Editor::App { + +class ScriptComponentInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override; + std::string_view GetDisplayName() const override; + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override; + bool SyncFieldValue( + const InspectorComponentEditorContext& context, + Widgets::UIEditorPropertyGridField& field) const override; + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override; + bool HandleFieldActivation( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override; + + bool CanAddTo( + const EditorSceneObjectSnapshot* gameObject) const override; + std::string_view GetAddDisabledReason( + const EditorSceneObjectSnapshot* gameObject) const override; + void AppendStructureSignature( + const InspectorComponentEditorContext& context, + std::string& signature) const override; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Features/Inspector/InspectorPanel.cpp b/editor/app/Features/Inspector/InspectorPanel.cpp index 77e3f73e..01b216fb 100644 --- a/editor/app/Features/Inspector/InspectorPanel.cpp +++ b/editor/app/Features/Inspector/InspectorPanel.cpp @@ -611,6 +611,65 @@ void InspectorPanel::RequestColorPicker( context.RequestOpenUtilityWindow(EditorUtilityWindowKind::ColorPicker); } +bool InspectorPanel::HandleActivatedField(std::string_view fieldId) { + if (m_sceneRuntime == nullptr || + m_subject.kind != InspectorSubjectKind::SceneObject) { + return false; + } + + const auto location = + Widgets::FindUIEditorPropertyGridFieldLocation(m_presentation.sections, fieldId); + if (!location.IsValid() || + location.sectionIndex >= m_presentation.sections.size() || + location.fieldIndex >= m_presentation.sections[location.sectionIndex].fields.size()) { + return false; + } + + const Widgets::UIEditorPropertyGridField& field = + m_presentation.sections[location.sectionIndex].fields[location.fieldIndex]; + const InspectorPresentationComponentBinding* binding = nullptr; + for (const InspectorPresentationComponentBinding& candidate : + m_presentation.componentBindings) { + if (std::find( + candidate.fieldIds.begin(), + candidate.fieldIds.end(), + field.fieldId) != candidate.fieldIds.end()) { + binding = &candidate; + break; + } + } + if (binding == nullptr) { + return false; + } + + const IInspectorComponentEditor* editor = + InspectorComponentEditorRegistry::Get().FindEditor(binding->typeName); + if (editor == nullptr) { + return false; + } + + const std::vector descriptors = + m_sceneRuntime->GetSelectedComponents(); + InspectorComponentEditorContext context = {}; + context.gameObject = &m_subject.sceneObject.object; + context.componentId = binding->componentId; + context.typeName = binding->typeName; + context.displayName = binding->displayName; + context.removable = binding->removable; + for (const EditorSceneComponentDescriptor& descriptor : descriptors) { + if (descriptor.componentId == binding->componentId) { + context.component = descriptor.view.get(); + break; + } + } + + if (context.component == nullptr) { + return false; + } + + return editor->HandleFieldActivation(*m_sceneRuntime, context, field); +} + void InspectorPanel::ResetAddComponentButtonState() { m_addComponentButtonHovered = false; m_addComponentButtonPressed = false; @@ -804,6 +863,12 @@ void InspectorPanel::Update( RequestColorPicker(context, interactionFrame.result.requestedFieldId); } + if (interactionFrame.result.activateRequested && + !interactionFrame.result.requestedFieldId.empty() && + HandleActivatedField(interactionFrame.result.requestedFieldId)) { + return; + } + if (interactionFrame.result.fieldValueChanged && !interactionFrame.result.changedFieldId.empty()) { if (ApplyChangedField(interactionFrame.result.changedFieldId)) { diff --git a/editor/app/Features/Inspector/InspectorPanel.h b/editor/app/Features/Inspector/InspectorPanel.h index 36e0ff3b..8e834a20 100644 --- a/editor/app/Features/Inspector/InspectorPanel.h +++ b/editor/app/Features/Inspector/InspectorPanel.h @@ -86,6 +86,7 @@ private: void ForceResyncPresentation(InspectorPanelContext& context); bool ApplyColorPickerToolValue(InspectorPanelContext& context); void RequestColorPicker(InspectorPanelContext& context, std::string_view fieldId); + bool HandleActivatedField(std::string_view fieldId); void ResetAddComponentButtonState(); void UpdateAddComponentButton( InspectorPanelContext& context, diff --git a/editor/app/Services/Engine/EngineEditorServices.cpp b/editor/app/Services/Engine/EngineEditorServices.cpp index f87e19c9..0a3941d8 100644 --- a/editor/app/Services/Engine/EngineEditorServices.cpp +++ b/editor/app/Services/Engine/EngineEditorServices.cpp @@ -10,12 +10,40 @@ #include "Scene/EngineEditorSceneBackend.h" #include +#include #include +#include + namespace XCEngine::UI::Editor::App { namespace { +using DeviceBridgeKey = ::XCEngine::RHI::RHIDevice*; + +DeviceBridgeKey ResolveSceneViewportBridgeKey( + const ::XCEngine::Rendering::RenderContext& renderContext) { + return renderContext.device; +} + +DeviceBridgeKey ResolveSceneViewportBridgeKey( + const ::XCEngine::Rendering::CameraFramePlan& framePlan) { + return framePlan.request.context.device; +} + +DeviceBridgeKey ResolveGameViewportBridgeKey( + const std::vector<::XCEngine::Rendering::CameraStackFramePlan>& framePlans) { + for (const ::XCEngine::Rendering::CameraStackFramePlan& stackPlan : framePlans) { + if (const ::XCEngine::Rendering::CameraFramePlan* sortKeyPlan = + stackPlan.GetSortKeyPlan(); + sortKeyPlan != nullptr) { + return sortKeyPlan->request.context.device; + } + } + + return nullptr; +} + class EngineEditorServices final : public EditorSceneBackendFactory , public SceneViewportEngineBridge @@ -28,7 +56,14 @@ public: } void Shutdown() override { - m_sceneViewportBridge.Shutdown(); + for (auto& [device, bridge] : m_sceneViewportBridges) { + (void)device; + if (bridge != nullptr) { + bridge->Shutdown(); + } + } + m_sceneViewportBridges.clear(); + m_gameViewportBridges.clear(); ::XCEngine::Resources::ResourceManager::Get().Shutdown(); } @@ -49,7 +84,7 @@ public: const ::XCEngine::Rendering::RenderContext& renderContext, const ::XCEngine::Rendering::RenderSurface& surface, ::XCEngine::Rendering::CameraFramePlan& outFramePlan) override { - return m_sceneViewportBridge.BuildFramePlan( + return ResolveSceneViewportBridge(renderContext).BuildFramePlan( request, renderContext, surface, @@ -58,7 +93,11 @@ public: bool RenderSceneViewportFramePlan( const ::XCEngine::Rendering::CameraFramePlan& framePlan) override { - return m_sceneViewportBridge.RenderFramePlan(framePlan); + if (!framePlan.IsValid()) { + return false; + } + + return ResolveSceneViewportBridge(framePlan).RenderFramePlan(framePlan); } GameViewportFramePlanBuildStatus BuildGameViewportFramePlans( @@ -66,7 +105,7 @@ public: const ::XCEngine::Rendering::RenderSurface& surface, std::vector<::XCEngine::Rendering::CameraStackFramePlan>& outFramePlans) override { - return m_gameViewportBridge.BuildFramePlans( + return ResolveGameViewportBridge(renderContext).BuildFramePlans( renderContext, surface, outFramePlans); @@ -75,20 +114,77 @@ public: bool RenderGameViewportFramePlans( const std::vector<::XCEngine::Rendering::CameraStackFramePlan>& framePlans) override { - return m_gameViewportBridge.RenderFramePlans(framePlans); + if (framePlans.empty()) { + return false; + } + + return ResolveGameViewportBridge(framePlans).RenderFramePlans(framePlans); } bool TryResolveActiveSceneRenderObjectId( ::XCEngine::Rendering::RenderObjectId renderObjectId, EditorSceneObjectId& outRuntimeObjectId) const override { - return m_sceneViewportBridge.TryResolveActiveSceneRenderObjectId( + if (!m_sceneViewportBridges.empty()) { + return m_sceneViewportBridges.begin()->second + ->TryResolveActiveSceneRenderObjectId( + renderObjectId, + outRuntimeObjectId); + } + + const EngineSceneViewportBridge fallbackBridge = {}; + return fallbackBridge.TryResolveActiveSceneRenderObjectId( renderObjectId, outRuntimeObjectId); } private: - EngineGameViewportBridge m_gameViewportBridge = {}; - EngineSceneViewportBridge m_sceneViewportBridge = {}; + EngineSceneViewportBridge& ResolveSceneViewportBridge( + const ::XCEngine::Rendering::RenderContext& renderContext) { + return ResolveSceneViewportBridge( + ResolveSceneViewportBridgeKey(renderContext)); + } + + EngineSceneViewportBridge& ResolveSceneViewportBridge( + const ::XCEngine::Rendering::CameraFramePlan& framePlan) { + return ResolveSceneViewportBridge( + ResolveSceneViewportBridgeKey(framePlan)); + } + + EngineSceneViewportBridge& ResolveSceneViewportBridge(DeviceBridgeKey key) { + std::unique_ptr& bridge = + m_sceneViewportBridges[key]; + if (bridge == nullptr) { + bridge = std::make_unique(); + } + + return *bridge; + } + + EngineGameViewportBridge& ResolveGameViewportBridge( + const ::XCEngine::Rendering::RenderContext& renderContext) { + return ResolveGameViewportBridge(renderContext.device); + } + + EngineGameViewportBridge& ResolveGameViewportBridge( + const std::vector<::XCEngine::Rendering::CameraStackFramePlan>& framePlans) { + return ResolveGameViewportBridge( + ResolveGameViewportBridgeKey(framePlans)); + } + + EngineGameViewportBridge& ResolveGameViewportBridge(DeviceBridgeKey key) { + std::unique_ptr& bridge = + m_gameViewportBridges[key]; + if (bridge == nullptr) { + bridge = std::make_unique(); + } + + return *bridge; + } + + std::unordered_map> + m_gameViewportBridges = {}; + std::unordered_map> + m_sceneViewportBridges = {}; }; } // namespace diff --git a/editor/app/Services/Runtime/EditorRuntimeCoordinator.cpp b/editor/app/Services/Runtime/EditorRuntimeCoordinator.cpp index ca575d68..ac9b08c7 100644 --- a/editor/app/Services/Runtime/EditorRuntimeCoordinator.cpp +++ b/editor/app/Services/Runtime/EditorRuntimeCoordinator.cpp @@ -4,6 +4,7 @@ #include "Scene/EditorSceneRuntime.h" #include "State/EditorSession.h" +#include #include #include @@ -75,6 +76,7 @@ void EditorRuntimeCoordinator::Initialize( m_sceneRuntime = &sceneRuntime; m_projectRuntime = &projectRuntime; m_runtimePaths = runtimePaths; + m_scriptingRuntimeService.Initialize(runtimePaths.projectRoot); ApplyStartupSceneDocument(startupScene); SetRuntimeMode(EditorRuntimeMode::Edit); CaptureCleanSceneRevision(); @@ -92,6 +94,7 @@ void EditorRuntimeCoordinator::Shutdown() { m_sceneRuntime = nullptr; m_projectRuntime = nullptr; m_runtimePaths = EditorRuntimePaths{}; + m_scriptingRuntimeService.Shutdown(); m_sceneDocument = {}; m_lastFrameTickTime = {}; m_lastCleanSceneContentRevision = 0u; @@ -365,8 +368,22 @@ EditorRuntimeCoordinator::EvaluateScriptCommand( } if (commandId == "scripts.rebuild") { - return BuildDisabledResult( - "Script rebuild is owned by the runtime coordinator, but no in-process script assembly builder is bound."); + if (IsPlayModeActive()) { + return BuildDisabledResult( + "Stop play mode before rebuilding script assemblies."); + } + + if (!m_scriptingRuntimeService.CanRebuildProjectAssemblies()) { + const std::string& message = m_scriptingRuntimeService.GetLastMessage(); + return BuildDisabledResult( + message.empty() + ? std::string_view( + "Script rebuild is unavailable because the scripting runtime is not initialized.") + : std::string_view(message)); + } + + return BuildExecutableResult( + "Rebuild project script assemblies and reload the editor scripting runtime."); } return BuildDisabledResult( @@ -378,6 +395,29 @@ EditorRuntimeCoordinator::DispatchScriptCommand( std::string_view commandId) { const UIEditorHostCommandEvaluationResult evaluation = EvaluateScriptCommand(commandId); + if (!evaluation.executable) { + SetLastMessage(evaluation.message); + return BuildRejectedDispatch(evaluation.message); + } + + if (commandId == "scripts.rebuild") { + const bool rebuilt = m_scriptingRuntimeService.RebuildProjectAssemblies(); + if (m_sceneRuntime != nullptr) { + m_sceneRuntime->NotifyExternalInspectorStateChanged(); + } + + const std::string& message = m_scriptingRuntimeService.GetLastMessage(); + SetLastMessage( + !message.empty() + ? message + : rebuilt + ? std::string("Script assemblies rebuilt.") + : std::string("Failed to rebuild script assemblies.")); + return rebuilt + ? BuildExecutedDispatch(m_lastMessage) + : BuildRejectedDispatch(m_lastMessage); + } + SetLastMessage(evaluation.message); return BuildRejectedDispatch(evaluation.message); } @@ -418,6 +458,9 @@ bool EditorRuntimeCoordinator::StartPlayMode() { if (!EnsurePlaySession()) { return false; } + + ::XCEngine::Scripting::ScriptEngine::Get().SetRuntimeFixedDeltaTime( + m_runtimeLoop.GetSettings().fixedDeltaTime); m_runtimeLoop.Start(m_playSession->GetRuntimeScene()); SetRuntimeMode(EditorRuntimeMode::Play); m_lastFrameTickTime = std::chrono::steady_clock::now(); @@ -466,6 +509,9 @@ bool EditorRuntimeCoordinator::StepPlayMode() { if (!EnsurePlaySession()) { return false; } + + ::XCEngine::Scripting::ScriptEngine::Get().SetRuntimeFixedDeltaTime( + m_runtimeLoop.GetSettings().fixedDeltaTime); m_runtimeLoop.Start(m_playSession->GetRuntimeScene()); } diff --git a/editor/app/Services/Runtime/EditorRuntimeCoordinator.h b/editor/app/Services/Runtime/EditorRuntimeCoordinator.h index 00a0b209..b62474a6 100644 --- a/editor/app/Services/Runtime/EditorRuntimeCoordinator.h +++ b/editor/app/Services/Runtime/EditorRuntimeCoordinator.h @@ -2,6 +2,7 @@ #include "Commands/EditorHostCommandBridge.h" #include "Environment/EditorRuntimePaths.h" +#include "Runtime/EditorScriptingRuntimeService.h" #include @@ -86,6 +87,7 @@ private: EditorSceneRuntime* m_sceneRuntime = nullptr; EditorProjectRuntime* m_projectRuntime = nullptr; EditorRuntimePaths m_runtimePaths = {}; + EditorScriptingRuntimeService m_scriptingRuntimeService = {}; SceneDocumentState m_sceneDocument = {}; ::XCEngine::Components::RuntimeLoop m_runtimeLoop{ ::XCEngine::Components::RuntimeLoop::Settings{} }; diff --git a/editor/app/Services/Runtime/EditorScriptingRuntimeService.cpp b/editor/app/Services/Runtime/EditorScriptingRuntimeService.cpp new file mode 100644 index 00000000..89a52947 --- /dev/null +++ b/editor/app/Services/Runtime/EditorScriptingRuntimeService.cpp @@ -0,0 +1,209 @@ +#include "Runtime/EditorScriptingRuntimeService.h" + +#include "Platform/Win32Utf8.h" +#include "Scripting/EditorScriptAssemblyBuilder.h" +#include "Utils/ProjectGraphicsSettings.h" + +#include +#include +#include + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +std::string PathToUtf8String(const std::filesystem::path& path) { + return ::XCEngine::Editor::Platform::WideToUtf8(path.wstring()); +} + +void ApplyProjectGraphicsSettings(const std::filesystem::path& projectRoot) { + if (projectRoot.empty()) { + return; + } + + ::XCEngine::Editor::ProjectGraphicsSettings::ApplyCurrentSelection( + PathToUtf8String(projectRoot)); +} + +} // namespace + +EditorScriptingRuntimeService::~EditorScriptingRuntimeService() { + Shutdown(); +} + +bool EditorScriptingRuntimeService::Initialize( + const std::filesystem::path& projectRoot) { + m_projectRoot = projectRoot.lexically_normal(); + return ReloadRuntime(); +} + +void EditorScriptingRuntimeService::Shutdown() { + ShutdownRuntime(); + m_projectRoot.clear(); + m_status = {}; +} + +bool EditorScriptingRuntimeService::ReloadRuntime() { + ShutdownRuntime(); + m_status = {}; + + if (m_projectRoot.empty()) { + m_status.statusMessage = + "Cannot initialize the scripting runtime without a loaded project."; + return false; + } + + const std::filesystem::path assemblyDirectoryPath = + m_projectRoot / "Library" / "ScriptAssemblies"; + m_status.assemblyDirectory = PathToUtf8String(assemblyDirectoryPath); + +#ifdef XCENGINE_ENABLE_MONO_SCRIPTING + namespace fs = std::filesystem; + + auto& logger = ::XCEngine::Debug::Logger::Get(); + m_status.backendEnabled = true; + + ::XCEngine::Scripting::MonoScriptRuntime::Settings settings = {}; + settings.assemblyDirectory = assemblyDirectoryPath; + settings.corlibDirectory = assemblyDirectoryPath; + settings.coreAssemblyPath = assemblyDirectoryPath / L"XCEngine.ScriptCore.dll"; + settings.appAssemblyPath = assemblyDirectoryPath / L"GameScripts.dll"; + + std::string assemblyDiscoveryError = {}; + if (!::XCEngine::Scripting::MonoScriptRuntime::DiscoverEngineAssemblies( + settings, + &assemblyDiscoveryError)) { + m_status.statusMessage = + "Failed to discover engine managed assemblies: " + + assemblyDiscoveryError; + logger.Warning( + ::XCEngine::Debug::LogCategory::Scripting, + m_status.statusMessage.c_str()); + ApplyProjectGraphicsSettings(m_projectRoot); + return false; + } + + std::error_code errorCode = {}; + const bool hasCoreAssembly = fs::exists(settings.coreAssemblyPath, errorCode); + errorCode.clear(); + bool hasEngineAssemblies = true; + for (const auto& assembly : settings.engineAssemblies) { + const bool hasAssembly = fs::exists(assembly.path, errorCode); + errorCode.clear(); + hasEngineAssemblies = hasEngineAssemblies && hasAssembly; + } + const bool hasAppAssembly = fs::exists(settings.appAssemblyPath, errorCode); + errorCode.clear(); + const bool hasCorlibAssembly = + fs::exists(assemblyDirectoryPath / L"mscorlib.dll", errorCode); + m_status.assembliesFound = + hasCoreAssembly && + hasEngineAssemblies && + hasAppAssembly && + hasCorlibAssembly; + + if (!m_status.assembliesFound) { + m_status.statusMessage = + "Script assemblies were not found in " + + PathToUtf8String(assemblyDirectoryPath) + + ". Script class discovery is disabled until the managed assemblies are built."; + logger.Warning( + ::XCEngine::Debug::LogCategory::Scripting, + m_status.statusMessage.c_str()); + ApplyProjectGraphicsSettings(m_projectRoot); + return false; + } + + auto runtime = + std::make_unique<::XCEngine::Scripting::MonoScriptRuntime>(settings); + if (!runtime->Initialize()) { + m_status.statusMessage = + "Failed to initialize editor script runtime: " + + runtime->GetLastError(); + logger.Warning( + ::XCEngine::Debug::LogCategory::Scripting, + m_status.statusMessage.c_str()); + ApplyProjectGraphicsSettings(m_projectRoot); + return false; + } + + ::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(runtime.get()); + m_status.runtimeLoaded = true; + m_scriptRuntime = std::move(runtime); + m_status.statusMessage = "Editor script runtime initialized."; + logger.Info( + ::XCEngine::Debug::LogCategory::Scripting, + m_status.statusMessage.c_str()); + ApplyProjectGraphicsSettings(m_projectRoot); + return true; +#else + m_status.backendEnabled = false; + m_status.statusMessage = + "This editor build does not include Mono scripting support."; + ApplyProjectGraphicsSettings(m_projectRoot); + return false; +#endif +} + +bool EditorScriptingRuntimeService::RebuildProjectAssemblies() { + if (m_projectRoot.empty()) { + m_status.statusMessage = + "Cannot rebuild script assemblies without a loaded project."; + return false; + } + +#ifdef XCENGINE_ENABLE_MONO_SCRIPTING + auto& logger = ::XCEngine::Debug::Logger::Get(); + logger.Info( + ::XCEngine::Debug::LogCategory::Scripting, + "Rebuilding project script assemblies..."); + + ShutdownRuntime(); + + const ::XCEngine::Editor::Scripting::EditorScriptAssemblyBuildResult buildResult = + ::XCEngine::Editor::Scripting::EditorScriptAssemblyBuilder::RebuildProjectAssemblies( + PathToUtf8String(m_projectRoot)); + if (!buildResult.succeeded) { + m_status.assemblyDirectory = PathToUtf8String( + m_projectRoot / "Library" / "ScriptAssemblies"); + m_status.backendEnabled = true; + m_status.statusMessage = buildResult.message; + logger.Error( + ::XCEngine::Debug::LogCategory::Scripting, + buildResult.message.c_str()); + ApplyProjectGraphicsSettings(m_projectRoot); + return false; + } + + logger.Info( + ::XCEngine::Debug::LogCategory::Scripting, + buildResult.message.c_str()); + return ReloadRuntime(); +#else + m_status.statusMessage = + "This editor build does not include Mono scripting support."; + return false; +#endif +} + +bool EditorScriptingRuntimeService::CanRebuildProjectAssemblies() const { + return m_status.backendEnabled && !m_status.assemblyDirectory.empty(); +} + +const EditorScriptRuntimeStatus& EditorScriptingRuntimeService::GetStatus() const { + return m_status; +} + +const std::string& EditorScriptingRuntimeService::GetLastMessage() const { + return m_status.statusMessage; +} + +void EditorScriptingRuntimeService::ShutdownRuntime() { + ::XCEngine::Scripting::ScriptEngine::Get().OnRuntimeStop(); + ::XCEngine::Scripting::ScriptEngine::Get().SetRuntime(nullptr); + m_scriptRuntime.reset(); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Services/Runtime/EditorScriptingRuntimeService.h b/editor/app/Services/Runtime/EditorScriptingRuntimeService.h new file mode 100644 index 00000000..a6f002c1 --- /dev/null +++ b/editor/app/Services/Runtime/EditorScriptingRuntimeService.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +struct EditorScriptRuntimeStatus { + bool backendEnabled = false; + bool assembliesFound = false; + bool runtimeLoaded = false; + std::string assemblyDirectory = {}; + std::string statusMessage = {}; +}; + +class EditorScriptingRuntimeService { +public: + EditorScriptingRuntimeService() = default; + ~EditorScriptingRuntimeService(); + EditorScriptingRuntimeService(const EditorScriptingRuntimeService&) = delete; + EditorScriptingRuntimeService& operator=(const EditorScriptingRuntimeService&) = delete; + + bool Initialize(const std::filesystem::path& projectRoot); + void Shutdown(); + + bool ReloadRuntime(); + bool RebuildProjectAssemblies(); + + bool CanRebuildProjectAssemblies() const; + const EditorScriptRuntimeStatus& GetStatus() const; + const std::string& GetLastMessage() const; + +private: + void ShutdownRuntime(); + + std::filesystem::path m_projectRoot = {}; + EditorScriptRuntimeStatus m_status = {}; + std::unique_ptr<::XCEngine::Scripting::IScriptRuntime> m_scriptRuntime = {}; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Services/Scene/EditorSceneRuntime.cpp b/editor/app/Services/Scene/EditorSceneRuntime.cpp index 42c5b8b5..c3964ddd 100644 --- a/editor/app/Services/Scene/EditorSceneRuntime.cpp +++ b/editor/app/Services/Scene/EditorSceneRuntime.cpp @@ -732,6 +732,10 @@ bool EditorSceneRuntime::RedoTransformEdit() { return true; } +void EditorSceneRuntime::NotifyExternalInspectorStateChanged() { + IncrementInspectorRevision(); +} + EditorSelectionService& EditorSceneRuntime::SelectionService() { return *m_selectionService; } diff --git a/editor/app/Services/Scene/EditorSceneRuntime.h b/editor/app/Services/Scene/EditorSceneRuntime.h index c5ce11e2..f8c7f8c4 100644 --- a/editor/app/Services/Scene/EditorSceneRuntime.h +++ b/editor/app/Services/Scene/EditorSceneRuntime.h @@ -116,6 +116,7 @@ public: bool CanRedoTransformEdit() const; bool UndoTransformEdit(); bool RedoTransformEdit(); + void NotifyExternalInspectorStateChanged(); private: struct TransformEditTransaction { diff --git a/editor/app/Services/Scene/EngineEditorSceneBackend.cpp b/editor/app/Services/Scene/EngineEditorSceneBackend.cpp index 83a2e6ed..9b79a9d6 100644 --- a/editor/app/Services/Scene/EngineEditorSceneBackend.cpp +++ b/editor/app/Services/Scene/EngineEditorSceneBackend.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #include @@ -44,10 +46,15 @@ using ::XCEngine::Components::Scene; using ::XCEngine::Components::SceneManager; using ::XCEngine::Components::TransformComponent; using ::XCEngine::Math::Quaternion; +using ::XCEngine::Math::Vector2; using ::XCEngine::Math::Vector3; +using ::XCEngine::Math::Vector4; using ::XCEngine::Resources::ResourceManager; +using ::XCEngine::Scripting::ScriptComponent; constexpr char kComponentIdSeparator = '#'; +constexpr std::string_view kScriptClassPropertyPath = "script_class"; +constexpr std::string_view kScriptFieldPropertyPrefix = "script_field."; void TraceSceneStartup(std::string message) { ::XCEngine::UI::Editor::AppendUIEditorRuntimeTrace("startup", std::move(message)); @@ -793,6 +800,104 @@ public: void SetReceiveShadows(bool value) { MutableComponent().SetReceiveShadows(value); } }; +class ScriptComponentViewAdapter final + : public ComponentViewAdapterBase { +public: + explicit ScriptComponentViewAdapter(ScriptComponent& component) + : ComponentViewAdapterBase(component, "ScriptComponent") {} + + std::uint64_t GetScriptComponentUUID() const override { + return ComponentRef().GetScriptComponentUUID(); + } + + bool HasScriptClass() const override { + return ComponentRef().HasScriptClass(); + } + + std::string GetAssemblyName() const override { + return ComponentRef().GetAssemblyName(); + } + + std::string GetNamespaceName() const override { + return ComponentRef().GetNamespaceName(); + } + + std::string GetClassName() const override { + return ComponentRef().GetClassName(); + } + + std::string GetFullClassName() const override { + return ComponentRef().GetFullClassName(); + } + + bool TryGetFieldModel( + ::XCEngine::Scripting::ScriptFieldModel& outModel) const override { + return ::XCEngine::Scripting::ScriptEngine::Get().TryGetScriptFieldModel( + &ComponentRef(), + outModel); + } +}; + +std::string EncodeScriptClassSelection( + std::string_view assemblyName, + std::string_view namespaceName, + std::string_view className) { + if (className.empty()) { + return {}; + } + + std::string encoded = std::string(assemblyName); + encoded.push_back('|'); + encoded += namespaceName; + encoded.push_back('|'); + encoded += className; + return encoded; +} + +bool DecodeScriptClassSelection( + std::string_view encodedValue, + std::string& outAssemblyName, + std::string& outNamespaceName, + std::string& outClassName) { + outAssemblyName.clear(); + outNamespaceName.clear(); + outClassName.clear(); + if (encodedValue.empty()) { + return true; + } + + const std::size_t firstSeparator = encodedValue.find('|'); + if (firstSeparator == std::string_view::npos) { + return false; + } + + const std::size_t secondSeparator = + encodedValue.find('|', firstSeparator + 1u); + if (secondSeparator == std::string_view::npos) { + return false; + } + + outAssemblyName = std::string(encodedValue.substr(0u, firstSeparator)); + outNamespaceName = std::string(encodedValue.substr( + firstSeparator + 1u, + secondSeparator - firstSeparator - 1u)); + outClassName = std::string(encodedValue.substr(secondSeparator + 1u)); + return !outClassName.empty(); +} + +bool TryParseScriptFieldProperty( + std::string_view propertyPath, + std::string& outFieldName) { + if (propertyPath.size() <= kScriptFieldPropertyPrefix.size() || + propertyPath.substr(0u, kScriptFieldPropertyPrefix.size()) != + kScriptFieldPropertyPrefix) { + return false; + } + + outFieldName = std::string(propertyPath.substr(kScriptFieldPropertyPrefix.size())); + return !outFieldName.empty(); +} + std::shared_ptr CreateComponentView(Component& component) { if (auto* transform = dynamic_cast(&component); transform != nullptr) { return std::make_shared(*transform); @@ -847,6 +952,10 @@ std::shared_ptr CreateComponentView(Component& compone volumeRenderer != nullptr) { return std::make_shared(*volumeRenderer); } + if (auto* scriptComponent = dynamic_cast(&component); + scriptComponent != nullptr) { + return std::make_shared(*scriptComponent); + } return std::make_shared(component.GetName()); } @@ -1456,6 +1565,186 @@ bool ApplyVolumeRendererComponentMutation( [&view](bool value) { view.SetReceiveShadows(value); }); } +bool TryResolveScriptFieldType( + const ::XCEngine::Scripting::ScriptFieldModel& model, + std::string_view fieldName, + ::XCEngine::Scripting::ScriptFieldType& outFieldType) { + for (const ::XCEngine::Scripting::ScriptFieldSnapshot& field : model.fields) { + if (field.metadata.name == fieldName) { + outFieldType = field.metadata.type; + return true; + } + } + + return false; +} + +bool TryResolveScriptFieldValue( + const EditorSceneComponentMutation& mutation, + ::XCEngine::Scripting::ScriptFieldType fieldType, + ::XCEngine::Scripting::ScriptFieldValue& outValue) { + using ::XCEngine::Scripting::ScriptFieldType; + using ::XCEngine::Scripting::ScriptFieldValue; + + switch (fieldType) { + case ScriptFieldType::Float: { + const float* value = TryGetMutationValue(mutation); + if (value == nullptr) { + return false; + } + outValue = *value; + return true; + } + case ScriptFieldType::Double: { + const double* value = TryGetMutationValue(mutation); + if (value == nullptr) { + return false; + } + outValue = *value; + return true; + } + case ScriptFieldType::Bool: { + const bool* value = TryGetMutationValue(mutation); + if (value == nullptr) { + return false; + } + outValue = *value; + return true; + } + case ScriptFieldType::Int32: { + const std::int32_t* value = TryGetMutationValue(mutation); + if (value == nullptr) { + return false; + } + outValue = *value; + return true; + } + case ScriptFieldType::UInt64: { + const std::uint64_t* value = TryGetMutationValue(mutation); + if (value == nullptr) { + return false; + } + outValue = *value; + return true; + } + case ScriptFieldType::String: { + const std::string* value = TryGetMutationValue(mutation); + if (value == nullptr) { + return false; + } + outValue = *value; + return true; + } + case ScriptFieldType::Vector2: { + const Vector2* value = TryGetMutationValue(mutation); + if (value == nullptr) { + return false; + } + outValue = *value; + return true; + } + case ScriptFieldType::Vector3: { + const Vector3* value = TryGetMutationValue(mutation); + if (value == nullptr) { + return false; + } + outValue = *value; + return true; + } + case ScriptFieldType::Vector4: { + const Vector4* value = TryGetMutationValue(mutation); + if (value == nullptr) { + return false; + } + outValue = *value; + return true; + } + case ScriptFieldType::GameObject: { + const ::XCEngine::Scripting::GameObjectReference* value = + TryGetMutationValue<::XCEngine::Scripting::GameObjectReference>(mutation); + if (value == nullptr) { + return false; + } + outValue = *value; + return true; + } + case ScriptFieldType::Component: { + const ::XCEngine::Scripting::ComponentReference* value = + TryGetMutationValue<::XCEngine::Scripting::ComponentReference>(mutation); + if (value == nullptr) { + return false; + } + outValue = *value; + return true; + } + case ScriptFieldType::None: + default: + return false; + } +} + +bool ApplyScriptComponentMutation( + ScriptComponent& component, + const EditorSceneComponentMutation& mutation) { + if (mutation.propertyPath == kScriptClassPropertyPath) { + const std::string* encodedValue = TryGetMutationValue(mutation); + if (encodedValue == nullptr) { + return false; + } + + if (encodedValue->empty()) { + component.ClearScriptClass(); + return true; + } + + std::string assemblyName = {}; + std::string namespaceName = {}; + std::string className = {}; + if (!DecodeScriptClassSelection( + *encodedValue, + assemblyName, + namespaceName, + className)) { + return false; + } + + component.SetScriptClass(assemblyName, namespaceName, className); + return true; + } + + std::string fieldName = {}; + if (!TryParseScriptFieldProperty(mutation.propertyPath, fieldName)) { + return false; + } + + ::XCEngine::Scripting::ScriptFieldModel model = {}; + if (!::XCEngine::Scripting::ScriptEngine::Get().TryGetScriptFieldModel( + &component, + model)) { + return false; + } + + ::XCEngine::Scripting::ScriptFieldType fieldType = + ::XCEngine::Scripting::ScriptFieldType::None; + if (!TryResolveScriptFieldType(model, fieldName, fieldType)) { + return false; + } + + ::XCEngine::Scripting::ScriptFieldValue fieldValue = {}; + if (!TryResolveScriptFieldValue(mutation, fieldType, fieldValue)) { + return false; + } + + std::vector<::XCEngine::Scripting::ScriptFieldWriteResult> results = {}; + return ::XCEngine::Scripting::ScriptEngine::Get().ApplyScriptFieldWrites( + &component, + { ::XCEngine::Scripting::ScriptFieldWriteRequest{ + .fieldName = fieldName, + .type = fieldType, + .value = std::move(fieldValue) } }, + results); +} + Component* ResolveComponent( const GameObject& gameObject, std::string_view componentId) { @@ -2211,6 +2500,10 @@ public: volumeRenderer != nullptr) { return ApplyVolumeRendererComponentMutation(*volumeRenderer, mutation); } + if (auto* scriptComponent = dynamic_cast(component); + scriptComponent != nullptr) { + return ApplyScriptComponentMutation(*scriptComponent, mutation); + } return false; } diff --git a/editor/include/XCEditor/Foundation/UIEditorCommandDispatcher.h b/editor/include/XCEditor/Foundation/UIEditorCommandDispatcher.h index 7858f32f..e9d68f58 100644 --- a/editor/include/XCEditor/Foundation/UIEditorCommandDispatcher.h +++ b/editor/include/XCEditor/Foundation/UIEditorCommandDispatcher.h @@ -23,10 +23,12 @@ public: virtual ~UIEditorHostCommandHandler() = default; virtual UIEditorHostCommandEvaluationResult EvaluateHostCommand( - std::string_view commandId) const = 0; + std::string_view commandId, + const UIEditorWorkspaceController& controller) const = 0; virtual UIEditorHostCommandDispatchResult DispatchHostCommand( - std::string_view commandId) = 0; + std::string_view commandId, + UIEditorWorkspaceController& controller) = 0; }; enum class UIEditorCommandEvaluationCode : std::uint8_t { diff --git a/editor/include/XCEditor/Shell/UIEditorShellInteraction.h b/editor/include/XCEditor/Shell/UIEditorShellInteraction.h index 487b8f88..c5c52849 100644 --- a/editor/include/XCEditor/Shell/UIEditorShellInteraction.h +++ b/editor/include/XCEditor/Shell/UIEditorShellInteraction.h @@ -64,6 +64,13 @@ struct UIEditorShellInteractionMenuButtonRequest { bool enabled = true; }; +struct UIEditorShellInteractionToolbarButtonRequest { + std::string buttonId = {}; + ::XCEngine::UI::UIRect rect = {}; + ::XCEngine::UI::UIInputPath path = {}; + bool enabled = true; +}; + struct UIEditorShellInteractionPopupItemRequest { std::string popupId = {}; std::string menuId = {}; @@ -95,6 +102,7 @@ struct UIEditorShellInteractionRequest { UIEditorShellComposeRequest shellRequest = {}; std::vector menuBarItems = {}; std::vector menuButtons = {}; + std::vector toolbarButtons = {}; std::vector popupRequests = {}; }; diff --git a/editor/src/Foundation/UIEditorCommandDispatcher.cpp b/editor/src/Foundation/UIEditorCommandDispatcher.cpp index e44dc1a7..f71dbe9f 100644 --- a/editor/src/Foundation/UIEditorCommandDispatcher.cpp +++ b/editor/src/Foundation/UIEditorCommandDispatcher.cpp @@ -116,7 +116,9 @@ UIEditorCommandEvaluationResult UIEditorCommandDispatcher::Evaluate( } const UIEditorHostCommandEvaluationResult hostEvaluation = - m_hostCommandHandler->EvaluateHostCommand(descriptor->commandId); + m_hostCommandHandler->EvaluateHostCommand( + descriptor->commandId, + controller); return MakeEvaluationResult( hostEvaluation.executable ? UIEditorCommandEvaluationCode::None @@ -205,7 +207,9 @@ UIEditorCommandDispatchResult UIEditorCommandDispatcher::Dispatch( } const UIEditorHostCommandDispatchResult hostDispatch = - m_hostCommandHandler->DispatchHostCommand(evaluation.commandId); + m_hostCommandHandler->DispatchHostCommand( + evaluation.commandId, + controller); return BuildDispatchResult( hostDispatch.commandExecuted ? UIEditorCommandDispatchStatus::Dispatched diff --git a/editor/src/Shell/ShellInteractionInternal.h b/editor/src/Shell/ShellInteractionInternal.h index 509651cd..17a0a2f8 100644 --- a/editor/src/Shell/ShellInteractionInternal.h +++ b/editor/src/Shell/ShellInteractionInternal.h @@ -10,10 +10,12 @@ inline constexpr ::XCEngine::UI::UIElementId kShellPathRoot = 0x1E1001ull; inline constexpr ::XCEngine::UI::UIElementId kMenuBarPathRoot = 0x1E1002ull; inline constexpr ::XCEngine::UI::UIElementId kPopupPathRoot = 0x1E1003ull; inline constexpr ::XCEngine::UI::UIElementId kMenuItemPathRoot = 0x1E1004ull; +inline constexpr ::XCEngine::UI::UIElementId kToolbarPathRoot = 0x1E1005ull; inline constexpr ::XCEngine::UI::UIElementId kOutsidePointerPath = 0x1E10FFull; struct RequestHit { const UIEditorShellInteractionMenuButtonRequest* menuButton = nullptr; + const UIEditorShellInteractionToolbarButtonRequest* toolbarButton = nullptr; const UIEditorShellInteractionPopupRequest* popupRequest = nullptr; const UIEditorShellInteractionPopupItemRequest* popupItem = nullptr; }; @@ -28,6 +30,7 @@ struct BuildRequestOutput { std::string BuildRootPopupId(std::string_view menuId); std::string BuildSubmenuPopupId(std::string_view popupId, std::string_view itemId); ::XCEngine::UI::UIInputPath BuildMenuButtonPath(std::string_view menuId); +::XCEngine::UI::UIInputPath BuildToolbarButtonPath(std::string_view buttonId); ::XCEngine::UI::UIInputPath BuildPopupSurfacePath(std::string_view popupId); ::XCEngine::UI::UIInputPath BuildMenuItemPath( std::string_view popupId, diff --git a/editor/src/Shell/UIEditorShellCompose.cpp b/editor/src/Shell/UIEditorShellCompose.cpp index c0f70a56..22f3afd7 100644 --- a/editor/src/Shell/UIEditorShellCompose.cpp +++ b/editor/src/Shell/UIEditorShellCompose.cpp @@ -20,6 +20,14 @@ float ClampNonNegative(float value) { return (std::max)(value, 0.0f); } +UIColor ScaleColor(const UIColor& color, float rgbScale, float alphaScale = 1.0f) { + return UIColor( + color.r * rgbScale, + color.g * rgbScale, + color.b * rgbScale, + color.a * alphaScale); +} + UIRect InsetRect(const UIRect& rect, float inset) { const float clampedInset = ClampNonNegative(inset); const float insetX = (std::min)(clampedInset, rect.width * 0.5f); @@ -121,13 +129,20 @@ void AppendUIEditorShellToolbar( const std::size_t buttonCount = (std::min)(buttons.size(), layout.buttonRects.size()); for (std::size_t index = 0; index < buttonCount; ++index) { const UIRect& buttonRect = layout.buttonRects[index]; + const bool enabled = buttons[index].enabled; + const UIColor buttonColor = + enabled ? palette.buttonColor : ScaleColor(palette.buttonColor, 0.72f, 0.90f); + const UIColor borderColor = + enabled ? palette.buttonBorderColor : ScaleColor(palette.buttonBorderColor, 0.80f, 0.90f); + const UIColor iconColor = + enabled ? palette.iconColor : ScaleColor(palette.iconColor, 0.45f, 0.75f); drawList.AddFilledRect( buttonRect, - palette.buttonColor, + buttonColor, metrics.buttonCornerRounding); drawList.AddRectOutline( buttonRect, - palette.buttonBorderColor, + borderColor, metrics.borderThickness, metrics.buttonCornerRounding); AppendToolbarGlyph( @@ -135,7 +150,7 @@ void AppendUIEditorShellToolbar( buttonRect, buttons[index].iconKind, iconResolver, - palette.iconColor); + iconColor); } } diff --git a/editor/src/Shell/UIEditorShellInteraction.cpp b/editor/src/Shell/UIEditorShellInteraction.cpp index 26a8c6fb..1819d857 100644 --- a/editor/src/Shell/UIEditorShellInteraction.cpp +++ b/editor/src/Shell/UIEditorShellInteraction.cpp @@ -30,6 +30,13 @@ using Widgets::UIEditorMenuBarInvalidIndex; using Widgets::UIEditorMenuPopupHitTargetKind; using Widgets::UIEditorMenuPopupInvalidIndex; +bool RectContains(const UIRect& rect, const UIPoint& point) { + return point.x >= rect.x && + point.y >= rect.y && + point.x <= rect.x + rect.width && + point.y <= rect.y + rect.height; +} + const UIEditorResolvedMenuDescriptor* FindResolvedMenu( const UIEditorResolvedMenuModel& model, std::string_view menuId) { @@ -138,6 +145,10 @@ UIInputPath BuildMenuButtonPath(std::string_view menuId) { return UIInputPath { kShellPathRoot, kMenuBarPathRoot, HashText(menuId) }; } +UIInputPath BuildToolbarButtonPath(std::string_view buttonId) { + return UIInputPath { kShellPathRoot, kToolbarPathRoot, HashText(buttonId) }; +} + UIInputPath BuildPopupSurfacePath(std::string_view popupId) { return UIInputPath { kShellPathRoot, kPopupPathRoot, HashText(popupId) }; } @@ -233,6 +244,29 @@ std::vector BuildMeasuredStatusSegments( return measuredSegments; } +std::vector ResolveToolbarButtons( + const std::vector& buttons, + const UIEditorWorkspaceController& controller, + const UIEditorShellInteractionServices& services) { + std::vector resolved = buttons; + if (services.commandDispatcher == nullptr) { + return resolved; + } + + for (UIEditorShellToolbarButton& button : resolved) { + if (button.buttonId.empty()) { + button.enabled = false; + continue; + } + + const UIEditorCommandEvaluationResult evaluation = + services.commandDispatcher->Evaluate(button.buttonId, controller); + button.enabled = button.enabled && evaluation.IsExecutable(); + } + + return resolved; +} + bool HasMeaningfulInteractionResult( const UIEditorShellInteractionResult& result) { return result.consumed || @@ -293,6 +327,20 @@ BuildRequestOutput BuildRequest( request.menuButtons.push_back(std::move(button)); } + request.toolbarButtons.reserve(output.resolvedModel.toolbarButtons.size()); + for (std::size_t index = 0; index < output.resolvedModel.toolbarButtons.size(); ++index) { + const UIEditorShellToolbarButton& sourceButton = + output.resolvedModel.toolbarButtons[index]; + UIEditorShellInteractionToolbarButtonRequest button = {}; + button.buttonId = sourceButton.buttonId; + button.enabled = sourceButton.enabled; + if (index < request.shellRequest.layout.toolbarLayout.buttonRects.size()) { + button.rect = request.shellRequest.layout.toolbarLayout.buttonRects[index]; + } + button.path = BuildToolbarButtonPath(button.buttonId); + request.toolbarButtons.push_back(std::move(button)); + } + const auto& popupStates = state.menuSession.GetPopupStates(); request.popupRequests.reserve(popupStates.size()); for (const UIEditorMenuPopupState& popupState : popupStates) { @@ -397,6 +445,15 @@ RequestHit HitTestRequest( if (menuHit.kind == UIEditorMenuBarHitTargetKind::Button && menuHit.index < request.menuButtons.size()) { hit.menuButton = &request.menuButtons[menuHit.index]; + return hit; + } + + for (const UIEditorShellInteractionToolbarButtonRequest& button : + request.toolbarButtons) { + if (RectContains(button.rect, point)) { + hit.toolbarButton = &button; + return hit; + } } return hit; @@ -514,7 +571,10 @@ UIEditorShellInteractionModel ResolveUIEditorShellInteractionModel( controller, services.shortcutManager); } - model.toolbarButtons = definition.toolbarButtons; + model.toolbarButtons = Internal::ResolveToolbarButtons( + definition.toolbarButtons, + controller, + services); model.statusSegments = definition.statusSegments; model.workspacePresentations = definition.workspacePresentations; return model; @@ -562,9 +622,14 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( UIEditorShellInteractionResult interactionResult = {}; bool menuModalDuringFrame = state.menuSession.HasOpenMenu(); + UIEditorShellInteractionModel resolvedModel = model; + resolvedModel.toolbarButtons = Internal::ResolveToolbarButtons( + model.toolbarButtons, + controller, + services); Internal::BuildRequestOutput requestBuild = Internal::BuildRequest( bounds, - model, + resolvedModel, state, metrics, services); @@ -578,7 +643,7 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( requestBuild = Internal::BuildRequest( bounds, - model, + resolvedModel, state, metrics, services); @@ -698,6 +763,21 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( eventResult.menuMutation = state.menuSession.DismissFromPointerDown(hit.popupItem->path); } + } else if (hit.toolbarButton != nullptr && hit.toolbarButton->enabled) { + state.focused = true; + eventResult.consumed = true; + eventResult.commandTriggered = true; + eventResult.commandId = hit.toolbarButton->buttonId; + if (services.commandDispatcher != nullptr) { + eventResult.commandDispatchResult = + services.commandDispatcher->Dispatch( + eventResult.commandId, + controller); + eventResult.commandDispatched = true; + } + if (state.menuSession.HasOpenMenu()) { + eventResult.menuMutation = state.menuSession.CloseAll(); + } } else if (hit.popupRequest != nullptr) { eventResult.consumed = true; eventResult.menuId = hit.popupRequest->menuId; @@ -735,7 +815,7 @@ UIEditorShellInteractionFrame UpdateUIEditorShellInteraction( if (eventResult.menuMutation.changed) { request = Internal::BuildRequest( bounds, - model, + resolvedModel, state, metrics, services).request; diff --git a/engine/include/XCEngine/Rendering/AGENTS.md b/engine/include/XCEngine/Rendering/AGENTS.md index 1377525b..a42ceca9 100644 --- a/engine/include/XCEngine/Rendering/AGENTS.md +++ b/engine/include/XCEngine/Rendering/AGENTS.md @@ -69,7 +69,7 @@ SceneRenderer 职责边界: - `SceneRenderer`:场景级 façade,只把 scene/override camera/context/surface 交给当前 pipeline host;不再直接拥有 top-level request planner。 -- `RenderPipelineAsset`:顶层 scene/camera request authority。`BuildSceneRenderRequests(...)` 是对齐 Unity `RenderPipeline.Render(...)` 入口责任的首个 native 合约面;未来所有 SRP/URP 级 camera policy、camera list 裁剪、renderer routing、tooling request 安装,都必须先经过这里。 +- `RenderPipelineAsset`:顶层 scene/camera request authority。`BuildSceneRenderRequests(...)` 是对齐 Unity `RenderPipeline.Render(...)` 入口责任的首个 native 合约面;所有 SRP/URP 级 camera policy、camera list 裁剪、renderer routing、tooling request 安装,都必须先经过这里。managed `ScriptableRenderPipelineAsset.BuildSceneRenderRequests(...)` 通过 `ScriptableRenderPipelineScenePlanningContext` 接入同一个入口,默认 native planner 只能由 C# 显式 `UseDefaultRequests()` 调用。 - `SceneRenderRequestPlanner`:默认 native planning utility。它负责收集可用相机、推导 viewport / clear / camera stack / directional shadow request;不再是顶层 authority,而是 `RenderPipelineAsset::BuildSceneRenderRequests(...)` 的默认 fallback 实现。 - `RenderPipelineHost`:向当前 asset 请求 scene render requests,把 requests 变成 frame plans / stack plans,稳定排序并驱动 `CameraRenderer`。 - `CameraFramePlanBuilder`:从 `CameraRenderRequest` 建 `CameraFramePlan`,调用 `RenderPipelineAsset::ConfigureCameraFramePlan`。 @@ -176,6 +176,7 @@ managed 侧当前已存在三层: - `ScriptableRenderPipelineAsset` - `ScriptableRenderPipeline` - `ScriptableRenderContext` + - `ScriptableRenderPipelineScenePlanningContext` - `ScriptableRenderPipelinePlanningContext` - `CommandBuffer` - `RendererListDesc` / `DrawingSettings` / `RenderStateBlock` @@ -195,6 +196,7 @@ managed 侧当前已存在三层: 改 managed SRP/URP 时: +- 顶层 scene/camera request 必须走 `ScriptableRenderPipelineAsset.BuildSceneRenderRequests(ScriptableRenderPipelineScenePlanningContext)`。默认行为是 `context.UseDefaultRequests()`;自定义相机列表时用 `GetDefaultCameras()` / `ClearRequests()` / `AddCamera(...)` 显式构建 request,不要让 C++ `SceneRenderRequestPlanner` 在 managed asset 背后自动接管。 - public API 命名优先贴近 Unity;内部扩展可以加 `Instance` / context helper,但不要污染面向用户的概念。 - `RenderPassEvent` 的 Unity 数值顺序是兼容契约;本项目额外插入的 `RenderOpaques`、`RenderSkybox`、`RenderTransparents`、final-output extension 必须保持相对顺序。 - `ScriptableRenderer.EnqueuePass` 只排 pass,长期资源由 feature `Create` / data runtime cache 管,不要每相机创建。 diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index 8b116470..027fb80d 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -284,6 +284,8 @@ private: uint64_t nativeHandle); MonoObject* CreateManagedDirectionalShadowExecutionContext( uint64_t nativeHandle); + MonoObject* CreateManagedScriptableRenderPipelineScenePlanningContext( + uint64_t nativeHandle); MonoObject* CreateManagedScriptableRenderPipelinePlanningContext( uint64_t nativeHandle); MonoObject* GetManagedObject(uint32_t gcHandle) const; @@ -328,6 +330,8 @@ private: MonoClass* m_cameraRenderRequestContextClass = nullptr; MonoClass* m_renderSceneSetupContextClass = nullptr; MonoClass* m_directionalShadowExecutionContextClass = nullptr; + MonoClass* m_scriptableRenderPipelineScenePlanningContextClass = + nullptr; MonoClass* m_scriptableRenderPipelinePlanningContextClass = nullptr; MonoClass* m_serializeFieldAttributeClass = nullptr; MonoMethod* m_gameObjectConstructor = nullptr; @@ -338,6 +342,8 @@ private: nullptr; MonoMethod* m_directionalShadowExecutionContextConstructor = nullptr; + MonoMethod* m_scriptableRenderPipelineScenePlanningContextConstructor = + nullptr; MonoMethod* m_scriptableRenderPipelinePlanningContextConstructor = nullptr; MonoClassField* m_managedGameObjectUUIDField = nullptr; diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index 6d05519a..ef83f568 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -24,6 +24,7 @@ #include "Rendering/Passes/BuiltinVectorFullscreenPass.h" #include "Rendering/Planning/FullscreenPassDesc.h" #include "Rendering/Planning/SceneRenderRequestPlanner.h" +#include "Rendering/Planning/SceneRenderRequestUtils.h" #include "Rendering/Pipelines/NativeSceneRecorder.h" #include "Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h" #include "Rendering/RenderPassGraphContract.h" @@ -172,6 +173,22 @@ struct ManagedScriptableRenderPipelinePlanningContextState { Rendering::CameraFramePlan* plan = nullptr; }; +struct ManagedScriptableRenderPipelineScenePlanningContextState { + uint64_t handle = 0; + const Components::Scene* scene = nullptr; + Components::CameraComponent* overrideCamera = nullptr; + const Rendering::RenderContext* context = nullptr; + const Rendering::RenderSurface* surface = nullptr; + Rendering::DirectionalShadowPlanningSettings + directionalShadowPlanningSettings = {}; + const MonoManagedRenderPipelineAssetRuntime* assetRuntime = nullptr; + std::vector* outputRequests = + nullptr; + size_t renderedBaseCameraCount = 0u; + uint64_t nextCameraStackId = 1u; + uint64_t activeCameraStackId = 0u; +}; + struct ManagedCommandBufferCommand { enum class Kind { ClearRenderTarget, @@ -1000,6 +1017,59 @@ void UnregisterManagedScriptableRenderPipelinePlanningContextState( GetManagedScriptableRenderPipelinePlanningContextRegistry().erase(handle); } +uint64_t& GetManagedScriptableRenderPipelineScenePlanningContextNextHandle() { + static uint64_t nextHandle = 1; + return nextHandle; +} + +std::unordered_map< + uint64_t, + ManagedScriptableRenderPipelineScenePlanningContextState*>& +GetManagedScriptableRenderPipelineScenePlanningContextRegistry() { + static std::unordered_map< + uint64_t, + ManagedScriptableRenderPipelineScenePlanningContextState*> registry; + return registry; +} + +ManagedScriptableRenderPipelineScenePlanningContextState* +FindManagedScriptableRenderPipelineScenePlanningContextState( + uint64_t handle) { + const auto it = + GetManagedScriptableRenderPipelineScenePlanningContextRegistry().find( + handle); + return it != + GetManagedScriptableRenderPipelineScenePlanningContextRegistry() + .end() + ? it->second + : nullptr; +} + +uint64_t RegisterManagedScriptableRenderPipelineScenePlanningContextState( + ManagedScriptableRenderPipelineScenePlanningContextState& state) { + uint64_t handle = + GetManagedScriptableRenderPipelineScenePlanningContextNextHandle()++; + if (handle == 0) { + handle = + GetManagedScriptableRenderPipelineScenePlanningContextNextHandle()++; + } + + state.handle = handle; + GetManagedScriptableRenderPipelineScenePlanningContextRegistry()[handle] = + &state; + return handle; +} + +void UnregisterManagedScriptableRenderPipelineScenePlanningContextState( + uint64_t handle) { + if (handle == 0) { + return; + } + + GetManagedScriptableRenderPipelineScenePlanningContextRegistry().erase( + handle); +} + bool TryResolveManagedCameraFrameStage( int32_t value, Rendering::CameraFrameStage& outStage) { @@ -2015,6 +2085,15 @@ public: std::unique_ptr CreateStageRecorder() const override; + bool BuildSceneRenderRequests( + const Components::Scene& scene, + Components::CameraComponent* overrideCamera, + const Rendering::RenderContext& context, + const Rendering::RenderSurface& surface, + const Rendering::DirectionalShadowPlanningSettings& + directionalShadowSettings, + std::vector& requests) + const override; void ConfigureCameraRenderRequest( Rendering::CameraRenderRequest& request, size_t renderedBaseCameraCount, @@ -2061,6 +2140,8 @@ private: MonoObject* GetManagedAssetObject() const; MonoMethod* ResolveDisposePipelineMethod(MonoObject* pipelineObject) const; MonoMethod* ResolveCreatePipelineMethod(MonoObject* assetObject) const; + MonoMethod* ResolveBuildSceneRenderRequestsMethod( + MonoObject* assetObject) const; MonoMethod* ResolveConfigureCameraRenderRequestMethod( MonoObject* assetObject) const; MonoMethod* ResolveConfigureCameraFramePlanMethod( @@ -2082,6 +2163,7 @@ private: mutable uint32_t m_assetHandle = 0; mutable MonoMethod* m_disposePipelineMethod = nullptr; mutable MonoMethod* m_createPipelineMethod = nullptr; + mutable MonoMethod* m_buildSceneRenderRequestsMethod = nullptr; mutable MonoMethod* m_configureCameraRenderRequestMethod = nullptr; mutable MonoMethod* m_configureCameraFramePlanMethod = nullptr; mutable MonoMethod* m_getDefaultFinalColorSettingsMethod = nullptr; @@ -2718,6 +2800,65 @@ MonoManagedRenderPipelineAssetRuntime::CreateStageRecorder() const { shared_from_this()); } +bool MonoManagedRenderPipelineAssetRuntime::BuildSceneRenderRequests( + const Components::Scene& scene, + Components::CameraComponent* overrideCamera, + const Rendering::RenderContext& context, + const Rendering::RenderSurface& surface, + const Rendering::DirectionalShadowPlanningSettings& + directionalShadowSettings, + std::vector& requests) const { + requests.clear(); + if (!EnsureManagedAsset()) { + return false; + } + + MonoObject* const assetObject = GetManagedAssetObject(); + MonoMethod* const method = + ResolveBuildSceneRenderRequestsMethod(assetObject); + if (assetObject == nullptr || method == nullptr) { + return false; + } + + ManagedScriptableRenderPipelineScenePlanningContextState + scenePlanningContextState = {}; + scenePlanningContextState.scene = &scene; + scenePlanningContextState.overrideCamera = overrideCamera; + scenePlanningContextState.context = &context; + scenePlanningContextState.surface = &surface; + scenePlanningContextState.directionalShadowPlanningSettings = + directionalShadowSettings; + scenePlanningContextState.assetRuntime = this; + scenePlanningContextState.outputRequests = &requests; + const uint64_t scenePlanningContextHandle = + RegisterManagedScriptableRenderPipelineScenePlanningContextState( + scenePlanningContextState); + MonoObject* const scenePlanningContextObject = + m_runtime->CreateManagedScriptableRenderPipelineScenePlanningContext( + scenePlanningContextHandle); + if (scenePlanningContextObject == nullptr) { + UnregisterManagedScriptableRenderPipelineScenePlanningContextState( + scenePlanningContextHandle); + return false; + } + + void* args[1] = { scenePlanningContextObject }; + MonoObject* result = nullptr; + const bool invokeSucceeded = + m_runtime->InvokeManagedMethod( + assetObject, + method, + args, + &result); + UnregisterManagedScriptableRenderPipelineScenePlanningContextState( + scenePlanningContextHandle); + + bool accepted = false; + return invokeSucceeded && + TryUnboxManagedBoolean(result, accepted) && + accepted; +} + void MonoManagedRenderPipelineAssetRuntime::ConfigureCameraRenderRequest( Rendering::CameraRenderRequest& request, size_t renderedBaseCameraCount, @@ -3240,6 +3381,20 @@ MonoMethod* MonoManagedRenderPipelineAssetRuntime::ResolveCreatePipelineMethod( return m_createPipelineMethod; } +MonoMethod* +MonoManagedRenderPipelineAssetRuntime::ResolveBuildSceneRenderRequestsMethod( + MonoObject* assetObject) const { + if (m_buildSceneRenderRequestsMethod == nullptr) { + m_buildSceneRenderRequestsMethod = + m_runtime->ResolveManagedMethod( + assetObject, + "BuildSceneRenderRequestsInstance", + 1); + } + + return m_buildSceneRenderRequestsMethod; +} + MonoMethod* MonoManagedRenderPipelineAssetRuntime::ResolveConfigureCameraRenderRequestMethod( MonoObject* assetObject) const { @@ -6953,6 +7108,207 @@ void InternalCall_Rendering_DirectionalShadowExecutionContext_ClearDirectionalSh state->explicitlyConfigured = true; } +Components::GameObject* FindGameObjectByUUIDInScene( + const Components::Scene& scene, + uint64_t uuid) { + if (uuid == 0) { + return nullptr; + } + + for (Components::GameObject* root : scene.GetRootGameObjects()) { + if (Components::GameObject* const found = + FindGameObjectByUUIDRecursive(root, uuid); + found != nullptr) { + return found; + } + } + + return nullptr; +} + +void ResetManagedScenePlanningRequestCounters( + ManagedScriptableRenderPipelineScenePlanningContextState& state) { + state.renderedBaseCameraCount = 0u; + state.nextCameraStackId = 1u; + state.activeCameraStackId = 0u; +} + +bool AppendManagedScenePlanningCameraRequest( + ManagedScriptableRenderPipelineScenePlanningContextState& state, + Components::CameraComponent* camera) { + if (state.scene == nullptr || + state.context == nullptr || + state.surface == nullptr || + state.outputRequests == nullptr || + !Rendering::SceneRenderRequestUtils::IsUsableCamera(camera)) { + return false; + } + + Rendering::CameraRenderRequest request = {}; + if (!Rendering::SceneRenderRequestUtils::BuildCameraRenderRequest( + *state.scene, + *camera, + *state.context, + *state.surface, + state.renderedBaseCameraCount, + state.outputRequests->size(), + request)) { + return false; + } + + if (camera->GetStackType() == Components::CameraStackType::Base || + state.activeCameraStackId == 0u) { + state.activeCameraStackId = state.nextCameraStackId++; + } + request.cameraStackId = state.activeCameraStackId; + + if (state.assetRuntime != nullptr) { + state.assetRuntime->ConfigureCameraRenderRequest( + request, + state.renderedBaseCameraCount, + state.outputRequests->size(), + state.directionalShadowPlanningSettings); + } else { + Rendering::ApplyDefaultRenderPipelineAssetCameraRenderRequestPolicy( + request, + state.renderedBaseCameraCount, + state.outputRequests->size(), + state.directionalShadowPlanningSettings); + } + + state.outputRequests->push_back(request); + if (camera->GetStackType() == Components::CameraStackType::Base) { + ++state.renderedBaseCameraCount; + } + + return true; +} + +int32_t +InternalCall_Rendering_ScriptableRenderPipelineScenePlanningContext_GetRequestCount( + uint64_t nativeHandle) { + const ManagedScriptableRenderPipelineScenePlanningContextState* const + state = + FindManagedScriptableRenderPipelineScenePlanningContextState( + nativeHandle); + return state != nullptr && + state->outputRequests != nullptr + ? static_cast(state->outputRequests->size()) + : 0; +} + +MonoArray* +InternalCall_Rendering_ScriptableRenderPipelineScenePlanningContext_GetDefaultCameraGameObjectUUIDs( + uint64_t nativeHandle) { + const ManagedScriptableRenderPipelineScenePlanningContextState* const + state = + FindManagedScriptableRenderPipelineScenePlanningContextState( + nativeHandle); + if (state == nullptr || state->scene == nullptr) { + return nullptr; + } + + Rendering::SceneRenderRequestPlanner requestPlanner = {}; + requestPlanner.SetDirectionalShadowPlanningSettings( + state->directionalShadowPlanningSettings); + const std::vector cameras = + requestPlanner.CollectCameras( + *state->scene, + state->overrideCamera); + + MonoDomain* const domain = mono_domain_get(); + MonoClass* const uint64Class = mono_get_uint64_class(); + if (domain == nullptr || uint64Class == nullptr) { + return nullptr; + } + + MonoArray* const array = + mono_array_new( + domain, + uint64Class, + static_cast(cameras.size())); + if (array == nullptr) { + return nullptr; + } + + for (uintptr_t index = 0; index < cameras.size(); ++index) { + const uint64_t gameObjectUUID = + cameras[index] != nullptr && + cameras[index]->GetGameObject() != nullptr + ? cameras[index]->GetGameObject()->GetUUID() + : 0u; + mono_array_set(array, uint64_t, index, gameObjectUUID); + } + + return array; +} + +mono_bool +InternalCall_Rendering_ScriptableRenderPipelineScenePlanningContext_UseDefaultRequests( + uint64_t nativeHandle) { + ManagedScriptableRenderPipelineScenePlanningContextState* const state = + FindManagedScriptableRenderPipelineScenePlanningContextState( + nativeHandle); + if (state == nullptr || + state->scene == nullptr || + state->outputRequests == nullptr) { + return 0; + } + + state->outputRequests->clear(); + ResetManagedScenePlanningRequestCounters(*state); + + Rendering::SceneRenderRequestPlanner requestPlanner = {}; + requestPlanner.SetDirectionalShadowPlanningSettings( + state->directionalShadowPlanningSettings); + const std::vector cameras = + requestPlanner.CollectCameras( + *state->scene, + state->overrideCamera); + for (Components::CameraComponent* camera : cameras) { + AppendManagedScenePlanningCameraRequest(*state, camera); + } + + return 1; +} + +void InternalCall_Rendering_ScriptableRenderPipelineScenePlanningContext_ClearRequests( + uint64_t nativeHandle) { + ManagedScriptableRenderPipelineScenePlanningContextState* const state = + FindManagedScriptableRenderPipelineScenePlanningContextState( + nativeHandle); + if (state == nullptr || state->outputRequests == nullptr) { + return; + } + + state->outputRequests->clear(); + ResetManagedScenePlanningRequestCounters(*state); +} + +mono_bool +InternalCall_Rendering_ScriptableRenderPipelineScenePlanningContext_AddCamera( + uint64_t nativeHandle, + uint64_t cameraGameObjectUUID) { + ManagedScriptableRenderPipelineScenePlanningContextState* const state = + FindManagedScriptableRenderPipelineScenePlanningContextState( + nativeHandle); + if (state == nullptr || state->scene == nullptr) { + return 0; + } + + Components::GameObject* const gameObject = + FindGameObjectByUUIDInScene( + *state->scene, + cameraGameObjectUUID); + Components::CameraComponent* const camera = + gameObject != nullptr + ? gameObject->GetComponent() + : nullptr; + return AppendManagedScenePlanningCameraRequest(*state, camera) + ? 1 + : 0; +} + int32_t InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_GetRendererIndex( uint64_t nativeHandle) { @@ -7414,6 +7770,11 @@ void RegisterInternalCalls() { mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_RecordScenePhase", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_RecordScenePhase)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_DrawRenderersByDesc", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_DrawRenderersByDesc)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_CreateRendererList", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_CreateRendererList)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelineScenePlanningContext_GetRequestCount", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelineScenePlanningContext_GetRequestCount)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelineScenePlanningContext_GetDefaultCameraGameObjectUUIDs", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelineScenePlanningContext_GetDefaultCameraGameObjectUUIDs)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelineScenePlanningContext_UseDefaultRequests", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelineScenePlanningContext_UseDefaultRequests)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelineScenePlanningContext_ClearRequests", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelineScenePlanningContext_ClearRequests)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelineScenePlanningContext_AddCamera", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelineScenePlanningContext_AddCamera)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelinePlanningContext_GetRendererIndex", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_GetRendererIndex)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelinePlanningContext_GetFramePlanId", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_GetFramePlanId)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelinePlanningContext_IsStageRequested", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_IsStageRequested)); @@ -7521,6 +7882,8 @@ void MonoScriptRuntime::Shutdown() { GetManagedRenderSceneSetupContextNextHandle() = 1; GetManagedDirectionalShadowExecutionContextRegistry().clear(); GetManagedDirectionalShadowExecutionContextNextHandle() = 1; + GetManagedScriptableRenderPipelineScenePlanningContextRegistry().clear(); + GetManagedScriptableRenderPipelineScenePlanningContextNextHandle() = 1; GetManagedScriptableRenderPipelinePlanningContextRegistry().clear(); GetManagedScriptableRenderPipelinePlanningContextNextHandle() = 1; ClearManagedInstances(); @@ -7542,6 +7905,7 @@ void MonoScriptRuntime::Shutdown() { m_cameraRenderRequestContextClass = nullptr; m_renderSceneSetupContextClass = nullptr; m_directionalShadowExecutionContextClass = nullptr; + m_scriptableRenderPipelineScenePlanningContextClass = nullptr; m_scriptableRenderPipelinePlanningContextClass = nullptr; m_serializeFieldAttributeClass = nullptr; m_gameObjectConstructor = nullptr; @@ -7549,6 +7913,7 @@ void MonoScriptRuntime::Shutdown() { m_cameraRenderRequestContextConstructor = nullptr; m_renderSceneSetupContextConstructor = nullptr; m_directionalShadowExecutionContextConstructor = nullptr; + m_scriptableRenderPipelineScenePlanningContextConstructor = nullptr; m_scriptableRenderPipelinePlanningContextConstructor = nullptr; m_managedGameObjectUUIDField = nullptr; m_gameObjectUUIDField = nullptr; @@ -8385,6 +8750,28 @@ bool MonoScriptRuntime::DiscoverScriptClasses() { return false; } + m_scriptableRenderPipelineScenePlanningContextClass = + mono_class_from_name( + m_coreImage, + kManagedRenderingNamespace, + "ScriptableRenderPipelineScenePlanningContext"); + if (!m_scriptableRenderPipelineScenePlanningContextClass) { + SetError( + "Failed to locate the managed ScriptableRenderPipelineScenePlanningContext type."); + return false; + } + + m_scriptableRenderPipelineScenePlanningContextConstructor = + mono_class_get_method_from_name( + m_scriptableRenderPipelineScenePlanningContextClass, + ".ctor", + 1); + if (!m_scriptableRenderPipelineScenePlanningContextConstructor) { + SetError( + "Failed to locate the managed ScriptableRenderPipelineScenePlanningContext constructor."); + return false; + } + m_scriptableRenderPipelinePlanningContextClass = mono_class_from_name( m_coreImage, kManagedRenderingNamespace, @@ -9294,6 +9681,46 @@ MonoScriptRuntime::CreateManagedDirectionalShadowExecutionContext( return contextObject; } +MonoObject* +MonoScriptRuntime::CreateManagedScriptableRenderPipelineScenePlanningContext( + uint64_t nativeHandle) { + if (!m_initialized || + nativeHandle == 0 || + m_scriptableRenderPipelineScenePlanningContextClass == nullptr || + m_scriptableRenderPipelineScenePlanningContextConstructor == nullptr) { + return nullptr; + } + + SetCurrentDomain(); + + MonoObject* const contextObject = + mono_object_new( + m_appDomain, + m_scriptableRenderPipelineScenePlanningContextClass); + if (contextObject == nullptr) { + SetError( + "Mono failed to allocate a managed ScriptableRenderPipelineScenePlanningContext."); + return nullptr; + } + + void* args[1]; + uint64_t nativeHandleArgument = nativeHandle; + args[0] = &nativeHandleArgument; + + MonoObject* exception = nullptr; + mono_runtime_invoke( + m_scriptableRenderPipelineScenePlanningContextConstructor, + contextObject, + args, + &exception); + if (exception != nullptr) { + RecordException(exception); + return nullptr; + } + + return contextObject; +} + MonoObject* MonoScriptRuntime::CreateManagedScriptableRenderPipelinePlanningContext( uint64_t nativeHandle) { diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index fb7451a0..6fe72519 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -204,6 +204,7 @@ set(XCENGINE_SCRIPT_CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderContext.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipeline.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineScenePlanningContext.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelinePlanningContext.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/SortingSettings.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/StencilFaceState.cs diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index 5207b7c5..beeb7e21 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -2122,6 +2122,42 @@ namespace Gameplay } } + public sealed class ManagedScenePlanningRenderPipelineProbeAsset + : UniversalRenderPipelineAsset + { + public ManagedScenePlanningRenderPipelineProbeAsset() + { + rendererDataList = + ProbeScriptableObjectFactory + .CreateRendererDataList( + ProbeScriptableObjectFactory + .Create()); + } + + protected override bool BuildSceneRenderRequests( + ScriptableRenderPipelineScenePlanningContext context) + { + if (context == null) + { + return false; + } + + Camera[] cameras = context.GetDefaultCameras(); + context.ClearRequests(); + for (int index = 0; index < cameras.Length; ++index) + { + Camera camera = cameras[index]; + if (camera != null && + camera.gameObject.Name == "SelectedCamera") + { + return context.AddCamera(camera); + } + } + + return true; + } + } + public sealed class ManagedDefaultRendererSelectionProbeAsset : UniversalRenderPipelineAsset { diff --git a/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs b/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs index 706b5f46..ec103100 100644 --- a/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs +++ b/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs @@ -49,6 +49,10 @@ namespace Gameplay public bool HasScriptableObjectType; public bool HasScriptableObjectCreateInstance; public bool HasRenderPipelineAssetScriptableObjectBase; + public bool HasScenePlanningContextType; + public bool HasPublicScenePlanningContextUseDefaultRequests; + public bool HasPublicScenePlanningContextAddCamera; + public bool HasPipelineAssetBuildSceneRenderRequests; public bool HasPlanningContextType; public bool HasPublicPlanningContextFramePlanId; public bool HasRendererFeatureConfigureCameraFramePlan; @@ -181,6 +185,8 @@ namespace Gameplay typeof(ScriptableRenderContext); System.Type requestContextType = typeof(CameraRenderRequestContext); + System.Type scenePlanningContextType = + typeof(ScriptableRenderPipelineScenePlanningContext); System.Type planningContextType = typeof(ScriptableRenderPipelinePlanningContext); System.Type pipelineAssetType = @@ -420,6 +426,22 @@ namespace Gameplay HasRenderPipelineAssetScriptableObjectBase = scriptableObjectType != null && pipelineAssetType.BaseType == scriptableObjectType; + HasScenePlanningContextType = + scenePlanningContextType != null; + HasPublicScenePlanningContextUseDefaultRequests = + scenePlanningContextType.GetMethod( + "UseDefaultRequests", + PublicInstanceMethodFlags) != null; + HasPublicScenePlanningContextAddCamera = + scenePlanningContextType.GetMethod( + "AddCamera", + PublicInstanceMethodFlags) != null; + HasPipelineAssetBuildSceneRenderRequests = + pipelineAssetType.GetMethod( + "BuildSceneRenderRequests", + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic) != null; HasPlanningContextType = planningContextType != null; HasPublicPlanningContextFramePlanId = diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalRenderPipelineAsset.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalRenderPipelineAsset.cs index 16805f5f..27c09e39 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalRenderPipelineAsset.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/UniversalRenderPipelineAsset.cs @@ -35,6 +35,12 @@ namespace XCEngine.Rendering.Universal return new UniversalRenderPipeline(this); } + protected override bool BuildSceneRenderRequests( + ScriptableRenderPipelineScenePlanningContext context) + { + return context != null && context.UseDefaultRequests(); + } + protected override FinalColorSettings GetDefaultFinalColorSettings() { diff --git a/managed/XCEngine.ScriptCore/InternalCalls.cs b/managed/XCEngine.ScriptCore/InternalCalls.cs index da38dc69..a0abf574 100644 --- a/managed/XCEngine.ScriptCore/InternalCalls.cs +++ b/managed/XCEngine.ScriptCore/InternalCalls.cs @@ -587,6 +587,32 @@ namespace XCEngine Rendering_CommandBuffer_DrawSkybox( ulong nativeHandle); + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern int + Rendering_ScriptableRenderPipelineScenePlanningContext_GetRequestCount( + ulong nativeHandle); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern ulong[] + Rendering_ScriptableRenderPipelineScenePlanningContext_GetDefaultCameraGameObjectUUIDs( + ulong nativeHandle); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool + Rendering_ScriptableRenderPipelineScenePlanningContext_UseDefaultRequests( + ulong nativeHandle); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void + Rendering_ScriptableRenderPipelineScenePlanningContext_ClearRequests( + ulong nativeHandle); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool + Rendering_ScriptableRenderPipelineScenePlanningContext_AddCamera( + ulong nativeHandle, + ulong cameraGameObjectUUID); + [MethodImpl(MethodImplOptions.InternalCall)] internal static extern int Rendering_ScriptableRenderPipelinePlanningContext_GetRendererIndex( diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs index e10fc7ab..914442c9 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs @@ -36,6 +36,12 @@ namespace XCEngine.Rendering ConfigureCameraFramePlan(context); } + internal bool BuildSceneRenderRequestsInstance( + ScriptableRenderPipelineScenePlanningContext context) + { + return BuildSceneRenderRequests(context); + } + internal bool ConfigureRenderSceneSetupInstance( RenderSceneSetupContext context) { @@ -60,6 +66,12 @@ namespace XCEngine.Rendering return null; } + protected virtual bool BuildSceneRenderRequests( + ScriptableRenderPipelineScenePlanningContext context) + { + return context != null && context.UseDefaultRequests(); + } + protected virtual void ConfigureCameraRenderRequest( CameraRenderRequestContext context) { diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineScenePlanningContext.cs b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineScenePlanningContext.cs new file mode 100644 index 00000000..fb57aaf4 --- /dev/null +++ b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineScenePlanningContext.cs @@ -0,0 +1,71 @@ +using System; +using XCEngine; + +namespace XCEngine.Rendering +{ + public sealed class ScriptableRenderPipelineScenePlanningContext + { + private readonly ulong m_nativeHandle; + + internal ScriptableRenderPipelineScenePlanningContext( + ulong nativeHandle) + { + m_nativeHandle = nativeHandle; + } + + public int requestCount => + InternalCalls + .Rendering_ScriptableRenderPipelineScenePlanningContext_GetRequestCount( + m_nativeHandle); + + public Camera[] GetDefaultCameras() + { + ulong[] cameraGameObjectUUIDs = + InternalCalls + .Rendering_ScriptableRenderPipelineScenePlanningContext_GetDefaultCameraGameObjectUUIDs( + m_nativeHandle); + if (cameraGameObjectUUIDs == null || + cameraGameObjectUUIDs.Length == 0) + { + return Array.Empty(); + } + + Camera[] cameras = + new Camera[cameraGameObjectUUIDs.Length]; + for (int index = 0; index < cameraGameObjectUUIDs.Length; ++index) + { + ulong gameObjectUUID = cameraGameObjectUUIDs[index]; + cameras[index] = + gameObjectUUID != 0 + ? new Camera(gameObjectUUID) + : null; + } + + return cameras; + } + + public bool UseDefaultRequests() + { + return InternalCalls + .Rendering_ScriptableRenderPipelineScenePlanningContext_UseDefaultRequests( + m_nativeHandle); + } + + public void ClearRequests() + { + InternalCalls + .Rendering_ScriptableRenderPipelineScenePlanningContext_ClearRequests( + m_nativeHandle); + } + + public bool AddCamera(Camera camera) + { + return InternalCalls + .Rendering_ScriptableRenderPipelineScenePlanningContext_AddCamera( + m_nativeHandle, + camera != null + ? camera.GameObjectUUID + : 0); + } + } +} diff --git a/mvs/editor/src/Platform/Win32Utf8.h b/mvs/editor/src/Platform/Win32Utf8.h index 92fc70a2..899ab90a 100644 --- a/mvs/editor/src/Platform/Win32Utf8.h +++ b/mvs/editor/src/Platform/Win32Utf8.h @@ -1,6 +1,10 @@ #pragma once #include + +#ifndef NOMINMAX +#define NOMINMAX +#endif #include namespace XCEngine { diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index 1338a289..6a599f15 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -69,6 +69,7 @@ set(EDITOR_APP_CORE_TEST_SOURCES test_editor_runtime_coordinator.cpp test_editor_scene_runtime_backend.cpp test_editor_shell_asset_validation.cpp + test_editor_window_frame_orchestrator.cpp test_project_browser_model.cpp test_hierarchy_scene_binding.cpp test_inspector_presentation.cpp @@ -136,6 +137,7 @@ if(TARGET XCEditorCore) ${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Host/Interfaces ${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Services ${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Support + ${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app/Windowing ${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/include ${CMAKE_SOURCE_DIR}/engine/include ) diff --git a/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp b/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp index b3867564..dd20b7d0 100644 --- a/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp +++ b/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp @@ -4,7 +4,9 @@ #include "Commands/EditorEditCommandRoute.h" #include "Commands/EditorHostCommandBridge.h" #include "State/EditorCommandFocusService.h" -#include "State/EditorSession.h" +#include + +#include namespace { @@ -12,9 +14,17 @@ using XCEngine::UI::Editor::App::EditorActionRoute; using XCEngine::UI::Editor::App::EditorCommandFocusService; using XCEngine::UI::Editor::App::EditorEditCommandRoute; using XCEngine::UI::Editor::App::EditorHostCommandBridge; -using XCEngine::UI::Editor::App::EditorSession; using XCEngine::UI::Editor::UIEditorHostCommandDispatchResult; using XCEngine::UI::Editor::UIEditorHostCommandEvaluationResult; +using XCEngine::UI::Editor::UIEditorWorkspaceController; +using XCEngine::UI::Editor::UIEditorWorkspaceModel; + +UIEditorWorkspaceController MakeWorkspaceController( + std::string_view activePanelId = {}) { + UIEditorWorkspaceModel workspace = {}; + workspace.activePanelId = std::string(activePanelId); + return UIEditorWorkspaceController({}, std::move(workspace), {}); +} class StubEditCommandRoute final : public EditorEditCommandRoute { public: @@ -106,9 +116,9 @@ public: }; TEST(EditorHostCommandBridgeTest, HierarchyEditCommandsDelegateToBoundRoute) { - EditorSession session = {}; EditorCommandFocusService commandFocus = {}; commandFocus.ClaimFocus(EditorActionRoute::Hierarchy); + UIEditorWorkspaceController controller = MakeWorkspaceController(); StubEditCommandRoute hierarchyRoute = {}; hierarchyRoute.evaluationResult.executable = true; @@ -117,38 +127,36 @@ TEST(EditorHostCommandBridgeTest, HierarchyEditCommandsDelegateToBoundRoute) { hierarchyRoute.dispatchResult.message = "Hierarchy rename dispatched."; EditorHostCommandBridge bridge = {}; - bridge.BindSession(session); bridge.BindCommandFocusService(commandFocus); bridge.BindEditCommandRoutes(&hierarchyRoute, nullptr, nullptr); const UIEditorHostCommandEvaluationResult evaluation = - bridge.EvaluateHostCommand("edit.rename"); + bridge.EvaluateHostCommand("edit.rename", controller); EXPECT_TRUE(evaluation.executable); EXPECT_EQ(evaluation.message, "Hierarchy route owns rename."); EXPECT_EQ(hierarchyRoute.lastEvaluatedCommandId, "edit.rename"); const UIEditorHostCommandDispatchResult dispatch = - bridge.DispatchHostCommand("edit.rename"); + bridge.DispatchHostCommand("edit.rename", controller); EXPECT_TRUE(dispatch.commandExecuted); EXPECT_EQ(dispatch.message, "Hierarchy rename dispatched."); EXPECT_EQ(hierarchyRoute.lastDispatchedCommandId, "edit.rename"); } TEST(EditorHostCommandBridgeTest, UnsupportedHostCommandsUseHonestMessages) { - EditorSession session = {}; + UIEditorWorkspaceController controller = MakeWorkspaceController(); EditorHostCommandBridge bridge = {}; - bridge.BindSession(session); const UIEditorHostCommandEvaluationResult aboutEvaluation = - bridge.EvaluateHostCommand("help.about"); + bridge.EvaluateHostCommand("help.about", controller); EXPECT_FALSE(aboutEvaluation.executable); EXPECT_EQ( aboutEvaluation.message, "About dialog is unavailable in the current shell."); const UIEditorHostCommandEvaluationResult fileEvaluation = - bridge.EvaluateHostCommand("file.save_scene"); + bridge.EvaluateHostCommand("file.save_scene", controller); EXPECT_FALSE(fileEvaluation.executable); EXPECT_EQ( fileEvaluation.message, @@ -156,6 +164,7 @@ TEST(EditorHostCommandBridgeTest, UnsupportedHostCommandsUseHonestMessages) { } TEST(EditorHostCommandBridgeTest, RuntimeCommandsDelegateToBoundRuntimeOwner) { + UIEditorWorkspaceController controller = MakeWorkspaceController(); StubRuntimeCommandOwner runtimeOwner = {}; runtimeOwner.fileEvaluationResult.executable = true; runtimeOwner.fileEvaluationResult.message = "Runtime owner can save."; @@ -171,38 +180,38 @@ TEST(EditorHostCommandBridgeTest, RuntimeCommandsDelegateToBoundRuntimeOwner) { bridge.BindRuntimeCommandOwner(&runtimeOwner); const UIEditorHostCommandEvaluationResult fileEvaluation = - bridge.EvaluateHostCommand("file.save_scene"); + bridge.EvaluateHostCommand("file.save_scene", controller); EXPECT_TRUE(fileEvaluation.executable); EXPECT_EQ(fileEvaluation.message, "Runtime owner can save."); EXPECT_EQ(runtimeOwner.lastEvaluatedFileCommandId, "file.save_scene"); const UIEditorHostCommandDispatchResult fileDispatch = - bridge.DispatchHostCommand("file.save_scene"); + bridge.DispatchHostCommand("file.save_scene", controller); EXPECT_TRUE(fileDispatch.commandExecuted); EXPECT_EQ(fileDispatch.message, "Runtime owner saved."); EXPECT_EQ(runtimeOwner.lastDispatchedFileCommandId, "file.save_scene"); const UIEditorHostCommandEvaluationResult runEvaluation = - bridge.EvaluateHostCommand("run.play"); + bridge.EvaluateHostCommand("run.play", controller); EXPECT_TRUE(runEvaluation.executable); EXPECT_EQ(runEvaluation.message, "Runtime owner can play."); EXPECT_EQ(runtimeOwner.lastEvaluatedRunCommandId, "run.play"); const UIEditorHostCommandDispatchResult runDispatch = - bridge.DispatchHostCommand("run.play"); + bridge.DispatchHostCommand("run.play", controller); EXPECT_TRUE(runDispatch.commandExecuted); EXPECT_EQ(runDispatch.message, "Runtime owner played."); EXPECT_EQ(runtimeOwner.lastDispatchedRunCommandId, "run.play"); const UIEditorHostCommandEvaluationResult scriptEvaluation = - bridge.EvaluateHostCommand("scripts.rebuild"); + bridge.EvaluateHostCommand("scripts.rebuild", controller); EXPECT_FALSE(scriptEvaluation.executable); EXPECT_EQ(scriptEvaluation.message, "Runtime owns scripts honestly."); EXPECT_EQ(runtimeOwner.lastEvaluatedScriptCommandId, "scripts.rebuild"); } TEST(EditorHostCommandBridgeTest, AssetCommandsDelegateToProjectRoute) { - EditorSession session = {}; + UIEditorWorkspaceController controller = MakeWorkspaceController(); StubEditCommandRoute projectRoute = {}; projectRoute.assetEvaluationResult.executable = true; @@ -211,26 +220,25 @@ TEST(EditorHostCommandBridgeTest, AssetCommandsDelegateToProjectRoute) { projectRoute.assetDispatchResult.message = "Project create folder dispatched."; EditorHostCommandBridge bridge = {}; - bridge.BindSession(session); bridge.BindEditCommandRoutes(nullptr, &projectRoute, nullptr); const UIEditorHostCommandEvaluationResult evaluation = - bridge.EvaluateHostCommand("assets.create_folder"); + bridge.EvaluateHostCommand("assets.create_folder", controller); EXPECT_TRUE(evaluation.executable); EXPECT_EQ(evaluation.message, "Project route owns create folder."); EXPECT_EQ(projectRoute.lastEvaluatedAssetCommandId, "assets.create_folder"); const UIEditorHostCommandDispatchResult dispatch = - bridge.DispatchHostCommand("assets.create_folder"); + bridge.DispatchHostCommand("assets.create_folder", controller); EXPECT_TRUE(dispatch.commandExecuted); EXPECT_EQ(dispatch.message, "Project create folder dispatched."); EXPECT_EQ(projectRoute.lastDispatchedAssetCommandId, "assets.create_folder"); } TEST(EditorHostCommandBridgeTest, SceneEditCommandsDelegateToBoundSceneRoute) { - EditorSession session = {}; EditorCommandFocusService commandFocus = {}; commandFocus.ClaimFocus(EditorActionRoute::Scene); + UIEditorWorkspaceController controller = MakeWorkspaceController(); StubEditCommandRoute sceneRoute = {}; sceneRoute.evaluationResult.executable = true; @@ -239,27 +247,26 @@ TEST(EditorHostCommandBridgeTest, SceneEditCommandsDelegateToBoundSceneRoute) { sceneRoute.dispatchResult.message = "Scene undo dispatched."; EditorHostCommandBridge bridge = {}; - bridge.BindSession(session); bridge.BindCommandFocusService(commandFocus); bridge.BindEditCommandRoutes(nullptr, nullptr, &sceneRoute); const UIEditorHostCommandEvaluationResult evaluation = - bridge.EvaluateHostCommand("edit.undo"); + bridge.EvaluateHostCommand("edit.undo", controller); EXPECT_TRUE(evaluation.executable); EXPECT_EQ(evaluation.message, "Scene route owns undo."); EXPECT_EQ(sceneRoute.lastEvaluatedCommandId, "edit.undo"); const UIEditorHostCommandDispatchResult dispatch = - bridge.DispatchHostCommand("edit.undo"); + bridge.DispatchHostCommand("edit.undo", controller); EXPECT_TRUE(dispatch.commandExecuted); EXPECT_EQ(dispatch.message, "Scene undo dispatched."); EXPECT_EQ(sceneRoute.lastDispatchedCommandId, "edit.undo"); } TEST(EditorHostCommandBridgeTest, InspectorEditCommandsDelegateToBoundInspectorRoute) { - EditorSession session = {}; EditorCommandFocusService commandFocus = {}; commandFocus.ClaimFocus(EditorActionRoute::Inspector); + UIEditorWorkspaceController controller = MakeWorkspaceController(); StubEditCommandRoute inspectorRoute = {}; inspectorRoute.evaluationResult.executable = true; @@ -268,44 +275,42 @@ TEST(EditorHostCommandBridgeTest, InspectorEditCommandsDelegateToBoundInspectorR inspectorRoute.dispatchResult.message = "Inspector delete dispatched."; EditorHostCommandBridge bridge = {}; - bridge.BindSession(session); bridge.BindCommandFocusService(commandFocus); bridge.BindEditCommandRoutes(nullptr, nullptr, nullptr, &inspectorRoute); const UIEditorHostCommandEvaluationResult evaluation = - bridge.EvaluateHostCommand("edit.delete"); + bridge.EvaluateHostCommand("edit.delete", controller); EXPECT_TRUE(evaluation.executable); EXPECT_EQ(evaluation.message, "Inspector route owns delete."); EXPECT_EQ(inspectorRoute.lastEvaluatedCommandId, "edit.delete"); const UIEditorHostCommandDispatchResult dispatch = - bridge.DispatchHostCommand("edit.delete"); + bridge.DispatchHostCommand("edit.delete", controller); EXPECT_TRUE(dispatch.commandExecuted); EXPECT_EQ(dispatch.message, "Inspector delete dispatched."); EXPECT_EQ(inspectorRoute.lastDispatchedCommandId, "edit.delete"); } TEST(EditorHostCommandBridgeTest, ActivePanelRouteIsUsedAsFallbackWhenNoExplicitCommandFocusExists) { - EditorSession session = {}; - session.activePanelId = XCEngine::UI::Editor::App::kHierarchyPanelId; + UIEditorWorkspaceController controller = + MakeWorkspaceController(XCEngine::UI::Editor::App::kHierarchyPanelId); StubEditCommandRoute hierarchyRoute = {}; hierarchyRoute.evaluationResult.executable = true; hierarchyRoute.evaluationResult.message = "Hierarchy route owns rename."; EditorHostCommandBridge bridge = {}; - bridge.BindSession(session); bridge.BindEditCommandRoutes(&hierarchyRoute, nullptr, nullptr); const UIEditorHostCommandEvaluationResult evaluation = - bridge.EvaluateHostCommand("edit.rename"); + bridge.EvaluateHostCommand("edit.rename", controller); EXPECT_TRUE(evaluation.executable); EXPECT_EQ(evaluation.message, "Hierarchy route owns rename."); } TEST(EditorHostCommandBridgeTest, ExplicitCommandFocusOverridesActivePanelFallback) { - EditorSession session = {}; - session.activePanelId = XCEngine::UI::Editor::App::kProjectPanelId; + UIEditorWorkspaceController controller = + MakeWorkspaceController(XCEngine::UI::Editor::App::kProjectPanelId); EditorCommandFocusService commandFocus = {}; commandFocus.ClaimFocus(EditorActionRoute::Scene); @@ -319,12 +324,11 @@ TEST(EditorHostCommandBridgeTest, ExplicitCommandFocusOverridesActivePanelFallba sceneRoute.evaluationResult.message = "Scene route."; EditorHostCommandBridge bridge = {}; - bridge.BindSession(session); bridge.BindCommandFocusService(commandFocus); bridge.BindEditCommandRoutes(nullptr, &projectRoute, &sceneRoute); const UIEditorHostCommandEvaluationResult evaluation = - bridge.EvaluateHostCommand("edit.undo"); + bridge.EvaluateHostCommand("edit.undo", controller); EXPECT_TRUE(evaluation.executable); EXPECT_EQ(evaluation.message, "Scene route."); EXPECT_EQ(sceneRoute.lastEvaluatedCommandId, "edit.undo"); diff --git a/tests/UI/Editor/unit/test_editor_runtime_coordinator.cpp b/tests/UI/Editor/unit/test_editor_runtime_coordinator.cpp index 4cbfb6f4..8d5a6444 100644 --- a/tests/UI/Editor/unit/test_editor_runtime_coordinator.cpp +++ b/tests/UI/Editor/unit/test_editor_runtime_coordinator.cpp @@ -167,7 +167,8 @@ Scene* FakeEditorScenePlaySession::GetRuntimeScene() const { } struct RuntimeCoordinatorHarness { - RuntimeCoordinatorHarness() { + RuntimeCoordinatorHarness( + EditorRuntimePaths runtimePaths = {}) { auto backend = std::make_unique(); backendPtr = backend.get(); sceneRuntime.SetBackend(std::move(backend)); @@ -178,7 +179,7 @@ struct RuntimeCoordinatorHarness { session, sceneRuntime, projectRuntime, - EditorRuntimePaths{}, + runtimePaths, startupScene); } @@ -200,6 +201,18 @@ TEST(EditorRuntimeCoordinatorTests, InitializeProjectsStartupSceneDocumentStateT EXPECT_EQ(harness.session.runtimeMode, EditorRuntimeMode::Edit); } +TEST(EditorRuntimeCoordinatorTests, ScriptRebuildEvaluationIsNoLongerHardcodedStub) { + EditorRuntimePaths runtimePaths = {}; + runtimePaths.projectRoot = "D:/Project"; + RuntimeCoordinatorHarness harness(runtimePaths); + + const UIEditorHostCommandEvaluationResult evaluation = + harness.coordinator.EvaluateScriptCommand("scripts.rebuild"); + EXPECT_NE( + evaluation.message, + "Script rebuild is owned by the runtime coordinator, but no in-process script assembly builder is bound."); +} + TEST(EditorRuntimeCoordinatorTests, NewSceneProjectsUnsavedDirtyDocumentStateToSession) { RuntimeCoordinatorHarness harness = {}; diff --git a/tests/UI/Editor/unit/test_editor_window_frame_orchestrator.cpp b/tests/UI/Editor/unit/test_editor_window_frame_orchestrator.cpp new file mode 100644 index 00000000..2d45c5ea --- /dev/null +++ b/tests/UI/Editor/unit/test_editor_window_frame_orchestrator.cpp @@ -0,0 +1,199 @@ +#include + +#include "Frame/EditorWindowFrameOrchestrator.h" + +#include +#include +#include + +namespace XCEngine::UI::Editor::App { +namespace { + +class FakeFrameServices final : public EditorFrameServices { +public: + void AttachTextMeasurer(const UIEditorTextMeasurer& textMeasurer) override { + m_textMeasurer = &textMeasurer; + } + + bool IsValid() const override { + return true; + } + + const std::string& GetValidationMessage() const override { + return m_validationMessage; + } + + const UIEditorTextMeasurer* GetTextMeasurer() const override { + return m_textMeasurer; + } + + UIEditorShellInteractionDefinition BuildShellDefinition( + const UIEditorWorkspaceController&, + std::string_view, + EditorShellVariant) const override { + return {}; + } + + std::optional ConsumeOpenUtilityWindowRequest() override { + std::optional request = m_pendingUtilityWindow; + m_pendingUtilityWindow.reset(); + return request; + } + + void SetStatus(std::string, std::string) override {} + + void UpdateStatusFromShellResult( + const UIEditorWorkspaceController&, + const UIEditorShellInteractionResult&) override {} + + std::string DescribeWorkspaceState( + const UIEditorWorkspaceController&, + const UIEditorShellInteractionState&) const override { + return {}; + } + + std::vector SyncWorkspacePanelFrameEvents( + const std::vector&) override { + return {}; + } + + void SyncSceneViewportRenderRequest( + EditorSceneViewportRuntime&) override {} + + std::optional m_pendingUtilityWindow = + EditorUtilityWindowKind::None; + +private: + const UIEditorTextMeasurer* m_textMeasurer = nullptr; + std::string m_validationMessage = {}; +}; + +class FakeWorkspaceShellRuntime final : public EditorWorkspaceShellRuntime { +public: + void Initialize( + const EditorRuntimePaths&, + Rendering::Host::UiTextureHost&, + Host::EditorHostResourceService&, + UIEditorTextMeasurer&, + SceneViewportEngineBridge&, + GameViewportEngineBridge&, + EditorShaderProvider&) override {} + + void Shutdown() override {} + void ResetInteractionState() override {} + + void AttachViewportWindowRenderer(Rendering::Host::ViewportRenderHost&) override {} + void DetachViewportWindowRenderer() override {} + void SetViewportSurfacePresentationEnabled(bool) override {} + + void Update( + EditorFrameServices&, + UIEditorWorkspaceController&, + const ::XCEngine::UI::UIRect&, + const std::vector<::XCEngine::UI::UIInputEvent>&, + std::string_view, + EditorShellVariant, + bool, + float, + float) override { + ++updateCallCount; + } + + void RenderRequestedViewports( + EditorFrameServices&, + const ::XCEngine::Rendering::RenderContext&) override {} + + void Append(::XCEngine::UI::UIDrawData&) const override {} + + const UIEditorShellInteractionFrame& GetShellFrame() const override { + return shellFrame; + } + + const UIEditorShellInteractionState& GetShellInteractionState() const override { + return shellInteractionState; + } + + const std::vector& GetTraceEntries() const override { + return traceEntries; + } + + void SetExternalDockHostDropPreview( + const Widgets::UIEditorDockHostDropPreviewState&) override {} + + void ClearExternalDockHostDropPreview() override {} + + EditorWorkspaceShellCursorKind GetHostedContentCursorKind() const override { + return EditorWorkspaceShellCursorKind::Arrow; + } + + Widgets::UIEditorDockHostCursorKind GetDockCursorKind() const override { + return Widgets::UIEditorDockHostCursorKind::Arrow; + } + + bool TryResolveDockTabDragHotspot( + std::string_view, + std::string_view, + const ::XCEngine::UI::UIPoint&, + ::XCEngine::UI::UIPoint&) const override { + return false; + } + + UIEditorDockHostTabDropTarget ResolveDockTabDropTarget( + const ::XCEngine::UI::UIPoint&) const override { + return {}; + } + + bool HasHostedContentCapture() const override { + return false; + } + + bool HasShellInteractiveCapture() const override { + return false; + } + + bool HasInteractiveCapture() const override { + return false; + } + + int updateCallCount = 0; + UIEditorShellInteractionFrame shellFrame = {}; + UIEditorShellInteractionState shellInteractionState = {}; + std::vector traceEntries = {}; +}; + +TEST(EditorWindowFrameOrchestratorTests, UtilityWindowRequestsFlowThroughFrameTransferRequests) { + EditorWindowFrameOrchestrator orchestrator = {}; + FakeFrameServices frameServices = {}; + FakeWorkspaceShellRuntime shellRuntime = {}; + UIEditorWorkspaceController workspaceController = {}; + ::XCEngine::UI::UIDrawData drawData = {}; + + frameServices.m_pendingUtilityWindow = EditorUtilityWindowKind::ColorPicker; + + const EditorWindowFrameTransferRequests transferRequests = + orchestrator.UpdateAndAppend( + frameServices, + workspaceController, + shellRuntime, + ::XCEngine::UI::UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + {}, + EditorWindowScreenPoint{ 320, 240 }, + {}, + true, + false, + false, + drawData); + + EXPECT_EQ(shellRuntime.updateCallCount, 1); + ASSERT_TRUE(transferRequests.utility.openUtilityWindow.has_value()); + EXPECT_EQ( + transferRequests.utility.openUtilityWindow->kind, + EditorUtilityWindowKind::ColorPicker); + EXPECT_TRUE(transferRequests.utility.openUtilityWindow->useCursorPlacement); + EXPECT_EQ(transferRequests.utility.openUtilityWindow->screenPoint.x, 320); + EXPECT_EQ(transferRequests.utility.openUtilityWindow->screenPoint.y, 240); + EXPECT_FALSE(frameServices.ConsumeOpenUtilityWindowRequest().has_value()); +} + +} // namespace +} // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_inspector_presentation.cpp b/tests/UI/Editor/unit/test_inspector_presentation.cpp index f9037a05..35d00e5a 100644 --- a/tests/UI/Editor/unit/test_inspector_presentation.cpp +++ b/tests/UI/Editor/unit/test_inspector_presentation.cpp @@ -11,11 +11,15 @@ #include #include #include +#include +#include +#include #include #include #include +#include namespace XCEngine::UI::Editor::App { namespace { @@ -23,9 +27,114 @@ namespace { using ::XCEngine::Components::GameObject; using ::XCEngine::Components::Scene; using ::XCEngine::Components::SceneManager; +using ::XCEngine::Scripting::ComponentReference; +using ::XCEngine::Scripting::GameObjectReference; +using ::XCEngine::Scripting::IScriptRuntime; +using ::XCEngine::Scripting::ScriptClassDescriptor; +using ::XCEngine::Scripting::ScriptComponent; +using ::XCEngine::Scripting::ScriptEngine; +using ::XCEngine::Scripting::ScriptFieldDefaultValue; +using ::XCEngine::Scripting::ScriptFieldMetadata; +using ::XCEngine::Scripting::ScriptFieldType; +using ::XCEngine::Scripting::ScriptFieldValue; +using ::XCEngine::Scripting::ScriptLifecycleMethod; +using ::XCEngine::Scripting::ScriptPhysicsMessage; +using ::XCEngine::Scripting::ScriptRuntimeContext; using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridFieldKind; using Widgets::UIEditorPropertyGridSection; +class FakeInspectorScriptRuntime final : public IScriptRuntime { +public: + void OnRuntimeStart(Scene*) override {} + void OnRuntimeStop(Scene*) override {} + + bool TryGetAvailableScriptClasses( + std::vector& outClasses) const override { + outClasses = scriptClasses; + return true; + } + + bool TryGetAvailableRenderPipelineAssetClasses( + std::vector& outClasses) const override { + outClasses = {}; + return true; + } + + bool TryGetClassFieldMetadata( + const std::string&, + const std::string&, + const std::string&, + std::vector& outFields) const override { + outFields = fieldMetadata; + return !outFields.empty(); + } + + bool TryGetClassFieldDefaultValues( + const std::string&, + const std::string&, + const std::string&, + std::vector& outFields) const override { + outFields = fieldDefaultValues; + return !outFields.empty(); + } + + bool TrySetManagedFieldValue( + const ScriptRuntimeContext&, + const std::string& fieldName, + const ScriptFieldValue& value) override { + managedFieldValues[fieldName] = value; + return true; + } + + bool TryGetManagedFieldValue( + const ScriptRuntimeContext&, + const std::string& fieldName, + ScriptFieldValue& outValue) const override { + const auto it = managedFieldValues.find(fieldName); + if (it == managedFieldValues.end()) { + return false; + } + + outValue = it->second; + return true; + } + + void SyncManagedFieldsToStorage(const ScriptRuntimeContext&) override {} + bool CreateScriptInstance(const ScriptRuntimeContext&) override { return true; } + void DestroyScriptInstance(const ScriptRuntimeContext&) override {} + void InvokeMethod( + const ScriptRuntimeContext&, + ScriptLifecycleMethod, + float) override {} + void InvokePhysicsMessage( + const ScriptRuntimeContext&, + ScriptPhysicsMessage, + GameObject*) override {} + + std::vector scriptClasses = {}; + std::vector fieldMetadata = {}; + std::vector fieldDefaultValues = {}; + std::unordered_map managedFieldValues = {}; +}; + +class ScopedScriptRuntimeOverride final { +public: + explicit ScopedScriptRuntimeOverride(IScriptRuntime& runtime) + : m_previousRuntime(ScriptEngine::Get().GetRuntime()) { + ScriptEngine::Get().OnRuntimeStop(); + ScriptEngine::Get().SetRuntime(&runtime); + } + + ~ScopedScriptRuntimeOverride() { + ScriptEngine::Get().OnRuntimeStop(); + ScriptEngine::Get().SetRuntime(m_previousRuntime); + } + +private: + IScriptRuntime* m_previousRuntime = nullptr; +}; + class ScopedSceneManagerReset final { public: ScopedSceneManagerReset() { @@ -317,6 +426,178 @@ TEST(InspectorPresentationModelTests, CameraSkyboxMaterialBuildsAssetField) { "Skybox.mat"); } +TEST(InspectorPresentationModelTests, ScriptComponentBuildsScriptSelectorSection) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot); + + EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + Scene* scene = SceneManager::Get().GetActiveScene(); + ASSERT_NE(scene, nullptr); + GameObject* parent = scene->Find("Parent"); + ASSERT_NE(parent, nullptr); + ASSERT_NE(parent->AddComponent<::XCEngine::Scripting::ScriptComponent>(), nullptr); + ASSERT_TRUE(runtime.SetSelection(parent->GetID())); + + const InspectorPresentationModel model = + BuildInspectorPresentationModel( + BuildInspectorSubject(EditorSession{}, runtime), + runtime, + InspectorComponentEditorRegistry::Get()); + + const auto* scriptSection = FindSection(model, "Script"); + ASSERT_NE(scriptSection, nullptr); + const auto* scriptField = FindField(*scriptSection, "Script"); + ASSERT_NE(scriptField, nullptr); + EXPECT_EQ(scriptField->kind, Widgets::UIEditorPropertyGridFieldKind::Enum); + EXPECT_FALSE(scriptField->enumValue.options.empty()); + EXPECT_EQ(scriptField->enumValue.options.front(), "None"); + + const auto* scriptBinding = FindBinding(model, "ScriptComponent"); + ASSERT_NE(scriptBinding, nullptr); + EXPECT_EQ(scriptBinding->displayName, "Script"); + EXPECT_TRUE(scriptBinding->removable); +} + +TEST(InspectorPresentationModelTests, ScriptComponentReferenceFieldsSupportActivationAndClear) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot); + + FakeInspectorScriptRuntime scriptRuntime = {}; + scriptRuntime.scriptClasses.push_back(ScriptClassDescriptor{ + "GameScripts", + "Gameplay", + "Probe" + }); + scriptRuntime.fieldMetadata = { + ScriptFieldMetadata{ "Target", ScriptFieldType::GameObject }, + ScriptFieldMetadata{ "TargetScript", ScriptFieldType::Component } + }; + ScopedScriptRuntimeOverride runtimeOverride(scriptRuntime); + + EditorSceneRuntime runtime = {}; + BindEngineSceneBackend(runtime); + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + Scene* scene = SceneManager::Get().GetActiveScene(); + ASSERT_NE(scene, nullptr); + + GameObject* parent = scene->Find("Parent"); + GameObject* child = scene->Find("Child"); + ASSERT_NE(parent, nullptr); + ASSERT_NE(child, nullptr); + + ScriptComponent* parentScript = parent->AddComponent(); + ScriptComponent* childScript = child->AddComponent(); + ASSERT_NE(parentScript, nullptr); + ASSERT_NE(childScript, nullptr); + parentScript->SetScriptClass("GameScripts", "Gameplay", "Probe"); + childScript->SetScriptClass("GameScripts", "Gameplay", "Probe"); + parentScript->GetFieldStorage().SetFieldValue( + "Target", + GameObjectReference{ child->GetUUID() }); + parentScript->GetFieldStorage().SetFieldValue( + "TargetScript", + ComponentReference{ + child->GetUUID(), + childScript->GetScriptComponentUUID() + }); + + ASSERT_TRUE(runtime.SetSelection(parent->GetID())); + + InspectorSubject subject = BuildInspectorSubject(EditorSession{}, runtime); + const InspectorPresentationModel model = + BuildInspectorPresentationModel( + subject, + runtime, + InspectorComponentEditorRegistry::Get()); + + const UIEditorPropertyGridSection* fieldsSection = + FindSection(model, "Script Fields"); + ASSERT_NE(fieldsSection, nullptr); + const UIEditorPropertyGridField* targetField = FindField(*fieldsSection, "Target"); + const UIEditorPropertyGridField* targetScriptField = + FindField(*fieldsSection, "TargetScript"); + ASSERT_NE(targetField, nullptr); + ASSERT_NE(targetScriptField, nullptr); + EXPECT_EQ(targetField->kind, UIEditorPropertyGridFieldKind::Asset); + EXPECT_EQ(targetField->assetValue.displayName, "Child"); + EXPECT_EQ(targetScriptField->kind, UIEditorPropertyGridFieldKind::Asset); + EXPECT_EQ(targetScriptField->assetValue.displayName, "Child"); + + const InspectorPresentationComponentBinding* binding = + FindBinding(model, "ScriptComponent"); + ASSERT_NE(binding, nullptr); + const std::vector descriptors = + runtime.GetSelectedComponents(); + InspectorComponentEditorContext editorContext = {}; + editorContext.gameObject = &subject.sceneObject.object; + editorContext.componentId = binding->componentId; + editorContext.typeName = binding->typeName; + editorContext.displayName = binding->displayName; + editorContext.removable = binding->removable; + for (const EditorSceneComponentDescriptor& descriptor : descriptors) { + if (descriptor.componentId == binding->componentId) { + editorContext.component = descriptor.view.get(); + break; + } + } + ASSERT_NE(editorContext.component, nullptr); + + const IInspectorComponentEditor* editor = + InspectorComponentEditorRegistry::Get().FindEditor("ScriptComponent"); + ASSERT_NE(editor, nullptr); + ASSERT_TRUE(editor->HandleFieldActivation(runtime, editorContext, *targetField)); + ASSERT_TRUE(runtime.GetSelectedObjectId().has_value()); + EXPECT_EQ(runtime.GetSelectedObjectId().value(), child->GetID()); + + ASSERT_TRUE(runtime.SetSelection(parent->GetID())); + subject = BuildInspectorSubject(EditorSession{}, runtime); + const InspectorPresentationModel refreshedModel = + BuildInspectorPresentationModel( + subject, + runtime, + InspectorComponentEditorRegistry::Get()); + fieldsSection = FindSection(refreshedModel, "Script Fields"); + ASSERT_NE(fieldsSection, nullptr); + targetField = FindField(*fieldsSection, "Target"); + targetScriptField = FindField(*fieldsSection, "TargetScript"); + ASSERT_NE(targetField, nullptr); + ASSERT_NE(targetScriptField, nullptr); + + UIEditorPropertyGridField clearedTargetField = *targetField; + clearedTargetField.assetValue.assetId.clear(); + clearedTargetField.assetValue.displayName.clear(); + clearedTargetField.assetValue.statusText.clear(); + ASSERT_TRUE(ApplyInspectorComponentBoundFieldValue( + runtime, + subject.sceneObject, + *FindBinding(refreshedModel, "ScriptComponent"), + clearedTargetField)); + + UIEditorPropertyGridField clearedTargetScriptField = *targetScriptField; + clearedTargetScriptField.assetValue.assetId.clear(); + clearedTargetScriptField.assetValue.displayName.clear(); + clearedTargetScriptField.assetValue.statusText.clear(); + ASSERT_TRUE(ApplyInspectorComponentBoundFieldValue( + runtime, + subject.sceneObject, + *FindBinding(refreshedModel, "ScriptComponent"), + clearedTargetScriptField)); + + GameObjectReference targetReference = {}; + ComponentReference targetScriptReference = {}; + ASSERT_TRUE(parentScript->GetFieldStorage().TryGetFieldValue("Target", targetReference)); + ASSERT_TRUE(parentScript->GetFieldStorage().TryGetFieldValue( + "TargetScript", + targetScriptReference)); + EXPECT_EQ(targetReference.gameObjectUUID, 0u); + EXPECT_EQ(targetScriptReference.gameObjectUUID, 0u); + EXPECT_EQ(targetScriptReference.scriptComponentUUID, 0u); +} + TEST(InspectorPresentationModelTests, BoundFieldValueApplierKeepsComponentViewAliveDuringApply) { ScopedSceneManagerReset reset = {}; TemporaryProjectRoot projectRoot = {}; diff --git a/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp index 4992f140..d3bd1d30 100644 --- a/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp @@ -24,6 +24,10 @@ using XCEngine::UI::Editor::UIEditorCommandDispatchStatus; using XCEngine::UI::Editor::UIEditorCommandDispatcher; using XCEngine::UI::Editor::UIEditorCommandPanelSource; using XCEngine::UI::Editor::UIEditorCommandRegistry; +using XCEngine::UI::Editor::UIEditorCommandKind; +using XCEngine::UI::Editor::UIEditorHostCommandDispatchResult; +using XCEngine::UI::Editor::UIEditorHostCommandEvaluationResult; +using XCEngine::UI::Editor::UIEditorHostCommandHandler; using XCEngine::UI::Editor::UIEditorMenuCheckedStateSource; using XCEngine::UI::Editor::UIEditorMenuItemKind; using XCEngine::UI::Editor::UIEditorMenuDescriptor; @@ -42,6 +46,7 @@ using XCEngine::UI::Editor::UIEditorShellInteractionPopupItemRequest; using XCEngine::UI::Editor::UIEditorShellInteractionServices; using XCEngine::UI::Editor::UIEditorShellInteractionRequest; using XCEngine::UI::Editor::UIEditorShellInteractionState; +using XCEngine::UI::Editor::UIEditorShellInteractionToolbarButtonRequest; using XCEngine::UI::Editor::UIEditorTextMeasureRequest; using XCEngine::UI::Editor::UIEditorTextMeasurer; using XCEngine::UI::Editor::UIEditorWorkspaceController; @@ -64,6 +69,28 @@ public: } }; +class StubHostCommandHandler final : public UIEditorHostCommandHandler { +public: + UIEditorHostCommandEvaluationResult EvaluateHostCommand( + std::string_view commandId, + const UIEditorWorkspaceController&) const override { + lastEvaluatedCommandId = std::string(commandId); + return evaluationResult; + } + + UIEditorHostCommandDispatchResult DispatchHostCommand( + std::string_view commandId, + UIEditorWorkspaceController&) override { + lastDispatchedCommandId = std::string(commandId); + return dispatchResult; + } + + mutable std::string lastEvaluatedCommandId = {}; + std::string lastDispatchedCommandId = {}; + UIEditorHostCommandEvaluationResult evaluationResult = {}; + UIEditorHostCommandDispatchResult dispatchResult = {}; +}; + UIEditorPanelRegistry BuildPanelRegistry() { UIEditorPanelRegistry registry = {}; registry.panels = { @@ -202,6 +229,25 @@ UIEditorShellInteractionModel BuildInteractionModel() { return model; } +UIEditorCommandRegistry BuildHostCommandRegistry() { + UIEditorCommandRegistry registry = {}; + + XCEngine::UI::Editor::UIEditorCommandDescriptor playCommand = {}; + playCommand.commandId = "run.play"; + playCommand.displayName = "Play"; + playCommand.kind = UIEditorCommandKind::Host; + playCommand.workspaceCommand.panelSource = UIEditorCommandPanelSource::None; + + XCEngine::UI::Editor::UIEditorCommandDescriptor pauseCommand = {}; + pauseCommand.commandId = "run.pause"; + pauseCommand.displayName = "Pause"; + pauseCommand.kind = UIEditorCommandKind::Host; + pauseCommand.workspaceCommand.panelSource = UIEditorCommandPanelSource::None; + + registry.commands = { playCommand, pauseCommand }; + return registry; +} + UIEditorShellInteractionDefinition BuildInteractionDefinition() { UIEditorMenuItemDescriptor focusInspector = {}; focusInspector.kind = UIEditorMenuItemKind::Command; @@ -227,6 +273,15 @@ UIEditorShellInteractionDefinition BuildInteractionDefinition() { return definition; } +UIEditorShellInteractionModel BuildToolbarInteractionModel() { + UIEditorShellInteractionModel model = BuildInteractionModel(); + model.toolbarButtons = { + { "run.play", 1u, true }, + { "run.pause", 2u, true } + }; + return model; +} + UIEditorWorkspaceController BuildController() { return BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); } @@ -337,6 +392,18 @@ const UIEditorShellInteractionPopupItemRequest* FindPopupItem( return nullptr; } +const UIEditorShellInteractionToolbarButtonRequest* FindToolbarButton( + const UIEditorShellInteractionFrame& frame, + std::string_view buttonId) { + for (const auto& button : frame.request.toolbarButtons) { + if (button.buttonId == buttonId) { + return &button; + } + } + + return nullptr; +} + UIPoint RectCenter(const UIRect& rect) { return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f); } @@ -561,6 +628,88 @@ TEST(UIEditorShellInteractionTest, ClickCommandDispatchesInsideShellAndUpdatesWo EXPECT_TRUE(frame.request.popupRequests.empty()); } +TEST(UIEditorShellInteractionTest, ToolbarButtonDispatchesHostCommandWhenEnabled) { + auto controller = BuildController(); + const auto model = BuildToolbarInteractionModel(); + UIEditorCommandDispatcher dispatcher(BuildHostCommandRegistry()); + StubHostCommandHandler hostHandler = {}; + hostHandler.evaluationResult.executable = true; + hostHandler.dispatchResult.commandExecuted = true; + hostHandler.dispatchResult.message = "Play dispatched."; + dispatcher.SetHostCommandHandler(&hostHandler); + + UIEditorShellInteractionServices services = {}; + services.commandDispatcher = &dispatcher; + + UIEditorShellInteractionState state = {}; + const auto frame = UpdateUIEditorShellInteraction( + state, + controller, + kShellBounds, + model, + {}, + services); + + const auto* playButton = FindToolbarButton(frame, "run.play"); + ASSERT_NE(playButton, nullptr); + + const auto clickedFrame = UpdateUIEditorShellInteraction( + state, + controller, + kShellBounds, + model, + { MakeLeftPointerDown(RectCenter(playButton->rect)) }, + services); + + EXPECT_TRUE(clickedFrame.result.consumed); + EXPECT_TRUE(clickedFrame.result.commandTriggered); + EXPECT_TRUE(clickedFrame.result.commandDispatched); + EXPECT_EQ(clickedFrame.result.commandId, "run.play"); + EXPECT_EQ( + clickedFrame.result.commandDispatchResult.status, + UIEditorCommandDispatchStatus::Dispatched); + EXPECT_TRUE(clickedFrame.result.commandDispatchResult.commandExecuted); + EXPECT_EQ(hostHandler.lastDispatchedCommandId, "run.play"); +} + +TEST(UIEditorShellInteractionTest, ToolbarButtonUsesCommandEvaluationForEnabledState) { + auto controller = BuildController(); + const auto model = BuildToolbarInteractionModel(); + UIEditorCommandDispatcher dispatcher(BuildHostCommandRegistry()); + StubHostCommandHandler hostHandler = {}; + hostHandler.evaluationResult.executable = false; + hostHandler.evaluationResult.message = "Disabled."; + dispatcher.SetHostCommandHandler(&hostHandler); + + UIEditorShellInteractionServices services = {}; + services.commandDispatcher = &dispatcher; + + UIEditorShellInteractionState state = {}; + const auto frame = UpdateUIEditorShellInteraction( + state, + controller, + kShellBounds, + model, + {}, + services); + + const auto* pauseButton = FindToolbarButton(frame, "run.pause"); + ASSERT_NE(pauseButton, nullptr); + EXPECT_FALSE(pauseButton->enabled); + + const auto clickedFrame = UpdateUIEditorShellInteraction( + state, + controller, + kShellBounds, + model, + { MakeLeftPointerDown(RectCenter(pauseButton->rect)) }, + services); + + EXPECT_FALSE(clickedFrame.result.commandTriggered); + EXPECT_FALSE(clickedFrame.result.commandDispatched); + EXPECT_TRUE(hostHandler.lastDispatchedCommandId.empty()); +} + TEST(UIEditorShellInteractionTest, DefinitionContractRefreshesResolvedMenuAfterCommandDispatch) { auto controller = BuildController(); const UIEditorShellInteractionDefinition definition = BuildInteractionDefinition(); diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index de00826b..93f8bfca 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -1539,6 +1539,73 @@ TEST_F( EXPECT_FLOAT_EQ(observedPostProcessScale.w, 1.0f); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineAssetBuildsSceneRequestsThroughManagedScenePlanningContext) { + Scene* runtimeScene = + CreateScene("ManagedScenePlanningRenderPipelineScene"); + + GameObject* rejectedCameraObject = + runtimeScene->CreateGameObject("RejectedCamera"); + auto* rejectedCamera = + rejectedCameraObject->AddComponent(); + ASSERT_NE(rejectedCamera, nullptr); + rejectedCamera->SetPrimary(true); + rejectedCamera->SetDepth(0.0f); + + GameObject* selectedCameraObject = + runtimeScene->CreateGameObject("SelectedCamera"); + auto* selectedCamera = + selectedCameraObject->AddComponent(); + ASSERT_NE(selectedCamera, nullptr); + selectedCamera->SetPrimary(true); + selectedCamera->SetDepth(1.0f); + + engine->OnRuntimeStart(runtimeScene); + + TestRenderDevice device; + TestRenderCommandList commandList; + TestRenderCommandQueue commandQueue; + TestRenderResourceView colorView( + XCEngine::RHI::ResourceViewType::RenderTarget, + XCEngine::RHI::ResourceViewDimension::Texture2D, + XCEngine::RHI::Format::R8G8B8A8_UNorm); + TestRenderResourceView depthView( + XCEngine::RHI::ResourceViewType::DepthStencil, + XCEngine::RHI::ResourceViewDimension::Texture2D, + XCEngine::RHI::Format::D32_Float); + + const XCEngine::Rendering::RenderContext context = + CreateRenderContext( + device, + commandList, + commandQueue); + XCEngine::Rendering::RenderSurface surface(64u, 64u); + surface.SetColorAttachment(&colorView); + surface.SetDepthAttachment(&depthView); + + auto asset = + std::make_shared< + XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset>( + XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor{ + "GameScripts", + "Gameplay", + "ManagedScenePlanningRenderPipelineProbeAsset"}); + + XCEngine::Rendering::RenderPipelineHost host(asset); + const std::vector plans = + host.BuildFramePlans( + *runtimeScene, + nullptr, + context, + surface); + + ASSERT_EQ(plans.size(), 1u); + EXPECT_EQ(plans[0].request.camera, selectedCamera); + EXPECT_NE(plans[0].request.camera, rejectedCamera); + EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError(); +} + TEST_F( MonoScriptRuntimeTest, DefaultSceneRendererUsesScriptCoreUniversalRendererFeatureForPlannedPostProcessRender) { @@ -1739,6 +1806,10 @@ TEST_F( bool hasPublicPipelineAssetConfigureCameraFramePlan = false; bool hasPipelineAssetSetDirty = false; bool hasPipelineAssetGetRuntimeResourceVersion = false; + bool hasScenePlanningContextType = false; + bool hasPublicScenePlanningContextUseDefaultRequests = false; + bool hasPublicScenePlanningContextAddCamera = false; + bool hasPipelineAssetBuildSceneRenderRequests = false; bool hasPlanningContextType = false; bool hasPublicPlanningContextFramePlanId = false; bool hasRendererFeatureConfigureCameraFramePlan = false; @@ -1915,6 +1986,22 @@ TEST_F( selectionScript, "HasPipelineAssetGetRuntimeResourceVersion", hasPipelineAssetGetRuntimeResourceVersion)); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "HasScenePlanningContextType", + hasScenePlanningContextType)); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "HasPublicScenePlanningContextUseDefaultRequests", + hasPublicScenePlanningContextUseDefaultRequests)); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "HasPublicScenePlanningContextAddCamera", + hasPublicScenePlanningContextAddCamera)); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "HasPipelineAssetBuildSceneRenderRequests", + hasPipelineAssetBuildSceneRenderRequests)); EXPECT_TRUE(runtime->TryGetFieldValue( selectionScript, "HasPlanningContextType", @@ -2106,6 +2193,10 @@ TEST_F( EXPECT_TRUE(hasPublicPipelineAssetConfigureCameraFramePlan); EXPECT_TRUE(hasPipelineAssetSetDirty); EXPECT_TRUE(hasPipelineAssetGetRuntimeResourceVersion); + EXPECT_TRUE(hasScenePlanningContextType); + EXPECT_TRUE(hasPublicScenePlanningContextUseDefaultRequests); + EXPECT_TRUE(hasPublicScenePlanningContextAddCamera); + EXPECT_TRUE(hasPipelineAssetBuildSceneRenderRequests); EXPECT_TRUE(hasPlanningContextType); EXPECT_TRUE(hasPublicPlanningContextFramePlanId); EXPECT_TRUE(hasRendererFeatureConfigureCameraFramePlan);