diff --git a/SandboxProject/Assets/UI/main_menu.rml.mcasset b/SandboxProject/Assets/UI/main_menu.rml.mcasset index a0c619e..6bebaad 100644 Binary files a/SandboxProject/Assets/UI/main_menu.rml.mcasset and b/SandboxProject/Assets/UI/main_menu.rml.mcasset differ diff --git a/SandboxProject/Ui/Generated/main_menu.rcss b/SandboxProject/Ui/Generated/main_menu.rcss index 3bd25ed..9e0f9eb 100644 --- a/SandboxProject/Ui/Generated/main_menu.rcss +++ b/SandboxProject/Ui/Generated/main_menu.rcss @@ -10,6 +10,57 @@ body { border-radius: 4px; } +.mcui-Panel { + overflow: hidden; +} + +.mcui-Text { + background-color: transparent; + border-color: transparent; +} + .mcui-Button { text-align: center; + cursor: pointer; + border-width: 1px; + border-bottom-width: 3px; + border-color: rgba(150, 190, 255, 200); +} + +.mcui-Button.mcui-disabled { + cursor: default; +} + +.mcui-Image { + background-color: transparent; + border-radius: 0; +} + +.mcui-node-Button { + background-color: rgba(46, 87, 148, 0.940); + border-top-color: rgba(117, 144, 184, 1.000); + border-left-color: rgba(79, 114, 165, 1.000); + border-right-color: rgba(27, 50, 86, 1.000); + border-bottom-color: rgba(27, 50, 86, 1.000); +} +.mcui-node-Button:hover { + background-color: rgba(79, 114, 165, 0.970); + border-top-color: rgba(132, 156, 192, 1.000); +} +.mcui-node-Button:active { + background-color: rgba(36, 68, 115, 0.980); + border-top-width: 3px; + border-bottom-width: 1px; + border-top-color: rgba(27, 50, 86, 1.000); + border-left-color: rgba(27, 50, 86, 1.000); + border-right-color: rgba(117, 144, 184, 1.000); + border-bottom-color: rgba(117, 144, 184, 1.000); +} +.mcui-node-Button.mcui-disabled, +.mcui-node-Button.mcui-disabled:hover, +.mcui-node-Button.mcui-disabled:active { + background-color: rgba(46, 48, 54, 0.720); + color: rgba(140, 148, 163, 1.000); + border-width: 1px; + border-color: rgba(77, 82, 92, 1.000); } diff --git a/SandboxProject/Ui/Generated/main_menu.rml b/SandboxProject/Ui/Generated/main_menu.rml index 8c80922..55fba70 100644 --- a/SandboxProject/Ui/Generated/main_menu.rml +++ b/SandboxProject/Ui/Generated/main_menu.rml @@ -2,9 +2,8 @@ - - + + +
文本
diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index ec8a310..23d692f 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -3196,7 +3196,7 @@ void MetaCoreDrawUiRendererComponentInspector(MetaCoreEditorContext& editorConte editorContext.GetScene().IncrementRevision(); } - ImGui::TextDisabled("2D 使用屏幕空间层级;3D 使用对象变换、世界尺寸和每单位像素。"); + ImGui::TextDisabled("2D 使用屏幕空间层级;3D 使用对象位置/旋转、世界尺寸和每单位像素。"); } void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) { diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index 2fe3960..3098c8c 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -47,6 +47,7 @@ #include #include #include +#include namespace MetaCore { @@ -78,7 +79,8 @@ enum class MetaCoreProjectCreateKind { Folder, Material, Scene, - Prefab + Prefab, + UiDocument }; static MetaCoreProjectCreateKind PendingCreateKind_ = MetaCoreProjectCreateKind::Folder; static std::filesystem::path PendingCreateDirectory_{}; @@ -206,6 +208,10 @@ void MetaCoreSelectGeneratedResource( std::string_view kind, const MetaCoreAssetGuid& assetGuid ); +[[nodiscard]] bool MetaCoreOpenUiDocumentInDesigner( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetRecord& assetRecord +); [[nodiscard]] const MetaCoreTextureAssetDocument* MetaCoreFindGeneratedTextureAsset( const MetaCoreModelAssetDocument& document, @@ -1091,17 +1097,34 @@ template [[nodiscard]] std::filesystem::path MetaCoreBuildUniqueUiDocumentPath( const MetaCoreProjectDescriptor& projectDescriptor, - const std::filesystem::path& relativeDirectory + const std::filesystem::path& relativeDirectory, + std::string baseName = "UiDocument" ) { const std::filesystem::path baseDirectory = relativeDirectory.empty() || relativeDirectory.string().rfind("Assets", 0) != 0 ? std::filesystem::path("Assets") / "Ui" : relativeDirectory; - std::filesystem::path candidate = baseDirectory / "UiDocument.mcui"; + if (baseName.empty()) { + baseName = "UiDocument"; + } + if (const std::filesystem::path namePath(baseName); namePath.extension() == ".mcui") { + baseName = namePath.stem().string(); + } + for (char& character : baseName) { + if (character == '/' || character == '\\' || character == ':' || character == '*' || + character == '?' || character == '"' || character == '<' || character == '>' || character == '|') { + character = '_'; + } + } + if (baseName.empty() || baseName == "." || baseName == "..") { + baseName = "UiDocument"; + } + + std::filesystem::path candidate = baseDirectory / (baseName + ".mcui"); std::size_t suffix = 1; while (std::filesystem::exists(projectDescriptor.RootPath / candidate)) { - candidate = baseDirectory / ("UiDocument" + std::to_string(suffix++) + ".mcui"); + candidate = baseDirectory / (baseName + std::to_string(suffix++) + ".mcui"); } return candidate.lexically_normal(); } @@ -1110,7 +1133,8 @@ template MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeDirectory, std::filesystem::path& createdRelativePath, - MetaCoreAssetGuid& createdAssetGuid + MetaCoreAssetGuid& createdAssetGuid, + std::string_view baseName = "UiDocument" ) { const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); const auto packageService = editorContext.GetModuleRegistry().ResolveService(); @@ -1119,7 +1143,7 @@ template return false; } - createdRelativePath = MetaCoreBuildUniqueUiDocumentPath(assetDatabaseService->GetProjectDescriptor(), relativeDirectory); + createdRelativePath = MetaCoreBuildUniqueUiDocumentPath(assetDatabaseService->GetProjectDescriptor(), relativeDirectory, std::string(baseName)); (void)std::filesystem::create_directories((assetDatabaseService->GetProjectDescriptor().RootPath / createdRelativePath).parent_path()); createdAssetGuid = MetaCoreAssetGuid::Generate(); @@ -1181,6 +1205,72 @@ template return iterator == document.Nodes.end() ? nullptr : &(*iterator); } +[[nodiscard]] glm::vec2 MetaCoreGetUiNodeAbsolutePosition( + const MetaCoreUiDocument& document, + const std::string& nodeId +) { + const MetaCoreUiNodeDocument* node = MetaCoreFindUiNode(document, nodeId); + if (node == nullptr) { + return glm::vec2(0.0F); + } + + glm::vec2 position(node->RectTransform.Position.x, node->RectTransform.Position.y); + std::string parentId = node->ParentId; + std::unordered_set visitedParents; + while (!parentId.empty() && visitedParents.insert(parentId).second) { + const MetaCoreUiNodeDocument* parent = MetaCoreFindUiNode(document, parentId); + if (parent == nullptr) { + break; + } + position.x += parent->RectTransform.Position.x; + position.y += parent->RectTransform.Position.y; + parentId = parent->ParentId; + } + return position; +} + +[[nodiscard]] glm::vec2 MetaCoreGetUiNodeParentAbsolutePosition( + const MetaCoreUiDocument& document, + const std::string& parentId +) { + return parentId.empty() ? glm::vec2(0.0F) : MetaCoreGetUiNodeAbsolutePosition(document, parentId); +} + +[[nodiscard]] glm::vec2 MetaCoreGetUiNodeParentSize( + const MetaCoreUiDocument& document, + const std::string& parentId +) { + if (parentId.empty()) { + return glm::vec2( + static_cast(std::max(1, document.ReferenceWidth)), + static_cast(std::max(1, document.ReferenceHeight)) + ); + } + + const MetaCoreUiNodeDocument* parent = MetaCoreFindUiNode(document, parentId); + if (parent == nullptr) { + return glm::vec2( + static_cast(std::max(1, document.ReferenceWidth)), + static_cast(std::max(1, document.ReferenceHeight)) + ); + } + return glm::vec2( + std::max(1.0F, parent->RectTransform.Size.x), + std::max(1.0F, parent->RectTransform.Size.y) + ); +} + +[[nodiscard]] std::vector* MetaCoreGetUiNodeSiblingList( + MetaCoreUiDocument& document, + const MetaCoreUiNodeDocument& node +) { + if (node.ParentId.empty()) { + return &document.RootNodeIds; + } + MetaCoreUiNodeDocument* parent = MetaCoreFindUiNode(document, node.ParentId); + return parent == nullptr ? nullptr : &parent->Children; +} + [[nodiscard]] bool MetaCoreUiNodeIsDescendant( const MetaCoreUiDocument& document, const std::string& nodeId, @@ -1221,7 +1311,8 @@ void MetaCoreCollectUiNodeDescendants( [[nodiscard]] bool MetaCoreReparentUiNode( MetaCoreUiDocument& document, const std::string& nodeId, - const std::string& newParentId + const std::string& newParentId, + bool keepAbsolutePosition = false ) { MetaCoreUiNodeDocument* node = MetaCoreFindUiNode(document, nodeId); if (node == nullptr) { @@ -1234,6 +1325,10 @@ void MetaCoreCollectUiNodeDescendants( return false; } + const glm::vec2 absolutePosition = keepAbsolutePosition + ? MetaCoreGetUiNodeAbsolutePosition(document, nodeId) + : glm::vec2(0.0F); + if (node->ParentId.empty()) { document.RootNodeIds.erase( std::remove(document.RootNodeIds.begin(), document.RootNodeIds.end(), nodeId), @@ -1257,6 +1352,15 @@ void MetaCoreCollectUiNodeDescendants( return false; } + if (keepAbsolutePosition) { + node = MetaCoreFindUiNode(document, nodeId); + if (node != nullptr) { + const glm::vec2 parentAbsolutePosition = MetaCoreGetUiNodeParentAbsolutePosition(document, newParentId); + node->RectTransform.Position.x = absolutePosition.x - parentAbsolutePosition.x; + node->RectTransform.Position.y = absolutePosition.y - parentAbsolutePosition.y; + } + } + return true; } @@ -2587,6 +2691,17 @@ public: if (ImGui::MenuItem("材质", nullptr, false, assetDatabaseService != nullptr && assetDatabaseService->HasProject())) { CreateNewMaterial(editorContext, *assetDatabaseService); } + if (ImGui::MenuItem("UI 文档", nullptr, false, assetDatabaseService != nullptr && assetDatabaseService->HasProject())) { + std::filesystem::path createdPath; + MetaCoreAssetGuid createdGuid; + if (MetaCoreCreateUiDocumentAsset(editorContext, std::filesystem::path("Assets") / "Ui", createdPath, createdGuid, "NewUi")) { + if (const auto record = assetDatabaseService->FindAssetByGuid(createdGuid)) { + (void)MetaCoreOpenUiDocumentInDesigner(editorContext, *record); + } + } else { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "UI", "创建 UI 资产失败"); + } + } ImGui::EndMenu(); } ImGui::Separator(); @@ -3714,36 +3829,239 @@ private: return MetaCoreRmlColorToStyle(imColor); } -[[nodiscard]] std::string MetaCoreCompileUiNodeToRml(const MetaCoreUiDocument& document, const MetaCoreUiNodeDocument& node, int indent) { +[[nodiscard]] glm::vec3 MetaCoreAdjustUiColor(const glm::vec3& color, float amount) { + if (amount >= 0.0F) { + return glm::vec3( + std::clamp(color.x + (1.0F - color.x) * amount, 0.0F, 1.0F), + std::clamp(color.y + (1.0F - color.y) * amount, 0.0F, 1.0F), + std::clamp(color.z + (1.0F - color.z) * amount, 0.0F, 1.0F) + ); + } + const float scale = std::max(0.0F, 1.0F + amount); + return glm::vec3( + std::clamp(color.x * scale, 0.0F, 1.0F), + std::clamp(color.y * scale, 0.0F, 1.0F), + std::clamp(color.z * scale, 0.0F, 1.0F) + ); +} + +[[nodiscard]] ImU32 MetaCoreUiColorToU32(const glm::vec3& color, float alpha = 1.0F) { + return ImGui::ColorConvertFloat4ToU32(ImVec4( + std::clamp(color.x, 0.0F, 1.0F), + std::clamp(color.y, 0.0F, 1.0F), + std::clamp(color.z, 0.0F, 1.0F), + std::clamp(alpha, 0.0F, 1.0F) + )); +} + +[[nodiscard]] std::string MetaCoreUiNodeCssClassName(const std::string& nodeId) { + std::string className = "mcui-node-"; + for (const char character : nodeId) { + const unsigned char value = static_cast(character); + className.push_back(std::isalnum(value) || character == '-' || character == '_' ? character : '_'); + } + if (className == "mcui-node-") { + className += "unnamed"; + } + return className; +} + +using MetaCoreNativeUiImageSourceMap = std::unordered_map; + +struct MetaCoreNativeUiTextureChoice { + MetaCoreAssetGuid AssetGuid{}; + std::string Label{}; +}; + +[[nodiscard]] std::vector MetaCoreCollectNativeUiTextureChoices( + const MetaCoreIAssetDatabaseService& assetDatabaseService +) { + std::vector choices; + for (const MetaCoreAssetRecord& record : assetDatabaseService.GetAssetRegistry()) { + if (record.Type != "texture" || !record.Guid.IsValid()) { + continue; + } + std::string label = record.RelativePath.filename().string(); + if (!record.RelativePath.parent_path().empty()) { + label += " | " + record.RelativePath.parent_path().generic_string(); + } + choices.push_back(MetaCoreNativeUiTextureChoice{record.Guid, std::move(label)}); + } + std::sort(choices.begin(), choices.end(), [](const auto& lhs, const auto& rhs) { + return lhs.Label < rhs.Label; + }); + return choices; +} + +[[nodiscard]] std::string MetaCoreNativeUiTextureLabel( + const MetaCoreIAssetDatabaseService& assetDatabaseService, + const MetaCoreAssetGuid& textureGuid +) { + if (!textureGuid.IsValid()) { + return "无贴图"; + } + if (const auto record = assetDatabaseService.FindAssetByGuid(textureGuid); record.has_value()) { + return record->RelativePath.filename().string(); + } + return "贴图缺失"; +} + +[[nodiscard]] std::filesystem::path MetaCoreNativeUiResolveTextureSourcePath( + const MetaCoreIAssetDatabaseService& assetDatabaseService, + const MetaCoreAssetGuid& textureGuid +) { + if (!textureGuid.IsValid() || !assetDatabaseService.HasProject()) { + return {}; + } + const auto record = assetDatabaseService.FindAssetByGuid(textureGuid); + if (!record.has_value()) { + return {}; + } + + std::filesystem::path sourcePath = !record->SourcePath.empty() ? record->SourcePath : record->RelativePath; + if (sourcePath.empty()) { + return {}; + } + if (sourcePath.is_relative()) { + sourcePath = assetDatabaseService.GetProjectDescriptor().RootPath / sourcePath; + } + std::error_code errorCode; + if (!std::filesystem::is_regular_file(sourcePath, errorCode)) { + return {}; + } + std::string extension = sourcePath.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) { + return static_cast(std::tolower(value)); + }); + if (extension != ".png" && + extension != ".jpg" && + extension != ".jpeg" && + extension != ".bmp" && + extension != ".tga" && + extension != ".ppm") { + return {}; + } + return sourcePath.lexically_normal(); +} + +[[nodiscard]] std::string MetaCoreUiNodeTextAlignCss(MetaCoreUiHorizontalAlignment alignment) { + switch (alignment) { + case MetaCoreUiHorizontalAlignment::Center: + return "center"; + case MetaCoreUiHorizontalAlignment::Right: + return "right"; + case MetaCoreUiHorizontalAlignment::Stretch: + case MetaCoreUiHorizontalAlignment::Left: + default: + return "left"; + } +} + +[[nodiscard]] std::string MetaCoreUiNodeVerticalAlignCss(MetaCoreUiVerticalAlignment alignment) { + switch (alignment) { + case MetaCoreUiVerticalAlignment::Center: + return "center"; + case MetaCoreUiVerticalAlignment::Bottom: + return "flex-end"; + case MetaCoreUiVerticalAlignment::Stretch: + case MetaCoreUiVerticalAlignment::Top: + default: + return "flex-start"; + } +} + +[[nodiscard]] std::string MetaCoreUiNodeJustifyCss(MetaCoreUiHorizontalAlignment alignment) { + switch (alignment) { + case MetaCoreUiHorizontalAlignment::Center: + return "center"; + case MetaCoreUiHorizontalAlignment::Right: + return "flex-end"; + case MetaCoreUiHorizontalAlignment::Stretch: + case MetaCoreUiHorizontalAlignment::Left: + default: + return "flex-start"; + } +} + +[[nodiscard]] std::string MetaCoreCompileUiNodeToRml( + const MetaCoreUiDocument& document, + const MetaCoreUiNodeDocument& node, + int indent, + const MetaCoreNativeUiImageSourceMap& imageSources +) { const std::string padding(static_cast(indent), ' '); - const std::string tag = node.Type == MetaCoreUiNodeType::Button ? "button" : "div"; + const auto imageSourceIterator = node.Style.ImageAssetGuid.IsValid() + ? imageSources.find(node.Style.ImageAssetGuid) + : imageSources.end(); + const bool hasImageSource = node.Type == MetaCoreUiNodeType::Image && imageSourceIterator != imageSources.end(); + const std::string tag = node.Type == MetaCoreUiNodeType::Button + ? "button" + : (hasImageSource ? "img" : "div"); + const bool isDisabledButton = node.Type == MetaCoreUiNodeType::Button && !node.Interactable; std::ostringstream rml; rml << padding << "<" << tag << " id=\"" << MetaCoreEscapeRmlText(node.Id) << "\"" - << " class=\"mcui mcui-" << MetaCoreEscapeRmlText(MetaCoreUiNodeTypeLabel(node.Type)) << "\""; - if (node.Type == MetaCoreUiNodeType::Button && node.Interactable) { - rml << " data-metacore-action=\"" << MetaCoreEscapeRmlText(node.Id) << "\""; + << " class=\"mcui mcui-" << MetaCoreEscapeRmlText(MetaCoreUiNodeTypeLabel(node.Type)) + << " " << MetaCoreEscapeRmlText(MetaCoreUiNodeCssClassName(node.Id)); + if (isDisabledButton) { + rml << " mcui-disabled"; + } + rml << "\""; + if (hasImageSource) { + rml << " src=\"" << MetaCoreEscapeRmlText(imageSourceIterator->second.generic_string()) << "\""; + } + if (node.Type == MetaCoreUiNodeType::Image && node.Style.ImageAssetGuid.IsValid()) { + rml << " data-metacore-image-guid=\"" << MetaCoreEscapeRmlText(node.Style.ImageAssetGuid.ToString()) << "\""; + } + const std::string action = node.Type == MetaCoreUiNodeType::Button && node.Interactable + ? (!node.Action.empty() ? node.Action : node.Id) + : std::string{}; + if (!action.empty()) { + rml << " data-metacore-action=\"" << MetaCoreEscapeRmlText(action) << "\""; + } + if (!node.DataBinding.empty()) { + rml << " data-metacore-bind=\"" << MetaCoreEscapeRmlText(node.DataBinding) << "\""; + } + if (!node.DataFormat.empty()) { + rml << " data-metacore-format=\"" << MetaCoreEscapeRmlText(node.DataFormat) << "\""; } rml << " style=\"position:absolute; left:" << node.RectTransform.Position.x << "px; top:" << node.RectTransform.Position.y << "px; width:" << node.RectTransform.Size.x << "px; height:" << node.RectTransform.Size.y << "px; color:" << MetaCoreUiNodeCssColor(node.Style.TextColor) - << "; background-color:" << MetaCoreUiNodeCssColor(node.Style.BackgroundColor, node.Type == MetaCoreUiNodeType::Text ? 0.0F : 0.85F) - << "; font-size:" << node.Style.FontSize << "px; padding:" << node.Style.Padding.y << "px " << node.Style.Padding.x << "px;\""; + << "; font-size:" << node.Style.FontSize << "px; padding:" << node.Style.Padding.y << "px " << node.Style.Padding.x << "px;" + << " text-align:" << MetaCoreUiNodeTextAlignCss(node.Style.HorizontalAlignment); + if (node.Type != MetaCoreUiNodeType::Button) { + rml << "; background-color:" << MetaCoreUiNodeCssColor( + node.Style.BackgroundColor, + (node.Type == MetaCoreUiNodeType::Text || hasImageSource) ? 0.0F : 0.85F + ); + } + if (node.Type == MetaCoreUiNodeType::Text || node.Type == MetaCoreUiNodeType::Button) { + rml << "; display:flex; align-items:" << MetaCoreUiNodeVerticalAlignCss(node.Style.VerticalAlignment) + << "; justify-content:" << MetaCoreUiNodeJustifyCss(node.Style.HorizontalAlignment); + } + if (hasImageSource) { + rml << "; display:block"; + } + rml << ";\""; if (!node.Visible) { rml << " hidden=\"true\""; } + if (isDisabledButton) { + rml << " disabled=\"true\""; + } rml << ">"; if (node.Type == MetaCoreUiNodeType::Text || node.Type == MetaCoreUiNodeType::Button) { rml << MetaCoreEscapeRmlText(node.Text.empty() ? node.Name : node.Text); } - if (!node.Children.empty()) { + if (node.Type == MetaCoreUiNodeType::Panel && !node.Children.empty()) { rml << "\n"; for (const std::string& childId : node.Children) { if (const MetaCoreUiNodeDocument* child = MetaCoreFindUiNode(document, childId); child != nullptr) { - rml << MetaCoreCompileUiNodeToRml(document, *child, indent + 2); + rml << MetaCoreCompileUiNodeToRml(document, *child, indent + 2, imageSources); } } rml << padding; @@ -3754,17 +4072,21 @@ private: [[nodiscard]] std::string MetaCoreCompileUiDocumentToRml( const MetaCoreUiDocument& document, - const std::filesystem::path& cssPath + const std::filesystem::path& cssPath, + const MetaCoreNativeUiImageSourceMap& imageSources ) { std::ostringstream rml; rml << "\n" << " \n" << " \n" << " \n" - << " \n"; + << " \n"; for (const std::string& rootNodeId : document.RootNodeIds) { if (const MetaCoreUiNodeDocument* rootNode = MetaCoreFindUiNode(document, rootNodeId); rootNode != nullptr) { - rml << MetaCoreCompileUiNodeToRml(document, *rootNode, 4); + rml << MetaCoreCompileUiNodeToRml(document, *rootNode, 4, imageSources); } } rml << " \n" @@ -3772,8 +4094,9 @@ private: return rml.str(); } -[[nodiscard]] std::string MetaCoreBuildNativeUiRcssDocument() { - return R"(body { +[[nodiscard]] std::string MetaCoreBuildNativeUiRcssDocument(const MetaCoreUiDocument& document) { + std::ostringstream rcss; + rcss << R"(body { margin: 0; padding: 0; background-color: transparent; @@ -3785,19 +4108,119 @@ private: border-radius: 4px; } +.mcui-Panel { + overflow: hidden; +} + +.mcui-Text { + background-color: transparent; + border-color: transparent; +} + .mcui-Button { text-align: center; + cursor: pointer; + border-width: 1px; + border-bottom-width: 3px; + border-color: rgba(150, 190, 255, 200); +} + +.mcui-Button.mcui-disabled { + cursor: default; +} + +.mcui-Image { + background-color: transparent; + border-radius: 0; } )"; + + for (const MetaCoreUiNodeDocument& node : document.Nodes) { + if (node.Type != MetaCoreUiNodeType::Button) { + continue; + } + + const std::string className = MetaCoreUiNodeCssClassName(node.Id); + const glm::vec3 base = node.Style.BackgroundColor; + const glm::vec3 hover = MetaCoreAdjustUiColor(base, 0.16F); + const glm::vec3 pressed = MetaCoreAdjustUiColor(base, -0.22F); + const glm::vec3 topLight = MetaCoreAdjustUiColor(base, 0.34F); + const glm::vec3 sideLight = MetaCoreAdjustUiColor(base, 0.16F); + const glm::vec3 bottomDark = MetaCoreAdjustUiColor(base, -0.42F); + const glm::vec3 disabled = glm::vec3(0.18F, 0.19F, 0.21F); + + rcss << "\n." << className << " {\n" + << " background-color: " << MetaCoreUiNodeCssColor(base, 0.94F) << ";\n" + << " border-top-color: " << MetaCoreUiNodeCssColor(topLight, 1.0F) << ";\n" + << " border-left-color: " << MetaCoreUiNodeCssColor(sideLight, 1.0F) << ";\n" + << " border-right-color: " << MetaCoreUiNodeCssColor(bottomDark, 1.0F) << ";\n" + << " border-bottom-color: " << MetaCoreUiNodeCssColor(bottomDark, 1.0F) << ";\n" + << "}\n" + << "." << className << ":hover {\n" + << " background-color: " << MetaCoreUiNodeCssColor(hover, 0.97F) << ";\n" + << " border-top-color: " << MetaCoreUiNodeCssColor(MetaCoreAdjustUiColor(hover, 0.30F), 1.0F) << ";\n" + << "}\n" + << "." << className << ":active {\n" + << " background-color: " << MetaCoreUiNodeCssColor(pressed, 0.98F) << ";\n" + << " border-top-width: 3px;\n" + << " border-bottom-width: 1px;\n" + << " border-top-color: " << MetaCoreUiNodeCssColor(bottomDark, 1.0F) << ";\n" + << " border-left-color: " << MetaCoreUiNodeCssColor(bottomDark, 1.0F) << ";\n" + << " border-right-color: " << MetaCoreUiNodeCssColor(topLight, 1.0F) << ";\n" + << " border-bottom-color: " << MetaCoreUiNodeCssColor(topLight, 1.0F) << ";\n" + << "}\n" + << "." << className << ".mcui-disabled,\n" + << "." << className << ".mcui-disabled:hover,\n" + << "." << className << ".mcui-disabled:active {\n" + << " background-color: " << MetaCoreUiNodeCssColor(disabled, 0.72F) << ";\n" + << " color: " << MetaCoreUiNodeCssColor(glm::vec3(0.55F, 0.58F, 0.64F), 1.0F) << ";\n" + << " border-width: 1px;\n" + << " border-color: " << MetaCoreUiNodeCssColor(glm::vec3(0.30F, 0.32F, 0.36F), 1.0F) << ";\n" + << "}\n"; + } + + return rcss.str(); } -class MetaCoreNativeUiDesignerPanelProvider final : public MetaCoreIEditorPanelProvider { -public: - std::string GetPanelId() const override { return "NativeUiDesigner"; } - std::string GetPanelTitle() const override { return "UI 设计器"; } - bool IsOpenByDefault() const override { return false; } +inline constexpr const char* MetaCoreUiDesignerNodeDragDropPayloadType = "MC_UI_DESIGNER_NODE"; - void DrawPanel(MetaCoreEditorContext& editorContext) override { +class MetaCoreNativeUiDesignerService final : public MetaCoreIUiDesignerService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.UiDesigner"; } + + [[nodiscard]] bool OpenUiDocument( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetRecord& assetRecord + ) override { + if (assetRecord.Type != "ui_document") { + return false; + } + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + const auto packageService = editorContext.GetModuleRegistry().ResolveService(); + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + return false; + } + if (!SaveDirtyDocumentBeforeSwitch(editorContext, *assetDatabaseService, *packageService, *reflectionRegistry)) { + return false; + } + if (!LoadNativeDocument(*assetDatabaseService, *packageService, *reflectionRegistry, assetRecord)) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "UI", "打开 UI 资产失败: " + assetRecord.RelativePath.generic_string()); + return false; + } + editorContext.SelectAsset(MetaCoreSelectedAssetState{ + assetRecord.Guid, + assetRecord.RelativePath, + assetRecord.Type, + assetRecord.StorageKind + }); + editorContext.ClearSelectedAssetSubId(); + editorContext.ClearSelection(); + editorContext.SetWorkspacePage(MetaCoreEditorWorkspacePage::UiDesigner); + return true; + } + + void DrawDesignerPage(MetaCoreEditorContext& editorContext) override { const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); const auto packageService = editorContext.GetModuleRegistry().ResolveService(); const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); @@ -3827,14 +4250,18 @@ public: ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("NativeUiCanvas", ImVec2(std::max(260.0F, available.x - hierarchyWidth - inspectorWidth - 20.0F), 0.0F), true, ImGuiWindowFlags_NoScrollbar); - DrawNativeCanvas(); + DrawNativeCanvas(*assetDatabaseService); ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("NativeUiInspector", ImVec2(inspectorWidth, 0.0F), true); - DrawNativeInspector(); + DrawNativeInspector(editorContext, *assetDatabaseService); ImGui::EndChild(); } + [[nodiscard]] bool HasDirtyDocument() const override { + return Dirty_; + } + private: void DrawNativeToolbar( MetaCoreEditorContext& editorContext, @@ -3849,7 +4276,7 @@ private: for (const MetaCoreAssetRecord& record : uiAssets) { const bool selected = LoadedRecord_.has_value() && LoadedRecord_->Guid == record.Guid; if (ImGui::Selectable(record.RelativePath.generic_string().c_str(), selected)) { - LoadNativeDocument(assetDatabaseService, packageService, reflectionRegistry, record); + (void)OpenUiDocument(editorContext, record); } if (selected) { ImGui::SetItemDefaultFocus(); @@ -3863,6 +4290,9 @@ private: ImGui::InputTextWithHint("##NativeUiName", "HUD", NewUiNameBuffer_.data(), NewUiNameBuffer_.size()); ImGui::SameLine(); if (ImGui::Button("新建 UI")) { + if (!SaveDirtyDocumentBeforeSwitch(editorContext, assetDatabaseService, packageService, reflectionRegistry)) { + return; + } CreateNativeDocument(editorContext, assetDatabaseService, packageService, reflectionRegistry); } @@ -3872,6 +4302,10 @@ private: SaveNativeDocument(editorContext, assetDatabaseService, packageService, reflectionRegistry); } ImGui::SameLine(); + if (ImGui::Button("重新加载")) { + ReloadNativeDocument(editorContext, assetDatabaseService, packageService, reflectionRegistry); + } + ImGui::SameLine(); if (ImGui::Button("导出到运行时")) { ExportNativeDocument(editorContext, assetDatabaseService); } @@ -3887,6 +4321,10 @@ private: AddNativeDocumentToScene(editorContext, assetDatabaseService, packageService, reflectionRegistry); } ImGui::EndDisabled(); + if (Dirty_) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.95F, 0.72F, 0.32F, 1.0F), "未保存"); + } } void DrawNativeHierarchyNode(const std::string& nodeId) { @@ -3905,8 +4343,58 @@ private: if (ImGui::IsItemClicked()) { SelectedNodeId_ = node->Id; } + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) { + ImGui::SetDragDropPayload( + MetaCoreUiDesignerNodeDragDropPayloadType, + node->Id.c_str(), + node->Id.size() + 1 + ); + ImGui::TextUnformatted(node->Name.empty() ? node->Id.c_str() : node->Name.c_str()); + ImGui::EndDragDropSource(); + } + if (node->Type == MetaCoreUiNodeType::Panel && ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(MetaCoreUiDesignerNodeDragDropPayloadType); + payload != nullptr && payload->Data != nullptr && payload->DataSize > 0) { + const std::string draggedNodeId(static_cast(payload->Data)); + if (draggedNodeId != node->Id && MetaCoreReparentUiNode(Document_, draggedNodeId, node->Id, true)) { + SelectedNodeId_ = draggedNodeId; + Dirty_ = true; + } + } + ImGui::EndDragDropTarget(); + } + if (ImGui::BeginPopupContextItem("NativeUiHierarchyNodeContext")) { + bool hierarchyMutated = false; + if (ImGui::MenuItem("复制")) { + DuplicateNativeNode(node->Id); + hierarchyMutated = true; + } + if (ImGui::MenuItem("上移")) { + MoveNativeNode(node->Id, -1); + hierarchyMutated = true; + } + if (ImGui::MenuItem("下移")) { + MoveNativeNode(node->Id, 1); + hierarchyMutated = true; + } + ImGui::Separator(); + if (ImGui::MenuItem("删除")) { + DeleteNativeNode(node->Id); + Dirty_ = true; + hierarchyMutated = true; + } + if (hierarchyMutated) { + ImGui::EndPopup(); + if (open) { + ImGui::TreePop(); + } + return; + } + ImGui::EndPopup(); + } if (open) { - for (const std::string& childId : node->Children) { + const std::vector childIds = node->Children; + for (const std::string& childId : childIds) { DrawNativeHierarchyNode(childId); } ImGui::TreePop(); @@ -3923,22 +4411,113 @@ private: ImGui::SameLine(); if (ImGui::Button("+ 图片")) AddNativeNode(MetaCoreUiNodeType::Image); ImGui::Separator(); - for (const std::string& rootNodeId : Document_.RootNodeIds) { + const bool hasSelection = !SelectedNodeId_.empty() && MetaCoreFindUiNode(Document_, SelectedNodeId_) != nullptr; + ImGui::BeginDisabled(!hasSelection); + if (ImGui::Button("复制")) DuplicateNativeNode(SelectedNodeId_); + ImGui::SameLine(); + if (ImGui::Button("删除")) { + DeleteNativeNode(SelectedNodeId_); + Dirty_ = true; + } + if (ImGui::Button("上移")) MoveNativeNode(SelectedNodeId_, -1); + ImGui::SameLine(); + if (ImGui::Button("下移")) MoveNativeNode(SelectedNodeId_, 1); + ImGui::EndDisabled(); + ImGui::Separator(); + ImGui::TextDisabled("根级"); + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(MetaCoreUiDesignerNodeDragDropPayloadType); + payload != nullptr && payload->Data != nullptr && payload->DataSize > 0) { + const std::string draggedNodeId(static_cast(payload->Data)); + if (MetaCoreReparentUiNode(Document_, draggedNodeId, {}, true)) { + SelectedNodeId_ = draggedNodeId; + Dirty_ = true; + } + } + ImGui::EndDragDropTarget(); + } + const std::vector rootNodeIds = Document_.RootNodeIds; + for (const std::string& rootNodeId : rootNodeIds) { DrawNativeHierarchyNode(rootNodeId); } } - void DrawNativeCanvas() { + [[nodiscard]] float SnapNativeUiValue(float value) const { + if (!SnapToGrid_) { + return value; + } + const float gridSize = std::max(1.0F, GridSize_); + return std::round(value / gridSize) * gridSize; + } + + [[nodiscard]] ImVec2 ComputeNativeTextPosition( + const MetaCoreUiNodeDocument& node, + const ImVec2& rectMin, + const ImVec2& rectMax, + const std::string& label + ) const { + const ImVec2 textSize = ImGui::CalcTextSize(label.c_str()); + const float paddingX = std::max(3.0F, node.Style.Padding.x); + const float paddingY = std::max(3.0F, node.Style.Padding.y); + float x = rectMin.x + paddingX; + float y = rectMin.y + paddingY; + + if (node.Style.HorizontalAlignment == MetaCoreUiHorizontalAlignment::Center) { + x = rectMin.x + std::max(0.0F, (rectMax.x - rectMin.x - textSize.x) * 0.5F); + } else if (node.Style.HorizontalAlignment == MetaCoreUiHorizontalAlignment::Right) { + x = rectMax.x - textSize.x - paddingX; + } + + if (node.Style.VerticalAlignment == MetaCoreUiVerticalAlignment::Center) { + y = rectMin.y + std::max(0.0F, (rectMax.y - rectMin.y - textSize.y) * 0.5F); + } else if (node.Style.VerticalAlignment == MetaCoreUiVerticalAlignment::Bottom) { + y = rectMax.y - textSize.y - paddingY; + } + + return ImVec2(std::max(rectMin.x + 2.0F, x), std::max(rectMin.y + 2.0F, y)); + } + + void DrawNativeCanvas(MetaCoreIAssetDatabaseService& assetDatabaseService) { + if (ImGui::SmallButton("适配")) { + CanvasZoom_ = 1.0F; + } + ImGui::SameLine(); + if (ImGui::SmallButton("-")) { + CanvasZoom_ = std::max(0.25F, CanvasZoom_ - 0.10F); + } + ImGui::SameLine(); + ImGui::TextDisabled("%.0f%%", CanvasZoom_ * 100.0F); + ImGui::SameLine(); + if (ImGui::SmallButton("+")) { + CanvasZoom_ = std::min(4.0F, CanvasZoom_ + 0.10F); + } + ImGui::SameLine(); + ImGui::Checkbox("吸附", &SnapToGrid_); + ImGui::SameLine(); + ImGui::SetNextItemWidth(72.0F); + ImGui::DragFloat("网格", &GridSize_, 1.0F, 2.0F, 128.0F, "%.0f"); + const ImVec2 origin = ImGui::GetCursorScreenPos(); const ImVec2 available = ImGui::GetContentRegionAvail(); const float referenceWidth = std::max(1, Document_.ReferenceWidth); const float referenceHeight = std::max(1, Document_.ReferenceHeight); - const float scale = std::max(0.2F, std::min(available.x / referenceWidth, available.y / referenceHeight)); + const float fitScale = std::max(0.2F, std::min(available.x / referenceWidth, available.y / referenceHeight)); + const float scale = std::clamp(fitScale * CanvasZoom_, 0.05F, 8.0F); const ImVec2 canvasSize(referenceWidth * scale, referenceHeight * scale); ImDrawList* drawList = ImGui::GetWindowDrawList(); const ImVec2 max(origin.x + canvasSize.x, origin.y + canvasSize.y); drawList->AddRectFilled(origin, max, IM_COL32(18, 21, 26, 255)); + drawList->AddRectFilled(origin, max, MetaCoreUiColorToU32(Document_.BackgroundColor, Document_.BackgroundAlpha)); drawList->AddRect(origin, max, IM_COL32(94, 104, 122, 255)); + const float gridStep = std::max(2.0F, GridSize_ * scale); + if (gridStep >= 4.0F) { + for (float x = origin.x + gridStep; x < max.x; x += gridStep) { + drawList->AddLine(ImVec2(x, origin.y), ImVec2(x, max.y), IM_COL32(45, 50, 61, 120)); + } + for (float y = origin.y + gridStep; y < max.y; y += gridStep) { + drawList->AddLine(ImVec2(origin.x, y), ImVec2(max.x, y), IM_COL32(45, 50, 61, 120)); + } + } ImGui::SetNextItemAllowOverlap(); ImGui::InvisibleButton("NativeUiCanvasBackground", canvasSize); const bool canvasClicked = ImGui::IsItemClicked(); @@ -3948,19 +4527,99 @@ private: if (!node.Visible) { continue; } - const ImVec2 rectMin(origin.x + node.RectTransform.Position.x * scale, origin.y + node.RectTransform.Position.y * scale); + const glm::vec2 absolutePosition = MetaCoreGetUiNodeAbsolutePosition(Document_, node.Id); + const ImVec2 rectMin(origin.x + absolutePosition.x * scale, origin.y + absolutePosition.y * scale); const ImVec2 rectMax(rectMin.x + node.RectTransform.Size.x * scale, rectMin.y + node.RectTransform.Size.y * scale); const bool selected = SelectedNodeId_ == node.Id; - const ImU32 bg = ImGui::ColorConvertFloat4ToU32(ImVec4( - node.Style.BackgroundColor.x, - node.Style.BackgroundColor.y, - node.Style.BackgroundColor.z, - node.Type == MetaCoreUiNodeType::Text ? 0.0F : 0.82F - )); - drawList->AddRectFilled(rectMin, rectMax, bg, 4.0F); - drawList->AddRect(rectMin, rectMax, selected ? IM_COL32(92, 168, 255, 255) : IM_COL32(128, 139, 156, 180), 4.0F, 0, selected ? 2.0F : 1.0F); - const std::string label = node.Text.empty() ? node.Name : node.Text; - drawList->AddText(ImVec2(rectMin.x + 8.0F, rectMin.y + 7.0F), IM_COL32_WHITE, label.c_str()); + const bool isText = node.Type == MetaCoreUiNodeType::Text; + const bool isButton = node.Type == MetaCoreUiNodeType::Button; + const bool isImage = node.Type == MetaCoreUiNodeType::Image; + const bool isPanel = node.Type == MetaCoreUiNodeType::Panel; + const ImVec2 mousePosition = ImGui::GetMousePos(); + const bool nodeHovered = + ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && + mousePosition.x >= rectMin.x && + mousePosition.x <= rectMax.x && + mousePosition.y >= rectMin.y && + mousePosition.y <= rectMax.y; + const bool buttonInteractive = isButton && node.Interactable; + const bool buttonHovered = buttonInteractive && nodeHovered; + const bool buttonPressed = buttonHovered && ImGui::IsMouseDown(ImGuiMouseButton_Left); + const float buttonYOffset = buttonPressed ? 2.0F : 0.0F; + const ImVec2 visualMin(rectMin.x, rectMin.y + buttonYOffset); + const ImVec2 visualMax(rectMax.x, rectMax.y + buttonYOffset); + const ImU32 borderColor = selected + ? IM_COL32(92, 168, 255, 255) + : (isText ? IM_COL32(128, 139, 156, 72) : IM_COL32(128, 139, 156, 180)); + + if (isPanel || isButton || isImage) { + const float alpha = isButton ? 0.92F : (isImage ? 0.52F : 0.82F); + glm::vec3 backgroundColor = node.Style.BackgroundColor; + if (isButton) { + if (!node.Interactable) { + backgroundColor = glm::vec3(0.18F, 0.19F, 0.21F); + } else if (buttonPressed) { + backgroundColor = MetaCoreAdjustUiColor(backgroundColor, -0.22F); + } else if (buttonHovered) { + backgroundColor = MetaCoreAdjustUiColor(backgroundColor, 0.16F); + } + const float shadowOffset = buttonPressed ? 1.0F : 4.0F; + drawList->AddRectFilled( + ImVec2(rectMin.x + 1.0F, rectMin.y + shadowOffset), + ImVec2(rectMax.x - 1.0F, rectMax.y + shadowOffset), + MetaCoreUiColorToU32(MetaCoreAdjustUiColor(node.Style.BackgroundColor, -0.62F), node.Interactable ? 0.42F : 0.18F), + 5.0F + ); + } + const ImU32 bg = MetaCoreUiColorToU32(backgroundColor, node.Interactable || !isButton ? alpha : 0.72F); + drawList->AddRectFilled(visualMin, visualMax, bg, isButton ? 5.0F : 4.0F); + } + + drawList->AddRect(visualMin, visualMax, borderColor, isButton ? 5.0F : 4.0F, 0, selected ? 2.0F : 1.0F); + + if (isButton) { + const glm::vec3 topColor = buttonPressed + ? MetaCoreAdjustUiColor(node.Style.BackgroundColor, -0.42F) + : MetaCoreAdjustUiColor(node.Style.BackgroundColor, 0.34F); + const glm::vec3 bottomColor = buttonPressed + ? MetaCoreAdjustUiColor(node.Style.BackgroundColor, 0.30F) + : MetaCoreAdjustUiColor(node.Style.BackgroundColor, -0.42F); + const ImU32 topU32 = node.Interactable ? MetaCoreUiColorToU32(topColor, 0.95F) : IM_COL32(90, 94, 102, 180); + const ImU32 bottomU32 = node.Interactable ? MetaCoreUiColorToU32(bottomColor, 0.95F) : IM_COL32(56, 59, 66, 180); + drawList->AddLine(ImVec2(visualMin.x + 5.0F, visualMin.y + 1.0F), ImVec2(visualMax.x - 5.0F, visualMin.y + 1.0F), topU32, buttonPressed ? 1.0F : 2.0F); + drawList->AddLine(ImVec2(visualMin.x + 1.0F, visualMin.y + 5.0F), ImVec2(visualMin.x + 1.0F, visualMax.y - 5.0F), topU32, buttonPressed ? 1.0F : 2.0F); + drawList->AddLine(ImVec2(visualMin.x + 5.0F, visualMax.y - 2.0F), ImVec2(visualMax.x - 5.0F, visualMax.y - 2.0F), bottomU32, buttonPressed ? 1.0F : 2.5F); + drawList->AddLine(ImVec2(visualMax.x - 2.0F, visualMin.y + 5.0F), ImVec2(visualMax.x - 2.0F, visualMax.y - 5.0F), bottomU32, buttonPressed ? 1.0F : 2.0F); + } + + if (isImage) { + const ImU32 tintColor = ImGui::ColorConvertFloat4ToU32(ImVec4( + node.Style.TintColor.x, + node.Style.TintColor.y, + node.Style.TintColor.z, + 0.34F + )); + drawList->AddRectFilled( + ImVec2(visualMin.x + 4.0F, visualMin.y + 4.0F), + ImVec2(visualMax.x - 4.0F, visualMax.y - 4.0F), + tintColor, + 3.0F + ); + drawList->AddLine(visualMin, visualMax, IM_COL32(220, 230, 245, 128), 1.0F); + drawList->AddLine(ImVec2(visualMax.x, visualMin.y), ImVec2(visualMin.x, visualMax.y), IM_COL32(220, 230, 245, 128), 1.0F); + const std::string imageLabel = MetaCoreNativeUiTextureLabel(assetDatabaseService, node.Style.ImageAssetGuid); + const ImVec2 labelPos = ComputeNativeTextPosition(node, visualMin, visualMax, imageLabel); + drawList->AddText(labelPos, IM_COL32(235, 240, 248, 230), imageLabel.c_str()); + } else if (isText || isButton) { + const std::string label = node.Text.empty() ? node.Name : node.Text; + if (!label.empty()) { + const glm::vec3 textColorValue = isButton && !node.Interactable + ? glm::vec3(0.55F, 0.58F, 0.64F) + : node.Style.TextColor; + const ImU32 textColor = MetaCoreUiColorToU32(textColorValue, 1.0F); + drawList->AddText(ComputeNativeTextPosition(node, visualMin, visualMax, label), textColor, label.c_str()); + } + } if (selected) { const ImVec2 handleMin(rectMax.x - 12.0F, rectMax.y - 12.0F); @@ -3978,8 +4637,11 @@ private: } if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { const ImVec2 delta = ImGui::GetIO().MouseDelta; - node.RectTransform.Position.x = std::clamp(node.RectTransform.Position.x + delta.x / scale, 0.0F, referenceWidth - node.RectTransform.Size.x); - node.RectTransform.Position.y = std::clamp(node.RectTransform.Position.y + delta.y / scale, 0.0F, referenceHeight - node.RectTransform.Size.y); + const glm::vec2 parentSize = MetaCoreGetUiNodeParentSize(Document_, node.ParentId); + const float maxX = std::max(0.0F, parentSize.x - node.RectTransform.Size.x); + const float maxY = std::max(0.0F, parentSize.y - node.RectTransform.Size.y); + node.RectTransform.Position.x = std::clamp(SnapNativeUiValue(node.RectTransform.Position.x + delta.x / scale), 0.0F, maxX); + node.RectTransform.Position.y = std::clamp(SnapNativeUiValue(node.RectTransform.Position.y + delta.y / scale), 0.0F, maxY); Dirty_ = true; nodeInteractionThisFrame = true; } @@ -3993,10 +4655,11 @@ private: } if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { const ImVec2 delta = ImGui::GetIO().MouseDelta; - const float maxWidth = std::max(24.0F, referenceWidth - node.RectTransform.Position.x); - const float maxHeight = std::max(20.0F, referenceHeight - node.RectTransform.Position.y); - node.RectTransform.Size.x = std::clamp(node.RectTransform.Size.x + delta.x / scale, 24.0F, maxWidth); - node.RectTransform.Size.y = std::clamp(node.RectTransform.Size.y + delta.y / scale, 20.0F, maxHeight); + const glm::vec2 parentSize = MetaCoreGetUiNodeParentSize(Document_, node.ParentId); + const float maxWidth = std::max(24.0F, parentSize.x - node.RectTransform.Position.x); + const float maxHeight = std::max(20.0F, parentSize.y - node.RectTransform.Position.y); + node.RectTransform.Size.x = std::clamp(SnapNativeUiValue(node.RectTransform.Size.x + delta.x / scale), 24.0F, maxWidth); + node.RectTransform.Size.y = std::clamp(SnapNativeUiValue(node.RectTransform.Size.y + delta.y / scale), 20.0F, maxHeight); Dirty_ = true; nodeInteractionThisFrame = true; } @@ -4009,11 +4672,335 @@ private: } } - void DrawNativeInspector() { + void ApplyNativeNodeTypeDefaults(MetaCoreUiNodeDocument& node, MetaCoreUiNodeType previousType) { + if (node.Type == MetaCoreUiNodeType::Button) { + node.Interactable = true; + if (node.Text.empty() || previousType == MetaCoreUiNodeType::Image || previousType == MetaCoreUiNodeType::Panel) { + node.Text = "按钮"; + } + if (node.Action.empty()) { + node.Action = node.Id; + } + node.Style.HorizontalAlignment = MetaCoreUiHorizontalAlignment::Center; + node.Style.VerticalAlignment = MetaCoreUiVerticalAlignment::Center; + node.Style.BackgroundColor = glm::vec3(0.18F, 0.34F, 0.58F); + node.RectTransform.Size.x = std::max(120.0F, node.RectTransform.Size.x); + node.RectTransform.Size.y = std::max(36.0F, node.RectTransform.Size.y); + return; + } + + node.Interactable = false; + node.Action.clear(); + if (node.Type == MetaCoreUiNodeType::Text) { + if (node.Text.empty() || previousType == MetaCoreUiNodeType::Image || previousType == MetaCoreUiNodeType::Panel) { + node.Text = "文本"; + } + node.Style.BackgroundColor = glm::vec3(0.0F, 0.0F, 0.0F); + node.Style.TextColor = glm::vec3(0.94F, 0.96F, 1.0F); + node.RectTransform.Size.x = std::max(120.0F, node.RectTransform.Size.x); + node.RectTransform.Size.y = std::max(28.0F, node.RectTransform.Size.y); + return; + } + + if (node.Type == MetaCoreUiNodeType::Image) { + node.DataBinding.clear(); + node.DataFormat.clear(); + node.Style.TintColor = glm::vec3(1.0F, 1.0F, 1.0F); + node.Style.PreserveAspect = true; + node.RectTransform.Size.x = std::max(96.0F, node.RectTransform.Size.x); + node.RectTransform.Size.y = std::max(96.0F, node.RectTransform.Size.y); + return; + } + + node.DataBinding.clear(); + node.DataFormat.clear(); + node.Style.BackgroundColor = glm::vec3(0.12F, 0.14F, 0.18F); + node.RectTransform.Size.x = std::max(180.0F, node.RectTransform.Size.x); + node.RectTransform.Size.y = std::max(120.0F, node.RectTransform.Size.y); + } + + void ApplyNativePanelStylePreset(MetaCoreUiNodeDocument& node, int presetIndex) { + node.Type = MetaCoreUiNodeType::Panel; + node.Interactable = false; + node.Action.clear(); + node.DataBinding.clear(); + node.DataFormat.clear(); + node.Style.TextColor = glm::vec3(0.94F, 0.96F, 1.0F); + node.Style.Padding = glm::vec3(12.0F, 12.0F, 0.0F); + switch (presetIndex) { + case 1: // Card + node.Style.BackgroundColor = glm::vec3(0.10F, 0.115F, 0.14F); + node.RectTransform.Size = glm::vec3(std::max(360.0F, node.RectTransform.Size.x), std::max(220.0F, node.RectTransform.Size.y), 0.0F); + node.Style.Padding = glm::vec3(16.0F, 16.0F, 0.0F); + break; + case 2: // Header bar + node.Style.BackgroundColor = glm::vec3(0.055F, 0.065F, 0.085F); + node.RectTransform.Size = glm::vec3(std::max(640.0F, node.RectTransform.Size.x), 64.0F, 0.0F); + node.Style.Padding = glm::vec3(18.0F, 10.0F, 0.0F); + break; + case 3: // Modal + node.Style.BackgroundColor = glm::vec3(0.15F, 0.16F, 0.19F); + node.RectTransform.Size = glm::vec3(std::max(560.0F, node.RectTransform.Size.x), std::max(360.0F, node.RectTransform.Size.y), 0.0F); + node.Style.Padding = glm::vec3(24.0F, 24.0F, 0.0F); + break; + case 0: + default: + node.Style.BackgroundColor = glm::vec3(0.12F, 0.14F, 0.18F); + node.RectTransform.Size = glm::vec3(std::max(320.0F, node.RectTransform.Size.x), std::max(180.0F, node.RectTransform.Size.y), 0.0F); + break; + } + } + + void ApplyNativeTextStylePreset(MetaCoreUiNodeDocument& node, int presetIndex) { + node.Type = MetaCoreUiNodeType::Text; + node.Interactable = false; + node.Action.clear(); + node.Style.BackgroundColor = glm::vec3(0.0F, 0.0F, 0.0F); + node.Style.Padding = glm::vec3(4.0F, 4.0F, 0.0F); + node.Style.HorizontalAlignment = MetaCoreUiHorizontalAlignment::Left; + node.Style.VerticalAlignment = MetaCoreUiVerticalAlignment::Top; + if (node.Text.empty()) { + node.Text = "文本"; + } + switch (presetIndex) { + case 1: // Title + node.Style.FontSize = 34.0F; + node.Style.TextColor = glm::vec3(0.98F, 0.99F, 1.0F); + node.RectTransform.Size = glm::vec3(std::max(360.0F, node.RectTransform.Size.x), std::max(52.0F, node.RectTransform.Size.y), 0.0F); + break; + case 2: // Subtitle + node.Style.FontSize = 22.0F; + node.Style.TextColor = glm::vec3(0.72F, 0.77F, 0.84F); + node.RectTransform.Size = glm::vec3(std::max(320.0F, node.RectTransform.Size.x), std::max(36.0F, node.RectTransform.Size.y), 0.0F); + break; + case 3: // Caption + node.Style.FontSize = 14.0F; + node.Style.TextColor = glm::vec3(0.58F, 0.63F, 0.71F); + node.RectTransform.Size = glm::vec3(std::max(180.0F, node.RectTransform.Size.x), std::max(24.0F, node.RectTransform.Size.y), 0.0F); + break; + case 4: // Warning + node.Style.FontSize = 18.0F; + node.Style.TextColor = glm::vec3(1.0F, 0.72F, 0.26F); + node.RectTransform.Size = glm::vec3(std::max(300.0F, node.RectTransform.Size.x), std::max(32.0F, node.RectTransform.Size.y), 0.0F); + break; + case 0: + default: + node.Style.FontSize = 18.0F; + node.Style.TextColor = glm::vec3(0.90F, 0.93F, 0.97F); + node.RectTransform.Size = glm::vec3(std::max(240.0F, node.RectTransform.Size.x), std::max(32.0F, node.RectTransform.Size.y), 0.0F); + break; + } + } + + void ApplyNativeImageStylePreset(MetaCoreUiNodeDocument& node, int presetIndex) { + node.Type = MetaCoreUiNodeType::Image; + node.Interactable = false; + node.Action.clear(); + node.DataBinding.clear(); + node.DataFormat.clear(); + node.Style.TintColor = glm::vec3(1.0F, 1.0F, 1.0F); + node.Style.BackgroundColor = glm::vec3(0.10F, 0.12F, 0.15F); + node.Style.PreserveAspect = true; + switch (presetIndex) { + case 1: // Icon + node.RectTransform.Size = glm::vec3(48.0F, 48.0F, 0.0F); + node.Style.BackgroundColor = glm::vec3(0.07F, 0.08F, 0.10F); + break; + case 2: // Avatar + node.RectTransform.Size = glm::vec3(96.0F, 96.0F, 0.0F); + node.Style.BackgroundColor = glm::vec3(0.13F, 0.15F, 0.18F); + break; + case 3: // Banner + node.RectTransform.Size = glm::vec3(std::max(640.0F, node.RectTransform.Size.x), 220.0F, 0.0F); + node.Style.BackgroundColor = glm::vec3(0.08F, 0.10F, 0.13F); + break; + case 4: // Thumbnail + node.RectTransform.Size = glm::vec3(160.0F, 90.0F, 0.0F); + node.Style.BackgroundColor = glm::vec3(0.09F, 0.11F, 0.14F); + break; + case 0: + default: + node.RectTransform.Size = glm::vec3(std::max(180.0F, node.RectTransform.Size.x), std::max(120.0F, node.RectTransform.Size.y), 0.0F); + break; + } + } + + void ApplyNativeButtonStylePreset(MetaCoreUiNodeDocument& node, int presetIndex) { + node.Type = MetaCoreUiNodeType::Button; + node.Interactable = true; + if (node.Text.empty()) { + node.Text = "按钮"; + } + if (node.Action.empty()) { + node.Action = node.Id; + } + node.Style.HorizontalAlignment = MetaCoreUiHorizontalAlignment::Center; + node.Style.VerticalAlignment = MetaCoreUiVerticalAlignment::Center; + node.Style.Padding = glm::vec3(14.0F, 8.0F, 0.0F); + node.Style.FontSize = 18.0F; + node.Style.TextColor = glm::vec3(0.96F, 0.98F, 1.0F); + switch (presetIndex) { + case 1: // Secondary + node.Style.BackgroundColor = glm::vec3(0.24F, 0.27F, 0.32F); + node.RectTransform.Size = glm::vec3(std::max(160.0F, node.RectTransform.Size.x), std::max(42.0F, node.RectTransform.Size.y), 0.0F); + break; + case 2: // Danger + node.Style.BackgroundColor = glm::vec3(0.70F, 0.18F, 0.16F); + node.RectTransform.Size = glm::vec3(std::max(160.0F, node.RectTransform.Size.x), std::max(42.0F, node.RectTransform.Size.y), 0.0F); + break; + case 3: // Ghost + node.Style.BackgroundColor = glm::vec3(0.06F, 0.07F, 0.09F); + node.Style.TextColor = glm::vec3(0.62F, 0.78F, 1.0F); + node.RectTransform.Size = glm::vec3(std::max(150.0F, node.RectTransform.Size.x), std::max(40.0F, node.RectTransform.Size.y), 0.0F); + break; + case 4: // Toolbar + node.Style.BackgroundColor = glm::vec3(0.16F, 0.18F, 0.22F); + node.Style.FontSize = 15.0F; + node.RectTransform.Size = glm::vec3(42.0F, 36.0F, 0.0F); + break; + case 0: + default: + node.Style.BackgroundColor = glm::vec3(0.18F, 0.34F, 0.58F); + node.RectTransform.Size = glm::vec3(std::max(180.0F, node.RectTransform.Size.x), std::max(44.0F, node.RectTransform.Size.y), 0.0F); + break; + } + } + + void ApplyNativeDefaultStylePreset(MetaCoreUiNodeDocument& node) { + switch (node.Type) { + case MetaCoreUiNodeType::Panel: + ApplyNativePanelStylePreset(node, 0); + break; + case MetaCoreUiNodeType::Text: + ApplyNativeTextStylePreset(node, 0); + break; + case MetaCoreUiNodeType::Image: + ApplyNativeImageStylePreset(node, 0); + break; + case MetaCoreUiNodeType::Button: + ApplyNativeButtonStylePreset(node, 0); + break; + } + } + + [[nodiscard]] bool DrawNativeStylePresetPicker(MetaCoreUiNodeDocument& node) { + bool changed = false; + if (!ImGui::BeginCombo("基础款式", "选择并应用")) { + return false; + } + const auto drawPreset = [&](const char* label, int presetIndex, auto applyPreset) { + if (ImGui::Selectable(label, false)) { + applyPreset(node, presetIndex); + changed = true; + } + }; + switch (node.Type) { + case MetaCoreUiNodeType::Panel: + drawPreset("默认容器", 0, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativePanelStylePreset(target, index); }); + drawPreset("卡片", 1, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativePanelStylePreset(target, index); }); + drawPreset("顶栏", 2, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativePanelStylePreset(target, index); }); + drawPreset("弹窗面板", 3, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativePanelStylePreset(target, index); }); + break; + case MetaCoreUiNodeType::Text: + drawPreset("正文", 0, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeTextStylePreset(target, index); }); + drawPreset("标题", 1, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeTextStylePreset(target, index); }); + drawPreset("副标题", 2, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeTextStylePreset(target, index); }); + drawPreset("说明文字", 3, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeTextStylePreset(target, index); }); + drawPreset("警告文本", 4, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeTextStylePreset(target, index); }); + break; + case MetaCoreUiNodeType::Image: + drawPreset("默认图片", 0, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeImageStylePreset(target, index); }); + drawPreset("图标", 1, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeImageStylePreset(target, index); }); + drawPreset("头像", 2, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeImageStylePreset(target, index); }); + drawPreset("横幅", 3, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeImageStylePreset(target, index); }); + drawPreset("缩略图", 4, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeImageStylePreset(target, index); }); + break; + case MetaCoreUiNodeType::Button: + drawPreset("主按钮", 0, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeButtonStylePreset(target, index); }); + drawPreset("次按钮", 1, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeButtonStylePreset(target, index); }); + drawPreset("危险按钮", 2, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeButtonStylePreset(target, index); }); + drawPreset("幽灵按钮", 3, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeButtonStylePreset(target, index); }); + drawPreset("工具按钮", 4, [this](MetaCoreUiNodeDocument& target, int index) { ApplyNativeButtonStylePreset(target, index); }); + break; + } + ImGui::EndCombo(); + return changed; + } + + [[nodiscard]] bool DrawNativeImageTextureSlot( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + MetaCoreUiNodeDocument& node + ) { + bool changed = false; + const std::vector textureChoices = MetaCoreCollectNativeUiTextureChoices(assetDatabaseService); + const std::string buttonLabel = MetaCoreNativeUiTextureLabel(assetDatabaseService, node.Style.ImageAssetGuid) + "##NativeUiImageTexture"; + if (ImGui::Button(buttonLabel.c_str(), ImVec2(std::max(80.0F, ImGui::GetContentRegionAvail().x - 104.0F), 0.0F))) { + ImGui::OpenPopup("NativeUiImageTexturePicker"); + } + if (ImGui::BeginPopup("NativeUiImageTexturePicker")) { + if (ImGui::Selectable("无贴图", !node.Style.ImageAssetGuid.IsValid())) { + node.Style.ImageAssetGuid = {}; + changed = true; + } + for (const MetaCoreNativeUiTextureChoice& choice : textureChoices) { + const bool selected = choice.AssetGuid == node.Style.ImageAssetGuid; + if (ImGui::Selectable(choice.Label.c_str(), selected)) { + node.Style.ImageAssetGuid = choice.AssetGuid; + changed = true; + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndPopup(); + } + ImGui::SameLine(); + if (ImGui::SmallButton("清空")) { + if (node.Style.ImageAssetGuid.IsValid()) { + node.Style.ImageAssetGuid = {}; + changed = true; + } + } + ImGui::SameLine(); + const auto textureRecord = node.Style.ImageAssetGuid.IsValid() + ? assetDatabaseService.FindAssetByGuid(node.Style.ImageAssetGuid) + : std::optional{}; + ImGui::BeginDisabled(!textureRecord.has_value()); + if (ImGui::SmallButton("定位") && textureRecord.has_value()) { + editorContext.SelectAsset(MetaCoreSelectedAssetState{ + textureRecord->Guid, + textureRecord->RelativePath, + textureRecord->Type, + textureRecord->StorageKind + }); + editorContext.ClearSelectedAssetSubId(); + editorContext.ClearSelection(); + } + ImGui::EndDisabled(); + if (node.Style.ImageAssetGuid.IsValid() && !textureRecord.has_value()) { + ImGui::TextColored(ImVec4(0.95F, 0.38F, 0.32F, 1.0F), "贴图缺失: %s", node.Style.ImageAssetGuid.ToString().c_str()); + } else if (textureRecord.has_value()) { + ImGui::TextDisabled("%s", textureRecord->RelativePath.generic_string().c_str()); + } + return changed; + } + + void DrawNativeInspector(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService) { ImGui::TextUnformatted("检查器"); ImGui::Separator(); + Dirty_ |= DrawNativeString("文档名称", Document_.Name, 128); if (ImGui::DragInt("参考宽度", &Document_.ReferenceWidth, 1.0F, 320, 7680)) Dirty_ = true; if (ImGui::DragInt("参考高度", &Document_.ReferenceHeight, 1.0F, 240, 4320)) Dirty_ = true; + Dirty_ |= ImGui::ColorEdit3("UI 背景颜色", &Document_.BackgroundColor.x); + if (ImGui::SliderFloat("UI 背景透明度", &Document_.BackgroundAlpha, 0.0F, 1.0F, "%.2f")) { + Document_.BackgroundAlpha = std::clamp(Document_.BackgroundAlpha, 0.0F, 1.0F); + Dirty_ = true; + } + if (ImGui::SmallButton("透明背景")) { + Document_.BackgroundAlpha = 0.0F; + Dirty_ = true; + } ImGui::Separator(); MetaCoreUiNodeDocument* node = SelectedNodeId_.empty() ? nullptr : MetaCoreFindUiNode(Document_, SelectedNodeId_); if (node == nullptr) { @@ -4021,19 +5008,34 @@ private: return; } - Dirty_ |= DrawNativeString("ID", node->Id, 128); + std::string editableId = node->Id; + if (DrawNativeString("ID", editableId, 128) && editableId != node->Id) { + if (RenameNativeNodeId(node->Id, editableId)) { + node = MetaCoreFindUiNode(Document_, SelectedNodeId_); + Dirty_ = true; + } + } + if (node == nullptr) { + return; + } Dirty_ |= DrawNativeString("名称", node->Name, 128); - Dirty_ |= DrawNativeString("文本", node->Text, 256); + const char* nodeTypeLabels[] = {"面板", "文本", "图片", "按钮"}; + int nodeTypeIndex = static_cast(node->Type); + if (ImGui::Combo("类型", &nodeTypeIndex, nodeTypeLabels, IM_ARRAYSIZE(nodeTypeLabels))) { + const MetaCoreUiNodeType previousType = node->Type; + node->Type = static_cast(std::clamp(nodeTypeIndex, 0, 3)); + ApplyNativeNodeTypeDefaults(*node, previousType); + ApplyNativeDefaultStylePreset(*node); + Dirty_ = true; + } + if (DrawNativeStylePresetPicker(*node)) { + Dirty_ = true; + } bool visible = node->Visible; if (ImGui::Checkbox("可见", &visible)) { node->Visible = visible; Dirty_ = true; } - bool interactable = node->Interactable; - if (ImGui::Checkbox("可交互", &interactable)) { - node->Interactable = interactable; - Dirty_ = true; - } float position[2]{node->RectTransform.Position.x, node->RectTransform.Position.y}; if (ImGui::DragFloat2("位置", position, 1.0F)) { node->RectTransform.Position.x = position[0]; @@ -4046,9 +5048,64 @@ private: node->RectTransform.Size.y = std::max(1.0F, size[1]); Dirty_ = true; } - if (ImGui::ColorEdit3("背景", &node->Style.BackgroundColor.x)) Dirty_ = true; - if (ImGui::ColorEdit3("文字颜色", &node->Style.TextColor.x)) Dirty_ = true; - if (ImGui::DragFloat("字号", &node->Style.FontSize, 0.5F, 1.0F, 200.0F)) Dirty_ = true; + + const bool isPanel = node->Type == MetaCoreUiNodeType::Panel; + const bool isText = node->Type == MetaCoreUiNodeType::Text; + const bool isImage = node->Type == MetaCoreUiNodeType::Image; + const bool isButton = node->Type == MetaCoreUiNodeType::Button; + + if (isPanel || isButton || isImage) { + Dirty_ |= ImGui::ColorEdit3(isImage ? "背景占位" : "背景", &node->Style.BackgroundColor.x); + } + + if (isText || isButton) { + Dirty_ |= DrawNativeString("文本", node->Text, 256); + Dirty_ |= ImGui::ColorEdit3("文字颜色", &node->Style.TextColor.x); + Dirty_ |= ImGui::DragFloat("字号", &node->Style.FontSize, 0.5F, 1.0F, 200.0F); + float padding[2]{node->Style.Padding.x, node->Style.Padding.y}; + if (ImGui::DragFloat2("内边距", padding, 0.5F, 0.0F, 512.0F)) { + node->Style.Padding.x = std::max(0.0F, padding[0]); + node->Style.Padding.y = std::max(0.0F, padding[1]); + Dirty_ = true; + } + const char* horizontalLabels[] = {"左", "居中", "右", "拉伸"}; + int horizontalIndex = static_cast(node->Style.HorizontalAlignment); + if (ImGui::Combo("水平对齐", &horizontalIndex, horizontalLabels, IM_ARRAYSIZE(horizontalLabels))) { + node->Style.HorizontalAlignment = static_cast(std::clamp(horizontalIndex, 0, 3)); + Dirty_ = true; + } + const char* verticalLabels[] = {"上", "居中", "下", "拉伸"}; + int verticalIndex = static_cast(node->Style.VerticalAlignment); + if (ImGui::Combo("垂直对齐", &verticalIndex, verticalLabels, IM_ARRAYSIZE(verticalLabels))) { + node->Style.VerticalAlignment = static_cast(std::clamp(verticalIndex, 0, 3)); + Dirty_ = true; + } + } + + if (isImage) { + ImGui::SeparatorText("图片"); + Dirty_ |= DrawNativeImageTextureSlot(editorContext, assetDatabaseService, *node); + Dirty_ |= ImGui::ColorEdit3("色调", &node->Style.TintColor.x); + bool preserveAspect = node->Style.PreserveAspect; + if (ImGui::Checkbox("保持比例", &preserveAspect)) { + node->Style.PreserveAspect = preserveAspect; + Dirty_ = true; + } + } + + ImGui::Separator(); + if (isButton) { + bool interactable = node->Interactable; + if (ImGui::Checkbox("可交互", &interactable)) { + node->Interactable = interactable; + Dirty_ = true; + } + Dirty_ |= DrawNativeString("按钮 Action", node->Action, 128); + } + if (isText || isButton) { + Dirty_ |= DrawNativeString("数据绑定", node->DataBinding, 128); + Dirty_ |= DrawNativeString("绑定格式", node->DataFormat, 128); + } if (ImGui::Button("删除节点")) { DeleteNativeNode(node->Id); Dirty_ = true; @@ -4065,22 +5122,62 @@ private: return false; } + [[nodiscard]] bool RenameNativeNodeId(const std::string& oldId, std::string newId) { + if (oldId.empty() || newId.empty() || oldId == newId) { + return false; + } + for (char& character : newId) { + const unsigned char value = static_cast(character); + if (!std::isalnum(value) && character != '_' && character != '-' && character != '.') { + character = '_'; + } + } + if (newId.empty() || MetaCoreFindUiNode(Document_, newId) != nullptr) { + return false; + } + MetaCoreUiNodeDocument* node = MetaCoreFindUiNode(Document_, oldId); + if (node == nullptr) { + return false; + } + node->Id = newId; + for (std::string& rootId : Document_.RootNodeIds) { + if (rootId == oldId) { + rootId = newId; + } + } + for (MetaCoreUiNodeDocument& candidate : Document_.Nodes) { + if (candidate.ParentId == oldId) { + candidate.ParentId = newId; + } + for (std::string& childId : candidate.Children) { + if (childId == oldId) { + childId = newId; + } + } + } + SelectedNodeId_ = newId; + return true; + } + void AddNativeNode(MetaCoreUiNodeType type) { MetaCoreUiNodeDocument node; node.Type = type; node.Id = MetaCoreMakeUniqueUiNodeId(Document_, type == MetaCoreUiNodeType::Button ? "Button" : type == MetaCoreUiNodeType::Text ? "Text" : type == MetaCoreUiNodeType::Image ? "Image" : "Panel"); node.Name = node.Id; - node.Text = type == MetaCoreUiNodeType::Button ? "按钮" : type == MetaCoreUiNodeType::Text ? "文本" : ""; - node.Interactable = type == MetaCoreUiNodeType::Button; node.RectTransform.Position = glm::vec3(80.0F + Document_.Nodes.size() * 12.0F, 80.0F + Document_.Nodes.size() * 12.0F, 0.0F); node.RectTransform.Size = type == MetaCoreUiNodeType::Panel ? glm::vec3(320.0F, 180.0F, 0.0F) : glm::vec3(180.0F, 44.0F, 0.0F); - node.Style.BackgroundColor = type == MetaCoreUiNodeType::Button ? glm::vec3(0.18F, 0.34F, 0.58F) : glm::vec3(0.12F, 0.14F, 0.18F); - if (!SelectedNodeId_.empty() && MetaCoreFindUiNode(Document_, SelectedNodeId_) != nullptr) { - node.ParentId = SelectedNodeId_; + ApplyNativeNodeTypeDefaults(node, MetaCoreUiNodeType::Panel); + ApplyNativeDefaultStylePreset(node); + if (!SelectedNodeId_.empty()) { + const MetaCoreUiNodeDocument* selectedNode = MetaCoreFindUiNode(Document_, SelectedNodeId_); + if (selectedNode != nullptr && selectedNode->Type == MetaCoreUiNodeType::Panel) { + node.ParentId = SelectedNodeId_; + } } const std::string newId = node.Id; + const std::string parentId = node.ParentId; Document_.Nodes.push_back(std::move(node)); - if (MetaCoreUiNodeDocument* parent = MetaCoreFindUiNode(Document_, SelectedNodeId_); parent != nullptr) { + if (MetaCoreUiNodeDocument* parent = MetaCoreFindUiNode(Document_, parentId); parent != nullptr) { parent->Children.push_back(newId); } else { Document_.RootNodeIds.push_back(newId); @@ -4106,7 +5203,88 @@ private: SelectedNodeId_.clear(); } - void LoadNativeDocument( + bool MoveNativeNode(const std::string& nodeId, int direction) { + MetaCoreUiNodeDocument* node = MetaCoreFindUiNode(Document_, nodeId); + if (node == nullptr || direction == 0) { + return false; + } + std::vector* siblings = MetaCoreGetUiNodeSiblingList(Document_, *node); + if (siblings == nullptr) { + return false; + } + const auto iterator = std::find(siblings->begin(), siblings->end(), nodeId); + if (iterator == siblings->end()) { + return false; + } + const std::ptrdiff_t index = std::distance(siblings->begin(), iterator); + const std::ptrdiff_t targetIndex = index + (direction < 0 ? -1 : 1); + if (targetIndex < 0 || targetIndex >= static_cast(siblings->size())) { + return false; + } + std::swap((*siblings)[static_cast(index)], (*siblings)[static_cast(targetIndex)]); + Dirty_ = true; + return true; + } + + std::string DuplicateNativeNodeRecursive( + const MetaCoreUiNodeDocument& sourceNode, + const std::string& newParentId, + bool offsetRoot + ) { + MetaCoreUiNodeDocument copy = sourceNode; + copy.Id = MetaCoreMakeUniqueUiNodeId(Document_, sourceNode.Id + "_Copy"); + copy.Name = sourceNode.Name.empty() ? copy.Id : sourceNode.Name + " Copy"; + copy.ParentId = newParentId; + copy.Children.clear(); + if (offsetRoot) { + copy.RectTransform.Position.x += 18.0F; + copy.RectTransform.Position.y += 18.0F; + } + + const std::string newId = copy.Id; + const std::vector sourceChildren = sourceNode.Children; + Document_.Nodes.push_back(std::move(copy)); + if (newParentId.empty()) { + Document_.RootNodeIds.push_back(newId); + } else if (MetaCoreUiNodeDocument* parent = MetaCoreFindUiNode(Document_, newParentId); parent != nullptr) { + parent->Children.push_back(newId); + } else { + Document_.RootNodeIds.push_back(newId); + } + + for (const std::string& childId : sourceChildren) { + if (const MetaCoreUiNodeDocument* child = MetaCoreFindUiNode(Document_, childId); child != nullptr) { + (void)DuplicateNativeNodeRecursive(*child, newId, false); + } + } + return newId; + } + + bool DuplicateNativeNode(const std::string& nodeId) { + const MetaCoreUiNodeDocument* sourceNode = MetaCoreFindUiNode(Document_, nodeId); + if (sourceNode == nullptr) { + return false; + } + const MetaCoreUiNodeDocument snapshot = *sourceNode; + SelectedNodeId_ = DuplicateNativeNodeRecursive(snapshot, snapshot.ParentId, true); + Dirty_ = true; + return true; + } + + [[nodiscard]] bool SaveDirtyDocumentBeforeSwitch( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + MetaCoreIPackageService& packageService, + MetaCoreIReflectionRegistry& reflectionRegistry + ) { + if (!Dirty_ || !LoadedRecord_.has_value()) { + return true; + } + SaveNativeDocument(editorContext, assetDatabaseService, packageService, reflectionRegistry); + return !Dirty_; + } + + [[nodiscard]] bool LoadNativeDocument( MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIPackageService& packageService, MetaCoreIReflectionRegistry& reflectionRegistry, @@ -4114,12 +5292,30 @@ private: ) { const auto document = MetaCoreLoadUiDocumentAsset(assetDatabaseService, packageService, reflectionRegistry, record); if (!document.has_value()) { - return; + return false; } LoadedRecord_ = record; Document_ = *document; SelectedNodeId_ = Document_.RootNodeIds.empty() ? std::string{} : Document_.RootNodeIds.front(); Dirty_ = false; + return true; + } + + void ReloadNativeDocument( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + MetaCoreIPackageService& packageService, + MetaCoreIReflectionRegistry& reflectionRegistry + ) { + if (!LoadedRecord_.has_value()) { + return; + } + const MetaCoreAssetRecord record = *LoadedRecord_; + if (LoadNativeDocument(assetDatabaseService, packageService, reflectionRegistry, record)) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "UI", "已重新加载 UI 资产"); + } else { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "UI", "重新加载 UI 资产失败"); + } } void CreateNativeDocument( @@ -4130,7 +5326,8 @@ private: ) { std::filesystem::path createdPath; MetaCoreAssetGuid createdGuid; - if (!MetaCoreCreateUiDocumentAsset(editorContext, std::filesystem::path("Assets") / "Ui", createdPath, createdGuid)) { + const std::string requestedName = NewUiNameBuffer_.data()[0] != '\0' ? NewUiNameBuffer_.data() : "UiDocument"; + if (!MetaCoreCreateUiDocumentAsset(editorContext, std::filesystem::path("Assets") / "Ui", createdPath, createdGuid, requestedName)) { editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "UI", "创建 UI 资产失败"); return; } @@ -4166,6 +5363,66 @@ private: } } + [[nodiscard]] MetaCoreNativeUiImageSourceMap PrepareNativeImageRuntimeSources( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + const std::filesystem::path& generatedDirectory + ) { + MetaCoreNativeUiImageSourceMap imageSources; + const MetaCoreProjectDescriptor& project = assetDatabaseService.GetProjectDescriptor(); + const std::filesystem::path imageDirectoryRelative = generatedDirectory / "Images"; + const std::filesystem::path imageDirectoryAbsolute = project.UiPath / imageDirectoryRelative; + std::error_code errorCode; + std::filesystem::create_directories(imageDirectoryAbsolute, errorCode); + if (errorCode) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "UI", "无法创建 UI 图片导出目录"); + return imageSources; + } + + std::unordered_set copiedGuids; + for (const MetaCoreUiNodeDocument& node : Document_.Nodes) { + if (node.Type != MetaCoreUiNodeType::Image || !node.Style.ImageAssetGuid.IsValid()) { + continue; + } + if (!copiedGuids.insert(node.Style.ImageAssetGuid).second) { + continue; + } + + const std::filesystem::path sourcePath = + MetaCoreNativeUiResolveTextureSourcePath(assetDatabaseService, node.Style.ImageAssetGuid); + if (sourcePath.empty()) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Warning, + "UI", + "跳过缺失的 UI 图片贴图: " + node.Style.ImageAssetGuid.ToString() + ); + continue; + } + + std::filesystem::path extension = sourcePath.extension(); + if (extension.empty()) { + extension = ".png"; + } + const std::filesystem::path targetFilename = node.Style.ImageAssetGuid.ToString() + extension.string(); + const std::filesystem::path targetAbsolute = imageDirectoryAbsolute / targetFilename; + errorCode.clear(); + if (!std::filesystem::equivalent(sourcePath, targetAbsolute, errorCode)) { + errorCode.clear(); + std::filesystem::copy_file(sourcePath, targetAbsolute, std::filesystem::copy_options::overwrite_existing, errorCode); + if (errorCode) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Warning, + "UI", + "复制 UI 图片失败: " + sourcePath.generic_string() + ); + continue; + } + } + imageSources[node.Style.ImageAssetGuid] = std::filesystem::path("Images") / targetFilename; + } + return imageSources; + } + void ExportNativeDocument(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService) { if (!LoadedRecord_.has_value()) { return; @@ -4176,8 +5433,10 @@ private: rmlPath.replace_extension(".rml"); std::filesystem::path rcssPath = rmlPath; rcssPath.replace_extension(".rcss"); - const bool wroteRml = MetaCoreWriteTextFile(project.UiPath / rmlPath, MetaCoreCompileUiDocumentToRml(Document_, rcssPath.filename())); - const bool wroteRcss = MetaCoreWriteTextFile(project.UiPath / rcssPath, MetaCoreBuildNativeUiRcssDocument()); + const MetaCoreNativeUiImageSourceMap imageSources = + PrepareNativeImageRuntimeSources(editorContext, assetDatabaseService, generatedDirectory); + const bool wroteRml = MetaCoreWriteTextFile(project.UiPath / rmlPath, MetaCoreCompileUiDocumentToRml(Document_, rcssPath.filename(), imageSources)); + const bool wroteRcss = MetaCoreWriteTextFile(project.UiPath / rcssPath, MetaCoreBuildNativeUiRcssDocument(Document_)); if (!wroteRml || !wroteRcss) { editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "UI", "导出运行时 RML 失败"); return; @@ -4238,11 +5497,7 @@ private: if (renderMode == MetaCoreUiRenderMode::WorldSpace) { transform.Position = glm::vec3(0.0F, 0.0F, 0.0F); transform.RotationEulerDegrees = glm::vec3(0.0F, 0.0F, 0.0F); - transform.Scale = glm::vec3( - std::max(0.05F, uiRenderer.WorldSize.x), - std::max(0.05F, uiRenderer.WorldSize.y), - 0.02F - ); + transform.Scale = glm::vec3(1.0F, 1.0F, 1.0F); } editorContext.SelectOnly(createdObjectId); @@ -4262,6 +5517,9 @@ private: bool Dirty_ = false; std::array NewUiNameBuffer_{"HUD"}; MetaCoreUiRenderMode PlacementRenderMode_ = MetaCoreUiRenderMode::ScreenSpace; + float CanvasZoom_ = 1.0F; + bool SnapToGrid_ = true; + float GridSize_ = 8.0F; }; struct MetaCoreGeneratedResourceListEntry { @@ -4327,6 +5585,18 @@ void MetaCoreSelectGeneratedResource( editorContext.SetSelectedAssetSubId(MetaCoreBuildGeneratedResourceSubId(kind, assetGuid)); } +[[nodiscard]] bool MetaCoreOpenUiDocumentInDesigner( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetRecord& assetRecord +) { + const auto uiDesignerService = editorContext.GetModuleRegistry().ResolveService(); + if (uiDesignerService == nullptr) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "UI", "UI 设计器服务不可用"); + return false; + } + return uiDesignerService->OpenUiDocument(editorContext, assetRecord); +} + void MetaCoreClearGeneratedResourceSelection(MetaCoreEditorContext& editorContext) { const std::string& selectedAssetSubId = editorContext.GetSelectedAssetSubId(); std::string kind; @@ -6156,7 +7426,9 @@ void DrawUiDocumentDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD ImGui::Text("节点总数: %d", (int)summary->NodeCount); ImGui::Text("根节点数: %d", (int)summary->RootCount); ImGui::Spacing(); - if (ImGui::Button("编辑布局", ImVec2(-1, 0))) { /* Logic */ } + if (ImGui::Button("编辑布局", ImVec2(-1, 0))) { + (void)MetaCoreOpenUiDocumentInDesigner(editorContext, *assetRecord); + } } ImGui::EndChild(); ImGui::PopStyleVar(); @@ -6923,6 +8195,8 @@ void MetaCoreRevealPathInFileManager(const std::filesystem::path& absolutePath) return MetaCoreIsScenesRelativePath(selectedDirectory) ? selectedDirectory : std::filesystem::path("Scenes"); case MetaCoreProjectCreateKind::Prefab: return MetaCoreIsAssetsRelativePath(selectedDirectory) ? selectedDirectory : std::filesystem::path("Assets") / "Prefabs"; + case MetaCoreProjectCreateKind::UiDocument: + return MetaCoreIsAssetsRelativePath(selectedDirectory) ? selectedDirectory : std::filesystem::path("Assets") / "Ui"; case MetaCoreProjectCreateKind::Folder: default: return selectedDirectory.empty() ? std::filesystem::path("Assets") : selectedDirectory; @@ -7177,6 +8451,7 @@ public: if (kind == MetaCoreProjectCreateKind::Material) defaultName = "NewMaterial"; if (kind == MetaCoreProjectCreateKind::Scene) defaultName = "NewScene"; if (kind == MetaCoreProjectCreateKind::Prefab) defaultName = "NewPrefab"; + if (kind == MetaCoreProjectCreateKind::UiDocument) defaultName = "NewUi"; if (kind == MetaCoreProjectCreateKind::Prefab && prefabObjectId != 0) { if (MetaCoreGameObject object = editorContext.GetScene().FindGameObject(prefabObjectId)) { std::snprintf(NewAssetNameBuffer_.data(), NewAssetNameBuffer_.size(), "%s", object.GetName().c_str()); @@ -7207,6 +8482,7 @@ public: if (ImGui::MenuItem("文件夹")) openCreate(MetaCoreProjectCreateKind::Folder, selectedDirectory); if (ImGui::MenuItem("材质")) openCreate(MetaCoreProjectCreateKind::Material, selectedDirectory); if (ImGui::MenuItem("场景")) openCreate(MetaCoreProjectCreateKind::Scene, selectedDirectory); + if (ImGui::MenuItem("UI 文档")) openCreate(MetaCoreProjectCreateKind::UiDocument, selectedDirectory); if (ImGui::MenuItem("从选中对象创建 Prefab", nullptr, false, editorContext.GetActiveObjectId() != 0)) { openCreate(MetaCoreProjectCreateKind::Prefab, selectedDirectory, editorContext.GetActiveObjectId()); } @@ -7304,6 +8580,8 @@ public: (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 if (entry.AssetRecord.has_value() && entry.AssetRecord->Type == "ui_document") { + (void)MetaCoreOpenUiDocumentInDesigner(editorContext, *entry.AssetRecord); } else { MetaCoreOpenPathExternally(project.RootPath / entry.RelativePath); } @@ -7357,6 +8635,12 @@ public: if (!entry.IsDirectory && ImGui::MenuItem("打开外部程序")) { MetaCoreOpenPathExternally(project.RootPath / entry.RelativePath); } + if (!entry.IsDirectory && + entry.AssetRecord.has_value() && + entry.AssetRecord->Type == "ui_document" && + ImGui::MenuItem("编辑 UI")) { + (void)MetaCoreOpenUiDocumentInDesigner(editorContext, *entry.AssetRecord); + } if (ImGui::MenuItem("在文件管理器中显示")) { MetaCoreRevealPathInFileManager(project.RootPath / entry.RelativePath); } @@ -7376,6 +8660,7 @@ public: 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("新建 UI 文档")) openCreate(MetaCoreProjectCreateKind::UiDocument, entry.RelativePath); if (ImGui::MenuItem("从选中对象创建 Prefab", nullptr, false, editorContext.GetActiveObjectId() != 0)) { openCreate(MetaCoreProjectCreateKind::Prefab, entry.RelativePath, editorContext.GetActiveObjectId()); } @@ -7450,6 +8735,7 @@ public: if (ImGui::MenuItem("新建文件夹")) openCreate(MetaCoreProjectCreateKind::Folder, selectedDirectory); if (ImGui::MenuItem("新建材质")) openCreate(MetaCoreProjectCreateKind::Material, selectedDirectory); if (ImGui::MenuItem("新建场景")) openCreate(MetaCoreProjectCreateKind::Scene, selectedDirectory); + if (ImGui::MenuItem("新建 UI 文档")) openCreate(MetaCoreProjectCreateKind::UiDocument, selectedDirectory); if (ImGui::MenuItem("从选中对象创建 Prefab", nullptr, false, editorContext.GetActiveObjectId() != 0)) { openCreate(MetaCoreProjectCreateKind::Prefab, selectedDirectory, editorContext.GetActiveObjectId()); } @@ -7507,6 +8793,7 @@ public: if (PendingCreateKind_ == MetaCoreProjectCreateKind::Material) title = "请输入材质名称:"; if (PendingCreateKind_ == MetaCoreProjectCreateKind::Scene) title = "请输入场景名称:"; if (PendingCreateKind_ == MetaCoreProjectCreateKind::Prefab) title = "请输入 Prefab 名称:"; + if (PendingCreateKind_ == MetaCoreProjectCreateKind::UiDocument) title = "请输入 UI 文档名称:"; ImGui::TextUnformatted(title); ImGui::TextDisabled("目标目录: %s", PendingCreateDirectory_.generic_string().c_str()); ImGui::InputText("##AssetName", NewAssetNameBuffer_.data(), NewAssetNameBuffer_.size()); @@ -7551,6 +8838,17 @@ public: editorContext.SelectAsset(MetaCoreSelectedAssetState{record->Guid, record->RelativePath, record->Type, record->StorageKind}); } } + } else if (PendingCreateKind_ == MetaCoreProjectCreateKind::UiDocument) { + name = MetaCoreStripKnownSuffix(MetaCoreSanitizeProjectBaseName(name, "NewUi"), ".mcui"); + std::filesystem::path createdPath; + MetaCoreAssetGuid createdGuid; + created = MetaCoreCreateUiDocumentAsset(editorContext, PendingCreateDirectory_, createdPath, createdGuid, name); + if (created) { + if (const auto record = assetDatabaseService->FindAssetByGuid(createdGuid)) { + editorContext.SelectAsset(MetaCoreSelectedAssetState{record->Guid, record->RelativePath, record->Type, record->StorageKind}); + (void)MetaCoreOpenUiDocumentInDesigner(editorContext, *record); + } + } } if (!created) { editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Project", "创建失败"); @@ -8174,7 +9472,7 @@ public: moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); - moduleRegistry.RegisterPanelProvider(std::make_unique()); + moduleRegistry.RegisterService(std::make_shared()); // The central Scene / Game viewport is not a dockable provider, but it keeps // its existing visibility state separate from the scene settings panel. moduleRegistry.AccessPanelOpenState("Scene") = true; diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index 9a84a7d..7d78031 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -42,7 +42,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -1043,11 +1045,132 @@ struct MetaCoreUiPreviewDocumentCacheEntry { )); } +[[nodiscard]] glm::vec3 MetaCoreAdjustUiPreviewColor(const glm::vec3& color, float amount) { + if (amount >= 0.0F) { + return glm::vec3( + std::clamp(color.x + (1.0F - color.x) * amount, 0.0F, 1.0F), + std::clamp(color.y + (1.0F - color.y) * amount, 0.0F, 1.0F), + std::clamp(color.z + (1.0F - color.z) * amount, 0.0F, 1.0F) + ); + } + const float scale = std::max(0.0F, 1.0F + amount); + return glm::vec3( + std::clamp(color.x * scale, 0.0F, 1.0F), + std::clamp(color.y * scale, 0.0F, 1.0F), + std::clamp(color.z * scale, 0.0F, 1.0F) + ); +} + +struct MetaCoreUiPreviewHit { + MetaCoreId ObjectId = 0; + std::string ObjectName{}; + std::string NodeId{}; + std::string Action{}; + bool WorldSpace = false; + + [[nodiscard]] bool IsValid() const { + return ObjectId != 0 && !NodeId.empty() && !Action.empty(); + } +}; + +struct MetaCoreUiPreviewInteractionState { + MetaCoreUiPreviewHit Hovered{}; + MetaCoreUiPreviewHit Pressed{}; + bool CapturedMouse = false; +}; + +MetaCoreUiPreviewInteractionState GMetaCoreUiPreviewInteractionState{}; + +[[nodiscard]] bool MetaCoreSameUiPreviewHit(const MetaCoreUiPreviewHit& lhs, const MetaCoreUiPreviewHit& rhs) { + return lhs.ObjectId == rhs.ObjectId && lhs.NodeId == rhs.NodeId; +} + +[[nodiscard]] std::string MetaCoreUiPreviewButtonAction(const MetaCoreUiNodeDocument& node) { + return !node.Action.empty() ? node.Action : node.Id; +} + +void MetaCoreDispatchUiPreviewAction(MetaCoreEditorContext& editorContext, const MetaCoreUiPreviewHit& hit) { + if (!hit.IsValid()) { + return; + } + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "UI", + "触发 UI Action: " + hit.Action + " (" + hit.ObjectName + "/" + hit.NodeId + ")" + ); +} + +[[nodiscard]] const MetaCoreUiNodeDocument* MetaCoreFindUiPreviewNode( + const MetaCoreUiDocument& document, + const std::string& nodeId +) { + const auto iterator = std::find_if(document.Nodes.begin(), document.Nodes.end(), [&](const MetaCoreUiNodeDocument& node) { + return node.Id == nodeId; + }); + return iterator != document.Nodes.end() ? &*iterator : nullptr; +} + +[[nodiscard]] glm::vec2 MetaCoreGetUiPreviewAbsolutePosition( + const MetaCoreUiDocument& document, + const MetaCoreUiNodeDocument& node +) { + glm::vec2 position(node.RectTransform.Position.x, node.RectTransform.Position.y); + std::string parentId = node.ParentId; + std::unordered_set visited; + while (!parentId.empty() && visited.insert(parentId).second) { + const MetaCoreUiNodeDocument* parent = MetaCoreFindUiPreviewNode(document, parentId); + if (parent == nullptr) { + break; + } + position.x += parent->RectTransform.Position.x; + position.y += parent->RectTransform.Position.y; + parentId = parent->ParentId; + } + return position; +} + +[[nodiscard]] ImVec2 MetaCoreUiPreviewTextPosition( + const MetaCoreUiNodeDocument& node, + const ImVec2& rectMin, + const ImVec2& rectMax, + const std::string& label, + float scaleX, + float scaleY, + float fontSize +) { + const ImVec2 textSize = ImGui::GetFont()->CalcTextSizeA( + std::max(1.0F, fontSize), + std::numeric_limits::max(), + 0.0F, + label.c_str() + ); + const float paddingX = std::max(3.0F, node.Style.Padding.x * scaleX); + const float paddingY = std::max(3.0F, node.Style.Padding.y * scaleY); + float x = rectMin.x + paddingX; + float y = rectMin.y + paddingY; + + if (node.Style.HorizontalAlignment == MetaCoreUiHorizontalAlignment::Center) { + x = rectMin.x + std::max(0.0F, (rectMax.x - rectMin.x - textSize.x) * 0.5F); + } else if (node.Style.HorizontalAlignment == MetaCoreUiHorizontalAlignment::Right) { + x = rectMax.x - textSize.x - paddingX; + } + + if (node.Style.VerticalAlignment == MetaCoreUiVerticalAlignment::Center) { + y = rectMin.y + std::max(0.0F, (rectMax.y - rectMin.y - textSize.y) * 0.5F); + } else if (node.Style.VerticalAlignment == MetaCoreUiVerticalAlignment::Bottom) { + y = rectMax.y - textSize.y - paddingY; + } + + return ImVec2(std::max(rectMin.x + 2.0F, x), std::max(rectMin.y + 2.0F, y)); +} + void MetaCoreDrawUiDocumentPreview( ImDrawList& drawList, const MetaCoreUiDocument& document, const ImVec2& previewMin, - const ImVec2& previewSize + const ImVec2& previewSize, + MetaCoreId objectId = 0, + const MetaCoreUiPreviewInteractionState* interactionState = nullptr ) { const float referenceWidth = static_cast(std::max(1, document.ReferenceWidth)); const float referenceHeight = static_cast(std::max(1, document.ReferenceHeight)); @@ -1056,14 +1179,20 @@ void MetaCoreDrawUiDocumentPreview( const ImVec2 previewMax(previewMin.x + previewSize.x, previewMin.y + previewSize.y); drawList.PushClipRect(previewMin, previewMax, true); + drawList.AddRectFilled( + previewMin, + previewMax, + MetaCoreUiPreviewColor(document.BackgroundColor, document.BackgroundAlpha) + ); for (const MetaCoreUiNodeDocument& node : document.Nodes) { if (!node.Visible) { continue; } + const glm::vec2 absolutePosition = MetaCoreGetUiPreviewAbsolutePosition(document, node); const ImVec2 rectMin( - previewMin.x + node.RectTransform.Position.x * scaleX, - previewMin.y + node.RectTransform.Position.y * scaleY + previewMin.x + absolutePosition.x * scaleX, + previewMin.y + absolutePosition.y * scaleY ); const ImVec2 rectSize( std::max(1.0F, node.RectTransform.Size.x * scaleX), @@ -1073,33 +1202,98 @@ void MetaCoreDrawUiDocumentPreview( const bool isText = node.Type == MetaCoreUiNodeType::Text; const bool isButton = node.Type == MetaCoreUiNodeType::Button; const bool isImage = node.Type == MetaCoreUiNodeType::Image; + const bool isPanel = node.Type == MetaCoreUiNodeType::Panel; + const bool hovered = + interactionState != nullptr && + interactionState->Hovered.ObjectId == objectId && + interactionState->Hovered.NodeId == node.Id; + const bool pressed = + hovered && + interactionState != nullptr && + interactionState->Pressed.ObjectId == objectId && + interactionState->Pressed.NodeId == node.Id && + ImGui::IsMouseDown(ImGuiMouseButton_Left); + const float buttonYOffset = isButton && pressed ? 2.0F : 0.0F; + const ImVec2 visualMin(rectMin.x, rectMin.y + buttonYOffset); + const ImVec2 visualMax(rectMax.x, rectMax.y + buttonYOffset); - const float fillAlpha = isText ? 0.0F : (isButton ? 0.92F : 0.82F); - if (!isText) { - drawList.AddRectFilled(rectMin, rectMax, MetaCoreUiPreviewColor(node.Style.BackgroundColor, fillAlpha), 4.0F); + const float fillAlpha = isText ? 0.0F : (isButton ? 0.92F : (isImage ? 0.52F : 0.82F)); + if (isPanel || isButton || isImage) { + glm::vec3 backgroundColor = node.Style.BackgroundColor; + if (isButton) { + if (!node.Interactable) { + backgroundColor = glm::vec3(0.18F, 0.19F, 0.21F); + } else if (pressed) { + backgroundColor = MetaCoreAdjustUiPreviewColor(backgroundColor, -0.22F); + } else if (hovered) { + backgroundColor = MetaCoreAdjustUiPreviewColor(backgroundColor, 0.16F); + } + const float shadowOffset = pressed ? 1.0F : 4.0F; + drawList.AddRectFilled( + ImVec2(rectMin.x + 1.0F, rectMin.y + shadowOffset), + ImVec2(rectMax.x - 1.0F, rectMax.y + shadowOffset), + MetaCoreUiPreviewColor(MetaCoreAdjustUiPreviewColor(node.Style.BackgroundColor, -0.62F), node.Interactable ? 0.42F : 0.18F), + 5.0F + ); + } + drawList.AddRectFilled(visualMin, visualMax, MetaCoreUiPreviewColor(backgroundColor, node.Interactable || !isButton ? fillAlpha : 0.72F), isButton ? 5.0F : 4.0F); } drawList.AddRect( - rectMin, - rectMax, + visualMin, + visualMax, isButton ? IM_COL32(130, 190, 255, 220) : IM_COL32(132, 145, 166, isText ? 80 : 170), - 4.0F, + isButton ? 5.0F : 4.0F, 0, isButton ? 1.4F : 1.0F ); - if (isImage) { - drawList.AddLine(rectMin, rectMax, IM_COL32(210, 220, 235, 115), 1.0F); - drawList.AddLine(ImVec2(rectMax.x, rectMin.y), ImVec2(rectMin.x, rectMax.y), IM_COL32(210, 220, 235, 115), 1.0F); + if (isButton) { + const glm::vec3 topColor = pressed + ? MetaCoreAdjustUiPreviewColor(node.Style.BackgroundColor, -0.42F) + : MetaCoreAdjustUiPreviewColor(node.Style.BackgroundColor, 0.34F); + const glm::vec3 bottomColor = pressed + ? MetaCoreAdjustUiPreviewColor(node.Style.BackgroundColor, 0.30F) + : MetaCoreAdjustUiPreviewColor(node.Style.BackgroundColor, -0.42F); + const ImU32 topU32 = node.Interactable ? MetaCoreUiPreviewColor(topColor, 0.95F) : IM_COL32(90, 94, 102, 180); + const ImU32 bottomU32 = node.Interactable ? MetaCoreUiPreviewColor(bottomColor, 0.95F) : IM_COL32(56, 59, 66, 180); + drawList.AddLine(ImVec2(visualMin.x + 5.0F, visualMin.y + 1.0F), ImVec2(visualMax.x - 5.0F, visualMin.y + 1.0F), topU32, pressed ? 1.0F : 2.0F); + drawList.AddLine(ImVec2(visualMin.x + 1.0F, visualMin.y + 5.0F), ImVec2(visualMin.x + 1.0F, visualMax.y - 5.0F), topU32, pressed ? 1.0F : 2.0F); + drawList.AddLine(ImVec2(visualMin.x + 5.0F, visualMax.y - 2.0F), ImVec2(visualMax.x - 5.0F, visualMax.y - 2.0F), bottomU32, pressed ? 1.0F : 2.5F); + drawList.AddLine(ImVec2(visualMax.x - 2.0F, visualMin.y + 5.0F), ImVec2(visualMax.x - 2.0F, visualMax.y - 5.0F), bottomU32, pressed ? 1.0F : 2.0F); } - const std::string label = !node.Text.empty() ? node.Text : (!node.Name.empty() ? node.Name : node.Id); - if (!label.empty() && rectSize.x > 12.0F && rectSize.y > 12.0F) { - const ImU32 textColor = MetaCoreUiPreviewColor(node.Style.TextColor, 0.96F); - const ImVec2 textPos( - rectMin.x + std::max(3.0F, node.Style.Padding.x * scaleX), - rectMin.y + std::max(3.0F, node.Style.Padding.y * scaleY) + if (isImage) { + drawList.AddRectFilled( + ImVec2(visualMin.x + 3.0F, visualMin.y + 3.0F), + ImVec2(visualMax.x - 3.0F, visualMax.y - 3.0F), + MetaCoreUiPreviewColor(node.Style.TintColor, 0.30F), + 2.0F + ); + drawList.AddLine(visualMin, visualMax, IM_COL32(210, 220, 235, 115), 1.0F); + drawList.AddLine(ImVec2(visualMax.x, visualMin.y), ImVec2(visualMin.x, visualMax.y), IM_COL32(210, 220, 235, 115), 1.0F); + } + + std::string label; + if (isText || isButton) { + label = !node.Text.empty() ? node.Text : (!node.Name.empty() ? node.Name : node.Id); + } else if (isImage) { + label = node.Style.ImageAssetGuid.IsValid() + ? "Image " + node.Style.ImageAssetGuid.ToString().substr(0, 8) + : "Image"; + } + if (!label.empty() && rectSize.x > 12.0F && rectSize.y > 12.0F) { + const glm::vec3 textColorValue = isButton && !node.Interactable + ? glm::vec3(0.55F, 0.58F, 0.64F) + : node.Style.TextColor; + const ImU32 textColor = MetaCoreUiPreviewColor(textColorValue, 0.96F); + const float fontSize = std::max(1.0F, node.Style.FontSize * std::max(0.05F, std::min(scaleX, scaleY))); + drawList.AddText( + ImGui::GetFont(), + fontSize, + MetaCoreUiPreviewTextPosition(node, visualMin, visualMax, label, scaleX, scaleY, fontSize), + textColor, + label.c_str() ); - drawList.AddText(textPos, textColor, label.c_str()); } } drawList.PopClipRect(); @@ -1130,25 +1324,115 @@ void MetaCoreDrawUiDocumentPreview( MetaCoreProjectUiLocalPoint(sceneView, viewportState, worldMatrix, glm::vec2(minPoint.x, maxPoint.y), screenPoints[3]); } +[[nodiscard]] glm::vec2 MetaCoreUiPreviewWorldSize(const MetaCoreUiRendererComponent& uiRenderer) { + return glm::vec2( + std::max(0.05F, uiRenderer.WorldSize.x), + std::max(0.05F, uiRenderer.WorldSize.y) + ); +} + +[[nodiscard]] glm::mat4 MetaCoreBuildUiPreviewWorldMatrix( + const MetaCoreTransformComponent& transform, + const MetaCoreUiRendererComponent& uiRenderer +) { + MetaCoreTransformComponent uiTransform = transform; + const glm::vec2 worldSize = MetaCoreUiPreviewWorldSize(uiRenderer); + uiTransform.Scale = glm::vec3(worldSize.x, worldSize.y, 1.0F); + return MetaCoreBuildTransformMatrix(uiTransform); +} + +void MetaCoreDrawProjectedUiText( + ImDrawList& drawList, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState, + const glm::mat4& worldMatrix, + float referenceWidth, + float referenceHeight, + const ImVec2& documentTextPosition, + float fontSize, + ImU32 textColor, + const std::string& label +) { + if (label.empty() || referenceWidth <= 1.0F || referenceHeight <= 1.0F) { + return; + } + + const float stagingScale = std::max( + 0.05F, + std::min(viewportState.Width / referenceWidth, viewportState.Height / referenceHeight) + ); + const ImVec2 stagingOrigin(viewportState.Left, viewportState.Top); + const ImVec2 stagingPosition( + stagingOrigin.x + documentTextPosition.x * stagingScale, + stagingOrigin.y + documentTextPosition.y * stagingScale + ); + const int vertexBegin = drawList.VtxBuffer.Size; + drawList.AddText( + ImGui::GetFont(), + std::max(1.0F, fontSize * stagingScale), + stagingPosition, + textColor, + label.c_str() + ); + const int vertexEnd = drawList.VtxBuffer.Size; + for (int vertexIndex = vertexBegin; vertexIndex < vertexEnd; ++vertexIndex) { + ImDrawVert& vertex = drawList.VtxBuffer[vertexIndex]; + const float documentX = (vertex.pos.x - stagingOrigin.x) / stagingScale; + const float documentY = (vertex.pos.y - stagingOrigin.y) / stagingScale; + const glm::vec2 localPoint( + documentX / referenceWidth - 0.5F, + 0.5F - documentY / referenceHeight + ); + + ImVec2 projectedPoint{}; + if (MetaCoreProjectUiLocalPoint(sceneView, viewportState, worldMatrix, localPoint, projectedPoint)) { + vertex.pos = projectedPoint; + } else { + vertex.col = 0; + } + } +} + void MetaCoreDrawProjectedUiDocumentPreview( ImDrawList& drawList, const MetaCoreUiDocument& document, const MetaCoreSceneView& sceneView, const MetaCoreSceneViewportState& viewportState, - const glm::mat4& worldMatrix + const glm::mat4& worldMatrix, + MetaCoreId objectId = 0, + const MetaCoreUiPreviewInteractionState* interactionState = nullptr ) { const float referenceWidth = static_cast(std::max(1, document.ReferenceWidth)); const float referenceHeight = static_cast(std::max(1, document.ReferenceHeight)); + if (document.BackgroundAlpha > 0.0F) { + std::array backgroundPoints{}; + if (MetaCoreProjectUiQuad( + sceneView, + viewportState, + worldMatrix, + glm::vec2(-0.5F, 0.5F), + glm::vec2(0.5F, -0.5F), + backgroundPoints + )) { + drawList.AddConvexPolyFilled( + backgroundPoints.data(), + static_cast(backgroundPoints.size()), + MetaCoreUiPreviewColor(document.BackgroundColor, document.BackgroundAlpha) + ); + } + } + for (const MetaCoreUiNodeDocument& node : document.Nodes) { if (!node.Visible) { continue; } - const float left = node.RectTransform.Position.x / referenceWidth - 0.5F; - const float right = (node.RectTransform.Position.x + node.RectTransform.Size.x) / referenceWidth - 0.5F; - const float top = 0.5F - node.RectTransform.Position.y / referenceHeight; - const float bottom = 0.5F - (node.RectTransform.Position.y + node.RectTransform.Size.y) / referenceHeight; + const glm::vec2 absolutePosition = MetaCoreGetUiPreviewAbsolutePosition(document, node); + const float left = absolutePosition.x / referenceWidth - 0.5F; + const float right = (absolutePosition.x + node.RectTransform.Size.x) / referenceWidth - 0.5F; + const float top = 0.5F - absolutePosition.y / referenceHeight; + const float bottom = 0.5F - (absolutePosition.y + node.RectTransform.Size.y) / referenceHeight; std::array points{}; if (!MetaCoreProjectUiQuad(sceneView, viewportState, worldMatrix, glm::vec2(left, top), glm::vec2(right, bottom), points)) { continue; @@ -1157,8 +1441,32 @@ void MetaCoreDrawProjectedUiDocumentPreview( const bool isText = node.Type == MetaCoreUiNodeType::Text; const bool isButton = node.Type == MetaCoreUiNodeType::Button; const bool isImage = node.Type == MetaCoreUiNodeType::Image; + const bool hovered = + interactionState != nullptr && + interactionState->Hovered.ObjectId == objectId && + interactionState->Hovered.NodeId == node.Id; + const bool pressed = + hovered && + interactionState != nullptr && + interactionState->Pressed.ObjectId == objectId && + interactionState->Pressed.NodeId == node.Id && + ImGui::IsMouseDown(ImGuiMouseButton_Left); + glm::vec3 backgroundColor = node.Style.BackgroundColor; + if (isButton) { + if (!node.Interactable) { + backgroundColor = glm::vec3(0.18F, 0.19F, 0.21F); + } else if (pressed) { + backgroundColor = MetaCoreAdjustUiPreviewColor(backgroundColor, -0.22F); + } else if (hovered) { + backgroundColor = MetaCoreAdjustUiPreviewColor(backgroundColor, 0.16F); + } + } if (!isText) { - drawList.AddConvexPolyFilled(points.data(), static_cast(points.size()), MetaCoreUiPreviewColor(node.Style.BackgroundColor, isButton ? 0.92F : 0.82F)); + drawList.AddConvexPolyFilled( + points.data(), + static_cast(points.size()), + MetaCoreUiPreviewColor(backgroundColor, isButton && !node.Interactable ? 0.72F : (isButton ? 0.92F : (isImage ? 0.52F : 0.82F))) + ); } drawList.AddPolyline( points.data(), @@ -1168,29 +1476,384 @@ void MetaCoreDrawProjectedUiDocumentPreview( isButton ? 1.4F : 1.0F ); + if (isButton) { + const glm::vec3 topColor = pressed + ? MetaCoreAdjustUiPreviewColor(node.Style.BackgroundColor, -0.42F) + : MetaCoreAdjustUiPreviewColor(node.Style.BackgroundColor, 0.34F); + const glm::vec3 bottomColor = pressed + ? MetaCoreAdjustUiPreviewColor(node.Style.BackgroundColor, 0.30F) + : MetaCoreAdjustUiPreviewColor(node.Style.BackgroundColor, -0.42F); + const ImU32 topU32 = node.Interactable ? MetaCoreUiPreviewColor(topColor, 0.95F) : IM_COL32(90, 94, 102, 180); + const ImU32 bottomU32 = node.Interactable ? MetaCoreUiPreviewColor(bottomColor, 0.95F) : IM_COL32(56, 59, 66, 180); + drawList.AddLine(points[0], points[1], topU32, pressed ? 1.0F : 2.0F); + drawList.AddLine(points[0], points[3], topU32, pressed ? 1.0F : 2.0F); + drawList.AddLine(points[3], points[2], bottomU32, pressed ? 1.0F : 2.5F); + drawList.AddLine(points[1], points[2], bottomU32, pressed ? 1.0F : 2.0F); + } + if (isImage) { drawList.AddLine(points[0], points[2], IM_COL32(210, 220, 235, 115), 1.0F); drawList.AddLine(points[1], points[3], IM_COL32(210, 220, 235, 115), 1.0F); } - const std::string label = !node.Text.empty() ? node.Text : (!node.Name.empty() ? node.Name : node.Id); + std::string label; + if (isText || isButton) { + label = !node.Text.empty() ? node.Text : (!node.Name.empty() ? node.Name : node.Id); + } else if (isImage) { + label = node.Style.ImageAssetGuid.IsValid() + ? "Image " + node.Style.ImageAssetGuid.ToString().substr(0, 8) + : "Image"; + } if (!label.empty()) { - ImVec2 textPoint{}; - const glm::vec2 textLocal( - left + std::max(3.0F, node.Style.Padding.x) / referenceWidth, - top - std::max(3.0F, node.Style.Padding.y) / referenceHeight + const glm::vec3 textColorValue = isButton && !node.Interactable + ? glm::vec3(0.55F, 0.58F, 0.64F) + : node.Style.TextColor; + const ImVec2 documentRectMin(absolutePosition.x, absolutePosition.y); + const ImVec2 documentRectMax( + absolutePosition.x + node.RectTransform.Size.x, + absolutePosition.y + node.RectTransform.Size.y + ); + MetaCoreDrawProjectedUiText( + drawList, + sceneView, + viewportState, + worldMatrix, + referenceWidth, + referenceHeight, + MetaCoreUiPreviewTextPosition(node, documentRectMin, documentRectMax, label, 1.0F, 1.0F, node.Style.FontSize), + std::max(1.0F, node.Style.FontSize), + MetaCoreUiPreviewColor(textColorValue, 0.96F), + label ); - if (MetaCoreProjectUiLocalPoint(sceneView, viewportState, worldMatrix, textLocal, textPoint)) { - drawList.AddText(textPoint, MetaCoreUiPreviewColor(node.Style.TextColor, 0.96F), label.c_str()); - } } } } +[[nodiscard]] bool MetaCorePointInScreenRect(const ImVec2& point, const ImVec2& rectMin, const ImVec2& rectMax) { + return point.x >= rectMin.x && point.x <= rectMax.x && point.y >= rectMin.y && point.y <= rectMax.y; +} + +[[nodiscard]] float MetaCoreUiPreviewSignedArea(const ImVec2& a, const ImVec2& b, const ImVec2& c) { + return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); +} + +[[nodiscard]] bool MetaCorePointInProjectedTriangle(const ImVec2& point, const ImVec2& a, const ImVec2& b, const ImVec2& c) { + const float area0 = MetaCoreUiPreviewSignedArea(point, a, b); + const float area1 = MetaCoreUiPreviewSignedArea(point, b, c); + const float area2 = MetaCoreUiPreviewSignedArea(point, c, a); + const bool hasNegative = area0 < 0.0F || area1 < 0.0F || area2 < 0.0F; + const bool hasPositive = area0 > 0.0F || area1 > 0.0F || area2 > 0.0F; + return !(hasNegative && hasPositive); +} + +[[nodiscard]] bool MetaCorePointInProjectedQuad(const ImVec2& point, const std::array& points) { + return MetaCorePointInProjectedTriangle(point, points[0], points[1], points[2]) || + MetaCorePointInProjectedTriangle(point, points[0], points[2], points[3]); +} + +[[nodiscard]] std::optional MetaCoreFindUiPreviewButtonHitInRect( + const MetaCoreUiDocument& document, + MetaCoreId objectId, + std::string_view objectName, + bool worldSpace, + const ImVec2& previewMin, + const ImVec2& previewSize, + const ImVec2& mousePosition +) { + const float referenceWidth = static_cast(std::max(1, document.ReferenceWidth)); + const float referenceHeight = static_cast(std::max(1, document.ReferenceHeight)); + const float scaleX = previewSize.x / referenceWidth; + const float scaleY = previewSize.y / referenceHeight; + std::optional hit; + for (const MetaCoreUiNodeDocument& node : document.Nodes) { + if (!node.Visible || !node.Interactable || node.Type != MetaCoreUiNodeType::Button) { + continue; + } + + const glm::vec2 absolutePosition = MetaCoreGetUiPreviewAbsolutePosition(document, node); + const ImVec2 rectMin( + previewMin.x + absolutePosition.x * scaleX, + previewMin.y + absolutePosition.y * scaleY + ); + const ImVec2 rectMax( + rectMin.x + std::max(1.0F, node.RectTransform.Size.x * scaleX), + rectMin.y + std::max(1.0F, node.RectTransform.Size.y * scaleY) + ); + if (!MetaCorePointInScreenRect(mousePosition, rectMin, rectMax)) { + continue; + } + + hit = MetaCoreUiPreviewHit{ + objectId, + std::string(objectName), + node.Id, + MetaCoreUiPreviewButtonAction(node), + worldSpace + }; + } + return hit; +} + +[[nodiscard]] std::optional MetaCoreFindUiPreviewButtonHitInProjectedDocument( + const MetaCoreUiDocument& document, + MetaCoreId objectId, + std::string_view objectName, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState, + const glm::mat4& worldMatrix, + const ImVec2& mousePosition +) { + const float referenceWidth = static_cast(std::max(1, document.ReferenceWidth)); + const float referenceHeight = static_cast(std::max(1, document.ReferenceHeight)); + std::optional hit; + for (const MetaCoreUiNodeDocument& node : document.Nodes) { + if (!node.Visible || !node.Interactable || node.Type != MetaCoreUiNodeType::Button) { + continue; + } + + const glm::vec2 absolutePosition = MetaCoreGetUiPreviewAbsolutePosition(document, node); + const float left = absolutePosition.x / referenceWidth - 0.5F; + const float right = (absolutePosition.x + node.RectTransform.Size.x) / referenceWidth - 0.5F; + const float top = 0.5F - absolutePosition.y / referenceHeight; + const float bottom = 0.5F - (absolutePosition.y + node.RectTransform.Size.y) / referenceHeight; + std::array points{}; + if (!MetaCoreProjectUiQuad(sceneView, viewportState, worldMatrix, glm::vec2(left, top), glm::vec2(right, bottom), points)) { + continue; + } + if (!MetaCorePointInProjectedQuad(mousePosition, points)) { + continue; + } + + hit = MetaCoreUiPreviewHit{ + objectId, + std::string(objectName), + node.Id, + MetaCoreUiPreviewButtonAction(node), + true + }; + } + return hit; +} + +[[nodiscard]] std::optional MetaCoreFindScreenSpaceUiPreviewHit( + const MetaCoreEditorContext& editorContext, + const MetaCoreScene& scene, + const MetaCoreSceneViewportState& viewportState +) { + if (!viewportState.Hovered || viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) { + return std::nullopt; + } + + struct UiPreviewItem { + MetaCoreId ObjectId = 0; + std::string Name{}; + std::int32_t Layer = 0; + float ReferenceWidth = 1920.0F; + float ReferenceHeight = 1080.0F; + std::optional Document{}; + }; + + std::vector items; + for (const MetaCoreGameObject& gameObject : scene.GetGameObjects()) { + if (!gameObject.HasComponent()) { + continue; + } + const MetaCoreUiRendererComponent& uiRenderer = gameObject.GetComponent(); + if (!uiRenderer.Visible || !uiRenderer.InputEnabled || uiRenderer.RenderMode != MetaCoreUiRenderMode::ScreenSpace) { + continue; + } + + std::optional document = MetaCoreLoadUiPreviewDocument(editorContext, uiRenderer); + if (!document.has_value()) { + continue; + } + items.push_back(UiPreviewItem{ + gameObject.GetId(), + gameObject.GetName(), + uiRenderer.Layer, + static_cast(std::max(1, document->ReferenceWidth)), + static_cast(std::max(1, document->ReferenceHeight)), + std::move(document) + }); + } + + std::sort(items.begin(), items.end(), [](const UiPreviewItem& lhs, const UiPreviewItem& rhs) { + if (lhs.Layer != rhs.Layer) { + return lhs.Layer < rhs.Layer; + } + return lhs.ObjectId < rhs.ObjectId; + }); + + const ImVec2 mousePosition = ImGui::GetMousePos(); + std::optional hit; + for (const UiPreviewItem& item : items) { + const float fitScale = std::min(viewportState.Width / item.ReferenceWidth, viewportState.Height / item.ReferenceHeight); + const float previewWidth = std::max(24.0F, item.ReferenceWidth * fitScale); + const float previewHeight = std::max(24.0F, item.ReferenceHeight * fitScale); + const ImVec2 rectMin( + viewportState.Left + (viewportState.Width - previewWidth) * 0.5F, + viewportState.Top + (viewportState.Height - previewHeight) * 0.5F + ); + if (const auto itemHit = MetaCoreFindUiPreviewButtonHitInRect( + *item.Document, + item.ObjectId, + item.Name, + false, + rectMin, + ImVec2(previewWidth, previewHeight), + mousePosition + ); + itemHit.has_value()) { + hit = itemHit; + } + } + return hit; +} + +[[nodiscard]] std::optional MetaCoreFindWorldSpaceUiPreviewHit( + const MetaCoreEditorContext& editorContext, + const MetaCoreScene& scene, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState +) { + if (!viewportState.Hovered || viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) { + return std::nullopt; + } + + const ImVec2 mousePosition = ImGui::GetMousePos(); + const glm::vec3 viewDirection = glm::normalize(sceneView.CameraTarget - sceneView.CameraPosition); + const float fovRadians = glm::radians(std::max(1.0F, sceneView.VerticalFieldOfViewDegrees)); + const float orthographicHeight = std::max(0.01F, sceneView.OrthographicSize * 2.0F); + std::optional hit; + + for (const MetaCoreGameObject& gameObject : scene.GetGameObjects()) { + if (!gameObject.HasComponent()) { + continue; + } + const MetaCoreUiRendererComponent& uiRenderer = gameObject.GetComponent(); + if (!uiRenderer.Visible || !uiRenderer.InputEnabled || uiRenderer.RenderMode != MetaCoreUiRenderMode::WorldSpace) { + continue; + } + + const std::optional document = MetaCoreLoadUiPreviewDocument(editorContext, uiRenderer); + if (!document.has_value()) { + continue; + } + + const MetaCoreTransformComponent& transform = gameObject.GetComponent(); + if (!uiRenderer.FaceCamera) { + const glm::mat4 worldMatrix = MetaCoreBuildUiPreviewWorldMatrix(transform, uiRenderer); + if (const auto itemHit = MetaCoreFindUiPreviewButtonHitInProjectedDocument( + *document, + gameObject.GetId(), + gameObject.GetName(), + sceneView, + viewportState, + worldMatrix, + mousePosition + ); + itemHit.has_value()) { + hit = itemHit; + } + continue; + } + + ImVec2 centerPoint{}; + if (!MetaCoreProjectWorldPointToViewport(sceneView, viewportState, transform.Position, centerPoint)) { + continue; + } + + const glm::vec2 worldSize = MetaCoreUiPreviewWorldSize(uiRenderer); + const float worldWidth = worldSize.x; + const float worldHeight = worldSize.y; + float pixelsPerWorldUnit = viewportState.Height / orthographicHeight; + if (sceneView.ProjectionMode == MetaCoreCameraProjectionMode::Perspective) { + const float distanceAlongView = glm::dot(transform.Position - sceneView.CameraPosition, viewDirection); + if (distanceAlongView <= sceneView.NearClip) { + continue; + } + pixelsPerWorldUnit = viewportState.Height / (2.0F * distanceAlongView * std::tan(fovRadians * 0.5F)); + } + const float rawPreviewWidth = worldWidth * pixelsPerWorldUnit; + const float rawPreviewHeight = worldHeight * pixelsPerWorldUnit; + const float maxPreviewWidth = std::max(64.0F, viewportState.Width * 0.9F); + const float maxPreviewHeight = std::max(64.0F, viewportState.Height * 0.9F); + const float fitScale = std::min(1.0F, std::min(maxPreviewWidth / std::max(1.0F, rawPreviewWidth), maxPreviewHeight / std::max(1.0F, rawPreviewHeight))); + const float previewWidth = std::max(24.0F, rawPreviewWidth * fitScale); + const float previewHeight = std::max(24.0F, rawPreviewHeight * fitScale); + const ImVec2 rectMin(centerPoint.x - previewWidth * 0.5F, centerPoint.y - previewHeight * 0.5F); + if (const auto itemHit = MetaCoreFindUiPreviewButtonHitInRect( + *document, + gameObject.GetId(), + gameObject.GetName(), + true, + rectMin, + ImVec2(previewWidth, previewHeight), + mousePosition + ); + itemHit.has_value()) { + hit = itemHit; + } + } + return hit; +} + +[[nodiscard]] std::optional MetaCoreFindUiPreviewHit( + const MetaCoreEditorContext& editorContext, + const MetaCoreScene& scene, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState +) { + std::optional hit = MetaCoreFindWorldSpaceUiPreviewHit(editorContext, scene, sceneView, viewportState); + if (const auto screenHit = MetaCoreFindScreenSpaceUiPreviewHit(editorContext, scene, viewportState); screenHit.has_value()) { + hit = screenHit; + } + return hit; +} + +[[nodiscard]] bool MetaCoreUpdateUiPreviewInteraction( + MetaCoreEditorContext& editorContext, + const MetaCoreScene& scene, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState +) { + const std::optional hit = MetaCoreFindUiPreviewHit(editorContext, scene, sceneView, viewportState); + GMetaCoreUiPreviewInteractionState.Hovered = hit.value_or(MetaCoreUiPreviewHit{}); + if (GMetaCoreUiPreviewInteractionState.Hovered.IsValid()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + } + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + if (GMetaCoreUiPreviewInteractionState.Hovered.IsValid()) { + GMetaCoreUiPreviewInteractionState.Pressed = GMetaCoreUiPreviewInteractionState.Hovered; + GMetaCoreUiPreviewInteractionState.CapturedMouse = true; + } else { + GMetaCoreUiPreviewInteractionState.Pressed = {}; + GMetaCoreUiPreviewInteractionState.CapturedMouse = false; + } + } + + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + if (GMetaCoreUiPreviewInteractionState.Pressed.IsValid() && + GMetaCoreUiPreviewInteractionState.Hovered.IsValid() && + MetaCoreSameUiPreviewHit(GMetaCoreUiPreviewInteractionState.Pressed, GMetaCoreUiPreviewInteractionState.Hovered)) { + MetaCoreDispatchUiPreviewAction(editorContext, GMetaCoreUiPreviewInteractionState.Hovered); + } + GMetaCoreUiPreviewInteractionState.Pressed = {}; + GMetaCoreUiPreviewInteractionState.CapturedMouse = false; + } else if (!ImGui::IsMouseDown(ImGuiMouseButton_Left) && GMetaCoreUiPreviewInteractionState.CapturedMouse) { + GMetaCoreUiPreviewInteractionState.Pressed = {}; + GMetaCoreUiPreviewInteractionState.CapturedMouse = false; + } + + return GMetaCoreUiPreviewInteractionState.Hovered.IsValid() || GMetaCoreUiPreviewInteractionState.CapturedMouse; +} + void MetaCoreDrawScreenSpaceUiPreviewOverlay( const MetaCoreEditorContext& editorContext, const MetaCoreScene& scene, - const MetaCoreSceneViewportState& viewportState + const MetaCoreSceneViewportState& viewportState, + const MetaCoreUiPreviewInteractionState* interactionState = nullptr ) { if (viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) { return; @@ -1263,13 +1926,20 @@ void MetaCoreDrawScreenSpaceUiPreviewOverlay( drawList->AddRectFilled(rectMin, rectMax, fillColor, 4.0F); drawList->AddRect(rectMin, rectMax, strokeColor, 4.0F, 0, selected ? 2.0F : 1.2F); if (item.Document.has_value()) { - MetaCoreDrawUiDocumentPreview(*drawList, *item.Document, rectMin, ImVec2(previewWidth, previewHeight)); + MetaCoreDrawUiDocumentPreview( + *drawList, + *item.Document, + rectMin, + ImVec2(previewWidth, previewHeight), + item.ObjectId, + interactionState + ); } - const std::string label = "2D UI: " + item.Name; - const ImVec2 labelPos(rectMin.x + 8.0F, rectMin.y + 7.0F); - drawList->AddText(labelPos, strokeColor, label.c_str()); if (!item.Document.has_value()) { + const std::string label = "2D UI: " + item.Name; + const ImVec2 labelPos(rectMin.x + 8.0F, rectMin.y + 7.0F); + drawList->AddText(labelPos, strokeColor, label.c_str()); drawList->AddText(ImVec2(rectMin.x + 8.0F, rectMin.y + 27.0F), IM_COL32(255, 196, 120, 230), "未能读取 UI 文档"); } } @@ -1281,7 +1951,8 @@ void MetaCoreDrawWorldSpaceUiPreviewOverlay( const MetaCoreEditorContext& editorContext, const MetaCoreScene& scene, const MetaCoreSceneView& sceneView, - const MetaCoreSceneViewportState& viewportState + const MetaCoreSceneViewportState& viewportState, + const MetaCoreUiPreviewInteractionState* interactionState = nullptr ) { if (viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) { return; @@ -1310,10 +1981,9 @@ void MetaCoreDrawWorldSpaceUiPreviewOverlay( const ImU32 fillColor = selected ? IM_COL32(44, 136, 110, 48) : IM_COL32(44, 136, 110, 30); const ImU32 strokeColor = selected ? IM_COL32(104, 224, 188, 230) : IM_COL32(104, 224, 188, 150); const std::optional document = MetaCoreLoadUiPreviewDocument(editorContext, uiRenderer); - const std::string label = "3D UI: " + gameObject.GetName(); if (!uiRenderer.FaceCamera) { - const glm::mat4 worldMatrix = MetaCoreBuildTransformMatrix(transform); + const glm::mat4 worldMatrix = MetaCoreBuildUiPreviewWorldMatrix(transform, uiRenderer); std::array panelPoints{}; if (!MetaCoreProjectUiQuad( sceneView, @@ -1329,11 +1999,20 @@ void MetaCoreDrawWorldSpaceUiPreviewOverlay( drawList->AddConvexPolyFilled(panelPoints.data(), static_cast(panelPoints.size()), fillColor); drawList->AddPolyline(panelPoints.data(), static_cast(panelPoints.size()), strokeColor, ImDrawFlags_Closed, selected ? 2.0F : 1.2F); if (document.has_value()) { - MetaCoreDrawProjectedUiDocumentPreview(*drawList, *document, sceneView, viewportState, worldMatrix); + MetaCoreDrawProjectedUiDocumentPreview( + *drawList, + *document, + sceneView, + viewportState, + worldMatrix, + gameObject.GetId(), + interactionState + ); } else { + const std::string label = "3D UI: " + gameObject.GetName(); drawList->AddText(ImVec2(panelPoints[0].x + 8.0F, panelPoints[0].y + 27.0F), IM_COL32(255, 196, 120, 230), "未能读取 UI 文档"); + drawList->AddText(ImVec2(panelPoints[0].x + 8.0F, panelPoints[0].y + 7.0F), strokeColor, label.c_str()); } - drawList->AddText(ImVec2(panelPoints[0].x + 8.0F, panelPoints[0].y + 7.0F), strokeColor, label.c_str()); ImVec2 centerPoint{}; if (MetaCoreProjectWorldPointToViewport(sceneView, viewportState, transform.Position, centerPoint)) { @@ -1347,8 +2026,9 @@ void MetaCoreDrawWorldSpaceUiPreviewOverlay( continue; } - const float worldWidth = std::max(0.05F, std::abs(transform.Scale.x) > 0.001F ? std::abs(transform.Scale.x) : uiRenderer.WorldSize.x); - const float worldHeight = std::max(0.05F, std::abs(transform.Scale.y) > 0.001F ? std::abs(transform.Scale.y) : uiRenderer.WorldSize.y); + const glm::vec2 worldSize = MetaCoreUiPreviewWorldSize(uiRenderer); + const float worldWidth = worldSize.x; + const float worldHeight = worldSize.y; float pixelsPerWorldUnit = viewportState.Height / orthographicHeight; if (sceneView.ProjectionMode == MetaCoreCameraProjectionMode::Perspective) { const float distanceAlongView = glm::dot(transform.Position - sceneView.CameraPosition, viewDirection); @@ -1371,12 +2051,20 @@ void MetaCoreDrawWorldSpaceUiPreviewOverlay( drawList->AddRect(rectMin, rectMax, strokeColor, 4.0F, 0, selected ? 2.0F : 1.2F); if (document.has_value()) { - MetaCoreDrawUiDocumentPreview(*drawList, *document, rectMin, ImVec2(previewWidth, previewHeight)); + MetaCoreDrawUiDocumentPreview( + *drawList, + *document, + rectMin, + ImVec2(previewWidth, previewHeight), + gameObject.GetId(), + interactionState + ); } else { + const std::string label = "3D UI: " + gameObject.GetName(); + drawList->AddText(ImVec2(rectMin.x + 8.0F, rectMin.y + 7.0F), strokeColor, label.c_str()); drawList->AddText(ImVec2(rectMin.x + 8.0F, rectMin.y + 27.0F), IM_COL32(255, 196, 120, 230), "未能读取 UI 文档"); } - drawList->AddText(ImVec2(rectMin.x + 8.0F, rectMin.y + 7.0F), strokeColor, label.c_str()); drawList->AddCircleFilled(centerPoint, 3.0F, strokeColor, 12); } @@ -1951,10 +2639,24 @@ void MetaCoreEditorApp::DrawEditorFrame() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0F, 4.0F)); ImGui::Begin("ViewportTabs", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav); - static int activeTab = 0; // 0: Scene, 1: Game - if (ImGui::Selectable(" 场景 ", activeTab == 0, 0, ImVec2(70, 0))) activeTab = 0; + MetaCoreEditorWorkspacePage activeWorkspacePage = EditorContext_->GetWorkspacePage(); + if (ImGui::Selectable(" 场景 ", activeWorkspacePage == MetaCoreEditorWorkspacePage::Scene, 0, ImVec2(70, 0))) { + EditorContext_->SetWorkspacePage(MetaCoreEditorWorkspacePage::Scene); + activeWorkspacePage = MetaCoreEditorWorkspacePage::Scene; + } ImGui::SameLine(); - if (ImGui::Selectable(" 游戏 ", activeTab == 1, 0, ImVec2(70, 0))) activeTab = 1; + if (ImGui::Selectable(" 游戏 ", activeWorkspacePage == MetaCoreEditorWorkspacePage::Game, 0, ImVec2(70, 0))) { + EditorContext_->SetWorkspacePage(MetaCoreEditorWorkspacePage::Game); + activeWorkspacePage = MetaCoreEditorWorkspacePage::Game; + } + ImGui::SameLine(); + const auto uiDesignerService = ModuleRegistry_.ResolveService(); + const std::string uiTabLabel = + uiDesignerService != nullptr && uiDesignerService->HasDirtyDocument() ? " UI* " : " UI "; + if (ImGui::Selectable(uiTabLabel.c_str(), activeWorkspacePage == MetaCoreEditorWorkspacePage::UiDesigner, 0, ImVec2(70, 0))) { + EditorContext_->SetWorkspacePage(MetaCoreEditorWorkspacePage::UiDesigner); + activeWorkspacePage = MetaCoreEditorWorkspacePage::UiDesigner; + } ImGui::End(); ImGui::PopStyleVar(); @@ -1963,7 +2665,6 @@ void MetaCoreEditorApp::DrawEditorFrame() { constexpr float sceneToolbarHeight = 0.0F; constexpr float tabHeaderHeight = 30.0F; MetaCoreSceneViewportState& viewportState = EditorContext_->GetSceneViewportState(); - const ImVec2 mainViewportPosition = ImGui::GetMainViewport()->Pos; viewportState.Left = centralNode->Pos.x; viewportState.Top = centralNode->Pos.y + sceneToolbarHeight + tabHeaderHeight; viewportState.Width = centralWidth; @@ -1975,6 +2676,35 @@ void MetaCoreEditorApp::DrawEditorFrame() { viewportState.Height = 1.0F; } + if (activeWorkspacePage == MetaCoreEditorWorkspacePage::UiDesigner) { + SceneInteractionService_.ResetFrameState(); + ViewportRenderer_.SetEditorGridVisible(false); + ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{}); + + ImGui::SetNextWindowPos(ImVec2(centralNode->Pos.x, centralNode->Pos.y + sceneToolbarHeight + tabHeaderHeight)); + ImGui::SetNextWindowSize(ImVec2(viewportState.Width, viewportState.Height)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0F); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0F); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0F, 8.0F)); + if (ImGui::Begin( + "MetaCoreUiDesignerPage", + nullptr, + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoBringToFrontOnFocus + )) { + if (const auto uiDesignerService = ModuleRegistry_.ResolveService(); + uiDesignerService != nullptr) { + uiDesignerService->DrawDesignerPage(*EditorContext_); + } else { + ImGui::TextDisabled("UI 设计器服务不可用。"); + } + } + ImGui::End(); + ImGui::PopStyleVar(3); + } else { // --- 补丁三重构开始 --- // 1. 准备 GizmoCanvas 窗口属性 @@ -2030,7 +2760,12 @@ void MetaCoreEditorApp::DrawEditorFrame() { mousePosition.y >= viewportState.Top && mousePosition.y <= viewportState.Top + viewportState.Height; viewportState.Focused = viewportState.Hovered && !ImGui::GetIO().WantCaptureMouse; - const bool isGameView = activeTab == 1; + const bool isGameView = activeWorkspacePage == MetaCoreEditorWorkspacePage::Game; + const MetaCoreSceneView preInteractionSceneView = EditorContext_->GetCameraController().BuildSceneView(); + const bool uiPreviewHoveredBeforeCamera = + !isGameView && + (GMetaCoreUiPreviewInteractionState.CapturedMouse || + MetaCoreFindUiPreviewHit(*EditorContext_, Scene_, preInteractionSceneView, viewportState).has_value()); // 处理聚焦快捷键 if (!isGameView && @@ -2049,7 +2784,7 @@ void MetaCoreEditorApp::DrawEditorFrame() { // 5. 更新相机并构建 SceneView (在渲染之前!) gizmoUsing = ImGuizmo::IsUsing(); - if (!isGameView && !gizmoUsing) { + if (!isGameView && !gizmoUsing && !uiPreviewHoveredBeforeCamera) { EditorContext_->GetCameraController().Update( viewportState, EditorContext_->GetInput(), @@ -2155,14 +2890,28 @@ void MetaCoreEditorApp::DrawEditorFrame() { } } + const bool uiPreviewConsumesMouse = + MetaCoreUpdateUiPreviewInteraction(*EditorContext_, Scene_, sceneView, viewportState); + // 绘制 Overlays if (!isGameView) { MetaCoreDrawWorldOriginOverlay(*EditorContext_, sceneView, viewportState); MetaCoreDrawSelectionBoundsOverlay(*EditorContext_, Scene_, sceneView, viewportState); MetaCoreDrawSelectionHierarchyOverlay(*EditorContext_, Scene_, sceneView, viewportState); - MetaCoreDrawScreenSpaceUiPreviewOverlay(*EditorContext_, Scene_, viewportState); - MetaCoreDrawWorldSpaceUiPreviewOverlay(*EditorContext_, Scene_, sceneView, viewportState); } + MetaCoreDrawWorldSpaceUiPreviewOverlay( + *EditorContext_, + Scene_, + sceneView, + viewportState, + &GMetaCoreUiPreviewInteractionState + ); + MetaCoreDrawScreenSpaceUiPreviewOverlay( + *EditorContext_, + Scene_, + viewportState, + &GMetaCoreUiPreviewInteractionState + ); // 启用资产拖拽到 3D 场景视口的高亮响应遮罩,并显示操作提示 if (!isGameView) { @@ -2171,7 +2920,7 @@ void MetaCoreEditorApp::DrawEditorFrame() { // 处理拾取 if (!isGameView && viewportState.Hovered && !gizmoHovering && !gizmoUsing && - !ImGui::GetIO().WantCaptureMouse && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + !uiPreviewConsumesMouse && !ImGui::GetIO().WantCaptureMouse && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { const MetaCoreId pickedObjectId = SceneInteractionService_.PickGameObjectFromViewport( *EditorContext_, sceneView, @@ -2185,7 +2934,7 @@ void MetaCoreEditorApp::DrawEditorFrame() { ImGui::PopStyleVar(3); SceneInteractionService_.HandleGizmoEndUse(*EditorContext_, gizmoUsing); - if (activeTab == 1 && shouldDrawGameCameraSelector) { + if (activeWorkspacePage == MetaCoreEditorWorkspacePage::Game && shouldDrawGameCameraSelector) { MetaCoreDrawGameCameraSelector( *EditorContext_, gameViewSnapshot, @@ -2193,10 +2942,11 @@ void MetaCoreEditorApp::DrawEditorFrame() { gameCameraSelectorViewportSize ); } - if (activeTab == 0) { + if (activeWorkspacePage == MetaCoreEditorWorkspacePage::Scene) { SceneInteractionService_.DrawViewportToolbar(*EditorContext_); } // --- 补丁三重构结束 --- + } } else { SceneInteractionService_.ResetFrameState(); ViewportRenderer_.SetEditorGridVisible(false); diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp index 40e56aa..c5fd35f 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp @@ -763,6 +763,14 @@ void MetaCoreEditorContext::ClearSelectedAssetSubId() { void MetaCoreEditorContext::SetDockLayoutBuilt(bool built) { DockLayoutBuilt_ = built; } bool MetaCoreEditorContext::HasDockLayoutBuilt() const { return DockLayoutBuilt_; } +MetaCoreEditorWorkspacePage MetaCoreEditorContext::GetWorkspacePage() const { + return WorkspacePage_; +} + +void MetaCoreEditorContext::SetWorkspacePage(MetaCoreEditorWorkspacePage page) { + WorkspacePage_ = page; +} + void MetaCoreEditorContext::NormalizeSelection() { std::vector sanitizedSelection; sanitizedSelection.reserve(SelectedObjectIds_.size()); diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h index 5b5a0a9..0d1e4fe 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h @@ -43,6 +43,12 @@ enum class MetaCoreReparentTransformRule { KeepLocal }; +enum class MetaCoreEditorWorkspacePage { + Scene = 0, + Game, + UiDesigner +}; + MC_STRUCT() struct MetaCoreEditorStateSnapshot { MC_GENERATED_BODY() @@ -187,6 +193,8 @@ public: void ClearSelectedAssetSubId(); void SetDockLayoutBuilt(bool built); [[nodiscard]] bool HasDockLayoutBuilt() const; + [[nodiscard]] MetaCoreEditorWorkspacePage GetWorkspacePage() const; + void SetWorkspacePage(MetaCoreEditorWorkspacePage page); private: void NormalizeSelection(); @@ -221,6 +229,7 @@ private: MetaCoreSelectedAssetState SelectedAsset_{}; std::filesystem::path SelectedProjectDirectory_{"Assets"}; std::string SelectedAssetSubId_{}; + MetaCoreEditorWorkspacePage WorkspacePage_ = MetaCoreEditorWorkspacePage::Scene; }; } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index 1dbb633..4cf3beb 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -298,6 +298,17 @@ public: [[nodiscard]] virtual bool RevertSelectedPrefabInstance(MetaCoreEditorContext& editorContext) = 0; }; +class MetaCoreIUiDesignerService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual bool OpenUiDocument( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetRecord& assetRecord + ) = 0; + + virtual void DrawDesignerPage(MetaCoreEditorContext& editorContext) = 0; + [[nodiscard]] virtual bool HasDirtyDocument() const = 0; +}; + class MetaCoreIComponentTypeRegistry : public MetaCoreIEditorService { public: struct MetaCoreComponentDescriptor { diff --git a/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp b/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp index 452f08f..3f4ee82 100644 --- a/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp @@ -197,43 +197,6 @@ MetaCoreSceneRenderSyncSnapshot MetaCoreSceneRenderSync::BuildSnapshot(const Met }); } - if (!gameObject.HasComponent() && - gameObject.HasComponent()) { - const auto& uiRenderer = gameObject.GetComponent(); - if (uiRenderer.Visible && uiRenderer.RenderMode == MetaCoreUiRenderMode::WorldSpace) { - snapshot.Renderables.push_back(MetaCoreRenderSyncRenderable{ - objectId, - parentId, - gameObject.GetName(), - localMatrix, - worldMatrix, - true, - MetaCoreMeshSourceKind::Builtin, - MetaCoreBuiltinMeshType::Cube, - MetaCoreAssetGuid{}, - MetaCoreAssetGuid{}, - {}, - -1, - objectId, - gameObject.GetName(), - false, - {}, - glm::vec3(0.12F, 0.38F, 0.78F), - 0.0F, - 0.38F, - glm::vec3(0.02F, 0.08F, 0.16F), - MetaCoreMeshAlphaMode::Opaque, - 0.5F, - true, - {}, - {}, - {}, - {}, - {} - }); - } - } - if (gameObject.HasComponent()) { const auto& camera = gameObject.GetComponent(); MetaCoreRenderSyncCamera syncCamera; diff --git a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp index d1aaeb4..77e1031 100644 --- a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp @@ -715,6 +715,9 @@ static json UiNodeToJson(const MetaCoreUiNodeDocument& node) { j["Style"] = UiStyleToJson(node.Style); j["Text"] = node.Text; j["Interactable"] = node.Interactable; + j["Action"] = node.Action; + j["DataBinding"] = node.DataBinding; + j["DataFormat"] = node.DataFormat; return j; } @@ -730,6 +733,9 @@ static MetaCoreUiNodeDocument JsonToUiNode(const json& j) { if (j.contains("Style")) node.Style = JsonToUiStyle(j["Style"]); if (j.contains("Text")) node.Text = j["Text"].get(); if (j.contains("Interactable")) node.Interactable = j["Interactable"].get(); + if (j.contains("Action")) node.Action = j["Action"].get(); + if (j.contains("DataBinding")) node.DataBinding = j["DataBinding"].get(); + if (j.contains("DataFormat")) node.DataFormat = j["DataFormat"].get(); return node; } @@ -744,6 +750,8 @@ bool MetaCoreSceneSerializer::SaveUiToJson( uiJson["Name"] = uiDocument.Name; uiJson["ReferenceWidth"] = uiDocument.ReferenceWidth; uiJson["ReferenceHeight"] = uiDocument.ReferenceHeight; + uiJson["BackgroundColor"] = Vec3ToJson(uiDocument.BackgroundColor); + uiJson["BackgroundAlpha"] = uiDocument.BackgroundAlpha; uiJson["RootNodeIds"] = uiDocument.RootNodeIds; json nodesArray = json::array(); @@ -780,6 +788,8 @@ std::optional MetaCoreSceneSerializer::LoadUiFromJson( if (uiJson.contains("Name")) doc.Name = uiJson["Name"].get(); if (uiJson.contains("ReferenceWidth")) doc.ReferenceWidth = uiJson["ReferenceWidth"].get(); if (uiJson.contains("ReferenceHeight")) doc.ReferenceHeight = uiJson["ReferenceHeight"].get(); + if (uiJson.contains("BackgroundColor")) doc.BackgroundColor = JsonToVec3(uiJson["BackgroundColor"]); + if (uiJson.contains("BackgroundAlpha")) doc.BackgroundAlpha = uiJson["BackgroundAlpha"].get(); if (uiJson.contains("RootNodeIds")) doc.RootNodeIds = uiJson["RootNodeIds"].get>(); if (uiJson.contains("Nodes") && uiJson["Nodes"].is_array()) { diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h index 0299327..56e98a9 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h @@ -196,6 +196,15 @@ struct MetaCoreUiNodeDocument { MC_PROPERTY() bool Interactable = false; + + MC_PROPERTY() + std::string Action{}; + + MC_PROPERTY() + std::string DataBinding{}; + + MC_PROPERTY() + std::string DataFormat{}; }; MC_STRUCT() @@ -211,6 +220,12 @@ struct MetaCoreUiDocument { MC_PROPERTY() std::int32_t ReferenceHeight = 1080; + MC_PROPERTY() + glm::vec3 BackgroundColor{0.0F, 0.0F, 0.0F}; + + MC_PROPERTY() + float BackgroundAlpha = 0.0F; + MC_PROPERTY() std::vector RootNodeIds{}; diff --git a/docs/designs/README.md b/docs/designs/README.md index e4286f8..61ea8e6 100644 --- a/docs/designs/README.md +++ b/docs/designs/README.md @@ -28,6 +28,7 @@ 2. 引擎能力与里程碑 - [metacore-engine-feature-baseline.md](D:/MetaCore/docs/designs/metacore-engine-feature-baseline.md) + - [metacore-mature-engine-gap-and-work-plan.md](D:/MetaCore/docs/designs/metacore-mature-engine-gap-and-work-plan.md) - [metacore-phase1-roadmap.md](D:/MetaCore/docs/designs/metacore-phase1-roadmap.md) - [metacore-phase1-m1-m4-tasklist-and-acceptance.md](D:/MetaCore/docs/designs/metacore-phase1-m1-m4-tasklist-and-acceptance.md) - [metacore-phase1-m1-m4-workstreams.md](D:/MetaCore/docs/designs/metacore-phase1-m1-m4-workstreams.md) @@ -41,16 +42,17 @@ ## 当前执行建议 -如果现在开始第一阶段开发,建议直接从下面两份文档开始: +如果现在开始第一阶段开发,建议直接从下面三份文档开始: +- [metacore-mature-engine-gap-and-work-plan.md](D:/MetaCore/docs/designs/metacore-mature-engine-gap-and-work-plan.md) - [metacore-phase1-roadmap.md](D:/MetaCore/docs/designs/metacore-phase1-roadmap.md) - [metacore-m1-p0-task-cards.md](D:/MetaCore/docs/designs/metacore-m1-p0-task-cards.md) 对应执行顺序: -1. 先按 roadmap 对齐阶段目标 -2. 再按 `M1 P0` 开任务卡 -3. 先做项目路径、项目文件和 startup scene 主链 +1. 先用差距与工作计划确认当前 P0 阻塞项 +2. 再按 roadmap 对齐阶段目标 +3. 把尚未完成的 P0 项拆入具体任务卡和验收矩阵 ## 专项设计文档 diff --git a/docs/designs/metacore-mature-engine-gap-and-work-plan.md b/docs/designs/metacore-mature-engine-gap-and-work-plan.md new file mode 100644 index 0000000..2fa6262 --- /dev/null +++ b/docs/designs/metacore-mature-engine-gap-and-work-plan.md @@ -0,0 +1,339 @@ +# MetaCore 与成熟引擎的差距及工作计划 + +更新时间:2026-07-13 +状态:当前差距评估与执行建议 +读者:产品、架构、引擎、编辑器、渲染、工具链、测试、交付 + +## 目的 + +本文基于当前仓库代码、测试和已有设计文档,回答两个问题: + +1. MetaCore 与 Unity、Unreal Engine、Godot 等成熟通用引擎相比,还缺少什么。 +2. 在 MetaCore 当前“工业仿真、数字孪生、国产化 C/S 交付”的产品定位下,接下来真正需要做什么。 + +本文不是要求第一阶段复制成熟引擎的全部功能,而是把“通用能力差距”和“当前产品必须补齐的能力”分开管理,避免功能面无限扩张。 + +## 当前结论 + +MetaCore 已经超过单纯编辑器 Demo 阶段,当前具备: + +- EnTT ECS 与 `GameObject + Component` 场景语义。 +- 场景层级、选择、复制、删除、重挂接、保存和加载。 +- ImGui 编辑器壳、Hierarchy、Scene、Inspector、Project、Console 等基础工作流。 +- Filament 场景渲染、PBR、glTF/GLB、环境光、基础阴影和编辑器离屏视口。 +- GUID、meta、Asset Database、导入、重导入、依赖图、热重载和 Cook 基础链路。 +- 基础 Prefab 创建、实例化、Apply 和 Revert。 +- 原生 C++ 行为注册及 `OnPlayStart / FixedUpdate / Update / LateUpdate / OnPlayStop` 生命周期。 +- glTF 动画元数据导入、Clip 选择、循环、速度控制和运行时播放。 +- RmlUi 运行时 UI、UI 文档和可视编辑工作流基础。 +- 独立 Player、RuntimeData TCP/文件回放、场景绑定和诊断。 + +截至 2026-07-13,当前 `MetaCoreSmokeTests` 构建成功并全部通过,一次完整运行耗时约 65.90 秒。 + +综合判断:项目处于“早期 Alpha 的垂直引擎底座”阶段。架构主线已经成立,但生产可用性、运行时系统广度、性能工程和发布可靠性仍明显弱于成熟引擎。 + +## 差距总览 + +| 能力域 | 当前基础 | 主要问题 | 产品优先级 | +| --- | --- | --- | --- | +| 发布与部署 | Player、Cook、运行时配置 | 尚未形成可重复、可验证的完整交付包 | P0 | +| 资产管线 | GUID、meta、依赖、热重载、glTF | Import Settings、派生数据、预览和引用修复不完整 | P0 | +| 用户逻辑开发 | 原生 C++ 行为和生命周期 | 缺用户脚本工程、编译热重载、调试和通用事件/时间系统 | P0 | +| 输入与运行时交互 | 键鼠采集、编辑器交互 | 缺 Input Map、运行时事件和统一输入路由 | P0 | +| 物理 | 未形成独立模块 | 缺碰撞、刚体、射线、触发器和角色控制 | P1;有仿真交互需求时升 P0 | +| 渲染生产力 | Filament、PBR、glTF、环境光 | 灯光、阴影、后处理、LOD、粒子和调试视图不足 | P1 | +| 动画 | Clip 导入与播放 | 缺状态机、混合、重定向、Root Motion 和动画工具 | P1 | +| UI | RmlUi 运行时与编辑器基础 | 控件、事件、适配、数据绑定和编辑/运行一致性不足 | P1 | +| Scene / Prefab | 单场景和基础 Prefab | 缺多场景、流式加载、嵌套 Prefab、Variant 和细粒度 Override | P1 | +| 工程基础设施 | CMake、代码生成、Smoke Tests | 缺分层测试、性能回归、GPU 回归、CI 和崩溃诊断 | P0/P1 | +| 音频 | 无正式系统 | 缺完整音频资源和运行时 | P2 | +| 导航 / AI | 无正式系统 | 缺 NavMesh、寻路、避障和调试工具 | P2 | +| XR / Web | 主要为架构预留 | 缺 OpenXR 与 Web 构建交付闭环 | 后续阶段 | + +## P0:当前必须解决的问题 + +### 1. 发布与部署还不具备正式交付厚度 + +#### 问题 + +- Cook 已存在,但尚未形成统一的一键 Build / Package 工作流。 +- 缺少 Debug、Development、Release 以及目标平台构建 Profile。 +- 缺少运行库、Filament、字体、材质和项目内容的自动依赖收集。 +- 缺少从空输出目录构建以及 clean machine 运行验证。 +- Linux Player 启动流程仍存在未接入路径。 +- 缺少发布清单、引擎/资产版本兼容规则、补丁和升级策略。 +- 现场崩溃后的日志、Dump、符号和诊断收集能力不足。 + +#### 需要做的事 + +- [ ] 定义统一 `BuildProfile`:平台、配置、图形后端、启动场景、输出目录和资源策略。 +- [ ] 建立 `Build -> Cook -> Stage -> Package -> Validate` 命令链。 +- [ ] 自动收集可执行文件、动态库、Filament 材质、RmlUi、字体、配置和 cooked 资产。 +- [ ] 生成包含文件 Hash、版本和依赖信息的发布 Manifest。 +- [ ] 完成 Windows 与 Linux Player 启动和路径规则统一。 +- [ ] 建立 clean-machine 或干净容器验证脚本。 +- [ ] 增加缺文件、损坏资产、版本不兼容时的明确错误报告。 +- [ ] 建立崩溃 Dump、日志归档和现场诊断包导出能力。 + +#### 验收标准 + +- 从干净输出目录执行一次命令即可生成完整交付包。 +- 交付包不依赖源码目录、vcpkg 安装目录或编辑器工作目录。 +- 在未安装开发工具链的目标机器上可以启动、加载启动场景并显示 UI。 +- 删除任一必需文件后,验证工具能够在交付前报告具体缺失项。 + +### 2. 资产管线还不是生产级 + +#### 问题 + +- glTF/GLB 是当前正式主线,但 FBX/OBJ 等格式仍偏占位或不完整。 +- 纹理缺少色彩空间、压缩、Mipmap、最大尺寸、法线贴图等 Import Settings。 +- 缺少稳定的缩略图、资源预览和派生数据缓存。 +- 批量导入和复杂重导入可能阻塞编辑器主线程。 +- 移动、重命名、删除和重导入后的引用检查与修复工具仍不完整。 +- 子资产、平台差异化 Cook 和缓存失效规则仍需强化。 + +#### 需要做的事 + +- [ ] 明确 glTF/GLB 的正式支持矩阵和导入错误规范。 +- [ ] 删除或明确标识通用占位导入结果,避免占位资产被当成成功导入。 +- [ ] 建立 Model、Texture、Material 的版本化 Import Settings。 +- [ ] 实现纹理色彩空间、Mipmap、尺寸、压缩和法线贴图设置。 +- [ ] 建立缩略图与预览缓存,并定义缓存失效条件。 +- [ ] 把大型模型解析、纹理处理和派生数据生成移出 UI 主线程。 +- [ ] 增加资源引用检查、孤立资源检查和 GUID/路径修复工具。 +- [ ] 建立导入黄金样本库,覆盖层级、材质、贴图、动画和损坏输入。 + +#### 验收标准 + +- 同一源资产和相同设置可生成稳定一致的导入结果。 +- 重导入不改变稳定子资产 GUID,也不破坏 Scene、Prefab 和 Material 引用。 +- 大型资产导入期间编辑器仍可响应,并显示进度、取消和错误信息。 +- 资产移动、重命名或删除后可准确列出受影响引用。 + +### 3. 用户逻辑开发闭环不完整 + +#### 问题 + +- 当前原生 Script Registry 能承载内置行为,但还不是面向项目开发者的成熟脚本系统。 +- 缺少用户脚本项目的创建、编译、加载、卸载和热重载流程。 +- 缺少稳定的脚本 API、组件访问、安全失效和版本兼容规则。 +- 缺少脚本断点、调用栈、字段错误和生命周期异常诊断。 +- 缺少统一事件总线、定时器、协程或任务调度抽象。 + +#### 需要做的事 + +- [ ] 明确第一阶段脚本路线:原生 C++ 插件、嵌入式脚本语言,或二者的阶段关系。 +- [ ] 建立项目脚本模板、构建目标和编辑器编译入口。 +- [ ] 支持脚本模块加载、卸载和失败回退。 +- [ ] 定义脚本 API 稳定边界和对象/组件句柄失效规则。 +- [ ] 支持可序列化字段、资源引用和 GameObject 引用。 +- [ ] 增加统一时间、定时器和事件系统。 +- [ ] 增加脚本错误定位、日志上下文和运行时异常隔离。 +- [ ] 在明确安全边界后实现脚本热重载。 + +#### 验收标准 + +- 用户可以在项目目录创建一个新行为并在编辑器中完成编译和挂载。 +- 行为字段可编辑、保存、加载,并进入 Player。 +- 编译或加载失败不会破坏当前场景,且能定位到具体脚本和错误。 +- Play/Stop 后运行态修改按照明确规则恢复或保留。 + +### 4. 缺少通用运行时输入与交互层 + +#### 问题 + +- 当前输入主要服务编辑器相机、选择和 Gizmo。 +- 缺少 Action / Axis、输入配置资源和用户重绑定。 +- 3D 对象、运行时 UI 和脚本之间缺少统一事件路由。 +- 缺少运行时 Raycast、Hover、Click、Drag 等高层交互语义。 + +#### 需要做的事 + +- [ ] 定义 Input Action、Axis、Binding 和 Input Context 数据模型。 +- [ ] 支持键盘、鼠标和基础手柄映射。 +- [ ] 建立编辑器输入、Game View 输入和 Player 输入的焦点规则。 +- [ ] 统一 RmlUi 与 3D 场景输入路由和事件消费顺序。 +- [ ] 为运行时对象提供点击、悬停、拖拽和射线命中事件。 +- [ ] 将输入配置纳入项目、Cook 和 Player 加载链路。 + +#### 验收标准 + +- 同一份输入配置可以在编辑器 Play Mode 和独立 Player 中工作。 +- UI 捕获输入时不会错误操作 3D 场景。 +- 用户可修改按键绑定而不重新编译项目。 + +### 5. 工程验证不足以支撑长期交付 + +#### 问题 + +- 测试覆盖面已有不错基础,但大部分集中在一个耗时约一分钟的 Smoke Test 程序中。 +- 缺少模块级单元测试、GPU 截图回归、性能基准和长稳测试。 +- 缺少 Windows、Linux、统信、麒麟等目标平台的持续构建矩阵。 +- 缺少内存、资源生命周期、启动时间和场景帧时间回归门槛。 + +#### 需要做的事 + +- [ ] 把 Smoke Tests 拆分为 Foundation、Scene、Assets、Editor、RuntimeData、RuntimeUi 等测试目标。 +- [ ] 保留端到端 Smoke Tests,同时缩短单次反馈时间。 +- [ ] 增加 glTF、Scene、Prefab、Material、UI 的黄金文件回归测试。 +- [ ] 增加 Filament 关键场景截图对比测试。 +- [ ] 建立启动、导入、加载、帧时间和内存基准。 +- [ ] 建立目标平台 CI 构建与 Player 启动检查。 +- [ ] 增加 AddressSanitizer、UndefinedBehaviorSanitizer 或对应平台内存诊断任务。 + +#### 验收标准 + +- 普通模块修改能在较短时间内得到对应测试结果。 +- 渲染输出、资产格式或性能发生非预期变化时,CI 能够发现。 +- 每个正式支持平台至少有构建、启动和最小场景加载验证。 + +## P1:主干闭环后需要补强的能力 + +### 6. 物理与空间查询 + +需要引入可替换的物理模块边界,并逐步提供: + +- [ ] Collider、RigidBody、Trigger 和 Physics Material。 +- [ ] Raycast、Shape Cast、Overlap Query。 +- [ ] 固定时间步、碰撞层和碰撞矩阵。 +- [ ] 约束、角色控制器和物理调试视图。 +- [ ] 场景序列化、Prefab、脚本回调和 Player 集成。 + +第一阶段若只展示静态数字孪生场景,可以延后刚体模拟,但空间查询和触发交互通常应优先实现。 + +### 7. 渲染生产力 + +需要补齐: + +- [ ] Directional、Point、Spot Light 的完整组件语义。 +- [ ] 阴影开关、级联、质量、偏移和性能配置。 +- [ ] 天空盒、IBL、反射探针和环境配置资产化。 +- [ ] 后处理 Profile:曝光、色调映射、Bloom、抗锯齿等。 +- [ ] Shader/Material 扩展工作流,而不只依赖固定 PBR 参数。 +- [ ] LOD Group、实例化、遮挡剔除和大型场景可见性策略。 +- [ ] 粒子、Line/Trail、Decal、Terrain 等常用表现组件。 +- [ ] Overdraw、Wireframe、法线、光照和性能调试视图。 + +### 8. 动画系统 + +当前 Clip 播放基础之上,需要逐步增加: + +- [ ] Animator Controller 和状态机。 +- [ ] Cross Fade、Blend Tree、Layer 和 Mask。 +- [ ] 骨骼 Avatar、动画重定向和 Root Motion。 +- [ ] 动画事件、参数驱动和脚本控制 API。 +- [ ] 动画预览、时间轴和状态调试器。 +- [ ] 机械装配动画和 Timeline 类编排能力。 + +### 9. UI 系统 + +需要补齐: + +- [ ] 稳定的 Button、Image、Text、Input、List、Scroll 等控件库。 +- [ ] UI 事件、焦点、键盘导航和输入消费规则。 +- [ ] Anchor、缩放、DPI、宽高比和多分辨率适配。 +- [ ] 主题、字体、图集、本地化和基础动效。 +- [ ] UI 到 RuntimeData、脚本和场景对象的统一数据绑定。 +- [ ] 编辑器预览与 Player 渲染的一致性测试。 + +### 10. Scene 与 Prefab + +需要补齐: + +- [ ] 多场景编辑、Additive Load、SubScene 和异步流式加载。 +- [ ] 大场景分区、按需加载和场景依赖检查。 +- [ ] 嵌套 Prefab、Prefab Variant 和实例同步。 +- [ ] 细粒度 Override 记录、可视化、Apply 和 Revert。 +- [ ] 跨场景对象引用规则。 +- [ ] Scene、Component、Prefab 的格式版本迁移机制。 + +### 11. 编辑器生产力与插件边界 + +需要补齐: + +- [ ] 完整 Game View 与分辨率/输入模拟。 +- [ ] 通用资源拖拽、上下文菜单、搜索和命令面板。 +- [ ] 布局持久化、崩溃恢复和场景自动保存。 +- [ ] 项目模板、创建向导、Build Settings 和平台设置面板。 +- [ ] 稳定的插件 SDK、扩展点、版本检查和插件隔离。 +- [ ] 大型 Hierarchy、Project Browser 和 Inspector 的性能优化。 + +## P2:按产品需要扩展 + +这些属于成熟通用引擎常见能力,但不应阻塞第一阶段工业 C/S 交付: + +- 音频资源、3D 音频、混音器和流式播放。 +- NavMesh、寻路、动态避障和导航调试。 +- 通用网络复制、RPC、会话和预测。 +- Terrain、植被、复杂粒子和电影级 Timeline。 +- OpenXR、VR 交互和设备适配。 +- WebGPU、浏览器资源加载和 Web 发布。 +- 多人协作编辑、资源锁和版本控制深度集成。 +- 可视化脚本、Shader Graph 和高级内容创作工具。 + +## 代码结构风险 + +当前代码虽已建立服务接口和模块机制,但大型实现仍然过度集中: + +- `MetaCoreBuiltinCoreServicesModule.cpp` 约 1 万行。 +- `MetaCoreBuiltinEditorModule.cpp` 约 9500 行。 +- `MetaCoreEditorApp.cpp` 约 3000 行。 +- `MetaCoreSmokeTests.cpp` 约 5500 行。 + +继续在这些文件中横向增加功能,会逐步放大编译时间、合并冲突、测试隔离和模块替换成本。 + +需要做的事: + +- [ ] 按 Asset、Prefab、Script、PlayMode、Material、UI、Build 等职责拆分实现文件。 +- [ ] 保留现有服务接口,把注册组合留在 Builtin Module 中。 +- [ ] 将无 UI 的业务逻辑从 ImGui Panel 中抽离为可测试服务。 +- [ ] 避免 Editor 直接承载 Player 所需的通用运行时逻辑。 +- [ ] 建立清晰依赖方向,防止 Foundation、Scene、Runtime 反向依赖 Editor。 +- [ ] 为资源处理、Cook 和后台任务建立统一 Job/Task 抽象。 + +## 推荐执行顺序 + +```text +R1:可交付闭环 + Build Profile -> Stage/Package -> clean-machine 验证 -> 崩溃诊断 + +R2:生产级内容管线 + Import Settings -> 异步导入 -> 缩略图/缓存 -> 引用检查与修复 + +R3:应用逻辑与交互 + 用户脚本工程 -> 时间/事件 -> Input Map -> UI/3D 事件路由 + +R4:表现与内容复用 + UI 强化 -> 灯光/阴影/后处理 -> 动画状态机 -> Prefab/Scene 强化 + +R5:工程化与平台化 + 测试拆分 -> 性能回归 -> 平台 CI -> 长稳与资源泄漏验证 + +R6:按项目需求扩展 + 物理 -> 音频 -> 导航 -> XR/Web -> 其他通用引擎能力 +``` + +其中 R1、R2、R3 是 MetaCore 从“可展示原型”进入“真实项目生产”的关键,不应被 P2 功能插队。 + +## 阶段完成定义 + +当以下条件全部成立时,可以认为 MetaCore 已从早期 Alpha 进入可交付基础版本: + +- [ ] 用户可以创建项目、导入正式支持的模型和编辑场景。 +- [ ] 材质、动画、UI、Prefab 和用户行为能够保存、Cook 并在 Player 中运行。 +- [ ] 项目可以通过统一命令生成不依赖开发环境的交付包。 +- [ ] 同一项目在 Editor Play Mode 和独立 Player 中的核心行为一致。 +- [ ] 资产重导入、移动和重命名不会静默破坏引用。 +- [ ] 脚本、资产和运行时配置错误均有可定位的诊断信息。 +- [ ] 目标平台具备自动构建、启动、最小渲染和长稳验证。 +- [ ] 关键工作流有分层测试、黄金资产回归和性能基线保护。 + +## 与既有计划的关系 + +本文是现状评估和后续任务入口,不替代已有专项设计: + +- 产品范围仍以 `metacore-product-plan.md` 和 `metacore-phase1-scope.md` 为准。 +- 现有 M1-M4 文档记录第一阶段原始里程碑,但其中部分状态已落后于当前代码。 +- 发布、UI、模型和材质的详细实现继续参考对应专项设计。 +- 后续更新 Roadmap 时,应优先把本文 P0 项合并进正式里程碑和验收矩阵。 diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 85cb1f9..f06214f 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -4282,6 +4282,8 @@ void MetaCoreTestUiDocumentSerialization() { document.Name = "MainHud"; document.ReferenceWidth = 1920; document.ReferenceHeight = 1080; + document.BackgroundColor = glm::vec3(0.02F, 0.04F, 0.08F); + document.BackgroundAlpha = 0.65F; document.RootNodeIds = {"root.panel"}; MetaCore::MetaCoreUiNodeDocument rootNode; @@ -4315,6 +4317,9 @@ void MetaCoreTestUiDocumentSerialization() { buttonNode.ParentId = "root.panel"; buttonNode.Text = "Start"; buttonNode.Interactable = true; + buttonNode.Action = "hud.start"; + buttonNode.DataBinding = "game.session.state"; + buttonNode.DataFormat = "State: {0}"; buttonNode.RectTransform.AnchorMin = glm::vec3(1.0F, 0.0F, 0.0F); buttonNode.RectTransform.AnchorMax = glm::vec3(1.0F, 0.0F, 0.0F); buttonNode.RectTransform.Pivot = glm::vec3(1.0F, 0.0F, 0.0F); @@ -4336,6 +4341,8 @@ void MetaCoreTestUiDocumentSerialization() { MetaCoreExpect(roundTrip.Name == document.Name, "UiDocument 名称应保持"); MetaCoreExpect(roundTrip.ReferenceWidth == 1920, "UiDocument 参考宽度应保持"); MetaCoreExpect(roundTrip.ReferenceHeight == 1080, "UiDocument 参考高度应保持"); + MetaCoreExpectVec3Near(roundTrip.BackgroundColor, glm::vec3(0.02F, 0.04F, 0.08F), "UiDocument 背景色应保持"); + MetaCoreExpect(std::abs(roundTrip.BackgroundAlpha - 0.65F) <= 0.0001F, "UiDocument 背景透明度应保持"); MetaCoreExpect(roundTrip.RootNodeIds.size() == 1, "UiDocument 根节点数应保持"); MetaCoreExpect(roundTrip.Nodes.size() == 3, "UiDocument 节点数量应保持"); MetaCoreExpect(roundTrip.Nodes[0].Children.size() == 2, "UiNode 子节点列表应保持"); @@ -4343,8 +4350,79 @@ void MetaCoreTestUiDocumentSerialization() { MetaCoreExpect(roundTrip.Nodes[1].Text == "MetaCore", "Ui Text 内容应保持"); MetaCoreExpect(roundTrip.Nodes[1].Style.FontSize == 28.0F, "Ui 字体大小应保持"); MetaCoreExpect(roundTrip.Nodes[2].Interactable, "Ui Button 可交互状态应保持"); + MetaCoreExpect(roundTrip.Nodes[2].Action == "hud.start", "Ui Button Action 应保持"); + MetaCoreExpect(roundTrip.Nodes[2].DataBinding == "game.session.state", "Ui 数据绑定字段应保持"); + MetaCoreExpect(roundTrip.Nodes[2].DataFormat == "State: {0}", "Ui 数据格式字段应保持"); MetaCoreExpect(roundTrip.Nodes[2].Style.HorizontalAlignment == MetaCore::MetaCoreUiHorizontalAlignment::Left, "默认水平对齐应保持"); MetaCoreExpectVec3Near(roundTrip.Nodes[0].Style.BackgroundColor, glm::vec3(0.05F, 0.08F, 0.12F), "Ui 背景色应保持"); + + const std::filesystem::path uiJsonPath = std::filesystem::temp_directory_path() / "MetaCoreUiDocumentSerialization.mcui.json"; + MetaCoreExpect( + MetaCore::MetaCoreSceneSerializer::SaveUiToJson(uiJsonPath, document, registry), + "UiDocument JSON 应能保存" + ); + const auto jsonRoundTrip = MetaCore::MetaCoreSceneSerializer::LoadUiFromJson(uiJsonPath, registry); + std::error_code removeError; + std::filesystem::remove(uiJsonPath, removeError); + MetaCoreExpect(jsonRoundTrip.has_value(), "UiDocument JSON 应能读取"); + MetaCoreExpectVec3Near(jsonRoundTrip->BackgroundColor, glm::vec3(0.02F, 0.04F, 0.08F), "UiDocument JSON 背景色应保持"); + MetaCoreExpect(std::abs(jsonRoundTrip->BackgroundAlpha - 0.65F) <= 0.0001F, "UiDocument JSON 背景透明度应保持"); + MetaCoreExpect(jsonRoundTrip->Nodes.size() == 3, "UiDocument JSON 节点数量应保持"); + MetaCoreExpect(jsonRoundTrip->Nodes[2].Action == "hud.start", "UiDocument JSON Action 应保持"); + MetaCoreExpect(jsonRoundTrip->Nodes[2].DataBinding == "game.session.state", "UiDocument JSON 数据绑定应保持"); + MetaCoreExpect(jsonRoundTrip->Nodes[2].DataFormat == "State: {0}", "UiDocument JSON 数据格式应保持"); + + const std::filesystem::path legacyUiJsonPath = std::filesystem::temp_directory_path() / "MetaCoreLegacyUiDocument.mcui.json"; + { + std::ofstream legacyFile(legacyUiJsonPath, std::ios::trunc); + MetaCoreExpect(legacyFile.is_open(), "应能写入旧 UiDocument JSON"); + legacyFile + << "{\n" + << " \"Name\": \"LegacyHud\",\n" + << " \"ReferenceWidth\": 1280,\n" + << " \"ReferenceHeight\": 720,\n" + << " \"RootNodeIds\": [\"legacy.button\"],\n" + << " \"Nodes\": [\n" + << " {\n" + << " \"Id\": \"legacy.button\",\n" + << " \"Name\": \"Legacy Button\",\n" + << " \"Type\": 3,\n" + << " \"ParentId\": \"\",\n" + << " \"Children\": [],\n" + << " \"Visible\": true,\n" + << " \"RectTransform\": {\n" + << " \"AnchorMin\": [0, 0, 0],\n" + << " \"AnchorMax\": [0, 0, 0],\n" + << " \"Pivot\": [0.5, 0.5, 0],\n" + << " \"Position\": [20, 30, 0],\n" + << " \"Size\": [160, 48, 0]\n" + << " },\n" + << " \"Style\": {\n" + << " \"BackgroundColor\": [0.1, 0.2, 0.3],\n" + << " \"TextColor\": [1, 1, 1],\n" + << " \"TintColor\": [1, 1, 1],\n" + << " \"FontSize\": 18,\n" + << " \"Padding\": [8, 8, 0],\n" + << " \"HorizontalAlignment\": 0,\n" + << " \"VerticalAlignment\": 0,\n" + << " \"ImageAssetGuid\": \"\",\n" + << " \"PreserveAspect\": true\n" + << " },\n" + << " \"Text\": \"Start\",\n" + << " \"Interactable\": true\n" + << " }\n" + << " ]\n" + << "}\n"; + } + const auto legacyDocument = MetaCore::MetaCoreSceneSerializer::LoadUiFromJson(legacyUiJsonPath, registry); + std::filesystem::remove(legacyUiJsonPath, removeError); + MetaCoreExpect(legacyDocument.has_value(), "旧 UiDocument JSON 应能读取"); + MetaCoreExpectVec3Near(legacyDocument->BackgroundColor, glm::vec3(0.0F, 0.0F, 0.0F), "旧 UiDocument 缺省背景色应为空黑"); + MetaCoreExpect(std::abs(legacyDocument->BackgroundAlpha) <= 0.0001F, "旧 UiDocument 缺省背景应透明"); + MetaCoreExpect(legacyDocument->Nodes.size() == 1, "旧 UiDocument JSON 节点数量应保持"); + MetaCoreExpect(legacyDocument->Nodes.front().Action.empty(), "旧 UiDocument 缺省 Action 应为空"); + MetaCoreExpect(legacyDocument->Nodes.front().DataBinding.empty(), "旧 UiDocument 缺省数据绑定应为空"); + MetaCoreExpect(legacyDocument->Nodes.front().DataFormat.empty(), "旧 UiDocument 缺省格式应为空"); } void MetaCoreTestUiRendererComponentSnapshotAndJson() { @@ -4382,12 +4460,10 @@ void MetaCoreTestUiRendererComponentSnapshotAndJson() { MetaCore::MetaCoreSceneRenderSync renderSync; const MetaCore::MetaCoreSceneRenderSyncSnapshot hiddenRenderSnapshot = renderSync.BuildSnapshot(scene); - MetaCoreExpect(hiddenRenderSnapshot.Renderables.empty(), "隐藏的 3D UI Renderer 不应生成预览 Renderable"); + MetaCoreExpect(hiddenRenderSnapshot.Renderables.empty(), "隐藏的 3D UI Renderer 不应生成 Renderable"); scene.FindGameObject(uiObject.GetId()).GetComponent().Visible = true; const MetaCore::MetaCoreSceneRenderSyncSnapshot renderSnapshot = renderSync.BuildSnapshot(scene); - MetaCoreExpect(renderSnapshot.Renderables.size() == 1, "可见的 3D UI Renderer 应生成一个预览 Renderable"); - MetaCoreExpect(renderSnapshot.Renderables.front().ObjectId == uiObject.GetId(), "UI 预览 Renderable 应映射回 UI 对象"); - MetaCoreExpect(renderSnapshot.Renderables.front().BuiltinMesh == MetaCore::MetaCoreBuiltinMeshType::Cube, "UI 预览应使用内置 Cube 面板"); + MetaCoreExpect(renderSnapshot.Renderables.empty(), "可见的 3D UI Renderer 不应生成占位 Renderable"); MetaCore::MetaCoreSceneDocument sceneDocument; sceneDocument.Name = "UiRendererScene";