Extract scene interaction logic into a dedicated service

This commit is contained in:
ayuan9957 2026-03-25 11:19:27 +08:00
parent 3409fec66b
commit 2ed3737c90
7 changed files with 294 additions and 169 deletions

View File

@ -156,6 +156,7 @@ set(METACORE_EDITOR_HEADERS
Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorCommandService.h
Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h
Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorModule.h
Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h
)
set(METACORE_EDITOR_PRIVATE_HEADERS
@ -171,6 +172,7 @@ set(METACORE_EDITOR_SOURCES
Source/MetaCoreEditor/Private/MetaCoreEditorCommandService.cpp
Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp
Source/MetaCoreEditor/Private/MetaCoreEditorModule.cpp
Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp
third_party/ImGuizmo/ImGuizmo.cpp
)

View File

@ -0,0 +1,10 @@
Shell=NullEditorShell, Actions=11, Panels=8, Hierarchy=4, Assets=3, Inspector=UI Root, Console=3, Status=Focused selected object, Toolbar=on, RuntimePreview=on
Workspace[Default]
- Main Menu (main_menu) @ TopMenuBar visible=true
- Toolbar (toolbar) @ TopToolbar visible=true
- Hierarchy (hierarchy) @ LeftSidebar visible=true
- Project (project) @ LeftSidebar visible=true
- Viewport (viewport) @ CenterViewport visible=true
- Inspector (inspector) @ RightSidebar visible=true
- Console (console) @ BottomConsole visible=true
- Runtime Preview (runtime_preview) @ BottomRuntimePreview visible=true

View File

@ -103,6 +103,38 @@
1
]
}
},
{
"editor_metadata": {
"editor_only": false,
"locked": false,
"selected": false
},
"id": "0000399eef6307dc90859f938da98af700000003",
"name": "UI Root",
"parent_id": "",
"transform": {
"position": [
0,
0,
0
],
"rotation": [
0,
0,
0
],
"scale": [
1,
1,
1
]
},
"ui_document": {
"document_path": "Assets/UI/main_menu.rml",
"stylesheet_path": "Assets/UI/main_menu.rcss",
"visible": true
}
}
]
}

View File

