Formalize staged scene viewport gizmo frame API

This commit is contained in:
2026-04-04 01:09:02 +08:00
parent 64a2efe993
commit d207043f8f
5 changed files with 486 additions and 44 deletions

View File

@@ -1,21 +1,40 @@
#include <gtest/gtest.h>
#include "Core/EventBus.h"
#include "Core/IEditorContext.h"
#include "Core/IProjectManager.h"
#include "Core/ISceneManager.h"
#include "Core/ISelectionManager.h"
#include "Core/IUndoManager.h"
#include "Viewport/IViewportHostService.h"
#include "Viewport/SceneViewportTransformGizmoCoordinator.h"
#include <XCEngine/Components/GameObject.h>
#include <algorithm>
namespace {
using XCEngine::Editor::BuildBeginSceneViewportTransformGizmoLifecycleCommand;
using XCEngine::Editor::BuildFrameSceneViewportTransformGizmoLifecycleCommand;
using XCEngine::Editor::BuildSceneViewportTransformGizmoFrameOptions;
using XCEngine::Editor::BuildSceneViewportTransformGizmoRefreshRequest;
using XCEngine::Editor::BuildSceneViewportTransformGizmoOverlaySubmission;
using XCEngine::Editor::CancelSceneViewportTransformGizmoFrame;
using XCEngine::Editor::EditorViewportFrame;
using XCEngine::Editor::EditorViewportKind;
using XCEngine::Editor::EditorActionRoute;
using XCEngine::Editor::EditorRuntimeMode;
using XCEngine::Editor::EventBus;
using XCEngine::Editor::IEditorContext;
using XCEngine::Editor::IProjectManager;
using XCEngine::Editor::ISceneManager;
using XCEngine::Editor::ISelectionManager;
using XCEngine::Editor::IViewportHostService;
using XCEngine::Editor::AssetItemPtr;
using XCEngine::Editor::SceneViewportActiveGizmoKind;
using XCEngine::Editor::SceneViewportTransformGizmoFrameOptions;
using XCEngine::Editor::SceneViewportTransformGizmoFrameUpdate;
using XCEngine::Editor::SceneViewportInput;
using XCEngine::Editor::SceneViewportInteractionActions;
using XCEngine::Editor::SceneViewportOrientationAxis;
@@ -24,8 +43,185 @@ using XCEngine::Editor::SceneViewportOverlayFrameData;
using XCEngine::Editor::SceneViewportTransformGizmoFrameState;
using XCEngine::Editor::SceneViewportTransformGizmoLifecycleStage;
using XCEngine::Editor::SceneViewportTransformGizmoOverlayState;
using XCEngine::Editor::RefreshAndSubmitSceneViewportTransformGizmoFrame;
using XCEngine::Editor::SubmitSceneViewportTransformGizmoOverlaySubmission;
using XCEngine::Editor::SceneSnapshot;
using XCEngine::Rendering::RenderContext;
using XCEngine::Math::Vector2;
class StubSelectionManager : public ISelectionManager {
public:
void SetSelectedEntity(uint64_t entityId) override {
selectedEntity = entityId;
selectedEntities = entityId == 0 ? std::vector<uint64_t>{} : std::vector<uint64_t>{entityId};
}
void SetSelectedEntities(const std::vector<uint64_t>& entityIds) override {
selectedEntities = entityIds;
selectedEntity = entityIds.empty() ? 0 : entityIds.front();
}
void AddToSelection(uint64_t entityId) override {
selectedEntities.push_back(entityId);
selectedEntity = entityId;
}
void RemoveFromSelection(uint64_t entityId) override {
selectedEntities.erase(
std::remove(selectedEntities.begin(), selectedEntities.end(), entityId),
selectedEntities.end());
if (selectedEntity == entityId) {
selectedEntity = selectedEntities.empty() ? 0 : selectedEntities.front();
}
}
void ClearSelection() override {
selectedEntity = 0;
selectedEntities.clear();
}
uint64_t GetSelectedEntity() const override {
return selectedEntity;
}
const std::vector<uint64_t>& GetSelectedEntities() const override {
return selectedEntities;
}
bool HasSelection() const override {
return !selectedEntities.empty();
}
size_t GetSelectionCount() const override {
return selectedEntities.size();
}
bool IsSelected(uint64_t entityId) const override {
return std::find(selectedEntities.begin(), selectedEntities.end(), entityId) != selectedEntities.end();
}
uint64_t selectedEntity = 0;
std::vector<uint64_t> selectedEntities = {};
};
class StubSceneManager : public ISceneManager {
public:
XCEngine::Components::GameObject* CreateEntity(
const std::string&,
XCEngine::Components::GameObject* = nullptr) override { return nullptr; }
void DeleteEntity(XCEngine::Components::GameObject::ID) override {}
void RenameEntity(XCEngine::Components::GameObject::ID, const std::string&) override {}
XCEngine::Components::GameObject* GetEntity(XCEngine::Components::GameObject::ID entityId) override {
return entity != nullptr && entity->GetID() == entityId ? entity : nullptr;
}
const std::vector<XCEngine::Components::GameObject*>& GetRootEntities() const override { return rootEntities; }
void CopyEntity(XCEngine::Components::GameObject::ID) override {}
XCEngine::Components::GameObject::ID PasteEntity(XCEngine::Components::GameObject::ID = 0) override { return 0; }
XCEngine::Components::GameObject::ID DuplicateEntity(XCEngine::Components::GameObject::ID) override { return 0; }
void MoveEntity(XCEngine::Components::GameObject::ID, XCEngine::Components::GameObject::ID) override {}
bool HasClipboardData() const override { return false; }
void NewScene(const std::string& = "Untitled Scene") override {}
bool LoadScene(const std::string&) override { return false; }
bool SaveScene() override { return false; }
bool SaveSceneAs(const std::string&) override { return false; }
bool LoadStartupScene(const std::string&) override { return false; }
bool HasActiveScene() const override { return false; }
bool IsSceneDirty() const override { return false; }
void MarkSceneDirty() override {}
void SetSceneDocumentDirtyTrackingEnabled(bool) override {}
bool IsSceneDocumentDirtyTrackingEnabled() const override { return false; }
const std::string& GetCurrentScenePath() const override { return empty; }
const std::string& GetCurrentSceneName() const override { return empty; }
XCEngine::Components::Scene* GetScene() override { return nullptr; }
const XCEngine::Components::Scene* GetScene() const override { return nullptr; }
SceneSnapshot CaptureSceneSnapshot() const override { return {}; }
bool RestoreSceneSnapshot(const SceneSnapshot&) override { return false; }
void CreateDemoScene() override {}
XCEngine::Components::GameObject* entity = nullptr;
private:
std::vector<XCEngine::Components::GameObject*> rootEntities = {};
std::string empty;
};
class StubProjectManager : public IProjectManager {
public:
const std::vector<AssetItemPtr>& GetCurrentItems() const override { return items; }
AssetItemPtr GetRootFolder() const override { return {}; }
AssetItemPtr GetCurrentFolder() const override { return {}; }
AssetItemPtr GetSelectedItem() const override { return {}; }
const std::string& GetSelectedItemPath() const override { return empty; }
int GetSelectedIndex() const override { return -1; }
void SetSelectedIndex(int) override {}
void SetSelectedItem(const AssetItemPtr&) override {}
void ClearSelection() override {}
int FindCurrentItemIndex(const std::string&) const override { return -1; }
void NavigateToFolder(const AssetItemPtr&) override {}
void NavigateBack() override {}
void NavigateToIndex(size_t) override {}
bool CanNavigateBack() const override { return false; }
std::string GetCurrentPath() const override { return empty; }
size_t GetPathDepth() const override { return 0; }
std::string GetPathName(size_t) const override { return {}; }
void Initialize(const std::string&) override {}
void RefreshCurrentFolder() override {}
AssetItemPtr CreateFolder(const std::string&) override { return {}; }
bool DeleteItem(const std::string&) override { return false; }
bool MoveItem(const std::string&, const std::string&) override { return false; }
bool RenameItem(const std::string&, const std::string&) override { return false; }
const std::string& GetProjectPath() const override { return empty; }
private:
std::vector<AssetItemPtr> items = {};
std::string empty;
};
class StubUndoManager : public XCEngine::Editor::IUndoManager {
public:
void ClearHistory() override {}
bool CanUndo() const override { return false; }
bool CanRedo() const override { return false; }
const std::string& GetUndoLabel() const override { return empty; }
const std::string& GetRedoLabel() const override { return empty; }
void Undo() override {}
void Redo() override {}
XCEngine::Editor::UndoStateSnapshot CaptureCurrentState() const override { return {}; }
void PushCommand(const std::string&, XCEngine::Editor::UndoStateSnapshot, XCEngine::Editor::UndoStateSnapshot) override {}
void BeginInteractiveChange(const std::string&) override {}
bool HasPendingInteractiveChange() const override { return false; }
void FinalizeInteractiveChange() override {}
void CancelInteractiveChange() override {}
private:
std::string empty;
};
class StubEditorContext : public IEditorContext {
public:
EventBus& GetEventBus() override { return eventBus; }
ISelectionManager& GetSelectionManager() override { return selectionManager; }
ISceneManager& GetSceneManager() override { return sceneManager; }
IProjectManager& GetProjectManager() override { return projectManager; }
XCEngine::Editor::IUndoManager& GetUndoManager() override { return undoManager; }
IViewportHostService* GetViewportHostService() override { return viewportHostService; }
void SetActiveActionRoute(EditorActionRoute route) override { activeRoute = route; }
EditorActionRoute GetActiveActionRoute() const override { return activeRoute; }
void SetRuntimeMode(EditorRuntimeMode mode) override { runtimeMode = mode; }
EditorRuntimeMode GetRuntimeMode() const override { return runtimeMode; }
void SetProjectPath(const std::string& path) override { projectPath = path; }
const std::string& GetProjectPath() const override { return projectPath; }
EventBus eventBus = {};
StubSelectionManager selectionManager = {};
StubSceneManager sceneManager = {};
StubProjectManager projectManager = {};
StubUndoManager undoManager = {};
IViewportHostService* viewportHostService = nullptr;
EditorActionRoute activeRoute = EditorActionRoute::None;
EditorRuntimeMode runtimeMode = EditorRuntimeMode::Edit;
std::string projectPath;
};
class StubViewportHostService : public IViewportHostService {
public:
@@ -77,6 +273,55 @@ TEST(SceneViewportTransformGizmoCoordinatorTest, BuildFrameCommandMapsUpdateAndE
EXPECT_EQ(endCommand.gizmoKind, SceneViewportActiveGizmoKind::Scale);
}
TEST(SceneViewportTransformGizmoCoordinatorTest, BuildFrameOptionsCopiesVisibilityAndModes) {
const SceneViewportTransformGizmoFrameOptions options = BuildSceneViewportTransformGizmoFrameOptions(
true,
true,
false,
true,
false,
true);
EXPECT_TRUE(options.useCenterPivot);
EXPECT_TRUE(options.localSpace);
EXPECT_FALSE(options.usingTransformTool);
EXPECT_TRUE(options.showingMoveGizmo);
EXPECT_FALSE(options.showingRotateGizmo);
EXPECT_TRUE(options.showingScaleGizmo);
}
TEST(SceneViewportTransformGizmoCoordinatorTest, BuildRefreshRequestCopiesInputs) {
StubEditorContext context = {};
SceneViewportOverlayData overlay = {};
overlay.valid = true;
overlay.verticalFovDegrees = 47.0f;
const SceneViewportTransformGizmoFrameOptions options = BuildSceneViewportTransformGizmoFrameOptions(
false,
true,
true,
true,
true,
false);
const auto request = BuildSceneViewportTransformGizmoRefreshRequest(
context,
overlay,
Vector2(320.0f, 180.0f),
Vector2(25.0f, 15.0f),
options);
EXPECT_TRUE(request.IsValid());
EXPECT_EQ(request.context, &context);
EXPECT_TRUE(request.overlay.valid);
EXPECT_FLOAT_EQ(request.overlay.verticalFovDegrees, 47.0f);
EXPECT_FLOAT_EQ(request.viewportSize.x, 320.0f);
EXPECT_FLOAT_EQ(request.viewportSize.y, 180.0f);
EXPECT_FLOAT_EQ(request.mousePosition.x, 25.0f);
EXPECT_FLOAT_EQ(request.mousePosition.y, 15.0f);
EXPECT_TRUE(request.options.localSpace);
EXPECT_TRUE(request.options.showingRotateGizmo);
}
TEST(SceneViewportTransformGizmoCoordinatorTest, OverlaySubmissionBuildsAndSubmitsState) {
XCEngine::Components::GameObject gameObject("CoordinatorTestObject");
XCEngine::Editor::SceneViewportMoveGizmo moveGizmo = {};
@@ -107,3 +352,44 @@ TEST(SceneViewportTransformGizmoCoordinatorTest, OverlaySubmissionBuildsAndSubmi
EXPECT_TRUE(viewportHostService.lastSubmission.hasMoveGizmo);
EXPECT_EQ(viewportHostService.lastSubmission.moveEntityId, gameObject.GetID());
}
TEST(SceneViewportTransformGizmoCoordinatorTest, RefreshAndSubmitFrameUsesCoordinatorRequest) {
XCEngine::Components::GameObject gameObject("CoordinatorRefreshObject");
StubEditorContext context = {};
StubViewportHostService viewportHostService = {};
context.viewportHostService = &viewportHostService;
context.sceneManager.entity = &gameObject;
context.selectionManager.SetSelectedEntity(gameObject.GetID());
XCEngine::Editor::SceneViewportMoveGizmo moveGizmo = {};
XCEngine::Editor::SceneViewportRotateGizmo rotateGizmo = {};
XCEngine::Editor::SceneViewportScaleGizmo scaleGizmo = {};
const SceneViewportTransformGizmoFrameOptions options = BuildSceneViewportTransformGizmoFrameOptions(
false,
false,
false,
true,
false,
false);
const SceneViewportTransformGizmoFrameUpdate update = RefreshAndSubmitSceneViewportTransformGizmoFrame(
viewportHostService,
BuildSceneViewportTransformGizmoRefreshRequest(
context,
{},
Vector2(640.0f, 360.0f),
Vector2(48.0f, 52.0f),
options),
moveGizmo,
rotateGizmo,
scaleGizmo);
EXPECT_EQ(update.frameState.moveContext.selectedObject, &gameObject);
EXPECT_EQ(update.frameState.selectionState.primaryObject, &gameObject);
EXPECT_TRUE(update.overlaySubmission.overlayState.hasMoveGizmo);
EXPECT_EQ(update.overlaySubmission.overlayState.moveEntityId, gameObject.GetID());
EXPECT_TRUE(viewportHostService.lastSubmission.hasMoveGizmo);
EXPECT_EQ(viewportHostService.lastSubmission.moveEntityId, gameObject.GetID());
CancelSceneViewportTransformGizmoFrame(context, moveGizmo, rotateGizmo, scaleGizmo);
}