refactor(editor): isolate engine service boundaries

This commit is contained in:
2026-04-29 03:19:46 +08:00
parent ef11651ec2
commit 313a571e43
60 changed files with 3804 additions and 2611 deletions

View File

@@ -75,6 +75,7 @@ set(EDITOR_APP_CORE_TEST_SOURCES
set(EDITOR_APP_FEATURE_TEST_SOURCES
test_editor_window_input_routing.cpp
test_game_viewport_runtime.cpp
test_project_panel.cpp
test_scene_viewport_render_plan.cpp
test_scene_viewport_runtime.cpp

View File

@@ -1,17 +1,11 @@
#include "Scene/EditorSceneBackend.h"
#include "Scene/EditorSceneRuntime.h"
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Scene/Scene.h>
#include <gtest/gtest.h>
namespace XCEngine::UI::Editor::App {
namespace {
using ::XCEngine::Components::GameObject;
using ::XCEngine::Components::Scene;
class FakeEditorSceneBackend final : public EditorSceneBackend {
public:
EditorStartupSceneResult EnsureStartupScene(
@@ -21,10 +15,6 @@ public:
return startupSceneResult;
}
Scene* GetActiveScene() const override {
return activeScene;
}
EditorSceneHierarchySnapshot BuildHierarchySnapshot() const override {
return hierarchySnapshot;
}
@@ -34,9 +24,15 @@ public:
return openSceneResult;
}
GameObject* FindGameObject(std::string_view itemId) const override {
lastFindItemId = std::string(itemId);
return foundGameObject;
std::optional<EditorSceneObjectSnapshot> GetObjectSnapshot(
std::string_view itemId) const override {
lastGetObjectSnapshotItemId = std::string(itemId);
if (objectSnapshot.has_value() &&
objectSnapshot->itemId == itemId) {
return objectSnapshot;
}
return std::nullopt;
}
bool AddComponent(
@@ -85,14 +81,13 @@ public:
EditorStartupSceneResult startupSceneResult = {};
EditorSceneHierarchySnapshot hierarchySnapshot = {};
Scene* activeScene = nullptr;
GameObject* foundGameObject = nullptr;
std::optional<EditorSceneObjectSnapshot> objectSnapshot = std::nullopt;
bool openSceneResult = false;
bool addComponentResult = false;
int ensureStartupSceneCallCount = 0;
std::filesystem::path lastProjectRoot = {};
std::filesystem::path lastOpenedScenePath = {};
mutable std::string lastFindItemId = {};
mutable std::string lastGetObjectSnapshotItemId = {};
std::string lastAddComponentItemId = {};
std::string lastAddComponentTypeName = {};
};
@@ -119,21 +114,30 @@ TEST(EditorSceneRuntimeBackendTests, InitializeUsesBoundBackend) {
EXPECT_EQ(runtime.GetStartupResult().sceneName, "Main");
}
TEST(EditorSceneRuntimeBackendTests, FindGameObjectUsesBoundBackend) {
TEST(EditorSceneRuntimeBackendTests, SetSelectionUsesBoundBackendObjectSnapshotLookup) {
auto backend = std::make_unique<FakeEditorSceneBackend>();
Scene scene("Main");
GameObject probe("Probe");
backend->startupSceneResult.ready = true;
backend->activeScene = &scene;
backend->foundGameObject = &probe;
backend->objectSnapshot = EditorSceneObjectSnapshot{
.itemId = "42",
.objectId = 42u,
.displayName = "Probe",
.componentTypeNames = {},
.visibleMaterialSlotCount = 1u
};
FakeEditorSceneBackend* const backendPtr = backend.get();
EditorSceneRuntime runtime = {};
runtime.SetBackend(std::move(backend));
ASSERT_TRUE(runtime.Initialize("D:/Xuanchi/Main/XCEngine/project"));
EXPECT_EQ(runtime.FindGameObject("42"), &probe);
EXPECT_EQ(backendPtr->lastFindItemId, "42");
ASSERT_TRUE(runtime.SetSelection("42"));
const std::optional<EditorSceneObjectSnapshot> selectedObject =
runtime.GetSelectedObjectSnapshot();
ASSERT_TRUE(selectedObject.has_value());
ASSERT_TRUE(runtime.GetSelectedObjectId().has_value());
EXPECT_EQ(runtime.GetSelectedObjectId().value(), 42u);
EXPECT_EQ(selectedObject->displayName, "Probe");
EXPECT_EQ(backendPtr->lastGetObjectSnapshotItemId, "42");
}
TEST(EditorSceneRuntimeBackendTests, BuildHierarchySnapshotUsesBoundBackend) {

View File

@@ -95,11 +95,11 @@ TEST(EditorShellAssetValidationTest, ProductManifestDeclaresPanelRuntimeAndViewp
const auto* gamePanel = FindEditorProductPanel(kGamePanelId);
ASSERT_NE(gamePanel, nullptr);
EXPECT_EQ(gamePanel->runtimeKind, EditorProductPanelRuntimeKind::None);
EXPECT_EQ(gamePanel->runtimeKind, EditorProductPanelRuntimeKind::Game);
EXPECT_EQ(
gamePanel->viewportRendererKind,
EditorProductViewportRendererKind::Placeholder);
EXPECT_FALSE(gamePanel->viewportPlaceholderStatus.empty());
EditorProductViewportRendererKind::Game);
EXPECT_TRUE(gamePanel->viewportPlaceholderStatus.empty());
}
TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspacePanelMissingFromRegistry) {

View File

@@ -0,0 +1,100 @@
#include <gtest/gtest.h>
#include "Game/GameViewportFeature.h"
#include "Panels/EditorPanelIds.h"
#include "State/EditorCommandFocusService.h"
#include "Viewport/GameViewportRenderService.h"
#include <XCEditor/Viewport/UIEditorViewportInputBridge.h>
namespace XCEngine::UI::Editor::App {
namespace {
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIPointerButton;
using ::XCEngine::UI::UIRect;
using ::XCEngine::UI::UISize;
using ::XCEngine::UI::Editor::UIEditorViewportInputBridgeState;
using ::XCEngine::UI::Editor::UIEditorWorkspaceComposeFrame;
using ::XCEngine::UI::Editor::UIEditorWorkspaceComposeState;
using ::XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationState;
using ::XCEngine::UI::Editor::UIEditorWorkspaceViewportComposeFrame;
using ::XCEngine::UI::Editor::UpdateUIEditorViewportInputBridge;
UIInputEvent MakePointerEvent(
UIInputEventType type,
float x,
float y,
UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = UIPoint(x, y);
event.pointerButton = button;
return event;
}
UIEditorWorkspaceComposeState BuildGameComposeState(
const UIEditorViewportInputBridgeState& inputBridgeState) {
UIEditorWorkspaceComposeState composeState = {};
UIEditorWorkspacePanelPresentationState panelState = {};
panelState.panelId = std::string(kGamePanelId);
panelState.viewportShellState.inputBridgeState = inputBridgeState;
composeState.panelStates.push_back(std::move(panelState));
return composeState;
}
UIEditorWorkspaceComposeFrame BuildGameComposeFrame(
const ::XCEngine::UI::Editor::UIEditorViewportInputBridgeFrame& inputFrame,
const UIRect& inputRect,
const UISize& requestedViewportSize) {
UIEditorWorkspaceComposeFrame composeFrame = {};
UIEditorWorkspaceViewportComposeFrame viewportFrame = {};
viewportFrame.panelId = std::string(kGamePanelId);
viewportFrame.viewportShellFrame.inputFrame = inputFrame;
viewportFrame.viewportShellFrame.requestedViewportSize = requestedViewportSize;
viewportFrame.viewportShellFrame.slotLayout.inputRect = inputRect;
viewportFrame.viewportShellFrame.slotLayout.bounds = inputRect;
composeFrame.viewportFrames.push_back(std::move(viewportFrame));
return composeFrame;
}
TEST(GameViewportRuntimeTests, PointerDownClaimsGameCommandFocus) {
GameViewportFeature feature = {};
EditorCommandFocusService commandFocusService = {};
feature.SetCommandFocusService(&commandFocusService);
UIEditorViewportInputBridgeState inputBridgeState = {};
const UIRect inputRect(100.0f, 80.0f, 640.0f, 360.0f);
const UISize viewportSize(640.0f, 360.0f);
const auto frame = UpdateUIEditorViewportInputBridge(
inputBridgeState,
inputRect,
{
MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 180.0f),
MakePointerEvent(
UIInputEventType::PointerButtonDown,
220.0f,
180.0f,
UIPointerButton::Left)
});
feature.Update(
BuildGameComposeState(inputBridgeState),
BuildGameComposeFrame(frame, inputRect, viewportSize));
EXPECT_EQ(commandFocusService.GetExplicitRoute(), EditorActionRoute::Game);
}
TEST(GameViewportRuntimeTests, GameViewportRendererUsesDefaultViewportResources) {
const ViewportResourceRequirements requirements =
GameViewportRenderService::GetViewportResourceRequirements();
EXPECT_FALSE(requirements.requiresDepthSampling);
EXPECT_FALSE(requirements.requiresObjectIdSurface);
EXPECT_FALSE(requirements.requiresSelectionMaskSurface);
}
} // namespace
} // namespace XCEngine::UI::Editor::App

View File

@@ -80,7 +80,9 @@ TEST(HierarchySceneBindingTests, BuildFromSceneUsesRealGameObjectIds) {
GameObject* child = scene->CreateGameObject("Child", root);
ASSERT_NE(child, nullptr);
const HierarchyModel model = HierarchyModel::BuildFromScene(scene);
const std::unique_ptr<EditorSceneBackend> backend = CreateTestSceneBackend();
const HierarchyModel model =
HierarchyModel::BuildFromSnapshot(backend->BuildHierarchySnapshot());
const HierarchyNode* rootNode =
model.FindNode(MakeEditorGameObjectItemId(root->GetID()));
ASSERT_NE(rootNode, nullptr);
@@ -109,7 +111,8 @@ TEST(HierarchySceneBindingTests, DuplicateGameObjectClonesHierarchyIntoScene) {
backend->DuplicateGameObject(MakeEditorGameObjectItemId(root->GetID()));
ASSERT_FALSE(duplicateId.empty());
const HierarchyModel model = HierarchyModel::BuildFromScene(scene);
const HierarchyModel model =
HierarchyModel::BuildFromSnapshot(backend->BuildHierarchySnapshot());
const HierarchyNode* duplicateNode = model.FindNode(duplicateId);
ASSERT_NE(duplicateNode, nullptr);
EXPECT_EQ(duplicateNode->label, "Root");
@@ -136,7 +139,8 @@ TEST(HierarchySceneBindingTests, RenameGameObjectUpdatesRealSceneAndProjection)
ASSERT_TRUE(backend->RenameGameObject(itemId, "PlayerCamera"));
EXPECT_EQ(gameObject->GetName(), "PlayerCamera");
const HierarchyModel model = HierarchyModel::BuildFromScene(scene);
const HierarchyModel model =
HierarchyModel::BuildFromSnapshot(backend->BuildHierarchySnapshot());
const HierarchyNode* node = model.FindNode(itemId);
ASSERT_NE(node, nullptr);
EXPECT_EQ(node->label, "PlayerCamera");
@@ -162,7 +166,8 @@ TEST(HierarchySceneBindingTests, ReparentAndMoveToRootOperateOnRealScene) {
MakeEditorGameObjectItemId(parentB->GetID())));
EXPECT_EQ(child->GetParent(), parentB);
HierarchyModel model = HierarchyModel::BuildFromScene(scene);
HierarchyModel model =
HierarchyModel::BuildFromSnapshot(backend->BuildHierarchySnapshot());
const HierarchyNode* parentBNode =
model.FindNode(MakeEditorGameObjectItemId(parentB->GetID()));
ASSERT_NE(parentBNode, nullptr);
@@ -173,7 +178,7 @@ TEST(HierarchySceneBindingTests, ReparentAndMoveToRootOperateOnRealScene) {
MakeEditorGameObjectItemId(child->GetID())));
EXPECT_EQ(child->GetParent(), nullptr);
model = HierarchyModel::BuildFromScene(scene);
model = HierarchyModel::BuildFromSnapshot(backend->BuildHierarchySnapshot());
const auto roots = scene->GetRootGameObjects();
EXPECT_EQ(roots.size(), 3u);
}
@@ -196,11 +201,12 @@ TEST(HierarchySceneBindingTests, EnsureStartupSceneLoadsMainSceneAndSetsActive)
backend->EnsureStartupScene(projectRoot.Root());
EXPECT_TRUE(result.ready);
EXPECT_TRUE(result.loadedFromDisk);
ASSERT_NE(backend->GetActiveScene(), nullptr);
EXPECT_EQ(backend->GetActiveScene()->GetName(), "Main");
Scene* const activeScene = SceneManager::Get().GetActiveScene();
ASSERT_NE(activeScene, nullptr);
EXPECT_EQ(activeScene->GetName(), "Main");
const HierarchyModel model =
HierarchyModel::BuildFromScene(backend->GetActiveScene());
HierarchyModel::BuildFromSnapshot(backend->BuildHierarchySnapshot());
EXPECT_FALSE(model.Empty());
SceneManager& sceneManager = SceneManager::Get();

View File

@@ -1,5 +1,6 @@
#include "Inspector/Components/IInspectorComponentEditor.h"
#include "Inspector/Components/InspectorComponentEditorRegistry.h"
#include "Inspector/InspectorFieldValueApplier.h"
#include "Inspector/InspectorPresentationModel.h"
#include "Inspector/InspectorSubject.h"
#include "Scene/EditorSceneRuntime.h"
@@ -118,6 +119,20 @@ const UIEditorPropertyGridField* FindField(
return nullptr;
}
const UIEditorPropertyGridField* FindFieldById(
const InspectorPresentationModel& model,
std::string_view fieldId) {
for (const UIEditorPropertyGridSection& section : model.sections) {
for (const UIEditorPropertyGridField& field : section.fields) {
if (field.fieldId == fieldId) {
return &field;
}
}
}
return nullptr;
}
const InspectorPresentationComponentBinding* FindBinding(
const InspectorPresentationModel& model,
std::string_view typeName) {
@@ -201,7 +216,7 @@ TEST(InspectorPresentationModelTests, SceneObjectSubjectBuildsRegisteredComponen
EditorSceneRuntime runtime = {};
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
Scene* scene = runtime.GetActiveScene();
Scene* scene = SceneManager::Get().GetActiveScene();
ASSERT_NE(scene, nullptr);
GameObject* parent = scene->Find("Parent");
ASSERT_NE(parent, nullptr);
@@ -273,7 +288,7 @@ TEST(InspectorPresentationModelTests, CameraSkyboxMaterialBuildsAssetField) {
EditorSceneRuntime runtime = {};
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
Scene* scene = runtime.GetActiveScene();
Scene* scene = SceneManager::Get().GetActiveScene();
ASSERT_NE(scene, nullptr);
GameObject* parent = scene->Find("Parent");
ASSERT_NE(parent, nullptr);
@@ -302,5 +317,57 @@ TEST(InspectorPresentationModelTests, CameraSkyboxMaterialBuildsAssetField) {
"Skybox.mat");
}
TEST(InspectorPresentationModelTests, BoundFieldValueApplierKeepsComponentViewAliveDuringApply) {
ScopedSceneManagerReset reset = {};
TemporaryProjectRoot projectRoot = {};
SaveMainScene(projectRoot);
EditorSceneRuntime runtime = {};
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
Scene* scene = SceneManager::Get().GetActiveScene();
ASSERT_NE(scene, nullptr);
GameObject* parent = scene->Find("Parent");
ASSERT_NE(parent, nullptr);
ASSERT_TRUE(runtime.SetSelection(parent->GetID()));
const InspectorSubject subject =
BuildInspectorSubject(EditorSession{}, runtime);
ASSERT_EQ(subject.kind, InspectorSubjectKind::SceneObject);
const InspectorPresentationModel model =
BuildInspectorPresentationModel(
subject,
runtime,
InspectorComponentEditorRegistry::Get());
const auto* transformBinding = FindBinding(model, "Transform");
ASSERT_NE(transformBinding, nullptr);
ASSERT_FALSE(transformBinding->fieldIds.empty());
const UIEditorPropertyGridField* originalField =
FindFieldById(model, transformBinding->fieldIds.front());
ASSERT_NE(originalField, nullptr);
ASSERT_EQ(originalField->kind, Widgets::UIEditorPropertyGridFieldKind::Vector3);
UIEditorPropertyGridField editedField = *originalField;
editedField.vector3Value.values = { 8.0, 9.0, 10.0 };
bool applied = false;
EXPECT_NO_THROW(
applied = ApplyInspectorComponentBoundFieldValue(
runtime,
subject.sceneObject,
*transformBinding,
editedField));
ASSERT_TRUE(applied);
const auto* transform = parent->GetTransform();
ASSERT_NE(transform, nullptr);
const auto position = transform->GetLocalPosition();
EXPECT_FLOAT_EQ(position.x, 8.0f);
EXPECT_FLOAT_EQ(position.y, 9.0f);
EXPECT_FLOAT_EQ(position.z, 10.0f);
}
} // namespace
} // namespace XCEngine::UI::Editor::App

View File

@@ -1,8 +1,5 @@
#include "Viewport/SceneViewportRenderPlan.h"
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/GameObject.h>
#include <gtest/gtest.h>
namespace {
@@ -72,17 +69,17 @@ public:
}
};
SceneViewportRenderRequest CreateValidRequest(
XCEngine::Components::GameObject& cameraObject) {
auto* camera =
cameraObject.AddComponent<XCEngine::Components::CameraComponent>();
EXPECT_NE(camera, nullptr);
EXPECT_NE(cameraObject.GetTransform(), nullptr);
cameraObject.GetTransform()->SetPosition(
XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
SceneViewportRenderRequest CreateValidRequest() {
SceneViewportRenderRequest request = {};
request.camera = camera;
request.orbitDistance = 9.0f;
request.camera.valid = true;
request.camera.position = XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f);
request.camera.forward = XCEngine::Math::Vector3::Forward();
request.camera.right = XCEngine::Math::Vector3::Right();
request.camera.up = XCEngine::Math::Vector3::Up();
request.camera.verticalFovDegrees = 60.0f;
request.camera.nearClipPlane = 0.03f;
request.camera.farClipPlane = 2000.0f;
request.camera.orbitDistance = 9.0f;
return request;
}
@@ -98,8 +95,7 @@ TEST(SceneViewportRenderPlanTests, BuildRenderPlanCreatesOutlinePassWhenSelectio
targets.selectionMaskView = &selectionMaskView;
targets.selectionMaskShaderView = &selectionMaskShaderView;
XCEngine::Components::GameObject cameraObject("SceneCamera");
SceneViewportRenderRequest request = CreateValidRequest(cameraObject);
SceneViewportRenderRequest request = CreateValidRequest();
request.selectedObjectIds = { 7u, 11u };
std::size_t gridFactoryCallCount = 0u;
@@ -136,8 +132,7 @@ TEST(SceneViewportRenderPlanTests, BuildRenderPlanCreatesOutlinePassWhenSelectio
TEST(SceneViewportRenderPlanTests, BuildRenderPlanWarnsWhenSelectionResourcesAreUnavailable) {
ViewportRenderTargets targets = {};
XCEngine::Components::GameObject cameraObject("SceneCamera");
SceneViewportRenderRequest request = CreateValidRequest(cameraObject);
SceneViewportRenderRequest request = CreateValidRequest();
request.selectedObjectIds = { 42u };
std::size_t gridFactoryCallCount = 0u;
@@ -167,8 +162,7 @@ TEST(SceneViewportRenderPlanTests, BuildRenderPlanWarnsWhenSelectionResourcesAre
}
TEST(SceneViewportRenderPlanTests, BuildSceneViewportGridPassDataCopiesCameraTransformAndLens) {
XCEngine::Components::GameObject cameraObject("SceneCamera");
SceneViewportRenderRequest request = CreateValidRequest(cameraObject);
SceneViewportRenderRequest request = CreateValidRequest();
const SceneViewportGridPassData gridData =
BuildSceneViewportGridPassData(request);
@@ -177,9 +171,9 @@ TEST(SceneViewportRenderPlanTests, BuildSceneViewportGridPassDataCopiesCameraTra
EXPECT_FLOAT_EQ(gridData.cameraPosition.x, 1.0f);
EXPECT_FLOAT_EQ(gridData.cameraPosition.y, 2.0f);
EXPECT_FLOAT_EQ(gridData.cameraPosition.z, 3.0f);
EXPECT_FLOAT_EQ(gridData.verticalFovDegrees, request.camera->GetFieldOfView());
EXPECT_FLOAT_EQ(gridData.nearClipPlane, request.camera->GetNearClipPlane());
EXPECT_FLOAT_EQ(gridData.farClipPlane, request.camera->GetFarClipPlane());
EXPECT_FLOAT_EQ(gridData.verticalFovDegrees, request.camera.verticalFovDegrees);
EXPECT_FLOAT_EQ(gridData.nearClipPlane, request.camera.nearClipPlane);
EXPECT_FLOAT_EQ(gridData.farClipPlane, request.camera.farClipPlane);
EXPECT_FLOAT_EQ(gridData.orbitDistance, 9.0f);
}
@@ -204,8 +198,7 @@ TEST(SceneViewportRenderPlanTests, ApplyRenderPlanAttachesPassesAndMarksRenderSt
targets.objectIdState = ResourceStates::Common;
targets.selectionMaskState = ResourceStates::Common;
XCEngine::Components::GameObject cameraObject("SceneCamera");
SceneViewportRenderRequest request = CreateValidRequest(cameraObject);
SceneViewportRenderRequest request = CreateValidRequest();
request.selectedObjectIds = { 24u };
auto result = BuildSceneViewportRenderPlan(

View File

@@ -23,8 +23,11 @@
#include <gtest/gtest.h>
#include <chrono>
#include <cmath>
#include <filesystem>
#include <cstdint>
#include <array>
#include <optional>
namespace XCEngine::UI::Editor::App {
namespace {
@@ -113,6 +116,10 @@ void BindEngineSceneBackend(EditorSceneRuntime& runtime) {
::XCEngine::Resources::ResourceManager::Get()));
}
Scene* GetLoadedActiveScene() {
return SceneManager::Get().GetActiveScene();
}
UIInputEvent MakePointerEvent(
UIInputEventType type,
float x,
@@ -208,6 +215,27 @@ const EditorSceneComponentDescriptor* FindComponentDescriptor(
return nullptr;
}
std::optional<UIPoint> FindHoveredTransformGizmoPoint(
SceneViewportTransformGizmo& gizmo,
EditorSceneRuntime& runtime,
const UIRect& viewportRect) {
for (float y = viewportRect.y + 8.0f;
y < viewportRect.y + viewportRect.height - 8.0f;
y += 6.0f) {
for (float x = viewportRect.x + 8.0f;
x < viewportRect.x + viewportRect.width - 8.0f;
x += 6.0f) {
const UIPoint point(x, y);
gizmo.Refresh(runtime, viewportRect, point, true);
if (gizmo.IsHoveringHandle()) {
return point;
}
}
}
return std::nullopt;
}
TEST(SceneViewportRuntimeTests, ApplySceneViewportCameraInputUpdatesCameraTransform) {
ScopedSceneManagerReset reset = {};
TemporaryProjectRoot projectRoot = {};
@@ -217,22 +245,21 @@ TEST(SceneViewportRuntimeTests, ApplySceneViewportCameraInputUpdatesCameraTransf
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
auto* camera = runtime.GetSceneViewCamera();
ASSERT_NE(camera, nullptr);
auto* transform = camera->GetGameObject()->GetTransform();
ASSERT_NE(transform, nullptr);
const Math::Vector3 before = transform->GetPosition();
const EditorSceneCameraSnapshot before =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(before.valid);
SceneViewportCameraInputState input = {};
input.viewportHeight = 720.0f;
input.zoomDelta = 1.0f;
runtime.ApplySceneViewportCameraInput(input);
const Math::Vector3 after = transform->GetPosition();
EXPECT_NE(before.x, after.x);
EXPECT_NE(before.y, after.y);
EXPECT_NE(before.z, after.z);
const EditorSceneCameraSnapshot after =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(after.valid);
EXPECT_NE(before.position.x, after.position.x);
EXPECT_NE(before.position.y, after.position.y);
EXPECT_NE(before.position.z, after.position.z);
}
TEST(SceneViewportRuntimeTests, FocusSceneSelectionRepositionsCameraAroundSelectedObject) {
@@ -243,25 +270,25 @@ TEST(SceneViewportRuntimeTests, FocusSceneSelectionRepositionsCameraAroundSelect
EditorSceneRuntime runtime = {};
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
Scene* scene = runtime.GetActiveScene();
Scene* scene = GetLoadedActiveScene();
ASSERT_NE(scene, nullptr);
GameObject* target = scene->Find("Target");
ASSERT_NE(target, nullptr);
ASSERT_TRUE(runtime.SetSelection(target->GetID()));
auto* camera = runtime.GetSceneViewCamera();
ASSERT_NE(camera, nullptr);
auto* transform = camera->GetGameObject()->GetTransform();
ASSERT_NE(transform, nullptr);
const Math::Vector3 before = transform->GetPosition();
const EditorSceneCameraSnapshot before =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(before.valid);
ASSERT_TRUE(runtime.FocusSceneSelection());
const Math::Vector3 after = transform->GetPosition();
EXPECT_NE(before.x, after.x);
EXPECT_NE(before.y, after.y);
EXPECT_NE(before.z, after.z);
const EditorSceneCameraSnapshot after =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(after.valid);
EXPECT_NE(before.position.x, after.position.x);
EXPECT_NE(before.position.y, after.position.y);
EXPECT_NE(before.position.z, after.position.z);
}
TEST(SceneViewportRuntimeTests, BuildSceneViewportRenderRequestIncludesSelectedObjectId) {
@@ -272,7 +299,7 @@ TEST(SceneViewportRuntimeTests, BuildSceneViewportRenderRequestIncludesSelectedO
EditorSceneRuntime runtime = {};
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
Scene* scene = runtime.GetActiveScene();
Scene* scene = GetLoadedActiveScene();
ASSERT_NE(scene, nullptr);
GameObject* target = scene->Find("Target");
@@ -284,7 +311,7 @@ TEST(SceneViewportRuntimeTests, BuildSceneViewportRenderRequestIncludesSelectedO
ASSERT_TRUE(request.IsValid());
ASSERT_EQ(request.selectedObjectIds.size(), 1u);
EXPECT_EQ(request.selectedObjectIds.front(), target->GetID());
EXPECT_GT(request.orbitDistance, 0.0f);
EXPECT_GT(request.camera.orbitDistance, 0.0f);
EXPECT_FALSE(request.debugSelectionMask);
}
@@ -296,7 +323,7 @@ TEST(SceneViewportRuntimeTests, SelectedComponentsExposeTransformAndAttachedCame
EditorSceneRuntime runtime = {};
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
Scene* scene = runtime.GetActiveScene();
Scene* scene = GetLoadedActiveScene();
ASSERT_NE(scene, nullptr);
GameObject* target = scene->Find("Target");
ASSERT_NE(target, nullptr);
@@ -327,7 +354,7 @@ TEST(SceneViewportRuntimeTests, RemoveSelectedComponentDropsRemovableDescriptorB
EditorSceneRuntime runtime = {};
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
Scene* scene = runtime.GetActiveScene();
Scene* scene = GetLoadedActiveScene();
ASSERT_NE(scene, nullptr);
GameObject* target = scene->Find("Target");
ASSERT_NE(target, nullptr);
@@ -356,7 +383,9 @@ TEST(SceneViewportRuntimeTests, TransformSetterApisWriteLocalValuesOnSelectedTra
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
runtime.EnsureSceneSelection();
auto* target = const_cast<GameObject*>(runtime.GetSelectedGameObject());
Scene* scene = GetLoadedActiveScene();
ASSERT_NE(scene, nullptr);
auto* target = scene->Find("Target");
ASSERT_NE(target, nullptr);
auto* transform = target->GetTransform();
ASSERT_NE(transform, nullptr);
@@ -400,7 +429,7 @@ TEST(SceneViewportRuntimeTests, SelectionStampAdvancesOnSceneSelectionChanges) {
const std::uint64_t initialStamp = runtime.GetSelectionStamp();
EXPECT_GT(initialStamp, 0u);
Scene* scene = runtime.GetActiveScene();
Scene* scene = GetLoadedActiveScene();
ASSERT_NE(scene, nullptr);
GameObject* secondary = scene->CreateGameObject("Secondary");
ASSERT_NE(secondary, nullptr);
@@ -438,7 +467,7 @@ TEST(SceneViewportRuntimeTests, InspectorSelectionResolverFollowsUnifiedSelectio
BuildInspectorSubject(session, runtime);
EXPECT_EQ(sceneSubject.kind, InspectorSubjectKind::SceneObject);
EXPECT_EQ(sceneSubject.source, InspectorSelectionSource::Scene);
EXPECT_EQ(sceneSubject.sceneObject.displayName, "Target");
EXPECT_EQ(sceneSubject.sceneObject.object.displayName, "Target");
selectionService.SetProjectSelection(
"asset:scene",
@@ -494,11 +523,9 @@ TEST(SceneViewportRuntimeTests, RightMouseDragRotatesSceneCameraThroughViewportC
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
auto* camera = runtime.GetSceneViewCamera();
ASSERT_NE(camera, nullptr);
auto* transform = camera->GetGameObject()->GetTransform();
ASSERT_NE(transform, nullptr);
const Math::Vector3 beforeForward = transform->GetForward();
const EditorSceneCameraSnapshot before =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(before.valid);
SceneViewportController controller = {};
SceneViewportRenderService sceneViewportRenderService = {};
@@ -540,10 +567,12 @@ TEST(SceneViewportRuntimeTests, RightMouseDragRotatesSceneCameraThroughViewportC
BuildSceneComposeState(inputBridgeState),
BuildSceneComposeFrame(dragFrame, inputRect, viewportSize));
const Math::Vector3 afterForward = transform->GetForward();
EXPECT_NE(beforeForward.x, afterForward.x);
EXPECT_NE(beforeForward.y, afterForward.y);
EXPECT_NE(beforeForward.z, afterForward.z);
const EditorSceneCameraSnapshot after =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(after.valid);
EXPECT_NE(before.forward.x, after.forward.x);
EXPECT_NE(before.forward.y, after.forward.y);
EXPECT_NE(before.forward.z, after.forward.z);
}
TEST(SceneViewportRuntimeTests, MoveRightInputMovesSceneCameraTowardPositiveCameraRight) {
@@ -555,11 +584,9 @@ TEST(SceneViewportRuntimeTests, MoveRightInputMovesSceneCameraTowardPositiveCame
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
auto* camera = runtime.GetSceneViewCamera();
ASSERT_NE(camera, nullptr);
auto* transform = camera->GetGameObject()->GetTransform();
ASSERT_NE(transform, nullptr);
const Math::Vector3 before = transform->GetPosition();
const EditorSceneCameraSnapshot before =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(before.valid);
SceneViewportCameraInputState input = {};
input.viewportHeight = 720.0f;
@@ -567,8 +594,10 @@ TEST(SceneViewportRuntimeTests, MoveRightInputMovesSceneCameraTowardPositiveCame
input.moveRight = 1.0f;
runtime.ApplySceneViewportCameraInput(input);
const Math::Vector3 after = transform->GetPosition();
EXPECT_GT(after.x, before.x);
const EditorSceneCameraSnapshot after =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(after.valid);
EXPECT_GT(after.position.x, before.position.x);
}
TEST(SceneViewportRuntimeTests, MiddleMouseDragPansSceneCameraWithGrabSemantics) {
@@ -580,11 +609,9 @@ TEST(SceneViewportRuntimeTests, MiddleMouseDragPansSceneCameraWithGrabSemantics)
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
auto* camera = runtime.GetSceneViewCamera();
ASSERT_NE(camera, nullptr);
auto* transform = camera->GetGameObject()->GetTransform();
ASSERT_NE(transform, nullptr);
const Math::Vector3 before = transform->GetPosition();
const EditorSceneCameraSnapshot before =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(before.valid);
SceneViewportController controller = {};
SceneViewportRenderService sceneViewportRenderService = {};
@@ -626,8 +653,10 @@ TEST(SceneViewportRuntimeTests, MiddleMouseDragPansSceneCameraWithGrabSemantics)
BuildSceneComposeState(inputBridgeState),
BuildSceneComposeFrame(dragFrame, inputRect, viewportSize));
const Math::Vector3 after = transform->GetPosition();
EXPECT_LT(after.x, before.x);
const EditorSceneCameraSnapshot after =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(after.valid);
EXPECT_LT(after.position.x, before.position.x);
}
TEST(SceneViewportRuntimeTests, ViewToolLeftMouseDragPansSceneCameraWithGrabSemantics) {
@@ -640,11 +669,9 @@ TEST(SceneViewportRuntimeTests, ViewToolLeftMouseDragPansSceneCameraWithGrabSema
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
runtime.SetToolMode(SceneToolMode::View);
auto* camera = runtime.GetSceneViewCamera();
ASSERT_NE(camera, nullptr);
auto* transform = camera->GetGameObject()->GetTransform();
ASSERT_NE(transform, nullptr);
const Math::Vector3 before = transform->GetPosition();
const EditorSceneCameraSnapshot before =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(before.valid);
SceneViewportController controller = {};
SceneViewportRenderService sceneViewportRenderService = {};
@@ -686,8 +713,10 @@ TEST(SceneViewportRuntimeTests, ViewToolLeftMouseDragPansSceneCameraWithGrabSema
BuildSceneComposeState(inputBridgeState),
BuildSceneComposeFrame(dragFrame, inputRect, viewportSize));
const Math::Vector3 after = transform->GetPosition();
EXPECT_LT(after.x, before.x);
const EditorSceneCameraSnapshot after =
runtime.BuildSceneViewCameraSnapshot();
ASSERT_TRUE(after.valid);
EXPECT_LT(after.position.x, before.position.x);
}
TEST(SceneViewportRuntimeTests, MouseWheelUsesSingleNotchNormalizationForSceneZoom) {
@@ -699,7 +728,7 @@ TEST(SceneViewportRuntimeTests, MouseWheelUsesSingleNotchNormalizationForSceneZo
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
const float beforeDistance =
runtime.BuildSceneViewportRenderRequest().orbitDistance;
runtime.BuildSceneViewportRenderRequest().camera.orbitDistance;
SceneViewportController controller = {};
SceneViewportRenderService sceneViewportRenderService = {};
@@ -732,7 +761,7 @@ TEST(SceneViewportRuntimeTests, MouseWheelUsesSingleNotchNormalizationForSceneZo
BuildSceneComposeFrame(wheelFrame, inputRect, viewportSize));
const float afterDistance =
runtime.BuildSceneViewportRenderRequest().orbitDistance;
runtime.BuildSceneViewportRenderRequest().camera.orbitDistance;
EXPECT_LT(afterDistance, beforeDistance);
EXPECT_GT(afterDistance, 4.0f);
}
@@ -924,6 +953,64 @@ TEST(SceneViewportRuntimeTests, SceneToolOverlayHandlesCoalescedClickInSingleFra
EXPECT_EQ(runtime.GetToolMode(), SceneToolMode::Rotate);
}
TEST(SceneViewportRuntimeTests, TranslateGizmoDragAppliesPreviewToSelectedObject) {
ScopedSceneManagerReset reset = {};
TemporaryProjectRoot projectRoot = {};
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
EditorSceneRuntime runtime = {};
BindEngineSceneBackend(runtime);
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
runtime.SetToolMode(SceneToolMode::Translate);
runtime.EnsureSceneSelection();
Scene* scene = GetLoadedActiveScene();
ASSERT_NE(scene, nullptr);
GameObject* target = scene->Find("Target");
ASSERT_NE(target, nullptr);
auto* transform = target->GetTransform();
ASSERT_NE(transform, nullptr);
SceneViewportTransformGizmo gizmo = {};
const UIRect viewportRect(0.0f, 0.0f, 640.0f, 360.0f);
const std::optional<UIPoint> hoverPoint =
FindHoveredTransformGizmoPoint(gizmo, runtime, viewportRect);
ASSERT_TRUE(hoverPoint.has_value());
const std::array<UIPoint, 6u> dragTargets = {
UIPoint(hoverPoint->x + 40.0f, hoverPoint->y),
UIPoint(hoverPoint->x - 40.0f, hoverPoint->y),
UIPoint(hoverPoint->x, hoverPoint->y + 40.0f),
UIPoint(hoverPoint->x, hoverPoint->y - 40.0f),
UIPoint(hoverPoint->x + 32.0f, hoverPoint->y + 32.0f),
UIPoint(hoverPoint->x - 32.0f, hoverPoint->y - 32.0f)
};
bool previewApplied = false;
for (const UIPoint& dragPoint : dragTargets) {
transform->SetPosition(Math::Vector3(0.0f, 0.0f, 0.0f));
gizmo.Refresh(runtime, viewportRect, hoverPoint.value(), true);
ASSERT_TRUE(gizmo.IsHoveringHandle());
ASSERT_TRUE(gizmo.TryBeginDrag(runtime));
gizmo.Refresh(runtime, viewportRect, dragPoint, true);
ASSERT_TRUE(gizmo.UpdateDrag(runtime));
const Math::Vector3 previewPosition = transform->GetPosition();
if (std::abs(previewPosition.x) > 0.0001f ||
std::abs(previewPosition.y) > 0.0001f ||
std::abs(previewPosition.z) > 0.0001f) {
previewApplied = true;
gizmo.CancelDrag(runtime);
break;
}
gizmo.CancelDrag(runtime);
}
EXPECT_TRUE(previewApplied);
}
TEST(SceneViewportRuntimeTests, SceneViewportRendererDeclaresExplicitAuxiliaryResourceRequirements) {
const ViewportResourceRequirements requirements =
SceneViewportRenderService::GetViewportResourceRequirements();