@ -26,9 +26,7 @@
#include <glm/gtx/matrix_decompose.hpp>
#include <glm/vec4.hpp>
#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);
@ -174,136 +172,6 @@ ImGuizmo::MODE MetaCoreToImGuizmoMode(MetaCoreGizmoMode mode) {
return mode == MetaCoreGizmoMode::World ? ImGuizmo::WORLD : ImGuizmo::LOCAL;
}
MetaCoreId MetaCorePickGameObjectFromViewport(
const MetaCoreScene& scene,
const MetaCoreSceneView& sceneView,
const MetaCoreSceneViewportState& viewportState,
const glm::vec2& cursorPosition
) {
if (viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) {
return 0;
}
const glm::vec2 localCursor(
cursorPosition.x - viewportState.Left,
cursorPosition.y - viewportState.Top
);
if (localCursor.x < 0.0F || localCursor.y < 0.0F || localCursor.x > viewportState.Width || localCursor.y > viewportState.Height) {
return 0;
}
const glm::mat4 viewMatrix = glm::lookAt(
sceneView.CameraPosition,
sceneView.CameraTarget,
sceneView.CameraUp
);
const glm::mat4 projectionMatrix = glm::perspective(
glm::radians(sceneView.VerticalFieldOfViewDegrees),
viewportState.Width / viewportState.Height,
0.05F,
500.0F
);
const float normalizedX = (localCursor.x / viewportState.Width) * 2.0F - 1.0F;
const float normalizedY = 1.0F - (localCursor.y / viewportState.Height) * 2.0F;
const glm::vec4 rayStartClip(normalizedX, normalizedY, -1.0F, 1.0F);
const glm::vec4 rayEndClip(normalizedX, normalizedY, 1.0F, 1.0F);
const glm::mat4 inverseViewProjection = glm::inverse(projectionMatrix * viewMatrix);
glm::vec4 rayStartWorld = inverseViewProjection * rayStartClip;
glm::vec4 rayEndWorld = inverseViewProjection * rayEndClip;
if (std::abs(rayStartWorld.w) < 0.0001F || std::abs(rayEndWorld.w) < 0.0001F) {
return 0;
}
rayStartWorld /= rayStartWorld.w;
rayEndWorld /= rayEndWorld.w;
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();
for (const MetaCoreGameObject& gameObject : scene.GetGameObjects()) {
if (!gameObject.MeshRenderer.has_value() || !gameObject.MeshRenderer->Visible) {
continue;
}
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)));
constexpr glm::vec3 localBoundsMin(-0.5F, -0.5F, -0.5F);
constexpr glm::vec3 localBoundsMax(0.5F, 0.5F, 0.5F);
float nearDistance = 0.0F;
float farDistance = bestHitDistance;
bool hit = true;
for (int axisIndex = 0; axisIndex < 3; ++axisIndex) {
const float rayAxisOrigin = localRayOrigin[axisIndex];
const float rayAxisDirection = localRayDirection[axisIndex];
if (std::abs(rayAxisDirection) < 0.0001F) {
if (rayAxisOrigin < localBoundsMin[axisIndex] || rayAxisOrigin > localBoundsMax[axisIndex]) {
hit = false;
break;
}
continue;
}
float axisNearDistance = (localBoundsMin[axisIndex] - rayAxisOrigin) / rayAxisDirection;
float axisFarDistance = (localBoundsMax[axisIndex] - rayAxisOrigin) / rayAxisDirection;
if (axisNearDistance > axisFarDistance) {
std::swap(axisNearDistance, axisFarDistance);
}
nearDistance = std::max(nearDistance, axisNearDistance);
farDistance = std::min(farDistance, axisFarDistance);
if (nearDistance > farDistance) {
hit = false;
break;
}
}
if (hit && farDistance >= 0.0F) {
const float hitDistance = nearDistance >= 0.0F ? nearDistance : farDistance;
if (hitDistance >= 0.0F && hitDistance < bestHitDistance) {
bestHitDistance = hitDistance;
bestObjectId = gameObject.Id;
}
}
}
return bestObjectId;
}
} // namespace
MetaCoreEditorApp::~MetaCoreEditorApp() {
@ -435,10 +303,6 @@ 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);
@ -639,10 +503,7 @@ void MetaCoreEditorApp::DrawEditorFrame() {
gizmoHovering = ImGuizmo::IsOver();
gizmoUsing = ImGuizmo::IsUsing();
if (gizmoUsing && !gizmoWasUsing) {
gizmoBeforeSnapshot = EditorContext_->CaptureStateSnapshot();
hasGizmoBeforeSnapshot = true;
}
SceneInteractionService_.HandleGizmoBeginUse(*EditorContext_, gizmoUsing);
if (gizmoUsing) {
// transformMatrix is in Panda space — apply directly.
switch (EditorContext_->GetGizmoOperation()) {
@ -691,38 +552,17 @@ void MetaCoreEditorApp::DrawEditorFrame() {
}
if (viewportState.Hovered && !gizmoHovering && !gizmoUsing && !ImGui::GetIO().WantCaptureMouse && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
const MetaCoreId pickedObjectId = MetaCorePickGameObjectFromViewport(
const MetaCoreId pickedObjectId = SceneInteractionService_.PickGameObjectFromViewport(
Scene_,
sceneView,
viewportState,
EditorContext_->GetInput().GetCursorPosition()
);
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 = EditorContext_->GetActiveObjectId();
} else if (!ctrlDown && !shiftDown) {
EditorContext_->ClearSelection();
sceneView.SelectedObjectId = 0;
}
SceneInteractionService_.ApplyViewportSelection(*EditorContext_, Scene_, pickedObjectId);
sceneView.SelectedObjectId = EditorContext_->GetActiveObjectId();
}
if (!gizmoUsing && gizmoWasUsing && hasGizmoBeforeSnapshot) {
const MetaCoreEditorStateSnapshot gizmoAfterSnapshot = EditorContext_->CaptureStateSnapshot();
(void)EditorContext_->CommitStateTransition("修改变换", gizmoBeforeSnapshot, gizmoAfterSnapshot, true);
hasGizmoBeforeSnapshot = false;
}
gizmoWasUsing = gizmoUsing;
SceneInteractionService_.HandleGizmoEndUse(*EditorContext_, gizmoUsing);
ImGui::SetNextWindowPos(centralNode->Pos);
ImGui::SetNextWindowSize(centralNode->Size);
@ -830,13 +670,11 @@ void MetaCoreEditorApp::DrawEditorFrame() {
ImGui::PopStyleVar(3);
} else {
gizmoWasUsing = false;
hasGizmoBeforeSnapshot = false;
SceneInteractionService_.ResetFrameState();
ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{});
}
} else {
gizmoWasUsing = false;
hasGizmoBeforeSnapshot = false;
SceneInteractionService_.ResetFrameState();
ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{});
}
}

