Implement Unity-like editor interactions with undo and multiselect

This commit is contained in:
ayuan9957 2026-03-25 10:50:42 +08:00
parent ea6291fba3
commit 3409fec66b
12 changed files with 1599 additions and 49 deletions

View File

@ -153,6 +153,7 @@ target_compile_options(MetaCoreRender PRIVATE ${METACORE_COMMON_WARNINGS})
set(METACORE_EDITOR_HEADERS
Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h
Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorCommandService.h
Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h
Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorModule.h
)
@ -167,6 +168,7 @@ set(METACORE_EDITOR_SOURCES
Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp
Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp
Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.cpp
Source/MetaCoreEditor/Private/MetaCoreEditorCommandService.cpp
Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp
Source/MetaCoreEditor/Private/MetaCoreEditorModule.cpp
third_party/ImGuizmo/ImGuizmo.cpp

View File

@ -2,11 +2,7 @@
#include "MetaCoreEditor/MetaCoreEditorContext.h"
#include "MetaCoreEditorCameraController.h"
#include "MetaCorePlatform/MetaCoreInput.h"
#include "MetaCorePlatform/MetaCoreWindow.h"
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
#include "MetaCoreRender/MetaCoreRenderTypes.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include <imgui.h>
@ -15,40 +11,227 @@
#include <array>
#include <cstdio>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace MetaCore {
namespace {
void MetaCoreDrawHierarchyNode(MetaCoreEditorContext& editorContext, MetaCoreId objectId) {
MetaCoreScene& scene = editorContext.GetScene();
MetaCoreGameObject* gameObject = scene.FindGameObject(objectId);
if (gameObject == nullptr) {
[[nodiscard]] bool MetaCoreRenameObject(MetaCoreEditorContext& editorContext, MetaCoreId objectId, const std::string& name) {
if (objectId == 0 || name.empty()) {
return false;
}
const bool renamed = editorContext.ExecuteSnapshotCommand("重命名对象", [&]() {
return editorContext.GetScene().RenameGameObject(objectId, name);
});
if (renamed) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Hierarchy", "已重命名对象: " + name);
}
return renamed;
}
[[nodiscard]] std::optional<MetaCoreId> MetaCoreCreateObject(
MetaCoreEditorContext& editorContext,
const std::string& objectName,
bool withMeshRenderer,
std::optional<MetaCoreId> forcedParentId
) {
MetaCoreId createdObjectId = 0;
const bool created = editorContext.ExecuteSnapshotCommand("创建对象", [&]() {
MetaCoreId parentId = forcedParentId.value_or(editorContext.GetActiveObjectId());
if (parentId != 0 && editorContext.GetScene().FindGameObject(parentId) == nullptr) {
parentId = 0;
}
MetaCoreGameObject& object = editorContext.GetScene().CreateGameObject(objectName, parentId);
if (withMeshRenderer) {
object.MeshRenderer = MetaCoreMeshRendererComponent{};
object.Transform.Position = glm::vec3(0.0F, 0.5F, 0.0F);
}
createdObjectId = object.Id;
editorContext.SelectOnly(createdObjectId);
editorContext.SetSelectionAnchorId(createdObjectId);
return true;
});
if (!created || createdObjectId == 0) {
return std::nullopt;
}
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Info,
"Scene",
withMeshRenderer ? "已创建立方体对象" : "已创建空对象"
);
return createdObjectId;
}
[[nodiscard]] bool MetaCoreDuplicateSelection(MetaCoreEditorContext& editorContext) {
const std::vector<MetaCoreId> selectedIds = editorContext.GetSelectedObjectIds();
if (selectedIds.empty()) {
return false;
}
std::vector<MetaCoreId> duplicatedRootIds;
const bool duplicated = editorContext.ExecuteSnapshotCommand("复制对象", [&]() {
duplicatedRootIds = editorContext.GetScene().DuplicateGameObjects(selectedIds);
if (duplicatedRootIds.empty()) {
return false;
}
editorContext.SetSelection(duplicatedRootIds, duplicatedRootIds.back());
return true;
});
if (duplicated) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已复制对象");
}
return duplicated;
}
[[nodiscard]] bool MetaCoreDeleteSelection(MetaCoreEditorContext& editorContext) {
const std::vector<MetaCoreId> selectedIds = editorContext.GetSelectedObjectIds();
if (selectedIds.empty()) {
return false;
}
std::vector<MetaCoreId> deletedIds;
const bool deleted = editorContext.ExecuteSnapshotCommand("删除对象", [&]() {
deletedIds = editorContext.GetScene().DeleteGameObjects(selectedIds);
if (deletedIds.empty()) {
return false;
}
editorContext.ClearSelection();
return true;
});
if (deleted) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已删除对象");
}
return deleted;
}
[[nodiscard]] bool MetaCoreReparentSelection(MetaCoreEditorContext& editorContext, MetaCoreId newParentId) {
const std::vector<MetaCoreId> selectedIds = editorContext.GetSelectedObjectIds();
if (selectedIds.empty()) {
return false;
}
const bool keepWorldTransform = editorContext.GetReparentTransformRule() == MetaCoreReparentTransformRule::KeepWorld;
const bool reparented = editorContext.ExecuteSnapshotCommand("重挂接对象", [&]() {
return editorContext.GetScene().ReparentGameObjects(selectedIds, newParentId, keepWorldTransform);
});
if (reparented) {
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Info,
"Hierarchy",
keepWorldTransform ? "已重挂接对象(保持世界变换)" : "已重挂接对象(保持局部变换)"
);
}
return reparented;
}
void MetaCoreHandleUndo(MetaCoreEditorContext& editorContext) {
if (editorContext.UndoCommand()) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Editor", "已撤销操作");
}
}
void MetaCoreHandleRedo(MetaCoreEditorContext& editorContext) {
if (editorContext.RedoCommand()) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Editor", "已重做操作");
}
}
void MetaCoreHandleGlobalEditorShortcuts(MetaCoreEditorContext& editorContext) {
const ImGuiIO& io = ImGui::GetIO();
if (io.WantTextInput) {
return;
}
const std::vector<MetaCoreId> childIds = scene.GetChildrenOf(objectId);
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth;
if (childIds.empty()) {
flags |= ImGuiTreeNodeFlags_Leaf;
}
if (editorContext.GetSelectedObjectId() == objectId) {
flags |= ImGuiTreeNodeFlags_Selected;
const bool ctrlDown = io.KeyCtrl;
const bool shiftDown = io.KeyShift;
if (ctrlDown && shiftDown && ImGui::IsKeyPressed(ImGuiKey_Z, false)) {
MetaCoreHandleRedo(editorContext);
return;
}
const bool nodeOpened = ImGui::TreeNodeEx(reinterpret_cast<void*>(static_cast<intptr_t>(objectId)), flags, "%s", gameObject->Name.c_str());
if (ImGui::IsItemClicked()) {
editorContext.SetSelectedObjectId(objectId);
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Hierarchy", "已选中对象: " + gameObject->Name);
if (ctrlDown && ImGui::IsKeyPressed(ImGuiKey_Z, false)) {
MetaCoreHandleUndo(editorContext);
return;
}
if (nodeOpened) {
for (MetaCoreId childId : childIds) {
MetaCoreDrawHierarchyNode(editorContext, childId);
if (ctrlDown && ImGui::IsKeyPressed(ImGuiKey_Y, false)) {
MetaCoreHandleRedo(editorContext);
return;
}
if (ctrlDown && ImGui::IsKeyPressed(ImGuiKey_D, false)) {
(void)MetaCoreDuplicateSelection(editorContext);
return;
}
if (ImGui::IsKeyPressed(ImGuiKey_Delete, false)) {
(void)MetaCoreDeleteSelection(editorContext);
return;
}
if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) {
editorContext.RequestRenameActiveObject();
}
}
void MetaCoreApplyHierarchySelection(MetaCoreEditorContext& editorContext, MetaCoreId objectId, bool allowToggle) {
if (objectId == 0 || editorContext.GetScene().FindGameObject(objectId) == nullptr) {
return;
}
const ImGuiIO& io = ImGui::GetIO();
if (io.KeyShift) {
editorContext.SelectRangeByOrderedIds(editorContext.GetScene().BuildHierarchyPreorder(), objectId, io.KeyCtrl);
return;
}
if (allowToggle && io.KeyCtrl) {
editorContext.ToggleSelection(objectId);
return;
}
editorContext.SelectOnly(objectId);
}
std::unordered_map<ImGuiID, MetaCoreEditorStateSnapshot>& MetaCoreGetInspectorEditSnapshots() {
static std::unordered_map<ImGuiID, MetaCoreEditorStateSnapshot> snapshots;
return snapshots;
}
void MetaCoreTrackInspectorEdit(MetaCoreEditorContext& editorContext, const char* commandLabel, bool allowMerge) {
const ImGuiID itemId = ImGui::GetItemID();
if (itemId == 0) {
return;
}
auto& snapshots = MetaCoreGetInspectorEditSnapshots();
if (ImGui::IsItemActivated()) {
snapshots[itemId] = editorContext.CaptureStateSnapshot();
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
const auto iterator = snapshots.find(itemId);
if (iterator != snapshots.end()) {
const MetaCoreEditorStateSnapshot afterSnapshot = editorContext.CaptureStateSnapshot();
(void)editorContext.CommitStateTransition(commandLabel, iterator->second, afterSnapshot, allowMerge);
snapshots.erase(iterator);
}
ImGui::TreePop();
}
}
@ -57,6 +240,8 @@ public:
std::string GetProviderId() const override { return "MetaCoreDefaultMenuProvider"; }
void DrawMenuBar(MetaCoreEditorContext& editorContext) override {
MetaCoreHandleGlobalEditorShortcuts(editorContext);
if (ImGui::BeginMenu("文件")) {
if (ImGui::MenuItem("重置布局")) {
editorContext.SetDockLayoutBuilt(false);
@ -68,8 +253,22 @@ public:
}
if (ImGui::BeginMenu("编辑")) {
ImGui::MenuItem("撤销", "Ctrl+Z", false, false);
ImGui::MenuItem("重做", "Ctrl+Y", false, false);
if (ImGui::MenuItem("撤销", "Ctrl+Z", false, editorContext.GetCommandService().CanUndo())) {
MetaCoreHandleUndo(editorContext);
}
if (ImGui::MenuItem("重做", "Ctrl+Y / Ctrl+Shift+Z", false, editorContext.GetCommandService().CanRedo())) {
MetaCoreHandleRedo(editorContext);
}
ImGui::Separator();
if (ImGui::MenuItem("复制", "Ctrl+D", false, !editorContext.GetSelectedObjectIds().empty())) {
(void)MetaCoreDuplicateSelection(editorContext);
}
if (ImGui::MenuItem("删除", "Delete", false, !editorContext.GetSelectedObjectIds().empty())) {
(void)MetaCoreDeleteSelection(editorContext);
}
if (ImGui::MenuItem("重命名", "F2", false, editorContext.GetActiveObjectId() != 0)) {
editorContext.RequestRenameActiveObject();
}
ImGui::EndMenu();
}
@ -80,16 +279,17 @@ public:
if (ImGui::BeginMenu("游戏对象")) {
if (ImGui::MenuItem("创建空对象")) {
MetaCoreGameObject& object = editorContext.GetScene().CreateGameObject("GameObject");
editorContext.SetSelectedObjectId(object.Id);
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已创建空对象");
(void)MetaCoreCreateObject(editorContext, "GameObject", false, std::nullopt);
}
if (ImGui::MenuItem("创建立方体")) {
MetaCoreGameObject& cube = editorContext.GetScene().CreateGameObject("Cube");
cube.MeshRenderer = MetaCoreMeshRendererComponent{};
cube.Transform.Position = glm::vec3(0.0F, 0.5F, 0.0F);
editorContext.SetSelectedObjectId(cube.Id);
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已创建立方体");
(void)MetaCoreCreateObject(editorContext, "Cube", true, std::nullopt);
}
ImGui::Separator();
if (ImGui::MenuItem("复制", "Ctrl+D", false, !editorContext.GetSelectedObjectIds().empty())) {
(void)MetaCoreDuplicateSelection(editorContext);
}
if (ImGui::MenuItem("删除", "Delete", false, !editorContext.GetSelectedObjectIds().empty())) {
(void)MetaCoreDeleteSelection(editorContext);
}
ImGui::EndMenu();
}
@ -121,13 +321,229 @@ public:
bool IsOpenByDefault() const override { return true; }
void DrawPanel(MetaCoreEditorContext& editorContext) override {
if (const MetaCoreId requestedRenameId = editorContext.ConsumeRenameRequestObjectId(); requestedRenameId != 0) {
BeginRename(editorContext, requestedRenameId);
}
DrawToolbar(editorContext);
ImGui::TextUnformatted("SampleScene");
ImGui::Separator();
for (MetaCoreId rootId : editorContext.GetScene().GetRootObjectIds()) {
MetaCoreDrawHierarchyNode(editorContext, rootId);
DrawHierarchyNode(editorContext, rootId);
}
DrawRootDropTarget(editorContext);
DrawWindowContextMenu(editorContext);
DrawRenamePopup(editorContext);
}
private:
void DrawToolbar(MetaCoreEditorContext& editorContext) {
const bool keepWorld = editorContext.GetReparentTransformRule() == MetaCoreReparentTransformRule::KeepWorld;
if (ImGui::Button("保持世界", ImVec2(84.0F, 0.0F))) {
editorContext.SetReparentTransformRule(MetaCoreReparentTransformRule::KeepWorld);
}
ImGui::SameLine();
if (ImGui::Button("保持局部", ImVec2(84.0F, 0.0F))) {
editorContext.SetReparentTransformRule(MetaCoreReparentTransformRule::KeepLocal);
}
ImGui::SameLine();
ImGui::TextDisabled(keepWorld ? "当前: 世界" : "当前: 局部");
ImGui::Separator();
}
void DrawHierarchyNode(MetaCoreEditorContext& editorContext, MetaCoreId objectId) {
MetaCoreScene& scene = editorContext.GetScene();
MetaCoreGameObject* gameObject = scene.FindGameObject(objectId);
if (gameObject == nullptr) {
return;
}
const std::vector<MetaCoreId> childIds = scene.GetChildrenOf(objectId);
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth;
if (childIds.empty()) {
flags |= ImGuiTreeNodeFlags_Leaf;
}
if (editorContext.IsObjectSelected(objectId)) {
flags |= ImGuiTreeNodeFlags_Selected;
}
const bool nodeOpened = ImGui::TreeNodeEx(reinterpret_cast<void*>(static_cast<intptr_t>(objectId)), flags, "%s", gameObject->Name.c_str());
const bool leftClicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
const bool rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right);
if ((leftClicked || rightClicked) && !ImGui::IsItemToggledOpen()) {
if (leftClicked) {
MetaCoreApplyHierarchySelection(editorContext, objectId, true);
} else if (!editorContext.IsObjectSelected(objectId)) {
editorContext.SelectOnly(objectId);
}
}
if (ImGui::BeginDragDropSource()) {
if (!editorContext.IsObjectSelected(objectId)) {
editorContext.SelectOnly(objectId);
}
MetaCoreId payloadId = objectId;
ImGui::SetDragDropPayload("MC_HIERARCHY_OBJECT", &payloadId, sizeof(payloadId));
ImGui::Text("拖拽 %zu 个对象", editorContext.GetSelectedObjectIds().size());
ImGui::EndDragDropSource();
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("MC_HIERARCHY_OBJECT"); payload != nullptr) {
if (payload->DataSize == sizeof(MetaCoreId)) {
(void)MetaCoreReparentSelection(editorContext, objectId);
}
}
ImGui::EndDragDropTarget();
}
if (ImGui::BeginPopupContextItem()) {
if (!editorContext.IsObjectSelected(objectId)) {
editorContext.SelectOnly(objectId);
}
DrawNodeContextMenu(editorContext, objectId);
ImGui::EndPopup();
}
if (nodeOpened) {
for (MetaCoreId childId : childIds) {
DrawHierarchyNode(editorContext, childId);
}
ImGui::TreePop();
}
}
void DrawNodeContextMenu(MetaCoreEditorContext& editorContext, MetaCoreId objectId) {
if (ImGui::MenuItem("创建空对象")) {
(void)MetaCoreCreateObject(editorContext, "GameObject", false, objectId);
}
if (ImGui::MenuItem("创建立方体")) {
(void)MetaCoreCreateObject(editorContext, "Cube", true, objectId);
}
ImGui::Separator();
if (ImGui::MenuItem("重命名", "F2", false, objectId != 0)) {
BeginRename(editorContext, objectId);
}
if (ImGui::MenuItem("复制", "Ctrl+D", false, !editorContext.GetSelectedObjectIds().empty())) {
(void)MetaCoreDuplicateSelection(editorContext);
}
if (ImGui::MenuItem("删除", "Delete", false, !editorContext.GetSelectedObjectIds().empty())) {
(void)MetaCoreDeleteSelection(editorContext);
}
}
void DrawWindowContextMenu(MetaCoreEditorContext& editorContext) {
if (!ImGui::BeginPopupContextWindow("MetaCoreHierarchyWindowContext", ImGuiPopupFlags_NoOpenOverItems | ImGuiPopupFlags_MouseButtonRight)) {
return;
}
if (ImGui::MenuItem("创建空对象")) {
(void)MetaCoreCreateObject(editorContext, "GameObject", false, 0);
}
if (ImGui::MenuItem("创建立方体")) {
(void)MetaCoreCreateObject(editorContext, "Cube", true, 0);
}
ImGui::Separator();
if (ImGui::MenuItem("重命名", "F2", false, editorContext.GetActiveObjectId() != 0)) {
BeginRename(editorContext, editorContext.GetActiveObjectId());
}
if (ImGui::MenuItem("复制", "Ctrl+D", false, !editorContext.GetSelectedObjectIds().empty())) {
(void)MetaCoreDuplicateSelection(editorContext);
}
if (ImGui::MenuItem("删除", "Delete", false, !editorContext.GetSelectedObjectIds().empty())) {
(void)MetaCoreDeleteSelection(editorContext);
}
ImGui::EndPopup();
}
void DrawRootDropTarget(MetaCoreEditorContext& editorContext) {
ImGui::Separator();
ImGui::TextDisabled("拖拽到下方设为根节点");
const ImVec2 dropAreaMin = ImGui::GetCursorScreenPos();
const ImVec2 dropAreaSize = ImVec2(ImGui::GetContentRegionAvail().x, 24.0F);
ImGui::InvisibleButton("MetaCoreHierarchyRootDrop", dropAreaSize);
const ImVec2 dropAreaMax = ImVec2(dropAreaMin.x + dropAreaSize.x, dropAreaMin.y + dropAreaSize.y);
ImGui::GetWindowDrawList()->AddRect(
dropAreaMin,
dropAreaMax,
IM_COL32(96, 106, 122, 180),
2.0F
);
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("MC_HIERARCHY_OBJECT"); payload != nullptr) {
if (payload->DataSize == sizeof(MetaCoreId)) {
(void)MetaCoreReparentSelection(editorContext, 0);
}
}
ImGui::EndDragDropTarget();
}
}
void BeginRename(MetaCoreEditorContext& editorContext, MetaCoreId objectId) {
if (objectId == 0) {
return;
}
const MetaCoreGameObject* object = editorContext.GetScene().FindGameObject(objectId);
if (object == nullptr) {
return;
}
RenameObjectId_ = objectId;
RenameBuffer_.fill('\0');
std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", object->Name.c_str());
OpenRenamePopup_ = true;
}
void DrawRenamePopup(MetaCoreEditorContext& editorContext) {
if (RenameObjectId_ == 0) {
return;
}
if (OpenRenamePopup_) {
ImGui::OpenPopup("重命名对象##MetaCoreHierarchy");
OpenRenamePopup_ = false;
}
if (!ImGui::BeginPopupModal("重命名对象##MetaCoreHierarchy", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
return;
}
if (ImGui::IsWindowAppearing()) {
ImGui::SetKeyboardFocusHere();
}
const bool submittedByEnter = ImGui::InputText(
"名称",
RenameBuffer_.data(),
RenameBuffer_.size(),
ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_EnterReturnsTrue
);
bool closePopup = false;
if (submittedByEnter || ImGui::Button("确定", ImVec2(90.0F, 0.0F))) {
closePopup = MetaCoreRenameObject(editorContext, RenameObjectId_, RenameBuffer_.data());
}
ImGui::SameLine();
if (ImGui::Button("取消", ImVec2(90.0F, 0.0F))) {
closePopup = true;
}
if (closePopup) {
RenameObjectId_ = 0;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
MetaCoreId RenameObjectId_ = 0;
std::array<char, 256> RenameBuffer_{};
bool OpenRenamePopup_ = false;
};
class MetaCoreScenePanelProvider final : public MetaCoreIEditorPanelProvider {
@ -187,11 +603,16 @@ public:
std::string GetDrawerId() const override { return "Transform"; }
bool Supports(const MetaCoreGameObject&) const override { return true; }
void DrawInspector(MetaCoreEditorContext&, MetaCoreGameObject& gameObject) override {
void DrawInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) override {
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::DragFloat3("Position", glm::value_ptr(gameObject.Transform.Position), 0.05F);
MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true);
ImGui::DragFloat3("Rotation", glm::value_ptr(gameObject.Transform.RotationEulerDegrees), 0.5F);
MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true);
ImGui::DragFloat3("Scale", glm::value_ptr(gameObject.Transform.Scale), 0.05F, 0.05F, 100.0F);
MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true);
}
}
};
@ -203,9 +624,15 @@ public:
bool IsOpenByDefault() const override { return true; }
void DrawPanel(MetaCoreEditorContext& editorContext) override {
const std::size_t selectedCount = editorContext.GetSelectedObjectIds().size();
if (selectedCount == 0) {
ImGui::TextUnformatted("当前没有选中对象。");
return;
}
MetaCoreGameObject* selectedObject = editorContext.GetSelectedGameObject();
if (selectedObject == nullptr) {
ImGui::TextUnformatted("当前没有选中对象。");
ImGui::TextUnformatted("当前没有可编辑的主对象。");
return;
}
@ -215,9 +642,26 @@ public:
std::snprintf(NameBuffer_.data(), NameBuffer_.size(), "%s", selectedObject->Name.c_str());
}
if (selectedCount > 1) {
ImGui::Text("已选中 %zu 个对象(当前编辑 Active 对象)", selectedCount);
ImGui::Separator();
}
ImGui::Text("对象 ID: %llu", static_cast<unsigned long long>(selectedObject->Id));
if (ImGui::InputText("名称", NameBuffer_.data(), NameBuffer_.size())) {
selectedObject->Name = NameBuffer_.data();
bool renameSubmitted = ImGui::InputText(
"名称",
NameBuffer_.data(),
NameBuffer_.size(),
ImGuiInputTextFlags_EnterReturnsTrue
);
if (ImGui::IsItemDeactivatedAfterEdit()) {
renameSubmitted = true;
}
if (renameSubmitted) {
if (!MetaCoreRenameObject(editorContext, selectedObject->Id, NameBuffer_.data())) {
NameBuffer_.fill('\0');
std::snprintf(NameBuffer_.data(), NameBuffer_.size(), "%s", selectedObject->Name.c_str());
}
}
if (ImGui::Button("聚焦对象")) {
@ -242,13 +686,16 @@ public:
ImGui::Separator();
ImGui::TextUnformatted("Light");
ImGui::DragFloat("Intensity", &selectedObject->Light->Intensity, 0.05F, 0.0F, 8.0F);
MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true);
}
if (selectedObject->MeshRenderer.has_value()) {
ImGui::Separator();
ImGui::TextUnformatted("MeshRenderer");
ImGui::ColorEdit3("BaseColor", glm::value_ptr(selectedObject->MeshRenderer->BaseColor));
MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", true);
ImGui::Checkbox("Visible", &selectedObject->MeshRenderer->Visible);
MetaCoreTrackInspectorEdit(editorContext, "修改检查器属性", false);
}
}

