feat: implement Unity/Infernux style ObjectField for material Inspector with drag-and-drop, clear, and search-picker

This commit is contained in:
ayuan9957 2026-06-08 10:25:17 +08:00
parent 1bb2c858e2
commit d7590c53b6
10 changed files with 2180 additions and 198 deletions

View File

@ -12,6 +12,7 @@
#include "MetaCorePlatform/MetaCoreInput.h"
#include "MetaCoreFoundation/MetaCoreAssetRegistry.h"
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
#include "MetaCoreScene/MetaCoreRuntimeLifecycle.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include <nlohmann/json.hpp>
@ -47,7 +48,9 @@
#include <iomanip>
#include <optional>
#include <regex>
#include <span>
#include <sstream>
#include <exception>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
@ -2791,6 +2794,19 @@ bool MetaCoreDeserializeReflectedComponentValue(
return path.extension() == ".mcprefab";
}
[[nodiscard]] std::filesystem::path MetaCoreNormalizePrefabJsonPath(const std::filesystem::path& path) {
std::filesystem::path normalizedPath = path.lexically_normal();
const std::string filename = normalizedPath.filename().string();
if (filename.length() > 14 && filename.compare(filename.length() - 14, 14, ".mcprefab.json") == 0) {
return normalizedPath;
}
if (normalizedPath.extension() == ".mcprefab") {
normalizedPath.replace_extension(".mcprefab.json");
return normalizedPath;
}
return normalizedPath.parent_path() / (normalizedPath.stem().string() + ".mcprefab.json");
}
[[nodiscard]] bool MetaCoreIsMaterialPath(const std::filesystem::path& path) {
const std::string filename = path.filename().string();
if (filename.length() > 16 && filename.compare(filename.length() - 16, 16, ".mcmaterial.json") == 0) {
@ -2827,6 +2843,66 @@ bool MetaCoreDeserializeReflectedComponentValue(
return MetaCoreIsScenePath(path) || MetaCoreIsPrefabPath(path) || MetaCoreIsMaterialPath(path) || MetaCoreIsUiPath(path);
}
void MetaCoreRewriteMovedAssetMetadata(
const std::filesystem::path& absoluteAssetPath,
const std::filesystem::path& absoluteMetaPath,
const std::filesystem::path& relativeAssetPath
) {
MetaCoreAssetMetadataDocument metadata;
if (!MetaCoreReadMetaJson(absoluteMetaPath, metadata)) {
return;
}
metadata.SourcePath = relativeAssetPath.lexically_normal();
metadata.PackagePath = MetaCoreIsJsonSourceAssetPath(relativeAssetPath)
? metadata.SourcePath
: MetaCoreBuildPackagePathFromSource(metadata.SourcePath);
metadata.SourceHash = MetaCoreHashFile(absoluteAssetPath).value_or(metadata.SourceHash);
(void)MetaCoreWriteMetaJson(absoluteMetaPath, metadata);
}
void MetaCoreRewriteMovedAssetMetadataTree(
const std::filesystem::path& projectRoot,
const std::filesystem::path& absoluteMovedPath,
const std::filesystem::path& relativeMovedPath
) {
if (!std::filesystem::exists(absoluteMovedPath)) {
return;
}
if (std::filesystem::is_regular_file(absoluteMovedPath)) {
const std::filesystem::path metaPath = MetaCoreBuildMetaPath(absoluteMovedPath);
if (std::filesystem::exists(metaPath)) {
MetaCoreRewriteMovedAssetMetadata(absoluteMovedPath, metaPath, relativeMovedPath);
}
return;
}
if (!std::filesystem::is_directory(absoluteMovedPath)) {
return;
}
for (const auto& entry : std::filesystem::recursive_directory_iterator(absoluteMovedPath)) {
if (!entry.is_regular_file() ||
MetaCoreIsMetaPath(entry.path()) ||
MetaCoreIsLegacyMetaPath(entry.path()) ||
MetaCoreIsPackagePath(entry.path())) {
continue;
}
const std::filesystem::path metaPath = MetaCoreBuildMetaPath(entry.path());
if (!std::filesystem::exists(metaPath)) {
continue;
}
MetaCoreRewriteMovedAssetMetadata(
entry.path(),
metaPath,
entry.path().lexically_relative(projectRoot)
);
}
}
[[nodiscard]] bool MetaCoreIsGeneratedProjectPath(const std::filesystem::path& relativePath) {
const auto normalized = relativePath.lexically_normal();
const auto iterator = normalized.begin();
@ -2836,6 +2912,16 @@ bool MetaCoreDeserializeReflectedComponentValue(
return *iterator == "Library" || *iterator == "Build" || *iterator == "Runtime";
}
[[nodiscard]] bool MetaCorePathIsUnderOrEqual(
const std::filesystem::path& path,
const std::filesystem::path& parent
) {
const std::string normalizedPath = path.lexically_normal().generic_string();
const std::string normalizedParent = parent.lexically_normal().generic_string();
return normalizedPath == normalizedParent ||
normalizedPath.rfind(normalizedParent + "/", 0) == 0;
}
[[nodiscard]] std::filesystem::path MetaCoreBuildModelRuntimePackagePath(
const std::filesystem::path& projectRoot,
const MetaCoreAssetGuid& modelAssetGuid
@ -3365,12 +3451,106 @@ template <typename T>
return subtreeObjects;
}
void MetaCoreNormalizePrefabSubtree(std::vector<MetaCoreGameObject>& gameObjects, MetaCoreId rootId) {
for (MetaCoreGameObject& gameObject : gameObjects) {
if (gameObject.GetId() == rootId) {
gameObject.SetParentId(0);
[[nodiscard]] bool MetaCoreIsNativePrefabComponentType(std::string_view typeId) {
return typeId == "Transform" ||
typeId == "Camera" ||
typeId == "Light" ||
typeId == "MeshRenderer";
}
void MetaCoreCapturePrefabSerializedComponents(
const MetaCoreGameObject& gameObject,
MetaCoreGameObjectData& data,
const std::shared_ptr<MetaCoreIComponentTypeRegistry>& componentRegistry,
const MetaCoreTypeRegistry& typeRegistry
) {
data.SerializedComponents.clear();
if (componentRegistry == nullptr) {
return;
}
for (const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor : componentRegistry->GetComponentDescriptors()) {
if (MetaCoreIsNativePrefabComponentType(descriptor.TypeId) ||
!descriptor.HasComponent ||
!descriptor.CopyComponentPayload ||
!descriptor.HasComponent(gameObject)) {
continue;
}
const auto payload = descriptor.CopyComponentPayload(gameObject, typeRegistry);
if (!payload.has_value() || payload->empty()) {
continue;
}
data.SerializedComponents.push_back(MetaCoreSerializedComponentData{
descriptor.TypeId,
*payload
});
}
std::sort(
data.SerializedComponents.begin(),
data.SerializedComponents.end(),
[](const MetaCoreSerializedComponentData& lhs, const MetaCoreSerializedComponentData& rhs) {
return lhs.TypeId < rhs.TypeId;
}
);
}
[[nodiscard]] MetaCoreGameObjectData MetaCoreCapturePrefabObjectData(
const MetaCoreGameObject& gameObject,
MetaCoreId rootId,
const std::shared_ptr<MetaCoreIComponentTypeRegistry>& componentRegistry,
const MetaCoreTypeRegistry& typeRegistry
) {
MetaCoreGameObjectData data;
data.Id = gameObject.GetId();
data.Name = gameObject.GetName();
data.ParentId = gameObject.GetId() == rootId ? 0 : gameObject.GetParentId();
data.Transform = gameObject.GetComponent<MetaCoreTransformComponent>();
if (gameObject.HasComponent<MetaCoreCameraComponent>()) data.Camera = gameObject.GetComponent<MetaCoreCameraComponent>();
if (gameObject.HasComponent<MetaCoreLightComponent>()) data.Light = gameObject.GetComponent<MetaCoreLightComponent>();
if (gameObject.HasComponent<MetaCoreMeshRendererComponent>()) data.MeshRenderer = gameObject.GetComponent<MetaCoreMeshRendererComponent>();
MetaCoreCapturePrefabSerializedComponents(gameObject, data, componentRegistry, typeRegistry);
return data;
}
void MetaCoreRestorePrefabSerializedComponents(
MetaCoreEditorContext& editorContext,
MetaCoreGameObject& gameObject,
const MetaCoreGameObjectData& data,
const std::shared_ptr<MetaCoreIComponentTypeRegistry>& componentRegistry,
const MetaCoreTypeRegistry& typeRegistry
) {
if (componentRegistry == nullptr || data.SerializedComponents.empty()) {
return;
}
for (const MetaCoreSerializedComponentData& componentData : data.SerializedComponents) {
if (MetaCoreIsNativePrefabComponentType(componentData.TypeId) || componentData.Payload.empty()) {
continue;
}
const auto* descriptor = componentRegistry->FindDescriptor(componentData.TypeId);
if (descriptor == nullptr || !descriptor->PasteComponentPayload) {
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Warning,
"Prefab",
"Skipped serialized component without registered C++ type: " + componentData.TypeId
);
continue;
}
if (!descriptor->PasteComponentPayload(
gameObject,
std::span<const std::byte>(componentData.Payload.data(), componentData.Payload.size()),
typeRegistry)) {
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Warning,
"Prefab",
"Failed to restore serialized component: " + componentData.TypeId
);
}
gameObject.RemoveComponent<MetaCorePrefabInstanceMetadata>();
}
}
@ -3532,11 +3712,26 @@ public:
[[nodiscard]] bool ReimportAsset(const MetaCoreAssetGuid& assetGuid) override;
bool Refresh() override;
bool CreateFolder(const std::filesystem::path& relativeDirectory) override;
std::optional<MetaCoreAssetRecord> CreateSceneAsset(
const std::filesystem::path& relativeScenePath,
bool makeStartupScene
) override;
std::optional<MetaCoreAssetRecord> CreatePrefabAsset(const std::filesystem::path& relativePrefabPath) override;
std::optional<MetaCoreAssetRecord> CreateMaterialAsset(const std::filesystem::path& relativeMaterialPath) override;
std::optional<MetaCoreAssetRecord> CreateUiDocumentAsset(const std::filesystem::path& relativeUiPath) override;
bool RegisterScenePath(const std::filesystem::path& relativeScenePath, bool makeStartupScene) override;
bool SetStartupScenePath(const std::filesystem::path& relativeScenePath) override;
private:
[[nodiscard]] std::optional<std::filesystem::path> ResolveDirectory(const std::filesystem::path& relativeDirectory) const;
[[nodiscard]] std::optional<MetaCoreAssetRecord> RefreshAndFindCreatedAsset(const std::filesystem::path& relativePath);
[[nodiscard]] bool WriteJsonAssetMetadata(
const std::filesystem::path& absolutePath,
const std::filesystem::path& relativePath,
const MetaCoreAssetGuid& assetGuid,
std::string_view assetType,
std::string_view importerId
) const;
void ApplyProjectDescriptorRoot(const std::filesystem::path& projectRoot);
void LoadProjectDescriptor();
void SaveProjectDescriptor() const;
@ -3712,6 +3907,19 @@ bool MetaCoreBuiltinAssetDatabaseService::RenamePath(
return false;
}
}
if (std::filesystem::exists(legacyMetaTarget)) {
MetaCoreRewriteMovedAssetMetadata(absoluteTargetPath, legacyMetaTarget, targetPath);
}
MetaCoreRewriteMovedAssetMetadataTree(Project_.RootPath, absoluteTargetPath, targetPath);
const std::filesystem::path packageSource = Project_.RootPath / MetaCoreBuildPackagePathFromSource(normalizedPath);
const std::filesystem::path packageTarget = Project_.RootPath / MetaCoreBuildPackagePathFromSource(targetPath);
if (std::filesystem::exists(packageSource) && !std::filesystem::exists(packageTarget)) {
std::filesystem::rename(packageSource, packageTarget, errorCode);
if (errorCode) {
return false;
}
}
const auto remapPath = [&](std::filesystem::path& pathValue) {
if (pathValue == normalizedPath) {
@ -3793,6 +4001,19 @@ bool MetaCoreBuiltinAssetDatabaseService::MovePath(
return false;
}
}
if (std::filesystem::exists(legacyMetaTarget)) {
MetaCoreRewriteMovedAssetMetadata(Project_.RootPath / targetPath, legacyMetaTarget, targetPath);
}
MetaCoreRewriteMovedAssetMetadataTree(Project_.RootPath, Project_.RootPath / targetPath, targetPath);
const std::filesystem::path packageSource = Project_.RootPath / MetaCoreBuildPackagePathFromSource(normalizedSourcePath);
const std::filesystem::path packageTarget = Project_.RootPath / MetaCoreBuildPackagePathFromSource(targetPath);
if (std::filesystem::exists(packageSource) && !std::filesystem::exists(packageTarget)) {
std::filesystem::rename(packageSource, packageTarget, errorCode);
if (errorCode) {
return false;
}
}
remapPath(Project_.StartupScenePath);
for (auto& scenePath : Project_.ScenePaths) {
@ -4053,6 +4274,51 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() {
}
}
if (std::filesystem::exists(Project_.UiPath) &&
!MetaCorePathIsUnderOrEqual(Project_.UiPath, Project_.AssetsPath)) {
for (const auto& entry : std::filesystem::recursive_directory_iterator(Project_.UiPath)) {
if (!entry.is_regular_file() ||
MetaCoreIsMetaPath(entry.path()) ||
MetaCoreIsLegacyMetaPath(entry.path()) ||
MetaCoreIsPackagePath(entry.path())) {
continue;
}
const std::filesystem::path relativePath = entry.path().lexically_relative(Project_.RootPath);
MetaCoreAssetMetadataDocument metadata;
bool hasMetadata = false;
const std::filesystem::path metaPath = MetaCoreBuildMetaPath(entry.path());
if (std::filesystem::exists(metaPath)) {
hasMetadata = MetaCoreReadMetaJson(metaPath, metadata);
}
if (!hasMetadata) {
metadata.AssetGuid = MetaCoreAssetGuid::Generate();
metadata.AssetType = MetaCoreDetectAssetType(entry.path());
metadata.ImporterId = MetaCoreDetectImporterId(entry.path());
metadata.SourcePath = relativePath;
metadata.PackagePath = MetaCoreIsJsonSourceAssetPath(relativePath)
? relativePath
: MetaCoreBuildPackagePathFromSource(relativePath);
metadata.SourceHash = MetaCoreHashFile(entry.path()).value_or(0);
hasMetadata = MetaCoreWriteMetaJson(metaPath, metadata);
}
AssetRecords_.push_back(MetaCoreAssetRecord{
hasMetadata ? metadata.AssetGuid : MetaCoreAssetGuid{},
relativePath,
hasMetadata ? metadata.AssetType : MetaCoreDetectAssetType(entry.path()),
MetaCoreAssetStorageKind::SourceFile,
relativePath,
hasMetadata
? metadata.PackagePath
: (MetaCoreIsJsonSourceAssetPath(relativePath) ? relativePath : MetaCoreBuildPackagePathFromSource(relativePath)),
hasMetadata ? metaPath.lexically_relative(Project_.RootPath) : std::filesystem::path{},
hasMetadata ? metadata.ImporterId : MetaCoreDetectImporterId(entry.path()),
hasMetadata ? metadata.SourceHash : 0
});
}
}
std::sort(AssetRecords_.begin(), AssetRecords_.end(), [](const auto& lhs, const auto& rhs) {
return MetaCorePathToPortableString(lhs.RelativePath) < MetaCorePathToPortableString(rhs.RelativePath);
});
@ -4136,7 +4402,199 @@ bool MetaCoreBuiltinAssetDatabaseService::CreateFolder(const std::filesystem::pa
if (!HasProject() || MetaCoreIsUnsafeRelativePath(relativeDirectory)) {
return false;
}
return std::filesystem::create_directories(Project_.RootPath / relativeDirectory.lexically_normal());
const bool created = std::filesystem::create_directories(Project_.RootPath / relativeDirectory.lexically_normal());
(void)Refresh();
return created;
}
std::optional<MetaCoreAssetRecord> MetaCoreBuiltinAssetDatabaseService::RefreshAndFindCreatedAsset(
const std::filesystem::path& relativePath
) {
if (!Refresh()) {
return std::nullopt;
}
return FindAssetByRelativePath(relativePath.lexically_normal());
}
bool MetaCoreBuiltinAssetDatabaseService::WriteJsonAssetMetadata(
const std::filesystem::path& absolutePath,
const std::filesystem::path& relativePath,
const MetaCoreAssetGuid& assetGuid,
std::string_view assetType,
std::string_view importerId
) const {
if (!assetGuid.IsValid()) {
return false;
}
MetaCoreAssetMetadataDocument metadata;
metadata.AssetGuid = assetGuid;
metadata.AssetType = std::string(assetType);
metadata.ImporterId = std::string(importerId);
metadata.SourcePath = relativePath.lexically_normal();
metadata.PackagePath = metadata.SourcePath;
metadata.SourceHash = MetaCoreHashFile(absolutePath).value_or(0);
return MetaCoreWriteMetaJson(MetaCoreBuildMetaPath(absolutePath), metadata);
}
std::optional<MetaCoreAssetRecord> MetaCoreBuiltinAssetDatabaseService::CreateSceneAsset(
const std::filesystem::path& relativeScenePath,
bool makeStartupScene
) {
if (!HasProject() || ReflectionRegistry_ == nullptr || relativeScenePath.empty() ||
MetaCoreIsUnsafeRelativePath(relativeScenePath)) {
return std::nullopt;
}
std::filesystem::path normalizedPath = relativeScenePath.lexically_normal();
if (normalizedPath.extension() == ".mcscene") {
normalizedPath.replace_extension(".mcscene.json");
} else if (!MetaCoreIsScenePath(normalizedPath)) {
normalizedPath = normalizedPath.parent_path() / (normalizedPath.stem().string() + ".mcscene.json");
}
if (MetaCoreIsUnsafeRelativePath(normalizedPath)) {
return std::nullopt;
}
const std::filesystem::path absolutePath = Project_.RootPath / normalizedPath;
if (std::filesystem::exists(absolutePath)) {
return std::nullopt;
}
(void)std::filesystem::create_directories(absolutePath.parent_path());
MetaCoreSceneDocument document;
document.Name = absolutePath.stem().stem().string();
document.GameObjects = MetaCoreCreateDefaultScene().CaptureSnapshot().GameObjects;
if (!MetaCoreSceneSerializer::SaveSceneToJson(absolutePath, document, ReflectionRegistry_->GetTypeRegistry())) {
return std::nullopt;
}
const MetaCoreAssetGuid sceneGuid = MetaCoreAssetGuid::Generate();
if (!WriteJsonAssetMetadata(absolutePath, normalizedPath, sceneGuid, "scene", "SceneImporter")) {
return std::nullopt;
}
(void)RegisterScenePath(normalizedPath, makeStartupScene);
return RefreshAndFindCreatedAsset(normalizedPath);
}
std::optional<MetaCoreAssetRecord> MetaCoreBuiltinAssetDatabaseService::CreatePrefabAsset(
const std::filesystem::path& relativePrefabPath
) {
if (!HasProject() || ReflectionRegistry_ == nullptr || relativePrefabPath.empty() ||
MetaCoreIsUnsafeRelativePath(relativePrefabPath)) {
return std::nullopt;
}
std::filesystem::path normalizedPath = relativePrefabPath.lexically_normal();
if (normalizedPath.extension() == ".mcprefab") {
normalizedPath.replace_extension(".mcprefab.json");
} else if (!MetaCoreIsPrefabPath(normalizedPath)) {
normalizedPath = normalizedPath.parent_path() / (normalizedPath.stem().string() + ".mcprefab.json");
}
if (MetaCoreIsUnsafeRelativePath(normalizedPath)) {
return std::nullopt;
}
const std::filesystem::path absolutePath = Project_.RootPath / normalizedPath;
if (std::filesystem::exists(absolutePath)) {
return std::nullopt;
}
(void)std::filesystem::create_directories(absolutePath.parent_path());
MetaCorePrefabDocument document;
document.Name = absolutePath.stem().stem().string();
MetaCoreGameObjectData rootObject;
rootObject.Id = 1;
rootObject.Name = document.Name.empty() ? "PrefabRoot" : document.Name;
document.GameObjects.push_back(std::move(rootObject));
if (!MetaCoreSceneSerializer::SavePrefabToJson(absolutePath, document, ReflectionRegistry_->GetTypeRegistry())) {
return std::nullopt;
}
const MetaCoreAssetGuid prefabGuid = MetaCoreAssetGuid::Generate();
if (!WriteJsonAssetMetadata(absolutePath, normalizedPath, prefabGuid, "prefab", "PrefabImporter")) {
return std::nullopt;
}
return RefreshAndFindCreatedAsset(normalizedPath);
}
std::optional<MetaCoreAssetRecord> MetaCoreBuiltinAssetDatabaseService::CreateMaterialAsset(
const std::filesystem::path& relativeMaterialPath
) {
if (!HasProject() || ReflectionRegistry_ == nullptr || relativeMaterialPath.empty() ||
MetaCoreIsUnsafeRelativePath(relativeMaterialPath)) {
return std::nullopt;
}
std::filesystem::path normalizedPath = relativeMaterialPath.lexically_normal();
if (!MetaCoreIsMaterialPath(normalizedPath)) {
normalizedPath = normalizedPath.parent_path() / (normalizedPath.stem().string() + ".mcmaterial.json");
}
if (MetaCoreIsUnsafeRelativePath(normalizedPath)) {
return std::nullopt;
}
const std::filesystem::path absolutePath = Project_.RootPath / normalizedPath;
if (std::filesystem::exists(absolutePath)) {
return std::nullopt;
}
(void)std::filesystem::create_directories(absolutePath.parent_path());
MetaCoreMaterialAssetDocument document;
document.AssetGuid = MetaCoreAssetGuid::Generate();
document.Name = absolutePath.stem().stem().string();
if (!MetaCoreSceneSerializer::SaveMaterialToJson(absolutePath, document, ReflectionRegistry_->GetTypeRegistry())) {
return std::nullopt;
}
if (!WriteJsonAssetMetadata(absolutePath, normalizedPath, document.AssetGuid, "material", "MaterialImporter")) {
return std::nullopt;
}
return RefreshAndFindCreatedAsset(normalizedPath);
}
std::optional<MetaCoreAssetRecord> MetaCoreBuiltinAssetDatabaseService::CreateUiDocumentAsset(
const std::filesystem::path& relativeUiPath
) {
if (!HasProject() || ReflectionRegistry_ == nullptr || relativeUiPath.empty() ||
MetaCoreIsUnsafeRelativePath(relativeUiPath)) {
return std::nullopt;
}
std::filesystem::path normalizedPath = relativeUiPath.lexically_normal();
if (normalizedPath.extension() == ".mcui") {
normalizedPath.replace_extension(".mcui.json");
} else if (!MetaCoreIsUiPath(normalizedPath)) {
normalizedPath = normalizedPath.parent_path() / (normalizedPath.stem().string() + ".mcui.json");
}
if (MetaCoreIsUnsafeRelativePath(normalizedPath)) {
return std::nullopt;
}
const std::filesystem::path absolutePath = Project_.RootPath / normalizedPath;
if (std::filesystem::exists(absolutePath)) {
return std::nullopt;
}
(void)std::filesystem::create_directories(absolutePath.parent_path());
MetaCoreUiDocument document;
document.Name = absolutePath.stem().stem().string();
MetaCoreUiNodeDocument rootNode;
rootNode.Id = "root";
rootNode.Name = "Root";
rootNode.Type = MetaCoreUiNodeType::Panel;
rootNode.RectTransform.Size = glm::vec3(1920.0F, 1080.0F, 0.0F);
document.RootNodeIds.push_back(rootNode.Id);
document.Nodes.push_back(std::move(rootNode));
if (!MetaCoreSceneSerializer::SaveUiToJson(absolutePath, document, ReflectionRegistry_->GetTypeRegistry())) {
return std::nullopt;
}
const MetaCoreAssetGuid uiGuid = MetaCoreAssetGuid::Generate();
if (!WriteJsonAssetMetadata(absolutePath, normalizedPath, uiGuid, "ui_document", "UiDocumentImporter")) {
return std::nullopt;
}
return RefreshAndFindCreatedAsset(normalizedPath);
}
bool MetaCoreBuiltinAssetDatabaseService::RegisterScenePath(
@ -6143,6 +6601,7 @@ public:
AssetDatabaseService_ = moduleRegistry.ResolveService<MetaCoreIAssetDatabaseService>();
PackageService_ = moduleRegistry.ResolveService<MetaCoreIPackageService>();
ReflectionRegistry_ = moduleRegistry.ResolveService<MetaCoreIReflectionRegistry>();
ComponentRegistry_ = moduleRegistry.ResolveService<MetaCoreIComponentTypeRegistry>();
}
void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override {
@ -6150,6 +6609,7 @@ public:
AssetDatabaseService_.reset();
PackageService_.reset();
ReflectionRegistry_.reset();
ComponentRegistry_.reset();
}
[[nodiscard]] bool SupportsPrefabWorkflows() const override { return true; }
@ -6180,6 +6640,7 @@ private:
std::shared_ptr<MetaCoreIAssetDatabaseService> AssetDatabaseService_{};
std::shared_ptr<MetaCoreIPackageService> PackageService_{};
std::shared_ptr<MetaCoreIReflectionRegistry> ReflectionRegistry_{};
std::shared_ptr<MetaCoreIComponentTypeRegistry> ComponentRegistry_{};
};
class MetaCoreBuiltinComponentTypeRegistry final : public MetaCoreIComponentTypeRegistry {
@ -6514,10 +6975,38 @@ public:
}
if (descriptor.Lifecycle.OnDrawGizmos) {
descriptor.Lifecycle.OnDrawGizmos(editorContext, gameObject);
try {
descriptor.Lifecycle.OnDrawGizmos(editorContext, gameObject);
} catch (const std::exception& exception) {
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Error,
"Gizmos",
descriptor.DisplayName + " OnDrawGizmos failed: " + exception.what()
);
} catch (...) {
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Error,
"Gizmos",
descriptor.DisplayName + " OnDrawGizmos failed with an unknown exception"
);
}
}
if (descriptor.Lifecycle.OnDrawGizmosSelected && editorContext.IsObjectSelected(objectId)) {
descriptor.Lifecycle.OnDrawGizmosSelected(editorContext, gameObject);
try {
descriptor.Lifecycle.OnDrawGizmosSelected(editorContext, gameObject);
} catch (const std::exception& exception) {
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Error,
"Gizmos",
descriptor.DisplayName + " OnDrawGizmosSelected failed: " + exception.what()
);
} catch (...) {
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Error,
"Gizmos",
descriptor.DisplayName + " OnDrawGizmosSelected failed with an unknown exception"
);
}
}
}
}
@ -6540,21 +7029,23 @@ public:
(void)moduleRegistry;
State_ = MetaCorePlayModeState::Edit;
EditStateSnapshot_.reset();
StartedLifecycleComponents_.clear();
RuntimeLifecycleExecutor_ = MetaCoreRuntimeLifecycleExecutor{};
ComponentRegistry_.reset();
}
[[nodiscard]] MetaCorePlayModeState GetState() const override { return State_; }
[[nodiscard]] bool IsRuntimeSceneActive() const override { return State_ != MetaCorePlayModeState::Edit; }
[[nodiscard]] bool CanEnterPlayMode() const override { return State_ == MetaCorePlayModeState::Edit; }
[[nodiscard]] bool EnterPlayMode(MetaCoreEditorContext& editorContext) override {
if (!CanEnterPlayMode()) {
return false;
}
EditStateSnapshot_ = editorContext.CaptureStateSnapshot();
StartedLifecycleComponents_.clear();
ElapsedPlayTimeSeconds_ = 0.0F;
editorContext.RestoreStateSnapshot(*EditStateSnapshot_);
editorContext.SetRuntimeSceneActive(true);
ConfigureRuntimeLifecycleExecutor(editorContext);
State_ = MetaCorePlayModeState::Playing;
InvokeLifecycleStart(editorContext);
RuntimeLifecycleExecutor_.EnterScene(editorContext.GetScene(), BuildLifecycleErrorSink(editorContext));
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "PlayMode", "Entered Play Mode");
return true;
}
@ -6562,14 +7053,15 @@ public:
if (State_ == MetaCorePlayModeState::Edit) {
return false;
}
InvokeLifecycleDestroy(editorContext);
RuntimeLifecycleExecutor_.ExitScene(editorContext.GetScene(), BuildLifecycleErrorSink(editorContext));
editorContext.SetRuntimeSceneActive(false);
if (EditStateSnapshot_.has_value()) {
editorContext.RestoreStateSnapshot(*EditStateSnapshot_);
}
EditStateSnapshot_.reset();
StartedLifecycleComponents_.clear();
ElapsedPlayTimeSeconds_ = 0.0F;
RuntimeLifecycleExecutor_ = MetaCoreRuntimeLifecycleExecutor{};
State_ = MetaCorePlayModeState::Edit;
editorContext.SetRuntimeSceneActive(false);
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "PlayMode", "Exited Play Mode");
return true;
}
@ -6593,91 +7085,110 @@ public:
if (State_ != MetaCorePlayModeState::Paused) {
return false;
}
const float sanitizedDeltaSeconds = std::max(deltaSeconds, 0.0F);
InvokeLifecycleStart(editorContext);
InvokeLifecycleUpdate(editorContext, sanitizedDeltaSeconds);
ElapsedPlayTimeSeconds_ += sanitizedDeltaSeconds;
RuntimeLifecycleExecutor_.TickScene(editorContext.GetScene(), deltaSeconds, BuildLifecycleErrorSink(editorContext));
return true;
}
[[nodiscard]] float GetElapsedPlayTimeSeconds() const override { return ElapsedPlayTimeSeconds_; }
[[nodiscard]] float GetElapsedPlayTimeSeconds() const override { return RuntimeLifecycleExecutor_.GetElapsedTimeSeconds(); }
void NotifyGameObjectsWillBeDestroyed(
MetaCoreEditorContext& editorContext,
const std::vector<MetaCoreId>& objectIds
) override {
if (State_ == MetaCorePlayModeState::Edit) {
return;
}
RuntimeLifecycleExecutor_.NotifyGameObjectsWillBeDestroyed(
editorContext.GetScene(),
objectIds,
BuildLifecycleErrorSink(editorContext)
);
}
void NotifyComponentWillBeRemoved(
MetaCoreEditorContext& editorContext,
MetaCoreId objectId,
std::string_view componentTypeId
) override {
if (State_ == MetaCorePlayModeState::Edit) {
return;
}
RuntimeLifecycleExecutor_.NotifyComponentWillBeRemoved(
editorContext.GetScene(),
objectId,
componentTypeId,
BuildLifecycleErrorSink(editorContext)
);
}
void TickPlayMode(MetaCoreEditorContext& editorContext, float deltaSeconds) override {
if (State_ != MetaCorePlayModeState::Playing) {
return;
}
const float sanitizedDeltaSeconds = std::max(deltaSeconds, 0.0F);
InvokeLifecycleStart(editorContext);
InvokeLifecycleUpdate(editorContext, sanitizedDeltaSeconds);
ElapsedPlayTimeSeconds_ += sanitizedDeltaSeconds;
RuntimeLifecycleExecutor_.TickScene(editorContext.GetScene(), deltaSeconds, BuildLifecycleErrorSink(editorContext));
}
private:
[[nodiscard]] std::string BuildLifecycleKey(MetaCoreId objectId, std::string_view componentTypeId) const {
return std::to_string(objectId) + ":" + std::string(componentTypeId);
[[nodiscard]] MetaCoreRuntimeLifecycleExecutor::ErrorSink BuildLifecycleErrorSink(MetaCoreEditorContext& editorContext) {
return [&editorContext](const MetaCoreRuntimeLifecycleError& error) {
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Error,
"PlayMode",
"Lifecycle " + error.Phase +
" failed for " + error.ComponentTypeId +
" on object " + std::to_string(error.ObjectId) +
": " + error.Message
);
};
}
template <typename TCallback>
void ForEachLifecycleComponent(MetaCoreEditorContext& editorContext, TCallback&& callback) {
void ConfigureRuntimeLifecycleExecutor(MetaCoreEditorContext& editorContext) {
RuntimeLifecycleExecutor_ = MetaCoreRuntimeLifecycleExecutor{};
if (ComponentRegistry_ == nullptr) {
return;
}
const std::vector<MetaCoreId> objectIds = editorContext.GetScene().BuildHierarchyPreorder();
for (const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor : ComponentRegistry_->GetComponentDescriptors()) {
for (MetaCoreId objectId : objectIds) {
MetaCoreGameObject gameObject = editorContext.GetScene().FindGameObject(objectId);
if (!gameObject || !descriptor.HasComponent || !descriptor.HasComponent(gameObject)) {
continue;
}
callback(descriptor, gameObject);
for (const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor :
ComponentRegistry_->GetComponentDescriptors()) {
if (!descriptor.HasComponent ||
(!descriptor.Lifecycle.OnStart &&
!descriptor.Lifecycle.OnUpdate &&
!descriptor.Lifecycle.OnDestroy)) {
continue;
}
MetaCoreRuntimeLifecycleDescriptor runtimeDescriptor;
runtimeDescriptor.TypeId = descriptor.TypeId;
runtimeDescriptor.HasComponent = descriptor.HasComponent;
if (descriptor.Lifecycle.OnStart) {
runtimeDescriptor.OnStart =
[&editorContext, onStart = descriptor.Lifecycle.OnStart](MetaCoreScene&, MetaCoreGameObject& gameObject) {
onStart(editorContext, gameObject);
};
}
if (descriptor.Lifecycle.OnUpdate) {
runtimeDescriptor.OnUpdate =
[&editorContext, onUpdate = descriptor.Lifecycle.OnUpdate](
MetaCoreScene&,
MetaCoreGameObject& gameObject,
float deltaSeconds
) {
onUpdate(editorContext, gameObject, deltaSeconds);
};
}
if (descriptor.Lifecycle.OnDestroy) {
runtimeDescriptor.OnDestroy =
[&editorContext, onDestroy = descriptor.Lifecycle.OnDestroy](MetaCoreScene&, MetaCoreGameObject& gameObject) {
onDestroy(editorContext, gameObject);
};
}
(void)RuntimeLifecycleExecutor_.RegisterDescriptor(std::move(runtimeDescriptor));
}
}
void InvokeLifecycleStart(MetaCoreEditorContext& editorContext) {
ForEachLifecycleComponent(editorContext, [&](const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, MetaCoreGameObject& gameObject) {
const std::string key = BuildLifecycleKey(gameObject.GetId(), descriptor.TypeId);
if (StartedLifecycleComponents_.contains(key)) {
return;
}
StartedLifecycleComponents_.insert(key);
if (descriptor.Lifecycle.OnStart) {
descriptor.Lifecycle.OnStart(editorContext, gameObject);
}
});
}
void InvokeLifecycleUpdate(MetaCoreEditorContext& editorContext, float deltaSeconds) {
ForEachLifecycleComponent(editorContext, [&](const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, MetaCoreGameObject& gameObject) {
if (!descriptor.Lifecycle.OnUpdate) {
return;
}
const std::string key = BuildLifecycleKey(gameObject.GetId(), descriptor.TypeId);
if (!StartedLifecycleComponents_.contains(key)) {
return;
}
descriptor.Lifecycle.OnUpdate(editorContext, gameObject, deltaSeconds);
});
}
void InvokeLifecycleDestroy(MetaCoreEditorContext& editorContext) {
ForEachLifecycleComponent(editorContext, [&](const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, MetaCoreGameObject& gameObject) {
if (!descriptor.Lifecycle.OnDestroy) {
return;
}
const std::string key = BuildLifecycleKey(gameObject.GetId(), descriptor.TypeId);
if (!StartedLifecycleComponents_.contains(key)) {
return;
}
descriptor.Lifecycle.OnDestroy(editorContext, gameObject);
});
}
MetaCorePlayModeState State_ = MetaCorePlayModeState::Edit;
float ElapsedPlayTimeSeconds_ = 0.0F;
std::optional<MetaCoreEditorStateSnapshot> EditStateSnapshot_{};
std::unordered_set<std::string> StartedLifecycleComponents_{};
MetaCoreRuntimeLifecycleExecutor RuntimeLifecycleExecutor_{};
std::shared_ptr<MetaCoreIComponentTypeRegistry> ComponentRegistry_{};
};
@ -6756,15 +7267,13 @@ std::optional<std::filesystem::path> MetaCoreBuiltinPrefabService::CreatePrefabF
return std::nullopt;
}
std::vector<MetaCoreGameObject> prefabObjects = MetaCoreCaptureSubtree(editorContext.GetScene(), activeObjectId);
const std::vector<MetaCoreGameObject> prefabObjects = MetaCoreCaptureSubtree(editorContext.GetScene(), activeObjectId);
if (prefabObjects.empty()) {
return std::nullopt;
}
MetaCoreNormalizePrefabSubtree(prefabObjects, activeObjectId);
const MetaCoreGameObject rootObject = editorContext.GetScene().FindGameObject(activeObjectId);
const std::filesystem::path normalizedPrefabPath = relativePrefabPath.lexically_normal();
const std::filesystem::path normalizedPrefabPath = MetaCoreNormalizePrefabJsonPath(relativePrefabPath);
const std::filesystem::path absolutePrefabPath = AssetDatabaseService_->GetProjectDescriptor().RootPath / normalizedPrefabPath;
MetaCoreAssetGuid prefabGuid;
@ -6788,15 +7297,12 @@ std::optional<std::filesystem::path> MetaCoreBuiltinPrefabService::CreatePrefabF
std::vector<MetaCoreGameObjectData> prefabDataObjects;
prefabDataObjects.reserve(prefabObjects.size());
for (const MetaCoreGameObject& gameObject : prefabObjects) {
MetaCoreGameObjectData data;
data.Id = gameObject.GetId();
data.Name = gameObject.GetName();
data.ParentId = gameObject.GetParentId();
data.Transform = gameObject.GetComponent<MetaCoreTransformComponent>();
if (gameObject.HasComponent<MetaCoreCameraComponent>()) data.Camera = gameObject.GetComponent<MetaCoreCameraComponent>();
if (gameObject.HasComponent<MetaCoreLightComponent>()) data.Light = gameObject.GetComponent<MetaCoreLightComponent>();
if (gameObject.HasComponent<MetaCoreMeshRendererComponent>()) data.MeshRenderer = gameObject.GetComponent<MetaCoreMeshRendererComponent>();
prefabDataObjects.push_back(std::move(data));
prefabDataObjects.push_back(MetaCoreCapturePrefabObjectData(
gameObject,
activeObjectId,
ComponentRegistry_,
ReflectionRegistry_->GetTypeRegistry()
));
}
prefabDocument.GameObjects = std::move(prefabDataObjects);
@ -6816,6 +7322,27 @@ std::optional<std::filesystem::path> MetaCoreBuiltinPrefabService::CreatePrefabF
(void)MetaCoreWriteMetaJson(MetaCoreBuildMetaPath(absolutePrefabPath), metadata);
(void)AssetDatabaseService_->Refresh();
(void)editorContext.ExecuteSnapshotCommand("Create Prefab Instance Link", [&]() {
bool updatedAnyMetadata = false;
for (MetaCoreGameObject gameObject : prefabObjects) {
if (!gameObject) {
continue;
}
const MetaCorePrefabInstanceMetadata prefabMetadata{
prefabGuid,
gameObject.GetId(),
activeObjectId
};
if (gameObject.HasComponent<MetaCorePrefabInstanceMetadata>()) {
gameObject.GetComponent<MetaCorePrefabInstanceMetadata>() = prefabMetadata;
} else {
gameObject.AddComponent<MetaCorePrefabInstanceMetadata>(prefabMetadata);
}
updatedAnyMetadata = true;
}
return updatedAnyMetadata;
});
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "已创建 Prefab: " + normalizedPrefabPath.generic_string());
return normalizedPrefabPath;
}
@ -6857,6 +7384,13 @@ std::optional<MetaCoreId> MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen
if (sourceObject.Camera.has_value()) clonedObject.AddComponent<MetaCoreCameraComponent>(*sourceObject.Camera);
if (sourceObject.Light.has_value()) clonedObject.AddComponent<MetaCoreLightComponent>(*sourceObject.Light);
if (sourceObject.MeshRenderer.has_value()) clonedObject.AddComponent<MetaCoreMeshRendererComponent>(*sourceObject.MeshRenderer);
MetaCoreRestorePrefabSerializedComponents(
editorContext,
clonedObject,
sourceObject,
ComponentRegistry_,
ReflectionRegistry_->GetTypeRegistry()
);
clonedObject.AddComponent<MetaCorePrefabInstanceMetadata>(MetaCorePrefabInstanceMetadata{
prefabAssetGuid,
@ -6967,6 +7501,12 @@ bool MetaCoreBuiltinPrefabService::ApplySelectedPrefabInstance(MetaCoreEditorCon
if (gameObject.HasComponent<MetaCoreCameraComponent>()) data.Camera = gameObject.GetComponent<MetaCoreCameraComponent>();
if (gameObject.HasComponent<MetaCoreLightComponent>()) data.Light = gameObject.GetComponent<MetaCoreLightComponent>();
if (gameObject.HasComponent<MetaCoreMeshRendererComponent>()) data.MeshRenderer = gameObject.GetComponent<MetaCoreMeshRendererComponent>();
MetaCoreCapturePrefabSerializedComponents(
gameObject,
data,
ComponentRegistry_,
ReflectionRegistry_->GetTypeRegistry()
);
prefabDataObjects.push_back(std::move(data));
}
@ -6982,9 +7522,7 @@ bool MetaCoreBuiltinPrefabService::ApplySelectedPrefabInstance(MetaCoreEditorCon
std::filesystem::path normalizedPrefabPath =
!prefabAsset->PackagePath.empty() ? prefabAsset->PackagePath : prefabAsset->RelativePath;
if (normalizedPrefabPath.extension() == ".mcprefab") {
normalizedPrefabPath.replace_extension(".mcprefab.json");
}
normalizedPrefabPath = MetaCoreNormalizePrefabJsonPath(normalizedPrefabPath);
const std::filesystem::path absolutePrefabPath = AssetDatabaseService_->GetProjectDescriptor().RootPath / normalizedPrefabPath;
bool saveSuccess = MetaCoreSceneSerializer::SavePrefabToJson(absolutePrefabPath, prefabDocument, ReflectionRegistry_->GetTypeRegistry());
@ -7021,6 +7559,10 @@ bool MetaCoreBuiltinPrefabService::RevertSelectedPrefabInstance(MetaCoreEditorCo
const MetaCoreId parentId = rootObject.GetParentId();
const bool reverted = editorContext.ExecuteSnapshotCommand("还原 Prefab 实例", [&]() {
if (const auto playModeService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIPlayModeService>();
playModeService != nullptr) {
playModeService->NotifyGameObjectsWillBeDestroyed(editorContext, {*instanceRootId});
}
std::vector<MetaCoreId> deletedIds = editorContext.GetScene().DeleteGameObjects({*instanceRootId});
if (deletedIds.empty()) {
return false;
@ -7098,6 +7640,10 @@ bool MetaCoreBuiltinClipboardService::DeleteSelection(MetaCoreEditorContext& edi
std::vector<MetaCoreId> deletedIds;
const bool deleted = editorContext.ExecuteSnapshotCommand("删除对象", [&]() {
if (const auto playModeService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIPlayModeService>();
playModeService != nullptr) {
playModeService->NotifyGameObjectsWillBeDestroyed(editorContext, selectedIds);
}
deletedIds = editorContext.GetScene().DeleteGameObjects(selectedIds);
if (deletedIds.empty()) {
return false;
@ -7451,8 +7997,8 @@ public:
moduleRegistry.RegisterService<MetaCoreISelectionService>(std::make_shared<MetaCoreBuiltinSelectionService>());
moduleRegistry.RegisterService<MetaCoreIClipboardService>(std::make_shared<MetaCoreBuiltinClipboardService>());
moduleRegistry.RegisterService<MetaCoreISceneEditingService>(std::make_shared<MetaCoreBuiltinSceneEditingService>());
moduleRegistry.RegisterService<MetaCoreIPrefabService>(std::make_shared<MetaCoreBuiltinPrefabService>());
moduleRegistry.RegisterService<MetaCoreIComponentTypeRegistry>(std::make_shared<MetaCoreBuiltinComponentTypeRegistry>());
moduleRegistry.RegisterService<MetaCoreIPrefabService>(std::make_shared<MetaCoreBuiltinPrefabService>());
moduleRegistry.RegisterService<MetaCoreIPlayModeService>(std::make_shared<MetaCoreBuiltinPlayModeService>());
if (const auto assetDatabase = moduleRegistry.ResolveService<MetaCoreIAssetDatabaseService>();

View File

@ -4249,6 +4249,165 @@ void MetaCorePollPlayerProcess(MetaCoreEditorContext& editorContext) {
}
}
// 辅助函数:绘制 Unity/Infernux 风格的贴图插槽控件 (ObjectField)
// 支持拖放接收 texture 资产、X 键清空和 ◎ 弹出 Picker 列表
void DrawTextureField(
const char* label,
MetaCoreAssetGuid& currentGuid,
MetaCoreIAssetDatabaseService& assetDatabaseService,
bool& changed
) {
ImGui::PushID(label);
// 1. 绘制标签
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(label);
// 将贴图插槽控件对齐到大约 40% 的宽度,保留与 Unity 面板一致的比例
ImGui::SameLine(ImGui::GetContentRegionMax().x * 0.40f);
// 2. 解析当前绑定的贴图名称
std::string previewName = "None (Texture)";
std::string fullPathStr = "None";
if (currentGuid.IsValid()) {
const auto record = assetDatabaseService.FindAssetByGuid(currentGuid);
if (record.has_value()) {
previewName = record->RelativePath.filename().string();
fullPathStr = record->RelativePath.string();
} else {
previewName = "Missing (" + currentGuid.ToString() + ")";
fullPathStr = "Missing Guid: " + currentGuid.ToString();
}
}
// 3. 贴图插槽按钮,宽度减去清除按钮、选择按钮以及额外空隙
float buttonWidth = ImGui::GetContentRegionAvail().x - 56.0f;
if (buttonWidth < 50.0f) {
buttonWidth = 50.0f;
}
// 未绑定贴图时文字呈现灰色以示区分
if (!currentGuid.IsValid()) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
}
if (ImGui::Button(previewName.c_str(), ImVec2(buttonWidth, 0.0f))) {
ImGui::OpenPopup("TexturePickerPopup");
}
if (!currentGuid.IsValid()) {
ImGui::PopStyleColor();
}
// 悬停显示完整相对路径
if (ImGui::IsItemHovered() && currentGuid.IsValid()) {
ImGui::SetTooltip("%s", fullPathStr.c_str());
}
// 支持拖拽放置
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(MetaCoreProjectAssetDragDropPayloadType)) {
if (payload->DataSize == sizeof(MetaCoreProjectAssetDragDropPayload)) {
const auto* assetPayload = static_cast<const MetaCoreProjectAssetDragDropPayload*>(payload->Data);
if (assetPayload != nullptr && std::string(assetPayload->AssetType) == "texture") {
if (currentGuid != assetPayload->AssetGuid) {
currentGuid = assetPayload->AssetGuid;
changed = true;
}
}
}
}
ImGui::EndDragDropTarget();
}
ImGui::SameLine(0, 4);
// 4. 清除按钮 (X)
if (ImGui::Button("X", ImVec2(22.0f, 0.0f))) {
if (currentGuid.IsValid()) {
currentGuid = MetaCoreAssetGuid{};
changed = true;
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("清除绑定的贴图");
}
ImGui::SameLine(0, 4);
// 5. Picker 弹出列表按钮 (◎)
if (ImGui::Button("", ImVec2(22.0f, 0.0f))) {
ImGui::OpenPopup("TexturePickerPopup");
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("选择贴图");
}
// 6. Picker 弹出列表
if (ImGui::BeginPopup("TexturePickerPopup")) {
ImGui::TextDisabled("选择贴图资源");
ImGui::Separator();
// 搜索过滤栏
static char filterBuffer[128] = "";
ImGui::InputText("##Filter", filterBuffer, IM_ARRAYSIZE(filterBuffer));
ImGui::SameLine();
if (ImGui::Button("清除")) {
filterBuffer[0] = '\0';
}
ImGui::Spacing();
if (ImGui::BeginChild("TextureList", ImVec2(250.0f, 200.0f), true)) {
// "None" 选项
const bool isNoneSelected = !currentGuid.IsValid();
if (ImGui::Selectable("None", isNoneSelected)) {
if (currentGuid.IsValid()) {
currentGuid = MetaCoreAssetGuid{};
changed = true;
}
ImGui::CloseCurrentPopup();
}
for (const auto& a : assetDatabaseService.GetAssetRegistry()) {
if (a.Type == "texture") {
std::string filename = a.RelativePath.filename().string();
// 忽略大小写进行搜索过滤
if (filterBuffer[0] != '\0') {
std::string lowerFilename = filename;
for (char& c : lowerFilename) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
std::string lowerFilter = filterBuffer;
for (char& c : lowerFilter) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
if (lowerFilename.find(lowerFilter) == std::string::npos) {
continue;
}
}
const bool selected = (currentGuid == a.Guid);
if (ImGui::Selectable(filename.c_str(), selected)) {
if (currentGuid != a.Guid) {
currentGuid = a.Guid;
changed = true;
}
ImGui::CloseCurrentPopup();
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", a.RelativePath.string().c_str());
}
}
}
ImGui::EndChild();
}
ImGui::EndPopup();
}
ImGui::PopID();
}
void DrawMaterialAssetDetails(
MetaCoreEditorContext& editorContext,
MetaCoreIAssetDatabaseService& assetDatabaseService,
@ -4282,7 +4441,7 @@ void DrawMaterialAssetDetails(
bool changed = false;
float color[3] = { materialDoc.BaseColor.r, materialDoc.BaseColor.g, materialDoc.BaseColor.b };
if (ImGui::ColorEdit3("基础颜色 (Base Color)", color)) {
if (ImGui::ColorEdit3("基础颜色 (Albedo)", color)) {
materialDoc.BaseColor = glm::vec3(color[0], color[1], color[2]);
changed = true;
}
@ -4302,46 +4461,16 @@ void DrawMaterialAssetDetails(
ImGui::Spacing();
ImGui::Separator();
ImGui::Text("贴图绑定 (Textures)");
ImGui::Spacing();
auto drawTextureCombo = [&](const char* label, MetaCoreAssetGuid& currentGuid) {
std::string previewName = "None";
if (currentGuid.IsValid()) {
const auto record = assetDatabaseService.FindAssetByGuid(currentGuid);
if (record.has_value()) {
previewName = record->RelativePath.filename().string();
} else {
previewName = "Missing (" + currentGuid.ToString() + ")";
}
}
if (ImGui::BeginCombo(label, previewName.c_str())) {
if (ImGui::Selectable("None", !currentGuid.IsValid())) {
if (currentGuid.IsValid()) {
currentGuid = MetaCoreAssetGuid{};
changed = true;
}
}
for (const auto& a : assetDatabaseService.GetAssetRegistry()) {
if (a.Type == "texture") {
const bool selected = (currentGuid == a.Guid);
const std::string name = a.RelativePath.filename().string();
if (ImGui::Selectable(name.c_str(), selected)) {
if (currentGuid != a.Guid) {
currentGuid = a.Guid;
changed = true;
}
}
}
}
ImGui::EndCombo();
}
};
drawTextureCombo("基础颜色贴图 (Base Color Texture)", materialDoc.BaseColorTexture);
drawTextureCombo("法线贴图 (Normal Texture)", materialDoc.NormalTexture);
drawTextureCombo("金属粗糙度贴图 (Metallic Roughness)", materialDoc.MetallicRoughnessTexture);
drawTextureCombo("遮蔽贴图 (Occlusion/Ao Texture)", materialDoc.AoTexture);
// 使用全新的 Unity/Infernux 风格贴图字段控件进行渲染
DrawTextureField("基础贴图 (Albedo Map)", materialDoc.BaseColorTexture, assetDatabaseService, changed);
ImGui::Spacing();
DrawTextureField("法线贴图 (Normal Map)", materialDoc.NormalTexture, assetDatabaseService, changed);
ImGui::Spacing();
DrawTextureField("金属粗糙度 (Metallic Roughness Map)", materialDoc.MetallicRoughnessTexture, assetDatabaseService, changed);
ImGui::Spacing();
DrawTextureField("遮蔽贴图 (Occlusion Map)", materialDoc.AoTexture, assetDatabaseService, changed);
if (changed) {
if (assetEditingService->SaveMaterialAsset(selectedAsset.Guid, materialDoc)) {

View File

@ -459,6 +459,20 @@ bool MetaCoreEditorContext::IsObjectSelected(MetaCoreId objectId) const {
return std::find(SelectedObjectIds_.begin(), SelectedObjectIds_.end(), objectId) != SelectedObjectIds_.end();
}
MetaCoreId MetaCoreEditorContext::GetHoveredObjectId() const { return HoveredObjectId_; }
bool MetaCoreEditorContext::IsObjectHovered(MetaCoreId objectId) const {
return objectId != 0 && HoveredObjectId_ == objectId;
}
void MetaCoreEditorContext::SetHoveredObjectId(MetaCoreId objectId) {
HoveredObjectId_ = (objectId != 0 && (bool)Scene_.FindGameObject(objectId)) ? objectId : 0;
}
void MetaCoreEditorContext::ClearHoveredObject() {
HoveredObjectId_ = 0;
}
void MetaCoreEditorContext::ClearSelection() {
SelectedObjectIds_.clear();
ActiveObjectId_ = 0;
@ -586,20 +600,100 @@ MetaCoreGizmoOperation MetaCoreEditorContext::GetGizmoOperation() const { return
void MetaCoreEditorContext::SetGizmoOperation(MetaCoreGizmoOperation operation) { GizmoOperation_ = operation; }
MetaCoreGizmoMode MetaCoreEditorContext::GetGizmoMode() const { return GizmoMode_; }
void MetaCoreEditorContext::SetGizmoMode(MetaCoreGizmoMode mode) { GizmoMode_ = mode; }
MetaCoreGizmoPivotMode MetaCoreEditorContext::GetGizmoPivotMode() const { return GizmoPivotMode_; }
void MetaCoreEditorContext::SetGizmoPivotMode(MetaCoreGizmoPivotMode mode) { GizmoPivotMode_ = mode; }
const MetaCoreGizmoSnapSettings& MetaCoreEditorContext::GetGizmoSnapSettings() const { return GizmoSnapSettings_; }
void MetaCoreEditorContext::SetGizmoSnapSettings(const MetaCoreGizmoSnapSettings& settings) { GizmoSnapSettings_ = settings; }
void MetaCoreEditorContext::SetGizmoSnapSettings(const MetaCoreGizmoSnapSettings& settings) {
GizmoSnapSettings_ = settings;
const auto sanitizeStep = [](float value, float fallback, float minimum, float maximum) {
return std::clamp(std::isfinite(value) ? value : fallback, minimum, maximum);
};
GizmoSnapSettings_.TranslationStep = sanitizeStep(settings.TranslationStep, 0.5F, 0.001F, 10000.0F);
GizmoSnapSettings_.RotationStepDegrees = sanitizeStep(settings.RotationStepDegrees, 15.0F, 0.1F, 360.0F);
GizmoSnapSettings_.ScaleStep = sanitizeStep(settings.ScaleStep, 0.1F, 0.001F, 10.0F);
}
void MetaCoreEditorContext::SetGizmoSnapEnabled(bool enabled) { GizmoSnapSettings_.Enabled = enabled; }
bool MetaCoreEditorContext::GetShowViewportGrid() const { return ShowViewportGrid_; }
void MetaCoreEditorContext::SetShowViewportGrid(bool show) { ShowViewportGrid_ = show; }
bool MetaCoreEditorContext::GetShowViewportSelectionOverlay() const { return ShowViewportSelectionOverlay_; }
void MetaCoreEditorContext::SetShowViewportSelectionOverlay(bool show) { ShowViewportSelectionOverlay_ = show; }
bool MetaCoreEditorContext::GetShowWorldOrigin() const { return ShowWorldOrigin_; }
void MetaCoreEditorContext::SetShowWorldOrigin(bool show) { ShowWorldOrigin_ = show; }
void MetaCoreEditorContext::ClearComponentGizmoDrawList() {
ComponentGizmoLines_.clear();
ComponentGizmoIcons_.clear();
}
void MetaCoreEditorContext::DrawGizmoLine(
const glm::vec3& start,
const glm::vec3& end,
const glm::vec4& color,
float thickness
) {
ComponentGizmoLines_.push_back(MetaCoreComponentGizmoLine{
start,
end,
color,
std::clamp(thickness, 0.5F, 16.0F)
});
}
void MetaCoreEditorContext::DrawGizmoWireBox(
const glm::vec3& center,
const glm::vec3& size,
const glm::vec4& color,
float thickness
) {
const glm::vec3 halfSize = glm::abs(size) * 0.5F;
const glm::vec3 corners[8] = {
center + glm::vec3(-halfSize.x, -halfSize.y, -halfSize.z),
center + glm::vec3( halfSize.x, -halfSize.y, -halfSize.z),
center + glm::vec3(-halfSize.x, halfSize.y, -halfSize.z),
center + glm::vec3( halfSize.x, halfSize.y, -halfSize.z),
center + glm::vec3(-halfSize.x, -halfSize.y, halfSize.z),
center + glm::vec3( halfSize.x, -halfSize.y, halfSize.z),
center + glm::vec3(-halfSize.x, halfSize.y, halfSize.z),
center + glm::vec3( halfSize.x, halfSize.y, halfSize.z)
};
constexpr int edges[12][2] = {
{0, 1}, {1, 3}, {3, 2}, {2, 0},
{4, 5}, {5, 7}, {7, 6}, {6, 4},
{0, 4}, {1, 5}, {2, 6}, {3, 7}
};
for (const auto& edge : edges) {
DrawGizmoLine(corners[edge[0]], corners[edge[1]], color, thickness);
}
}
void MetaCoreEditorContext::DrawGizmoIcon(
const glm::vec3& worldPosition,
std::string label,
const glm::vec4& color,
float size
) {
ComponentGizmoIcons_.push_back(MetaCoreComponentGizmoIcon{
worldPosition,
color,
std::move(label),
std::clamp(size, 3.0F, 32.0F)
});
}
const std::vector<MetaCoreComponentGizmoLine>& MetaCoreEditorContext::GetComponentGizmoLines() const {
return ComponentGizmoLines_;
}
const std::vector<MetaCoreComponentGizmoIcon>& MetaCoreEditorContext::GetComponentGizmoIcons() const {
return ComponentGizmoIcons_;
}
MetaCoreReparentTransformRule MetaCoreEditorContext::GetReparentTransformRule() const { return ReparentTransformRule_; }
void MetaCoreEditorContext::SetReparentTransformRule(MetaCoreReparentTransformRule rule) { ReparentTransformRule_ = rule; }
MetaCoreEditorCommandService& MetaCoreEditorContext::GetCommandService() { return CommandService_; }
const MetaCoreEditorCommandService& MetaCoreEditorContext::GetCommandService() const { return CommandService_; }
bool MetaCoreEditorContext::IsRuntimeSceneActive() const { return RuntimeSceneActive_; }
void MetaCoreEditorContext::SetRuntimeSceneActive(bool active) { RuntimeSceneActive_ = active; }
bool MetaCoreEditorContext::ExecuteCommand(std::unique_ptr<MetaCoreIEditorCommand> command) {
if (RuntimeSceneActive_) {
return command != nullptr && command->Execute(*this);
}
if (!CommandService_.Execute(std::move(command), *this)) {
return false;
}
@ -610,6 +704,10 @@ bool MetaCoreEditorContext::ExecuteCommand(std::unique_ptr<MetaCoreIEditorComman
}
bool MetaCoreEditorContext::UndoCommand() {
if (RuntimeSceneActive_) {
return false;
}
if (!CommandService_.Undo(*this)) {
return false;
}
@ -621,6 +719,10 @@ bool MetaCoreEditorContext::UndoCommand() {
}
bool MetaCoreEditorContext::RedoCommand() {
if (RuntimeSceneActive_) {
return false;
}
if (!CommandService_.Redo(*this)) {
return false;
}
@ -659,6 +761,11 @@ bool MetaCoreEditorContext::CommitStateTransition(
const MetaCoreEditorStateSnapshot& afterSnapshot,
bool allowMerge
) {
if (RuntimeSceneActive_) {
NormalizeSelection();
return !MetaCoreStateSnapshotEqual(beforeSnapshot, afterSnapshot);
}
const auto beforeBytes = MetaCoreTrySerializeStateSnapshot(ModuleRegistry_, beforeSnapshot);
const auto afterBytes = MetaCoreTrySerializeStateSnapshot(ModuleRegistry_, afterSnapshot);
if (beforeBytes.has_value() && afterBytes.has_value()) {
@ -678,6 +785,15 @@ bool MetaCoreEditorContext::CommitStateTransition(
}
bool MetaCoreEditorContext::ExecuteSnapshotCommand(const std::string& label, const std::function<bool()>& mutator) {
if (RuntimeSceneActive_) {
(void)label;
if (!mutator()) {
return false;
}
NormalizeSelection();
return true;
}
const MetaCoreEditorStateSnapshot beforeSnapshot = CaptureStateSnapshot();
if (!mutator()) {
return false;
@ -1095,6 +1211,9 @@ void MetaCoreEditorContext::NormalizeSelection() {
if (SelectionAnchorId_ == 0 || !deduplicatedIds.contains(SelectionAnchorId_)) {
SelectionAnchorId_ = ActiveObjectId_;
}
if (HoveredObjectId_ != 0 && !Scene_.FindGameObject(HoveredObjectId_)) {
HoveredObjectId_ = 0;
}
}
void MetaCoreEditorContext::NotifySelectionChanged() {

View File

@ -101,6 +101,42 @@ std::vector<MetaCoreId> MetaCoreBuildSelectionTransformRoots(
return rootIds;
}
glm::mat4 MetaCoreBuildSelectionGizmoWorldMatrix(
const MetaCoreEditorContext& editorContext,
MetaCoreId activeObjectId
) {
glm::mat4 gizmoWorldMatrix = MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), activeObjectId);
if (editorContext.GetGizmoPivotMode() != MetaCoreGizmoPivotMode::Center) {
return gizmoWorldMatrix;
}
const std::vector<MetaCoreId> rootIds = MetaCoreBuildSelectionTransformRoots(
editorContext.GetScene(),
editorContext.GetSelectedObjectIds(),
activeObjectId
);
if (rootIds.size() < 2) {
return gizmoWorldMatrix;
}
glm::vec3 center(0.0F);
std::size_t validRootCount = 0;
for (const MetaCoreId rootId : rootIds) {
const MetaCoreGameObject rootObject = editorContext.GetScene().FindGameObject(rootId);
if (!rootObject) {
continue;
}
center += glm::vec3(MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), rootId)[3]);
++validRootCount;
}
if (validRootCount == 0) {
return gizmoWorldMatrix;
}
gizmoWorldMatrix[3] = glm::vec4(center / static_cast<float>(validRootCount), 1.0F);
return gizmoWorldMatrix;
}
ImGuizmo::OPERATION MetaCoreToImGuizmoOperation(MetaCoreGizmoOperation operation) {
switch (operation) {
case MetaCoreGizmoOperation::Translate: return ImGuizmo::TRANSLATE;
@ -128,6 +164,40 @@ const char* MetaCoreGizmoModeLabel(MetaCoreGizmoMode mode) {
}
}
void MetaCoreExpandFocusBounds(
const MetaCoreScene& scene,
MetaCoreId objectId,
bool& initialized,
glm::vec3& boundsMin,
glm::vec3& boundsMax
) {
const MetaCoreGameObject object = scene.FindGameObject(objectId);
if (!object) {
return;
}
const glm::mat4 worldMatrix = MetaCoreBuildWorldTransformMatrix(scene, objectId);
const glm::vec3 worldPosition(worldMatrix[3]);
const float extent = std::max({
glm::length(glm::vec3(worldMatrix[0])),
glm::length(glm::vec3(worldMatrix[1])),
glm::length(glm::vec3(worldMatrix[2])),
0.5F
});
const glm::vec3 objectMin = worldPosition - glm::vec3(extent);
const glm::vec3 objectMax = worldPosition + glm::vec3(extent);
if (!initialized) {
boundsMin = objectMin;
boundsMax = objectMax;
initialized = true;
return;
}
boundsMin = glm::min(boundsMin, objectMin);
boundsMax = glm::max(boundsMax, objectMax);
}
} // namespace
void MetaCoreSceneInteractionService::ResetFrameState() {
@ -265,6 +335,26 @@ MetaCoreId MetaCoreSceneInteractionService::PickGameObjectFromViewport(
return bestObjectId;
}
void MetaCoreSceneInteractionService::UpdateViewportHover(
MetaCoreEditorContext& editorContext,
const MetaCoreSceneView& sceneView,
bool allowPicking
) const {
const MetaCoreSceneViewportState& viewportState = editorContext.GetSceneViewportState();
if (!allowPicking || !viewportState.Hovered) {
editorContext.ClearHoveredObject();
return;
}
const MetaCoreId hoveredObjectId = PickGameObjectFromViewport(
editorContext.GetScene(),
sceneView,
viewportState,
editorContext.GetInput().GetCursorPosition()
);
editorContext.SetHoveredObjectId(hoveredObjectId);
}
void MetaCoreSceneInteractionService::ApplyViewportSelection(MetaCoreEditorContext& editorContext, MetaCoreId pickedObjectId) const {
if (const auto selectionService = editorContext.GetModuleRegistry().ResolveService<MetaCoreISelectionService>();
selectionService != nullptr) {
@ -293,7 +383,7 @@ void MetaCoreSceneInteractionService::HandleGizmoEndUse(MetaCoreEditorContext& e
if (!gizmoUsing && GizmoWasUsing_) {
if (HasGizmoBeforeSnapshot_) {
const MetaCoreEditorStateSnapshot afterSnapshot = editorContext.CaptureStateSnapshot();
editorContext.CommitStateTransition("Gizmo Transform", GizmoBeforeSnapshot_, afterSnapshot, true);
(void)editorContext.CommitStateTransition("Gizmo Transform", GizmoBeforeSnapshot_, afterSnapshot, false);
}
GizmoWasUsing_ = false;
HasGizmoBeforeSnapshot_ = false;
@ -307,41 +397,11 @@ void MetaCoreSceneInteractionService::FocusSelectedObject(MetaCoreEditorContext&
return;
}
if (selectedObjectIds.size() == 1) {
MetaCoreGameObject selectedObject = editorContext.GetSelectedGameObject();
if (selectedObject) {
editorContext.GetCameraController().FocusGameObject(selectedObject);
}
return;
}
bool initialized = false;
glm::vec3 boundsMin(0.0F);
glm::vec3 boundsMax(0.0F);
for (const MetaCoreId selectedObjectId : selectedObjectIds) {
const MetaCoreGameObject selectedObject = editorContext.GetScene().FindGameObject(selectedObjectId);
if (!selectedObject) {
continue;
}
const float extent = std::max({
std::abs(selectedObject.GetComponent<MetaCoreTransformComponent>().Scale.x),
std::abs(selectedObject.GetComponent<MetaCoreTransformComponent>().Scale.y),
std::abs(selectedObject.GetComponent<MetaCoreTransformComponent>().Scale.z),
0.5F
});
const glm::vec3 objectMin = selectedObject.GetComponent<MetaCoreTransformComponent>().Position - glm::vec3(extent);
const glm::vec3 objectMax = selectedObject.GetComponent<MetaCoreTransformComponent>().Position + glm::vec3(extent);
if (!initialized) {
boundsMin = objectMin;
boundsMax = objectMax;
initialized = true;
continue;
}
boundsMin = glm::min(boundsMin, objectMin);
boundsMax = glm::max(boundsMax, objectMax);
MetaCoreExpandFocusBounds(editorContext.GetScene(), selectedObjectId, initialized, boundsMin, boundsMax);
}
if (!initialized) {
@ -365,24 +425,7 @@ void MetaCoreSceneInteractionService::FocusVisibleScene(MetaCoreEditorContext& e
continue;
}
const float extent = std::max({
std::abs(gameObject.GetComponent<MetaCoreTransformComponent>().Scale.x),
std::abs(gameObject.GetComponent<MetaCoreTransformComponent>().Scale.y),
std::abs(gameObject.GetComponent<MetaCoreTransformComponent>().Scale.z),
0.5F
});
const glm::vec3 objectMin = gameObject.GetComponent<MetaCoreTransformComponent>().Position - glm::vec3(extent);
const glm::vec3 objectMax = gameObject.GetComponent<MetaCoreTransformComponent>().Position + glm::vec3(extent);
if (!initialized) {
boundsMin = objectMin;
boundsMax = objectMax;
initialized = true;
continue;
}
boundsMin = glm::min(boundsMin, objectMin);
boundsMax = glm::max(boundsMax, objectMax);
MetaCoreExpandFocusBounds(editorContext.GetScene(), gameObject.GetId(), initialized, boundsMin, boundsMax);
}
if (!initialized) {
@ -400,7 +443,8 @@ bool MetaCoreSceneInteractionService::ApplyWorldTransformDeltaToSelection(
MetaCoreEditorContext& editorContext,
MetaCoreId manipulatedObjectId,
const glm::mat4& originalWorldMatrix,
const glm::mat4& newWorldMatrix
const glm::mat4& newWorldMatrix,
MetaCoreGizmoPivotMode pivotMode
) const {
if (manipulatedObjectId == 0 || !editorContext.GetScene().FindGameObject(manipulatedObjectId)) {
return false;
@ -429,7 +473,9 @@ bool MetaCoreSceneInteractionService::ApplyWorldTransformDeltaToSelection(
const glm::mat4 currentWorldMatrix = MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), objectId);
const glm::mat4 targetWorldMatrix =
objectId == manipulatedObjectId ? newWorldMatrix : deltaMatrix * currentWorldMatrix;
pivotMode == MetaCoreGizmoPivotMode::Pivot && objectId == manipulatedObjectId
? newWorldMatrix
: deltaMatrix * currentWorldMatrix;
glm::mat4 targetLocalMatrix = targetWorldMatrix;
if (object.GetParentId() != 0) {
@ -468,7 +514,7 @@ void MetaCoreSceneInteractionService::HandleGizmoManipulation(MetaCoreEditorCont
gizmoAspect, 0.1F, 1000.0F
);
glm::mat4 worldMatrixMetaCore = MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), selectedObject.GetId());
glm::mat4 worldMatrixMetaCore = MetaCoreBuildSelectionGizmoWorldMatrix(editorContext, selectedObject.GetId());
const glm::mat4 originalWorldMatrixMetaCore = worldMatrixMetaCore;
ImGuizmo::SetOrthographic(false);
@ -514,7 +560,8 @@ void MetaCoreSceneInteractionService::HandleGizmoManipulation(MetaCoreEditorCont
editorContext,
selectedObject.GetId(),
originalWorldMatrixMetaCore,
gizmoMatrix
gizmoMatrix,
editorContext.GetGizmoPivotMode()
);
}
HandleGizmoEndUse(editorContext, gizmoUsing);
@ -609,6 +656,15 @@ void MetaCoreSceneInteractionService::DrawViewportToolbar(MetaCoreEditorContext&
editorContext.SetGizmoMode(MetaCoreGizmoMode::Global);
}
ImGui::SameLine(0, 12);
if (drawToolbarToggle(" Pivot ", editorContext.GetGizmoPivotMode() == MetaCoreGizmoPivotMode::Pivot)) {
editorContext.SetGizmoPivotMode(MetaCoreGizmoPivotMode::Pivot);
}
ImGui::SameLine(0, 2);
if (drawToolbarToggle(" Center ", editorContext.GetGizmoPivotMode() == MetaCoreGizmoPivotMode::Center)) {
editorContext.SetGizmoPivotMode(MetaCoreGizmoPivotMode::Center);
}
ImGui::SameLine(0, 12);
const MetaCoreGizmoSnapSettings& snapSettings = editorContext.GetGizmoSnapSettings();
if (drawToolbarToggle(" Snap ", snapSettings.Enabled)) {
@ -637,6 +693,11 @@ void MetaCoreSceneInteractionService::DrawViewportToolbar(MetaCoreEditorContext&
editorContext.SetShowViewportGrid(!showGrid);
}
ImGui::SameLine();
bool showSelectionOverlay = editorContext.GetShowViewportSelectionOverlay();
if (drawToolbarToggle(" Outline ", showSelectionOverlay)) {
editorContext.SetShowViewportSelectionOverlay(!showSelectionOverlay);
}
ImGui::SameLine();
bool showOrigin = editorContext.GetShowWorldOrigin();
if (drawToolbarToggle(" 原点 ", showOrigin)) {
editorContext.SetShowWorldOrigin(!showOrigin);
@ -669,6 +730,13 @@ void MetaCoreSceneInteractionService::DrawViewportToolbar(MetaCoreEditorContext&
activeObject ? activeObject.GetName().c_str() : "<缺失>"
);
}
ImGui::SameLine(0, 12);
const MetaCoreGameObject hoveredObject =
editorContext.GetScene().FindGameObject(editorContext.GetHoveredObjectId());
ImGui::TextDisabled(
"Hover: %s",
hoveredObject ? hoveredObject.GetName().c_str() : "None"
);
}
ImGui::End();
ImGui::PopStyleColor();

View File

@ -11,6 +11,8 @@
#include <functional>
#include <filesystem>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#include <memory>
#include <optional>
#include <string>
@ -38,6 +40,11 @@ enum class MetaCoreGizmoMode {
Global
};
enum class MetaCoreGizmoPivotMode {
Pivot = 0,
Center
};
enum class MetaCoreReparentTransformRule {
KeepWorld = 0,
KeepLocal
@ -82,6 +89,20 @@ struct MetaCoreSceneViewportState {
bool Focused = false;
};
struct MetaCoreComponentGizmoLine {
glm::vec3 Start{0.0F};
glm::vec3 End{0.0F};
glm::vec4 Color{1.0F};
float Thickness = 1.5F;
};
struct MetaCoreComponentGizmoIcon {
glm::vec3 WorldPosition{0.0F};
glm::vec4 Color{1.0F};
std::string Label{};
float Size = 7.0F;
};
struct MetaCoreSelectedAssetState {
MetaCoreAssetGuid Guid{};
std::filesystem::path RelativePath{};
@ -144,6 +165,10 @@ public:
[[nodiscard]] const std::vector<MetaCoreId>& GetSelectedObjectIds() const;
[[nodiscard]] MetaCoreId GetActiveObjectId() const;
[[nodiscard]] bool IsObjectSelected(MetaCoreId objectId) const;
[[nodiscard]] MetaCoreId GetHoveredObjectId() const;
[[nodiscard]] bool IsObjectHovered(MetaCoreId objectId) const;
void SetHoveredObjectId(MetaCoreId objectId);
void ClearHoveredObject();
void ClearSelection();
void SelectOnly(MetaCoreId objectId);
void ToggleSelection(MetaCoreId objectId);
@ -160,17 +185,44 @@ public:
void SetGizmoOperation(MetaCoreGizmoOperation operation);
[[nodiscard]] MetaCoreGizmoMode GetGizmoMode() const;
void SetGizmoMode(MetaCoreGizmoMode mode);
[[nodiscard]] MetaCoreGizmoPivotMode GetGizmoPivotMode() const;
void SetGizmoPivotMode(MetaCoreGizmoPivotMode mode);
[[nodiscard]] const MetaCoreGizmoSnapSettings& GetGizmoSnapSettings() const;
void SetGizmoSnapSettings(const MetaCoreGizmoSnapSettings& settings);
void SetGizmoSnapEnabled(bool enabled);
[[nodiscard]] bool GetShowViewportGrid() const;
void SetShowViewportGrid(bool show);
[[nodiscard]] bool GetShowViewportSelectionOverlay() const;
void SetShowViewportSelectionOverlay(bool show);
[[nodiscard]] bool GetShowWorldOrigin() const;
void SetShowWorldOrigin(bool show);
void ClearComponentGizmoDrawList();
void DrawGizmoLine(
const glm::vec3& start,
const glm::vec3& end,
const glm::vec4& color = glm::vec4(1.0F),
float thickness = 1.5F
);
void DrawGizmoWireBox(
const glm::vec3& center,
const glm::vec3& size,
const glm::vec4& color = glm::vec4(1.0F),
float thickness = 1.5F
);
void DrawGizmoIcon(
const glm::vec3& worldPosition,
std::string label,
const glm::vec4& color = glm::vec4(1.0F),
float size = 7.0F
);
[[nodiscard]] const std::vector<MetaCoreComponentGizmoLine>& GetComponentGizmoLines() const;
[[nodiscard]] const std::vector<MetaCoreComponentGizmoIcon>& GetComponentGizmoIcons() const;
[[nodiscard]] MetaCoreReparentTransformRule GetReparentTransformRule() const;
void SetReparentTransformRule(MetaCoreReparentTransformRule rule);
[[nodiscard]] MetaCoreEditorCommandService& GetCommandService();
[[nodiscard]] const MetaCoreEditorCommandService& GetCommandService() const;
[[nodiscard]] bool IsRuntimeSceneActive() const;
void SetRuntimeSceneActive(bool active);
bool ExecuteCommand(std::unique_ptr<MetaCoreIEditorCommand> command);
bool UndoCommand();
bool RedoCommand();
@ -225,13 +277,19 @@ private:
std::vector<MetaCoreId> SelectedObjectIds_{};
MetaCoreId ActiveObjectId_ = 0;
MetaCoreId SelectionAnchorId_ = 0;
MetaCoreId HoveredObjectId_ = 0;
MetaCoreGizmoOperation GizmoOperation_ = MetaCoreGizmoOperation::Translate;
MetaCoreGizmoMode GizmoMode_ = MetaCoreGizmoMode::Local;
MetaCoreGizmoPivotMode GizmoPivotMode_ = MetaCoreGizmoPivotMode::Pivot;
MetaCoreGizmoSnapSettings GizmoSnapSettings_{};
bool ShowViewportGrid_ = true;
bool ShowViewportSelectionOverlay_ = true;
bool ShowWorldOrigin_ = true;
std::vector<MetaCoreComponentGizmoLine> ComponentGizmoLines_{};
std::vector<MetaCoreComponentGizmoIcon> ComponentGizmoIcons_{};
MetaCoreReparentTransformRule ReparentTransformRule_ = MetaCoreReparentTransformRule::KeepWorld;
MetaCoreEditorCommandService CommandService_{};
bool RuntimeSceneActive_ = false;
MetaCoreId PendingRenameObjectId_ = 0;
bool DockLayoutBuilt_ = false;
bool RuntimeDataConfigLoaded_ = false;

View File

@ -14,6 +14,7 @@
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace MetaCore {
@ -147,6 +148,13 @@ public:
[[nodiscard]] virtual bool ReimportAsset(const MetaCoreAssetGuid& assetGuid) = 0;
virtual bool Refresh() = 0;
virtual bool CreateFolder(const std::filesystem::path& relativeDirectory) = 0;
virtual std::optional<MetaCoreAssetRecord> CreateSceneAsset(
const std::filesystem::path& relativeScenePath,
bool makeStartupScene
) = 0;
virtual std::optional<MetaCoreAssetRecord> CreatePrefabAsset(const std::filesystem::path& relativePrefabPath) = 0;
virtual std::optional<MetaCoreAssetRecord> CreateMaterialAsset(const std::filesystem::path& relativeMaterialPath) = 0;
virtual std::optional<MetaCoreAssetRecord> CreateUiDocumentAsset(const std::filesystem::path& relativeUiPath) = 0;
virtual bool RegisterScenePath(const std::filesystem::path& relativeScenePath, bool makeStartupScene) = 0;
virtual bool SetStartupScenePath(const std::filesystem::path& relativeScenePath) = 0;
};
@ -363,6 +371,7 @@ enum class MetaCorePlayModeState {
class MetaCoreIPlayModeService : public MetaCoreIEditorService {
public:
[[nodiscard]] virtual MetaCorePlayModeState GetState() const = 0;
[[nodiscard]] virtual bool IsRuntimeSceneActive() const = 0;
[[nodiscard]] virtual bool CanEnterPlayMode() const = 0;
[[nodiscard]] virtual bool EnterPlayMode(MetaCoreEditorContext& editorContext) = 0;
[[nodiscard]] virtual bool ExitPlayMode(MetaCoreEditorContext& editorContext) = 0;
@ -370,6 +379,15 @@ public:
[[nodiscard]] virtual bool ResumePlayMode(MetaCoreEditorContext& editorContext) = 0;
[[nodiscard]] virtual bool StepPlayMode(MetaCoreEditorContext& editorContext, float deltaSeconds) = 0;
[[nodiscard]] virtual float GetElapsedPlayTimeSeconds() const = 0;
virtual void NotifyGameObjectsWillBeDestroyed(
MetaCoreEditorContext& editorContext,
const std::vector<MetaCoreId>& objectIds
) = 0;
virtual void NotifyComponentWillBeRemoved(
MetaCoreEditorContext& editorContext,
MetaCoreId objectId,
std::string_view componentTypeId
) = 0;
virtual void TickPlayMode(MetaCoreEditorContext& editorContext, float deltaSeconds) = 0;
};

View File

@ -21,6 +21,11 @@ public:
const glm::vec2& cursorPosition
) const;
void UpdateViewportHover(
MetaCoreEditorContext& editorContext,
const MetaCoreSceneView& sceneView,
bool allowPicking
) const;
void ApplyViewportSelection(MetaCoreEditorContext& editorContext, MetaCoreId pickedObjectId) const;
void HandleGizmoBeginUse(MetaCoreEditorContext& editorContext, bool gizmoUsing);
void HandleGizmoEndUse(MetaCoreEditorContext& editorContext, bool gizmoUsing);
@ -30,7 +35,8 @@ public:
MetaCoreEditorContext& editorContext,
MetaCoreId manipulatedObjectId,
const glm::mat4& originalWorldMatrix,
const glm::mat4& newWorldMatrix
const glm::mat4& newWorldMatrix,
MetaCoreGizmoPivotMode pivotMode = MetaCoreGizmoPivotMode::Pivot
) const;
void HandleGizmoManipulation(MetaCoreEditorContext& editorContext);
void DrawViewportToolbar(MetaCoreEditorContext& editorContext);

View File

@ -0,0 +1,326 @@
#include "MetaCoreScene/MetaCoreRuntimeLifecycle.h"
#include <algorithm>
#include <exception>
#include <unordered_set>
namespace MetaCore {
void MetaCoreRuntimeLifecycleExecutor::ClearDescriptors() {
Descriptors_.clear();
StartedComponents_.clear();
Running_ = false;
ElapsedTimeSeconds_ = 0.0F;
}
bool MetaCoreRuntimeLifecycleExecutor::RegisterDescriptor(MetaCoreRuntimeLifecycleDescriptor descriptor) {
if (descriptor.TypeId.empty() || !descriptor.HasComponent) {
return false;
}
const auto iterator = std::find_if(
Descriptors_.begin(),
Descriptors_.end(),
[&](const MetaCoreRuntimeLifecycleDescriptor& registeredDescriptor) {
return registeredDescriptor.TypeId == descriptor.TypeId;
}
);
if (iterator != Descriptors_.end()) {
return false;
}
Descriptors_.push_back(std::move(descriptor));
return true;
}
std::size_t MetaCoreRuntimeLifecycleExecutor::RegisterDescriptors(
const std::vector<MetaCoreRuntimeLifecycleDescriptor>& descriptors
) {
std::size_t registeredCount = 0;
for (const MetaCoreRuntimeLifecycleDescriptor& descriptor : descriptors) {
if (RegisterDescriptor(descriptor)) {
++registeredCount;
}
}
return registeredCount;
}
const std::vector<MetaCoreRuntimeLifecycleDescriptor>& MetaCoreRuntimeLifecycleExecutor::GetDescriptors() const {
return Descriptors_;
}
void MetaCoreRuntimeLifecycleExecutor::EnterScene(MetaCoreScene& scene, ErrorSink errorSink) {
StartedComponents_.clear();
ElapsedTimeSeconds_ = 0.0F;
Running_ = true;
InvokeStartForNewComponents(scene, errorSink);
}
void MetaCoreRuntimeLifecycleExecutor::TickScene(MetaCoreScene& scene, float deltaSeconds, ErrorSink errorSink) {
if (!Running_) {
return;
}
const float sanitizedDeltaSeconds = std::max(deltaSeconds, 0.0F);
InvokeStartForNewComponents(scene, errorSink);
const std::vector<MetaCoreId> objectIds = scene.BuildHierarchyPreorder();
for (const MetaCoreRuntimeLifecycleDescriptor& descriptor : Descriptors_) {
if (!descriptor.OnUpdate) {
continue;
}
for (const MetaCoreId objectId : objectIds) {
MetaCoreGameObject gameObject = scene.FindGameObject(objectId);
if (!gameObject || !descriptor.HasComponent(gameObject)) {
continue;
}
const std::string key = BuildLifecycleKey(objectId, descriptor.TypeId);
if (!StartedComponents_.contains(key)) {
continue;
}
try {
descriptor.OnUpdate(scene, gameObject, sanitizedDeltaSeconds);
} catch (const std::exception& exception) {
ReportException(errorSink, "OnUpdate", descriptor, objectId, exception);
} catch (...) {
ReportUnknownException(errorSink, "OnUpdate", descriptor, objectId);
}
}
}
ElapsedTimeSeconds_ += sanitizedDeltaSeconds;
}
void MetaCoreRuntimeLifecycleExecutor::NotifyGameObjectsWillBeDestroyed(
MetaCoreScene& scene,
const std::vector<MetaCoreId>& objectIds,
ErrorSink errorSink
) {
if (!Running_ || objectIds.empty()) {
return;
}
std::vector<MetaCoreId> affectedObjectIds;
std::unordered_set<MetaCoreId> deduplicatedObjectIds;
for (const MetaCoreId objectId : objectIds) {
if (objectId == 0 || deduplicatedObjectIds.contains(objectId)) {
continue;
}
const std::vector<MetaCoreId> subtreeObjectIds = scene.GetSubtreeObjectIds(objectId);
if (subtreeObjectIds.empty()) {
continue;
}
for (const MetaCoreId subtreeObjectId : subtreeObjectIds) {
if (deduplicatedObjectIds.insert(subtreeObjectId).second) {
affectedObjectIds.push_back(subtreeObjectId);
}
}
}
for (const MetaCoreId objectId : affectedObjectIds) {
for (const MetaCoreRuntimeLifecycleDescriptor& descriptor : Descriptors_) {
(void)InvokeDestroyForStartedComponent(scene, descriptor, objectId, errorSink);
}
}
}
void MetaCoreRuntimeLifecycleExecutor::NotifyComponentWillBeRemoved(
MetaCoreScene& scene,
MetaCoreId objectId,
std::string_view componentTypeId,
ErrorSink errorSink
) {
if (!Running_ || objectId == 0 || componentTypeId.empty()) {
return;
}
const auto iterator = std::find_if(
Descriptors_.begin(),
Descriptors_.end(),
[&](const MetaCoreRuntimeLifecycleDescriptor& descriptor) {
return descriptor.TypeId == componentTypeId;
}
);
if (iterator == Descriptors_.end()) {
return;
}
(void)InvokeDestroyForStartedComponent(scene, *iterator, objectId, errorSink);
}
void MetaCoreRuntimeLifecycleExecutor::ExitScene(MetaCoreScene& scene, ErrorSink errorSink) {
if (!Running_) {
return;
}
const std::vector<MetaCoreId> objectIds = scene.BuildHierarchyPreorder();
for (const MetaCoreRuntimeLifecycleDescriptor& descriptor : Descriptors_) {
for (const MetaCoreId objectId : objectIds) {
(void)InvokeDestroyForStartedComponent(scene, descriptor, objectId, errorSink);
}
}
StartedComponents_.clear();
Running_ = false;
ElapsedTimeSeconds_ = 0.0F;
}
bool MetaCoreRuntimeLifecycleExecutor::IsRunning() const {
return Running_;
}
float MetaCoreRuntimeLifecycleExecutor::GetElapsedTimeSeconds() const {
return ElapsedTimeSeconds_;
}
std::string MetaCoreRuntimeLifecycleExecutor::BuildLifecycleKey(
MetaCoreId objectId,
std::string_view componentTypeId
) const {
return std::to_string(objectId) + ":" + std::string(componentTypeId);
}
void MetaCoreRuntimeLifecycleExecutor::InvokeStartForNewComponents(
MetaCoreScene& scene,
const ErrorSink& errorSink
) {
const std::vector<MetaCoreId> objectIds = scene.BuildHierarchyPreorder();
for (const MetaCoreRuntimeLifecycleDescriptor& descriptor : Descriptors_) {
for (const MetaCoreId objectId : objectIds) {
MetaCoreGameObject gameObject = scene.FindGameObject(objectId);
if (!gameObject || !descriptor.HasComponent(gameObject)) {
continue;
}
const std::string key = BuildLifecycleKey(objectId, descriptor.TypeId);
if (StartedComponents_.contains(key)) {
continue;
}
StartedComponents_.insert(key);
if (!descriptor.OnStart) {
continue;
}
try {
descriptor.OnStart(scene, gameObject);
} catch (const std::exception& exception) {
ReportException(errorSink, "OnStart", descriptor, objectId, exception);
} catch (...) {
ReportUnknownException(errorSink, "OnStart", descriptor, objectId);
}
}
}
}
bool MetaCoreRuntimeLifecycleExecutor::InvokeDestroyForStartedComponent(
MetaCoreScene& scene,
const MetaCoreRuntimeLifecycleDescriptor& descriptor,
MetaCoreId objectId,
const ErrorSink& errorSink
) {
if (descriptor.TypeId.empty() || objectId == 0) {
return false;
}
const std::string key = BuildLifecycleKey(objectId, descriptor.TypeId);
if (!StartedComponents_.contains(key)) {
return false;
}
MetaCoreGameObject gameObject = scene.FindGameObject(objectId);
if (!gameObject || !descriptor.HasComponent(gameObject)) {
StartedComponents_.erase(key);
return false;
}
if (descriptor.OnDestroy) {
try {
descriptor.OnDestroy(scene, gameObject);
} catch (const std::exception& exception) {
ReportException(errorSink, "OnDestroy", descriptor, objectId, exception);
} catch (...) {
ReportUnknownException(errorSink, "OnDestroy", descriptor, objectId);
}
}
StartedComponents_.erase(key);
return true;
}
void MetaCoreRuntimeLifecycleExecutor::ReportException(
const ErrorSink& errorSink,
std::string_view phase,
const MetaCoreRuntimeLifecycleDescriptor& descriptor,
MetaCoreId objectId,
const std::exception& exception
) const {
if (!errorSink) {
return;
}
errorSink(MetaCoreRuntimeLifecycleError{
std::string(phase),
descriptor.TypeId,
objectId,
exception.what()
});
}
void MetaCoreRuntimeLifecycleExecutor::ReportUnknownException(
const ErrorSink& errorSink,
std::string_view phase,
const MetaCoreRuntimeLifecycleDescriptor& descriptor,
MetaCoreId objectId
) const {
if (!errorSink) {
return;
}
errorSink(MetaCoreRuntimeLifecycleError{
std::string(phase),
descriptor.TypeId,
objectId,
"unknown exception"
});
}
void MetaCoreRuntimeLifecycleRegistry::Clear() {
Descriptors_.clear();
}
bool MetaCoreRuntimeLifecycleRegistry::RegisterDescriptor(MetaCoreRuntimeLifecycleDescriptor descriptor) {
if (descriptor.TypeId.empty() || !descriptor.HasComponent) {
return false;
}
const auto iterator = std::find_if(
Descriptors_.begin(),
Descriptors_.end(),
[&](const MetaCoreRuntimeLifecycleDescriptor& registeredDescriptor) {
return registeredDescriptor.TypeId == descriptor.TypeId;
}
);
if (iterator != Descriptors_.end()) {
return false;
}
Descriptors_.push_back(std::move(descriptor));
return true;
}
const std::vector<MetaCoreRuntimeLifecycleDescriptor>& MetaCoreRuntimeLifecycleRegistry::GetDescriptors() const {
return Descriptors_;
}
MetaCoreRuntimeLifecycleRegistry& MetaCoreGetRuntimeLifecycleRegistry() {
static MetaCoreRuntimeLifecycleRegistry registry;
return registry;
}
} // namespace MetaCore

View File

@ -0,0 +1,99 @@
#pragma once
#include "MetaCoreFoundation/MetaCoreId.h"
#include "MetaCoreScene/MetaCoreGameObject.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include <exception>
#include <functional>
#include <string>
#include <string_view>
#include <unordered_set>
#include <vector>
namespace MetaCore {
struct MetaCoreRuntimeLifecycleError {
std::string Phase{};
std::string ComponentTypeId{};
MetaCoreId ObjectId = 0;
std::string Message{};
};
struct MetaCoreRuntimeLifecycleDescriptor {
std::string TypeId{};
std::function<bool(const MetaCoreGameObject&)> HasComponent{};
std::function<void(MetaCoreScene&, MetaCoreGameObject&)> OnStart{};
std::function<void(MetaCoreScene&, MetaCoreGameObject&, float)> OnUpdate{};
std::function<void(MetaCoreScene&, MetaCoreGameObject&)> OnDestroy{};
};
class MetaCoreRuntimeLifecycleExecutor {
public:
using ErrorSink = std::function<void(const MetaCoreRuntimeLifecycleError&)>;
void ClearDescriptors();
bool RegisterDescriptor(MetaCoreRuntimeLifecycleDescriptor descriptor);
std::size_t RegisterDescriptors(const std::vector<MetaCoreRuntimeLifecycleDescriptor>& descriptors);
[[nodiscard]] const std::vector<MetaCoreRuntimeLifecycleDescriptor>& GetDescriptors() const;
void EnterScene(MetaCoreScene& scene, ErrorSink errorSink = {});
void TickScene(MetaCoreScene& scene, float deltaSeconds, ErrorSink errorSink = {});
void NotifyGameObjectsWillBeDestroyed(
MetaCoreScene& scene,
const std::vector<MetaCoreId>& objectIds,
ErrorSink errorSink = {}
);
void NotifyComponentWillBeRemoved(
MetaCoreScene& scene,
MetaCoreId objectId,
std::string_view componentTypeId,
ErrorSink errorSink = {}
);
void ExitScene(MetaCoreScene& scene, ErrorSink errorSink = {});
[[nodiscard]] bool IsRunning() const;
[[nodiscard]] float GetElapsedTimeSeconds() const;
private:
[[nodiscard]] std::string BuildLifecycleKey(MetaCoreId objectId, std::string_view componentTypeId) const;
void InvokeStartForNewComponents(MetaCoreScene& scene, const ErrorSink& errorSink);
bool InvokeDestroyForStartedComponent(
MetaCoreScene& scene,
const MetaCoreRuntimeLifecycleDescriptor& descriptor,
MetaCoreId objectId,
const ErrorSink& errorSink
);
void ReportException(
const ErrorSink& errorSink,
std::string_view phase,
const MetaCoreRuntimeLifecycleDescriptor& descriptor,
MetaCoreId objectId,
const std::exception& exception
) const;
void ReportUnknownException(
const ErrorSink& errorSink,
std::string_view phase,
const MetaCoreRuntimeLifecycleDescriptor& descriptor,
MetaCoreId objectId
) const;
std::vector<MetaCoreRuntimeLifecycleDescriptor> Descriptors_{};
std::unordered_set<std::string> StartedComponents_{};
bool Running_ = false;
float ElapsedTimeSeconds_ = 0.0F;
};
class MetaCoreRuntimeLifecycleRegistry {
public:
void Clear();
bool RegisterDescriptor(MetaCoreRuntimeLifecycleDescriptor descriptor);
[[nodiscard]] const std::vector<MetaCoreRuntimeLifecycleDescriptor>& GetDescriptors() const;
private:
std::vector<MetaCoreRuntimeLifecycleDescriptor> Descriptors_{};
};
MetaCoreRuntimeLifecycleRegistry& MetaCoreGetRuntimeLifecycleRegistry();
} // namespace MetaCore

View File

@ -19,6 +19,7 @@
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h"
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCoreRuntimeLifecycle.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreScene/MetaCoreUiRmlCompiler.h"
@ -34,6 +35,8 @@
#include <filesystem>
#include <fstream>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <thread>
#include <utility>
@ -480,6 +483,121 @@ void MetaCoreTestAssetDatabaseCreateAndOpenProject() {
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestAssetDatabaseCreatesFirstClassProjectAssets() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreP2CreateProjectAssets";
std::filesystem::remove_all(tempProjectRoot);
_putenv_s("METACORE_PROJECT_PATH", "");
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto assetDatabase = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetDatabaseService>();
const auto reflectionRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIReflectionRegistry>();
MetaCoreExpect(assetDatabase != nullptr, "AssetDatabaseService should be available");
MetaCoreExpect(reflectionRegistry != nullptr, "ReflectionRegistry should be available");
MetaCoreExpect(assetDatabase->CreateProject(tempProjectRoot, "P2CreateAssets"), "Project creation should succeed");
const auto sceneRecord = assetDatabase->CreateSceneAsset(std::filesystem::path("Scenes") / "P2Scene.mcscene", true);
MetaCoreExpect(sceneRecord.has_value(), "CreateSceneAsset should create a scene record");
MetaCoreExpect(sceneRecord->Type == "scene", "Created scene should be registered as scene");
MetaCoreExpect(sceneRecord->RelativePath == std::filesystem::path("Scenes") / "P2Scene.mcscene.json", "Scene extension should normalize to .mcscene.json");
MetaCoreExpect(assetDatabase->GetProjectDescriptor().StartupScenePath == sceneRecord->RelativePath, "Created startup scene should update project descriptor");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / sceneRecord->RelativePath), "Created scene file should exist");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / (sceneRecord->RelativePath.string() + ".mcmeta")), "Created scene should have mcmeta");
const auto prefabRecord = assetDatabase->CreatePrefabAsset(std::filesystem::path("Assets") / "Prefabs" / "P2Prefab.mcprefab");
MetaCoreExpect(prefabRecord.has_value(), "CreatePrefabAsset should create a prefab record");
MetaCoreExpect(prefabRecord->Type == "prefab", "Created prefab should be registered as prefab");
MetaCoreExpect(prefabRecord->RelativePath == std::filesystem::path("Assets") / "Prefabs" / "P2Prefab.mcprefab.json", "Prefab extension should normalize to .mcprefab.json");
const auto prefabDocument = MetaCore::MetaCoreSceneSerializer::LoadPrefabFromJson(
tempProjectRoot / prefabRecord->RelativePath,
reflectionRegistry->GetTypeRegistry()
);
MetaCoreExpect(prefabDocument.has_value(), "Created prefab should be loadable");
MetaCoreExpect(prefabDocument->GameObjects.size() == 1, "Created prefab should contain a root object");
const auto materialRecord = assetDatabase->CreateMaterialAsset(std::filesystem::path("Assets") / "Materials" / "P2Material");
MetaCoreExpect(materialRecord.has_value(), "CreateMaterialAsset should create a material record");
MetaCoreExpect(materialRecord->Type == "material", "Created material should be registered as material");
const auto materialDocument = MetaCore::MetaCoreSceneSerializer::LoadMaterialFromJson(
tempProjectRoot / materialRecord->RelativePath,
reflectionRegistry->GetTypeRegistry()
);
MetaCoreExpect(materialDocument.has_value(), "Created material should be loadable");
MetaCoreExpect(materialDocument->AssetGuid == materialRecord->Guid, "Material document guid should match AssetDatabase guid");
const auto uiRecord = assetDatabase->CreateUiDocumentAsset(std::filesystem::path("Ui") / "P2Hud.mcui");
MetaCoreExpect(uiRecord.has_value(), "CreateUiDocumentAsset should create a UI record");
MetaCoreExpect(uiRecord->Type == "ui_document", "Created UI document should be registered as ui_document");
MetaCoreExpect(uiRecord->RelativePath == std::filesystem::path("Ui") / "P2Hud.mcui.json", "UI extension should normalize to .mcui.json");
const auto uiDocument = MetaCore::MetaCoreSceneSerializer::LoadUiFromJson(
tempProjectRoot / uiRecord->RelativePath,
reflectionRegistry->GetTypeRegistry()
);
MetaCoreExpect(uiDocument.has_value(), "Created UI document should be loadable");
MetaCoreExpect(uiDocument->RootNodeIds.size() == 1 && uiDocument->Nodes.size() == 1, "Created UI document should contain one root panel");
MetaCoreExpect(assetDatabase->CreateFolder(std::filesystem::path("Assets") / "Moved"), "CreateFolder should create a project folder");
const MetaCore::MetaCoreAssetGuid materialGuid = materialRecord->Guid;
MetaCoreExpect(assetDatabase->MovePath(materialRecord->RelativePath, std::filesystem::path("Assets") / "Moved"), "MovePath should move created material");
const std::filesystem::path movedMaterialPath = std::filesystem::path("Assets") / "Moved" / materialRecord->RelativePath.filename();
const auto movedMaterialRecord = assetDatabase->FindAssetByRelativePath(movedMaterialPath);
MetaCoreExpect(movedMaterialRecord.has_value(), "Moved material should be registered at new path");
MetaCoreExpect(movedMaterialRecord->Guid == materialGuid, "Moved material should keep GUID");
MetaCoreExpect(movedMaterialRecord->PackagePath == movedMaterialPath, "Moved JSON material package path should follow source path");
const MetaCore::MetaCoreAssetGuid uiGuid = uiRecord->Guid;
MetaCoreExpect(assetDatabase->RenamePath(uiRecord->RelativePath, "P2HudRenamed.mcui.json"), "RenamePath should rename created UI document");
const std::filesystem::path renamedUiPath = std::filesystem::path("Ui") / "P2HudRenamed.mcui.json";
const auto renamedUiRecord = assetDatabase->FindAssetByRelativePath(renamedUiPath);
MetaCoreExpect(renamedUiRecord.has_value(), "Renamed UI should be registered at new path");
MetaCoreExpect(renamedUiRecord->Guid == uiGuid, "Renamed UI should keep GUID");
MetaCoreExpect(renamedUiRecord->PackagePath == renamedUiPath, "Renamed JSON UI package path should follow source path");
MetaCoreExpect(assetDatabase->CreateFolder(std::filesystem::path("Assets") / "FolderMoveSource"), "CreateFolder should create folder move source");
const auto folderMaterialRecord =
assetDatabase->CreateMaterialAsset(std::filesystem::path("Assets") / "FolderMoveSource" / "FolderMaterial");
MetaCoreExpect(folderMaterialRecord.has_value(), "CreateMaterialAsset should create material inside folder");
const MetaCore::MetaCoreAssetGuid folderMaterialGuid = folderMaterialRecord->Guid;
MetaCoreExpect(assetDatabase->CreateFolder(std::filesystem::path("Assets") / "FolderMoveTarget"), "CreateFolder should create folder move target");
MetaCoreExpect(
assetDatabase->MovePath(std::filesystem::path("Assets") / "FolderMoveSource", std::filesystem::path("Assets") / "FolderMoveTarget"),
"MovePath should move a folder containing assets"
);
const std::filesystem::path movedFolderMaterialPath =
std::filesystem::path("Assets") / "FolderMoveTarget" / "FolderMoveSource" / "FolderMaterial.mcmaterial.json";
const auto movedFolderMaterialRecord = assetDatabase->FindAssetByRelativePath(movedFolderMaterialPath);
MetaCoreExpect(movedFolderMaterialRecord.has_value(), "Asset inside moved folder should be registered at new path");
MetaCoreExpect(movedFolderMaterialRecord->Guid == folderMaterialGuid, "Asset inside moved folder should keep GUID");
MetaCoreExpect(
movedFolderMaterialRecord->PackagePath == movedFolderMaterialPath,
"Asset inside moved folder should rewrite package path metadata"
);
MetaCoreExpect(
assetDatabase->RenamePath(std::filesystem::path("Assets") / "FolderMoveTarget" / "FolderMoveSource", "FolderRenamed"),
"RenamePath should rename a folder containing assets"
);
const std::filesystem::path renamedFolderMaterialPath =
std::filesystem::path("Assets") / "FolderMoveTarget" / "FolderRenamed" / "FolderMaterial.mcmaterial.json";
const auto renamedFolderMaterialRecord = assetDatabase->FindAssetByRelativePath(renamedFolderMaterialPath);
MetaCoreExpect(renamedFolderMaterialRecord.has_value(), "Asset inside renamed folder should be registered at new path");
MetaCoreExpect(renamedFolderMaterialRecord->Guid == folderMaterialGuid, "Asset inside renamed folder should keep GUID");
MetaCoreExpect(
renamedFolderMaterialRecord->PackagePath == renamedFolderMaterialPath,
"Asset inside renamed folder should rewrite package path metadata"
);
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreRenamePathProject";
@ -980,14 +1098,124 @@ void MetaCoreTestPlayModeLifecycleAndSceneIsolation() {
const auto componentRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIComponentTypeRegistry>();
const auto reflectionRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIReflectionRegistry>();
const auto playModeService = moduleRegistry.ResolveService<MetaCore::MetaCoreIPlayModeService>();
const auto clipboardService = moduleRegistry.ResolveService<MetaCore::MetaCoreIClipboardService>();
MetaCoreExpect(componentRegistry != nullptr, "Component registry should be available for play mode lifecycle test");
MetaCoreExpect(reflectionRegistry != nullptr, "Reflection registry should be available for play mode lifecycle test");
MetaCoreExpect(playModeService != nullptr, "Play mode service should be available");
MetaCoreExpect(clipboardService != nullptr, "Clipboard service should be available for runtime deletion test");
MetaCore::MetaCoreGameObject object = scene.CreateGameObject("LifecycleObject");
object.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(1.0F, 2.0F, 3.0F);
object.AddComponent<MetaCore::MetaCoreModelRootTag>().SourceModelPath = "Assets/Models/Source.glb";
const MetaCore::MetaCoreId objectId = object.GetId();
MetaCore::MetaCoreGameObject runtimeDeletedObject = scene.CreateGameObject("RuntimeDeletedObject");
runtimeDeletedObject.AddComponent<MetaCore::MetaCoreCameraComponent>(MetaCore::MetaCoreCameraComponent{});
const MetaCore::MetaCoreId runtimeDeletedObjectId = runtimeDeletedObject.GetId();
MetaCore::MetaCoreGameObject runtimeComponentRemovedObject = scene.CreateGameObject("RuntimeComponentRemovedObject");
runtimeComponentRemovedObject.AddComponent<MetaCore::MetaCoreCameraComponent>(MetaCore::MetaCoreCameraComponent{});
const MetaCore::MetaCoreId runtimeComponentRemovedObjectId = runtimeComponentRemovedObject.GetId();
int runtimeStartCount = 0;
int runtimeUpdateCount = 0;
int runtimeDestroyCount = 0;
std::vector<MetaCore::MetaCoreRuntimeLifecycleError> runtimeLifecycleErrors;
MetaCore::MetaCoreRuntimeLifecycleExecutor runtimeLifecycleExecutor;
MetaCore::MetaCoreRuntimeLifecycleDescriptor runtimeLifecycleDescriptor{};
runtimeLifecycleDescriptor.TypeId = "RuntimeModelRootTag";
runtimeLifecycleDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) {
return gameObject.HasComponent<MetaCore::MetaCoreModelRootTag>();
};
runtimeLifecycleDescriptor.OnStart = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) {
++runtimeStartCount;
};
runtimeLifecycleDescriptor.OnUpdate = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&, float) {
++runtimeUpdateCount;
throw std::runtime_error("runtime executor fault");
};
runtimeLifecycleDescriptor.OnDestroy = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) {
++runtimeDestroyCount;
};
MetaCoreExpect(
runtimeLifecycleExecutor.RegisterDescriptor(std::move(runtimeLifecycleDescriptor)),
"Runtime lifecycle executor should register a descriptor"
);
const auto runtimeErrorSink = [&](const MetaCore::MetaCoreRuntimeLifecycleError& error) {
runtimeLifecycleErrors.push_back(error);
};
runtimeLifecycleExecutor.EnterScene(scene, runtimeErrorSink);
runtimeLifecycleExecutor.TickScene(scene, 0.25F, runtimeErrorSink);
runtimeLifecycleExecutor.ExitScene(scene, runtimeErrorSink);
MetaCoreExpect(runtimeStartCount == 1, "Runtime lifecycle executor should run OnStart");
MetaCoreExpect(runtimeUpdateCount == 1, "Runtime lifecycle executor should run OnUpdate");
MetaCoreExpect(runtimeDestroyCount == 1, "Runtime lifecycle executor should run OnDestroy");
MetaCoreExpect(!runtimeLifecycleExecutor.IsRunning(), "Runtime lifecycle executor should stop after ExitScene");
MetaCoreExpect(!runtimeLifecycleErrors.empty(), "Runtime lifecycle executor should report callback exceptions");
MetaCoreExpect(
runtimeLifecycleErrors.front().Message.find("runtime executor fault") != std::string::npos,
"Runtime lifecycle executor should preserve exception messages"
);
MetaCore::MetaCoreGameObject transientObject = scene.CreateGameObject("TransientLifecycleObject");
transientObject.AddComponent<MetaCore::MetaCoreModelRootTag>().SourceModelPath = "Assets/Models/Transient.glb";
const MetaCore::MetaCoreId transientObjectId = transientObject.GetId();
int transientDestroyCount = 0;
MetaCore::MetaCoreRuntimeLifecycleExecutor transientLifecycleExecutor;
MetaCore::MetaCoreRuntimeLifecycleDescriptor transientLifecycleDescriptor{};
transientLifecycleDescriptor.TypeId = "TransientRuntimeModelRootTag";
transientLifecycleDescriptor.HasComponent = [transientObjectId](const MetaCore::MetaCoreGameObject& gameObject) {
return gameObject.GetId() == transientObjectId &&
gameObject.HasComponent<MetaCore::MetaCoreModelRootTag>();
};
transientLifecycleDescriptor.OnDestroy = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) {
++transientDestroyCount;
};
MetaCoreExpect(
transientLifecycleExecutor.RegisterDescriptor(std::move(transientLifecycleDescriptor)),
"Runtime lifecycle executor should register transient descriptor"
);
transientLifecycleExecutor.EnterScene(scene, runtimeErrorSink);
transientLifecycleExecutor.NotifyGameObjectsWillBeDestroyed(scene, {transientObjectId}, runtimeErrorSink);
MetaCoreExpect(transientDestroyCount == 1, "Runtime lifecycle executor should destroy before runtime object delete");
const std::vector<MetaCore::MetaCoreId> transientDeletedIds = scene.DeleteGameObjects({transientObjectId});
MetaCoreExpect(!transientDeletedIds.empty(), "Transient runtime object should be deleted after destroy notification");
transientLifecycleExecutor.ExitScene(scene, runtimeErrorSink);
MetaCoreExpect(
transientDestroyCount == 1,
"Runtime lifecycle executor should not destroy a notified object again on ExitScene"
);
int registryStartCount = 0;
auto& runtimeLifecycleRegistry = MetaCore::MetaCoreGetRuntimeLifecycleRegistry();
runtimeLifecycleRegistry.Clear();
MetaCore::MetaCoreRuntimeLifecycleDescriptor registryLifecycleDescriptor{};
registryLifecycleDescriptor.TypeId = "RegisteredRuntimeModelRootTag";
registryLifecycleDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) {
return gameObject.HasComponent<MetaCore::MetaCoreModelRootTag>();
};
registryLifecycleDescriptor.OnStart = [&](MetaCore::MetaCoreScene&, MetaCore::MetaCoreGameObject&) {
++registryStartCount;
};
MetaCoreExpect(
runtimeLifecycleRegistry.RegisterDescriptor(std::move(registryLifecycleDescriptor)),
"Runtime lifecycle registry should accept a valid static descriptor"
);
MetaCoreExpect(
!runtimeLifecycleRegistry.RegisterDescriptor(MetaCore::MetaCoreRuntimeLifecycleDescriptor{}),
"Runtime lifecycle registry should reject invalid descriptors"
);
MetaCore::MetaCoreRuntimeLifecycleExecutor registeredRuntimeLifecycleExecutor;
MetaCoreExpect(
registeredRuntimeLifecycleExecutor.RegisterDescriptors(runtimeLifecycleRegistry.GetDescriptors()) == 1,
"Runtime lifecycle executor should import descriptors from the runtime registry"
);
MetaCoreExpect(
registeredRuntimeLifecycleExecutor.RegisterDescriptors(runtimeLifecycleRegistry.GetDescriptors()) == 0,
"Runtime lifecycle executor should reject duplicate registry descriptors"
);
registeredRuntimeLifecycleExecutor.EnterScene(scene, runtimeErrorSink);
registeredRuntimeLifecycleExecutor.ExitScene(scene, runtimeErrorSink);
MetaCoreExpect(registryStartCount == 1, "Registered runtime lifecycle descriptor should execute OnStart");
runtimeLifecycleRegistry.Clear();
MetaCore::MetaCoreEditorContext editorContext(
window,
@ -1004,6 +1232,9 @@ void MetaCoreTestPlayModeLifecycleAndSceneIsolation() {
int destroyCount = 0;
int drawGizmosCount = 0;
int drawSelectedGizmosCount = 0;
int faultyDrawGizmosCount = 0;
int runtimeDeleteStartCount = 0;
int runtimeDeleteDestroyCount = 0;
MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor lifecycleDescriptor{};
lifecycleDescriptor.TypeId = "LifecycleModelRootTag";
@ -1044,11 +1275,26 @@ void MetaCoreTestPlayModeLifecycleAndSceneIsolation() {
lifecycleDescriptor.Lifecycle.OnDestroy = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) {
++destroyCount;
};
lifecycleDescriptor.Lifecycle.OnDrawGizmos = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) {
lifecycleDescriptor.Lifecycle.OnDrawGizmos = [&](MetaCore::MetaCoreEditorContext& context, MetaCore::MetaCoreGameObject&) {
++drawGizmosCount;
context.DrawGizmoLine(
glm::vec3(0.0F),
glm::vec3(1.0F, 0.0F, 0.0F),
glm::vec4(1.0F, 0.0F, 0.0F, 1.0F)
);
};
lifecycleDescriptor.Lifecycle.OnDrawGizmosSelected = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) {
lifecycleDescriptor.Lifecycle.OnDrawGizmosSelected = [&](MetaCore::MetaCoreEditorContext& context, MetaCore::MetaCoreGameObject&) {
++drawSelectedGizmosCount;
context.DrawGizmoWireBox(
glm::vec3(0.0F),
glm::vec3(2.0F),
glm::vec4(0.0F, 1.0F, 0.0F, 1.0F)
);
context.DrawGizmoIcon(
glm::vec3(0.0F, 1.0F, 0.0F),
"Lifecycle",
glm::vec4(0.0F, 0.5F, 1.0F, 1.0F)
);
};
MetaCoreExpect(
@ -1056,44 +1302,220 @@ void MetaCoreTestPlayModeLifecycleAndSceneIsolation() {
"Play mode lifecycle component descriptor should register"
);
MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor faultyLifecycleDescriptor{};
faultyLifecycleDescriptor.TypeId = "FaultyLifecycleModelRootTag";
faultyLifecycleDescriptor.DisplayName = "Faulty Lifecycle Model Root Tag";
faultyLifecycleDescriptor.Category = "Scripts";
faultyLifecycleDescriptor.ReflectedType = reflectionRegistry->GetTypeRegistry().FindStruct<MetaCore::MetaCoreModelRootTag>();
faultyLifecycleDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* {
return gameObject.HasComponent<MetaCore::MetaCoreModelRootTag>() ? &gameObject.GetComponent<MetaCore::MetaCoreModelRootTag>() : nullptr;
};
faultyLifecycleDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* {
return gameObject.HasComponent<MetaCore::MetaCoreModelRootTag>() ? &gameObject.GetComponent<MetaCore::MetaCoreModelRootTag>() : nullptr;
};
faultyLifecycleDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) {
return gameObject.HasComponent<MetaCore::MetaCoreModelRootTag>();
};
faultyLifecycleDescriptor.Lifecycle.OnUpdate = [](
MetaCore::MetaCoreEditorContext&,
MetaCore::MetaCoreGameObject&,
float
) {
throw std::runtime_error("intentional lifecycle fault");
};
faultyLifecycleDescriptor.Lifecycle.OnDrawGizmos = [&](
MetaCore::MetaCoreEditorContext&,
MetaCore::MetaCoreGameObject&
) {
++faultyDrawGizmosCount;
throw std::runtime_error("intentional gizmo fault");
};
MetaCoreExpect(
componentRegistry->RegisterComponentDescriptor(std::move(faultyLifecycleDescriptor)),
"Faulty play mode lifecycle descriptor should register"
);
MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor runtimeDeleteDescriptor{};
runtimeDeleteDescriptor.TypeId = "LifecycleCamera";
runtimeDeleteDescriptor.DisplayName = "Lifecycle Camera";
runtimeDeleteDescriptor.Category = "Scripts";
runtimeDeleteDescriptor.ReflectedType = reflectionRegistry->GetTypeRegistry().FindStruct<MetaCore::MetaCoreCameraComponent>();
runtimeDeleteDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* {
return gameObject.HasComponent<MetaCore::MetaCoreCameraComponent>() ? &gameObject.GetComponent<MetaCore::MetaCoreCameraComponent>() : nullptr;
};
runtimeDeleteDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* {
return gameObject.HasComponent<MetaCore::MetaCoreCameraComponent>() ? &gameObject.GetComponent<MetaCore::MetaCoreCameraComponent>() : nullptr;
};
runtimeDeleteDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) {
return gameObject.HasComponent<MetaCore::MetaCoreCameraComponent>();
};
runtimeDeleteDescriptor.Lifecycle.OnStart = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) {
++runtimeDeleteStartCount;
};
runtimeDeleteDescriptor.Lifecycle.OnDestroy = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) {
++runtimeDeleteDestroyCount;
};
MetaCoreExpect(
componentRegistry->RegisterComponentDescriptor(std::move(runtimeDeleteDescriptor)),
"Runtime delete lifecycle descriptor should register"
);
MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Edit, "Initial play mode state should be Edit");
MetaCoreExpect(!playModeService->IsRuntimeSceneActive(), "Runtime scene should be inactive in edit mode");
MetaCoreExpect(!editorContext.IsRuntimeSceneActive(), "Editor context should start in edit scene mode");
componentRegistry->DrawComponentGizmos(editorContext);
MetaCoreExpect(drawGizmosCount == 1, "OnDrawGizmos should run for registered static C++ components");
MetaCoreExpect(drawSelectedGizmosCount == 1, "OnDrawGizmosSelected should run for selected registered static C++ components");
MetaCoreExpect(
faultyDrawGizmosCount == 1,
"Component gizmo failures should be isolated without stopping other gizmo callbacks"
);
MetaCoreExpect(
editorContext.GetComponentGizmoLines().size() == 13,
"Component gizmos should queue lines and wire boxes for the Scene View"
);
MetaCoreExpect(
editorContext.GetComponentGizmoIcons().size() == 1 &&
editorContext.GetComponentGizmoIcons().front().Label == "Lifecycle",
"Component gizmos should queue labeled Scene View icons"
);
editorContext.ClearComponentGizmoDrawList();
MetaCoreExpect(
editorContext.GetComponentGizmoLines().empty() && editorContext.GetComponentGizmoIcons().empty(),
"Component gizmo draw list should clear between Scene View frames"
);
MetaCoreExpect(
editorContext.ExecuteSnapshotCommand("Pre-play edit command", [&]() {
scene.FindGameObject(objectId).GetComponent<MetaCore::MetaCoreTransformComponent>().Scale = glm::vec3(2.0F);
return true;
}),
"Edit mode commands should be recorded before entering play mode"
);
const std::size_t undoCountBeforePlay = editorContext.GetCommandService().GetUndoCount();
MetaCoreExpect(undoCountBeforePlay > 0, "Pre-play edit command should be present in undo stack");
MetaCoreExpect(playModeService->EnterPlayMode(editorContext), "EnterPlayMode should succeed");
MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Playing, "Play mode state should be Playing");
MetaCoreExpect(playModeService->IsRuntimeSceneActive(), "Runtime scene should be active in play mode");
MetaCoreExpect(editorContext.IsRuntimeSceneActive(), "Editor context should expose runtime scene mode while playing");
MetaCoreExpect(startCount == 1, "OnStart should run once on enter play mode");
MetaCoreExpect(runtimeDeleteStartCount == 2, "Runtime delete lifecycle objects should start on enter play mode");
MetaCoreExpectVec3Near(
scene.FindGameObject(objectId).GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(10.0F, 2.0F, 3.0F),
"OnStart should be able to mutate runtime scene state"
);
MetaCoreExpect(
editorContext.ExecuteSnapshotCommand("Runtime-only edit command", [&]() {
scene.FindGameObject(objectId).GetComponent<MetaCore::MetaCoreTransformComponent>().Position.z = 99.0F;
return true;
}),
"Runtime scene commands should still mutate the runtime scene"
);
MetaCoreExpect(
editorContext.GetCommandService().GetUndoCount() == undoCountBeforePlay,
"Runtime scene commands should not be pushed into the edit undo stack"
);
MetaCoreExpect(!editorContext.UndoCommand(), "Undo should be disabled while runtime scene is active");
MetaCoreExpectVec3Near(
scene.FindGameObject(objectId).GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(10.0F, 2.0F, 99.0F),
"Runtime-only edit should affect only the runtime scene"
);
playModeService->TickPlayMode(editorContext, 0.5F);
MetaCoreExpect(startCount == 1, "OnStart should not repeat during tick");
MetaCoreExpect(updateCount == 1, "OnUpdate should run during play tick");
const auto hasLifecycleErrorLog = [&]() {
return std::any_of(
logService.GetEntries().begin(),
logService.GetEntries().end(),
[](const MetaCore::MetaCoreLogEntry& entry) {
return entry.Level == MetaCore::MetaCoreLogLevel::Error &&
entry.Category == "PlayMode" &&
entry.Message.find("intentional lifecycle fault") != std::string::npos;
}
);
};
MetaCoreExpect(hasLifecycleErrorLog(), "Lifecycle exceptions should be reported to the PlayMode console log");
MetaCoreExpectVec3Near(
scene.FindGameObject(objectId).GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(10.0F, 2.5F, 3.0F),
glm::vec3(10.0F, 2.5F, 99.0F),
"OnUpdate should mutate runtime scene state"
);
MetaCoreExpect(std::abs(playModeService->GetElapsedPlayTimeSeconds() - 0.5F) <= 0.0001F, "Play time should advance during play tick");
MetaCore::MetaCoreGameObject runtimeComponentRemovedGameObject =
scene.FindGameObject(runtimeComponentRemovedObjectId);
MetaCoreExpect(
runtimeComponentRemovedGameObject &&
runtimeComponentRemovedGameObject.HasComponent<MetaCore::MetaCoreCameraComponent>(),
"Runtime component removal test object should have a Camera component"
);
playModeService->NotifyComponentWillBeRemoved(
editorContext,
runtimeComponentRemovedObjectId,
"LifecycleCamera"
);
runtimeComponentRemovedGameObject.RemoveComponent<MetaCore::MetaCoreCameraComponent>();
MetaCoreExpect(
runtimeDeleteDestroyCount == 1,
"Runtime component removal should notify lifecycle OnDestroy before removing the component"
);
editorContext.SelectOnly(runtimeDeletedObjectId);
MetaCoreExpect(clipboardService->DeleteSelection(editorContext), "Runtime DeleteSelection should delete the selected runtime object");
MetaCoreExpect(!scene.FindGameObject(runtimeDeletedObjectId), "Deleted runtime object should be removed from the runtime scene");
MetaCoreExpect(
runtimeDeleteDestroyCount == 2,
"Runtime DeleteSelection should notify lifecycle OnDestroy before deleting the object"
);
editorContext.SelectOnly(objectId);
MetaCoreExpect(playModeService->PausePlayMode(editorContext), "PausePlayMode should succeed");
MetaCoreExpect(playModeService->IsRuntimeSceneActive(), "Runtime scene should remain active while paused");
playModeService->TickPlayMode(editorContext, 0.25F);
MetaCoreExpect(updateCount == 1, "OnUpdate should not run while paused");
MetaCoreExpect(std::abs(playModeService->GetElapsedPlayTimeSeconds() - 0.5F) <= 0.0001F, "Play time should not advance during paused tick");
MetaCoreExpect(playModeService->StepPlayMode(editorContext, 0.125F), "StepPlayMode should succeed while paused");
MetaCoreExpect(updateCount == 2, "OnUpdate should run for a paused step");
MetaCoreExpect(std::abs(playModeService->GetElapsedPlayTimeSeconds() - 0.625F) <= 0.0001F, "Play time should advance by step delta");
MetaCoreExpect(playModeService->ResumePlayMode(editorContext), "ResumePlayMode should succeed");
playModeService->TickPlayMode(editorContext, 0.25F);
MetaCoreExpect(updateCount == 2, "OnUpdate should resume after pause");
MetaCoreExpect(updateCount == 3, "OnUpdate should resume after pause");
MetaCoreExpect(playModeService->ExitPlayMode(editorContext), "ExitPlayMode should succeed");
MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Edit, "Play mode state should return to Edit");
MetaCoreExpect(!playModeService->IsRuntimeSceneActive(), "Runtime scene should be inactive after exiting play mode");
MetaCoreExpect(!editorContext.IsRuntimeSceneActive(), "Editor context should return to edit scene mode after exiting play mode");
MetaCoreExpect(destroyCount == 1, "OnDestroy should run on exit play mode");
MetaCoreExpect(
runtimeDeleteDestroyCount == 2,
"Runtime removed components and deleted objects should not receive duplicate OnDestroy on exit play mode"
);
MetaCoreExpect(
scene.FindGameObject(runtimeDeletedObjectId),
"ExitPlayMode should restore runtime-deleted objects from the edit scene snapshot"
);
MetaCoreExpect(
scene.FindGameObject(runtimeComponentRemovedObjectId).HasComponent<MetaCore::MetaCoreCameraComponent>(),
"ExitPlayMode should restore runtime-removed components from the edit scene snapshot"
);
MetaCoreExpectVec3Near(
scene.FindGameObject(objectId).GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(1.0F, 2.0F, 3.0F),
"ExitPlayMode should restore edit scene snapshot"
);
MetaCoreExpectVec3Near(
scene.FindGameObject(objectId).GetComponent<MetaCore::MetaCoreTransformComponent>().Scale,
glm::vec3(2.0F),
"ExitPlayMode should keep pre-play edit scene changes"
);
MetaCoreExpect(
editorContext.GetCommandService().GetUndoCount() == undoCountBeforePlay,
"ExitPlayMode should preserve the pre-play edit undo stack without runtime commands"
);
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
@ -1130,9 +1552,9 @@ void MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo() {
logService,
moduleRegistry
);
editorContext.SetSelection({parentId, childId, siblingId}, parentId);
MetaCore::MetaCoreSceneInteractionService sceneInteractionService;
editorContext.SetSelection({parentId, childId, siblingId}, parentId);
const glm::mat4 originalParentWorld =
MetaCore::MetaCoreBuildTransformMatrix(scene.FindGameObject(parentId).GetComponent<MetaCore::MetaCoreTransformComponent>());
const glm::mat4 movedParentWorld = glm::translate(glm::mat4(1.0F), glm::vec3(3.0F, 0.0F, 0.0F));
@ -1186,10 +1608,137 @@ void MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo() {
"Redo gizmo drag should reapply other selected root transform"
);
MetaCoreExpect(
editorContext.GetGizmoPivotMode() == MetaCore::MetaCoreGizmoPivotMode::Pivot,
"Gizmo pivot mode should default to Pivot"
);
editorContext.SetGizmoPivotMode(MetaCore::MetaCoreGizmoPivotMode::Center);
MetaCoreExpect(
editorContext.GetGizmoPivotMode() == MetaCore::MetaCoreGizmoPivotMode::Center,
"Gizmo pivot mode should switch to Center"
);
const glm::mat4 originalCenterWorld = glm::translate(glm::mat4(1.0F), glm::vec3(5.0F, 0.0F, 0.0F));
const glm::mat4 rotatedCenterWorld =
originalCenterWorld * glm::rotate(glm::mat4(1.0F), glm::radians(90.0F), glm::vec3(0.0F, 0.0F, 1.0F));
sceneInteractionService.HandleGizmoBeginUse(editorContext, true);
MetaCoreExpect(
sceneInteractionService.ApplyWorldTransformDeltaToSelection(
editorContext,
parentId,
originalCenterWorld,
rotatedCenterWorld,
MetaCore::MetaCoreGizmoPivotMode::Center
),
"Center gizmo delta should apply around the shared selection center"
);
sceneInteractionService.HandleGizmoEndUse(editorContext, true);
sceneInteractionService.HandleGizmoEndUse(editorContext, false);
MetaCoreExpectVec3Near(
scene.FindGameObject(parentId).GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(5.0F, -2.0F, 0.0F),
"Center rotation should rotate active root around shared center"
);
MetaCoreExpectVec3Near(
scene.FindGameObject(siblingId).GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(5.0F, 2.0F, 0.0F),
"Center rotation should rotate other selected root around shared center"
);
MetaCoreExpectVec3Near(
scene.FindGameObject(childId).GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(0.0F, 2.0F, 0.0F),
"Center rotation should not double-apply selected child local transform"
);
MetaCoreExpect(editorContext.UndoCommand(), "Undo center gizmo rotation should succeed");
MetaCoreExpectVec3Near(
scene.FindGameObject(parentId).GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(3.0F, 0.0F, 0.0F),
"Undo center rotation should restore active root"
);
MetaCoreExpectVec3Near(
scene.FindGameObject(siblingId).GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(7.0F, 0.0F, 0.0F),
"Undo center rotation should restore other root"
);
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
}
void MetaCoreTestSceneViewportHoverPicking() {
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
MetaCore::MetaCoreGameObject hoveredObject = scene.CreateGameObject("HoverTarget");
hoveredObject.AddComponent<MetaCore::MetaCoreMeshRendererComponent>(MetaCore::MetaCoreMeshRendererComponent{});
const MetaCore::MetaCoreId hoveredObjectId = hoveredObject.GetId();
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
MetaCore::MetaCoreSceneViewportState& viewportState = editorContext.GetSceneViewportState();
viewportState.Left = 0.0F;
viewportState.Top = 0.0F;
viewportState.Width = 128.0F;
viewportState.Height = 128.0F;
viewportState.Hovered = true;
viewportState.Focused = true;
editorContext.GetInput().SetCursorPosition(glm::vec2(64.0F, 64.0F));
MetaCore::MetaCoreSceneView sceneView;
sceneView.CameraPosition = glm::vec3(0.0F, 0.0F, 5.0F);
sceneView.CameraTarget = glm::vec3(0.0F, 0.0F, 0.0F);
sceneView.CameraUp = glm::vec3(0.0F, 1.0F, 0.0F);
sceneView.VerticalFieldOfViewDegrees = 60.0F;
MetaCore::MetaCoreSceneInteractionService sceneInteractionService;
sceneInteractionService.UpdateViewportHover(editorContext, sceneView, true);
MetaCoreExpect(editorContext.GetHoveredObjectId() == hoveredObjectId, "Scene viewport hover should track picked object");
MetaCoreExpect(editorContext.IsObjectHovered(hoveredObjectId), "Hovered object predicate should match picked object");
MetaCoreExpect(
editorContext.GetShowViewportSelectionOverlay(),
"Viewport selection overlay should be enabled by default"
);
const MetaCore::MetaCoreEditorStateSnapshot snapshot = editorContext.CaptureStateSnapshot();
editorContext.ClearHoveredObject();
editorContext.SetShowViewportSelectionOverlay(false);
editorContext.RestoreStateSnapshot(snapshot);
MetaCoreExpect(
editorContext.GetHoveredObjectId() == 0,
"Editor state snapshot restore should not restore transient hover state"
);
MetaCoreExpect(
!editorContext.GetShowViewportSelectionOverlay(),
"Editor state snapshot restore should not restore transient viewport overlay state"
);
editorContext.SetShowViewportSelectionOverlay(true);
MetaCoreExpect(
editorContext.GetShowViewportSelectionOverlay(),
"Viewport selection overlay flag should be mutable"
);
editorContext.SetHoveredObjectId(hoveredObjectId);
sceneInteractionService.UpdateViewportHover(editorContext, sceneView, false);
MetaCoreExpect(editorContext.GetHoveredObjectId() == 0, "Scene viewport hover should clear when picking is blocked");
editorContext.SetHoveredObjectId(hoveredObjectId);
viewportState.Hovered = false;
sceneInteractionService.UpdateViewportHover(editorContext, sceneView, true);
MetaCoreExpect(editorContext.GetHoveredObjectId() == 0, "Scene viewport hover should clear outside the viewport");
}
void MetaCoreTestGizmoSnapSettings() {
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
@ -1242,6 +1791,25 @@ void MetaCoreTestGizmoSnapSettings() {
std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Scale) - 0.2F) < 0.0001F,
"Custom scale snap step should be used"
);
MetaCore::MetaCoreGizmoSnapSettings invalidSnap;
invalidSnap.Enabled = true;
invalidSnap.TranslationStep = -1.0F;
invalidSnap.RotationStepDegrees = 900.0F;
invalidSnap.ScaleStep = std::numeric_limits<float>::quiet_NaN();
editorContext.SetGizmoSnapSettings(invalidSnap);
MetaCoreExpect(
std::abs(editorContext.GetGizmoSnapSettings().TranslationStep - 0.001F) < 0.0001F,
"Negative translate snap step should clamp to a positive minimum"
);
MetaCoreExpect(
std::abs(editorContext.GetGizmoSnapSettings().RotationStepDegrees - 360.0F) < 0.0001F,
"Oversized rotation snap step should clamp to 360 degrees"
);
MetaCoreExpect(
std::abs(editorContext.GetGizmoSnapSettings().ScaleStep - 0.1F) < 0.0001F,
"Non-finite scale snap step should fall back to the default"
);
}
void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() {
@ -2458,9 +3026,14 @@ void MetaCoreTestPrefabWorkflow() {
moduleRegistry
);
MetaCore::MetaCoreGameObject root = scene.CreateGameObject("PrefabRoot");
MetaCore::MetaCoreGameObject sceneParent = scene.CreateGameObject("SceneParent");
const MetaCore::MetaCoreId sceneParentId = sceneParent.GetId();
MetaCore::MetaCoreGameObject root = scene.CreateGameObject("PrefabRoot", sceneParentId);
root.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
root.AddComponent<MetaCoreSmokeScriptComponent>();
root.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(1.0F, 2.0F, 3.0F);
root.GetComponent<MetaCoreSmokeScriptComponent>().Speed = 4.25F;
root.GetComponent<MetaCoreSmokeScriptComponent>().Target = "PrefabTarget";
const MetaCore::MetaCoreId rootId = root.GetId();
MetaCore::MetaCoreGameObject child = scene.CreateGameObject("PrefabChild", rootId);
child.AddComponent<MetaCore::MetaCoreLightComponent>();
@ -2471,35 +3044,63 @@ void MetaCoreTestPrefabWorkflow() {
const auto assetDatabaseService = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetDatabaseService>();
const auto packageService = moduleRegistry.ResolveService<MetaCore::MetaCoreIPackageService>();
const auto reflectionRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIReflectionRegistry>();
const auto componentRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIComponentTypeRegistry>();
MetaCoreExpect(prefabService != nullptr, "应解析到 PrefabService");
MetaCoreExpect(prefabService->SupportsPrefabWorkflows(), "PrefabService 应支持工作流");
MetaCoreExpect(assetDatabaseService != nullptr, "应解析到 AssetDatabaseService");
MetaCoreExpect(packageService != nullptr, "应解析到 PackageService");
MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry");
MetaCoreExpect(componentRegistry != nullptr, "Prefab workflow should resolve ComponentRegistry");
MetaCoreRegisterSmokeScriptComponentForTesting(*componentRegistry, *reflectionRegistry, "PrefabSmokeScript");
const std::filesystem::path prefabRequestPath = std::filesystem::path("Assets") / "Prefabs" / "PrefabRoot.mcprefab";
const std::filesystem::path prefabPath = std::filesystem::path("Assets") / "Prefabs" / "PrefabRoot.mcprefab.json";
const auto createdPrefabPath = prefabService->CreatePrefabFromSelection(editorContext, prefabPath);
const auto createdPrefabPath = prefabService->CreatePrefabFromSelection(editorContext, prefabRequestPath);
MetaCoreExpect(createdPrefabPath.has_value(), "应能从选中对象创建 prefab");
MetaCoreExpect(std::filesystem::exists(tempProjectRoot / prefabPath), "应写出 mcprefab.json 文件");
MetaCoreExpect(assetDatabaseService->Refresh(), "创建 prefab 后应能刷新资产数据库");
MetaCoreExpect(*createdPrefabPath == prefabPath, "CreatePrefabFromSelection should normalize .mcprefab to .mcprefab.json");
MetaCoreExpect(root.GetParentId() == sceneParentId, "Create Prefab should preserve the selected object's scene parent");
MetaCoreExpect(root.HasComponent<MetaCore::MetaCorePrefabInstanceMetadata>(), "Create Prefab should link the original root as a prefab instance");
MetaCoreExpect(child.HasComponent<MetaCore::MetaCorePrefabInstanceMetadata>(), "Create Prefab should link the original child as a prefab instance");
const auto prefabRecord = assetDatabaseService->FindAssetByRelativePath(prefabPath);
MetaCoreExpect(prefabRecord.has_value(), "应能找到 prefab 资产记录");
MetaCoreExpect(prefabRecord->Type == "prefab", "prefab 资产类型应正确");
MetaCoreExpect(root.GetComponent<MetaCore::MetaCorePrefabInstanceMetadata>().PrefabAssetGuid == prefabRecord->Guid, "Original root should store the prefab asset guid");
MetaCoreExpect(root.GetComponent<MetaCore::MetaCorePrefabInstanceMetadata>().PrefabObjectId == rootId, "Original root should map to its source prefab object id");
MetaCoreExpect(root.GetComponent<MetaCore::MetaCorePrefabInstanceMetadata>().PrefabInstanceRootId == rootId, "Original root should store itself as the prefab instance root");
const auto instantiatedRootId = prefabService->InstantiatePrefab(editorContext, prefabRecord->Guid, std::nullopt);
MetaCoreExpect(instantiatedRootId.has_value(), "应能实例化 prefab");
MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId);
MetaCoreExpect(instantiatedRoot, "实例化后应能找到根对象");
MetaCoreExpect(instantiatedRoot.HasComponent<MetaCore::MetaCorePrefabInstanceMetadata>(), "实例根应带有 prefab 元数据");
MetaCoreExpect(instantiatedRoot.HasComponent<MetaCoreSmokeScriptComponent>(), "Prefab instance should restore static C++ components");
MetaCoreExpect(std::abs(instantiatedRoot.GetComponent<MetaCoreSmokeScriptComponent>().Speed - 4.25F) <= 0.0001F, "Prefab instance should restore static C++ component field values");
MetaCoreExpect(instantiatedRoot.GetComponent<MetaCoreSmokeScriptComponent>().Target == "PrefabTarget", "Prefab instance should restore static C++ component string fields");
instantiatedRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(8.0F, 9.0F, 10.0F);
instantiatedRoot.GetComponent<MetaCoreSmokeScriptComponent>().Speed = 6.75F;
instantiatedRoot.GetComponent<MetaCoreSmokeScriptComponent>().Target = "AppliedPrefabTarget";
editorContext.SelectOnly(instantiatedRoot.GetId());
MetaCoreExpect(prefabService->ApplySelectedPrefabInstance(editorContext), "应能应用 prefab 实例");
const auto appliedPrefabDocument = MetaCore::MetaCoreSceneSerializer::LoadPrefabFromJson(tempProjectRoot / prefabPath, reflectionRegistry->GetTypeRegistry());
MetaCoreExpect(appliedPrefabDocument.has_value(), "Apply 后 prefab 应能被 JSON 解析");
MetaCoreExpect(!appliedPrefabDocument->GameObjects.empty(), "Apply 后 prefab 应保留对象数据");
MetaCoreExpect(
!appliedPrefabDocument->GameObjects.front().SerializedComponents.empty(),
"Apply should write static C++ components into the prefab document"
);
MetaCoreExpect(
appliedPrefabDocument->GameObjects.front().SerializedComponents.front().TypeId == "PrefabSmokeScript",
"Apply should preserve the static C++ component TypeId in the prefab document"
);
MetaCoreExpectVec3Near(
appliedPrefabDocument->GameObjects.front().Transform.Position,
glm::vec3(8.0F, 9.0F, 10.0F),
@ -2512,7 +3113,12 @@ void MetaCoreTestPrefabWorkflow() {
MetaCoreExpect(secondInstanceRoot, "第二个实例应存在");
MetaCoreExpectVec3Near(secondInstanceRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Apply 后新实例应继承修改过的 transform");
MetaCoreExpect(secondInstanceRoot.HasComponent<MetaCoreSmokeScriptComponent>(), "Apply should make new prefab instances restore static C++ components");
MetaCoreExpect(std::abs(secondInstanceRoot.GetComponent<MetaCoreSmokeScriptComponent>().Speed - 6.75F) <= 0.0001F, "Apply should make new prefab instances inherit static C++ component fields");
MetaCoreExpect(secondInstanceRoot.GetComponent<MetaCoreSmokeScriptComponent>().Target == "AppliedPrefabTarget", "Apply should make new prefab instances inherit static C++ string fields");
secondInstanceRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(-1.0F, -2.0F, -3.0F);
secondInstanceRoot.GetComponent<MetaCoreSmokeScriptComponent>().Speed = 1.0F;
editorContext.SelectOnly(secondInstanceRoot.GetId());
MetaCoreExpect(prefabService->RevertSelectedPrefabInstance(editorContext), "应能还原 prefab 实例");
@ -2520,6 +3126,9 @@ void MetaCoreTestPrefabWorkflow() {
MetaCoreExpect(revertedRoot, "还原后应重新选中实例根");
MetaCoreExpectVec3Near(revertedRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Revert 后实例应恢复为 prefab 内容");
MetaCoreExpect(revertedRoot.HasComponent<MetaCoreSmokeScriptComponent>(), "Revert should restore static C++ components");
MetaCoreExpect(std::abs(revertedRoot.GetComponent<MetaCoreSmokeScriptComponent>().Speed - 6.75F) <= 0.0001F, "Revert should restore static C++ component fields");
const MetaCore::MetaCoreId revertedRootId = revertedRoot.GetId();
const std::vector<MetaCore::MetaCoreId> revertedSubtreeIds = scene.GetSubtreeObjectIds(revertedRootId);
@ -5512,6 +6121,8 @@ int main() {
MetaCoreTestProjectFileRoundTripAndDiscovery();
std::cout << "[RUN] MetaCoreTestAssetDatabaseCreateAndOpenProject..." << std::endl;
MetaCoreTestAssetDatabaseCreateAndOpenProject();
std::cout << "[RUN] MetaCoreTestAssetDatabaseCreatesFirstClassProjectAssets..." << std::endl;
MetaCoreTestAssetDatabaseCreatesFirstClassProjectAssets();
std::cout << "[RUN] MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor..." << std::endl;
MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor();
std::cout << "[RUN] MetaCoreTestAssetDatabaseMovePathUpdatesProjectDescriptor..." << std::endl;
@ -5524,6 +6135,8 @@ int main() {
MetaCoreTestPlayModeLifecycleAndSceneIsolation();
std::cout << "[RUN] MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo..." << std::endl;
MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo();
std::cout << "[RUN] MetaCoreTestSceneViewportHoverPicking..." << std::endl;
MetaCoreTestSceneViewportHoverPicking();
std::cout << "[RUN] MetaCoreTestGizmoSnapSettings..." << std::endl;
MetaCoreTestGizmoSnapSettings();
std::cout << "[RUN] MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot..." << std::endl;