View File

@ -0,0 +1,207 @@
#include "MetaCoreEditor/MetaCoreSceneInteractionService.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include <imgui.h>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <algorithm>
#include <limits>
#include <unordered_map>
namespace MetaCore {
namespace {
glm::mat4 MetaCoreBuildTransformMatrix(const MetaCoreTransformComponent& transform) {
const glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0F), transform.Position);
const glm::mat4 rotationMatrix = glm::yawPitchRoll(
glm::radians(transform.RotationEulerDegrees.y),
glm::radians(transform.RotationEulerDegrees.x),
glm::radians(transform.RotationEulerDegrees.z)
);
const glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0F), transform.Scale);
return translationMatrix * rotationMatrix * scaleMatrix;
}
} // namespace
void MetaCoreSceneInteractionService::ResetFrameState() {
GizmoWasUsing_ = false;
HasGizmoBeforeSnapshot_ = false;
}
MetaCoreId MetaCoreSceneInteractionService::PickGameObjectFromViewport(
const MetaCoreScene& scene,
const MetaCoreSceneView& sceneView,
const MetaCoreSceneViewportState& viewportState,
const glm::vec2& cursorPosition
) const {
if (viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) {
return 0;
}
const glm::vec2 localCursor(
cursorPosition.x - viewportState.Left,
cursorPosition.y - viewportState.Top
);
if (localCursor.x < 0.0F || localCursor.y < 0.0F || localCursor.x > viewportState.Width || localCursor.y > viewportState.Height) {
return 0;
}
const glm::mat4 viewMatrix = glm::lookAt(
sceneView.CameraPosition,
sceneView.CameraTarget,
sceneView.CameraUp
);
const glm::mat4 projectionMatrix = glm::perspective(
glm::radians(sceneView.VerticalFieldOfViewDegrees),
viewportState.Width / viewportState.Height,
0.05F,
500.0F
);
const float normalizedX = (localCursor.x / viewportState.Width) * 2.0F - 1.0F;
const float normalizedY = 1.0F - (localCursor.y / viewportState.Height) * 2.0F;
const glm::vec4 rayStartClip(normalizedX, normalizedY, -1.0F, 1.0F);
const glm::vec4 rayEndClip(normalizedX, normalizedY, 1.0F, 1.0F);
const glm::mat4 inverseViewProjection = glm::inverse(projectionMatrix * viewMatrix);
glm::vec4 rayStartWorld = inverseViewProjection * rayStartClip;
glm::vec4 rayEndWorld = inverseViewProjection * rayEndClip;
if (std::abs(rayStartWorld.w) < 0.0001F || std::abs(rayEndWorld.w) < 0.0001F) {
return 0;
}
rayStartWorld /= rayStartWorld.w;
rayEndWorld /= rayEndWorld.w;
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();
for (const MetaCoreGameObject& gameObject : scene.GetGameObjects()) {
if (!gameObject.MeshRenderer.has_value() || !gameObject.MeshRenderer->Visible) {
continue;
}
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)));
constexpr glm::vec3 localBoundsMin(-0.5F, -0.5F, -0.5F);
constexpr glm::vec3 localBoundsMax(0.5F, 0.5F, 0.5F);
float nearDistance = 0.0F;
float farDistance = bestHitDistance;
bool hit = true;
for (int axisIndex = 0; axisIndex < 3; ++axisIndex) {
const float rayAxisOrigin = localRayOrigin[axisIndex];
const float rayAxisDirection = localRayDirection[axisIndex];
if (std::abs(rayAxisDirection) < 0.0001F) {
if (rayAxisOrigin < localBoundsMin[axisIndex] || rayAxisOrigin > localBoundsMax[axisIndex]) {
hit = false;
break;
}
continue;
}
float axisNearDistance = (localBoundsMin[axisIndex] - rayAxisOrigin) / rayAxisDirection;
float axisFarDistance = (localBoundsMax[axisIndex] - rayAxisOrigin) / rayAxisDirection;
if (axisNearDistance > axisFarDistance) {
std::swap(axisNearDistance, axisFarDistance);
}
nearDistance = std::max(nearDistance, axisNearDistance);
farDistance = std::min(farDistance, axisFarDistance);
if (nearDistance > farDistance) {
hit = false;
break;
}
}
if (hit && farDistance >= 0.0F) {
const float hitDistance = nearDistance >= 0.0F ? nearDistance : farDistance;
if (hitDistance >= 0.0F && hitDistance < bestHitDistance) {
bestHitDistance = hitDistance;
bestObjectId = gameObject.Id;
}
}
}
return bestObjectId;
}
void MetaCoreSceneInteractionService::ApplyViewportSelection(
MetaCoreEditorContext& editorContext,
const MetaCoreScene& scene,
MetaCoreId pickedObjectId
) const {
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);
}
} else if (!ctrlDown && !shiftDown) {
editorContext.ClearSelection();
}
}
void MetaCoreSceneInteractionService::HandleGizmoBeginUse(MetaCoreEditorContext& editorContext, bool gizmoUsing) {
if (gizmoUsing && !GizmoWasUsing_) {
GizmoBeforeSnapshot_ = editorContext.CaptureStateSnapshot();
HasGizmoBeforeSnapshot_ = true;
}
}
void MetaCoreSceneInteractionService::HandleGizmoEndUse(MetaCoreEditorContext& editorContext, bool gizmoUsing) {
if (!gizmoUsing && GizmoWasUsing_ && HasGizmoBeforeSnapshot_) {
const MetaCoreEditorStateSnapshot gizmoAfterSnapshot = editorContext.CaptureStateSnapshot();
(void)editorContext.CommitStateTransition("修改变换", GizmoBeforeSnapshot_, gizmoAfterSnapshot, true);
HasGizmoBeforeSnapshot_ = false;
}
GizmoWasUsing_ = gizmoUsing;
}
} // namespace MetaCore