View File

@ -28,6 +28,7 @@
#include <memory>
#include <limits>
#include <iostream>
#include <unordered_map>
#include <vector>
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
@ -221,6 +222,30 @@ MetaCoreId MetaCorePickGameObjectFromViewport(
const glm::vec3 rayOrigin(rayStartWorld);
const glm::vec3 rayDirection = glm::normalize(glm::vec3(rayEndWorld - rayStartWorld));
std::unordered_map<MetaCoreId, glm::mat4> worldMatrixCache;
const auto buildWorldMatrix = [&](const auto& self, MetaCoreId objectId) -> glm::mat4 {
if (objectId == 0) {
return glm::mat4(1.0F);
}
if (const auto cacheIterator = worldMatrixCache.find(objectId); cacheIterator != worldMatrixCache.end()) {
return cacheIterator->second;
}
const MetaCoreGameObject* object = scene.FindGameObject(objectId);
if (object == nullptr) {
return glm::mat4(1.0F);
}
glm::mat4 worldMatrix = MetaCoreBuildTransformMatrix(object->Transform);
if (object->ParentId != 0) {
worldMatrix = self(self, object->ParentId) * worldMatrix;
}
worldMatrixCache.emplace(objectId, worldMatrix);
return worldMatrix;
};
MetaCoreId bestObjectId = 0;
float bestHitDistance = std::numeric_limits<float>::max();
@ -229,7 +254,7 @@ MetaCoreId MetaCorePickGameObjectFromViewport(
continue;
}
const glm::mat4 worldMatrix = MetaCoreBuildTransformMatrix(gameObject.Transform);
const glm::mat4 worldMatrix = buildWorldMatrix(buildWorldMatrix, gameObject.Id);
const glm::mat4 inverseWorldMatrix = glm::inverse(worldMatrix);
const glm::vec3 localRayOrigin = glm::vec3(inverseWorldMatrix * glm::vec4(rayOrigin, 1.0F));
const glm::vec3 localRayDirection = glm::normalize(glm::vec3(inverseWorldMatrix * glm::vec4(rayDirection, 0.0F)));
@ -410,6 +435,10 @@ void MetaCoreEditorApp::ShutdownImGui() {
}
void MetaCoreEditorApp::DrawEditorFrame() {
static bool gizmoWasUsing = false;
static bool hasGizmoBeforeSnapshot = false;
static MetaCoreEditorStateSnapshot gizmoBeforeSnapshot{};
const ImGuiViewport* mainViewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(mainViewport->WorkPos);
ImGui::SetNextWindowSize(mainViewport->WorkSize);
@ -610,6 +639,10 @@ void MetaCoreEditorApp::DrawEditorFrame() {
gizmoHovering = ImGuizmo::IsOver();
gizmoUsing = ImGuizmo::IsUsing();
if (gizmoUsing && !gizmoWasUsing) {
gizmoBeforeSnapshot = EditorContext_->CaptureStateSnapshot();
hasGizmoBeforeSnapshot = true;
}
if (gizmoUsing) {
// transformMatrix is in Panda space — apply directly.
switch (EditorContext_->GetGizmoOperation()) {
@ -664,15 +697,33 @@ void MetaCoreEditorApp::DrawEditorFrame() {
viewportState,
EditorContext_->GetInput().GetCursorPosition()
);
if (pickedObjectId != 0 && pickedObjectId != EditorContext_->GetSelectedObjectId()) {
EditorContext_->SetSelectedObjectId(pickedObjectId);
const bool ctrlDown = ImGui::GetIO().KeyCtrl;
const bool shiftDown = ImGui::GetIO().KeyShift;
if (pickedObjectId != 0) {
if (shiftDown) {
EditorContext_->SelectRangeByOrderedIds(Scene_.BuildHierarchyPreorder(), pickedObjectId, ctrlDown);
} else if (ctrlDown) {
EditorContext_->ToggleSelection(pickedObjectId);
} else {
EditorContext_->SelectOnly(pickedObjectId);
}
if (const MetaCoreGameObject* selectedObject = EditorContext_->GetSelectedGameObject(); selectedObject != nullptr) {
EditorContext_->AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "视口已选中对象: " + selectedObject->Name);
}
sceneView.SelectedObjectId = pickedObjectId;
sceneView.SelectedObjectId = EditorContext_->GetActiveObjectId();
} else if (!ctrlDown && !shiftDown) {
EditorContext_->ClearSelection();
sceneView.SelectedObjectId = 0;
}
}
if (!gizmoUsing && gizmoWasUsing && hasGizmoBeforeSnapshot) {
const MetaCoreEditorStateSnapshot gizmoAfterSnapshot = EditorContext_->CaptureStateSnapshot();
(void)EditorContext_->CommitStateTransition("修改变换", gizmoBeforeSnapshot, gizmoAfterSnapshot, true);
hasGizmoBeforeSnapshot = false;
}
gizmoWasUsing = gizmoUsing;
ImGui::SetNextWindowPos(centralNode->Pos);
ImGui::SetNextWindowSize(centralNode->Size);
ImGui::SetNextWindowBgAlpha(0.0F);
@ -779,9 +830,13 @@ void MetaCoreEditorApp::DrawEditorFrame() {
ImGui::PopStyleVar(3);
} else {
gizmoWasUsing = false;
hasGizmoBeforeSnapshot = false;
ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{});
}
} else {
gizmoWasUsing = false;
hasGizmoBeforeSnapshot = false;
ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{});
}
}

