#include "Features/Inspector/Components/IInspectorComponentEditor.h" #include "Features/Inspector/Components/InspectorComponentEditorRegistry.h" #include "Features/Inspector/InspectorPresentationModel.h" #include "Features/Inspector/InspectorSubject.h" #include "Scene/EditorSceneRuntime.h" #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 Widgets::UIEditorPropertyGridField; using Widgets::UIEditorPropertyGridSection; class ScopedSceneManagerReset final { public: ScopedSceneManagerReset() { Reset(); } ~ScopedSceneManagerReset() { Reset(); ::XCEngine::Resources::ResourceManager::Get().Shutdown(); } private: static void Reset() { SceneManager& manager = SceneManager::Get(); const auto scenes = manager.GetAllScenes(); for (Scene* scene : scenes) { manager.UnloadScene(scene); } } }; class TemporaryProjectRoot final { public: TemporaryProjectRoot() { const auto uniqueSuffix = std::chrono::steady_clock::now().time_since_epoch().count(); m_root = std::filesystem::temp_directory_path() / ("xcui_inspector_presentation_" + std::to_string(uniqueSuffix)); } ~TemporaryProjectRoot() { std::error_code errorCode = {}; std::filesystem::remove_all(m_root, errorCode); } const std::filesystem::path& Root() const { return m_root; } std::filesystem::path MainScenePath() const { return m_root / "Assets" / "Scenes" / "Main.xc"; } private: std::filesystem::path m_root = {}; }; void SaveMainScene(const TemporaryProjectRoot& projectRoot) { const std::filesystem::path scenePath = projectRoot.MainScenePath(); std::filesystem::create_directories(scenePath.parent_path()); Scene scene("Main"); GameObject* parent = scene.CreateGameObject("Parent"); ASSERT_NE(parent, nullptr); ASSERT_NE(parent->GetTransform(), nullptr); parent->GetTransform()->SetLocalPosition(::XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); parent->GetTransform()->SetLocalScale(::XCEngine::Math::Vector3(4.0f, 5.0f, 6.0f)); ASSERT_NE(scene.CreateGameObject("Child", parent), nullptr); ASSERT_NE(parent->AddComponent<::XCEngine::Components::CameraComponent>(), nullptr); scene.Save(scenePath.string()); } const UIEditorPropertyGridSection* FindSection( const InspectorPresentationModel& model, std::string_view title) { for (const UIEditorPropertyGridSection& section : model.sections) { if (section.title == title) { return §ion; } } return nullptr; } const UIEditorPropertyGridField* FindField( const UIEditorPropertyGridSection& section, std::string_view label) { for (const UIEditorPropertyGridField& field : section.fields) { if (field.label == label) { return &field; } } return nullptr; } const InspectorPresentationComponentBinding* FindBinding( const InspectorPresentationModel& model, std::string_view typeName) { for (const InspectorPresentationComponentBinding& binding : model.componentBindings) { if (binding.typeName == typeName) { return &binding; } } return nullptr; } TEST(InspectorPresentationModelTests, EmptySubjectBuildsDefaultEmptyState) { EditorSceneRuntime runtime = {}; const InspectorPresentationModel model = BuildInspectorPresentationModel( {}, runtime, InspectorComponentEditorRegistry::Get()); EXPECT_FALSE(model.hasSelection); EXPECT_EQ(model.title, "Nothing selected"); EXPECT_EQ(model.subtitle, "Select a hierarchy item or project asset."); EXPECT_TRUE(model.sections.empty()); EXPECT_TRUE(model.componentBindings.empty()); } TEST(InspectorPresentationModelTests, ProjectAssetSubjectBuildsIdentityAndLocationSections) { InspectorSubject subject = {}; subject.kind = InspectorSubjectKind::ProjectAsset; subject.source = InspectorSelectionSource::Project; subject.projectAsset.selection.kind = EditorSelectionKind::ProjectItem; subject.projectAsset.selection.itemId = "asset:materials/test"; subject.projectAsset.selection.displayName = "TestMaterial"; subject.projectAsset.selection.absolutePath = std::filesystem::path("D:/Xuanchi/Main/XCEngine/project/Assets/Materials/Test.mat"); EditorSceneRuntime runtime = {}; const InspectorPresentationModel model = BuildInspectorPresentationModel( subject, runtime, InspectorComponentEditorRegistry::Get()); ASSERT_TRUE(model.hasSelection); EXPECT_EQ(model.title, "TestMaterial"); EXPECT_EQ(model.subtitle, "Asset"); ASSERT_EQ(model.sections.size(), 2u); const auto* identity = FindSection(model, "Identity"); ASSERT_NE(identity, nullptr); ASSERT_EQ(identity->fields.size(), 3u); const auto* typeField = FindField(*identity, "Type"); const auto* nameField = FindField(*identity, "Name"); const auto* idField = FindField(*identity, "Id"); ASSERT_NE(typeField, nullptr); ASSERT_NE(nameField, nullptr); ASSERT_NE(idField, nullptr); EXPECT_EQ(typeField->valueText, "Asset"); EXPECT_EQ(nameField->valueText, "TestMaterial"); EXPECT_EQ(idField->valueText, "asset:materials/test"); const auto* location = FindSection(model, "Location"); ASSERT_NE(location, nullptr); ASSERT_EQ(location->fields.size(), 1u); const auto* pathField = FindField(*location, "Path"); ASSERT_NE(pathField, nullptr); EXPECT_NE( pathField->valueText.find("Assets/Materials/Test.mat"), std::string::npos); } TEST(InspectorPresentationModelTests, SceneObjectSubjectBuildsRegisteredComponentSections) { ScopedSceneManagerReset reset = {}; TemporaryProjectRoot projectRoot = {}; SaveMainScene(projectRoot); EditorSceneRuntime runtime = {}; ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); Scene* scene = runtime.GetActiveScene(); ASSERT_NE(scene, nullptr); GameObject* parent = scene->Find("Parent"); ASSERT_NE(parent, nullptr); ASSERT_TRUE(runtime.SetSelection(parent->GetID())); const InspectorSubject subject = BuildInspectorSubject(EditorSession{}, runtime); ASSERT_EQ(subject.kind, InspectorSubjectKind::SceneObject); const InspectorPresentationModel model = BuildInspectorPresentationModel( subject, runtime, InspectorComponentEditorRegistry::Get()); ASSERT_TRUE(model.hasSelection); EXPECT_EQ(model.title, "Parent"); EXPECT_EQ(model.subtitle, "GameObject"); ASSERT_EQ(model.sections.size(), 4u); ASSERT_EQ(model.componentBindings.size(), 2u); const auto* identity = FindSection(model, "Identity"); ASSERT_NE(identity, nullptr); const auto* sceneTypeField = FindField(*identity, "Type"); const auto* sceneNameField = FindField(*identity, "Name"); const auto* sceneIdField = FindField(*identity, "Id"); ASSERT_NE(sceneTypeField, nullptr); ASSERT_NE(sceneNameField, nullptr); ASSERT_NE(sceneIdField, nullptr); EXPECT_EQ(sceneTypeField->valueText, "GameObject"); EXPECT_EQ(sceneNameField->valueText, "Parent"); EXPECT_EQ(sceneIdField->valueText, runtime.GetSelectedItemId()); const auto* hierarchy = FindSection(model, "Hierarchy"); ASSERT_NE(hierarchy, nullptr); const auto* childrenField = FindField(*hierarchy, "Children"); const auto* parentField = FindField(*hierarchy, "Parent"); ASSERT_NE(childrenField, nullptr); ASSERT_NE(parentField, nullptr); EXPECT_EQ(childrenField->valueText, "1"); EXPECT_EQ(parentField->valueText, "Scene Root"); const auto* transform = FindSection(model, "Transform"); ASSERT_NE(transform, nullptr); const auto* positionField = FindField(*transform, "Position"); const auto* rotationField = FindField(*transform, "Rotation"); const auto* scaleField = FindField(*transform, "Scale"); ASSERT_NE(positionField, nullptr); ASSERT_NE(rotationField, nullptr); ASSERT_NE(scaleField, nullptr); EXPECT_EQ(positionField->kind, Widgets::UIEditorPropertyGridFieldKind::Vector3); EXPECT_DOUBLE_EQ(positionField->vector3Value.values[0], 1.0); EXPECT_DOUBLE_EQ(positionField->vector3Value.values[1], 2.0); EXPECT_DOUBLE_EQ(positionField->vector3Value.values[2], 3.0); EXPECT_EQ(rotationField->kind, Widgets::UIEditorPropertyGridFieldKind::Vector3); EXPECT_EQ(scaleField->kind, Widgets::UIEditorPropertyGridFieldKind::Vector3); EXPECT_DOUBLE_EQ(scaleField->vector3Value.values[0], 4.0); EXPECT_DOUBLE_EQ(scaleField->vector3Value.values[1], 5.0); EXPECT_DOUBLE_EQ(scaleField->vector3Value.values[2], 6.0); const auto* camera = FindSection(model, "Camera"); ASSERT_NE(camera, nullptr); const auto* projectionField = FindField(*camera, "Projection"); const auto* primaryField = FindField(*camera, "Primary"); const auto* clearColorField = FindField(*camera, "Clear Color"); ASSERT_NE(projectionField, nullptr); ASSERT_NE(primaryField, nullptr); ASSERT_NE(clearColorField, nullptr); EXPECT_EQ(projectionField->kind, Widgets::UIEditorPropertyGridFieldKind::Enum); EXPECT_EQ(projectionField->enumValue.selectedIndex, 0u); EXPECT_EQ(primaryField->kind, Widgets::UIEditorPropertyGridFieldKind::Bool); EXPECT_TRUE(primaryField->boolValue); EXPECT_EQ(clearColorField->kind, Widgets::UIEditorPropertyGridFieldKind::Color); const auto* transformBinding = FindBinding(model, "Transform"); const auto* cameraBinding = FindBinding(model, "Camera"); ASSERT_NE(transformBinding, nullptr); ASSERT_NE(cameraBinding, nullptr); EXPECT_FALSE(transformBinding->removable); EXPECT_TRUE(cameraBinding->removable); EXPECT_EQ(transformBinding->fieldIds.size(), 3u); EXPECT_GE(cameraBinding->fieldIds.size(), 8u); } TEST(InspectorPresentationModelTests, CameraSkyboxMaterialBuildsAssetField) { ScopedSceneManagerReset reset = {}; TemporaryProjectRoot projectRoot = {}; SaveMainScene(projectRoot); EditorSceneRuntime runtime = {}; ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); Scene* scene = runtime.GetActiveScene(); ASSERT_NE(scene, nullptr); GameObject* parent = scene->Find("Parent"); ASSERT_NE(parent, nullptr); auto* camera = parent->GetComponent<::XCEngine::Components::CameraComponent>(); ASSERT_NE(camera, nullptr); camera->SetSkyboxEnabled(true); camera->SetSkyboxMaterialPath("Assets/Materials/Skybox.mat"); ASSERT_TRUE(runtime.SetSelection(parent->GetID())); const InspectorPresentationModel model = BuildInspectorPresentationModel( BuildInspectorSubject(EditorSession{}, runtime), runtime, InspectorComponentEditorRegistry::Get()); const auto* cameraSection = FindSection(model, "Camera"); ASSERT_NE(cameraSection, nullptr); const auto* skyboxMaterialField = FindField(*cameraSection, "Skybox Material"); ASSERT_NE(skyboxMaterialField, nullptr); EXPECT_EQ(skyboxMaterialField->kind, Widgets::UIEditorPropertyGridFieldKind::Asset); EXPECT_EQ( skyboxMaterialField->assetValue.assetId, "Assets/Materials/Skybox.mat"); EXPECT_EQ( skyboxMaterialField->assetValue.displayName, "Skybox.mat"); } } // namespace } // namespace XCEngine::UI::Editor::App