diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index 0b6797f..3ac775b 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -4350,6 +4350,7 @@ public: [[nodiscard]] bool OpenProject(const std::filesystem::path& projectRoot) override; [[nodiscard]] bool RenamePath(const std::filesystem::path& relativePath, const std::filesystem::path& newName) override; [[nodiscard]] bool MovePath(const std::filesystem::path& relativePath, const std::filesystem::path& targetDirectory) override; + [[nodiscard]] bool MovePathTo(const std::filesystem::path& relativePath, const std::filesystem::path& targetRelativePath) override; [[nodiscard]] bool DeletePath(const std::filesystem::path& relativePath) override; bool ApplyExternalFileChanges(const std::vector& changes) override; [[nodiscard]] const std::vector& GetAssetRegistry() const override { return AssetRecords_; } @@ -4403,6 +4404,8 @@ public: bool Refresh() override; bool CreateFolder(const std::filesystem::path& relativeDirectory) override; + bool CreateMaterialAsset(const std::filesystem::path& relativeMaterialPath) override; + bool CreateEmptySceneAsset(const std::filesystem::path& relativeScenePath, bool makeStartupScene) override; bool RegisterScenePath(const std::filesystem::path& relativeScenePath, bool makeStartupScene) override; bool SetStartupScenePath(const std::filesystem::path& relativeScenePath) override; @@ -4570,49 +4573,7 @@ bool MetaCoreBuiltinAssetDatabaseService::RenamePath( const std::filesystem::path normalizedPath = relativePath.lexically_normal(); const std::filesystem::path targetPath = (normalizedPath.parent_path() / newName).lexically_normal(); - if (MetaCoreIsUnsafeRelativePath(targetPath)) { - return false; - } - - const std::filesystem::path absoluteSourcePath = Project_.RootPath / normalizedPath; - const std::filesystem::path absoluteTargetPath = Project_.RootPath / targetPath; - if (!std::filesystem::exists(absoluteSourcePath) || std::filesystem::exists(absoluteTargetPath)) { - return false; - } - - std::error_code errorCode; - std::filesystem::rename(absoluteSourcePath, absoluteTargetPath, errorCode); - if (errorCode) { - return false; - } - - MetaCoreMoveSidecarIfPresent(Project_.RootPath, normalizedPath, targetPath); - MetaCoreUpdateMovedMetadata(Project_.RootPath, normalizedPath, targetPath); - - const auto remapPath = [&](std::filesystem::path& pathValue) { - if (pathValue == normalizedPath) { - pathValue = targetPath; - } else { - const auto normalizedValue = pathValue.lexically_normal(); - const auto normalizedString = normalizedValue.generic_string(); - const auto sourcePrefix = normalizedPath.generic_string() + "/"; - if (normalizedString.rfind(sourcePrefix, 0) == 0) { - pathValue = (targetPath / normalizedValue.lexically_relative(normalizedPath)).lexically_normal(); - } - } - }; - - remapPath(Project_.StartupScenePath); - for (auto& scenePath : Project_.ScenePaths) { - remapPath(scenePath); - } - std::sort(Project_.ScenePaths.begin(), Project_.ScenePaths.end()); - Project_.ScenePaths.erase(std::unique(Project_.ScenePaths.begin(), Project_.ScenePaths.end()), Project_.ScenePaths.end()); - SaveProjectDescriptor(); - - const bool refreshed = Refresh(); - RescanHotReloadBaseline(); - return refreshed; + return MovePathTo(normalizedPath, targetPath); } bool MetaCoreBuiltinAssetDatabaseService::MovePath( @@ -4633,42 +4594,65 @@ bool MetaCoreBuiltinAssetDatabaseService::MovePath( return true; } - const std::filesystem::path absoluteSourcePath = Project_.RootPath / normalizedSourcePath; const std::filesystem::path absoluteTargetDirectory = Project_.RootPath / normalizedTargetDirectory; - if (!std::filesystem::exists(absoluteSourcePath) || !std::filesystem::exists(absoluteTargetDirectory)) { + if (!std::filesystem::exists(absoluteTargetDirectory) || !std::filesystem::is_directory(absoluteTargetDirectory)) { return false; } const std::filesystem::path targetPath = (normalizedTargetDirectory / normalizedSourcePath.filename()).lexically_normal(); - if (MetaCoreIsUnsafeRelativePath(targetPath) || std::filesystem::exists(Project_.RootPath / targetPath)) { + return MovePathTo(normalizedSourcePath, targetPath); +} + +bool MetaCoreBuiltinAssetDatabaseService::MovePathTo( + const std::filesystem::path& relativePath, + const std::filesystem::path& targetRelativePath +) { + if (!HasProject() || + MetaCoreIsUnsafeRelativePath(relativePath) || + MetaCoreIsUnsafeRelativePath(targetRelativePath) || + relativePath.empty() || + targetRelativePath.empty()) { return false; } - auto remapPath = [&](std::filesystem::path& pathValue) { - if (pathValue == normalizedSourcePath) { - pathValue = targetPath; - } else { - const auto normalizedValue = pathValue.lexically_normal(); - const auto normalizedString = normalizedValue.generic_string(); - const auto sourcePrefix = normalizedSourcePath.generic_string() + "/"; - if (normalizedString.rfind(sourcePrefix, 0) == 0) { - pathValue = (targetPath / normalizedValue.lexically_relative(normalizedSourcePath)).lexically_normal(); - } + const std::filesystem::path normalizedSourcePath = relativePath.lexically_normal(); + const std::filesystem::path normalizedTargetPath = targetRelativePath.lexically_normal(); + if (normalizedSourcePath == normalizedTargetPath) { + return true; + } + + const std::filesystem::path absoluteSourcePath = Project_.RootPath / normalizedSourcePath; + const std::filesystem::path absoluteTargetPath = Project_.RootPath / normalizedTargetPath; + if (!std::filesystem::exists(absoluteSourcePath) || std::filesystem::exists(absoluteTargetPath)) { + return false; + } + + if (std::filesystem::is_directory(absoluteSourcePath)) { + const std::string sourcePrefix = normalizedSourcePath.generic_string() + "/"; + const std::string targetString = normalizedTargetPath.generic_string() + "/"; + if (targetString.rfind(sourcePrefix, 0) == 0) { + return false; } - }; + } std::error_code errorCode; - std::filesystem::rename(absoluteSourcePath, Project_.RootPath / targetPath, errorCode); + std::filesystem::create_directories(absoluteTargetPath.parent_path(), errorCode); if (errorCode) { return false; } - MetaCoreMoveSidecarIfPresent(Project_.RootPath, normalizedSourcePath, targetPath); - MetaCoreUpdateMovedMetadata(Project_.RootPath, normalizedSourcePath, targetPath); + errorCode.clear(); + std::filesystem::rename(absoluteSourcePath, absoluteTargetPath, errorCode); + if (errorCode) { + return false; + } - remapPath(Project_.StartupScenePath); + MetaCoreMoveSidecarIfPresent(Project_.RootPath, normalizedSourcePath, normalizedTargetPath); + MetaCoreUpdateMovedMetadata(Project_.RootPath, normalizedSourcePath, normalizedTargetPath); + + Project_.StartupScenePath = MetaCoreRemapRelativePath(Project_.StartupScenePath, normalizedSourcePath, normalizedTargetPath); for (auto& scenePath : Project_.ScenePaths) { - remapPath(scenePath); + scenePath = MetaCoreRemapRelativePath(scenePath, normalizedSourcePath, normalizedTargetPath); } std::sort(Project_.ScenePaths.begin(), Project_.ScenePaths.end()); Project_.ScenePaths.erase(std::unique(Project_.ScenePaths.begin(), Project_.ScenePaths.end()), Project_.ScenePaths.end()); @@ -5031,6 +5015,108 @@ bool MetaCoreBuiltinAssetDatabaseService::CreateFolder(const std::filesystem::pa return std::filesystem::create_directories(Project_.RootPath / relativeDirectory.lexically_normal()); } +bool MetaCoreBuiltinAssetDatabaseService::CreateMaterialAsset(const std::filesystem::path& relativeMaterialPath) { + if (!HasProject() || ReflectionRegistry_ == nullptr || MetaCoreIsUnsafeRelativePath(relativeMaterialPath)) { + return false; + } + + const std::filesystem::path normalizedPath = relativeMaterialPath.lexically_normal(); + if (!MetaCoreIsMaterialPath(normalizedPath)) { + return false; + } + + const std::filesystem::path absolutePath = Project_.RootPath / normalizedPath; + if (std::filesystem::exists(absolutePath)) { + return false; + } + + std::error_code errorCode; + std::filesystem::create_directories(absolutePath.parent_path(), errorCode); + if (errorCode) { + return false; + } + + MetaCoreMaterialAssetDocument materialDocument; + materialDocument.AssetGuid = MetaCoreAssetGuid::Generate(); + materialDocument.Name = normalizedPath.stem().stem().string(); + + if (!MetaCoreSceneSerializer::SaveMaterialToJson(absolutePath, materialDocument, ReflectionRegistry_->GetTypeRegistry())) { + return false; + } + + MetaCoreAssetMetadataDocument metadata; + metadata.AssetGuid = materialDocument.AssetGuid; + metadata.AssetType = "material"; + metadata.ImporterId = "MaterialImporter"; + metadata.SourcePath = normalizedPath.generic_string(); + metadata.PackagePath = normalizedPath.generic_string(); + metadata.SourceHash = MetaCoreHashFile(absolutePath).value_or(0); + if (!MetaCoreWriteMetaJson(MetaCoreBuildMetaPath(absolutePath), metadata)) { + return false; + } + + const bool refreshed = Refresh(); + RescanHotReloadBaseline(); + return refreshed; +} + +bool MetaCoreBuiltinAssetDatabaseService::CreateEmptySceneAsset( + const std::filesystem::path& relativeScenePath, + bool makeStartupScene +) { + if (!HasProject() || ReflectionRegistry_ == nullptr || MetaCoreIsUnsafeRelativePath(relativeScenePath)) { + return false; + } + + const std::filesystem::path normalizedPath = relativeScenePath.lexically_normal(); + if (!MetaCoreIsScenePath(normalizedPath)) { + return false; + } + + const std::filesystem::path absolutePath = Project_.RootPath / normalizedPath; + if (std::filesystem::exists(absolutePath)) { + return false; + } + + std::error_code errorCode; + std::filesystem::create_directories(absolutePath.parent_path(), errorCode); + if (errorCode) { + return false; + } + + const MetaCoreAssetGuid sceneGuid = MetaCoreAssetGuid::Generate(); + MetaCoreSceneDocument sceneDocument; + sceneDocument.Name = MetaCoreSceneDisplayNameFromPath(normalizedPath); + sceneDocument.GameObjects = {}; + if (!MetaCoreSceneSerializer::SaveSceneToJson(absolutePath, sceneDocument, ReflectionRegistry_->GetTypeRegistry())) { + return false; + } + + MetaCoreAssetMetadataDocument metadata; + metadata.AssetGuid = sceneGuid; + metadata.AssetType = "scene"; + metadata.ImporterId = "SceneImporter"; + metadata.SourcePath = normalizedPath.generic_string(); + metadata.PackagePath = normalizedPath.generic_string(); + metadata.SourceHash = MetaCoreHashFile(absolutePath).value_or(0); + if (!MetaCoreWriteMetaJson(MetaCoreBuildMetaPath(absolutePath), metadata)) { + return false; + } + + if (std::find(Project_.ScenePaths.begin(), Project_.ScenePaths.end(), normalizedPath) == Project_.ScenePaths.end()) { + Project_.ScenePaths.push_back(normalizedPath); + std::sort(Project_.ScenePaths.begin(), Project_.ScenePaths.end()); + } + if (makeStartupScene) { + Project_.StartupScenePath = normalizedPath; + } + SaveProjectDescriptor(); + + const bool refreshed = Refresh(); + RescanHotReloadBaseline(); + return refreshed; +} + bool MetaCoreBuiltinAssetDatabaseService::RegisterScenePath( const std::filesystem::path& relativeScenePath, bool makeStartupScene @@ -6227,6 +6313,11 @@ public: MetaCoreEditorContext& editorContext, const std::filesystem::path& relativePrefabPath ) override; + [[nodiscard]] std::optional CreatePrefabFromObject( + MetaCoreEditorContext& editorContext, + MetaCoreId objectId, + const std::filesystem::path& relativePrefabPath + ) override; [[nodiscard]] std::optional InstantiatePrefab( MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& prefabAssetGuid, @@ -7490,6 +7581,14 @@ std::optional MetaCoreBuiltinPrefabService::LoadPrefabDo std::optional MetaCoreBuiltinPrefabService::CreatePrefabFromSelection( MetaCoreEditorContext& editorContext, const std::filesystem::path& relativePrefabPath +) { + return CreatePrefabFromObject(editorContext, editorContext.GetActiveObjectId(), relativePrefabPath); +} + +std::optional MetaCoreBuiltinPrefabService::CreatePrefabFromObject( + MetaCoreEditorContext& editorContext, + MetaCoreId objectId, + const std::filesystem::path& relativePrefabPath ) { if (AssetDatabaseService_ == nullptr || ReflectionRegistry_ == nullptr || @@ -7498,19 +7597,18 @@ std::optional MetaCoreBuiltinPrefabService::CreatePrefabF return std::nullopt; } - const MetaCoreId activeObjectId = editorContext.GetActiveObjectId(); - if (activeObjectId == 0 || !editorContext.GetScene().FindGameObject(activeObjectId)) { + if (objectId == 0 || !editorContext.GetScene().FindGameObject(objectId)) { return std::nullopt; } - std::vector prefabObjects = MetaCoreCaptureSubtree(editorContext.GetScene(), activeObjectId); + std::vector prefabObjects = MetaCoreCaptureSubtree(editorContext.GetScene(), objectId); if (prefabObjects.empty()) { return std::nullopt; } - MetaCoreNormalizePrefabSubtree(prefabObjects, activeObjectId); + MetaCoreNormalizePrefabSubtree(prefabObjects, objectId); - const MetaCoreGameObject rootObject = editorContext.GetScene().FindGameObject(activeObjectId); + const MetaCoreGameObject rootObject = editorContext.GetScene().FindGameObject(objectId); const std::filesystem::path normalizedPrefabPath = relativePrefabPath.lexically_normal(); const std::filesystem::path absolutePrefabPath = AssetDatabaseService_->GetProjectDescriptor().RootPath / normalizedPrefabPath; diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index 3a0388c..5e9a8ee 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -1,6 +1,7 @@ #include "MetaCoreBuiltinEditorModule.h" #if defined(_WIN32) #include +#include #endif #include "MetaCoreEditor/MetaCoreEditorContext.h" @@ -19,13 +20,19 @@ #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" +#include "stb_image.h" + #include #include #include #include +#include #include #include +#include +#include +#include #include #include #include @@ -57,9 +64,22 @@ void CreateNewMaterial(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatab static std::string GeneratedResourceFilter_{}; static std::string GeneratedResourceKindFilter_{}; static std::array AssetFilterBuffer_{}; +static float ProjectThumbnailSize_ = 76.0F; static std::filesystem::path PendingRenamePath_{}; static std::array RenameBuffer_{}; static std::array NewFolderBuffer_{}; +static std::filesystem::path PendingDeletePath_{}; +static bool PendingDeleteIsDirectory_ = false; +enum class MetaCoreProjectCreateKind { + Folder, + Material, + Scene, + Prefab +}; +static MetaCoreProjectCreateKind PendingCreateKind_ = MetaCoreProjectCreateKind::Folder; +static std::filesystem::path PendingCreateDirectory_{}; +static MetaCoreId PendingCreatePrefabObjectId_ = 0; +static std::array NewAssetNameBuffer_{}; #if defined(_WIN32) static HANDLE PlayerProcessHandle_ = NULL; static HANDLE PlayerStdoutRead_ = NULL; @@ -3553,21 +3573,39 @@ void DrawModelAssetDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD return assetRecord.Type == "model" || assetRecord.Type == "prefab" || assetRecord.Type == "material" || assetRecord.Type == "texture"; } -void MetaCoreBeginProjectAssetDragDropSource(const MetaCoreAssetRecord& assetRecord) { - if (!MetaCoreCanDragProjectAsset(assetRecord) || !ImGui::BeginDragDropSource()) { +void MetaCoreBeginProjectPathDragDropSource( + const std::filesystem::path& relativePath, + bool isDirectory, + const MetaCoreAssetGuid& assetGuid = {}, + MetaCoreAssetStorageKind storageKind = MetaCoreAssetStorageKind::SourceFile, + std::string_view assetType = {} +) { + if (relativePath.empty() || !ImGui::BeginDragDropSource()) { return; } MetaCoreProjectAssetDragDropPayload payload{}; - payload.AssetGuid = assetRecord.Guid; - payload.StorageKind = assetRecord.StorageKind; - std::snprintf(payload.AssetType, sizeof(payload.AssetType), "%s", assetRecord.Type.c_str()); + payload.AssetGuid = assetGuid; + payload.StorageKind = storageKind; + payload.IsDirectory = isDirectory; + std::snprintf(payload.AssetType, sizeof(payload.AssetType), "%s", std::string(assetType).c_str()); + std::snprintf(payload.RelativePath, sizeof(payload.RelativePath), "%s", relativePath.generic_string().c_str()); ImGui::SetDragDropPayload(MetaCoreProjectAssetDragDropPayloadType, &payload, sizeof(payload)); - ImGui::TextUnformatted(assetRecord.RelativePath.filename().string().c_str()); - ImGui::TextDisabled("%s", payload.AssetType); + ImGui::TextUnformatted(relativePath.filename().string().c_str()); + ImGui::TextDisabled("%s", isDirectory ? "文件夹" : (payload.AssetType[0] != '\0' ? payload.AssetType : "file")); ImGui::EndDragDropSource(); } +void MetaCoreBeginProjectAssetDragDropSource(const MetaCoreAssetRecord& assetRecord) { + MetaCoreBeginProjectPathDragDropSource( + assetRecord.RelativePath, + false, + assetRecord.Guid, + assetRecord.StorageKind, + assetRecord.Type + ); +} + } // namespace [[nodiscard]] bool MetaCoreHandleProjectAssetDrop( @@ -3580,7 +3618,7 @@ void MetaCoreBeginProjectAssetDragDropSource(const MetaCoreAssetRecord& assetRec } const auto* assetPayload = static_cast(payload->Data); - if (assetPayload == nullptr || !assetPayload->AssetGuid.IsValid()) { + if (assetPayload == nullptr || assetPayload->IsDirectory || !assetPayload->AssetGuid.IsValid()) { return false; } @@ -3985,7 +4023,13 @@ void DrawMaterialAssetDetails( void MetaCoreDrawAssetInspector(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIPackageService& packageService, MetaCoreIReflectionRegistry& reflectionRegistry) { if (!editorContext.HasSelectedAsset()) return; const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset(); - if (selectedAsset.Type == "model") DrawModelAssetDetails(editorContext, assetDatabaseService); + if (!selectedAsset.Guid.IsValid()) { + ImGui::Separator(); + ImGui::Text("文件: %s", selectedAsset.RelativePath.filename().string().c_str()); + ImGui::TextDisabled("类型: %s", selectedAsset.Type.c_str()); + ImGui::TextDisabled("路径: %s", selectedAsset.RelativePath.generic_string().c_str()); + } + else if (selectedAsset.Type == "model") DrawModelAssetDetails(editorContext, assetDatabaseService); else if (selectedAsset.Type == "prefab") DrawPrefabDetails(editorContext, assetDatabaseService, packageService, reflectionRegistry); else if (selectedAsset.Type == "material") DrawMaterialAssetDetails(editorContext, assetDatabaseService, reflectionRegistry); else if (selectedAsset.Type == "ui_document") DrawUiDocumentDetails(editorContext, assetDatabaseService, packageService, reflectionRegistry); @@ -4032,6 +4076,740 @@ void DrawDirectoryTree( } } +struct MetaCoreProjectFileEntry { + std::filesystem::path RelativePath{}; + bool IsDirectory = false; + std::optional AssetRecord{}; +}; + +[[nodiscard]] bool MetaCorePathBeginsWith(const std::filesystem::path& relativePath, std::string_view rootName) { + const std::filesystem::path normalizedPath = relativePath.lexically_normal(); + const auto iterator = normalizedPath.begin(); + return iterator != normalizedPath.end() && iterator->generic_string() == rootName; +} + +[[nodiscard]] bool MetaCoreIsSameOrChildPath( + const std::filesystem::path& path, + const std::filesystem::path& possibleParent +) { + const std::filesystem::path normalizedPath = path.lexically_normal(); + const std::filesystem::path normalizedParent = possibleParent.lexically_normal(); + if (normalizedPath == normalizedParent) { + return true; + } + const std::string pathString = normalizedPath.generic_string(); + const std::string parentPrefix = normalizedParent.generic_string() + "/"; + return pathString.rfind(parentPrefix, 0) == 0; +} + +[[nodiscard]] bool MetaCoreIsScenesRelativePath(const std::filesystem::path& relativePath) { + return MetaCorePathBeginsWith(relativePath, "Scenes"); +} + +[[nodiscard]] bool MetaCoreIsVisibleProjectRoot(std::string_view rootName) { + return rootName == "Assets" || rootName == "Scenes" || rootName == "Ui" || rootName == "Runtime"; +} + +[[nodiscard]] bool MetaCoreIsHiddenProjectEntry(const std::filesystem::path& path, bool isDirectory) { + const std::string filename = path.filename().string(); + if (filename.empty() || filename == "." || filename == "..") { + return true; + } + if (filename == ".DS_Store" || filename == "imgui.ini") { + return true; + } + if (filename == "Library" || filename == "Build") { + return true; + } + if (!filename.empty() && (filename.front() == '.' || filename.front() == '~' || filename.back() == '~')) { + return true; + } + if (isDirectory) { + return false; + } + const std::string extension = path.extension().string(); + return extension == ".mcmeta" || + extension == ".meta" || + extension == ".mcasset" || + extension == ".tmp" || + extension == ".swp"; +} + +[[nodiscard]] std::string MetaCoreProjectFileTypeLabel(const std::filesystem::path& path) { + const std::string extension = path.extension().string(); + if (extension == ".json") { + const std::string filename = path.filename().string(); + if (filename.ends_with(".mcscene.json")) return "scene"; + if (filename.ends_with(".mcprefab.json")) return "prefab"; + if (filename.ends_with(".mcmaterial.json")) return "material"; + if (filename.ends_with(".mcui.json")) return "ui_document"; + return "json"; + } + if (extension == ".glb" || extension == ".gltf" || extension == ".fbx" || extension == ".obj") return "model"; + if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga" || extension == ".bmp" || extension == ".ppm" || extension == ".ktx") return "texture"; + if (extension == ".wav" || extension == ".ogg" || extension == ".mp3") return "audio"; + if (extension == ".txt" || extension == ".md") return "text"; + return extension.empty() ? "file" : extension.substr(1); +} + +[[nodiscard]] std::string MetaCoreProjectDisplayName(const std::filesystem::path& path, bool isDirectory) { + const std::string filename = path.filename().string(); + if (isDirectory) { + return filename; + } + for (std::string_view suffix : {".mcmaterial.json", ".mcscene.json", ".mcprefab.json", ".mcui.json"}) { + if (filename.size() > suffix.size() && filename.ends_with(suffix)) { + return filename.substr(0, filename.size() - suffix.size()); + } + } + const std::filesystem::path stem = path.stem(); + return stem.empty() ? filename : stem.string(); +} + +[[nodiscard]] const char* MetaCoreProjectChineseTypeLabel(std::string_view type, bool isDirectory) { + if (isDirectory) return "文件夹"; + if (type == "model") return "模型"; + if (type == "prefab") return "Prefab"; + if (type == "material") return "材质"; + if (type == "texture") return "贴图"; + if (type == "scene") return "场景"; + if (type == "ui_document") return "UI"; + if (type == "audio") return "音频"; + if (type == "text") return "文本"; + if (type == "json") return "JSON"; + if (type == "file") return "文件"; + return "资源"; +} + +[[nodiscard]] const char* MetaCoreProjectThumbnailTag(std::string_view type, bool isDirectory) { + if (isDirectory) return "DIR"; + if (type == "model") return "3D"; + if (type == "prefab") return "PRE"; + if (type == "material") return "MAT"; + if (type == "texture") return "IMG"; + if (type == "scene") return "SCN"; + if (type == "ui_document") return "UI"; + if (type == "audio") return "AUD"; + if (type == "text") return "TXT"; + if (type == "json") return "JSON"; + return "FILE"; +} + +[[nodiscard]] ImU32 MetaCoreProjectTypeColor(std::string_view type, bool isDirectory) { + if (isDirectory) return IM_COL32(214, 163, 82, 255); + if (type == "model") return IM_COL32(97, 143, 226, 255); + if (type == "prefab") return IM_COL32(116, 174, 234, 255); + if (type == "material") return IM_COL32(197, 133, 223, 255); + if (type == "texture") return IM_COL32(87, 178, 142, 255); + if (type == "scene") return IM_COL32(101, 195, 205, 255); + if (type == "ui_document") return IM_COL32(238, 164, 91, 255); + if (type == "audio") return IM_COL32(105, 200, 112, 255); + if (type == "text") return IM_COL32(176, 184, 195, 255); + return IM_COL32(142, 151, 166, 255); +} + +struct MetaCoreProjectPreviewColorCacheEntry { + std::uint64_t Mtime = 0; + ImU32 Color = IM_COL32(72, 78, 90, 255); + bool Valid = false; +}; + +static std::unordered_map ProjectPreviewColorCache_{}; + +[[nodiscard]] std::uint64_t MetaCoreProjectFileMtime(const std::filesystem::path& absolutePath) { + std::error_code errorCode; + const auto fileTime = std::filesystem::last_write_time(absolutePath, errorCode); + if (errorCode) { + return 0; + } + const auto raw = std::chrono::duration_cast(fileTime.time_since_epoch()).count(); + std::uint64_t result = 0; + static_assert(sizeof(result) == sizeof(raw)); + std::memcpy(&result, &raw, sizeof(result)); + return result; +} + +[[nodiscard]] ImU32 MetaCoreProjectPreviewColorFromFloats(float r, float g, float b) { + const auto toByte = [](float value) { + return static_cast(std::clamp(value, 0.0F, 1.0F) * 255.0F + 0.5F); + }; + return IM_COL32(toByte(r), toByte(g), toByte(b), 255); +} + +[[nodiscard]] bool MetaCoreProjectReadMaterialPreviewColor( + const std::filesystem::path& absolutePath, + ImU32& outColor +) { + const std::string cacheKey = "mat|" + absolutePath.generic_string(); + const std::uint64_t mtime = MetaCoreProjectFileMtime(absolutePath); + if (const auto it = ProjectPreviewColorCache_.find(cacheKey); + it != ProjectPreviewColorCache_.end() && it->second.Mtime == mtime) { + outColor = it->second.Color; + return it->second.Valid; + } + + MetaCoreProjectPreviewColorCacheEntry cacheEntry; + cacheEntry.Mtime = mtime; + try { + std::ifstream input(absolutePath); + nlohmann::json json; + input >> json; + const nlohmann::json* baseColor = nullptr; + if (json.contains("Material") && json["Material"].contains("BaseColor")) { + baseColor = &json["Material"]["BaseColor"]; + } else if (json.contains("BaseColor")) { + baseColor = &json["BaseColor"]; + } + if (baseColor != nullptr && baseColor->is_array() && baseColor->size() >= 3) { + cacheEntry.Color = MetaCoreProjectPreviewColorFromFloats( + (*baseColor)[0].get(), + (*baseColor)[1].get(), + (*baseColor)[2].get() + ); + cacheEntry.Valid = true; + } + } catch (...) { + cacheEntry.Valid = false; + } + + outColor = cacheEntry.Color; + ProjectPreviewColorCache_[cacheKey] = cacheEntry; + return cacheEntry.Valid; +} + +[[nodiscard]] bool MetaCoreProjectReadTexturePreviewColor( + const std::filesystem::path& absolutePath, + ImU32& outColor +) { + const std::string cacheKey = "tex|" + absolutePath.generic_string(); + const std::uint64_t mtime = MetaCoreProjectFileMtime(absolutePath); + if (const auto it = ProjectPreviewColorCache_.find(cacheKey); + it != ProjectPreviewColorCache_.end() && it->second.Mtime == mtime) { + outColor = it->second.Color; + return it->second.Valid; + } + + MetaCoreProjectPreviewColorCacheEntry cacheEntry; + cacheEntry.Mtime = mtime; + int width = 0; + int height = 0; + int channels = 0; + unsigned char* pixels = stbi_load(absolutePath.string().c_str(), &width, &height, &channels, 4); + if (pixels != nullptr && width > 0 && height > 0) { + std::uint64_t red = 0; + std::uint64_t green = 0; + std::uint64_t blue = 0; + std::uint64_t count = 0; + const int stepX = std::max(width / 32, 1); + const int stepY = std::max(height / 32, 1); + for (int y = 0; y < height; y += stepY) { + for (int x = 0; x < width; x += stepX) { + const unsigned char* pixel = pixels + (static_cast(y) * static_cast(width) + static_cast(x)) * 4U; + red += pixel[0]; + green += pixel[1]; + blue += pixel[2]; + ++count; + } + } + if (count > 0) { + cacheEntry.Color = IM_COL32( + static_cast(red / count), + static_cast(green / count), + static_cast(blue / count), + 255 + ); + cacheEntry.Valid = true; + } + } + if (pixels != nullptr) { + stbi_image_free(pixels); + } + + outColor = cacheEntry.Color; + ProjectPreviewColorCache_[cacheKey] = cacheEntry; + return cacheEntry.Valid; +} + +[[nodiscard]] std::string MetaCoreProjectEllipsize(std::string text, float maxWidth) { + if (ImGui::CalcTextSize(text.c_str()).x <= maxWidth) { + return text; + } + static constexpr const char* kEllipsis = "..."; + while (text.size() > 1) { + text.pop_back(); + const std::string candidate = text + kEllipsis; + if (ImGui::CalcTextSize(candidate.c_str()).x <= maxWidth) { + return candidate; + } + } + return kEllipsis; +} + +void MetaCoreDrawFolderThumbnail(ImDrawList& drawList, const ImVec2& min, const ImVec2& max, ImU32 color) { + const float width = max.x - min.x; + const float height = max.y - min.y; + const ImVec2 tabMin(min.x + width * 0.10F, min.y + height * 0.20F); + const ImVec2 tabMax(min.x + width * 0.46F, min.y + height * 0.36F); + const ImVec2 bodyMin(min.x + width * 0.08F, min.y + height * 0.30F); + const ImVec2 bodyMax(min.x + width * 0.92F, min.y + height * 0.78F); + drawList.AddRectFilled(tabMin, tabMax, color, 7.0F, ImDrawFlags_RoundCornersTop); + drawList.AddRectFilled(bodyMin, bodyMax, color, 8.0F); + drawList.AddRect(bodyMin, bodyMax, IM_COL32(255, 255, 255, 42), 8.0F); +} + +void MetaCoreDrawProjectThumbnail( + const MetaCoreProjectFileEntry& entry, + const MetaCoreProjectDescriptor& project, + std::string_view type, + const ImVec2& min, + const ImVec2& max +) { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + if (drawList == nullptr) { + return; + } + + const ImU32 baseColor = MetaCoreProjectTypeColor(type, entry.IsDirectory); + ImU32 previewColor = baseColor; + bool hasContentColor = false; + if (!entry.IsDirectory) { + const std::filesystem::path absolutePath = project.RootPath / entry.RelativePath; + if (type == "material") { + hasContentColor = MetaCoreProjectReadMaterialPreviewColor(absolutePath, previewColor); + } else if (type == "texture") { + hasContentColor = MetaCoreProjectReadTexturePreviewColor(absolutePath, previewColor); + } + } + + drawList->AddRectFilled(min, max, IM_COL32(31, 35, 43, 255), 10.0F); + if (entry.IsDirectory) { + MetaCoreDrawFolderThumbnail(*drawList, min, max, baseColor); + return; + } + + const ImVec2 innerMin(min.x + 7.0F, min.y + 7.0F); + const ImVec2 innerMax(max.x - 7.0F, max.y - 7.0F); + if (hasContentColor) { + drawList->AddRectFilled(innerMin, innerMax, previewColor, 8.0F); + drawList->AddRectFilled( + ImVec2(innerMin.x, innerMin.y), + ImVec2(innerMax.x, innerMin.y + (innerMax.y - innerMin.y) * 0.34F), + IM_COL32(255, 255, 255, 34), + 8.0F, + ImDrawFlags_RoundCornersTop + ); + } else { + drawList->AddRectFilled(innerMin, innerMax, baseColor, 8.0F); + } + drawList->AddRect(innerMin, innerMax, IM_COL32(255, 255, 255, 44), 8.0F); + + const char* tag = MetaCoreProjectThumbnailTag(type, false); + const ImVec2 tagSize = ImGui::CalcTextSize(tag); + drawList->AddText( + ImVec2( + innerMin.x + ((innerMax.x - innerMin.x) - tagSize.x) * 0.5F, + innerMin.y + ((innerMax.y - innerMin.y) - tagSize.y) * 0.5F + ), + IM_COL32(255, 255, 255, 235), + tag + ); +} + +void MetaCoreDrawProjectBreadcrumb( + MetaCoreEditorContext& editorContext, + const std::filesystem::path& selectedDirectory +) { + ImGui::TextDisabled("位置:"); + ImGui::SameLine(); + std::filesystem::path current; + int index = 0; + for (const auto& part : selectedDirectory.lexically_normal()) { + if (part.empty() || part == ".") { + continue; + } + current /= part; + if (index > 0) { + ImGui::SameLine(); + ImGui::TextDisabled(">"); + ImGui::SameLine(); + } + const std::string label = part.string(); + if (ImGui::SmallButton(label.c_str())) { + editorContext.SetSelectedProjectDirectory(current); + editorContext.ClearSelectedAsset(); + editorContext.ClearSelectedAssetSubId(); + } + ++index; + } +} + +[[nodiscard]] std::vector MetaCoreBuildProjectFileEntries( + MetaCoreIAssetDatabaseService& assetDatabaseService, + const std::filesystem::path& relativeDirectory +) { + std::vector entries; + const auto& project = assetDatabaseService.GetProjectDescriptor(); + const std::filesystem::path normalizedDirectory = relativeDirectory.lexically_normal(); + const std::filesystem::path absoluteDirectory = project.RootPath / normalizedDirectory; + std::error_code errorCode; + if (!std::filesystem::exists(absoluteDirectory, errorCode) || !std::filesystem::is_directory(absoluteDirectory, errorCode)) { + return entries; + } + + for (const auto& entry : std::filesystem::directory_iterator(absoluteDirectory, errorCode)) { + if (errorCode) { + break; + } + const bool isDirectory = entry.is_directory(errorCode); + if (errorCode) { + errorCode.clear(); + continue; + } + if (MetaCoreIsHiddenProjectEntry(entry.path(), isDirectory)) { + continue; + } + + const std::filesystem::path relativePath = entry.path().lexically_relative(project.RootPath).lexically_normal(); + MetaCoreProjectFileEntry fileEntry; + fileEntry.RelativePath = relativePath; + fileEntry.IsDirectory = isDirectory; + if (!isDirectory) { + fileEntry.AssetRecord = assetDatabaseService.FindAssetByRelativePath(relativePath); + } + entries.push_back(std::move(fileEntry)); + } + + std::sort(entries.begin(), entries.end(), [](const auto& lhs, const auto& rhs) { + if (lhs.IsDirectory != rhs.IsDirectory) { + return lhs.IsDirectory; + } + return lhs.RelativePath.filename().string() < rhs.RelativePath.filename().string(); + }); + return entries; +} + +[[nodiscard]] std::string MetaCoreShellQuote(const std::filesystem::path& path) { + std::string value = path.string(); + std::string quoted = "'"; + for (char character : value) { + if (character == '\'') { + quoted += "'\\''"; + } else { + quoted += character; + } + } + quoted += "'"; + return quoted; +} + +void MetaCoreOpenPathExternally(const std::filesystem::path& absolutePath) { +#if defined(_WIN32) + (void)ShellExecuteW(nullptr, L"open", absolutePath.wstring().c_str(), nullptr, nullptr, SW_SHOWNORMAL); +#elif defined(__APPLE__) + const std::string command = "open " + MetaCoreShellQuote(absolutePath) + " >/dev/null 2>&1 &"; + (void)std::system(command.c_str()); +#else + const std::string command = "xdg-open " + MetaCoreShellQuote(absolutePath) + " >/dev/null 2>&1 &"; + (void)std::system(command.c_str()); +#endif +} + +void MetaCoreRevealPathInFileManager(const std::filesystem::path& absolutePath) { +#if defined(_WIN32) + const std::wstring argument = L"/select,\"" + absolutePath.wstring() + L"\""; + (void)ShellExecuteW(nullptr, L"open", L"explorer.exe", argument.c_str(), nullptr, SW_SHOWNORMAL); +#elif defined(__APPLE__) + const std::string command = "open -R " + MetaCoreShellQuote(absolutePath) + " >/dev/null 2>&1 &"; + (void)std::system(command.c_str()); +#else + const std::filesystem::path target = std::filesystem::is_regular_file(absolutePath) ? absolutePath.parent_path() : absolutePath; + const std::string command = "xdg-open " + MetaCoreShellQuote(target) + " >/dev/null 2>&1 &"; + (void)std::system(command.c_str()); +#endif +} + +[[nodiscard]] bool MetaCoreIsSafeProjectItemName(std::string_view name) { + if (name.empty() || name == "." || name == "..") { + return false; + } + return name.find('/') == std::string_view::npos && name.find('\\') == std::string_view::npos; +} + +[[nodiscard]] std::string MetaCoreStripKnownSuffix(std::string name, std::string_view suffix) { + if (name.size() >= suffix.size() && name.ends_with(suffix)) { + name.resize(name.size() - suffix.size()); + } + return name; +} + +[[nodiscard]] std::string MetaCoreSanitizeProjectBaseName(std::string name, std::string_view fallback) { + if (name.empty()) { + name = std::string(fallback); + } + for (char& character : name) { + if (character == '/' || character == '\\' || character == ':' || character == '*' || + character == '?' || character == '"' || character == '<' || character == '>' || character == '|') { + character = '_'; + } + } + if (name.empty() || name == "." || name == "..") { + return std::string(fallback); + } + return name; +} + +[[nodiscard]] std::filesystem::path MetaCoreBuildUniqueProjectPath( + const MetaCoreProjectDescriptor& project, + const std::filesystem::path& relativeDirectory, + std::string baseName, + std::string_view extension +) { + baseName = MetaCoreSanitizeProjectBaseName(std::move(baseName), "NewAsset"); + std::filesystem::path candidate = (relativeDirectory / (baseName + std::string(extension))).lexically_normal(); + int index = 1; + while (std::filesystem::exists(project.RootPath / candidate)) { + candidate = (relativeDirectory / (baseName + " " + std::to_string(index) + std::string(extension))).lexically_normal(); + ++index; + } + return candidate; +} + +[[nodiscard]] std::filesystem::path MetaCoreDefaultCreateDirectory( + MetaCoreProjectCreateKind kind, + const std::filesystem::path& selectedDirectory +) { + switch (kind) { + case MetaCoreProjectCreateKind::Material: + return MetaCoreIsAssetsRelativePath(selectedDirectory) ? selectedDirectory : std::filesystem::path("Assets") / "Materials"; + case MetaCoreProjectCreateKind::Scene: + return MetaCoreIsScenesRelativePath(selectedDirectory) ? selectedDirectory : std::filesystem::path("Scenes"); + case MetaCoreProjectCreateKind::Prefab: + return MetaCoreIsAssetsRelativePath(selectedDirectory) ? selectedDirectory : std::filesystem::path("Assets") / "Prefabs"; + case MetaCoreProjectCreateKind::Folder: + default: + return selectedDirectory.empty() ? std::filesystem::path("Assets") : selectedDirectory; + } +} + +[[nodiscard]] std::filesystem::path MetaCoreBuildUniqueMoveTarget( + const MetaCoreProjectDescriptor& project, + const std::filesystem::path& sourceRelativePath, + const std::filesystem::path& targetDirectory +) { + std::filesystem::path desired = (targetDirectory / sourceRelativePath.filename()).lexically_normal(); + if (!std::filesystem::exists(project.RootPath / desired)) { + return desired; + } + + const bool sourceIsDirectory = std::filesystem::is_directory(project.RootPath / sourceRelativePath); + std::string baseName = sourceIsDirectory ? sourceRelativePath.filename().string() : sourceRelativePath.stem().string(); + std::string extension = sourceIsDirectory ? std::string{} : sourceRelativePath.extension().string(); + int index = 1; + while (std::filesystem::exists(project.RootPath / desired)) { + desired = (targetDirectory / (baseName + " " + std::to_string(index) + extension)).lexically_normal(); + ++index; + } + return desired; +} + +void MetaCoreImportExternalPathsToProjectDirectory( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + const std::vector& sourcePaths, + const std::filesystem::path& targetRelativeDirectory +) { + if (sourcePaths.empty()) { + return; + } + if (!MetaCoreIsAssetsRelativePath(targetRelativeDirectory)) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "Asset", "外部资源只能拖入 Assets 目录"); + return; + } + + const auto& project = assetDatabaseService.GetProjectDescriptor(); + const std::filesystem::path absoluteTargetDirectory = project.RootPath / targetRelativeDirectory.lexically_normal(); + std::error_code errorCode; + std::filesystem::create_directories(absoluteTargetDirectory, errorCode); + if (errorCode) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Asset", "导入失败:无法创建目标目录"); + return; + } + + int importedCount = 0; + for (const std::filesystem::path& sourcePath : sourcePaths) { + if (sourcePath.empty()) { + continue; + } + const std::filesystem::path absoluteSourcePath = sourcePath.is_absolute() + ? sourcePath.lexically_normal() + : (std::filesystem::current_path() / sourcePath).lexically_normal(); + if (!std::filesystem::exists(absoluteSourcePath, errorCode)) { + errorCode.clear(); + continue; + } + + const std::filesystem::path absoluteTargetPath = absoluteTargetDirectory / absoluteSourcePath.filename(); + if (std::filesystem::equivalent(absoluteSourcePath, absoluteTargetPath, errorCode)) { + errorCode.clear(); + continue; + } + + errorCode.clear(); + if (std::filesystem::is_directory(absoluteSourcePath, errorCode)) { + std::filesystem::copy( + absoluteSourcePath, + absoluteTargetPath, + std::filesystem::copy_options::recursive | std::filesystem::copy_options::overwrite_existing, + errorCode + ); + } else { + std::filesystem::copy_file( + absoluteSourcePath, + absoluteTargetPath, + std::filesystem::copy_options::overwrite_existing, + errorCode + ); + } + if (!errorCode) { + ++importedCount; + } else { + errorCode.clear(); + } + } + + if (importedCount <= 0) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "Asset", "没有可导入的外部文件"); + return; + } + + (void)assetDatabaseService.Refresh(); + if (const auto hotReloadService = editorContext.GetModuleRegistry().ResolveService(); + hotReloadService != nullptr) { + hotReloadService->RescanBaseline(); + } + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Asset", "已导入外部资源: " + std::to_string(importedCount)); +} + +void MetaCoreCreatePrefabFromObjectInDirectory( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + MetaCoreId objectId, + const std::filesystem::path& requestedDirectory +) { + const auto prefabService = editorContext.GetModuleRegistry().ResolveService(); + if (prefabService == nullptr || !prefabService->SupportsPrefabWorkflows()) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Prefab", "Prefab 服务不可用"); + return; + } + + MetaCoreGameObject sourceObject = editorContext.GetScene().FindGameObject(objectId); + if (!sourceObject) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "Prefab", "未找到要保存为 Prefab 的对象"); + return; + } + + const std::filesystem::path targetDirectory = MetaCoreDefaultCreateDirectory(MetaCoreProjectCreateKind::Prefab, requestedDirectory); + const std::filesystem::path prefabPath = MetaCoreBuildUniqueProjectPath( + assetDatabaseService.GetProjectDescriptor(), + targetDirectory, + sourceObject.GetName(), + ".mcprefab.json" + ); + (void)prefabService->CreatePrefabFromObject(editorContext, objectId, prefabPath); +} + +void MetaCoreHandleProjectDirectoryDrop( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + const std::filesystem::path& targetDirectory +) { + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(MetaCoreProjectAssetDragDropPayloadType); + payload != nullptr && payload->DataSize == sizeof(MetaCoreProjectAssetDragDropPayload)) { + const auto* projectPayload = static_cast(payload->Data); + if (projectPayload != nullptr && projectPayload->RelativePath[0] != '\0') { + const std::filesystem::path sourcePath = std::filesystem::path(projectPayload->RelativePath).lexically_normal(); + const std::filesystem::path normalizedTargetDirectory = targetDirectory.lexically_normal(); + if (sourcePath != normalizedTargetDirectory && sourcePath.parent_path() != normalizedTargetDirectory) { + const std::filesystem::path targetPath = MetaCoreBuildUniqueMoveTarget( + assetDatabaseService.GetProjectDescriptor(), + sourcePath, + normalizedTargetDirectory + ); + if (!assetDatabaseService.MovePathTo(sourcePath, targetPath)) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Assets", "移动失败"); + } else { + editorContext.SetSelectedProjectDirectory(normalizedTargetDirectory); + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Assets", "已移动到: " + targetPath.generic_string()); + } + } + } + } + + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("MC_HIERARCHY_OBJECT"); + payload != nullptr && payload->DataSize == sizeof(MetaCoreId)) { + const auto objectId = *static_cast(payload->Data); + MetaCoreCreatePrefabFromObjectInDirectory(editorContext, assetDatabaseService, objectId, targetDirectory); + } + ImGui::EndDragDropTarget(); + } +} + +void MetaCoreDrawProjectDirectoryTree( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + const std::filesystem::path& relativeDirectory +) { + const auto& project = assetDatabaseService.GetProjectDescriptor(); + const std::filesystem::path normalizedDirectory = relativeDirectory.lexically_normal(); + const std::filesystem::path absoluteDirectory = project.RootPath / normalizedDirectory; + std::error_code errorCode; + if (!std::filesystem::exists(absoluteDirectory, errorCode) || !std::filesystem::is_directory(absoluteDirectory, errorCode)) { + return; + } + + const std::string label = normalizedDirectory.filename().empty() + ? normalizedDirectory.generic_string() + : normalizedDirectory.filename().string(); + const bool selected = editorContext.GetSelectedProjectDirectory() == normalizedDirectory; + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth; + if (selected) { + flags |= ImGuiTreeNodeFlags_Selected; + } + + const bool open = ImGui::TreeNodeEx( + normalizedDirectory.generic_string().c_str(), + flags, + "%s %s", + selected ? ">" : "#", + label.c_str() + ); + if (ImGui::IsItemClicked()) { + editorContext.SetSelectedProjectDirectory(normalizedDirectory); + editorContext.ClearSelectedAsset(); + editorContext.ClearSelectedAssetSubId(); + } + MetaCoreHandleProjectDirectoryDrop(editorContext, assetDatabaseService, normalizedDirectory); + if (open) { + for (const auto& entry : std::filesystem::directory_iterator(absoluteDirectory, errorCode)) { + if (errorCode) { + break; + } + const bool isDirectory = entry.is_directory(errorCode); + if (errorCode) { + errorCode.clear(); + continue; + } + if (!isDirectory || MetaCoreIsHiddenProjectEntry(entry.path(), true)) { + continue; + } + const std::filesystem::path childRelativePath = entry.path().lexically_relative(project.RootPath).lexically_normal(); + MetaCoreDrawProjectDirectoryTree(editorContext, assetDatabaseService, childRelativePath); + } + ImGui::TreePop(); + } +} + class MetaCoreProjectPanelProvider final : public MetaCoreIEditorPanelProvider { public: std::string GetPanelId() const override { return "Project"; } @@ -4040,141 +4818,342 @@ public: void DrawPanel(MetaCoreEditorContext& editorContext) override { const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); - const auto packageService = editorContext.GetModuleRegistry().ResolveService(); - const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); if (!assetDatabaseService || !assetDatabaseService->HasProject()) return; - if (editorContext.GetSelectedProjectDirectory().empty()) editorContext.SetSelectedProjectDirectory("Assets"); + + const auto& project = assetDatabaseService->GetProjectDescriptor(); + if (editorContext.GetSelectedProjectDirectory().empty() || + !std::filesystem::exists(project.RootPath / editorContext.GetSelectedProjectDirectory())) { + editorContext.SetSelectedProjectDirectory("Assets"); + } + bool openRenamePopup = false; + bool openCreatePopup = false; + bool openDeletePopup = false; - ImGui::TextDisabled("%s | %s", assetDatabaseService->GetProjectDescriptor().Name.c_str(), assetDatabaseService->GetProjectDescriptor().RootPath.string().c_str()); - if (ImGui::Button("刷新")) { MetaCoreHandleReloadAssets(editorContext); assetDatabaseService->Refresh(); } - ImGui::InputTextWithHint("##Filter", "过滤资源...", AssetFilterBuffer_.data(), AssetFilterBuffer_.size()); - - ImGui::Columns(2, "ProjectCols", true); - DrawDirectoryTree(editorContext, *assetDatabaseService, "Assets", "Assets"); - DrawDirectoryTree(editorContext, *assetDatabaseService, "Scenes", "Scenes"); - DrawDirectoryTree(editorContext, *assetDatabaseService, "Ui", "Ui"); - DrawDirectoryTree(editorContext, *assetDatabaseService, "Runtime", "Runtime"); - ImGui::NextColumn(); - - const std::filesystem::path selectedDirectory = editorContext.GetSelectedProjectDirectory(); - ImGui::TextDisabled("目录: %s", selectedDirectory.generic_string().c_str()); - ImGui::Separator(); - for (const auto& d : assetDatabaseService->GetDirectoriesUnder(selectedDirectory)) { - if (MatchesAssetFilter(d.filename().string()) && ImGui::Selectable(("[Dir] " + d.filename().string()).c_str(), selectedDirectory == d)) { - editorContext.SetSelectedProjectDirectory(d); + const auto openCreate = [&](MetaCoreProjectCreateKind kind, std::filesystem::path directory, MetaCoreId prefabObjectId = 0) { + PendingCreateKind_ = kind; + PendingCreateDirectory_ = MetaCoreDefaultCreateDirectory(kind, directory); + PendingCreatePrefabObjectId_ = prefabObjectId; + const char* defaultName = "NewFolder"; + if (kind == MetaCoreProjectCreateKind::Material) defaultName = "NewMaterial"; + if (kind == MetaCoreProjectCreateKind::Scene) defaultName = "NewScene"; + if (kind == MetaCoreProjectCreateKind::Prefab) defaultName = "NewPrefab"; + if (kind == MetaCoreProjectCreateKind::Prefab && prefabObjectId != 0) { + if (MetaCoreGameObject object = editorContext.GetScene().FindGameObject(prefabObjectId)) { + std::snprintf(NewAssetNameBuffer_.data(), NewAssetNameBuffer_.size(), "%s", object.GetName().c_str()); + } else { + std::snprintf(NewAssetNameBuffer_.data(), NewAssetNameBuffer_.size(), "%s", defaultName); + } + } else { + std::snprintf(NewAssetNameBuffer_.data(), NewAssetNameBuffer_.size(), "%s", defaultName); } - if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) { - if (ImGui::MenuItem("重命名...")) { - PendingRenamePath_ = d; - std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", d.filename().string().c_str()); - openRenamePopup = true; - } - if (ImGui::MenuItem("删除")) { - assetDatabaseService->DeletePath(d); - } - ImGui::EndPopup(); + openCreatePopup = true; + }; + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(7.0F, 4.0F)); + ImGui::TextUnformatted(project.Name.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled("%s", project.RootPath.string().c_str()); + ImGui::SameLine(); + if (ImGui::Button("刷新")) { + MetaCoreHandleReloadAssets(editorContext); + assetDatabaseService->Refresh(); + } + ImGui::SameLine(); + if (ImGui::Button("新建")) { + ImGui::OpenPopup("ProjectCreateQuickMenu"); + } + if (ImGui::BeginPopup("ProjectCreateQuickMenu")) { + const std::filesystem::path selectedDirectory = editorContext.GetSelectedProjectDirectory().lexically_normal(); + if (ImGui::MenuItem("文件夹")) openCreate(MetaCoreProjectCreateKind::Folder, selectedDirectory); + if (ImGui::MenuItem("材质")) openCreate(MetaCoreProjectCreateKind::Material, selectedDirectory); + if (ImGui::MenuItem("场景")) openCreate(MetaCoreProjectCreateKind::Scene, selectedDirectory); + if (ImGui::MenuItem("从选中对象创建 Prefab", nullptr, false, editorContext.GetActiveObjectId() != 0)) { + openCreate(MetaCoreProjectCreateKind::Prefab, selectedDirectory, editorContext.GetActiveObjectId()); + } + ImGui::EndPopup(); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(118.0F); + ImGui::SliderFloat("缩略图", &ProjectThumbnailSize_, 56.0F, 124.0F, "%.0f"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(240.0F); + ImGui::InputTextWithHint("##Filter", "过滤资源...", AssetFilterBuffer_.data(), AssetFilterBuffer_.size()); + ImGui::PopStyleVar(); + + const std::filesystem::path selectedDirectory = editorContext.GetSelectedProjectDirectory().lexically_normal(); + if (ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows)) { + const std::vector droppedFiles = editorContext.GetWindow().ConsumeDroppedFiles(); + if (!droppedFiles.empty()) { + MetaCoreImportExternalPathsToProjectDirectory(editorContext, *assetDatabaseService, droppedFiles, selectedDirectory); } } - const MetaCoreAssetGuid selectedAssetGuid = editorContext.GetSelectedAsset().Guid; - for (const auto& a : assetDatabaseService->GetAssetsUnder(selectedDirectory)) { - if (!MatchesAssetFilter(a.RelativePath.filename().string()) && !MatchesAssetFilter(a.Type)) continue; - const std::string label = a.RelativePath.filename().string() + " [" + a.Type + "]"; - const bool selected = selectedAssetGuid == a.Guid; - if (ImGui::Selectable(label.c_str(), selected)) { + + ImGui::Separator(); + const float panelHeight = std::max(ImGui::GetContentRegionAvail().y, 120.0F); + const float sidebarWidth = std::min(235.0F, std::max(170.0F, ImGui::GetContentRegionAvail().x * 0.30F)); + ImGui::BeginChild("ProjectDirectorySidebar", ImVec2(sidebarWidth, panelHeight), true); + ImGui::TextDisabled("目录"); + ImGui::Separator(); + for (const std::filesystem::path root : {std::filesystem::path("Assets"), std::filesystem::path("Scenes"), std::filesystem::path("Ui"), std::filesystem::path("Runtime")}) { + MetaCoreDrawProjectDirectoryTree(editorContext, *assetDatabaseService, root); + } + ImGui::EndChild(); + ImGui::SameLine(); + + ImGui::BeginChild("ProjectContentArea", ImVec2(0.0F, panelHeight), true); + MetaCoreDrawProjectBreadcrumb(editorContext, selectedDirectory); + ImGui::SameLine(); + ImGui::TextDisabled("外部拖入: %s", MetaCoreIsAssetsRelativePath(selectedDirectory) ? "允许导入" : "仅 Assets"); + ImGui::Separator(); + + const std::vector entries = MetaCoreBuildProjectFileEntries(*assetDatabaseService, selectedDirectory); + const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset(); + const float thumbnailSize = std::clamp(ProjectThumbnailSize_, 56.0F, 124.0F); + const float cardWidth = thumbnailSize + 34.0F; + const float cardHeight = thumbnailSize + ImGui::GetTextLineHeightWithSpacing() * 2.0F + 24.0F; + const float availableWidth = std::max(ImGui::GetContentRegionAvail().x, cardWidth); + const int columnCount = std::max(1, static_cast(availableWidth / cardWidth)); + bool hasVisibleEntries = false; + + ImGui::Columns(columnCount, "ProjectThumbnailGrid", false); + for (const MetaCoreProjectFileEntry& entry : entries) { + const std::string name = entry.RelativePath.filename().string(); + const std::string type = entry.IsDirectory + ? std::string("folder") + : (entry.AssetRecord.has_value() ? entry.AssetRecord->Type : MetaCoreProjectFileTypeLabel(entry.RelativePath)); + const std::string displayName = MetaCoreProjectDisplayName(entry.RelativePath, entry.IsDirectory); + const std::string chineseType = MetaCoreProjectChineseTypeLabel(type, entry.IsDirectory); + if (!MatchesAssetFilter(name) && !MatchesAssetFilter(displayName) && !MatchesAssetFilter(type) && !MatchesAssetFilter(chineseType)) { + continue; + } + hasVisibleEntries = true; + + ImGui::PushID(entry.RelativePath.generic_string().c_str()); + const bool entrySelected = + !entry.IsDirectory && + selectedAsset.RelativePath.lexically_normal() == entry.RelativePath.lexically_normal() && + editorContext.GetSelectedAssetSubId().empty(); + const ImVec2 cardMin = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("##ProjectAssetCard", ImVec2(cardWidth, cardHeight)); + const bool hovered = ImGui::IsItemHovered(); + const bool leftClicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); + const bool doubleClicked = hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left); + + if (leftClicked && !entry.IsDirectory) { editorContext.SelectAsset(MetaCoreSelectedAssetState{ - a.Guid, - a.RelativePath, - a.Type, - a.StorageKind + entry.AssetRecord.has_value() ? entry.AssetRecord->Guid : MetaCoreAssetGuid{}, + entry.RelativePath, + type, + entry.AssetRecord.has_value() ? entry.AssetRecord->StorageKind : MetaCoreAssetStorageKind::SourceFile }); editorContext.ClearSelectedAssetSubId(); editorContext.ClearSelection(); - if (ImGui::IsMouseDoubleClicked(0)) { - if (a.Type == "scene" && scenePersistenceService) (void)scenePersistenceService->LoadScene(editorContext, a.RelativePath); - else if (a.Type == "model") (void)MetaCoreInstantiateModelAsset(editorContext, a.Guid, std::nullopt); - else if (a.Type == "prefab") (void)MetaCoreInstantiatePrefab(editorContext, a.Guid, std::nullopt); + } else if (leftClicked && entry.IsDirectory) { + editorContext.ClearSelectedAsset(); + editorContext.ClearSelectedAssetSubId(); + } + + if (doubleClicked) { + if (entry.IsDirectory) { + editorContext.SetSelectedProjectDirectory(entry.RelativePath); + editorContext.ClearSelectedAsset(); + editorContext.ClearSelectedAssetSubId(); + } else if (entry.AssetRecord.has_value() && entry.AssetRecord->Type == "scene" && scenePersistenceService) { + (void)scenePersistenceService->LoadScene(editorContext, entry.RelativePath); + } else if (entry.AssetRecord.has_value() && entry.AssetRecord->Type == "model") { + (void)MetaCoreInstantiateModelAsset(editorContext, entry.AssetRecord->Guid, std::nullopt); + } else if (entry.AssetRecord.has_value() && entry.AssetRecord->Type == "prefab") { + (void)MetaCoreInstantiatePrefab(editorContext, entry.AssetRecord->Guid, std::nullopt); + } else { + MetaCoreOpenPathExternally(project.RootPath / entry.RelativePath); } } - MetaCoreBeginProjectAssetDragDropSource(a); - if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) { + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const ImVec2 cardMax(cardMin.x + cardWidth, cardMin.y + cardHeight); + const ImU32 cardBackground = entrySelected + ? IM_COL32(48, 74, 112, 255) + : (hovered ? IM_COL32(43, 48, 58, 255) : IM_COL32(29, 32, 39, 255)); + drawList->AddRectFilled(cardMin, cardMax, cardBackground, 9.0F); + drawList->AddRect(cardMin, cardMax, entrySelected ? IM_COL32(104, 157, 255, 255) : IM_COL32(255, 255, 255, hovered ? 40 : 18), 9.0F, 0, entrySelected ? 2.0F : 1.0F); + + const float thumbnailX = cardMin.x + (cardWidth - thumbnailSize) * 0.5F; + const ImVec2 thumbnailMin(thumbnailX, cardMin.y + 10.0F); + const ImVec2 thumbnailMax(thumbnailX + thumbnailSize, cardMin.y + 10.0F + thumbnailSize); + MetaCoreDrawProjectThumbnail(entry, project, type, thumbnailMin, thumbnailMax); + + const std::string clippedName = MetaCoreProjectEllipsize(displayName, cardWidth - 14.0F); + const std::string clippedType = MetaCoreProjectEllipsize(chineseType, cardWidth - 14.0F); + const ImVec2 nameSize = ImGui::CalcTextSize(clippedName.c_str()); + const ImVec2 typeSize = ImGui::CalcTextSize(clippedType.c_str()); + drawList->AddText( + ImVec2(cardMin.x + (cardWidth - nameSize.x) * 0.5F, thumbnailMax.y + 8.0F), + IM_COL32(231, 235, 242, 255), + clippedName.c_str() + ); + drawList->AddText( + ImVec2(cardMin.x + (cardWidth - typeSize.x) * 0.5F, thumbnailMax.y + 8.0F + ImGui::GetTextLineHeight()), + IM_COL32(153, 162, 176, 255), + clippedType.c_str() + ); + + MetaCoreBeginProjectPathDragDropSource( + entry.RelativePath, + entry.IsDirectory, + entry.AssetRecord.has_value() ? entry.AssetRecord->Guid : MetaCoreAssetGuid{}, + entry.AssetRecord.has_value() ? entry.AssetRecord->StorageKind : MetaCoreAssetStorageKind::SourceFile, + entry.AssetRecord.has_value() ? entry.AssetRecord->Type : type + ); + if (entry.IsDirectory) { + MetaCoreHandleProjectDirectoryDrop(editorContext, *assetDatabaseService, entry.RelativePath); + } + + if (ImGui::BeginPopupContextItem("ProjectEntryContextMenu", ImGuiPopupFlags_MouseButtonRight)) { + if (entry.IsDirectory && ImGui::MenuItem("打开文件夹")) { + editorContext.SetSelectedProjectDirectory(entry.RelativePath); + editorContext.ClearSelectedAsset(); + editorContext.ClearSelectedAssetSubId(); + } + if (!entry.IsDirectory && ImGui::MenuItem("打开外部程序")) { + MetaCoreOpenPathExternally(project.RootPath / entry.RelativePath); + } + if (ImGui::MenuItem("在文件管理器中显示")) { + MetaCoreRevealPathInFileManager(project.RootPath / entry.RelativePath); + } + ImGui::Separator(); if (ImGui::MenuItem("重命名...")) { - PendingRenamePath_ = a.RelativePath; - std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", a.RelativePath.filename().string().c_str()); + PendingRenamePath_ = entry.RelativePath; + std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", entry.RelativePath.filename().string().c_str()); openRenamePopup = true; } - if (ImGui::MenuItem("删除")) { - assetDatabaseService->DeletePath(a.RelativePath); + if (ImGui::MenuItem("删除...")) { + PendingDeletePath_ = entry.RelativePath; + PendingDeleteIsDirectory_ = entry.IsDirectory; + openDeletePopup = true; + } + if (entry.IsDirectory) { + ImGui::Separator(); + if (ImGui::MenuItem("新建文件夹")) openCreate(MetaCoreProjectCreateKind::Folder, entry.RelativePath); + if (ImGui::MenuItem("新建材质")) openCreate(MetaCoreProjectCreateKind::Material, entry.RelativePath); + if (ImGui::MenuItem("新建场景")) openCreate(MetaCoreProjectCreateKind::Scene, entry.RelativePath); + if (ImGui::MenuItem("从选中对象创建 Prefab", nullptr, false, editorContext.GetActiveObjectId() != 0)) { + openCreate(MetaCoreProjectCreateKind::Prefab, entry.RelativePath, editorContext.GetActiveObjectId()); + } } ImGui::EndPopup(); } - - if (selected && a.Type == "model") { - const auto modelDocument = MetaCoreLoadModelAssetDocument(editorContext, a); - if (modelDocument.has_value()) { - ImGui::Indent(18.0F); - for (const auto& generatedEntry : BuildGeneratedResourceListEntries(*modelDocument)) { - if (!MatchesGeneratedResourceKindFilter(generatedEntry.Kind)) { - continue; - } - const std::string filterLabel = generatedEntry.Name + " " + generatedEntry.SecondaryText; - if (!MatchesGeneratedResourceFilter(filterLabel)) { - continue; - } - - const std::string subId = MetaCoreBuildGeneratedResourceSubId( - generatedEntry.Kind, - generatedEntry.AssetGuid - ); - const bool generatedSelected = - editorContext.GetSelectedAsset().Guid == a.Guid && - editorContext.GetSelectedAssetSubId() == subId; - const std::string generatedLabel = - "↳ " + generatedEntry.Name + " [" + generatedEntry.SecondaryText + "]"; - if (ImGui::Selectable(generatedLabel.c_str(), generatedSelected)) { - editorContext.SelectAsset(MetaCoreSelectedAssetState{ - a.Guid, - a.RelativePath, - a.Type, - a.StorageKind - }); - editorContext.SetSelectedAssetSubId(subId); - editorContext.ClearSelection(); - } - } - ImGui::Unindent(18.0F); - } - } + ImGui::PopID(); + ImGui::NextColumn(); } ImGui::Columns(1); - if (openRenamePopup) { - ImGui::OpenPopup("重命名资源"); + if (!hasVisibleEntries) { + ImGui::Dummy(ImVec2(1.0F, 24.0F)); + ImGui::TextDisabled("当前目录没有匹配的资源。"); } - if (ImGui::BeginPopupContextWindow("ProjectBlankContextMenu", ImGuiPopupFlags_MouseButtonRight)) { + if (selectedAsset.Guid.IsValid() && selectedAsset.Type == "model" && + selectedAsset.RelativePath.parent_path().lexically_normal() == selectedDirectory.lexically_normal()) { + if (const auto selectedRecord = assetDatabaseService->FindAssetByGuid(selectedAsset.Guid); + selectedRecord.has_value()) { + const auto modelDocument = MetaCoreLoadModelAssetDocument(editorContext, *selectedRecord); + if (modelDocument.has_value()) { + ImGui::Spacing(); + ImGui::SeparatorText("生成资源"); + for (const auto& generatedEntry : BuildGeneratedResourceListEntries(*modelDocument)) { + if (!MatchesGeneratedResourceKindFilter(generatedEntry.Kind)) { + continue; + } + const std::string filterLabel = generatedEntry.Name + " " + generatedEntry.SecondaryText; + if (!MatchesGeneratedResourceFilter(filterLabel)) { + continue; + } + + const std::string subId = MetaCoreBuildGeneratedResourceSubId( + generatedEntry.Kind, + generatedEntry.AssetGuid + ); + const bool generatedSelected = + editorContext.GetSelectedAsset().Guid == selectedRecord->Guid && + editorContext.GetSelectedAssetSubId() == subId; + const std::string generatedLabel = + generatedEntry.Name + " [" + generatedEntry.SecondaryText + "]"; + if (ImGui::Selectable(generatedLabel.c_str(), generatedSelected)) { + editorContext.SelectAsset(MetaCoreSelectedAssetState{ + selectedRecord->Guid, + selectedRecord->RelativePath, + selectedRecord->Type, + selectedRecord->StorageKind + }); + editorContext.SetSelectedAssetSubId(subId); + editorContext.ClearSelection(); + } + } + } + } + } + + const float dropHeight = std::max(28.0F, ImGui::GetContentRegionAvail().y); + ImGui::InvisibleButton("ProjectContentDropTarget", ImVec2(-1.0F, dropHeight)); + if (ImGui::IsItemClicked()) { + editorContext.ClearSelectedAsset(); + editorContext.ClearSelectedAssetSubId(); + } + MetaCoreHandleProjectDirectoryDrop(editorContext, *assetDatabaseService, selectedDirectory); + + if (ImGui::BeginPopupContextItem("ProjectContentBlankContextMenu", ImGuiPopupFlags_MouseButtonRight)) { if (ImGui::MenuItem("刷新")) { MetaCoreHandleReloadAssets(editorContext); assetDatabaseService->Refresh(); } - if (ImGui::MenuItem("新建文件夹")) { - std::snprintf(NewFolderBuffer_.data(), NewFolderBuffer_.size(), "NewFolder"); - ImGui::OpenPopup("新建文件夹"); - } - if (ImGui::MenuItem("新建材质")) { - CreateNewMaterial(editorContext, *assetDatabaseService); + ImGui::Separator(); + if (ImGui::MenuItem("新建文件夹")) openCreate(MetaCoreProjectCreateKind::Folder, selectedDirectory); + if (ImGui::MenuItem("新建材质")) openCreate(MetaCoreProjectCreateKind::Material, selectedDirectory); + if (ImGui::MenuItem("新建场景")) openCreate(MetaCoreProjectCreateKind::Scene, selectedDirectory); + if (ImGui::MenuItem("从选中对象创建 Prefab", nullptr, false, editorContext.GetActiveObjectId() != 0)) { + openCreate(MetaCoreProjectCreateKind::Prefab, selectedDirectory, editorContext.GetActiveObjectId()); } + ImGui::Separator(); + ImGui::TextDisabled("外部拖入目标: %s", MetaCoreIsAssetsRelativePath(selectedDirectory) ? selectedDirectory.generic_string().c_str() : "仅支持 Assets"); ImGui::EndPopup(); } + ImGui::EndChild(); + + if (openRenamePopup) { + ImGui::OpenPopup("重命名资源"); + } + if (openCreatePopup) { + ImGui::OpenPopup("新建资源"); + } + if (openDeletePopup) { + ImGui::OpenPopup("删除资源"); + } if (ImGui::BeginPopupModal("重命名资源", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("请输入新名称:"); ImGui::InputText("##NewName", RenameBuffer_.data(), RenameBuffer_.size()); if (ImGui::Button("确定", ImVec2(120, 0))) { std::string newName(RenameBuffer_.data()); - if (!newName.empty()) { - assetDatabaseService->RenamePath(PendingRenamePath_, newName); + if (MetaCoreIsSafeProjectItemName(newName)) { + const std::filesystem::path newPath = (PendingRenamePath_.parent_path() / newName).lexically_normal(); + if (assetDatabaseService->RenamePath(PendingRenamePath_, newName)) { + if (editorContext.GetSelectedProjectDirectory() == PendingRenamePath_) { + editorContext.SetSelectedProjectDirectory(newPath); + } + if (editorContext.GetSelectedAsset().RelativePath == PendingRenamePath_) { + if (const auto record = assetDatabaseService->FindAssetByRelativePath(newPath)) { + editorContext.SelectAsset(MetaCoreSelectedAssetState{record->Guid, record->RelativePath, record->Type, record->StorageKind}); + } else { + editorContext.SelectAsset(MetaCoreSelectedAssetState{{}, newPath, MetaCoreProjectFileTypeLabel(newPath), MetaCoreAssetStorageKind::SourceFile}); + } + } + } else { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Assets", "重命名失败"); + } + } else { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Assets", "名称不可用"); } ImGui::CloseCurrentPopup(); } @@ -4185,16 +5164,87 @@ public: ImGui::EndPopup(); } - if (ImGui::BeginPopupModal("新建文件夹", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("请输入文件夹名称:"); - ImGui::InputText("##FolderName", NewFolderBuffer_.data(), NewFolderBuffer_.size()); + if (ImGui::BeginPopupModal("新建资源", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + const char* title = "请输入名称:"; + if (PendingCreateKind_ == MetaCoreProjectCreateKind::Material) title = "请输入材质名称:"; + if (PendingCreateKind_ == MetaCoreProjectCreateKind::Scene) title = "请输入场景名称:"; + if (PendingCreateKind_ == MetaCoreProjectCreateKind::Prefab) title = "请输入 Prefab 名称:"; + ImGui::TextUnformatted(title); + ImGui::TextDisabled("目标目录: %s", PendingCreateDirectory_.generic_string().c_str()); + ImGui::InputText("##AssetName", NewAssetNameBuffer_.data(), NewAssetNameBuffer_.size()); if (ImGui::Button("确定", ImVec2(120, 0))) { - std::string folderName(NewFolderBuffer_.data()); - if (!folderName.empty()) { - std::filesystem::path newDirPath = assetDatabaseService->GetProjectDescriptor().RootPath / selectedDirectory / folderName; - std::error_code ec; - std::filesystem::create_directories(newDirPath, ec); - assetDatabaseService->Refresh(); + std::string name(NewAssetNameBuffer_.data()); + bool created = false; + if (PendingCreateKind_ == MetaCoreProjectCreateKind::Folder) { + name = MetaCoreSanitizeProjectBaseName(name, "NewFolder"); + if (MetaCoreIsSafeProjectItemName(name)) { + const std::filesystem::path folderPath = MetaCoreBuildUniqueProjectPath(project, PendingCreateDirectory_, name, ""); + created = assetDatabaseService->CreateFolder(folderPath); + (void)assetDatabaseService->Refresh(); + editorContext.SetSelectedProjectDirectory(folderPath); + } + } else if (PendingCreateKind_ == MetaCoreProjectCreateKind::Material) { + name = MetaCoreStripKnownSuffix(MetaCoreSanitizeProjectBaseName(name, "NewMaterial"), ".mcmaterial.json"); + const std::filesystem::path materialPath = MetaCoreBuildUniqueProjectPath(project, PendingCreateDirectory_, name, ".mcmaterial.json"); + created = assetDatabaseService->CreateMaterialAsset(materialPath); + if (created) { + if (const auto record = assetDatabaseService->FindAssetByRelativePath(materialPath)) { + editorContext.SelectAsset(MetaCoreSelectedAssetState{record->Guid, record->RelativePath, record->Type, record->StorageKind}); + } + } + } else if (PendingCreateKind_ == MetaCoreProjectCreateKind::Scene) { + name = MetaCoreStripKnownSuffix(MetaCoreSanitizeProjectBaseName(name, "NewScene"), ".mcscene.json"); + const std::filesystem::path scenePath = MetaCoreBuildUniqueProjectPath(project, PendingCreateDirectory_, name, ".mcscene.json"); + created = assetDatabaseService->CreateEmptySceneAsset(scenePath, false); + if (created) { + if (const auto record = assetDatabaseService->FindAssetByRelativePath(scenePath)) { + editorContext.SelectAsset(MetaCoreSelectedAssetState{record->Guid, record->RelativePath, record->Type, record->StorageKind}); + } + } + } else if (PendingCreateKind_ == MetaCoreProjectCreateKind::Prefab) { + name = MetaCoreStripKnownSuffix(MetaCoreSanitizeProjectBaseName(name, "NewPrefab"), ".mcprefab.json"); + const std::filesystem::path prefabPath = MetaCoreBuildUniqueProjectPath(project, PendingCreateDirectory_, name, ".mcprefab.json"); + const auto prefabService = editorContext.GetModuleRegistry().ResolveService(); + created = + prefabService != nullptr && + prefabService->CreatePrefabFromObject(editorContext, PendingCreatePrefabObjectId_, prefabPath).has_value(); + if (created) { + if (const auto record = assetDatabaseService->FindAssetByRelativePath(prefabPath)) { + editorContext.SelectAsset(MetaCoreSelectedAssetState{record->Guid, record->RelativePath, record->Type, record->StorageKind}); + } + } + } + if (!created) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Project", "创建失败"); + } + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("取消", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopupModal("删除资源", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("确定删除 %s?", PendingDeletePath_.filename().string().c_str()); + ImGui::TextDisabled("%s", PendingDeletePath_.generic_string().c_str()); + if (PendingDeleteIsDirectory_) { + ImGui::TextColored(ImVec4(1.0F, 0.65F, 0.2F, 1.0F), "文件夹会递归删除。"); + } + if (ImGui::Button("删除", ImVec2(120, 0))) { + const std::filesystem::path deletedPath = PendingDeletePath_.lexically_normal(); + if (assetDatabaseService->DeletePath(deletedPath)) { + if (MetaCoreIsSameOrChildPath(editorContext.GetSelectedProjectDirectory(), deletedPath)) { + const std::filesystem::path parent = deletedPath.parent_path().empty() ? std::filesystem::path("Assets") : deletedPath.parent_path(); + editorContext.SetSelectedProjectDirectory(parent); + } + if (MetaCoreIsSameOrChildPath(editorContext.GetSelectedAsset().RelativePath, deletedPath)) { + editorContext.ClearSelectedAsset(); + editorContext.ClearSelectedAssetSubId(); + } + } else { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Project", "删除失败"); } ImGui::CloseCurrentPopup(); } diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index 171cdb8..2c9f3aa 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -483,7 +483,7 @@ void MetaCoreDrawInfernuxStatusBar(MetaCoreEditorContext& editorContext, const I MetaCoreEditorContext& editorContext, const MetaCoreProjectAssetDragDropPayload& payload ) { - if (payload.StorageKind != MetaCoreAssetStorageKind::SourcePackage) { + if (payload.IsDirectory || !payload.AssetGuid.IsValid()) { return false; } @@ -525,8 +525,18 @@ void MetaCoreDrawSceneViewportDropTarget( const ImVec2& viewportSize ) { const ImGuiPayload* dragDropPayload = ImGui::GetDragDropPayload(); - const bool projectAssetDragActive = - dragDropPayload != nullptr && dragDropPayload->IsDataType(MetaCoreProjectAssetDragDropPayloadType); + bool projectAssetDragActive = + dragDropPayload != nullptr && + dragDropPayload->IsDataType(MetaCoreProjectAssetDragDropPayloadType) && + dragDropPayload->DataSize == sizeof(MetaCoreProjectAssetDragDropPayload); + if (projectAssetDragActive) { + const auto* projectAssetPayload = static_cast(dragDropPayload->Data); + const std::string assetType = projectAssetPayload == nullptr ? std::string{} : std::string(projectAssetPayload->AssetType); + projectAssetDragActive = + projectAssetPayload != nullptr && + !projectAssetPayload->IsDirectory && + (assetType == "model" || assetType == "prefab"); + } if (!projectAssetDragActive) { return; } @@ -1130,57 +1140,7 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) { MetaCoreTraceStartup("metacore.app: initialize message handler"); #if defined(_WIN32) - DragAcceptFiles(static_cast(Window_.GetNativeWindowHandle()), TRUE); - - // 允许低权限的 Windows 资源管理器 (Explorer) 向高权限的编辑器进程发送拖放消息,彻底解决拖拽禁止图标的系统底层限制 -#ifndef MSGFLT_ADD -#define MSGFLT_ADD 1 -#endif - ChangeWindowMessageFilter(WM_DROPFILES, MSGFLT_ADD); - ChangeWindowMessageFilter(0x0049, MSGFLT_ADD); // WM_COPYGLOBALMEM - - Window_.SetNativeWindowMessageHandler([this](void* nativeWindowHandle, unsigned int message, std::uintptr_t wparam, std::intptr_t lparam) { - if (message == WM_DROPFILES) { - HDROP hDrop = reinterpret_cast(wparam); - UINT fileCount = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0); - const auto assetDatabaseService = ModuleRegistry_.ResolveService(); - const auto importPipelineService = ModuleRegistry_.ResolveService(); - if (assetDatabaseService != nullptr && assetDatabaseService->HasProject() && importPipelineService != nullptr) { - const std::filesystem::path assetsPath = assetDatabaseService->GetProjectDescriptor().AssetsPath; - - // 【修复外部拖入目录问题】:如果当前编辑器选中了特定子文件夹目录(例如 Assets/Models),则应当拷贝入该子目录中而不是根目录 - std::filesystem::path targetFolder = assetsPath; - if (EditorContext_ != nullptr) { - const std::filesystem::path selectedDir = EditorContext_->GetSelectedProjectDirectory(); - if (!selectedDir.empty()) { - const std::filesystem::path candidate = assetDatabaseService->GetProjectDescriptor().RootPath / selectedDir; - if (std::filesystem::exists(candidate)) { - targetFolder = candidate; - } - } - } - - bool importedAny = false; - for (UINT i = 0; i < fileCount; ++i) { - wchar_t filePath[MAX_PATH]; - if (DragQueryFileW(hDrop, i, filePath, MAX_PATH)) { - std::filesystem::path sourcePath(filePath); - std::filesystem::path targetPath = targetFolder / sourcePath.filename(); - try { - // 使用 overwrite_existing 允许外部重构模型直接拖入刷新,触发引擎自动重导入并更新引用 - std::filesystem::copy_file(sourcePath, targetPath, std::filesystem::copy_options::overwrite_existing); - importedAny = true; - } catch (...) { } - } - } - if (importedAny) { - (void)importPipelineService->RefreshImports(); - (void)assetDatabaseService->Refresh(); - } - } - DragFinish(hDrop); - return true; - } + Window_.SetNativeWindowMessageHandler([](void* nativeWindowHandle, unsigned int message, std::uintptr_t wparam, std::intptr_t lparam) { return ImGui_ImplWin32_WndProcHandler(static_cast(nativeWindowHandle), message, static_cast(wparam), static_cast(lparam)) != 0; }); #endif diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h index e5f5d31..c7ccc37 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h @@ -70,7 +70,7 @@ struct MetaCoreSelectedAssetState { MetaCoreAssetStorageKind StorageKind = MetaCoreAssetStorageKind::SourcePackage; [[nodiscard]] bool IsValid() const { - return Guid.IsValid(); + return Guid.IsValid() || !RelativePath.empty(); } void Clear() { @@ -87,6 +87,8 @@ struct MetaCoreProjectAssetDragDropPayload { MetaCoreAssetGuid AssetGuid{}; MetaCoreAssetStorageKind StorageKind = MetaCoreAssetStorageKind::SourcePackage; char AssetType[32]{}; + char RelativePath[512]{}; + bool IsDirectory = false; }; inline constexpr const char* MetaCoreGeneratedMaterialDragDropPayloadType = "MC_GENERATED_MATERIAL_ASSET"; diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index 5fed434..7e1987d 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -142,6 +142,7 @@ public: [[nodiscard]] virtual bool OpenProject(const std::filesystem::path& projectRoot) = 0; [[nodiscard]] virtual bool RenamePath(const std::filesystem::path& relativePath, const std::filesystem::path& newName) = 0; [[nodiscard]] virtual bool MovePath(const std::filesystem::path& relativePath, const std::filesystem::path& targetDirectory) = 0; + [[nodiscard]] virtual bool MovePathTo(const std::filesystem::path& relativePath, const std::filesystem::path& targetRelativePath) = 0; [[nodiscard]] virtual bool DeletePath(const std::filesystem::path& relativePath) = 0; [[nodiscard]] virtual std::vector GetDirectoriesUnder(const std::filesystem::path& relativeDirectory) const = 0; [[nodiscard]] virtual std::vector GetAssetsUnder(const std::filesystem::path& relativeDirectory) const = 0; @@ -149,6 +150,8 @@ public: virtual bool ApplyExternalFileChanges(const std::vector& changes) = 0; virtual bool Refresh() = 0; virtual bool CreateFolder(const std::filesystem::path& relativeDirectory) = 0; + virtual bool CreateMaterialAsset(const std::filesystem::path& relativeMaterialPath) = 0; + virtual bool CreateEmptySceneAsset(const std::filesystem::path& relativeScenePath, bool makeStartupScene) = 0; virtual bool RegisterScenePath(const std::filesystem::path& relativeScenePath, bool makeStartupScene) = 0; virtual bool SetStartupScenePath(const std::filesystem::path& relativeScenePath) = 0; }; @@ -280,6 +283,11 @@ public: MetaCoreEditorContext& editorContext, const std::filesystem::path& relativePrefabPath ) = 0; + [[nodiscard]] virtual std::optional CreatePrefabFromObject( + MetaCoreEditorContext& editorContext, + MetaCoreId objectId, + const std::filesystem::path& relativePrefabPath + ) = 0; [[nodiscard]] virtual std::optional InstantiatePrefab( MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& prefabAssetGuid, diff --git a/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp b/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp index 98a69b5..df3175f 100644 --- a/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp +++ b/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp @@ -4,16 +4,19 @@ #define WIN32_LEAN_AND_MEAN #endif #include +#include #include #include #include #include +#include #include #include #include #include #include +#include namespace MetaCore { @@ -92,6 +95,7 @@ public: HWND NativeHandle = nullptr; MetaCoreInput Input{}; MetaCoreNativeWindowMessageHandler MessageHandler{}; + std::vector DroppedFiles{}; WNDPROC OriginalWindowProc = nullptr; std::chrono::steady_clock::time_point LastFrameTime = std::chrono::steady_clock::now(); float DeltaSeconds = 1.0F / 60.0F; @@ -107,6 +111,25 @@ LRESULT CALLBACK MetaCoreWindowSubclassProc(HWND hwnd, UINT msg, WPARAM wparam, } MetaCoreWindow::MetaCoreWindowImpl& owner = *iterator->second; + + if (msg == WM_DROPFILES) { + HDROP dropHandle = reinterpret_cast(wparam); + const UINT fileCount = DragQueryFileW(dropHandle, 0xFFFFFFFF, nullptr, 0); + for (UINT index = 0; index < fileCount; ++index) { + const UINT length = DragQueryFileW(dropHandle, index, nullptr, 0); + if (length == 0) { + continue; + } + std::wstring buffer(static_cast(length) + 1, L'\0'); + if (DragQueryFileW(dropHandle, index, buffer.data(), length + 1) != 0) { + buffer.resize(length); + owner.DroppedFiles.emplace_back(buffer); + } + } + DragFinish(dropHandle); + return 1; + } + bool handledByEditor = false; if (owner.MessageHandler) { handledByEditor = owner.MessageHandler(hwnd, msg, static_cast(wparam), static_cast(lparam)); @@ -186,6 +209,12 @@ bool MetaCoreWindow::Initialize(int width, int height, const std::string& title) MetaCoreWindowTrace("metacore.window: window created"); MetaCoreGetWindowMap()[Impl_->NativeHandle] = Impl_.get(); + DragAcceptFiles(Impl_->NativeHandle, TRUE); +#ifndef MSGFLT_ADD +#define MSGFLT_ADD 1 +#endif + ChangeWindowMessageFilter(WM_DROPFILES, MSGFLT_ADD); + ChangeWindowMessageFilter(0x0049, MSGFLT_ADD); Impl_->CloseRequested = false; Impl_->LastFrameTime = std::chrono::steady_clock::now(); @@ -306,6 +335,12 @@ std::pair MetaCoreWindow::GetFramebufferSize() const { return GetWindowSize(); } +std::vector MetaCoreWindow::ConsumeDroppedFiles() { + std::vector droppedFiles; + droppedFiles.swap(Impl_->DroppedFiles); + return droppedFiles; +} + void MetaCoreWindow::SetNativeWindowMessageHandler(MetaCoreNativeWindowMessageHandler handler) { Impl_->MessageHandler = std::move(handler); } diff --git a/Source/MetaCorePlatform/Private/MetaCoreWindowGlfw.cpp b/Source/MetaCorePlatform/Private/MetaCoreWindowGlfw.cpp index 6f45f6b..516f745 100644 --- a/Source/MetaCorePlatform/Private/MetaCoreWindowGlfw.cpp +++ b/Source/MetaCorePlatform/Private/MetaCoreWindowGlfw.cpp @@ -5,9 +5,11 @@ #include #include #include +#include #include #include #include +#include namespace MetaCore { @@ -59,6 +61,7 @@ public: GLFWwindow* NativeHandle = nullptr; MetaCoreInput Input{}; MetaCoreNativeWindowMessageHandler MessageHandler{}; + std::vector DroppedFiles{}; std::chrono::steady_clock::time_point LastFrameTime = std::chrono::steady_clock::now(); float DeltaSeconds = 1.0F / 60.0F; bool Initialized = false; @@ -84,6 +87,16 @@ void MetaCoreGlfwScrollCallback(GLFWwindow* window, double, double yOffset) { } } +void MetaCoreGlfwDropCallback(GLFWwindow* window, int count, const char** paths) { + if (auto* owner = MetaCoreGetWindowImpl(window); owner != nullptr) { + for (int index = 0; index < count; ++index) { + if (paths[index] != nullptr && paths[index][0] != '\0') { + owner->DroppedFiles.emplace_back(paths[index]); + } + } + } +} + } // namespace MetaCoreWindow::MetaCoreWindow() @@ -118,6 +131,7 @@ bool MetaCoreWindow::Initialize(int width, int height, const std::string& title) glfwSetWindowUserPointer(Impl_->NativeHandle, Impl_.get()); glfwSetWindowCloseCallback(Impl_->NativeHandle, MetaCoreGlfwWindowCloseCallback); glfwSetScrollCallback(Impl_->NativeHandle, MetaCoreGlfwScrollCallback); + glfwSetDropCallback(Impl_->NativeHandle, MetaCoreGlfwDropCallback); Impl_->CloseRequested = false; Impl_->LastFrameTime = std::chrono::steady_clock::now(); Impl_->Initialized = true; @@ -139,6 +153,7 @@ bool MetaCoreWindow::Initialize(int width, int height, const std::string& title) void MetaCoreWindow::Shutdown() { if (Impl_->NativeHandle != nullptr) { + glfwSetDropCallback(Impl_->NativeHandle, nullptr); glfwSetWindowUserPointer(Impl_->NativeHandle, nullptr); glfwDestroyWindow(Impl_->NativeHandle); Impl_->NativeHandle = nullptr; @@ -250,6 +265,12 @@ std::pair MetaCoreWindow::GetFramebufferSize() const { return {width, height}; } +std::vector MetaCoreWindow::ConsumeDroppedFiles() { + std::vector droppedFiles; + droppedFiles.swap(Impl_->DroppedFiles); + return droppedFiles; +} + void MetaCoreWindow::SetNativeWindowMessageHandler(MetaCoreNativeWindowMessageHandler handler) { Impl_->MessageHandler = std::move(handler); } diff --git a/Source/MetaCorePlatform/Public/MetaCorePlatform/MetaCoreWindow.h b/Source/MetaCorePlatform/Public/MetaCorePlatform/MetaCoreWindow.h index d809b7a..87c0da9 100644 --- a/Source/MetaCorePlatform/Public/MetaCorePlatform/MetaCoreWindow.h +++ b/Source/MetaCorePlatform/Public/MetaCorePlatform/MetaCoreWindow.h @@ -3,10 +3,12 @@ #include "MetaCorePlatform/MetaCoreInput.h" #include +#include #include #include #include #include +#include namespace MetaCore { @@ -36,6 +38,7 @@ public: [[nodiscard]] float GetDeltaSeconds() const; [[nodiscard]] std::pair GetWindowSize() const; [[nodiscard]] std::pair GetFramebufferSize() const; + [[nodiscard]] std::vector ConsumeDroppedFiles(); void SetNativeWindowMessageHandler(MetaCoreNativeWindowMessageHandler handler); diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 5372ca6..ed52afb 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -601,6 +601,83 @@ void MetaCoreTestAssetDatabaseMovePathUpdatesProjectDescriptor() { std::filesystem::remove_all(tempProjectRoot); } +void MetaCoreTestProjectAssetDatabaseOperations() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreProjectAssetDatabaseOperations"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"ProjectAssetOps\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + { + std::ofstream sourceFile(tempProjectRoot / "Assets" / "Source.dat", std::ios::trunc); + sourceFile << "asset"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto assetDatabase = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); + MetaCoreExpect(assetDatabase->HasProject(), "项目应已加载"); + MetaCoreExpect(assetDatabase->Refresh(), "应能刷新资产数据库"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / "Assets" / "Source.dat.mcmeta"), "Refresh 应生成源资产 mcmeta"); + + const std::filesystem::path movedPath = std::filesystem::path("Assets") / "Moved" / "SourceMoved.dat"; + MetaCoreExpect(assetDatabase->MovePathTo(std::filesystem::path("Assets") / "Source.dat", movedPath), "MovePathTo 应能精确移动文件"); + MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Assets" / "Source.dat"), "移动后旧源文件应消失"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / movedPath), "移动后目标文件应存在"); + MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Assets" / "Source.dat.mcmeta"), "移动后旧 mcmeta 应消失"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / "Assets" / "Moved" / "SourceMoved.dat.mcmeta"), "移动后新 mcmeta 应存在"); + MetaCoreExpect(!assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Source.dat").has_value(), "移动后旧路径资产记录应失效"); + MetaCoreExpect(assetDatabase->FindAssetByRelativePath(movedPath).has_value(), "移动后新路径资产记录应存在"); + + const std::filesystem::path materialPath = std::filesystem::path("Assets") / "Materials" / "PanelMaterial.mcmaterial.json"; + MetaCoreExpect(assetDatabase->CreateMaterialAsset(materialPath), "应能创建材质资源"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / materialPath), "材质源文件应存在"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / (materialPath.generic_string() + ".mcmeta")), "材质 mcmeta 应存在"); + const auto materialRecord = assetDatabase->FindAssetByRelativePath(materialPath); + MetaCoreExpect(materialRecord.has_value(), "材质应进入资产数据库"); + MetaCoreExpect(materialRecord->Guid.IsValid(), "材质 GUID 应有效"); + MetaCoreExpect(materialRecord->Type == "material", "材质资产类型应正确"); + + const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "PanelScene.mcscene.json"; + MetaCoreExpect(assetDatabase->CreateEmptySceneAsset(scenePath, true), "应能创建空场景资源"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / scenePath), "场景源文件应存在"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / (scenePath.generic_string() + ".mcmeta")), "场景 mcmeta 应存在"); + MetaCoreExpect(assetDatabase->GetProjectDescriptor().StartupScenePath == scenePath, "创建场景可设置为启动场景"); + MetaCoreExpect( + std::find( + assetDatabase->GetProjectDescriptor().ScenePaths.begin(), + assetDatabase->GetProjectDescriptor().ScenePaths.end(), + scenePath + ) != assetDatabase->GetProjectDescriptor().ScenePaths.end(), + "创建场景后应注册到项目场景列表" + ); + + MetaCoreExpect(assetDatabase->DeletePath(scenePath), "应能删除场景资源"); + MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / scenePath), "删除后场景源文件应消失"); + MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / (scenePath.generic_string() + ".mcmeta")), "删除后场景 mcmeta 应消失"); + MetaCoreExpect(assetDatabase->GetProjectDescriptor().StartupScenePath.empty(), "删除启动场景后 startup scene 应清空"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + void MetaCoreTestComponentRegistryDescriptors() { MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); @@ -2482,6 +2559,25 @@ void MetaCoreTestPrefabWorkflow() { MetaCoreExpect(revertedRoot, "还原后应重新选中实例根"); MetaCoreExpectVec3Near(revertedRoot.GetComponent().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Revert 后实例应恢复为 prefab 内容"); + MetaCore::MetaCoreGameObject draggedSource = scene.CreateGameObject("DraggedPrefabSource"); + draggedSource.GetComponent().Position = glm::vec3(4.0F, 5.0F, 6.0F); + const std::filesystem::path draggedPrefabPath = std::filesystem::path("Assets") / "Prefabs" / "DraggedPrefabSource.mcprefab.json"; + const auto draggedCreatedPath = prefabService->CreatePrefabFromObject(editorContext, draggedSource.GetId(), draggedPrefabPath); + MetaCoreExpect(draggedCreatedPath.has_value(), "应能从指定对象创建 prefab"); + MetaCoreExpect(std::filesystem::exists(tempProjectRoot / draggedPrefabPath), "指定对象 prefab 文件应存在"); + MetaCoreExpect(assetDatabaseService->Refresh(), "指定对象 prefab 创建后应能刷新资产数据库"); + const auto draggedPrefabRecord = assetDatabaseService->FindAssetByRelativePath(draggedPrefabPath); + MetaCoreExpect(draggedPrefabRecord.has_value(), "指定对象 prefab 应进入资产数据库"); + const auto draggedPrefabDocument = MetaCore::MetaCoreSceneSerializer::LoadPrefabFromJson(tempProjectRoot / draggedPrefabPath, reflectionRegistry->GetTypeRegistry()); + MetaCoreExpect(draggedPrefabDocument.has_value(), "指定对象 prefab 应能被 JSON 解析"); + MetaCoreExpect(!draggedPrefabDocument->GameObjects.empty(), "指定对象 prefab 应包含对象数据"); + MetaCoreExpect(draggedPrefabDocument->GameObjects.front().Name == "DraggedPrefabSource", "指定对象 prefab 应使用目标对象数据"); + MetaCoreExpectVec3Near( + draggedPrefabDocument->GameObjects.front().Transform.Position, + glm::vec3(4.0F, 5.0F, 6.0F), + "指定对象 prefab 应保留目标对象 transform" + ); + coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); @@ -4217,6 +4313,8 @@ int main() { MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor(); std::cout << "[RUN] MetaCoreTestAssetDatabaseMovePathUpdatesProjectDescriptor..." << std::endl; MetaCoreTestAssetDatabaseMovePathUpdatesProjectDescriptor(); + std::cout << "[RUN] MetaCoreTestProjectAssetDatabaseOperations..." << std::endl; + MetaCoreTestProjectAssetDatabaseOperations(); std::cout << "[RUN] MetaCoreTestComponentRegistryDescriptors..." << std::endl; MetaCoreTestComponentRegistryDescriptors(); std::cout << "[RUN] MetaCoreTestSceneEditingServiceCreateCamera..." << std::endl; @@ -4249,6 +4347,8 @@ int main() { MetaCoreTestInstantiateImportedModelAssetIntoScene(); std::cout << "[RUN] MetaCoreTestInstantiateMultiNodeModelAssetIntoScene..." << std::endl; MetaCoreTestInstantiateMultiNodeModelAssetIntoScene(); + std::cout << "[RUN] MetaCoreTestPrefabWorkflow..." << std::endl; + MetaCoreTestPrefabWorkflow(); std::cout << "[RUN] MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport..." << std::endl; MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport(); std::cout << "[RUN] MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport..." << std::endl; @@ -4257,8 +4357,6 @@ int main() { MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable(); std::cout << "[RUN] MetaCoreTestBootstrapStartupSceneCreation..." << std::endl; MetaCoreTestBootstrapStartupSceneCreation(); - std::cout << "[RUN] MetaCoreTestPrefabWorkflow..." << std::endl; - MetaCoreTestPrefabWorkflow(); std::cout << "[RUN] MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi..." << std::endl; MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi(); std::cout << "[RUN] MetaCoreTestComponentRegistryOperations..." << std::endl;