View File

@ -0,0 +1,107 @@
#include "MetaCoreEditor/MetaCoreEditorCommandService.h"
#include "MetaCoreEditor/MetaCoreEditorContext.h"
#include <utility>
namespace MetaCore {
bool MetaCoreIEditorCommand::CanMergeWith(const MetaCoreIEditorCommand& command) const {
(void)command;
return false;
}
bool MetaCoreIEditorCommand::MergeWith(const MetaCoreIEditorCommand& command) {
(void)command;
return false;
}
bool MetaCoreEditorCommandService::Execute(std::unique_ptr<MetaCoreIEditorCommand> command, MetaCoreEditorContext& editorContext) {
if (command == nullptr || !command->Execute(editorContext)) {
return false;
}
return PushExecuted(std::move(command));
}
bool MetaCoreEditorCommandService::PushExecuted(std::unique_ptr<MetaCoreIEditorCommand> command) {
if (command == nullptr) {
return false;
}
if (!UndoStack_.empty() && UndoStack_.back()->CanMergeWith(*command)) {
if (UndoStack_.back()->MergeWith(*command)) {
RedoStack_.clear();
return true;
}
}
UndoStack_.push_back(std::move(command));
RedoStack_.clear();
return true;
}
bool MetaCoreEditorCommandService::Undo(MetaCoreEditorContext& editorContext) {
if (UndoStack_.empty()) {
return false;
}
std::unique_ptr<MetaCoreIEditorCommand> command = std::move(UndoStack_.back());
UndoStack_.pop_back();
command->Undo(editorContext);
RedoStack_.push_back(std::move(command));
return true;
}
bool MetaCoreEditorCommandService::Redo(MetaCoreEditorContext& editorContext) {
if (RedoStack_.empty()) {
return false;
}
std::unique_ptr<MetaCoreIEditorCommand> command = std::move(RedoStack_.back());
RedoStack_.pop_back();
if (!command->Execute(editorContext)) {
return false;
}
UndoStack_.push_back(std::move(command));
return true;
}
bool MetaCoreEditorCommandService::CanUndo() const {
return !UndoStack_.empty();
}
bool MetaCoreEditorCommandService::CanRedo() const {
return !RedoStack_.empty();
}
std::size_t MetaCoreEditorCommandService::GetUndoCount() const {
return UndoStack_.size();
}
std::size_t MetaCoreEditorCommandService::GetRedoCount() const {
return RedoStack_.size();
}
void MetaCoreEditorCommandService::Clear() {
UndoStack_.clear();
RedoStack_.clear();
}
void MetaCoreEditorCommandService::RegisterFactory(const std::string& commandType, MetaCoreEditorCommandFactory factory) {
if (commandType.empty() || !factory) {
return;
}
Factories_[commandType] = std::move(factory);
}
std::unique_ptr<MetaCoreIEditorCommand> MetaCoreEditorCommandService::CreateCommand(const std::string& commandType) const {
const auto iterator = Factories_.find(commandType);
if (iterator == Factories_.end()) {
return nullptr;
}
return iterator->second();
}
} // namespace MetaCore

