#include "Product/Features/Workspace/Inspector/Components/IInspectorComponentEditor.h" #include "Product/Features/Workspace/Inspector/Components/InspectorComponentEditorRegistry.h" #include "Product/Features/Workspace/Inspector/InspectorFieldValueApplier.h" #include "Product/Features/Workspace/Inspector/InspectorPresentationModel.h" #include "Product/Features/Workspace/Inspector/InspectorSubject.h" #include "Product/Services/Project/EditorProjectRuntime.h" #include "Product/Services/Scene/EditorSceneRuntime.h" #include "Product/Services/Scene/EngineEditorSceneBackend.h" #include #include #include #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::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() { 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()); } void BindEngineSceneBackend(EditorSceneRuntime& runtime) { runtime.SetBackend(CreateEngineEditorSceneBackend( SceneManager::Get(), ::XCEngine::Resources::ResourceManager::Get())); } 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 UIEditorPropertyGridField* FindFieldById( const InspectorPresentationModel& model, std::string_view fieldId) { for (const UIEditorPropertyGridSection& section : model.sections) { for (const UIEditorPropertyGridField& field : section.fields) { if (field.fieldId == fieldId) { 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 = {}; BindEngineSceneBackend(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 = {}; BindEngineSceneBackend(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 = {}; 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_TRUE(runtime.SetSelection(parent->GetID())); EditorProjectRuntime projectRuntime = {}; const InspectorSubject subject = BuildInspectorSubject(projectRuntime, 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(), 2u); ASSERT_EQ(model.componentBindings.size(), 2u); 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 = {}; 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); 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())); EditorProjectRuntime projectRuntime = {}; const InspectorPresentationModel model = BuildInspectorPresentationModel( BuildInspectorSubject(projectRuntime, 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"); } 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())); EditorProjectRuntime projectRuntime = {}; const InspectorPresentationModel model = BuildInspectorPresentationModel( BuildInspectorSubject(projectRuntime, 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())); EditorProjectRuntime projectRuntime = {}; InspectorSubject subject = BuildInspectorSubject(projectRuntime, 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(projectRuntime, 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 = {}; 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_TRUE(runtime.SetSelection(parent->GetID())); EditorProjectRuntime projectRuntime = {}; const InspectorSubject subject = BuildInspectorSubject(projectRuntime, runtime); ASSERT_EQ(subject.kind, InspectorSubjectKind::SceneObject); const InspectorPresentationModel model = BuildInspectorPresentationModel( subject, runtime, InspectorComponentEditorRegistry::Get()); const auto* transformBinding = FindBinding(model, "Transform"); ASSERT_NE(transformBinding, nullptr); ASSERT_FALSE(transformBinding->fieldIds.empty()); const UIEditorPropertyGridField* originalField = FindFieldById(model, transformBinding->fieldIds.front()); ASSERT_NE(originalField, nullptr); ASSERT_EQ(originalField->kind, Widgets::UIEditorPropertyGridFieldKind::Vector3); UIEditorPropertyGridField editedField = *originalField; editedField.vector3Value.values = { 8.0, 9.0, 10.0 }; bool applied = false; EXPECT_NO_THROW( applied = ApplyInspectorComponentBoundFieldValue( runtime, subject.sceneObject, *transformBinding, editedField)); ASSERT_TRUE(applied); const auto* transform = parent->GetTransform(); ASSERT_NE(transform, nullptr); const auto position = transform->GetLocalPosition(); EXPECT_FLOAT_EQ(position.x, 8.0f); EXPECT_FLOAT_EQ(position.y, 9.0f); EXPECT_FLOAT_EQ(position.z, 10.0f); } } // namespace } // namespace XCEngine::UI::Editor::App