feat: implement Unity/Infernux style ObjectField for material Inspector with drag-and-drop, clear, and search-picker
This commit is contained in:
parent
1bb2c858e2
commit
d7590c53b6
@ -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>();
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
326
Source/MetaCoreScene/Private/MetaCoreRuntimeLifecycle.cpp
Normal file
326
Source/MetaCoreScene/Private/MetaCoreRuntimeLifecycle.cpp
Normal 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
|
||||
@ -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
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user