From 052ac28aa37540684fdf4115582974e855ffd39c Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 3 Apr 2026 16:26:20 +0800 Subject: [PATCH] Introduce scene viewport overlay providers --- ...neView_Gizmo系统完整审查与正式化重构方案.md | 42 ++ editor/CMakeLists.txt | 1 + .../Viewport/SceneViewportOverlayBuilder.cpp | 411 +------------- .../Viewport/SceneViewportOverlayBuilder.h | 20 +- .../SceneViewportOverlayProviders.cpp | 502 ++++++++++++++++++ .../Viewport/SceneViewportOverlayProviders.h | 66 +++ editor/src/Viewport/ViewportHostService.h | 3 +- tests/editor/CMakeLists.txt | 4 + .../test_application_asset_cache_stub.cpp | 45 ++ .../test_scene_viewport_overlay_providers.cpp | 210 ++++++++ 10 files changed, 905 insertions(+), 399 deletions(-) create mode 100644 editor/src/Viewport/SceneViewportOverlayProviders.cpp create mode 100644 editor/src/Viewport/SceneViewportOverlayProviders.h create mode 100644 tests/editor/test_application_asset_cache_stub.cpp create mode 100644 tests/editor/test_scene_viewport_overlay_providers.cpp diff --git a/docs/plan/Unity式SceneView_Gizmo系统完整审查与正式化重构方案.md b/docs/plan/Unity式SceneView_Gizmo系统完整审查与正式化重构方案.md index 725f9689..f4831d16 100644 --- a/docs/plan/Unity式SceneView_Gizmo系统完整审查与正式化重构方案.md +++ b/docs/plan/Unity式SceneView_Gizmo系统完整审查与正式化重构方案.md @@ -1004,3 +1004,45 @@ ISceneViewportGizmoProvider - 合并 `SceneViewportOverlayFrameData` 这一步完成后,当前 SceneView gizmo 系统才算真正进入“可持续扩展”的第二阶段。 + +--- + +## 13. 进度更新 2026-04-03 Phase 3A 已完成 + +本次已完成 `Phase 3A: Overlay Provider Registry 落地`,且已经通过代码、测试与 editor 编译验证。 + +已落地内容: + +1. 新增 `SceneViewportOverlayBuildContext` +2. 新增 `ISceneViewportOverlayProvider` +3. 新增 `SceneViewportOverlayProviderRegistry` +4. 新增默认 provider: + - `SceneViewportCameraOverlayProvider` + - `SceneViewportLightOverlayProvider` +5. `SceneViewportOverlayBuilder` 已从大单体逻辑改为 provider 聚合器 +6. `ViewportHostService` 已改为持有 `SceneViewportOverlayBuilder` 实例,而不是依赖静态构建入口 + +本阶段行为保持不变的部分: + +- `BuildSceneViewEditorOverlayContentSignature(...)` 未改 +- `SceneViewPanel` 的 transient transform gizmo 注入链路未改 +- orientation gizmo / HUD 分层未改 +- grid / outline pass 未改 + +新增验证: + +1. `SceneViewportOverlayProviderRegistryTest.AppendsProvidersInRegistrationOrder` +2. `SceneViewportOverlayProviderRegistryTest.CameraProviderBuildsSceneIconAndSelectedFrustum` +3. `SceneViewportOverlayProviderRegistryTest.LightProviderBuildsSceneIconAndSelectedDirectionalHelper` +4. `SceneViewportOverlayProviderRegistryTest.OverlayBuilderUsesDefaultRegistryToAggregateCameraAndLightProviders` + +已执行验证: + +- `cmake --build build --config Debug --target editor_tests -- /p:BuildProjectReferences=false` +- `build/tests/Editor/Debug/editor_tests.exe --gtest_filter=SceneViewportOverlayProviderRegistryTest.*:ViewportRenderFlowUtilsTest.*` +- `cmake --build build --config Debug --target XCEditor` + +阶段结论: + +- Phase 3A 已完成并可作为后续 `Phase 3B: Transform Gizmo 并入正式 Provider 体系` 的稳定起点 +- 当前 `camera/light overlay` 的职责边界已经从单体 builder 中抽离,但 `transform gizmo` 仍然保留在 transient 注入链路,需要下一阶段继续收口 diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 0d5b1fe6..6dbf7a5e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -84,6 +84,7 @@ add_executable(${PROJECT_NAME} WIN32 src/Viewport/SceneViewportScaleGizmo.cpp src/Viewport/SceneViewportOrientationGizmo.cpp src/Viewport/SceneViewportOverlayBuilder.cpp + src/Viewport/SceneViewportOverlayProviders.cpp src/Viewport/SceneViewportOverlayRenderer.cpp src/Viewport/Passes/SceneViewportEditorOverlayPass.cpp src/Viewport/Passes/SceneViewportGridPass.cpp diff --git a/editor/src/Viewport/SceneViewportOverlayBuilder.cpp b/editor/src/Viewport/SceneViewportOverlayBuilder.cpp index 3172e202..d16adc95 100644 --- a/editor/src/Viewport/SceneViewportOverlayBuilder.cpp +++ b/editor/src/Viewport/SceneViewportOverlayBuilder.cpp @@ -2,392 +2,29 @@ #include "Core/IEditorContext.h" #include "Core/ISceneManager.h" -#include "SceneViewportOverlayHandleBuilder.h" -#include "SceneViewportMath.h" -#include -#include -#include -#include -#include #include -#include -#include -#include +#include namespace XCEngine { namespace Editor { -namespace { - -bool CanBuildOverlayForGameObject(const Components::GameObject* gameObject) { - return gameObject != nullptr && - gameObject->GetTransform() != nullptr && - gameObject->IsActiveInHierarchy(); +SceneViewportOverlayBuilder::SceneViewportOverlayBuilder() + : m_providerRegistry(BuildDefaultSceneViewportOverlayProviderRegistry()) { } -float ResolveCameraAspect( - const Components::CameraComponent& camera, - uint32_t viewportWidth, - uint32_t viewportHeight) { - const Math::Rect viewportRect = camera.GetViewportRect(); - const float resolvedWidth = static_cast(viewportWidth) * - (viewportRect.width > Math::EPSILON ? viewportRect.width : 1.0f); - const float resolvedHeight = static_cast(viewportHeight) * - (viewportRect.height > Math::EPSILON ? viewportRect.height : 1.0f); - return resolvedHeight > Math::EPSILON - ? resolvedWidth / resolvedHeight - : 1.0f; +SceneViewportOverlayBuilder::SceneViewportOverlayBuilder( + SceneViewportOverlayProviderRegistry providerRegistry) + : m_providerRegistry(std::move(providerRegistry)) { } -float ComputeWorldUnitsPerPixel( - const SceneViewportOverlayData& overlay, - const Math::Vector3& worldPoint, - uint32_t viewportHeight) { - if (!overlay.valid || viewportHeight <= 1u) { - return 0.0f; - } - - const Math::Vector3 cameraForward = overlay.cameraForward.Normalized(); - const float depth = Math::Vector3::Dot(worldPoint - overlay.cameraPosition, cameraForward); - if (depth <= Math::EPSILON) { - return 0.0f; - } - - return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / - static_cast(viewportHeight); -} - -void AppendWorldLine( - SceneViewportOverlayFrameData& frameData, - const Math::Vector3& startWorld, - const Math::Vector3& endWorld, - const Math::Color& color, - float thicknessPixels, - SceneViewportOverlayDepthMode depthMode) { - SceneViewportOverlayLinePrimitive& line = frameData.worldLines.emplace_back(); - line.startWorld = startWorld; - line.endWorld = endWorld; - line.color = color; - line.thicknessPixels = thicknessPixels; - line.depthMode = depthMode; -} - -void AppendWorldSprite( - SceneViewportOverlayFrameData& frameData, - const Math::Vector3& worldPosition, - const Math::Vector2& sizePixels, - const Math::Color& tintColor, - float sortDepth, - uint64_t entityId, - SceneViewportOverlaySpriteTextureKind textureKind, - SceneViewportOverlayDepthMode depthMode) { - if (entityId == 0 || sizePixels.x <= Math::EPSILON || sizePixels.y <= Math::EPSILON) { - return; - } - - SceneViewportOverlaySpritePrimitive& sprite = frameData.worldSprites.emplace_back(); - sprite.worldPosition = worldPosition; - sprite.sizePixels = sizePixels; - sprite.tintColor = tintColor; - sprite.sortDepth = sortDepth; - sprite.entityId = entityId; - sprite.textureKind = textureKind; - sprite.depthMode = depthMode; -} - -void AppendHandleRecord( - SceneViewportOverlayFrameData& frameData, - SceneViewportOverlayHandleKind kind, - uint64_t handleId, - uint64_t entityId, - const Math::Vector3& worldPosition, - const Math::Vector2& sizePixels, - float sortDepth) { - if (kind == SceneViewportOverlayHandleKind::None || - handleId == 0 || - entityId == 0 || - sizePixels.x <= Math::EPSILON || - sizePixels.y <= Math::EPSILON) { - return; - } - - SceneViewportOverlayHandleRecord& handleRecord = frameData.handleRecords.emplace_back(); - handleRecord.kind = kind; - handleRecord.handleId = handleId; - handleRecord.entityId = entityId; - handleRecord.shape = SceneViewportOverlayHandleShape::WorldRect; - handleRecord.priority = Detail::kSceneViewportHandlePrioritySceneIcon; - handleRecord.worldPosition = worldPosition; - handleRecord.sizePixels = sizePixels; - handleRecord.sortDepth = sortDepth; -} - -void AppendSceneIconOverlay( - SceneViewportOverlayFrameData& frameData, - const SceneViewportOverlayData& overlay, - uint32_t viewportWidth, - uint32_t viewportHeight, - const Components::GameObject& gameObject, - const Math::Vector2& sizePixels, - SceneViewportOverlaySpriteTextureKind textureKind) { - const Components::TransformComponent* transform = gameObject.GetTransform(); - if (transform == nullptr) { - return; - } - - const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint( - overlay, - static_cast(viewportWidth), - static_cast(viewportHeight), - transform->GetPosition()); - if (!projectedPoint.visible) { - return; - } - - AppendWorldSprite( - frameData, - transform->GetPosition(), - sizePixels, - Math::Color::White(), - projectedPoint.ndcDepth, - gameObject.GetID(), - textureKind, - SceneViewportOverlayDepthMode::AlwaysOnTop); - AppendHandleRecord( - frameData, - SceneViewportOverlayHandleKind::SceneIcon, - gameObject.GetID(), - gameObject.GetID(), - transform->GetPosition(), - sizePixels, - projectedPoint.ndcDepth); -} - -void AppendCameraFrustumOverlay( - SceneViewportOverlayFrameData& frameData, - const Components::CameraComponent& camera, - const Components::GameObject& gameObject, - uint32_t viewportWidth, - uint32_t viewportHeight) { - const Components::TransformComponent* transform = gameObject.GetTransform(); - if (transform == nullptr) { - return; - } - - const Math::Vector3 position = transform->GetPosition(); - const Math::Vector3 forward = transform->GetForward().Normalized(); - const Math::Vector3 right = transform->GetRight().Normalized(); - const Math::Vector3 up = transform->GetUp().Normalized(); - if (forward.SqrMagnitude() <= Math::EPSILON || - right.SqrMagnitude() <= Math::EPSILON || - up.SqrMagnitude() <= Math::EPSILON) { - return; - } - - const float nearClip = (std::max)(camera.GetNearClipPlane(), 0.01f); - const float farClip = (std::max)(camera.GetFarClipPlane(), nearClip + 0.01f); - const float aspect = ResolveCameraAspect(camera, viewportWidth, viewportHeight); - - float nearHalfHeight = 0.0f; - float nearHalfWidth = 0.0f; - float farHalfHeight = 0.0f; - float farHalfWidth = 0.0f; - if (camera.GetProjectionType() == Components::CameraProjectionType::Perspective) { - const float halfFovRadians = - std::clamp(camera.GetFieldOfView(), 1.0f, 179.0f) * Math::DEG_TO_RAD * 0.5f; - nearHalfHeight = std::tan(halfFovRadians) * nearClip; - nearHalfWidth = nearHalfHeight * aspect; - farHalfHeight = std::tan(halfFovRadians) * farClip; - farHalfWidth = farHalfHeight * aspect; - } else { - const float halfHeight = (std::max)(camera.GetOrthographicSize(), 0.01f); - const float halfWidth = halfHeight * aspect; - nearHalfHeight = halfHeight; - nearHalfWidth = halfWidth; - farHalfHeight = halfHeight; - farHalfWidth = halfWidth; - } - - const Math::Vector3 nearCenter = position + forward * nearClip; - const Math::Vector3 farCenter = position + forward * farClip; - const std::array corners = {{ - nearCenter + up * nearHalfHeight - right * nearHalfWidth, - nearCenter + up * nearHalfHeight + right * nearHalfWidth, - nearCenter - up * nearHalfHeight + right * nearHalfWidth, - nearCenter - up * nearHalfHeight - right * nearHalfWidth, - farCenter + up * farHalfHeight - right * farHalfWidth, - farCenter + up * farHalfHeight + right * farHalfWidth, - farCenter - up * farHalfHeight + right * farHalfWidth, - farCenter - up * farHalfHeight - right * farHalfWidth - }}; - - static constexpr std::array, 12> kFrustumEdges = {{ - { 0u, 1u }, { 1u, 2u }, { 2u, 3u }, { 3u, 0u }, - { 4u, 5u }, { 5u, 6u }, { 6u, 7u }, { 7u, 4u }, - { 0u, 4u }, { 1u, 5u }, { 2u, 6u }, { 3u, 7u } - }}; - constexpr Math::Color kFrustumColor(1.0f, 1.0f, 1.0f, 1.0f); - - for (const auto& edge : kFrustumEdges) { - AppendWorldLine( - frameData, - corners[edge.first], - corners[edge.second], - kFrustumColor, - 1.6f, - SceneViewportOverlayDepthMode::AlwaysOnTop); - } -} - -void AppendDirectionalLightOverlay( - SceneViewportOverlayFrameData& frameData, - const Components::GameObject& gameObject, - const SceneViewportOverlayData& overlay, - uint32_t viewportHeight) { - const Components::TransformComponent* transform = gameObject.GetTransform(); - if (transform == nullptr) { - return; - } - - const Math::Vector3 position = transform->GetPosition(); - const Math::Vector3 lightDirection = (transform->GetForward() * -1.0f).Normalized(); - const Math::Vector3 right = transform->GetRight().Normalized(); - const Math::Vector3 up = transform->GetUp().Normalized(); - if (lightDirection.SqrMagnitude() <= Math::EPSILON || - right.SqrMagnitude() <= Math::EPSILON || - up.SqrMagnitude() <= Math::EPSILON) { - return; - } - - const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(overlay, position, viewportHeight); - if (worldUnitsPerPixel <= Math::EPSILON) { - return; - } - - constexpr Math::Color kDirectionalLightColor(1.0f, 0.92f, 0.24f, 1.0f); - constexpr float kLineThickness = 1.8f; - constexpr size_t kRingSegmentCount = 32; - constexpr std::array kRayAngles = {{ - 0.0f, - Math::PI / 3.0f, - Math::PI * 2.0f / 3.0f, - Math::PI, - Math::PI * 4.0f / 3.0f, - Math::PI * 5.0f / 3.0f - }}; - - const float ringRadius = worldUnitsPerPixel * 26.0f; - const float ringOffset = worldUnitsPerPixel * 54.0f; - const float innerRayRadius = ringRadius * 0.52f; - const float rayLength = worldUnitsPerPixel * 96.0f; - const Math::Vector3 ringCenter = position + lightDirection * ringOffset; - - for (size_t segmentIndex = 0; segmentIndex < kRingSegmentCount; ++segmentIndex) { - const float angle0 = - static_cast(segmentIndex) / static_cast(kRingSegmentCount) * Math::PI * 2.0f; - const float angle1 = - static_cast(segmentIndex + 1u) / static_cast(kRingSegmentCount) * Math::PI * 2.0f; - const Math::Vector3 p0 = - ringCenter + right * std::cos(angle0) * ringRadius + up * std::sin(angle0) * ringRadius; - const Math::Vector3 p1 = - ringCenter + right * std::cos(angle1) * ringRadius + up * std::sin(angle1) * ringRadius; - AppendWorldLine( - frameData, - p0, - p1, - kDirectionalLightColor, - kLineThickness, - SceneViewportOverlayDepthMode::AlwaysOnTop); - } - - AppendWorldLine( - frameData, - position, - ringCenter, - kDirectionalLightColor, - kLineThickness, - SceneViewportOverlayDepthMode::AlwaysOnTop); - AppendWorldLine( - frameData, - ringCenter, - ringCenter + lightDirection * rayLength, - kDirectionalLightColor, - kLineThickness, - SceneViewportOverlayDepthMode::AlwaysOnTop); - - for (float angle : kRayAngles) { - const Math::Vector3 rayStart = - ringCenter + right * std::cos(angle) * innerRayRadius + up * std::sin(angle) * innerRayRadius; - AppendWorldLine( - frameData, - rayStart, - rayStart + lightDirection * rayLength, - kDirectionalLightColor, - kLineThickness, - SceneViewportOverlayDepthMode::AlwaysOnTop); - } -} - -void AppendSceneObjectIconOverlays( - SceneViewportOverlayFrameData& frameData, - const Components::Scene& scene, - const SceneViewportOverlayData& overlay, - uint32_t viewportWidth, - uint32_t viewportHeight) { - constexpr Math::Vector2 kCameraIconSize(90.0f, 90.0f); - constexpr Math::Vector2 kLightIconSize(100.0f, 100.0f); - - for (Components::CameraComponent* camera : scene.FindObjectsOfType()) { - if (camera == nullptr || !camera->IsEnabled()) { - continue; - } - - Components::GameObject* gameObject = camera->GetGameObject(); - if (!CanBuildOverlayForGameObject(gameObject)) { - continue; - } - - AppendSceneIconOverlay( - frameData, - overlay, - viewportWidth, - viewportHeight, - *gameObject, - kCameraIconSize, - SceneViewportOverlaySpriteTextureKind::Camera); - } - - for (Components::LightComponent* light : scene.FindObjectsOfType()) { - if (light == nullptr || !light->IsEnabled()) { - continue; - } - - Components::GameObject* gameObject = light->GetGameObject(); - if (!CanBuildOverlayForGameObject(gameObject)) { - continue; - } - - AppendSceneIconOverlay( - frameData, - overlay, - viewportWidth, - viewportHeight, - *gameObject, - kLightIconSize, - SceneViewportOverlaySpriteTextureKind::Light); - } -} - -} // namespace - SceneViewportOverlayFrameData SceneViewportOverlayBuilder::Build( IEditorContext& context, const SceneViewportOverlayData& overlay, uint32_t viewportWidth, uint32_t viewportHeight, - const std::vector& selectedObjectIds) { + const std::vector& selectedObjectIds) const { SceneViewportOverlayFrameData frameData = {}; frameData.overlay = overlay; if (!overlay.valid || viewportWidth == 0u || viewportHeight == 0u) { @@ -399,31 +36,15 @@ SceneViewportOverlayFrameData SceneViewportOverlayBuilder::Build( return frameData; } - AppendSceneObjectIconOverlays(frameData, *scene, overlay, viewportWidth, viewportHeight); - - for (uint64_t entityId : selectedObjectIds) { - if (entityId == 0) { - continue; - } - - Components::GameObject* gameObject = context.GetSceneManager().GetEntity(entityId); - if (!CanBuildOverlayForGameObject(gameObject)) { - continue; - } - - if (Components::CameraComponent* camera = gameObject->GetComponent(); - camera != nullptr && camera->IsEnabled()) { - AppendCameraFrustumOverlay(frameData, *camera, *gameObject, viewportWidth, viewportHeight); - } - - if (Components::LightComponent* light = gameObject->GetComponent(); - light != nullptr && - light->IsEnabled() && - light->GetLightType() == Components::LightType::Directional) { - AppendDirectionalLightOverlay(frameData, *gameObject, overlay, viewportHeight); - } - } - + const SceneViewportOverlayBuildContext buildContext = { + &context, + scene, + &overlay, + viewportWidth, + viewportHeight, + &selectedObjectIds + }; + m_providerRegistry.AppendOverlay(buildContext, frameData); return frameData; } diff --git a/editor/src/Viewport/SceneViewportOverlayBuilder.h b/editor/src/Viewport/SceneViewportOverlayBuilder.h index b68a1139..05173a5e 100644 --- a/editor/src/Viewport/SceneViewportOverlayBuilder.h +++ b/editor/src/Viewport/SceneViewportOverlayBuilder.h @@ -1,7 +1,7 @@ #pragma once #include "IViewportHostService.h" -#include "SceneViewportEditorOverlayData.h" +#include "SceneViewportOverlayProviders.h" #include #include @@ -13,12 +13,26 @@ class IEditorContext; class SceneViewportOverlayBuilder { public: - static SceneViewportOverlayFrameData Build( + SceneViewportOverlayBuilder(); + explicit SceneViewportOverlayBuilder(SceneViewportOverlayProviderRegistry providerRegistry); + + SceneViewportOverlayFrameData Build( IEditorContext& context, const SceneViewportOverlayData& overlay, uint32_t viewportWidth, uint32_t viewportHeight, - const std::vector& selectedObjectIds); + const std::vector& selectedObjectIds) const; + + const SceneViewportOverlayProviderRegistry& GetProviderRegistry() const { + return m_providerRegistry; + } + + SceneViewportOverlayProviderRegistry& GetProviderRegistry() { + return m_providerRegistry; + } + +private: + SceneViewportOverlayProviderRegistry m_providerRegistry = {}; }; } // namespace Editor diff --git a/editor/src/Viewport/SceneViewportOverlayProviders.cpp b/editor/src/Viewport/SceneViewportOverlayProviders.cpp new file mode 100644 index 00000000..8ef7e2c1 --- /dev/null +++ b/editor/src/Viewport/SceneViewportOverlayProviders.cpp @@ -0,0 +1,502 @@ +#include "SceneViewportOverlayProviders.h" + +#include +#include +#include +#include +#include +#include + +#include "SceneViewportOverlayHandleBuilder.h" +#include "SceneViewportMath.h" + +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { + +namespace { + +bool CanBuildOverlayForGameObject(const Components::GameObject* gameObject) { + return gameObject != nullptr && + gameObject->GetTransform() != nullptr && + gameObject->IsActiveInHierarchy(); +} + +float ResolveCameraAspect( + const Components::CameraComponent& camera, + uint32_t viewportWidth, + uint32_t viewportHeight) { + const Math::Rect viewportRect = camera.GetViewportRect(); + const float resolvedWidth = static_cast(viewportWidth) * + (viewportRect.width > Math::EPSILON ? viewportRect.width : 1.0f); + const float resolvedHeight = static_cast(viewportHeight) * + (viewportRect.height > Math::EPSILON ? viewportRect.height : 1.0f); + return resolvedHeight > Math::EPSILON + ? resolvedWidth / resolvedHeight + : 1.0f; +} + +float ComputeWorldUnitsPerPixel( + const SceneViewportOverlayData& overlay, + const Math::Vector3& worldPoint, + uint32_t viewportHeight) { + if (!overlay.valid || viewportHeight <= 1u) { + return 0.0f; + } + + const Math::Vector3 cameraForward = overlay.cameraForward.Normalized(); + const float depth = Math::Vector3::Dot(worldPoint - overlay.cameraPosition, cameraForward); + if (depth <= Math::EPSILON) { + return 0.0f; + } + + return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / + static_cast(viewportHeight); +} + +void AppendWorldLine( + SceneViewportOverlayFrameData& frameData, + const Math::Vector3& startWorld, + const Math::Vector3& endWorld, + const Math::Color& color, + float thicknessPixels, + SceneViewportOverlayDepthMode depthMode) { + SceneViewportOverlayLinePrimitive& line = frameData.worldLines.emplace_back(); + line.startWorld = startWorld; + line.endWorld = endWorld; + line.color = color; + line.thicknessPixels = thicknessPixels; + line.depthMode = depthMode; +} + +void AppendWorldSprite( + SceneViewportOverlayFrameData& frameData, + const Math::Vector3& worldPosition, + const Math::Vector2& sizePixels, + const Math::Color& tintColor, + float sortDepth, + uint64_t entityId, + SceneViewportOverlaySpriteTextureKind textureKind, + SceneViewportOverlayDepthMode depthMode) { + if (entityId == 0 || sizePixels.x <= Math::EPSILON || sizePixels.y <= Math::EPSILON) { + return; + } + + SceneViewportOverlaySpritePrimitive& sprite = frameData.worldSprites.emplace_back(); + sprite.worldPosition = worldPosition; + sprite.sizePixels = sizePixels; + sprite.tintColor = tintColor; + sprite.sortDepth = sortDepth; + sprite.entityId = entityId; + sprite.textureKind = textureKind; + sprite.depthMode = depthMode; +} + +void AppendHandleRecord( + SceneViewportOverlayFrameData& frameData, + SceneViewportOverlayHandleKind kind, + uint64_t handleId, + uint64_t entityId, + const Math::Vector3& worldPosition, + const Math::Vector2& sizePixels, + float sortDepth) { + if (kind == SceneViewportOverlayHandleKind::None || + handleId == 0 || + entityId == 0 || + sizePixels.x <= Math::EPSILON || + sizePixels.y <= Math::EPSILON) { + return; + } + + SceneViewportOverlayHandleRecord& handleRecord = frameData.handleRecords.emplace_back(); + handleRecord.kind = kind; + handleRecord.handleId = handleId; + handleRecord.entityId = entityId; + handleRecord.shape = SceneViewportOverlayHandleShape::WorldRect; + handleRecord.priority = Detail::kSceneViewportHandlePrioritySceneIcon; + handleRecord.worldPosition = worldPosition; + handleRecord.sizePixels = sizePixels; + handleRecord.sortDepth = sortDepth; +} + +void AppendSceneIconOverlay( + SceneViewportOverlayFrameData& frameData, + const SceneViewportOverlayData& overlay, + uint32_t viewportWidth, + uint32_t viewportHeight, + const Components::GameObject& gameObject, + const Math::Vector2& sizePixels, + SceneViewportOverlaySpriteTextureKind textureKind) { + const Components::TransformComponent* transform = gameObject.GetTransform(); + if (transform == nullptr) { + return; + } + + const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint( + overlay, + static_cast(viewportWidth), + static_cast(viewportHeight), + transform->GetPosition()); + if (!projectedPoint.visible) { + return; + } + + AppendWorldSprite( + frameData, + transform->GetPosition(), + sizePixels, + Math::Color::White(), + projectedPoint.ndcDepth, + gameObject.GetID(), + textureKind, + SceneViewportOverlayDepthMode::AlwaysOnTop); + AppendHandleRecord( + frameData, + SceneViewportOverlayHandleKind::SceneIcon, + gameObject.GetID(), + gameObject.GetID(), + transform->GetPosition(), + sizePixels, + projectedPoint.ndcDepth); +} + +void AppendCameraFrustumOverlay( + SceneViewportOverlayFrameData& frameData, + const Components::CameraComponent& camera, + const Components::GameObject& gameObject, + uint32_t viewportWidth, + uint32_t viewportHeight) { + const Components::TransformComponent* transform = gameObject.GetTransform(); + if (transform == nullptr) { + return; + } + + const Math::Vector3 position = transform->GetPosition(); + const Math::Vector3 forward = transform->GetForward().Normalized(); + const Math::Vector3 right = transform->GetRight().Normalized(); + const Math::Vector3 up = transform->GetUp().Normalized(); + if (forward.SqrMagnitude() <= Math::EPSILON || + right.SqrMagnitude() <= Math::EPSILON || + up.SqrMagnitude() <= Math::EPSILON) { + return; + } + + const float nearClip = (std::max)(camera.GetNearClipPlane(), 0.01f); + const float farClip = (std::max)(camera.GetFarClipPlane(), nearClip + 0.01f); + const float aspect = ResolveCameraAspect(camera, viewportWidth, viewportHeight); + + float nearHalfHeight = 0.0f; + float nearHalfWidth = 0.0f; + float farHalfHeight = 0.0f; + float farHalfWidth = 0.0f; + if (camera.GetProjectionType() == Components::CameraProjectionType::Perspective) { + const float halfFovRadians = + std::clamp(camera.GetFieldOfView(), 1.0f, 179.0f) * Math::DEG_TO_RAD * 0.5f; + nearHalfHeight = std::tan(halfFovRadians) * nearClip; + nearHalfWidth = nearHalfHeight * aspect; + farHalfHeight = std::tan(halfFovRadians) * farClip; + farHalfWidth = farHalfHeight * aspect; + } else { + const float halfHeight = (std::max)(camera.GetOrthographicSize(), 0.01f); + const float halfWidth = halfHeight * aspect; + nearHalfHeight = halfHeight; + nearHalfWidth = halfWidth; + farHalfHeight = halfHeight; + farHalfWidth = halfWidth; + } + + const Math::Vector3 nearCenter = position + forward * nearClip; + const Math::Vector3 farCenter = position + forward * farClip; + const std::array corners = {{ + nearCenter + up * nearHalfHeight - right * nearHalfWidth, + nearCenter + up * nearHalfHeight + right * nearHalfWidth, + nearCenter - up * nearHalfHeight + right * nearHalfWidth, + nearCenter - up * nearHalfHeight - right * nearHalfWidth, + farCenter + up * farHalfHeight - right * farHalfWidth, + farCenter + up * farHalfHeight + right * farHalfWidth, + farCenter - up * farHalfHeight + right * farHalfWidth, + farCenter - up * farHalfHeight - right * farHalfWidth + }}; + + static constexpr std::array, 12> kFrustumEdges = {{ + { 0u, 1u }, { 1u, 2u }, { 2u, 3u }, { 3u, 0u }, + { 4u, 5u }, { 5u, 6u }, { 6u, 7u }, { 7u, 4u }, + { 0u, 4u }, { 1u, 5u }, { 2u, 6u }, { 3u, 7u } + }}; + constexpr Math::Color kFrustumColor(1.0f, 1.0f, 1.0f, 1.0f); + + for (const auto& edge : kFrustumEdges) { + AppendWorldLine( + frameData, + corners[edge.first], + corners[edge.second], + kFrustumColor, + 1.6f, + SceneViewportOverlayDepthMode::AlwaysOnTop); + } +} + +void AppendDirectionalLightOverlay( + SceneViewportOverlayFrameData& frameData, + const Components::GameObject& gameObject, + const SceneViewportOverlayData& overlay, + uint32_t viewportHeight) { + const Components::TransformComponent* transform = gameObject.GetTransform(); + if (transform == nullptr) { + return; + } + + const Math::Vector3 position = transform->GetPosition(); + const Math::Vector3 lightDirection = (transform->GetForward() * -1.0f).Normalized(); + const Math::Vector3 right = transform->GetRight().Normalized(); + const Math::Vector3 up = transform->GetUp().Normalized(); + if (lightDirection.SqrMagnitude() <= Math::EPSILON || + right.SqrMagnitude() <= Math::EPSILON || + up.SqrMagnitude() <= Math::EPSILON) { + return; + } + + const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(overlay, position, viewportHeight); + if (worldUnitsPerPixel <= Math::EPSILON) { + return; + } + + constexpr Math::Color kDirectionalLightColor(1.0f, 0.92f, 0.24f, 1.0f); + constexpr float kLineThickness = 1.8f; + constexpr size_t kRingSegmentCount = 32u; + constexpr std::array kRayAngles = {{ + 0.0f, + Math::PI / 3.0f, + Math::PI * 2.0f / 3.0f, + Math::PI, + Math::PI * 4.0f / 3.0f, + Math::PI * 5.0f / 3.0f + }}; + + const float ringRadius = worldUnitsPerPixel * 26.0f; + const float ringOffset = worldUnitsPerPixel * 54.0f; + const float innerRayRadius = ringRadius * 0.52f; + const float rayLength = worldUnitsPerPixel * 96.0f; + const Math::Vector3 ringCenter = position + lightDirection * ringOffset; + + for (size_t segmentIndex = 0; segmentIndex < kRingSegmentCount; ++segmentIndex) { + const float angle0 = + static_cast(segmentIndex) / static_cast(kRingSegmentCount) * Math::PI * 2.0f; + const float angle1 = + static_cast(segmentIndex + 1u) / static_cast(kRingSegmentCount) * Math::PI * 2.0f; + const Math::Vector3 p0 = + ringCenter + right * std::cos(angle0) * ringRadius + up * std::sin(angle0) * ringRadius; + const Math::Vector3 p1 = + ringCenter + right * std::cos(angle1) * ringRadius + up * std::sin(angle1) * ringRadius; + AppendWorldLine( + frameData, + p0, + p1, + kDirectionalLightColor, + kLineThickness, + SceneViewportOverlayDepthMode::AlwaysOnTop); + } + + AppendWorldLine( + frameData, + position, + ringCenter, + kDirectionalLightColor, + kLineThickness, + SceneViewportOverlayDepthMode::AlwaysOnTop); + AppendWorldLine( + frameData, + ringCenter, + ringCenter + lightDirection * rayLength, + kDirectionalLightColor, + kLineThickness, + SceneViewportOverlayDepthMode::AlwaysOnTop); + + for (float angle : kRayAngles) { + const Math::Vector3 rayStart = + ringCenter + right * std::cos(angle) * innerRayRadius + up * std::sin(angle) * innerRayRadius; + AppendWorldLine( + frameData, + rayStart, + rayStart + lightDirection * rayLength, + kDirectionalLightColor, + kLineThickness, + SceneViewportOverlayDepthMode::AlwaysOnTop); + } +} + +class SceneViewportCameraOverlayProvider final : public ISceneViewportOverlayProvider { +public: + const char* GetName() const override { + return "SceneViewportCameraOverlayProvider"; + } + + void AppendOverlay( + const SceneViewportOverlayBuildContext& context, + SceneViewportOverlayFrameData& frameData) const override { + if (!context.IsValid()) { + return; + } + + constexpr Math::Vector2 kCameraIconSize(90.0f, 90.0f); + + for (Components::CameraComponent* camera : context.scene->FindObjectsOfType()) { + if (camera == nullptr || !camera->IsEnabled()) { + continue; + } + + Components::GameObject* gameObject = camera->GetGameObject(); + if (!CanBuildOverlayForGameObject(gameObject)) { + continue; + } + + AppendSceneIconOverlay( + frameData, + *context.overlay, + context.viewportWidth, + context.viewportHeight, + *gameObject, + kCameraIconSize, + SceneViewportOverlaySpriteTextureKind::Camera); + } + + for (uint64_t entityId : *context.selectedObjectIds) { + if (entityId == 0) { + continue; + } + + Components::GameObject* gameObject = context.scene->FindByID(entityId); + if (!CanBuildOverlayForGameObject(gameObject)) { + continue; + } + + Components::CameraComponent* camera = gameObject->GetComponent(); + if (camera == nullptr || !camera->IsEnabled()) { + continue; + } + + AppendCameraFrustumOverlay( + frameData, + *camera, + *gameObject, + context.viewportWidth, + context.viewportHeight); + } + } +}; + +class SceneViewportLightOverlayProvider final : public ISceneViewportOverlayProvider { +public: + const char* GetName() const override { + return "SceneViewportLightOverlayProvider"; + } + + void AppendOverlay( + const SceneViewportOverlayBuildContext& context, + SceneViewportOverlayFrameData& frameData) const override { + if (!context.IsValid()) { + return; + } + + constexpr Math::Vector2 kLightIconSize(100.0f, 100.0f); + + for (Components::LightComponent* light : context.scene->FindObjectsOfType()) { + if (light == nullptr || !light->IsEnabled()) { + continue; + } + + Components::GameObject* gameObject = light->GetGameObject(); + if (!CanBuildOverlayForGameObject(gameObject)) { + continue; + } + + AppendSceneIconOverlay( + frameData, + *context.overlay, + context.viewportWidth, + context.viewportHeight, + *gameObject, + kLightIconSize, + SceneViewportOverlaySpriteTextureKind::Light); + } + + for (uint64_t entityId : *context.selectedObjectIds) { + if (entityId == 0) { + continue; + } + + Components::GameObject* gameObject = context.scene->FindByID(entityId); + if (!CanBuildOverlayForGameObject(gameObject)) { + continue; + } + + Components::LightComponent* light = gameObject->GetComponent(); + if (light == nullptr || + !light->IsEnabled() || + light->GetLightType() != Components::LightType::Directional) { + continue; + } + + AppendDirectionalLightOverlay( + frameData, + *gameObject, + *context.overlay, + context.viewportHeight); + } + } +}; + +} // namespace + +void SceneViewportOverlayProviderRegistry::AddProvider( + std::unique_ptr provider) { + if (provider == nullptr) { + return; + } + + m_providers.emplace_back(std::move(provider)); +} + +size_t SceneViewportOverlayProviderRegistry::GetProviderCount() const { + return m_providers.size(); +} + +const ISceneViewportOverlayProvider* SceneViewportOverlayProviderRegistry::GetProvider(size_t index) const { + return index < m_providers.size() ? m_providers[index].get() : nullptr; +} + +void SceneViewportOverlayProviderRegistry::AppendOverlay( + const SceneViewportOverlayBuildContext& context, + SceneViewportOverlayFrameData& frameData) const { + if (!context.IsValid()) { + return; + } + + for (const auto& provider : m_providers) { + if (provider != nullptr) { + provider->AppendOverlay(context, frameData); + } + } +} + +std::unique_ptr CreateSceneViewportCameraOverlayProvider() { + return std::make_unique(); +} + +std::unique_ptr CreateSceneViewportLightOverlayProvider() { + return std::make_unique(); +} + +SceneViewportOverlayProviderRegistry BuildDefaultSceneViewportOverlayProviderRegistry() { + SceneViewportOverlayProviderRegistry registry; + registry.AddProvider(CreateSceneViewportCameraOverlayProvider()); + registry.AddProvider(CreateSceneViewportLightOverlayProvider()); + return registry; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportOverlayProviders.h b/editor/src/Viewport/SceneViewportOverlayProviders.h new file mode 100644 index 00000000..5bb4c174 --- /dev/null +++ b/editor/src/Viewport/SceneViewportOverlayProviders.h @@ -0,0 +1,66 @@ +#pragma once + +#include "SceneViewportEditorOverlayData.h" + +#include +#include +#include +#include + +namespace XCEngine { +namespace Components { +class Scene; +} // namespace Components + +namespace Editor { + +class IEditorContext; + +struct SceneViewportOverlayBuildContext { + IEditorContext* editorContext = nullptr; + const Components::Scene* scene = nullptr; + const SceneViewportOverlayData* overlay = nullptr; + uint32_t viewportWidth = 0u; + uint32_t viewportHeight = 0u; + const std::vector* selectedObjectIds = nullptr; + + bool IsValid() const { + return editorContext != nullptr && + scene != nullptr && + overlay != nullptr && + overlay->valid && + viewportWidth > 0u && + viewportHeight > 0u && + selectedObjectIds != nullptr; + } +}; + +class ISceneViewportOverlayProvider { +public: + virtual ~ISceneViewportOverlayProvider() = default; + + virtual const char* GetName() const = 0; + virtual void AppendOverlay( + const SceneViewportOverlayBuildContext& context, + SceneViewportOverlayFrameData& frameData) const = 0; +}; + +class SceneViewportOverlayProviderRegistry { +public: + void AddProvider(std::unique_ptr provider); + size_t GetProviderCount() const; + const ISceneViewportOverlayProvider* GetProvider(size_t index) const; + void AppendOverlay( + const SceneViewportOverlayBuildContext& context, + SceneViewportOverlayFrameData& frameData) const; + +private: + std::vector> m_providers = {}; +}; + +std::unique_ptr CreateSceneViewportCameraOverlayProvider(); +std::unique_ptr CreateSceneViewportLightOverlayProvider(); +SceneViewportOverlayProviderRegistry BuildDefaultSceneViewportOverlayProviderRegistry(); + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/ViewportHostService.h b/editor/src/Viewport/ViewportHostService.h index f9e0918f..28df3fbd 100644 --- a/editor/src/Viewport/ViewportHostService.h +++ b/editor/src/Viewport/ViewportHostService.h @@ -595,7 +595,7 @@ private: m_sceneViewEditorOverlayFrameData = {}; m_sceneViewEditorOverlayFrameData.overlay = overlay; if (scene != nullptr && overlay.valid && viewportWidth > 0u && viewportHeight > 0u) { - m_sceneViewEditorOverlayFrameData = SceneViewportOverlayBuilder::Build( + m_sceneViewEditorOverlayFrameData = m_sceneViewportOverlayBuilder.Build( context, overlay, viewportWidth, @@ -873,6 +873,7 @@ private: UI::ImGuiBackendBridge* m_backend = nullptr; RHI::RHIDevice* m_device = nullptr; std::unique_ptr m_sceneRenderer; + SceneViewportOverlayBuilder m_sceneViewportOverlayBuilder = {}; Rendering::RenderContext m_sceneViewLastRenderContext = {}; std::array m_entries = {}; SceneViewCameraState m_sceneViewCamera; diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index 1edbbc51..b537c85f 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -4,6 +4,7 @@ project(XCEngine_EditorTests) set(EDITOR_TEST_SOURCES test_action_routing.cpp + test_application_asset_cache_stub.cpp test_play_session_controller.cpp test_scene_viewport_camera_controller.cpp test_scene_viewport_move_gizmo.cpp @@ -12,6 +13,7 @@ set(EDITOR_TEST_SOURCES test_scene_viewport_picker.cpp test_scene_viewport_shader_paths.cpp test_scene_viewport_overlay_renderer.cpp + test_scene_viewport_overlay_providers.cpp test_script_component_editor_utils.cpp test_viewport_host_surface_utils.cpp test_viewport_object_id_picker.cpp @@ -31,6 +33,8 @@ set(EDITOR_TEST_SOURCES ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportMoveGizmo.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportRotateGizmo.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportScaleGizmo.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayBuilder.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportOverlayProviders.cpp ) if(XCENGINE_ENABLE_MONO_SCRIPTING AND TARGET xcengine_managed_assemblies) diff --git a/tests/editor/test_application_asset_cache_stub.cpp b/tests/editor/test_application_asset_cache_stub.cpp new file mode 100644 index 00000000..0d213e96 --- /dev/null +++ b/tests/editor/test_application_asset_cache_stub.cpp @@ -0,0 +1,45 @@ +#include "Application.h" + +#include +#ifdef XCENGINE_ENABLE_MONO_SCRIPTING +#include +#endif + +namespace XCEngine::Editor { + +Application& Application::Get() { + alignas(Application) static unsigned char storage[sizeof(Application)] = {}; + return *reinterpret_cast(storage); +} + +bool Application::CanReimportProjectAsset(const std::string& assetPath) const { + if (assetPath.empty()) { + return false; + } + + return ::XCEngine::Resources::ResourceManager::Get().CanReimportProjectAsset(assetPath.c_str()); +} + +bool Application::ReimportProjectAsset(const std::string& assetPath) { + if (assetPath.empty()) { + return false; + } + + auto& resourceManager = ::XCEngine::Resources::ResourceManager::Get(); + resourceManager.Initialize(); + return resourceManager.ReimportProjectAsset(assetPath.c_str()); +} + +bool Application::ReimportAllProjectAssets() { + auto& resourceManager = ::XCEngine::Resources::ResourceManager::Get(); + resourceManager.Initialize(); + return resourceManager.RebuildProjectAssetCache(); +} + +bool Application::ClearProjectLibrary() { + auto& resourceManager = ::XCEngine::Resources::ResourceManager::Get(); + resourceManager.Initialize(); + return resourceManager.ClearProjectLibraryCache(); +} + +} // namespace XCEngine::Editor diff --git a/tests/editor/test_scene_viewport_overlay_providers.cpp b/tests/editor/test_scene_viewport_overlay_providers.cpp new file mode 100644 index 00000000..f9aa0b6b --- /dev/null +++ b/tests/editor/test_scene_viewport_overlay_providers.cpp @@ -0,0 +1,210 @@ +#include + +#include "Core/EditorContext.h" +#include "Viewport/SceneViewportOverlayBuilder.h" +#include "Viewport/SceneViewportOverlayProviders.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace XCEngine::Editor { +namespace { + +class MarkerOverlayProvider final : public ISceneViewportOverlayProvider { +public: + explicit MarkerOverlayProvider(float marker) + : m_marker(marker) { + } + + const char* GetName() const override { + return "MarkerOverlayProvider"; + } + + void AppendOverlay( + const SceneViewportOverlayBuildContext& context, + SceneViewportOverlayFrameData& frameData) const override { + if (!context.IsValid()) { + return; + } + + SceneViewportOverlayLinePrimitive& line = frameData.worldLines.emplace_back(); + line.startWorld = Math::Vector3(m_marker, 0.0f, 0.0f); + line.endWorld = Math::Vector3(m_marker, 1.0f, 0.0f); + line.color = Math::Color::White(); + } + +private: + float m_marker = 0.0f; +}; + +SceneViewportOverlayData CreateValidOverlay() { + SceneViewportOverlayData overlay = {}; + overlay.valid = true; + overlay.cameraPosition = Math::Vector3::Zero(); + overlay.cameraForward = Math::Vector3::Forward(); + overlay.cameraRight = Math::Vector3::Right(); + overlay.cameraUp = Math::Vector3::Up(); + overlay.verticalFovDegrees = 60.0f; + overlay.nearClipPlane = 0.1f; + overlay.farClipPlane = 1000.0f; + overlay.orbitDistance = 6.0f; + return overlay; +} + +SceneViewportOverlayBuildContext CreateBuildContext( + EditorContext& context, + const SceneViewportOverlayData& overlay, + const std::vector& selectedObjectIds, + uint32_t viewportWidth = 1280u, + uint32_t viewportHeight = 720u) { + return { + &context, + context.GetSceneManager().GetScene(), + &overlay, + viewportWidth, + viewportHeight, + &selectedObjectIds + }; +} + +bool ContainsSpriteKind( + const SceneViewportOverlayFrameData& frameData, + SceneViewportOverlaySpriteTextureKind textureKind) { + return std::any_of( + frameData.worldSprites.begin(), + frameData.worldSprites.end(), + [textureKind](const SceneViewportOverlaySpritePrimitive& sprite) { + return sprite.textureKind == textureKind; + }); +} + +TEST(SceneViewportOverlayProviderRegistryTest, AppendsProvidersInRegistrationOrder) { + EditorContext context; + context.GetSceneManager().NewScene("Overlay Provider Registry"); + + const SceneViewportOverlayData overlay = CreateValidOverlay(); + const std::vector selectedObjectIds = {}; + const SceneViewportOverlayBuildContext buildContext = + CreateBuildContext(context, overlay, selectedObjectIds); + + SceneViewportOverlayProviderRegistry registry; + registry.AddProvider(std::make_unique(1.0f)); + registry.AddProvider(std::make_unique(2.0f)); + + ASSERT_EQ(registry.GetProviderCount(), 2u); + ASSERT_NE(registry.GetProvider(0u), nullptr); + ASSERT_NE(registry.GetProvider(1u), nullptr); + + SceneViewportOverlayFrameData frameData = {}; + frameData.overlay = overlay; + registry.AppendOverlay(buildContext, frameData); + + ASSERT_EQ(frameData.worldLines.size(), 2u); + EXPECT_FLOAT_EQ(frameData.worldLines[0].startWorld.x, 1.0f); + EXPECT_FLOAT_EQ(frameData.worldLines[1].startWorld.x, 2.0f); +} + +TEST(SceneViewportOverlayProviderRegistryTest, CameraProviderBuildsSceneIconAndSelectedFrustum) { + EditorContext context; + context.GetSceneManager().NewScene("Camera Overlay Provider"); + + auto* cameraEntity = context.GetSceneManager().CreateEntity("SceneCamera"); + ASSERT_NE(cameraEntity, nullptr); + cameraEntity->GetTransform()->SetPosition(Math::Vector3(0.0f, 0.0f, 5.0f)); + + auto* camera = cameraEntity->AddComponent(); + ASSERT_NE(camera, nullptr); + camera->SetNearClipPlane(0.3f); + camera->SetFarClipPlane(20.0f); + + const SceneViewportOverlayData overlay = CreateValidOverlay(); + const std::vector selectedObjectIds = { cameraEntity->GetID() }; + const SceneViewportOverlayBuildContext buildContext = + CreateBuildContext(context, overlay, selectedObjectIds); + + auto provider = CreateSceneViewportCameraOverlayProvider(); + ASSERT_NE(provider, nullptr); + + SceneViewportOverlayFrameData frameData = {}; + frameData.overlay = overlay; + provider->AppendOverlay(buildContext, frameData); + + ASSERT_EQ(frameData.worldSprites.size(), 1u); + EXPECT_EQ(frameData.worldSprites[0].textureKind, SceneViewportOverlaySpriteTextureKind::Camera); + ASSERT_EQ(frameData.handleRecords.size(), 1u); + EXPECT_EQ(frameData.handleRecords[0].entityId, cameraEntity->GetID()); + EXPECT_EQ(frameData.worldLines.size(), 12u); +} + +TEST(SceneViewportOverlayProviderRegistryTest, LightProviderBuildsSceneIconAndSelectedDirectionalHelper) { + EditorContext context; + context.GetSceneManager().NewScene("Light Overlay Provider"); + + auto* lightEntity = context.GetSceneManager().CreateEntity("DirectionalLight"); + ASSERT_NE(lightEntity, nullptr); + lightEntity->GetTransform()->SetPosition(Math::Vector3(0.0f, 0.0f, 5.0f)); + + auto* light = lightEntity->AddComponent(); + ASSERT_NE(light, nullptr); + light->SetLightType(Components::LightType::Directional); + + const SceneViewportOverlayData overlay = CreateValidOverlay(); + const std::vector selectedObjectIds = { lightEntity->GetID() }; + const SceneViewportOverlayBuildContext buildContext = + CreateBuildContext(context, overlay, selectedObjectIds); + + auto provider = CreateSceneViewportLightOverlayProvider(); + ASSERT_NE(provider, nullptr); + + SceneViewportOverlayFrameData frameData = {}; + frameData.overlay = overlay; + provider->AppendOverlay(buildContext, frameData); + + ASSERT_EQ(frameData.worldSprites.size(), 1u); + EXPECT_EQ(frameData.worldSprites[0].textureKind, SceneViewportOverlaySpriteTextureKind::Light); + ASSERT_EQ(frameData.handleRecords.size(), 1u); + EXPECT_EQ(frameData.handleRecords[0].entityId, lightEntity->GetID()); + EXPECT_GT(frameData.worldLines.size(), 0u); +} + +TEST( + SceneViewportOverlayProviderRegistryTest, + OverlayBuilderUsesDefaultRegistryToAggregateCameraAndLightProviders) { + EditorContext context; + context.GetSceneManager().NewScene("Overlay Builder"); + + auto* cameraEntity = context.GetSceneManager().CreateEntity("SceneCamera"); + ASSERT_NE(cameraEntity, nullptr); + cameraEntity->GetTransform()->SetPosition(Math::Vector3(-0.5f, 0.0f, 5.0f)); + ASSERT_NE(cameraEntity->AddComponent(), nullptr); + + auto* lightEntity = context.GetSceneManager().CreateEntity("DirectionalLight"); + ASSERT_NE(lightEntity, nullptr); + lightEntity->GetTransform()->SetPosition(Math::Vector3(0.5f, 0.0f, 5.0f)); + auto* light = lightEntity->AddComponent(); + ASSERT_NE(light, nullptr); + light->SetLightType(Components::LightType::Directional); + + const SceneViewportOverlayData overlay = CreateValidOverlay(); + const std::vector selectedObjectIds = { cameraEntity->GetID(), lightEntity->GetID() }; + + SceneViewportOverlayBuilder builder; + const SceneViewportOverlayFrameData frameData = + builder.Build(context, overlay, 1280u, 720u, selectedObjectIds); + + EXPECT_TRUE(frameData.overlay.valid); + EXPECT_EQ(frameData.worldSprites.size(), 2u); + EXPECT_EQ(frameData.handleRecords.size(), 2u); + EXPECT_TRUE(ContainsSpriteKind(frameData, SceneViewportOverlaySpriteTextureKind::Camera)); + EXPECT_TRUE(ContainsSpriteKind(frameData, SceneViewportOverlaySpriteTextureKind::Light)); + EXPECT_GT(frameData.worldLines.size(), 12u); +} + +} // namespace +} // namespace XCEngine::Editor