From 998edb23e784df32cfa0f41d7c0501a65cd9db95 Mon Sep 17 00:00:00 2001 From: ayuan9957 <107920784+ayuan9957@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:29:12 +0800 Subject: [PATCH] feat: implement viewport toolbar overlay, Collapse/Filter Console list, and split Build Settings with Scenes In Build drag-drop --- .../Private/MetaCoreBuiltinEditorModule.cpp | 340 ++++++++++++++++-- .../Private/MetaCoreEditorApp.cpp | 103 ++++++ 2 files changed, 408 insertions(+), 35 deletions(-) diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index 32be2fa..fdb0d47 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -83,6 +83,12 @@ static std::filesystem::path PendingRenamePath_{}; static std::array RenameBuffer_{}; static std::array NewFolderBuffer_{}; static float IconSize_ = 64.0f; +static bool ConsoleCollapse_ = true; +static bool ConsoleShowInfo_ = true; +static bool ConsoleShowWarning_ = true; +static bool ConsoleShowError_ = true; +static int ConsoleSelectedEntryIdx_ = -1; +static std::vector> BuildScenes_{}; static HANDLE PlayerProcessHandle_ = NULL; static HANDLE PlayerStdoutRead_ = NULL; @@ -5115,18 +5121,168 @@ public: bool IsOpenByDefault() const override { return true; } void DrawPanel(MetaCoreEditorContext& editorContext) override { - for (const MetaCoreLogEntry& entry : editorContext.GetLogService().GetEntries()) { - ImVec4 textColor = ImVec4(0.85F, 0.85F, 0.88F, 1.0F); - const char* icon = "(i)"; - if (entry.Level == MetaCoreLogLevel::Warning) { - textColor = ImVec4(0.95F, 0.75F, 0.25F, 1.0F); - icon = "(!)"; - } else if (entry.Level == MetaCoreLogLevel::Error) { - textColor = ImVec4(0.95F, 0.35F, 0.30F, 1.0F); - icon = "(X)"; - } - ImGui::TextColored(textColor, "%s [%s] %s", icon, entry.Category.c_str(), entry.Message.c_str()); + auto& logService = editorContext.GetLogService(); + + // 1. 顶部工具栏 (Toolbar) + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 2.0f)); + + if (ImGui::Button("Clear")) { + logService.Clear(); + ConsoleSelectedEntryIdx_ = -1; } + ImGui::SameLine(); + + ImGui::Checkbox("Collapse", &ConsoleCollapse_); + + // 计算各级别日志计数 + int infoCount = 0; + int warnCount = 0; + int errCount = 0; + for (const auto& entry : logService.GetEntries()) { + if (entry.Level == MetaCoreLogLevel::Info) infoCount++; + else if (entry.Level == MetaCoreLogLevel::Warning) warnCount++; + else if (entry.Level == MetaCoreLogLevel::Error) errCount++; + } + + // 日志分级过滤 (Info / Warning / Error) + char infoLabel[32]; + char warnLabel[32]; + char errLabel[32]; + std::snprintf(infoLabel, sizeof(infoLabel), "ℹ️ %d", infoCount); + std::snprintf(warnLabel, sizeof(warnLabel), "⚠️ %d", warnCount); + std::snprintf(errLabel, sizeof(errLabel), "🛑 %d", errCount); + + ImGui::SameLine(ImGui::GetContentRegionMax().x - 170.0f); + + auto drawToggle = [](const char* label, bool* value, const ImVec4& activeColor) { + if (*value) { + ImGui::PushStyleColor(ImGuiCol_Button, activeColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(activeColor.x * 1.1f, activeColor.y * 1.1f, activeColor.z * 1.1f, activeColor.w)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(activeColor.x * 0.9f, activeColor.y * 0.9f, activeColor.z * 0.9f, activeColor.w)); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.4f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.3f, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.15f, 0.15f, 0.15f, 0.6f)); + } + if (ImGui::Button(label, ImVec2(50.0f, 20.0f))) { + *value = !*value; + } + ImGui::PopStyleColor(3); + }; + + drawToggle(infoLabel, &ConsoleShowInfo_, ImVec4(0.173f, 0.365f, 0.529f, 0.6f)); + ImGui::SameLine(0, 2); + drawToggle(warnLabel, &ConsoleShowWarning_, ImVec4(0.85f, 0.65f, 0.15f, 0.6f)); + ImGui::SameLine(0, 2); + drawToggle(errLabel, &ConsoleShowError_, ImVec4(0.75f, 0.25f, 0.25f, 0.6f)); + + ImGui::PopStyleVar(); + ImGui::Separator(); + + // 2. 日志列表区域 + // 为下方详细文本保留 85px 高度 + float availHeight = ImGui::GetContentRegionAvail().y - 85.0f; + if (availHeight < 50.0f) { + availHeight = 50.0f; + } + + if (ImGui::BeginChild("LogList", ImVec2(0, availHeight), true)) { + struct DisplayEntry { + size_t OriginalIndex = 0; + MetaCoreLogLevel Level = MetaCoreLogLevel::Info; + std::string Category; + std::string Message; + int Count = 1; + }; + std::vector displayList; + + const auto& entries = logService.GetEntries(); + for (size_t i = 0; i < entries.size(); ++i) { + const auto& entry = entries[i]; + if (entry.Level == MetaCoreLogLevel::Info && !ConsoleShowInfo_) continue; + if (entry.Level == MetaCoreLogLevel::Warning && !ConsoleShowWarning_) continue; + if (entry.Level == MetaCoreLogLevel::Error && !ConsoleShowError_) continue; + + if (ConsoleCollapse_ && !displayList.empty() && + displayList.back().Level == entry.Level && + displayList.back().Category == entry.Category && + displayList.back().Message == entry.Message) { + displayList.back().Count++; + } else { + DisplayEntry de{}; + de.OriginalIndex = i; + de.Level = entry.Level; + de.Category = entry.Category; + de.Message = entry.Message; + de.Count = 1; + displayList.push_back(de); + } + } + + for (size_t i = 0; i < displayList.size(); ++i) { + const auto& de = displayList[i]; + ImGui::PushID(static_cast(i)); + + // 斑马线交替背景 + ImVec4 rowBg = (i % 2 == 0) ? ImVec4(0.22f, 0.22f, 0.22f, 0.4f) : ImVec4(0.18f, 0.18f, 0.18f, 0.4f); + if (ConsoleSelectedEntryIdx_ == static_cast(de.OriginalIndex)) { + rowBg = ImVec4(0.173f, 0.365f, 0.529f, 0.7f); + } + ImGui::PushStyleColor(ImGuiCol_Header, rowBg); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(rowBg.x * 1.15f, rowBg.y * 1.15f, rowBg.z * 1.15f, rowBg.w + 0.15f)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(rowBg.x * 0.9f, rowBg.y * 0.9f, rowBg.z * 0.9f, rowBg.w + 0.2f)); + + const char* iconStr = "ℹ️"; + if (de.Level == MetaCoreLogLevel::Warning) { + iconStr = "⚠️"; + } else if (de.Level == MetaCoreLogLevel::Error) { + iconStr = "🛑"; + } + + char rowLabel[512]; + std::snprintf(rowLabel, sizeof(rowLabel), "%s [%s] %s", iconStr, de.Category.c_str(), de.Message.c_str()); + + bool isSelected = (ConsoleSelectedEntryIdx_ == static_cast(de.OriginalIndex)); + if (ImGui::Selectable(rowLabel, isSelected, ImGuiSelectableFlags_SpanAllColumns)) { + ConsoleSelectedEntryIdx_ = static_cast(de.OriginalIndex); + } + + ImGui::PopStyleColor(3); + + if (ConsoleCollapse_ && de.Count > 1) { + ImGui::SameLine(ImGui::GetContentRegionMax().x - 45.0f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.35f, 0.35f, 0.8f)); + ImGui::Button(std::to_string(de.Count).c_str(), ImVec2(32.0f, 16.0f)); + ImGui::PopStyleColor(); + } + + ImGui::PopID(); + } + } + ImGui::EndChild(); + + ImGui::Separator(); + + // 3. 详细展示区 + if (ImGui::BeginChild("LogDetail", ImVec2(0, 0), true)) { + if (ConsoleSelectedEntryIdx_ >= 0 && ConsoleSelectedEntryIdx_ < static_cast(logService.GetEntries().size())) { + const auto& entry = logService.GetEntries()[ConsoleSelectedEntryIdx_]; + ImVec4 txtColor = ImVec4(0.9f, 0.9f, 0.92f, 1.0f); + if (entry.Level == MetaCoreLogLevel::Warning) { + txtColor = ImVec4(0.95f, 0.75f, 0.25f, 1.0f); + } else if (entry.Level == MetaCoreLogLevel::Error) { + txtColor = ImVec4(0.95f, 0.35f, 0.30f, 1.0f); + } + + ImGui::TextColored(txtColor, "[%s]", entry.Category.c_str()); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.85f, 0.85f, 0.85f, 1.0f)); + ImGui::TextWrapped("%s", entry.Message.c_str()); + ImGui::PopStyleColor(); + } else { + ImGui::TextDisabled("选择任意日志项以查看详细信息。"); + } + } + ImGui::EndChild(); } }; @@ -5756,42 +5912,156 @@ public: const MetaCoreProjectDescriptor& project = assetDatabaseService->GetProjectDescriptor(); BindProjectDefaults(project); - ImGui::Text("Project: %s", project.Name.c_str()); - ImGui::Text("Root: %s", project.RootPath.string().c_str()); - ImGui::Text("Project Startup Scene: %s", project.StartupScenePath.generic_string().c_str()); - ImGui::Separator(); + // 1. Scenes In Build 场景构建列表 + ImGui::Text("Scenes In Build"); + ImGui::TextDisabled("可以将 .mcscene 场景资产从项目面板拖拽至下方列表中添加"); + + // 自动初始化场景列表 + if (BuildScenes_.empty()) { + for (const auto& a : assetDatabaseService->GetAssetRegistry()) { + if (a.Type == "scene") { + BuildScenes_.push_back({ a.RelativePath.generic_string(), true }); + } + } + } - ImGui::InputText("Startup Scene", StartupScenePathBuffer_.data(), StartupScenePathBuffer_.size()); - ImGui::InputText("Startup UI", StartupUiPathBuffer_.data(), StartupUiPathBuffer_.size()); - ImGui::InputText("Build Profile", BuildProfileNameBuffer_.data(), BuildProfileNameBuffer_.size()); - ImGui::InputText("Target Platform", TargetPlatformBuffer_.data(), TargetPlatformBuffer_.size()); - ImGui::InputText("Player Exe", PlayerExecutablePathBuffer_.data(), PlayerExecutablePathBuffer_.size()); - ImGui::InputText("Output Dir", OutputDirectoryBuffer_.data(), OutputDirectoryBuffer_.size()); - ImGui::InputText("Cooked Assets Dir", CookedAssetsDirectoryBuffer_.data(), CookedAssetsDirectoryBuffer_.size()); - ImGui::Checkbox("Use cooked assets", &UseCookedAssetsInPackage_); - ImGui::Separator(); - ImGui::TextUnformatted("Runtime Data"); - ImGui::InputText("Data Sources", DataSourcesPathBuffer_.data(), DataSourcesPathBuffer_.size()); - ImGui::InputText("Bindings", BindingsPathBuffer_.data(), BindingsPathBuffer_.size()); - ImGui::InputText("Diagnostics", DiagnosticsPathBuffer_.data(), DiagnosticsPathBuffer_.size()); - ImGui::Separator(); - ImGui::Checkbox("Cook before package", &CookBeforePackage_); - ImGui::Checkbox("Copy loose Assets/Scenes", &CopyLooseContent_); - ImGui::Checkbox("Copy Runtime config", &CopyRuntimeConfig_); + // 用 Child Window 包裹展现边框列表 + if (ImGui::BeginChild("ScenesInBuildList", ImVec2(0, 110.0f), true)) { + for (size_t i = 0; i < BuildScenes_.size(); ++i) { + ImGui::PushID(static_cast(i)); + ImGui::Checkbox("##enabled", &BuildScenes_[i].second); + ImGui::SameLine(); + ImGui::Text("%s", BuildScenes_[i].first.c_str()); + + // 右键菜单支持移除场景 + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Remove Scene")) { + BuildScenes_.erase(BuildScenes_.begin() + i); + } + ImGui::EndPopup(); + } + ImGui::PopID(); + } + + // 接收拖放 + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(MetaCoreProjectAssetDragDropPayloadType)) { + if (payload->DataSize == sizeof(MetaCoreProjectAssetDragDropPayload)) { + const auto* assetPayload = static_cast(payload->Data); + if (assetPayload && std::string(assetPayload->AssetType) == "scene") { + const auto record = assetDatabaseService->FindAssetByGuid(assetPayload->AssetGuid); + if (record.has_value()) { + std::string pStr = record->RelativePath.generic_string(); + bool dup = false; + for (const auto& s : BuildScenes_) { + if (s.first == pStr) { + dup = true; + break; + } + } + if (!dup) { + BuildScenes_.push_back({ pStr, true }); + } + } + } + } + } + ImGui::EndDragDropTarget(); + } + } + ImGui::EndChild(); + if (ImGui::Button("Add Open Scenes")) { + for (const auto& a : assetDatabaseService->GetAssetRegistry()) { + if (a.Type == "scene") { + std::string pStr = a.RelativePath.generic_string(); + bool dup = false; + for (const auto& s : BuildScenes_) { + if (s.first == pStr) { + dup = true; + break; + } + } + if (!dup) { + BuildScenes_.push_back({ pStr, true }); + } + } + } + } + + ImGui::Separator(); + ImGui::Spacing(); + + // 2. 双栏布局:左栏平台,右栏打包配置项 + ImGui::Columns(2, "BuildSettingsSplit", false); + ImGui::SetColumnWidth(0, 150.0f); + + ImGui::Text("Platform"); + if (ImGui::BeginChild("PlatformList", ImVec2(0, 180.0f), true)) { + // Unity 风格的高亮单选列表 + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.173f, 0.365f, 0.529f, 0.8f)); + ImGui::Selectable("💻 PC, Mac & Linux", true); + ImGui::PopStyleColor(); + + ImGui::Selectable("📱 Android", false); + ImGui::Selectable("🍏 iOS", false); + ImGui::Selectable("🌐 WebGL", false); + } + ImGui::EndChild(); + + ImGui::NextColumn(); + + ImGui::Text("Build Configuration"); + if (ImGui::BeginChild("ConfigDetails", ImVec2(0, 180.0f), false)) { + ImGui::InputText("Startup Scene", StartupScenePathBuffer_.data(), StartupScenePathBuffer_.size()); + ImGui::InputText("Startup UI", StartupUiPathBuffer_.data(), StartupUiPathBuffer_.size()); + ImGui::InputText("Build Profile", BuildProfileNameBuffer_.data(), BuildProfileNameBuffer_.size()); + ImGui::InputText("Target Platform", TargetPlatformBuffer_.data(), TargetPlatformBuffer_.size()); + ImGui::InputText("Player Exe", PlayerExecutablePathBuffer_.data(), PlayerExecutablePathBuffer_.size()); + ImGui::InputText("Output Dir", OutputDirectoryBuffer_.data(), OutputDirectoryBuffer_.size()); + ImGui::InputText("Cooked Assets Dir", CookedAssetsDirectoryBuffer_.data(), CookedAssetsDirectoryBuffer_.size()); + ImGui::Checkbox("Use cooked assets", &UseCookedAssetsInPackage_); + + ImGui::Separator(); + ImGui::TextDisabled("Runtime Config Paths"); + ImGui::InputText("Data Sources", DataSourcesPathBuffer_.data(), DataSourcesPathBuffer_.size()); + ImGui::InputText("Bindings", BindingsPathBuffer_.data(), BindingsPathBuffer_.size()); + ImGui::InputText("Diagnostics", DiagnosticsPathBuffer_.data(), DiagnosticsPathBuffer_.size()); + + ImGui::Separator(); + ImGui::Checkbox("Cook before package", &CookBeforePackage_); + ImGui::Checkbox("Copy loose Assets/Scenes", &CopyLooseContent_); + ImGui::Checkbox("Copy Runtime config", &CopyRuntimeConfig_); + } + ImGui::EndChild(); + + ImGui::Columns(1); + ImGui::Separator(); + ImGui::Spacing(); + + // 3. 底部主按钮 (右对齐) const bool canBuild = buildService != nullptr; if (!canBuild) { ImGui::BeginDisabled(); } - if (ImGui::Button("Save Build Settings", ImVec2(-1.0F, 28.0F))) { + + // 保存配置 + if (ImGui::Button("Save Settings", ImVec2(120.0f, 26.0f))) { (void)SaveBuildSettings(editorContext, project, true); } - if (ImGui::Button("Build Player Package", ImVec2(-1.0F, 28.0F))) { + ImGui::SameLine(); + + // 打包 (Build) + if (ImGui::Button("Build", ImVec2(100.0f, 26.0f))) { BuildPackage(editorContext, *buildService, false); } - if (ImGui::Button("Build Cooked-Only Package", ImVec2(-1.0F, 28.0F))) { + ImGui::SameLine(); + + // 烹饪并打包 (Build Cooked) + if (ImGui::Button("Build Cooked", ImVec2(120.0f, 26.0f))) { BuildPackage(editorContext, *buildService, true); } + if (!canBuild) { ImGui::EndDisabled(); } diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index 764f3b2..9aec10c 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -1276,6 +1276,109 @@ void MetaCoreEditorApp::DrawSceneViewWindow() { ImGui::Dummy(viewportSize); } + // 场景视口浮动快捷工具条 (Toolbar Overlay) + // 浮动在视口左上角,采用半透明暗色背景,1:1 复刻 Unity 视口功能控制体验 + ImGui::SetCursorScreenPos(ImVec2(viewportPos.x + 10.0f, viewportPos.y + 10.0f)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.12f, 0.12f, 0.85f)); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 4.0f)); + + if (ImGui::BeginChild("ViewportToolbar", ImVec2(410.0f, 28.0f), false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { + // A. Gizmo 轴操作切换 (T: 位移, R: 旋转, S: 缩放) + auto currentOp = EditorContext_->GetGizmoOperation(); + + auto drawToolbarButton = [](const char* label, bool active) -> bool { + if (active) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.173f, 0.365f, 0.529f, 1.0f)); // 选中亮蓝色 + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.22f, 0.45f, 0.65f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.12f, 0.28f, 0.42f, 1.0f)); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); // 平时透明 + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 1.0f, 1.0f, 0.12f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 1.0f, 1.0f, 0.20f)); + } + bool clicked = ImGui::Button(label, ImVec2(24.0f, 20.0f)); + ImGui::PopStyleColor(3); + return clicked; + }; + + if (drawToolbarButton("T", currentOp == MetaCoreGizmoOperation::Translate)) { + EditorContext_->SetGizmoOperation(MetaCoreGizmoOperation::Translate); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("移动工具 (W)"); + + ImGui::SameLine(0, 2); + if (drawToolbarButton("R", currentOp == MetaCoreGizmoOperation::Rotate)) { + EditorContext_->SetGizmoOperation(MetaCoreGizmoOperation::Rotate); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("旋转工具 (E)"); + + ImGui::SameLine(0, 2); + if (drawToolbarButton("S", currentOp == MetaCoreGizmoOperation::Scale)) { + EditorContext_->SetGizmoOperation(MetaCoreGizmoOperation::Scale); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("缩放工具 (R)"); + + ImGui::SameLine(0, 6); + ImGui::TextDisabled("|"); + ImGui::SameLine(0, 6); + + // B. 坐标系切换 (Local本地 / Global世界) + auto currentMode = EditorContext_->GetGizmoMode(); + const char* modeLabel = (currentMode == MetaCoreGizmoMode::Local) ? "Local" : "Global"; + if (ImGui::Button(modeLabel, ImVec2(50.0f, 20.0f))) { + EditorContext_->SetGizmoMode(currentMode == MetaCoreGizmoMode::Local ? MetaCoreGizmoMode::Global : MetaCoreGizmoMode::Local); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("切换本地/全局坐标系"); + + ImGui::SameLine(0, 6); + ImGui::TextDisabled("|"); + ImGui::SameLine(0, 6); + + // C. 枢轴模式切换 (Pivot枢轴 / Center中心) + auto currentPivot = EditorContext_->GetGizmoPivotMode(); + const char* pivotLabel = (currentPivot == MetaCoreGizmoPivotMode::Pivot) ? "Pivot" : "Center"; + if (ImGui::Button(pivotLabel, ImVec2(55.0f, 20.0f))) { + EditorContext_->SetGizmoPivotMode(currentPivot == MetaCoreGizmoPivotMode::Pivot ? MetaCoreGizmoPivotMode::Center : MetaCoreGizmoPivotMode::Pivot); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("切换 Pivot/Center 枢轴中心"); + + ImGui::SameLine(0, 6); + ImGui::TextDisabled("|"); + ImGui::SameLine(0, 6); + + // D. 网格显示/隐藏控制 (G) + bool showGrid = EditorContext_->GetShowViewportGrid(); + if (drawToolbarButton("G", showGrid)) { + EditorContext_->SetShowViewportGrid(!showGrid); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip(showGrid ? "隐藏网格" : "显示网格"); + + ImGui::SameLine(0, 4); + + // E. Gizmo 吸附开启/隐藏控制 (U) + bool snapEnabled = EditorContext_->GetGizmoSnapSettings().Enabled; + if (drawToolbarButton("U", snapEnabled)) { + EditorContext_->SetGizmoSnapEnabled(!snapEnabled); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip(snapEnabled ? "关闭吸附" : "开启吸附"); + + // F. 视口摄像机一键复位 (Reset Cam) + ImGui::SameLine(0, 6); + ImGui::TextDisabled("|"); + ImGui::SameLine(0, 6); + if (ImGui::Button("Reset Cam", ImVec2(75.0f, 20.0f))) { + EditorContext_->GetCameraController().FocusGameObject({}); // 传入空 GameObject 表示复位摄像机到初始中心位置 + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("复位视口摄像机"); + } + ImGui::EndChild(); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(); + + // 绘制完工具条后,将游标坐标复位,以避免影响后续子节点与 DragDrop 的正常绘制 + ImGui::SetCursorScreenPos(viewportPos); + if (ImGui::BeginDragDropTarget()) { (void)MetaCoreHandleProjectAssetDrop(*EditorContext_, std::nullopt); ImGui::EndDragDropTarget();