317 lines
12 KiB
C++
317 lines
12 KiB
C++
|
|
#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 <XCEngine/Components/CameraComponent.h>
|
||
|
|
#include <XCEngine/Components/GameObject.h>
|
||
|
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||
|
|
#include <XCEngine/Scene/Scene.h>
|
||
|
|
#include <XCEngine/Scene/SceneManager.h>
|
||
|
|
|
||
|
|
#include <gtest/gtest.h>
|
||
|
|
|
||
|
|
#include <chrono>
|
||
|
|
#include <filesystem>
|
||
|
|
|
||
|
|
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
|