View File

@ -4,10 +4,137 @@
#include "MetaCorePlatform/MetaCoreWindow.h"
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
#include "MetaCoreRender/MetaCoreRenderDevice.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include <algorithm>
#include <cmath>
#include <unordered_set>
namespace MetaCore {
namespace {
class MetaCoreSnapshotEditorCommand final : public MetaCoreIEditorCommand {
public:
MetaCoreSnapshotEditorCommand(
std::string label,
MetaCoreEditorStateSnapshot beforeSnapshot,
MetaCoreEditorStateSnapshot afterSnapshot,
bool allowMerge
)
: Label_(std::move(label)),
BeforeSnapshot_(std::move(beforeSnapshot)),
AfterSnapshot_(std::move(afterSnapshot)),
AllowMerge_(allowMerge) {
}
[[nodiscard]] std::string GetLabel() const override { return Label_; }
bool Execute(MetaCoreEditorContext& editorContext) override {
editorContext.RestoreStateSnapshot(AfterSnapshot_);
return true;
}
void Undo(MetaCoreEditorContext& editorContext) override {
editorContext.RestoreStateSnapshot(BeforeSnapshot_);
}
[[nodiscard]] bool CanMergeWith(const MetaCoreIEditorCommand& command) const override {
const auto* snapshotCommand = dynamic_cast<const MetaCoreSnapshotEditorCommand*>(&command);
return AllowMerge_ && snapshotCommand != nullptr && snapshotCommand->AllowMerge_ && snapshotCommand->Label_ == Label_;
}
bool MergeWith(const MetaCoreIEditorCommand& command) override {
const auto* snapshotCommand = dynamic_cast<const MetaCoreSnapshotEditorCommand*>(&command);
if (snapshotCommand == nullptr || snapshotCommand->Label_ != Label_) {
return false;
}
AfterSnapshot_ = snapshotCommand->AfterSnapshot_;
return true;
}
private:
std::string Label_{};
MetaCoreEditorStateSnapshot BeforeSnapshot_{};
MetaCoreEditorStateSnapshot AfterSnapshot_{};
bool AllowMerge_ = false;
};
[[nodiscard]] bool MetaCoreNearlyEqual(float lhs, float rhs) {
return std::abs(lhs - rhs) <= 0.0001F;
}
[[nodiscard]] bool MetaCoreNearlyEqualVec3(const glm::vec3& lhs, const glm::vec3& rhs) {
return MetaCoreNearlyEqual(lhs.x, rhs.x) &&
MetaCoreNearlyEqual(lhs.y, rhs.y) &&
MetaCoreNearlyEqual(lhs.z, rhs.z);
}
[[nodiscard]] bool MetaCoreTransformEqual(const MetaCoreTransformComponent& lhs, const MetaCoreTransformComponent& rhs) {
return MetaCoreNearlyEqualVec3(lhs.Position, rhs.Position) &&
MetaCoreNearlyEqualVec3(lhs.RotationEulerDegrees, rhs.RotationEulerDegrees) &&
MetaCoreNearlyEqualVec3(lhs.Scale, rhs.Scale);
}
[[nodiscard]] bool MetaCoreGameObjectEqual(const MetaCoreGameObject& lhs, const MetaCoreGameObject& rhs) {
if (lhs.Id != rhs.Id || lhs.ParentId != rhs.ParentId || lhs.Name != rhs.Name || !MetaCoreTransformEqual(lhs.Transform, rhs.Transform)) {
return false;
}
if (lhs.Camera.has_value() != rhs.Camera.has_value()) {
return false;
}
if (lhs.Camera.has_value()) {
if (!MetaCoreNearlyEqual(lhs.Camera->FieldOfViewDegrees, rhs.Camera->FieldOfViewDegrees) ||
!MetaCoreNearlyEqual(lhs.Camera->NearClip, rhs.Camera->NearClip) ||
!MetaCoreNearlyEqual(lhs.Camera->FarClip, rhs.Camera->FarClip) ||
lhs.Camera->IsPrimary != rhs.Camera->IsPrimary) {
return false;
}
}
if (lhs.MeshRenderer.has_value() != rhs.MeshRenderer.has_value()) {
return false;
}
if (lhs.MeshRenderer.has_value()) {
if (lhs.MeshRenderer->BuiltinMesh != rhs.MeshRenderer->BuiltinMesh ||
!MetaCoreNearlyEqualVec3(lhs.MeshRenderer->BaseColor, rhs.MeshRenderer->BaseColor) ||
lhs.MeshRenderer->Visible != rhs.MeshRenderer->Visible) {
return false;
}
}
if (lhs.Light.has_value() != rhs.Light.has_value()) {
return false;
}
if (lhs.Light.has_value()) {
if (!MetaCoreNearlyEqualVec3(lhs.Light->Color, rhs.Light->Color) ||
!MetaCoreNearlyEqual(lhs.Light->Intensity, rhs.Light->Intensity)) {
return false;
}
}
return true;
}
[[nodiscard]] bool MetaCoreStateSnapshotEqual(const MetaCoreEditorStateSnapshot& lhs, const MetaCoreEditorStateSnapshot& rhs) {
if (lhs.SceneSnapshot.GameObjects.size() != rhs.SceneSnapshot.GameObjects.size()) {
return false;
}
for (std::size_t index = 0; index < lhs.SceneSnapshot.GameObjects.size(); ++index) {
if (!MetaCoreGameObjectEqual(lhs.SceneSnapshot.GameObjects[index], rhs.SceneSnapshot.GameObjects[index])) {
return false;
}
}
return lhs.SelectionSnapshot.SelectedObjectIds == rhs.SelectionSnapshot.SelectedObjectIds &&
lhs.SelectionSnapshot.ActiveObjectId == rhs.SelectionSnapshot.ActiveObjectId &&
lhs.SelectionSnapshot.SelectionAnchorId == rhs.SelectionSnapshot.SelectionAnchorId;
}
} // namespace
MetaCoreEditorContext::MetaCoreEditorContext(
MetaCoreWindow& window,
MetaCoreRenderDevice& renderDevice,
@ -42,14 +169,227 @@ MetaCoreSceneViewportState& MetaCoreEditorContext::GetSceneViewportState() { ret
const MetaCoreSceneViewportState& MetaCoreEditorContext::GetSceneViewportState() const { return SceneViewportState_; }
MetaCoreEditorCameraController& MetaCoreEditorContext::GetCameraController() { return *CameraController_; }
const MetaCoreEditorCameraController& MetaCoreEditorContext::GetCameraController() const { return *CameraController_; }
MetaCoreId MetaCoreEditorContext::GetSelectedObjectId() const { return SelectedObjectId_; }
void MetaCoreEditorContext::SetSelectedObjectId(MetaCoreId objectId) { SelectedObjectId_ = objectId; }
MetaCoreGameObject* MetaCoreEditorContext::GetSelectedGameObject() { return Scene_.FindGameObject(SelectedObjectId_); }
const MetaCoreGameObject* MetaCoreEditorContext::GetSelectedGameObject() const { return Scene_.FindGameObject(SelectedObjectId_); }
const std::vector<MetaCoreId>& MetaCoreEditorContext::GetSelectedObjectIds() const { return SelectedObjectIds_; }
MetaCoreId MetaCoreEditorContext::GetActiveObjectId() const { return ActiveObjectId_; }
bool MetaCoreEditorContext::IsObjectSelected(MetaCoreId objectId) const {
return std::find(SelectedObjectIds_.begin(), SelectedObjectIds_.end(), objectId) != SelectedObjectIds_.end();
}
void MetaCoreEditorContext::ClearSelection() {
SelectedObjectIds_.clear();
ActiveObjectId_ = 0;
SelectionAnchorId_ = 0;
}
void MetaCoreEditorContext::SelectOnly(MetaCoreId objectId) {
SetSelection(objectId == 0 ? std::vector<MetaCoreId>{} : std::vector<MetaCoreId>{objectId}, objectId);
}
void MetaCoreEditorContext::ToggleSelection(MetaCoreId objectId) {
if (objectId == 0 || Scene_.FindGameObject(objectId) == nullptr) {
return;
}
auto iterator = std::find(SelectedObjectIds_.begin(), SelectedObjectIds_.end(), objectId);
if (iterator == SelectedObjectIds_.end()) {
SelectedObjectIds_.push_back(objectId);
ActiveObjectId_ = objectId;
} else {
SelectedObjectIds_.erase(iterator);
if (ActiveObjectId_ == objectId) {
ActiveObjectId_ = SelectedObjectIds_.empty() ? 0 : SelectedObjectIds_.back();
}
}
SelectionAnchorId_ = ActiveObjectId_ != 0 ? ActiveObjectId_ : SelectionAnchorId_;
NormalizeSelection();
}
void MetaCoreEditorContext::AddToSelection(MetaCoreId objectId, bool makeActive) {
if (objectId == 0 || Scene_.FindGameObject(objectId) == nullptr) {
return;
}
if (!IsObjectSelected(objectId)) {
SelectedObjectIds_.push_back(objectId);
}
if (makeActive) {
ActiveObjectId_ = objectId;
SelectionAnchorId_ = objectId;
}
NormalizeSelection();
}
void MetaCoreEditorContext::SetSelection(const std::vector<MetaCoreId>& objectIds, MetaCoreId activeObjectId) {
SelectedObjectIds_.clear();
SelectedObjectIds_.reserve(objectIds.size());
std::unordered_set<MetaCoreId> deduplicatedIds;
for (MetaCoreId objectId : objectIds) {
if (objectId == 0 || Scene_.FindGameObject(objectId) == nullptr || deduplicatedIds.contains(objectId)) {
continue;
}
deduplicatedIds.insert(objectId);
SelectedObjectIds_.push_back(objectId);
}
if (activeObjectId != 0 && deduplicatedIds.contains(activeObjectId)) {
ActiveObjectId_ = activeObjectId;
} else {
ActiveObjectId_ = SelectedObjectIds_.empty() ? 0 : SelectedObjectIds_.back();
}
SelectionAnchorId_ = ActiveObjectId_;
NormalizeSelection();
}
MetaCoreId MetaCoreEditorContext::GetSelectionAnchorId() const { return SelectionAnchorId_; }
void MetaCoreEditorContext::SetSelectionAnchorId(MetaCoreId objectId) {
SelectionAnchorId_ = (objectId != 0 && Scene_.FindGameObject(objectId) != nullptr) ? objectId : 0;
}
void MetaCoreEditorContext::SelectRangeByOrderedIds(const std::vector<MetaCoreId>& orderedIds, MetaCoreId endId, bool additive) {
if (endId == 0 || Scene_.FindGameObject(endId) == nullptr) {
return;
}
const MetaCoreId startId = SelectionAnchorId_ != 0 ? SelectionAnchorId_ : (ActiveObjectId_ != 0 ? ActiveObjectId_ : endId);
auto startIterator = std::find(orderedIds.begin(), orderedIds.end(), startId);
auto endIterator = std::find(orderedIds.begin(), orderedIds.end(), endId);
if (startIterator == orderedIds.end() || endIterator == orderedIds.end()) {
SelectOnly(endId);
return;
}
const std::size_t startIndex = static_cast<std::size_t>(std::distance(orderedIds.begin(), startIterator));
const std::size_t endIndex = static_cast<std::size_t>(std::distance(orderedIds.begin(), endIterator));
const std::size_t minIndex = std::min(startIndex, endIndex);
const std::size_t maxIndex = std::max(startIndex, endIndex);
std::unordered_set<MetaCoreId> resultingIds;
if (additive) {
for (MetaCoreId objectId : SelectedObjectIds_) {
resultingIds.insert(objectId);
}
}
for (std::size_t index = minIndex; index <= maxIndex; ++index) {
resultingIds.insert(orderedIds[index]);
}
std::vector<MetaCoreId> orderedSelection;
orderedSelection.reserve(resultingIds.size());
for (MetaCoreId objectId : orderedIds) {
if (resultingIds.contains(objectId)) {
orderedSelection.push_back(objectId);
}
}
SetSelection(orderedSelection, endId);
SelectionAnchorId_ = startId;
}
MetaCoreId MetaCoreEditorContext::GetSelectedObjectId() const { return ActiveObjectId_; }
void MetaCoreEditorContext::SetSelectedObjectId(MetaCoreId objectId) { SelectOnly(objectId); }
MetaCoreGameObject* MetaCoreEditorContext::GetSelectedGameObject() { return Scene_.FindGameObject(ActiveObjectId_); }
const MetaCoreGameObject* MetaCoreEditorContext::GetSelectedGameObject() const { return Scene_.FindGameObject(ActiveObjectId_); }
MetaCoreGizmoOperation MetaCoreEditorContext::GetGizmoOperation() const { return GizmoOperation_; }
void MetaCoreEditorContext::SetGizmoOperation(MetaCoreGizmoOperation operation) { GizmoOperation_ = operation; }
MetaCoreGizmoMode MetaCoreEditorContext::GetGizmoMode() const { return GizmoMode_; }
void MetaCoreEditorContext::SetGizmoMode(MetaCoreGizmoMode mode) { GizmoMode_ = mode; }
MetaCoreReparentTransformRule MetaCoreEditorContext::GetReparentTransformRule() const { return ReparentTransformRule_; }
void MetaCoreEditorContext::SetReparentTransformRule(MetaCoreReparentTransformRule rule) { ReparentTransformRule_ = rule; }
MetaCoreEditorCommandService& MetaCoreEditorContext::GetCommandService() { return CommandService_; }
const MetaCoreEditorCommandService& MetaCoreEditorContext::GetCommandService() const { return CommandService_; }
bool MetaCoreEditorContext::ExecuteCommand(std::unique_ptr<MetaCoreIEditorCommand> command) {
if (!CommandService_.Execute(std::move(command), *this)) {
return false;
}
NormalizeSelection();
return true;
}
bool MetaCoreEditorContext::UndoCommand() {
if (!CommandService_.Undo(*this)) {
return false;
}
PendingRenameObjectId_ = 0;
NormalizeSelection();
return true;
}
bool MetaCoreEditorContext::RedoCommand() {
if (!CommandService_.Redo(*this)) {
return false;
}
PendingRenameObjectId_ = 0;
NormalizeSelection();
return true;
}
MetaCoreEditorStateSnapshot MetaCoreEditorContext::CaptureStateSnapshot() const {
MetaCoreEditorStateSnapshot snapshot;
snapshot.SceneSnapshot = Scene_.CaptureSnapshot();
snapshot.SelectionSnapshot.SelectedObjectIds = SelectedObjectIds_;
snapshot.SelectionSnapshot.ActiveObjectId = ActiveObjectId_;
snapshot.SelectionSnapshot.SelectionAnchorId = SelectionAnchorId_;
return snapshot;
}
void MetaCoreEditorContext::RestoreStateSnapshot(const MetaCoreEditorStateSnapshot& snapshot) {
Scene_.RestoreSnapshot(snapshot.SceneSnapshot);
SetSelection(snapshot.SelectionSnapshot.SelectedObjectIds, snapshot.SelectionSnapshot.ActiveObjectId);
if (snapshot.SelectionSnapshot.SelectionAnchorId != 0 &&
Scene_.FindGameObject(snapshot.SelectionSnapshot.SelectionAnchorId) != nullptr) {
SelectionAnchorId_ = snapshot.SelectionSnapshot.SelectionAnchorId;
}
NormalizeSelection();
}
bool MetaCoreEditorContext::CommitStateTransition(
const std::string& label,
const MetaCoreEditorStateSnapshot& beforeSnapshot,
const MetaCoreEditorStateSnapshot& afterSnapshot,
bool allowMerge
) {
if (MetaCoreStateSnapshotEqual(beforeSnapshot, afterSnapshot)) {
return false;
}
if (!CommandService_.PushExecuted(std::make_unique<MetaCoreSnapshotEditorCommand>(label, beforeSnapshot, afterSnapshot, allowMerge))) {
return false;
}
NormalizeSelection();
return true;
}
bool MetaCoreEditorContext::ExecuteSnapshotCommand(const std::string& label, const std::function<bool()>& mutator) {
const MetaCoreEditorStateSnapshot beforeSnapshot = CaptureStateSnapshot();
if (!mutator()) {
return false;
}
const MetaCoreEditorStateSnapshot afterSnapshot = CaptureStateSnapshot();
return CommitStateTransition(label, beforeSnapshot, afterSnapshot);
}
void MetaCoreEditorContext::RequestRenameActiveObject() {
PendingRenameObjectId_ = ActiveObjectId_;
}
MetaCoreId MetaCoreEditorContext::ConsumeRenameRequestObjectId() {
const MetaCoreId objectId = PendingRenameObjectId_;
PendingRenameObjectId_ = 0;
return objectId;
}
void MetaCoreEditorContext::AddConsoleMessage(MetaCoreLogLevel level, const std::string& category, const std::string& message) {
LogService_.AddEntry(level, category, message);
@ -58,4 +398,26 @@ void MetaCoreEditorContext::AddConsoleMessage(MetaCoreLogLevel level, const std:
void MetaCoreEditorContext::SetDockLayoutBuilt(bool built) { DockLayoutBuilt_ = built; }
bool MetaCoreEditorContext::HasDockLayoutBuilt() const { return DockLayoutBuilt_; }
void MetaCoreEditorContext::NormalizeSelection() {
std::vector<MetaCoreId> sanitizedSelection;
sanitizedSelection.reserve(SelectedObjectIds_.size());
std::unordered_set<MetaCoreId> deduplicatedIds;
for (MetaCoreId objectId : SelectedObjectIds_) {
if (objectId == 0 || Scene_.FindGameObject(objectId) == nullptr || deduplicatedIds.contains(objectId)) {
continue;
}
deduplicatedIds.insert(objectId);
sanitizedSelection.push_back(objectId);
}
SelectedObjectIds_ = std::move(sanitizedSelection);
if (ActiveObjectId_ == 0 || !deduplicatedIds.contains(ActiveObjectId_)) {
ActiveObjectId_ = SelectedObjectIds_.empty() ? 0 : SelectedObjectIds_.back();
}
if (SelectionAnchorId_ == 0 || !deduplicatedIds.contains(SelectionAnchorId_)) {
SelectionAnchorId_ = ActiveObjectId_;
}
}
} // namespace MetaCore

View File

@ -0,0 +1,49 @@
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace MetaCore {
class MetaCoreEditorContext;
class MetaCoreIEditorCommand {
public:
virtual ~MetaCoreIEditorCommand() = default;
[[nodiscard]] virtual std::string GetLabel() const = 0;
virtual bool Execute(MetaCoreEditorContext& editorContext) = 0;
virtual void Undo(MetaCoreEditorContext& editorContext) = 0;
[[nodiscard]] virtual bool CanMergeWith(const MetaCoreIEditorCommand& command) const;
virtual bool MergeWith(const MetaCoreIEditorCommand& command);
};
class MetaCoreEditorCommandService {
public:
using MetaCoreEditorCommandFactory = std::function<std::unique_ptr<MetaCoreIEditorCommand>()>;
bool Execute(std::unique_ptr<MetaCoreIEditorCommand> command, MetaCoreEditorContext& editorContext);
bool PushExecuted(std::unique_ptr<MetaCoreIEditorCommand> command);
bool Undo(MetaCoreEditorContext& editorContext);
bool Redo(MetaCoreEditorContext& editorContext);
[[nodiscard]] bool CanUndo() const;
[[nodiscard]] bool CanRedo() const;
[[nodiscard]] std::size_t GetUndoCount() const;
[[nodiscard]] std::size_t GetRedoCount() const;
void Clear();
void RegisterFactory(const std::string& commandType, MetaCoreEditorCommandFactory factory);
[[nodiscard]] std::unique_ptr<MetaCoreIEditorCommand> CreateCommand(const std::string& commandType) const;
private:
std::vector<std::unique_ptr<MetaCoreIEditorCommand>> UndoStack_{};
std::vector<std::unique_ptr<MetaCoreIEditorCommand>> RedoStack_{};
std::unordered_map<std::string, MetaCoreEditorCommandFactory> Factories_{};
};
} // namespace MetaCore

View File

@ -2,9 +2,13 @@
#include "MetaCoreFoundation/MetaCoreId.h"
#include "MetaCoreFoundation/MetaCoreLogService.h"
#include "MetaCoreEditor/MetaCoreEditorCommandService.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace MetaCore {
@ -12,7 +16,6 @@ class MetaCoreWindow;
class MetaCoreInput;
class MetaCoreRenderDevice;
class MetaCoreEditorViewportRenderer;
class MetaCoreScene;
struct MetaCoreGameObject;
class MetaCoreEditorModuleRegistry;
class MetaCoreEditorCameraController;
@ -29,6 +32,22 @@ enum class MetaCoreGizmoMode {
World
};
enum class MetaCoreReparentTransformRule {
KeepWorld = 0,
KeepLocal
};
struct MetaCoreEditorSelectionSnapshot {
std::vector<MetaCoreId> SelectedObjectIds;
MetaCoreId ActiveObjectId = 0;
MetaCoreId SelectionAnchorId = 0;
};
struct MetaCoreEditorStateSnapshot {
MetaCoreSceneSnapshot SceneSnapshot{};
MetaCoreEditorSelectionSnapshot SelectionSnapshot{};
};
struct MetaCoreSceneViewportState {
float Left = 0.0F;
float Top = 0.0F;
@ -65,6 +84,17 @@ public:
[[nodiscard]] const MetaCoreSceneViewportState& GetSceneViewportState() const;
[[nodiscard]] MetaCoreEditorCameraController& GetCameraController();
[[nodiscard]] const MetaCoreEditorCameraController& GetCameraController() const;
[[nodiscard]] const std::vector<MetaCoreId>& GetSelectedObjectIds() const;
[[nodiscard]] MetaCoreId GetActiveObjectId() const;
[[nodiscard]] bool IsObjectSelected(MetaCoreId objectId) const;
void ClearSelection();
void SelectOnly(MetaCoreId objectId);
void ToggleSelection(MetaCoreId objectId);
void AddToSelection(MetaCoreId objectId, bool makeActive);
void SetSelection(const std::vector<MetaCoreId>& objectIds, MetaCoreId activeObjectId = 0);
[[nodiscard]] MetaCoreId GetSelectionAnchorId() const;
void SetSelectionAnchorId(MetaCoreId objectId);
void SelectRangeByOrderedIds(const std::vector<MetaCoreId>& orderedIds, MetaCoreId endId, bool additive);
[[nodiscard]] MetaCoreId GetSelectedObjectId() const;
void SetSelectedObjectId(MetaCoreId objectId);
[[nodiscard]] MetaCoreGameObject* GetSelectedGameObject();
@ -73,11 +103,31 @@ public:
void SetGizmoOperation(MetaCoreGizmoOperation operation);
[[nodiscard]] MetaCoreGizmoMode GetGizmoMode() const;
void SetGizmoMode(MetaCoreGizmoMode mode);
[[nodiscard]] MetaCoreReparentTransformRule GetReparentTransformRule() const;
void SetReparentTransformRule(MetaCoreReparentTransformRule rule);
[[nodiscard]] MetaCoreEditorCommandService& GetCommandService();
[[nodiscard]] const MetaCoreEditorCommandService& GetCommandService() const;
bool ExecuteCommand(std::unique_ptr<MetaCoreIEditorCommand> command);
bool UndoCommand();
bool RedoCommand();
bool ExecuteSnapshotCommand(const std::string& label, const std::function<bool()>& mutator);
[[nodiscard]] MetaCoreEditorStateSnapshot CaptureStateSnapshot() const;
void RestoreStateSnapshot(const MetaCoreEditorStateSnapshot& snapshot);
bool CommitStateTransition(
const std::string& label,
const MetaCoreEditorStateSnapshot& beforeSnapshot,
const MetaCoreEditorStateSnapshot& afterSnapshot,
bool allowMerge = false
);
void RequestRenameActiveObject();
[[nodiscard]] MetaCoreId ConsumeRenameRequestObjectId();
void AddConsoleMessage(MetaCoreLogLevel level, const std::string& category, const std::string& message);
void SetDockLayoutBuilt(bool built);
[[nodiscard]] bool HasDockLayoutBuilt() const;
private:
void NormalizeSelection();
MetaCoreWindow& Window_;
MetaCoreRenderDevice& RenderDevice_;
MetaCoreEditorViewportRenderer& ViewportRenderer_;
@ -86,9 +136,14 @@ private:
MetaCoreEditorModuleRegistry& ModuleRegistry_;
std::unique_ptr<MetaCoreEditorCameraController> CameraController_;
MetaCoreSceneViewportState SceneViewportState_{};
MetaCoreId SelectedObjectId_ = 0;
std::vector<MetaCoreId> SelectedObjectIds_{};
MetaCoreId ActiveObjectId_ = 0;
MetaCoreId SelectionAnchorId_ = 0;
MetaCoreGizmoOperation GizmoOperation_ = MetaCoreGizmoOperation::Translate;
MetaCoreGizmoMode GizmoMode_ = MetaCoreGizmoMode::Local;
MetaCoreReparentTransformRule ReparentTransformRule_ = MetaCoreReparentTransformRule::KeepWorld;
MetaCoreEditorCommandService CommandService_{};
MetaCoreId PendingRenameObjectId_ = 0;
bool DockLayoutBuilt_ = false;
};

View File

@ -14,4 +14,11 @@ MetaCoreId MetaCoreIdGenerator::Generate() {
return GMetaCoreNextId.fetch_add(1, std::memory_order_relaxed);
}
void MetaCoreIdGenerator::EnsureAbove(MetaCoreId value) {
std::uint64_t desired = value + 1;
std::uint64_t current = GMetaCoreNextId.load(std::memory_order_relaxed);
while (current < desired && !GMetaCoreNextId.compare_exchange_weak(current, desired, std::memory_order_relaxed)) {
}
}
} // namespace MetaCore

View File

@ -12,6 +12,9 @@ class MetaCoreIdGenerator {
public:
// 生成一个新的全局唯一标识。
static MetaCoreId Generate();
// 确保后续生成的标识严格大于给定值。
static void EnsureAbove(MetaCoreId value);
};
} // namespace MetaCore

View File

@ -2,8 +2,45 @@
#include "MetaCoreFoundation/MetaCoreId.h"
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/ext/matrix_transform.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <glm/gtx/matrix_decompose.hpp>
#include <algorithm>
#include <optional>
#include <unordered_map>
#include <unordered_set>
namespace MetaCore {
namespace {
glm::mat4 MetaCoreBuildTransformMatrix(const MetaCoreTransformComponent& transform) {
const glm::mat4 translation = glm::translate(glm::mat4(1.0F), transform.Position);
const glm::mat4 rotation = glm::yawPitchRoll(
glm::radians(transform.RotationEulerDegrees.y),
glm::radians(transform.RotationEulerDegrees.x),
glm::radians(transform.RotationEulerDegrees.z)
);
const glm::mat4 scale = glm::scale(glm::mat4(1.0F), transform.Scale);
return translation * rotation * scale;
}
void MetaCoreApplyMatrixToTransform(const glm::mat4& matrix, MetaCoreTransformComponent& transform) {
glm::vec3 skew{};
glm::vec4 perspective{};
glm::quat rotation{};
glm::decompose(matrix, transform.Scale, rotation, transform.Position, skew, perspective);
transform.RotationEulerDegrees = glm::degrees(glm::eulerAngles(rotation));
}
std::string MetaCoreBuildCopyName(const std::string& name) {
return name + " Copy";
}
} // namespace
MetaCoreGameObject& MetaCoreScene::CreateGameObject(const std::string& name, MetaCoreId parentId) {
GameObjects_.push_back(MetaCoreGameObject{
MetaCoreIdGenerator::Generate(),
@ -63,6 +100,296 @@ std::vector<MetaCoreId> MetaCoreScene::GetChildrenOf(MetaCoreId parentId) const
return childIds;
}
void MetaCoreScene::CollectPreorder(MetaCoreId objectId, std::vector<MetaCoreId>& output) const {
if (FindGameObject(objectId) == nullptr) {
return;
}
output.push_back(objectId);
for (MetaCoreId childId : GetChildrenOf(objectId)) {
CollectPreorder(childId, output);
}
}
std::vector<MetaCoreId> MetaCoreScene::BuildHierarchyPreorder() const {
std::vector<MetaCoreId> orderedIds;
for (MetaCoreId rootId : GetRootObjectIds()) {
CollectPreorder(rootId, orderedIds);
}
return orderedIds;
}
std::vector<MetaCoreId> MetaCoreScene::GetSubtreeObjectIds(MetaCoreId rootId) const {
std::vector<MetaCoreId> subtreeIds;
CollectPreorder(rootId, subtreeIds);
return subtreeIds;
}
bool MetaCoreScene::IsDescendantOf(MetaCoreId objectId, MetaCoreId ancestorId) const {
if (objectId == 0 || ancestorId == 0 || objectId == ancestorId) {
return false;
}
const MetaCoreGameObject* currentObject = FindGameObject(objectId);
while (currentObject != nullptr && currentObject->ParentId != 0) {
if (currentObject->ParentId == ancestorId) {
return true;
}
currentObject = FindGameObject(currentObject->ParentId);
}
return false;
}
bool MetaCoreScene::RenameGameObject(MetaCoreId objectId, const std::string& name) {
MetaCoreGameObject* object = FindGameObject(objectId);
if (object == nullptr || name.empty()) {
return false;
}
object->Name = name;
return true;
}
std::vector<MetaCoreId> MetaCoreScene::DeleteGameObjects(const std::vector<MetaCoreId>& objectIds) {
std::unordered_set<MetaCoreId> selectedIds;
for (MetaCoreId objectId : objectIds) {
if (FindGameObject(objectId) != nullptr) {
selectedIds.insert(objectId);
}
}
if (selectedIds.empty()) {
return {};
}
std::vector<MetaCoreId> rootIds;
rootIds.reserve(selectedIds.size());
for (MetaCoreId objectId : selectedIds) {
const MetaCoreGameObject* object = FindGameObject(objectId);
bool hasSelectedAncestor = false;
while (object != nullptr && object->ParentId != 0) {
if (selectedIds.contains(object->ParentId)) {
hasSelectedAncestor = true;
break;
}
object = FindGameObject(object->ParentId);
}
if (!hasSelectedAncestor) {
rootIds.push_back(objectId);
}
}
std::unordered_set<MetaCoreId> idsToDelete;
for (MetaCoreId rootId : rootIds) {
for (MetaCoreId subtreeId : GetSubtreeObjectIds(rootId)) {
idsToDelete.insert(subtreeId);
}
}
std::vector<MetaCoreId> deletedIds;
deletedIds.reserve(idsToDelete.size());
std::erase_if(GameObjects_, [&](const MetaCoreGameObject& object) {
if (!idsToDelete.contains(object.Id)) {
return false;
}
deletedIds.push_back(object.Id);
return true;
});
return deletedIds;
}
std::vector<MetaCoreId> MetaCoreScene::DuplicateGameObjects(const std::vector<MetaCoreId>& objectIds) {
std::unordered_set<MetaCoreId> selectedIds;
for (MetaCoreId objectId : objectIds) {
if (FindGameObject(objectId) != nullptr) {
selectedIds.insert(objectId);
}
}
if (selectedIds.empty()) {
return {};
}
std::vector<MetaCoreId> orderedIds = BuildHierarchyPreorder();
std::vector<MetaCoreId> rootIds;
for (MetaCoreId objectId : orderedIds) {
if (!selectedIds.contains(objectId)) {
continue;
}
const MetaCoreGameObject* object = FindGameObject(objectId);
bool hasSelectedAncestor = false;
while (object != nullptr && object->ParentId != 0) {
if (selectedIds.contains(object->ParentId)) {
hasSelectedAncestor = true;
break;
}
object = FindGameObject(object->ParentId);
}
if (!hasSelectedAncestor) {
rootIds.push_back(objectId);
}
}
std::vector<MetaCoreGameObject> clonedObjects;
std::vector<MetaCoreId> duplicatedRootIds;
const auto cloneSubtree = [&](const auto& self, MetaCoreId sourceId, MetaCoreId duplicatedParentId, bool isRoot) -> void {
const MetaCoreGameObject* sourceObject = FindGameObject(sourceId);
if (sourceObject == nullptr) {
return;
}
MetaCoreGameObject clonedObject = *sourceObject;
clonedObject.Id = MetaCoreIdGenerator::Generate();
clonedObject.ParentId = duplicatedParentId;
if (isRoot) {
clonedObject.Name = MetaCoreBuildCopyName(sourceObject->Name);
duplicatedRootIds.push_back(clonedObject.Id);
}
clonedObjects.push_back(std::move(clonedObject));
const MetaCoreId newObjectId = clonedObjects.back().Id;
for (MetaCoreId childId : GetChildrenOf(sourceId)) {
self(self, childId, newObjectId, false);
}
};
for (MetaCoreId rootId : rootIds) {
const MetaCoreGameObject* rootObject = FindGameObject(rootId);
if (rootObject == nullptr) {
continue;
}
cloneSubtree(cloneSubtree, rootId, rootObject->ParentId, true);
}
GameObjects_.insert(GameObjects_.end(), clonedObjects.begin(), clonedObjects.end());
return duplicatedRootIds;
}
bool MetaCoreScene::ReparentGameObjects(const std::vector<MetaCoreId>& objectIds, MetaCoreId newParentId, bool keepWorldTransform) {
if (newParentId != 0 && FindGameObject(newParentId) == nullptr) {
return false;
}
std::unordered_set<MetaCoreId> selectedIds;
for (MetaCoreId objectId : objectIds) {
if (FindGameObject(objectId) != nullptr) {
selectedIds.insert(objectId);
}
}
if (selectedIds.empty()) {
return false;
}
std::vector<MetaCoreId> orderedIds = BuildHierarchyPreorder();
std::vector<MetaCoreId> rootIds;
for (MetaCoreId objectId : orderedIds) {
if (!selectedIds.contains(objectId)) {
continue;
}
const MetaCoreGameObject* object = FindGameObject(objectId);
bool hasSelectedAncestor = false;
while (object != nullptr && object->ParentId != 0) {
if (selectedIds.contains(object->ParentId)) {
hasSelectedAncestor = true;
break;
}
object = FindGameObject(object->ParentId);
}
if (!hasSelectedAncestor) {
rootIds.push_back(objectId);
}
}
if (rootIds.empty()) {
return false;
}
for (MetaCoreId objectId : rootIds) {
if (objectId == newParentId || IsDescendantOf(newParentId, objectId)) {
return false;
}
}
std::unordered_map<MetaCoreId, glm::mat4> worldMatrices;
if (keepWorldTransform) {
const auto buildWorldMatrix = [&](const auto& self, MetaCoreId objectId) -> glm::mat4 {
const MetaCoreGameObject* object = FindGameObject(objectId);
if (object == nullptr) {
return glm::mat4(1.0F);
}
const glm::mat4 localMatrix = MetaCoreBuildTransformMatrix(object->Transform);
if (object->ParentId == 0) {
return localMatrix;
}
return self(self, object->ParentId) * localMatrix;
};
for (MetaCoreId rootId : rootIds) {
worldMatrices.emplace(rootId, buildWorldMatrix(buildWorldMatrix, rootId));
}
if (newParentId != 0) {
worldMatrices.emplace(newParentId, buildWorldMatrix(buildWorldMatrix, newParentId));
}
}
for (MetaCoreId rootId : rootIds) {
MetaCoreGameObject* object = FindGameObject(rootId);
if (object != nullptr) {
object->ParentId = newParentId;
}
}
if (keepWorldTransform) {
const glm::mat4 newParentWorldMatrix =
newParentId == 0 ? glm::mat4(1.0F) : worldMatrices[newParentId];
const glm::mat4 inverseParentWorldMatrix = glm::inverse(newParentWorldMatrix);
for (MetaCoreId rootId : rootIds) {
MetaCoreGameObject* object = FindGameObject(rootId);
if (object == nullptr) {
continue;
}
const auto matrixIterator = worldMatrices.find(rootId);
if (matrixIterator == worldMatrices.end()) {
continue;
}
const glm::mat4 newLocalMatrix = inverseParentWorldMatrix * matrixIterator->second;
MetaCoreApplyMatrixToTransform(newLocalMatrix, object->Transform);
}
}
return true;
}
MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const {
MetaCoreSceneSnapshot snapshot;
snapshot.GameObjects = GameObjects_;
return snapshot;
}
void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) {
GameObjects_ = snapshot.GameObjects;
MetaCoreId maxId = 0;
for (const MetaCoreGameObject& object : GameObjects_) {
maxId = std::max(maxId, object.Id);
}
MetaCoreIdGenerator::EnsureAbove(maxId);
}
MetaCoreScene MetaCoreCreateDefaultScene() {
MetaCoreScene scene;

View File

@ -6,6 +6,10 @@
namespace MetaCore {
struct MetaCoreSceneSnapshot {
std::vector<MetaCoreGameObject> GameObjects;
};
// 负责存储和查询场景中的全部对象。
class MetaCoreScene {
public:
@ -26,7 +30,34 @@ public:
// 返回某个对象的直接子对象列表。
[[nodiscard]] std::vector<MetaCoreId> GetChildrenOf(MetaCoreId parentId) const;
// 按层级前序遍历顺序返回对象标识列表。
[[nodiscard]] std::vector<MetaCoreId> BuildHierarchyPreorder() const;
// 返回指定根对象的完整子树标识(含自身)。
[[nodiscard]] std::vector<MetaCoreId> GetSubtreeObjectIds(MetaCoreId rootId) const;
// 判断对象是否是另一个对象的后代。
[[nodiscard]] bool IsDescendantOf(MetaCoreId objectId, MetaCoreId ancestorId) const;
// 重命名对象。
bool RenameGameObject(MetaCoreId objectId, const std::string& name);
// 删除对象(包含其子树)。返回实际删除的全部对象标识。
[[nodiscard]] std::vector<MetaCoreId> DeleteGameObjects(const std::vector<MetaCoreId>& objectIds);
// 复制对象(包含其子树)。返回复制后顶层对象标识。
[[nodiscard]] std::vector<MetaCoreId> DuplicateGameObjects(const std::vector<MetaCoreId>& objectIds);
// 批量重挂接对象到新父节点。
bool ReparentGameObjects(const std::vector<MetaCoreId>& objectIds, MetaCoreId newParentId, bool keepWorldTransform);
// 场景快照捕获与恢复。
[[nodiscard]] MetaCoreSceneSnapshot CaptureSnapshot() const;
void RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot);
private:
void CollectPreorder(MetaCoreId objectId, std::vector<MetaCoreId>& output) const;
std::vector<MetaCoreGameObject> GameObjects_{};
};

View File

@ -1,5 +1,9 @@
#include "MetaCoreEditor/MetaCoreEditorContext.h"
#include "MetaCoreEditor/MetaCoreEditorModule.h"
#include "MetaCoreFoundation/MetaCoreLogService.h"
#include "MetaCorePlatform/MetaCoreWindow.h"
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
#include "MetaCoreRender/MetaCoreRenderDevice.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include <cstdlib>
@ -22,6 +26,103 @@ public:
void DrawPanel(MetaCore::MetaCoreEditorContext&) override {}
};
void MetaCoreTestSceneEditApi() {
MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene();
const std::size_t originalCount = scene.GetGameObjects().size();
const auto roots = scene.GetRootObjectIds();
MetaCoreExpect(!roots.empty(), "默认场景应有根对象");
const std::vector<MetaCore::MetaCoreId> duplicateRoots = scene.DuplicateGameObjects({roots.front()});
MetaCoreExpect(!duplicateRoots.empty(), "复制应返回新根对象");
MetaCoreExpect(scene.GetGameObjects().size() > originalCount, "复制后对象数量应增加");
const MetaCore::MetaCoreId duplicatedRootId = duplicateRoots.front();
MetaCoreExpect(scene.FindGameObject(duplicatedRootId) != nullptr, "复制后的对象应可被查找到");
const bool reparented = scene.ReparentGameObjects({duplicatedRootId}, 0, true);
MetaCoreExpect(reparented, "重挂接操作应成功");
const std::vector<MetaCore::MetaCoreId> deletedIds = scene.DeleteGameObjects({duplicatedRootId});
MetaCoreExpect(!deletedIds.empty(), "删除应返回被删除对象");
MetaCoreExpect(scene.FindGameObject(duplicatedRootId) == nullptr, "删除后对象不应存在");
}
void MetaCoreTestSelectionStateMachine() {
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
const MetaCore::MetaCoreId objectAId = scene.CreateGameObject("A").Id;
const MetaCore::MetaCoreId objectBId = scene.CreateGameObject("B").Id;
const MetaCore::MetaCoreId objectCId = scene.CreateGameObject("C").Id;
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
editorContext.SelectOnly(objectAId);
MetaCoreExpect(editorContext.GetActiveObjectId() == objectAId, "单选后 Active 应为 A");
MetaCoreExpect(editorContext.GetSelectedObjectIds().size() == 1, "单选后应仅包含一个对象");
editorContext.ToggleSelection(objectBId);
MetaCoreExpect(editorContext.IsObjectSelected(objectAId), "Toggle 后应保留已选对象 A");
MetaCoreExpect(editorContext.IsObjectSelected(objectBId), "Toggle 后应包含对象 B");
MetaCoreExpect(editorContext.GetActiveObjectId() == objectBId, "Toggle 新增对象后 Active 应为 B");
editorContext.SetSelectionAnchorId(objectAId);
editorContext.SelectRangeByOrderedIds(scene.BuildHierarchyPreorder(), objectCId, false);
MetaCoreExpect(editorContext.IsObjectSelected(objectAId), "范围选择应包含起点 A");
MetaCoreExpect(editorContext.IsObjectSelected(objectBId), "范围选择应包含中间对象 B");
MetaCoreExpect(editorContext.IsObjectSelected(objectCId), "范围选择应包含终点 C");
MetaCoreExpect(editorContext.GetActiveObjectId() == objectCId, "范围选择后 Active 应为终点 C");
}
void MetaCoreTestUndoRedo() {
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene();
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const std::size_t beforeCreateCount = scene.GetGameObjects().size();
const bool created = editorContext.ExecuteSnapshotCommand("创建对象", [&]() {
MetaCore::MetaCoreGameObject& object = scene.CreateGameObject("UndoRedoObject");
editorContext.SelectOnly(object.Id);
return true;
});
MetaCoreExpect(created, "创建快照命令应执行成功");
MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount + 1, "执行命令后对象数量应增加");
MetaCoreExpect(editorContext.GetCommandService().CanUndo(), "执行命令后应可撤销");
const bool undoSucceeded = editorContext.UndoCommand();
MetaCoreExpect(undoSucceeded, "Undo 应成功");
MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount, "Undo 后对象数量应恢复");
MetaCoreExpect(editorContext.GetCommandService().CanRedo(), "Undo 后应可重做");
const bool redoSucceeded = editorContext.RedoCommand();
MetaCoreExpect(redoSucceeded, "Redo 应成功");
MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount + 1, "Redo 后对象数量应再次增加");
}
} // namespace
int main() {
@ -49,6 +150,10 @@ int main() {
logService.AddEntry(MetaCore::MetaCoreLogLevel::Info, "Test", "Smoke");
MetaCoreExpect(logService.GetEntries().size() == 1, "日志服务应记录一条日志");
MetaCoreTestSceneEditApi();
MetaCoreTestSelectionStateMachine();
MetaCoreTestUndoRedo();
std::cout << "MetaCoreSmokeTests passed\n";
return 0;
}