#include #include "Core/EditorContext.h" #include "Viewport/SceneViewportOverlayBuilder.h" #include "Viewport/SceneViewportOverlayHandleBuilder.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, const SceneViewportTransformGizmoOverlayState* transformGizmoOverlayState = nullptr) { return { &context, context.GetSceneManager().GetScene(), &overlay, viewportWidth, viewportHeight, &selectedObjectIds, transformGizmoOverlayState }; } SceneViewportTransformGizmoOverlayState CreateMoveGizmoOverlayState(uint64_t entityId = 99u) { SceneViewportTransformGizmoOverlayState state = {}; state.hasMoveGizmo = true; state.moveEntityId = entityId; state.moveGizmo.visible = true; SceneViewportMoveGizmoHandleDrawData& handle = state.moveGizmo.handles[0]; handle.axis = SceneViewportGizmoAxis::X; handle.start = Math::Vector2(24.0f, 48.0f); handle.end = Math::Vector2(104.0f, 48.0f); handle.color = Math::Color(0.91f, 0.09f, 0.05f, 1.0f); handle.visible = true; return state; } 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; }); } const SceneViewportOverlayLinePrimitive* FindLineStartingAt( const SceneViewportOverlayFrameData& frameData, const Math::Vector3& position) { const auto matchesPosition = [&position](const SceneViewportOverlayLinePrimitive& line) { return (line.startWorld - position).SqrMagnitude() <= 1e-4f; }; const auto it = std::find_if( frameData.worldLines.begin(), frameData.worldLines.end(), matchesPosition); return it != frameData.worldLines.end() ? &(*it) : nullptr; } 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::DirectionalLight); ASSERT_EQ(frameData.handleRecords.size(), 1u); EXPECT_EQ(frameData.handleRecords[0].entityId, lightEntity->GetID()); EXPECT_GT(frameData.worldLines.size(), 0u); const SceneViewportOverlayLinePrimitive* connectorLine = FindLineStartingAt(frameData, lightEntity->GetTransform()->GetPosition()); ASSERT_NE(connectorLine, nullptr); EXPECT_GT(connectorLine->endWorld.z, connectorLine->startWorld.z); } TEST(SceneViewportOverlayProviderRegistryTest, LightProviderBuildsSelectedPointLightHelper) { EditorContext context; context.GetSceneManager().NewScene("Point Light Overlay Provider"); auto* lightEntity = context.GetSceneManager().CreateEntity("PointLight"); ASSERT_NE(lightEntity, nullptr); lightEntity->GetTransform()->SetPosition(Math::Vector3(0.0f, 0.0f, 12.0f)); auto* light = lightEntity->AddComponent(); ASSERT_NE(light, nullptr); light->SetLightType(Components::LightType::Point); light->SetRange(3.5f); 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::PointLight); ASSERT_EQ(frameData.handleRecords.size(), 1u); EXPECT_EQ(frameData.handleRecords[0].entityId, lightEntity->GetID()); EXPECT_EQ(frameData.worldLines.size(), 96u); EXPECT_NEAR( (frameData.worldLines[0].startWorld - lightEntity->GetTransform()->GetPosition()).Magnitude(), light->GetRange(), 1e-3f); } TEST(SceneViewportOverlayProviderRegistryTest, LightProviderBuildsSelectedSpotLightHelper) { EditorContext context; context.GetSceneManager().NewScene("Spot Light Overlay Provider"); auto* lightEntity = context.GetSceneManager().CreateEntity("SpotLight"); ASSERT_NE(lightEntity, nullptr); lightEntity->GetTransform()->SetPosition(Math::Vector3(0.0f, 0.0f, 6.0f)); auto* light = lightEntity->AddComponent(); ASSERT_NE(light, nullptr); light->SetLightType(Components::LightType::Spot); light->SetRange(4.0f); light->SetSpotAngle(30.0f); 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::SpotLight); ASSERT_EQ(frameData.handleRecords.size(), 1u); EXPECT_EQ(frameData.handleRecords[0].entityId, lightEntity->GetID()); EXPECT_EQ(frameData.worldLines.size(), 37u); const SceneViewportOverlayLinePrimitive* connectorLine = FindLineStartingAt(frameData, lightEntity->GetTransform()->GetPosition()); ASSERT_NE(connectorLine, nullptr); EXPECT_GT(connectorLine->endWorld.z, connectorLine->startWorld.z); } TEST(SceneViewportOverlayProviderRegistryTest, TransformGizmoProviderBuildsOverlayFromFormalState) { EditorContext context; context.GetSceneManager().NewScene("Transform Gizmo Overlay Provider"); const SceneViewportOverlayData overlay = CreateValidOverlay(); const std::vector selectedObjectIds = {}; const SceneViewportTransformGizmoOverlayState gizmoState = CreateMoveGizmoOverlayState(73u); const SceneViewportOverlayBuildContext buildContext = CreateBuildContext(context, overlay, selectedObjectIds, 1280u, 720u, &gizmoState); auto provider = CreateSceneViewportTransformGizmoOverlayProvider(); ASSERT_NE(provider, nullptr); SceneViewportOverlayFrameData frameData = {}; frameData.overlay = overlay; provider->AppendOverlay(buildContext, frameData); ASSERT_EQ(frameData.handleRecords.size(), 1u); EXPECT_EQ(frameData.handleRecords[0].kind, SceneViewportOverlayHandleKind::MoveAxis); EXPECT_EQ(frameData.handleRecords[0].handleId, static_cast(SceneViewportGizmoAxis::X)); EXPECT_EQ(frameData.handleRecords[0].entityId, 73u); EXPECT_GT(frameData.screenTriangles.size(), 0u); EXPECT_TRUE(frameData.worldLines.empty()); EXPECT_TRUE(frameData.worldSprites.empty()); } 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::DirectionalLight)); EXPECT_GT(frameData.worldLines.size(), 12u); } TEST( SceneViewportOverlayProviderRegistryTest, OverlayBuilderAppendsFormalTransformGizmoProviderOutput) { EditorContext context; context.GetSceneManager().NewScene("Overlay Builder Transform Gizmo"); const SceneViewportOverlayData overlay = CreateValidOverlay(); const std::vector selectedObjectIds = {}; const SceneViewportTransformGizmoOverlayState gizmoState = CreateMoveGizmoOverlayState(91u); SceneViewportOverlayBuilder builder; const SceneViewportOverlayFrameData frameData = builder.Build(context, overlay, 1280u, 720u, selectedObjectIds, &gizmoState); EXPECT_TRUE(frameData.overlay.valid); ASSERT_EQ(frameData.handleRecords.size(), 1u); EXPECT_EQ(frameData.handleRecords[0].entityId, 91u); EXPECT_GT(frameData.screenTriangles.size(), 0u); } } // namespace } // namespace XCEngine::Editor