Implement Unity-like editor interactions with undo and multiselect
This commit is contained in:
parent
ea6291fba3
commit
3409fec66b
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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{});
|
||||
}
|
||||
}
|
||||
|
||||
107
Source/MetaCoreEditor/Private/MetaCoreEditorCommandService.cpp
Normal file
107
Source/MetaCoreEditor/Private/MetaCoreEditorCommandService.cpp
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -12,6 +12,9 @@ class MetaCoreIdGenerator {
|
||||
public:
|
||||
// 生成一个新的全局唯一标识。
|
||||
static MetaCoreId Generate();
|
||||
|
||||
// 确保后续生成的标识严格大于给定值。
|
||||
static void EnsureAbove(MetaCoreId value);
|
||||
};
|
||||
|
||||
} // namespace MetaCore
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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_{};
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user