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";