feat(new_editor): wire project, inspector, and viewport runtime

This commit is contained in:
2026-04-19 00:03:25 +08:00
parent 8257403036
commit a57b322bc7
168 changed files with 14829 additions and 2507 deletions

View File

@@ -0,0 +1,316 @@
#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 &section;
}
}
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