feat(editor): refine scene workflow and viewport grid

This commit is contained in:
ayuan9957 2026-06-17 16:16:40 +08:00
parent f9083c4aec
commit 35b394ee37
11 changed files with 831 additions and 138 deletions

View File

@ -62,6 +62,8 @@ endif()
set(METACORE_UI_BLIT_MATERIAL_SOURCE "${CMAKE_SOURCE_DIR}/third_party/filament/libs/filagui/src/materials/uiBlit.mat")
set(METACORE_UI_BLIT_MATERIAL_OUTPUT "${CMAKE_BINARY_DIR}/uiBlit.filamat")
set(METACORE_EDITOR_GRID_MATERIAL_SOURCE "${CMAKE_SOURCE_DIR}/Source/MetaCoreRender/Private/editorGrid.mat")
set(METACORE_EDITOR_GRID_MATERIAL_OUTPUT "${CMAKE_BINARY_DIR}/editorGrid.filamat")
set(METACORE_FILAMENT_MATC "${METACORE_FILAMENT_ROOT}/bin/matc.exe")
add_custom_command(
@ -81,12 +83,33 @@ add_custom_target(MetaCoreUiBlitMaterial
DEPENDS "${METACORE_UI_BLIT_MATERIAL_OUTPUT}"
)
add_custom_command(
OUTPUT "${METACORE_EDITOR_GRID_MATERIAL_OUTPUT}"
COMMAND "${METACORE_FILAMENT_MATC}"
--platform desktop
--api all
--output "${METACORE_EDITOR_GRID_MATERIAL_OUTPUT}"
"${METACORE_EDITOR_GRID_MATERIAL_SOURCE}"
DEPENDS
"${METACORE_EDITOR_GRID_MATERIAL_SOURCE}"
"${METACORE_FILAMENT_MATC}"
VERBATIM
)
add_custom_target(MetaCoreEditorGridMaterial
DEPENDS "${METACORE_EDITOR_GRID_MATERIAL_OUTPUT}"
)
function(metacore_stage_ui_blit_material target_name)
add_dependencies(${target_name} MetaCoreUiBlitMaterial)
add_dependencies(${target_name} MetaCoreEditorGridMaterial)
add_custom_command(TARGET ${target_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${METACORE_UI_BLIT_MATERIAL_OUTPUT}"
"$<TARGET_FILE_DIR:${target_name}>/uiBlit.filamat"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${METACORE_EDITOR_GRID_MATERIAL_OUTPUT}"
"$<TARGET_FILE_DIR:${target_name}>/editorGrid.filamat"
VERBATIM
)
endfunction()

View File

@ -742,70 +742,6 @@ void MetaCoreReimportProjectAsset(MetaCoreEditorContext& editorContext, const Me
}
}
void MetaCoreEnsureProjectStartupScene(MetaCoreEditorContext& editorContext) {
const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIScenePersistenceService>();
if (assetDatabaseService == nullptr || scenePersistenceService == nullptr || !assetDatabaseService->HasProject()) {
return;
}
if (scenePersistenceService->LoadStartupScene(editorContext)) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Project", "已加载项目启动场景");
return;
}
if (!assetDatabaseService->GetProjectDescriptor().ScenePaths.empty()) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "Project", "启动场景不可用,已保留当前场景");
return;
}
editorContext.GetScene().RestoreSnapshot(MetaCoreCreateDefaultScene().CaptureSnapshot());
editorContext.ClearSelection();
const std::filesystem::path bootstrapScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json";
if (scenePersistenceService->SaveSceneAs(editorContext, bootstrapScenePath)) {
(void)assetDatabaseService->SetStartupScenePath(bootstrapScenePath);
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Project", "已为新项目创建默认启动场景");
} else {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "Project", "项目已打开,但默认启动场景创建失败");
}
}
void MetaCoreOpenProject(MetaCoreEditorContext& editorContext, const std::filesystem::path& projectRoot) {
const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
if (assetDatabaseService == nullptr) {
return;
}
if (assetDatabaseService->OpenProject(projectRoot)) {
MetaCoreEnsureProjectStartupScene(editorContext);
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Info,
"Project",
"已打开项目: " + assetDatabaseService->GetProjectDescriptor().RootPath.string()
);
} else {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Project", "打开项目失败");
}
}
void MetaCoreCreateProject(MetaCoreEditorContext& editorContext, const std::filesystem::path& projectRoot, const std::string& projectName) {
const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
if (assetDatabaseService == nullptr) {
return;
}
if (assetDatabaseService->CreateProject(projectRoot, projectName)) {
MetaCoreEnsureProjectStartupScene(editorContext);
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Info,
"Project",
"已创建项目: " + assetDatabaseService->GetProjectDescriptor().RootPath.string()
);
} else {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Project", "创建项目失败");
}
}
void MetaCoreHandleGlobalEditorShortcuts(MetaCoreEditorContext& editorContext) {
const ImGuiIO& io = ImGui::GetIO();
if (io.WantTextInput) {
@ -2537,25 +2473,27 @@ public:
const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIScenePersistenceService>();
const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
// 顶级菜单 1项目 (对应 Project)
if (ImGui::BeginMenu("项目")) {
if (ImGui::MenuItem("新建项目...")) {
if (assetDatabaseService != nullptr && assetDatabaseService->HasProject()) {
const auto& project = assetDatabaseService->GetProjectDescriptor();
std::snprintf(CreateProjectPathBuffer_, sizeof(CreateProjectPathBuffer_), "%s", project.RootPath.parent_path().string().c_str());
} else {
std::snprintf(CreateProjectPathBuffer_, sizeof(CreateProjectPathBuffer_), "%s", std::filesystem::current_path().string().c_str());
}
std::snprintf(CreateProjectNameBuffer_, sizeof(CreateProjectNameBuffer_), "%s", "NewMetaCoreProject");
ImGui::OpenPopup("MetaCoreCreateProjectPopup");
// 顶级菜单 1场景。项目创建/打开由 Hub 负责Editor 内只处理当前项目中的场景。
if (ImGui::BeginMenu("场景")) {
if (ImGui::MenuItem("新建场景...", nullptr, false, assetDatabaseService != nullptr && assetDatabaseService->HasProject())) {
std::snprintf(NewScenePathBuffer_, sizeof(NewScenePathBuffer_), "%s", "Scenes/NewScene.mcscene.json");
ImGui::OpenPopup("MetaCoreCreateScenePopup");
}
if (ImGui::MenuItem("打开项目...")) {
if (assetDatabaseService != nullptr && assetDatabaseService->HasProject()) {
std::snprintf(OpenProjectPathBuffer_, sizeof(OpenProjectPathBuffer_), "%s", assetDatabaseService->GetProjectDescriptor().RootPath.string().c_str());
} else {
std::snprintf(OpenProjectPathBuffer_, sizeof(OpenProjectPathBuffer_), "%s", std::filesystem::current_path().string().c_str());
if (ImGui::MenuItem("打开场景...", nullptr, false, scenePersistenceService != nullptr && assetDatabaseService != nullptr && assetDatabaseService->HasProject())) {
const std::filesystem::path defaultScenePath = scenePersistenceService != nullptr && scenePersistenceService->HasOpenScene()
? scenePersistenceService->GetCurrentScenePath()
: std::filesystem::path("Scenes") / "Main.mcscene.json";
std::snprintf(OpenScenePathBuffer_, sizeof(OpenScenePathBuffer_), "%s", defaultScenePath.generic_string().c_str());
ImGui::OpenPopup("MetaCoreOpenScenePopup");
}
if (scenePersistenceService != nullptr && ImGui::BeginMenu("项目场景", !scenePersistenceService->GetKnownScenePaths().empty())) {
for (const std::filesystem::path& scenePath : scenePersistenceService->GetKnownScenePaths()) {
if (ImGui::MenuItem(scenePath.generic_string().c_str())) {
PendingScenePath_ = scenePath;
RequestSceneMenuAction(editorContext, scenePersistenceService, PendingSceneAction::OpenScene);
}
}
ImGui::OpenPopup("MetaCoreOpenProjectPopup");
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem("保存场景", "Ctrl+S", false, scenePersistenceService != nullptr)) {
@ -2594,20 +2532,19 @@ public:
ImGui::MenuItem("当前运行模式MetaCore 编辑器", nullptr, false, false);
ImGui::EndMenu();
}
if (ImGui::MenuItem("退出")) {
editorContext.GetWindow().RequestClose();
RequestSceneMenuAction(editorContext, scenePersistenceService, PendingSceneAction::Exit);
}
ImGui::EndMenu();
}
// 新建项目弹窗中文化
if (ImGui::BeginPopupModal("MetaCoreCreateProjectPopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::InputText("项目目录", CreateProjectPathBuffer_, sizeof(CreateProjectPathBuffer_));
ImGui::InputText("项目名称", CreateProjectNameBuffer_, sizeof(CreateProjectNameBuffer_));
if (ImGui::BeginPopupModal("MetaCoreCreateScenePopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::TextDisabled("相对于当前项目根目录");
ImGui::InputText("场景路径", NewScenePathBuffer_, sizeof(NewScenePathBuffer_));
if (ImGui::Button("创建")) {
const std::filesystem::path projectRoot = std::filesystem::path(CreateProjectPathBuffer_) / std::filesystem::path(CreateProjectNameBuffer_);
MetaCoreCreateProject(editorContext, projectRoot, CreateProjectNameBuffer_);
PendingScenePath_ = std::filesystem::path(NewScenePathBuffer_);
RequestSceneMenuAction(editorContext, scenePersistenceService, PendingSceneAction::NewScene);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
@ -2617,11 +2554,12 @@ public:
ImGui::EndPopup();
}
// 打开项目弹窗中文化
if (ImGui::BeginPopupModal("MetaCoreOpenProjectPopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::InputText("项目路径", OpenProjectPathBuffer_, sizeof(OpenProjectPathBuffer_));
if (ImGui::BeginPopupModal("MetaCoreOpenScenePopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::TextDisabled("相对于当前项目根目录");
ImGui::InputText("场景路径", OpenScenePathBuffer_, sizeof(OpenScenePathBuffer_));
if (ImGui::Button("打开")) {
MetaCoreOpenProject(editorContext, std::filesystem::path(OpenProjectPathBuffer_));
PendingScenePath_ = std::filesystem::path(OpenScenePathBuffer_);
RequestSceneMenuAction(editorContext, scenePersistenceService, PendingSceneAction::OpenScene);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
@ -2631,6 +2569,8 @@ public:
ImGui::EndPopup();
}
DrawPendingSceneActionPopup(editorContext, scenePersistenceService);
// 顶级菜单 2编辑 (对应 Edit)
if (ImGui::BeginMenu("编辑")) {
if (ImGui::MenuItem("撤销", "Ctrl+Z", false, editorContext.GetCommandService().CanUndo())) {
@ -2701,9 +2641,119 @@ public:
}
private:
char CreateProjectPathBuffer_[512]{};
char CreateProjectNameBuffer_[256]{};
char OpenProjectPathBuffer_[512]{};
enum class PendingSceneAction {
None,
NewScene,
OpenScene,
Exit
};
void RequestSceneMenuAction(
MetaCoreEditorContext& editorContext,
const std::shared_ptr<MetaCoreIScenePersistenceService>& scenePersistenceService,
PendingSceneAction action
) {
if (action == PendingSceneAction::None) {
return;
}
if (scenePersistenceService == nullptr || !scenePersistenceService->IsSceneDirty()) {
ExecuteSceneMenuAction(editorContext, scenePersistenceService, action);
return;
}
PendingAction_ = action;
OpenPendingActionPopup_ = true;
}
void ExecuteSceneMenuAction(
MetaCoreEditorContext& editorContext,
const std::shared_ptr<MetaCoreIScenePersistenceService>& scenePersistenceService,
PendingSceneAction action
) {
switch (action) {
case PendingSceneAction::NewScene:
CreateAndOpenScene(editorContext, scenePersistenceService);
break;
case PendingSceneAction::OpenScene:
if (scenePersistenceService != nullptr && !PendingScenePath_.empty()) {
(void)scenePersistenceService->LoadScene(editorContext, PendingScenePath_);
}
break;
case PendingSceneAction::Exit:
editorContext.GetWindow().RequestClose();
break;
case PendingSceneAction::None:
break;
}
PendingAction_ = PendingSceneAction::None;
PendingScenePath_.clear();
}
void DrawPendingSceneActionPopup(
MetaCoreEditorContext& editorContext,
const std::shared_ptr<MetaCoreIScenePersistenceService>& scenePersistenceService
) {
if (OpenPendingActionPopup_) {
ImGui::OpenPopup("Scene Action Unsaved Scene Changes");
OpenPendingActionPopup_ = false;
}
if (ImGui::BeginPopupModal("Scene Action Unsaved Scene Changes", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
const std::string currentScene = scenePersistenceService != nullptr && scenePersistenceService->HasOpenScene()
? scenePersistenceService->GetCurrentSceneDisplayName()
: std::string("Untitled");
ImGui::Text("Save changes to %s before continuing?", currentScene.c_str());
ImGui::Separator();
if (ImGui::Button("Save and Continue", ImVec2(150, 0))) {
if (scenePersistenceService == nullptr || scenePersistenceService->SaveCurrentScene(editorContext)) {
ExecuteSceneMenuAction(editorContext, scenePersistenceService, PendingAction_);
ImGui::CloseCurrentPopup();
} else {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Scene", "保存失败,已取消操作");
}
}
ImGui::SameLine();
if (ImGui::Button("Don't Save", ImVec2(110, 0))) {
ExecuteSceneMenuAction(editorContext, scenePersistenceService, PendingAction_);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(90, 0))) {
PendingAction_ = PendingSceneAction::None;
PendingScenePath_.clear();
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
void CreateAndOpenScene(
MetaCoreEditorContext& editorContext,
const std::shared_ptr<MetaCoreIScenePersistenceService>& scenePersistenceService
) {
const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
if (assetDatabaseService == nullptr || scenePersistenceService == nullptr || PendingScenePath_.empty()) {
return;
}
const auto createdScene = assetDatabaseService->CreateSceneAsset(PendingScenePath_, false);
if (!createdScene.has_value()) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Scene", "创建场景失败: " + PendingScenePath_.generic_string());
return;
}
(void)scenePersistenceService->LoadScene(editorContext, createdScene->RelativePath);
}
char NewScenePathBuffer_[512]{};
char OpenScenePathBuffer_[512]{};
std::filesystem::path PendingScenePath_{};
PendingSceneAction PendingAction_ = PendingSceneAction::None;
bool OpenPendingActionPopup_ = false;
};
class MetaCoreHierarchyPanelProvider final : public MetaCoreIEditorPanelProvider {

View File

@ -21,6 +21,7 @@
#include <cstdlib>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <exception>
#include <filesystem>
#include <glm/ext/matrix_clip_space.hpp>
@ -297,9 +298,9 @@ bool MetaCoreProjectWorldPointToViewport(
}
void MetaCoreDrawWorldOriginOverlay(
const MetaCoreEditorContext& editorContext,
const MetaCoreSceneView& sceneView,
const MetaCoreSceneViewportState& viewportState
const MetaCoreEditorContext&,
const MetaCoreSceneView&,
const MetaCoreSceneViewportState&
) {
// 用户要求去掉小 XYZ 轴和原点指示
return;
@ -307,62 +308,161 @@ void MetaCoreDrawWorldOriginOverlay(
void MetaCoreDrawViewportGridOverlay(
const MetaCoreEditorContext& editorContext,
const MetaCoreSceneView& sceneView,
const MetaCoreSceneViewportState& viewportState
) {
if (!editorContext.GetShowViewportGrid()) {
(void)editorContext;
(void)sceneView;
(void)viewportState;
// The editor grid is rendered by Filament as a real 3D ground plane.
return;
if (!editorContext.GetShowViewportGrid() || viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) {
return;
}
if (viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) {
return;
constexpr float gridY = 0.0F;
constexpr float gridSpacing = 1.0F;
constexpr int majorLineInterval = 5;
constexpr int maxVisibleLines = 240;
constexpr float fallbackHalfExtent = 80.0F;
const glm::mat4 viewMatrix = glm::lookAt(sceneView.CameraPosition, sceneView.CameraTarget, sceneView.CameraUp);
const glm::mat4 projectionMatrix = glm::perspective(
glm::radians(sceneView.VerticalFieldOfViewDegrees),
viewportState.Width / viewportState.Height,
0.05F,
500.0F
);
const glm::mat4 inverseViewProjection = glm::inverse(projectionMatrix * viewMatrix);
const auto raycastGroundAtViewportPoint = [&](float screenX, float screenY, glm::vec3& groundPoint) -> bool {
const float ndcX = ((screenX - viewportState.Left) / viewportState.Width) * 2.0F - 1.0F;
const float ndcY = 1.0F - ((screenY - viewportState.Top) / viewportState.Height) * 2.0F;
glm::vec4 nearPoint = inverseViewProjection * glm::vec4(ndcX, ndcY, -1.0F, 1.0F);
glm::vec4 farPoint = inverseViewProjection * glm::vec4(ndcX, ndcY, 1.0F, 1.0F);
if (std::abs(nearPoint.w) <= 0.0001F || std::abs(farPoint.w) <= 0.0001F) {
return false;
}
nearPoint /= nearPoint.w;
farPoint /= farPoint.w;
const glm::vec3 rayOrigin = glm::vec3(nearPoint);
const glm::vec3 rayDirection = glm::normalize(glm::vec3(farPoint - nearPoint));
if (std::abs(rayDirection.y) <= 0.0001F) {
return false;
}
const float t = (gridY - rayOrigin.y) / rayDirection.y;
if (t <= 0.0F) {
return false;
}
groundPoint = rayOrigin + rayDirection * t;
return true;
};
std::vector<glm::vec3> groundPoints;
groundPoints.reserve(9);
for (int yStep = 0; yStep <= 2; ++yStep) {
for (int xStep = 0; xStep <= 2; ++xStep) {
glm::vec3 groundPoint{};
const float screenX = viewportState.Left + viewportState.Width * (static_cast<float>(xStep) * 0.5F);
const float screenY = viewportState.Top + viewportState.Height * (static_cast<float>(yStep) * 0.5F);
if (raycastGroundAtViewportPoint(screenX, screenY, groundPoint)) {
groundPoints.push_back(groundPoint);
}
}
}
float minX = sceneView.CameraTarget.x - fallbackHalfExtent;
float maxX = sceneView.CameraTarget.x + fallbackHalfExtent;
float minZ = sceneView.CameraTarget.z - fallbackHalfExtent;
float maxZ = sceneView.CameraTarget.z + fallbackHalfExtent;
if (!groundPoints.empty()) {
minX = maxX = groundPoints.front().x;
minZ = maxZ = groundPoints.front().z;
for (const glm::vec3& point : groundPoints) {
minX = std::min(minX, point.x);
maxX = std::max(maxX, point.x);
minZ = std::min(minZ, point.z);
maxZ = std::max(maxZ, point.z);
}
const float padding = std::max(gridSpacing * 4.0F, glm::distance(sceneView.CameraPosition, sceneView.CameraTarget) * 0.08F);
minX -= padding;
maxX += padding;
minZ -= padding;
maxZ += padding;
}
int startX = static_cast<int>(std::floor(minX / gridSpacing));
int endX = static_cast<int>(std::ceil(maxX / gridSpacing));
int startZ = static_cast<int>(std::floor(minZ / gridSpacing));
int endZ = static_cast<int>(std::ceil(maxZ / gridSpacing));
if (endX - startX > maxVisibleLines) {
const int center = static_cast<int>(std::floor(sceneView.CameraTarget.x / gridSpacing));
startX = center - maxVisibleLines / 2;
endX = center + maxVisibleLines / 2;
}
if (endZ - startZ > maxVisibleLines) {
const int center = static_cast<int>(std::floor(sceneView.CameraTarget.z / gridSpacing));
startZ = center - maxVisibleLines / 2;
endZ = center + maxVisibleLines / 2;
}
ImDrawList* drawList = ImGui::GetForegroundDrawList();
const ImVec2 topLeft(viewportState.Left, viewportState.Top);
const ImVec2 bottomRight(viewportState.Left + viewportState.Width, viewportState.Top + viewportState.Height);
const ImVec2 center((topLeft.x + bottomRight.x) * 0.5F, (topLeft.y + bottomRight.y) * 0.5F);
const ImU32 minorColor = IM_COL32(150, 165, 180, 18);
const ImU32 majorColor = IM_COL32(180, 195, 210, 30);
const ImU32 axisXColor = IM_COL32(220, 85, 85, 65);
const ImU32 axisZColor = IM_COL32(85, 145, 230, 65);
const float minorSpacing = 32.0F;
const int verticalMinorCount = static_cast<int>(viewportState.Width / minorSpacing) + 2;
const int horizontalMinorCount = static_cast<int>(viewportState.Height / minorSpacing) + 2;
const ImU32 minorColor = IM_COL32(110, 120, 135, 40);
const ImU32 majorColor = IM_COL32(150, 165, 185, 70);
const ImU32 axisXColor = IM_COL32(220, 80, 80, 95);
const ImU32 axisYColor = IM_COL32(120, 210, 100, 95);
for (int lineIndex = -verticalMinorCount; lineIndex <= verticalMinorCount; ++lineIndex) {
const float x = center.x + static_cast<float>(lineIndex) * minorSpacing;
if (x < topLeft.x || x > bottomRight.x) {
continue;
const auto drawProjectedGroundLine = [&](const glm::vec3& start, const glm::vec3& end, ImU32 color, float thickness) {
constexpr int segmentCount = 48;
ImVec2 previousPoint{};
bool previousVisible = false;
for (int segmentIndex = 0; segmentIndex <= segmentCount; ++segmentIndex) {
const float t = static_cast<float>(segmentIndex) / static_cast<float>(segmentCount);
const glm::vec3 worldPoint = start + (end - start) * t;
ImVec2 screenPoint{};
glm::vec3 projectedPixelGroundPoint{};
const bool visible = MetaCoreProjectWorldPointToViewport(sceneView, viewportState, worldPoint, screenPoint)
&& raycastGroundAtViewportPoint(screenPoint.x, screenPoint.y, projectedPixelGroundPoint);
if (visible && previousVisible) {
drawList->AddLine(previousPoint, screenPoint, color, thickness);
}
previousPoint = screenPoint;
previousVisible = visible;
}
const bool isMajor = (lineIndex % 4) == 0;
drawList->AddLine(
ImVec2(x, topLeft.y),
ImVec2(x, bottomRight.y),
lineIndex == 0 ? axisXColor : (isMajor ? majorColor : minorColor),
lineIndex == 0 ? 1.8F : (isMajor ? 1.2F : 1.0F)
};
drawList->PushClipRect(topLeft, bottomRight, true);
for (int xLine = startX; xLine <= endX; ++xLine) {
const bool isMajor = (xLine % majorLineInterval) == 0;
const float x = static_cast<float>(xLine) * gridSpacing;
drawProjectedGroundLine(
glm::vec3(x, gridY, static_cast<float>(startZ) * gridSpacing),
glm::vec3(x, gridY, static_cast<float>(endZ) * gridSpacing),
xLine == 0 ? axisZColor : (isMajor ? majorColor : minorColor),
xLine == 0 ? 1.4F : 1.0F
);
}
for (int lineIndex = -horizontalMinorCount; lineIndex <= horizontalMinorCount; ++lineIndex) {
const float y = center.y + static_cast<float>(lineIndex) * minorSpacing;
if (y < topLeft.y || y > bottomRight.y) {
continue;
}
const bool isMajor = (lineIndex % 4) == 0;
drawList->AddLine(
ImVec2(topLeft.x, y),
ImVec2(bottomRight.x, y),
lineIndex == 0 ? axisYColor : (isMajor ? majorColor : minorColor),
lineIndex == 0 ? 1.8F : (isMajor ? 1.2F : 1.0F)
for (int zLine = startZ; zLine <= endZ; ++zLine) {
const bool isMajor = (zLine % majorLineInterval) == 0;
const float z = static_cast<float>(zLine) * gridSpacing;
drawProjectedGroundLine(
glm::vec3(static_cast<float>(startX) * gridSpacing, gridY, z),
glm::vec3(static_cast<float>(endX) * gridSpacing, gridY, z),
zLine == 0 ? axisXColor : (isMajor ? majorColor : minorColor),
zLine == 0 ? 1.4F : 1.0F
);
}
drawList->PopClipRect();
}
void MetaCoreDrawSelectionBoundsOverlay(
const MetaCoreEditorContext& editorContext,
const MetaCoreScene& scene,
const MetaCoreSceneView& sceneView,
const MetaCoreSceneViewportState& viewportState
const MetaCoreEditorContext&,
const MetaCoreScene&,
const MetaCoreSceneView&,
const MetaCoreSceneViewportState&
) {
// 用户要求去掉左下角的黄色框和文字
return;
@ -716,6 +816,10 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) {
ChangeWindowMessageFilter(0x0049, MSGFLT_ADD); // WM_COPYGLOBALMEM
Window_.SetNativeWindowMessageHandler([this](void* nativeWindowHandle, unsigned int message, std::uintptr_t wparam, std::intptr_t lparam) {
if (message == WM_CLOSE) {
return RequestEditorCloseWithSavePrompt();
}
if (message == WM_DROPFILES) {
HDROP hDrop = reinterpret_cast<HDROP>(wparam);
UINT fileCount = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0);
@ -906,6 +1010,7 @@ int MetaCoreEditorApp::Run() {
// 1. 收集并更新场景数据(但不在这里渲染)
MetaCoreSceneView sceneView = EditorContext_->GetCameraController().BuildSceneView();
sceneView.SelectedObjectId = EditorContext_->GetSelectedObjectId();
ViewportRenderer_.SetEditorGridVisible(EditorContext_->GetShowViewportGrid());
ViewportRenderer_.RenderSceneToViewport(Scene_, sceneView);
// 2. 统一在一个 Frame 内渲染离屏 3D 和上屏 UI
@ -1128,6 +1233,54 @@ void MetaCoreEditorApp::DrawEditorFrame() {
DrawPlaceholderDockWindow("动画状态机编辑器###animfsm_editor", "动画状态机编辑器");
ApplyPendingDockTabSelections();
DrawEditorStatusBar();
DrawPendingExitPopup();
}
}
bool MetaCoreEditorApp::RequestEditorCloseWithSavePrompt() {
if (const auto scenePersistenceService = ModuleRegistry_.ResolveService<MetaCoreIScenePersistenceService>();
scenePersistenceService != nullptr && scenePersistenceService->IsSceneDirty()) {
PendingExitPopup_ = true;
return true;
}
Window_.RequestClose();
return true;
}
void MetaCoreEditorApp::DrawPendingExitPopup() {
if (PendingExitPopup_) {
ImGui::OpenPopup("Exit Unsaved Scene Changes");
PendingExitPopup_ = false;
}
if (ImGui::BeginPopupModal("Exit Unsaved Scene Changes", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
const auto scenePersistenceService = ModuleRegistry_.ResolveService<MetaCoreIScenePersistenceService>();
const std::string currentScene = scenePersistenceService != nullptr && scenePersistenceService->HasOpenScene()
? scenePersistenceService->GetCurrentSceneDisplayName()
: std::string("Untitled");
ImGui::Text("Save changes to %s before exiting?", currentScene.c_str());
ImGui::Separator();
if (ImGui::Button("Save and Exit", ImVec2(130, 0))) {
if (scenePersistenceService == nullptr || scenePersistenceService->SaveCurrentScene(*EditorContext_)) {
Window_.RequestClose();
ImGui::CloseCurrentPopup();
} else {
EditorContext_->AddConsoleMessage(MetaCoreLogLevel::Error, "Scene", "保存失败,已取消退出");
}
}
ImGui::SameLine();
if (ImGui::Button("Don't Save", ImVec2(110, 0))) {
Window_.RequestClose();
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(90, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
@ -1601,6 +1754,7 @@ void MetaCoreEditorApp::DrawSceneViewWindow() {
MetaCoreSceneView sceneView = EditorContext_->GetCameraController().BuildSceneView();
sceneView.SelectedObjectId = EditorContext_->GetSelectedObjectId();
ViewportRenderer_.SetEditorGridVisible(EditorContext_->GetShowViewportGrid());
ViewportRenderer_.RenderSceneToViewport(Scene_, sceneView);
if (void* texPtr = ViewportRenderer_.GetFilamentTexturePointer(); texPtr != nullptr) {
@ -1753,7 +1907,7 @@ void MetaCoreEditorApp::DrawSceneViewWindow() {
}
}
MetaCoreDrawViewportGridOverlay(*EditorContext_, viewportState);
MetaCoreDrawViewportGridOverlay(*EditorContext_, sceneView, viewportState);
MetaCoreDrawWorldOriginOverlay(*EditorContext_, sceneView, viewportState);
MetaCoreDrawSelectionBoundsOverlay(*EditorContext_, Scene_, sceneView, viewportState);
MetaCoreDrawSelectionHierarchyOverlay(*EditorContext_, Scene_, sceneView, viewportState);

View File

@ -34,6 +34,8 @@ private:
void DrawEditorFrame();
void DrawEditorToolbar();
void DrawEditorStatusBar();
void DrawPendingExitPopup();
bool RequestEditorCloseWithSavePrompt();
void DrawSceneViewWindow();
void DrawGameViewWindow();
void DrawPlaceholderDockWindow(const char* label, const char* caption);
@ -56,6 +58,7 @@ private:
std::filesystem::path ImGuiLayoutProjectRoot_{};
std::filesystem::path ViewportProjectRoot_{};
std::string ImGuiIniPath_{};
bool PendingExitPopup_ = false;
bool Initialized_ = false;
};

View File

@ -112,6 +112,10 @@ LRESULT CALLBACK MetaCoreWindowSubclassProc(HWND hwnd, UINT msg, WPARAM wparam,
handledByEditor = owner.MessageHandler(hwnd, msg, static_cast<std::uintptr_t>(wparam), static_cast<std::intptr_t>(lparam));
}
if (msg == WM_CLOSE && handledByEditor) {
return 1;
}
if (msg == WM_MOUSEWHEEL) {
owner.Input.AddMouseWheelDelta(static_cast<float>(GET_WHEEL_DELTA_WPARAM(wparam)) / static_cast<float>(WHEEL_DELTA));
} else if (msg == WM_CLOSE) {

View File

@ -79,6 +79,12 @@ void MetaCoreEditorViewportRenderer::SetViewportRect(const MetaCoreViewportRect&
}
}
void MetaCoreEditorViewportRenderer::SetEditorGridVisible(bool visible) {
(void)visible;
EditorGridVisible_ = false;
FilamentSceneBridge_.SetEditorGridVisible(false);
}
void MetaCoreEditorViewportRenderer::RenderSceneToViewport(MetaCoreScene& scene, const MetaCoreSceneView& sceneView, bool useScenePrimaryCamera) {
if (RenderDevice_ == nullptr) {
return;
@ -88,6 +94,7 @@ void MetaCoreEditorViewportRenderer::RenderSceneToViewport(MetaCoreScene& scene,
// 动态调整离屏纹理大小以匹配 ImGui 视口
FilamentSceneBridge_.Resize(static_cast<int>(ViewportRect_.Width), static_cast<int>(ViewportRect_.Height));
FilamentSceneBridge_.SetEditorGridVisible(false);
// 更新并同步 Filament 桥接器(但不在这里调用 Render交由 RenderAll 统一渲染)
FilamentSceneBridge_.ApplySceneView(sceneView);

View File

@ -24,6 +24,8 @@
#include <filament/Material.h>
#include <filament/MaterialInstance.h>
#include <filament/IndirectLight.h>
#include <filament/IndexBuffer.h>
#include <filament/VertexBuffer.h>
#include <gltfio/AssetLoader.h>
#include <gltfio/FilamentAsset.h>
@ -46,7 +48,10 @@
#include <iostream>
#include <fstream>
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <filesystem>
#include <unordered_map>
#include <unordered_set>
#include <map>
@ -70,6 +75,45 @@ namespace MetaCore {
namespace {
struct MetaCoreEditorGridVertex {
float X;
float Y;
float Z;
};
[[nodiscard]] std::filesystem::path MetaCoreGetExecutableDirectory() {
std::array<wchar_t, 32768> executablePath{};
const DWORD length = GetModuleFileNameW(
nullptr,
executablePath.data(),
static_cast<DWORD>(executablePath.size())
);
if (length == 0 || length >= executablePath.size()) {
return {};
}
return std::filesystem::path(executablePath.data()).parent_path();
}
[[nodiscard]] std::ifstream MetaCoreOpenEditorGridMaterial() {
std::vector<std::filesystem::path> candidates{};
const std::filesystem::path executableDirectory = MetaCoreGetExecutableDirectory();
if (!executableDirectory.empty()) {
candidates.push_back(executableDirectory / "editorGrid.filamat");
}
candidates.emplace_back("editorGrid.filamat");
candidates.emplace_back("../editorGrid.filamat");
candidates.emplace_back("build/editorGrid.filamat");
for (const std::filesystem::path& candidate : candidates) {
std::ifstream file(candidate, std::ios::binary);
if (file.is_open()) {
return file;
}
}
return {};
}
[[nodiscard]] filament::backend::Backend MetaCoreToFilamentBackend(MetaCoreRenderBackend backend) {
switch (backend) {
case MetaCoreRenderBackend::OpenGL:
@ -423,6 +467,7 @@ public:
// 开启高级后处理
View_->setAntiAliasing(filament::View::AntiAliasing::FXAA);
View_->setBloomOptions({ .strength = 0.15f, .resolution = 384, .levels = 6, .blendMode = filament::View::BloomOptions::BlendMode::ADD, .threshold = true });
CreateEditorGridResources();
std::cout << "FilamentSceneBridge: Initialized with Offscreen=" << (offscreen ? "true" : "false") << std::endl;
return true;
@ -431,6 +476,7 @@ public:
void Shutdown() {
if (Engine_) {
std::cout << "FilamentSceneBridge: Shutting down..." << std::endl;
DestroyEditorGridResources();
// 1. 销毁所有加载的资产,切断其内部 Renderable 对克隆材质实例的占用引用
std::cout << "[DEBUG] LoadedAssets_ size: " << LoadedAssets_.size() << std::endl;
@ -579,6 +625,181 @@ public:
ProjectRootPath_ = projectRootPath;
}
void SetEditorGridVisible(bool visible) {
EditorGridVisible_ = visible;
SyncEditorGridVisibility();
}
void CreateEditorGridResources() {
if (Engine_ == nullptr || Scene_ == nullptr || EditorGridEntity_) {
return;
}
std::ifstream materialFile = MetaCoreOpenEditorGridMaterial();
if (!materialFile.is_open()) {
std::cerr << "FilamentSceneBridge: editorGrid.filamat not found; viewport grid disabled." << std::endl;
return;
}
std::vector<char> materialBuffer(
(std::istreambuf_iterator<char>(materialFile)),
std::istreambuf_iterator<char>()
);
if (materialBuffer.empty()) {
std::cerr << "FilamentSceneBridge: editorGrid.filamat is empty; viewport grid disabled." << std::endl;
return;
}
EditorGridMaterial_ = filament::Material::Builder()
.package(materialBuffer.data(), materialBuffer.size())
.build(*Engine_);
if (EditorGridMaterial_ == nullptr) {
std::cerr << "FilamentSceneBridge: failed to build editor grid material." << std::endl;
return;
}
EditorGridMaterialInstance_ = EditorGridMaterial_->createInstance();
if (EditorGridMaterialInstance_ != nullptr) {
EditorGridMaterialInstance_->setParameter("minorAlpha", 0.045F);
EditorGridMaterialInstance_->setParameter("majorAlpha", 0.11F);
EditorGridMaterialInstance_->setParameter("axisAlpha", 0.18F);
EditorGridMaterialInstance_->setParameter("cameraPosition", filament::math::float3{0.0F, 0.0F, 0.0F});
EditorGridMaterialInstance_->setParameter("fadeStart", 45.0F);
EditorGridMaterialInstance_->setParameter("fadeEnd", 220.0F);
}
constexpr float extent = 1000.0F;
const MetaCoreEditorGridVertex vertices[] = {
{-extent, -extent, 0.0F},
{ extent, -extent, 0.0F},
{ extent, extent, 0.0F},
{-extent, extent, 0.0F}
};
const uint16_t indices[] = {0, 1, 2, 0, 2, 3};
EditorGridVertexBuffer_ = filament::VertexBuffer::Builder()
.vertexCount(4)
.bufferCount(1)
.attribute(filament::VertexAttribute::POSITION, 0, filament::VertexBuffer::AttributeType::FLOAT3, 0, sizeof(MetaCoreEditorGridVertex))
.build(*Engine_);
EditorGridIndexBuffer_ = filament::IndexBuffer::Builder()
.indexCount(6)
.bufferType(filament::IndexBuffer::IndexType::USHORT)
.build(*Engine_);
if (EditorGridVertexBuffer_ == nullptr || EditorGridIndexBuffer_ == nullptr || EditorGridMaterialInstance_ == nullptr) {
std::cerr << "FilamentSceneBridge: failed to allocate editor grid buffers." << std::endl;
DestroyEditorGridResources();
return;
}
auto* vertexCopy = new MetaCoreEditorGridVertex[4];
std::copy(std::begin(vertices), std::end(vertices), vertexCopy);
EditorGridVertexBuffer_->setBufferAt(
*Engine_,
0,
filament::VertexBuffer::BufferDescriptor(
vertexCopy,
sizeof(vertices),
[](void* buffer, size_t, void*) { delete[] static_cast<MetaCoreEditorGridVertex*>(buffer); }
)
);
auto* indexCopy = new uint16_t[6];
std::copy(std::begin(indices), std::end(indices), indexCopy);
EditorGridIndexBuffer_->setBuffer(
*Engine_,
filament::IndexBuffer::BufferDescriptor(
indexCopy,
sizeof(indices),
[](void* buffer, size_t, void*) { delete[] static_cast<uint16_t*>(buffer); }
)
);
EditorGridEntity_ = utils::EntityManager::get().create();
filament::RenderableManager::Builder(1)
.boundingBox({{0.0F, 0.0F, 0.0F}, {extent, extent, 0.01F}})
.geometry(0, filament::RenderableManager::PrimitiveType::TRIANGLES, EditorGridVertexBuffer_, EditorGridIndexBuffer_, 0, 6)
.material(0, EditorGridMaterialInstance_)
.culling(false)
.castShadows(false)
.receiveShadows(false)
.build(*Engine_, EditorGridEntity_);
SyncEditorGridVisibility();
}
void DestroyEditorGridResources() {
if (Engine_ == nullptr) {
return;
}
if (EditorGridEntity_) {
if (Scene_ != nullptr) {
Scene_->remove(EditorGridEntity_);
}
Engine_->destroy(EditorGridEntity_);
utils::EntityManager::get().destroy(EditorGridEntity_);
EditorGridEntity_.clear();
}
if (EditorGridVertexBuffer_ != nullptr) {
Engine_->destroy(EditorGridVertexBuffer_);
EditorGridVertexBuffer_ = nullptr;
}
if (EditorGridIndexBuffer_ != nullptr) {
Engine_->destroy(EditorGridIndexBuffer_);
EditorGridIndexBuffer_ = nullptr;
}
if (EditorGridMaterialInstance_ != nullptr) {
Engine_->destroy(EditorGridMaterialInstance_);
EditorGridMaterialInstance_ = nullptr;
}
if (EditorGridMaterial_ != nullptr) {
Engine_->destroy(EditorGridMaterial_);
EditorGridMaterial_ = nullptr;
}
EditorGridInScene_ = false;
}
void SyncEditorGridVisibility() {
if (Scene_ == nullptr || !EditorGridEntity_) {
EditorGridInScene_ = false;
return;
}
if (EditorGridVisible_ && !EditorGridInScene_) {
Scene_->addEntity(EditorGridEntity_);
EditorGridInScene_ = true;
} else if (!EditorGridVisible_ && EditorGridInScene_) {
Scene_->remove(EditorGridEntity_);
EditorGridInScene_ = false;
}
}
void UpdateEditorGridTransform(const MetaCoreSceneView& sceneView) {
if (Engine_ == nullptr || !EditorGridEntity_) {
return;
}
auto& transformManager = Engine_->getTransformManager();
auto instance = transformManager.getInstance(EditorGridEntity_);
if (!instance) {
return;
}
const float x = std::floor(sceneView.CameraTarget.x / 10.0F) * 10.0F;
const float y = std::floor(sceneView.CameraTarget.y / 10.0F) * 10.0F;
const glm::mat4 transform = glm::translate(glm::mat4(1.0F), glm::vec3(x, y, 0.0F));
transformManager.setTransform(instance, *reinterpret_cast<const filament::math::mat4f*>(glm::value_ptr(transform)));
if (EditorGridMaterialInstance_ != nullptr) {
EditorGridMaterialInstance_->setParameter(
"cameraPosition",
filament::math::float3{sceneView.CameraPosition.x, sceneView.CameraPosition.y, sceneView.CameraPosition.z}
);
}
}
void ParseModelHierarchyToEcs(MetaCoreScene& scene, MetaCoreGameObject& parentObject, filament::gltfio::FilamentAsset* asset) {
if (!asset) return;
@ -1192,6 +1413,7 @@ public:
void ApplySceneView(const MetaCoreSceneView& sceneView) {
if (!Camera_ || !View_) return;
UpdateEditorGridTransform(sceneView);
// 1. 视图矩阵同步
const glm::mat4 viewMatrix = glm::lookAt(sceneView.CameraPosition, sceneView.CameraTarget, sceneView.CameraUp);
@ -1659,6 +1881,14 @@ private:
std::unordered_map<MetaCoreId, utils::Entity> SceneLightEntities_;
std::unordered_set<utils::Entity, utils::Entity::Hasher> EntitiesInScene_;
utils::Entity EditorGridEntity_{};
filament::VertexBuffer* EditorGridVertexBuffer_ = nullptr;
filament::IndexBuffer* EditorGridIndexBuffer_ = nullptr;
filament::Material* EditorGridMaterial_ = nullptr;
filament::MaterialInstance* EditorGridMaterialInstance_ = nullptr;
bool EditorGridVisible_ = true;
bool EditorGridInScene_ = false;
filament::View* UIView_ = nullptr;
MetaCoreImGuiHelper* ImGuiHelper_ = nullptr;
filament::View* RuntimeUIView_ = nullptr;
@ -1708,6 +1938,10 @@ void MetaCoreFilamentSceneBridge::ApplySceneView(const MetaCoreSceneView& sceneV
Impl_->ApplySceneView(sceneView);
}
void MetaCoreFilamentSceneBridge::SetEditorGridVisible(bool visible) {
Impl_->SetEditorGridVisible(visible);
}
void MetaCoreFilamentSceneBridge::SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height) {
Impl_->SetRuntimeUiOverlayFrame(frame, width, height);
}

View File

@ -0,0 +1,83 @@
material {
name : MetaCoreEditorGrid,
blending : transparent,
culling : none,
depthCulling : true,
depthWrite : false,
shadingModel : unlit,
variantFilter : [ skinning, shadowReceiver, vsm, fog ],
parameters : [
{
type : float,
name : minorAlpha
},
{
type : float,
name : majorAlpha
},
{
type : float,
name : axisAlpha
},
{
type : float3,
name : cameraPosition
},
{
type : float,
name : fadeStart
},
{
type : float,
name : fadeEnd
}
],
variables : [ gridWorldPosition ]
}
vertex {
void materialVertex(inout MaterialVertexInputs material) {
highp vec4 worldPosition = getWorldFromModelMatrix() * getPosition();
material.gridWorldPosition.xyz = worldPosition.xyz;
}
}
fragment {
highp float gridLine(highp vec2 coord, highp float pixelWidth) {
highp vec2 dudv = max(fwidth(coord), vec2(1e-6));
highp vec2 uvMod = fract(coord);
highp vec2 uvDist = min(uvMod, 1.0 - uvMod);
highp vec2 distInPixels = uvDist / dudv;
highp vec2 lineAlpha = 1.0 - smoothstep(pixelWidth, pixelWidth + 0.85, distInPixels);
return max(lineAlpha.x, lineAlpha.y);
}
highp float axisLine(highp float coord, highp float pixelWidth) {
highp float dudv = max(fwidth(coord), 1e-6);
highp float distInPixels = abs(coord) / dudv;
return 1.0 - smoothstep(pixelWidth, pixelWidth + 1.0, distInPixels);
}
void material(inout MaterialInputs material) {
prepareMaterial(material);
highp vec2 coord = variable_gridWorldPosition.xy;
highp float distanceToCamera = distance(coord, materialParams.cameraPosition.xy);
highp float distanceFade = 1.0 - smoothstep(materialParams.fadeStart, materialParams.fadeEnd, distanceToCamera);
highp vec2 minorDeriv = fwidth(coord);
highp float minorLodFade = 1.0 - smoothstep(0.10, 0.38, max(minorDeriv.x, minorDeriv.y));
highp float minor = gridLine(coord / 2.0, 0.18) * materialParams.minorAlpha * minorLodFade;
highp float major = gridLine(coord / 10.0, 0.32) * materialParams.majorAlpha;
highp float axis = max(axisLine(coord.x, 0.35), axisLine(coord.y, 0.35)) * materialParams.axisAlpha;
highp float alpha = max(max(minor, major), axis) * distanceFade;
if (alpha < 0.004) {
discard;
}
material.baseColor = vec4(vec3(0.46, 0.50, 0.56), alpha);
}
}

View File

@ -19,6 +19,7 @@ public:
bool Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window, bool offscreen = true);
void Shutdown();
void SetViewportRect(const MetaCoreViewportRect& viewportRect);
void SetEditorGridVisible(bool visible);
void RenderSceneToViewport(MetaCoreScene& scene, const MetaCoreSceneView& sceneView, bool useScenePrimaryCamera = false);
void SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height);
void RenderAll();
@ -32,6 +33,7 @@ private:
MetaCoreRenderDevice* RenderDevice_ = nullptr;
MetaCoreFilamentSceneBridge FilamentSceneBridge_{};
MetaCoreViewportRect ViewportRect_{};
bool EditorGridVisible_ = false;
};

View File

@ -32,6 +32,7 @@ public:
void SyncScene(MetaCoreScene& scene, bool compatibilityMeshOnly = false, bool useScenePrimaryCamera = false);
void SyncScene(const MetaCoreSceneRenderSyncSnapshot& snapshot, MetaCoreScene* scene = nullptr, bool compatibilityMeshOnly = false, bool useScenePrimaryCamera = false);
void ApplySceneView(const MetaCoreSceneView& sceneView);
void SetEditorGridVisible(bool visible);
void SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height);
void RenderAll();
[[nodiscard]] uint32_t GetGLTextureId() const;

View File

@ -277,6 +277,136 @@ void MetaCoreTestUndoRedo() {
MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount + 1, "Redo 后对象数量应再次增加");
}
void MetaCoreTestHierarchyEditingCommandsDirtyAndUndo() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreHierarchyEditingCommandsSmoke";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "Library");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
MetaCoreExpect(projectFile.is_open(), "应能写入 Hierarchy 命令测试项目文件");
projectFile << "{\n"
<< " \"name\": \"HierarchyEditingCommandsSmoke\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene();
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto scenePersistenceService = moduleRegistry.ResolveService<MetaCore::MetaCoreIScenePersistenceService>();
const auto sceneEditingService = moduleRegistry.ResolveService<MetaCore::MetaCoreISceneEditingService>();
const auto clipboardService = moduleRegistry.ResolveService<MetaCore::MetaCoreIClipboardService>();
MetaCoreExpect(scenePersistenceService != nullptr, "Hierarchy command test should resolve scene persistence service");
MetaCoreExpect(sceneEditingService != nullptr, "Hierarchy command test should resolve scene editing service");
MetaCoreExpect(clipboardService != nullptr, "Hierarchy command test should resolve clipboard service");
const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "HierarchyCommands.mcscene.json";
MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, scenePath), "Hierarchy command baseline scene should save");
MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Hierarchy command baseline should be clean after save");
const std::size_t baselineCount = scene.GetGameObjects().size();
const auto createdId = sceneEditingService->CreateGameObject(
editorContext,
"HierarchyObject",
false,
std::optional<MetaCore::MetaCoreId>{0}
);
MetaCoreExpect(createdId.has_value(), "CreateGameObject command should create an object");
MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "CreateGameObject command should mark scene dirty");
MetaCoreExpect(editorContext.UndoCommand(), "Undo CreateGameObject should succeed");
MetaCoreExpect(scene.GetGameObjects().size() == baselineCount, "Undo CreateGameObject should restore object count");
MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Undo CreateGameObject should restore clean baseline");
MetaCoreExpect(editorContext.RedoCommand(), "Redo CreateGameObject should succeed");
MetaCoreExpect(scene.FindGameObject(*createdId), "Redo CreateGameObject should restore created object");
MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "Redo CreateGameObject should mark scene dirty again");
MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "Hierarchy command scene should save after create");
MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Hierarchy command scene should be clean after create save");
MetaCoreExpect(sceneEditingService->RenameGameObject(editorContext, *createdId, "HierarchyObjectRenamed"), "RenameGameObject command should rename object");
MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "RenameGameObject command should mark scene dirty");
MetaCoreExpect(editorContext.UndoCommand(), "Undo RenameGameObject should succeed");
MetaCoreExpect(scene.FindGameObject(*createdId).GetName() == "HierarchyObject", "Undo RenameGameObject should restore old name");
MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Undo RenameGameObject should restore clean baseline");
MetaCoreExpect(editorContext.RedoCommand(), "Redo RenameGameObject should succeed");
MetaCoreExpect(scene.FindGameObject(*createdId).GetName() == "HierarchyObjectRenamed", "Redo RenameGameObject should restore new name");
MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "Hierarchy command scene should save after rename");
const auto parentId = sceneEditingService->CreateGameObject(
editorContext,
"HierarchyParent",
false,
std::optional<MetaCore::MetaCoreId>{0}
);
MetaCoreExpect(parentId.has_value(), "CreateGameObject command should create a parent object");
MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "Hierarchy command scene should save after parent create");
MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Hierarchy command scene should be clean before reparent");
editorContext.SelectOnly(*createdId);
MetaCoreExpect(sceneEditingService->ReparentSelection(editorContext, *parentId), "ReparentSelection command should reparent selected object");
MetaCoreExpect(scene.FindGameObject(*createdId).GetParentId() == *parentId, "ReparentSelection should set parent id");
MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "ReparentSelection command should mark scene dirty");
MetaCoreExpect(editorContext.UndoCommand(), "Undo ReparentSelection should succeed");
MetaCoreExpect(scene.FindGameObject(*createdId).GetParentId() == 0, "Undo ReparentSelection should restore root parent");
MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Undo ReparentSelection should restore clean baseline");
MetaCoreExpect(editorContext.RedoCommand(), "Redo ReparentSelection should succeed");
MetaCoreExpect(scene.FindGameObject(*createdId).GetParentId() == *parentId, "Redo ReparentSelection should restore parent id");
MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "Hierarchy command scene should save after reparent");
editorContext.SelectOnly(*createdId);
const std::size_t countBeforeDuplicate = scene.GetGameObjects().size();
MetaCoreExpect(clipboardService->DuplicateSelection(editorContext), "DuplicateSelection command should duplicate selected object");
MetaCoreExpect(scene.GetGameObjects().size() > countBeforeDuplicate, "DuplicateSelection should add at least one object");
MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "DuplicateSelection command should mark scene dirty");
MetaCoreExpect(editorContext.UndoCommand(), "Undo DuplicateSelection should succeed");
MetaCoreExpect(scene.GetGameObjects().size() == countBeforeDuplicate, "Undo DuplicateSelection should restore object count");
MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Undo DuplicateSelection should restore clean baseline");
MetaCoreExpect(editorContext.RedoCommand(), "Redo DuplicateSelection should succeed");
MetaCoreExpect(scene.GetGameObjects().size() > countBeforeDuplicate, "Redo DuplicateSelection should add object again");
MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "Hierarchy command scene should save after duplicate");
const MetaCore::MetaCoreId duplicatedId = editorContext.GetActiveObjectId();
MetaCoreExpect(duplicatedId != 0 && duplicatedId != *createdId, "DuplicateSelection should select duplicated object");
const std::size_t countBeforeDelete = scene.GetGameObjects().size();
MetaCoreExpect(clipboardService->DeleteSelection(editorContext), "DeleteSelection command should delete selected object");
MetaCoreExpect(scene.GetGameObjects().size() < countBeforeDelete, "DeleteSelection should remove selected object");
MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "DeleteSelection command should mark scene dirty");
MetaCoreExpect(editorContext.UndoCommand(), "Undo DeleteSelection should succeed");
MetaCoreExpect(scene.GetGameObjects().size() == countBeforeDelete, "Undo DeleteSelection should restore object count");
MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Undo DeleteSelection should restore clean baseline");
MetaCoreExpect(editorContext.RedoCommand(), "Redo DeleteSelection should succeed");
MetaCoreExpect(scene.GetGameObjects().size() < countBeforeDelete, "Redo DeleteSelection should remove object again");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestBuiltinModuleComposition() {
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
@ -6358,6 +6488,8 @@ int main() {
MetaCoreTestSelectionStateMachine();
std::cout << "[RUN] MetaCoreTestUndoRedo..." << std::endl;
MetaCoreTestUndoRedo();
std::cout << "[RUN] MetaCoreTestHierarchyEditingCommandsDirtyAndUndo..." << std::endl;
MetaCoreTestHierarchyEditingCommandsDirtyAndUndo();
std::cout << "[RUN] MetaCoreTestBuiltinModuleComposition..." << std::endl;
MetaCoreTestBuiltinModuleComposition();
std::cout << "[RUN] MetaCoreTestLegacyBinarySceneStartupLoadCompatibility..." << std::endl;