View File

@ -2,6 +2,7 @@
#include "MetaCoreEditor/MetaCoreEditorContext.h"
#include "MetaCoreEditor/MetaCoreEditorModule.h"
#include "MetaCoreEditor/MetaCoreSceneInteractionService.h"
#include "MetaCoreFoundation/MetaCoreLogService.h"
#include "MetaCorePlatform/MetaCoreWindow.h"
@ -38,6 +39,7 @@ private:
MetaCoreScene Scene_{};
MetaCoreLogService LogService_{};
MetaCoreEditorModuleRegistry ModuleRegistry_{};
MetaCoreSceneInteractionService SceneInteractionService_{};
std::vector<std::unique_ptr<MetaCoreIModule>> Modules_{};
std::unique_ptr<MetaCoreEditorContext> EditorContext_{};
bool Initialized_ = false;

View File

@ -0,0 +1,34 @@
#pragma once
#include "MetaCoreEditor/MetaCoreEditorContext.h"
#include "MetaCoreRender/MetaCoreRenderTypes.h"
#include <glm/vec2.hpp>
namespace MetaCore {
class MetaCoreScene;
// 管理 Scene 视口交互:拾取选择与 Gizmo 变换提交。
class MetaCoreSceneInteractionService {
public:
void ResetFrameState();
[[nodiscard]] MetaCoreId PickGameObjectFromViewport(
const MetaCoreScene& scene,
const MetaCoreSceneView& sceneView,
const MetaCoreSceneViewportState& viewportState,
const glm::vec2& cursorPosition
) const;
void ApplyViewportSelection(MetaCoreEditorContext& editorContext, const MetaCoreScene& scene, MetaCoreId pickedObjectId) const;
void HandleGizmoBeginUse(MetaCoreEditorContext& editorContext, bool gizmoUsing);
void HandleGizmoEndUse(MetaCoreEditorContext& editorContext, bool gizmoUsing);
private:
bool GizmoWasUsing_ = false;
bool HasGizmoBeforeSnapshot_ = false;
MetaCoreEditorStateSnapshot GizmoBeforeSnapshot_{};
};
} // namespace MetaCore