chore: checkpoint current workspace changes

This commit is contained in:
2026-04-11 22:14:02 +08:00
parent 3e55f8c204
commit 8848cfd958
227 changed files with 34027 additions and 6711 deletions

View File

@@ -5,6 +5,7 @@
#include "Actions/MainMenuActionRouter.h"
#include "Actions/ProjectActionRouter.h"
#include "Commands/EntityCommands.h"
#include "Commands/ProjectCommands.h"
#include "Commands/SceneCommands.h"
#include "Core/EditorContext.h"
#include "Core/PlaySessionController.h"
@@ -15,6 +16,7 @@
#include <XCEngine/Core/Math/Quaternion.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Resources/Model/Model.h>
#include <chrono>
#include <filesystem>
@@ -22,11 +24,41 @@
#include <iterator>
#include <string>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
namespace fs = std::filesystem;
namespace XCEngine::Editor {
namespace {
fs::path GetRepositoryRoot() {
return fs::path(XCENGINE_EDITOR_REPO_ROOT);
}
std::string GetMeshFixturePath(const char* fileName) {
return (GetRepositoryRoot() / "tests" / "Fixtures" / "Resources" / "Mesh" / fileName).string();
}
void CopyTexturedTriangleFixture(const fs::path& assetsDir) {
fs::copy_file(
GetMeshFixturePath("textured_triangle.obj"),
assetsDir / "textured_triangle.obj",
fs::copy_options::overwrite_existing);
fs::copy_file(
GetMeshFixturePath("textured_triangle.mtl"),
assetsDir / "textured_triangle.mtl",
fs::copy_options::overwrite_existing);
fs::copy_file(
GetMeshFixturePath("checker.bmp"),
assetsDir / "checker.bmp",
fs::copy_options::overwrite_existing);
}
bool DirectoryHasEntries(const fs::path& directoryPath) {
std::error_code ec;
if (!fs::exists(directoryPath, ec) || !fs::is_directory(directoryPath, ec)) {
@@ -36,6 +68,18 @@ bool DirectoryHasEntries(const fs::path& directoryPath) {
return fs::directory_iterator(directoryPath) != fs::directory_iterator();
}
#ifdef _WIN32
struct AssimpDllGuard {
HMODULE module = nullptr;
~AssimpDllGuard() {
if (module != nullptr) {
FreeLibrary(module);
}
}
};
#endif
class EditorActionRoutingTest : public ::testing::Test {
protected:
void SetUp() override {
@@ -98,6 +142,26 @@ protected:
return total;
}
template <typename ComponentType>
static ::XCEngine::Components::GameObject* FindFirstEntityWithComponent(
::XCEngine::Components::GameObject* gameObject) {
if (gameObject == nullptr) {
return nullptr;
}
if (gameObject->GetComponent<ComponentType>() != nullptr) {
return gameObject;
}
for (size_t i = 0; i < gameObject->GetChildCount(); ++i) {
if (auto* found = FindFirstEntityWithComponent<ComponentType>(gameObject->GetChild(i))) {
return found;
}
}
return nullptr;
}
EditorContext m_context;
fs::path m_projectRoot;
};
@@ -223,6 +287,138 @@ TEST_F(EditorActionRoutingTest, ProjectRouteExecutesOpenBackAndDelete) {
EXPECT_FALSE(fs::exists(filePath));
}
TEST_F(EditorActionRoutingTest, ProjectCommandsInstantiateModelAssetBuildsHierarchyAndSupportsUndoRedo) {
using ::XCEngine::Resources::ResourceManager;
const fs::path assetsDir = m_projectRoot / "Assets";
CopyTexturedTriangleFixture(assetsDir);
m_context.GetProjectManager().RefreshCurrentFolder();
const AssetItemPtr modelItem = FindCurrentItemByName("textured_triangle.obj");
ASSERT_NE(modelItem, nullptr);
EXPECT_EQ(modelItem->type, "Model");
EXPECT_TRUE(Commands::CanInstantiateModelAsset(m_context, modelItem));
#ifdef _WIN32
AssimpDllGuard dllGuard;
const fs::path assimpDllPath =
GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
ASSERT_TRUE(fs::exists(assimpDllPath));
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
ASSERT_NE(dllGuard.module, nullptr);
#endif
ResourceManager& resourceManager = ResourceManager::Get();
resourceManager.Initialize();
resourceManager.SetResourceRoot(m_projectRoot.string().c_str());
const size_t entityCountBeforeInstantiate = CountHierarchyEntities(m_context.GetSceneManager());
auto* createdRoot = Commands::InstantiateModelAsset(m_context, modelItem);
ASSERT_NE(createdRoot, nullptr);
const uint64_t createdRootId = createdRoot->GetID();
const size_t entityCountAfterInstantiate = CountHierarchyEntities(m_context.GetSceneManager());
EXPECT_GT(entityCountAfterInstantiate, entityCountBeforeInstantiate);
EXPECT_EQ(m_context.GetSelectionManager().GetSelectedEntity(), createdRootId);
EXPECT_TRUE(m_context.GetUndoManager().CanUndo());
auto* meshObject = FindFirstEntityWithComponent<::XCEngine::Components::MeshFilterComponent>(createdRoot);
ASSERT_NE(meshObject, nullptr);
auto* meshFilter = meshObject->GetComponent<::XCEngine::Components::MeshFilterComponent>();
auto* meshRenderer = meshObject->GetComponent<::XCEngine::Components::MeshRendererComponent>();
ASSERT_NE(meshFilter, nullptr);
ASSERT_NE(meshRenderer, nullptr);
EXPECT_TRUE(meshFilter->GetMeshAssetRef().IsValid());
ASSERT_FALSE(meshRenderer->GetMaterialAssetRefs().empty());
EXPECT_TRUE(meshRenderer->GetMaterialAssetRefs()[0].IsValid());
m_context.GetUndoManager().Undo();
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeInstantiate);
EXPECT_EQ(m_context.GetSceneManager().GetEntity(createdRootId), nullptr);
EXPECT_FALSE(m_context.GetSelectionManager().HasSelection());
m_context.GetUndoManager().Redo();
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountAfterInstantiate);
EXPECT_EQ(m_context.GetSelectionManager().GetSelectedEntity(), createdRootId);
auto* restoredRoot = m_context.GetSceneManager().GetEntity(createdRootId);
ASSERT_NE(restoredRoot, nullptr);
auto* restoredMeshObject =
FindFirstEntityWithComponent<::XCEngine::Components::MeshFilterComponent>(restoredRoot);
ASSERT_NE(restoredMeshObject, nullptr);
auto* restoredMeshFilter =
restoredMeshObject->GetComponent<::XCEngine::Components::MeshFilterComponent>();
auto* restoredMeshRenderer =
restoredMeshObject->GetComponent<::XCEngine::Components::MeshRendererComponent>();
ASSERT_NE(restoredMeshFilter, nullptr);
ASSERT_NE(restoredMeshRenderer, nullptr);
EXPECT_TRUE(restoredMeshFilter->GetMeshAssetRef().IsValid());
ASSERT_FALSE(restoredMeshRenderer->GetMaterialAssetRefs().empty());
EXPECT_TRUE(restoredMeshRenderer->GetMaterialAssetRefs()[0].IsValid());
resourceManager.UnloadAll();
resourceManager.SetResourceRoot("");
resourceManager.Shutdown();
}
TEST_F(EditorActionRoutingTest, ProjectCommandsInstantiateModelAssetAppliesSidecarMaterialOverrides) {
using ::XCEngine::Resources::Model;
using ::XCEngine::Resources::ResourceManager;
using ::XCEngine::Resources::ResourceType;
const fs::path assetsDir = m_projectRoot / "Assets";
CopyTexturedTriangleFixture(assetsDir);
std::ofstream(assetsDir / "OverrideMaterial.mat")
<< "{\n"
" \"renderQueue\": \"geometry\"\n"
"}\n";
#ifdef _WIN32
AssimpDllGuard dllGuard;
const fs::path assimpDllPath =
GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
ASSERT_TRUE(fs::exists(assimpDllPath));
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
ASSERT_NE(dllGuard.module, nullptr);
#endif
ResourceManager& resourceManager = ResourceManager::Get();
resourceManager.Initialize();
resourceManager.SetResourceRoot(m_projectRoot.string().c_str());
const auto modelHandle = resourceManager.Load<Model>("Assets/textured_triangle.obj");
ASSERT_TRUE(modelHandle.IsValid());
ASSERT_FALSE(modelHandle->GetMeshBindings().Empty());
std::ofstream(assetsDir / "textured_triangle.obj.materialmap")
<< modelHandle->GetMeshBindings()[0].meshLocalID
<< "=Assets/OverrideMaterial.mat\n";
m_context.GetProjectManager().RefreshCurrentFolder();
const AssetItemPtr modelItem = FindCurrentItemByName("textured_triangle.obj");
ASSERT_NE(modelItem, nullptr);
auto* createdRoot = Commands::InstantiateModelAsset(m_context, modelItem);
ASSERT_NE(createdRoot, nullptr);
auto* meshObject = FindFirstEntityWithComponent<::XCEngine::Components::MeshFilterComponent>(createdRoot);
ASSERT_NE(meshObject, nullptr);
auto* meshRenderer = meshObject->GetComponent<::XCEngine::Components::MeshRendererComponent>();
ASSERT_NE(meshRenderer, nullptr);
::XCEngine::Resources::AssetRef overrideMaterialRef;
ASSERT_TRUE(resourceManager.TryGetAssetRef("Assets/OverrideMaterial.mat", ResourceType::Material, overrideMaterialRef));
ASSERT_FALSE(meshRenderer->GetMaterialAssetRefs().empty());
EXPECT_EQ(meshRenderer->GetMaterialAssetRefs()[0].assetGuid, overrideMaterialRef.assetGuid);
EXPECT_EQ(meshRenderer->GetMaterialAssetRefs()[0].localID, overrideMaterialRef.localID);
EXPECT_EQ(meshRenderer->GetMaterialAssetRefs()[0].resourceType, ResourceType::Material);
EXPECT_EQ(meshRenderer->GetMaterialPath(0), "Assets/OverrideMaterial.mat");
resourceManager.UnloadAll();
resourceManager.SetResourceRoot("");
resourceManager.Shutdown();
}
TEST_F(EditorActionRoutingTest, LoadSceneResetsSelectionAndUndoAfterFallbackSave) {
auto* savedEntity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Saved", "SavedEntity");
ASSERT_NE(savedEntity, nullptr);