refactor(editor): isolate scene backend boundary
This commit is contained in:
@@ -67,6 +67,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
|
||||
set(EDITOR_APP_CORE_TEST_SOURCES
|
||||
test_editor_host_command_bridge.cpp
|
||||
test_editor_project_runtime.cpp
|
||||
test_editor_scene_runtime_backend.cpp
|
||||
test_editor_shell_asset_validation.cpp
|
||||
test_project_browser_model.cpp
|
||||
test_hierarchy_scene_binding.cpp
|
||||
|
||||
124
tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp
Normal file
124
tests/UI/Editor/unit/test_editor_scene_runtime_backend.cpp
Normal file
@@ -0,0 +1,124 @@
|
||||
#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(
|
||||
const std::filesystem::path& projectRoot) override {
|
||||
++ensureStartupSceneCallCount;
|
||||
lastProjectRoot = projectRoot;
|
||||
return startupSceneResult;
|
||||
}
|
||||
|
||||
Scene* GetActiveScene() const override {
|
||||
return activeScene;
|
||||
}
|
||||
|
||||
bool OpenSceneAsset(const std::filesystem::path& scenePath) override {
|
||||
lastOpenedScenePath = scenePath;
|
||||
return openSceneResult;
|
||||
}
|
||||
|
||||
GameObject* FindGameObject(std::string_view itemId) const override {
|
||||
lastFindItemId = std::string(itemId);
|
||||
return foundGameObject;
|
||||
}
|
||||
|
||||
bool RenameGameObject(
|
||||
std::string_view,
|
||||
std::string_view) override {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DeleteGameObject(std::string_view) override {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string DuplicateGameObject(std::string_view) override {
|
||||
return {};
|
||||
}
|
||||
|
||||
bool ReparentGameObject(
|
||||
std::string_view,
|
||||
std::string_view) override {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MoveGameObjectBefore(
|
||||
std::string_view,
|
||||
std::string_view) override {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MoveGameObjectAfter(
|
||||
std::string_view,
|
||||
std::string_view) override {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MoveGameObjectToRoot(std::string_view) override {
|
||||
return false;
|
||||
}
|
||||
|
||||
EditorStartupSceneResult startupSceneResult = {};
|
||||
Scene* activeScene = nullptr;
|
||||
GameObject* foundGameObject = nullptr;
|
||||
bool openSceneResult = false;
|
||||
int ensureStartupSceneCallCount = 0;
|
||||
std::filesystem::path lastProjectRoot = {};
|
||||
std::filesystem::path lastOpenedScenePath = {};
|
||||
mutable std::string lastFindItemId = {};
|
||||
};
|
||||
|
||||
TEST(EditorSceneRuntimeBackendTests, InitializeFailsWithoutBoundBackend) {
|
||||
EditorSceneRuntime runtime = {};
|
||||
EXPECT_FALSE(runtime.Initialize("D:/Xuanchi/Main/XCEngine/project"));
|
||||
}
|
||||
|
||||
TEST(EditorSceneRuntimeBackendTests, InitializeUsesBoundBackend) {
|
||||
auto backend = std::make_unique<FakeEditorSceneBackend>();
|
||||
backend->startupSceneResult.ready = true;
|
||||
backend->startupSceneResult.sceneName = "Main";
|
||||
FakeEditorSceneBackend* const backendPtr = backend.get();
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
runtime.SetBackend(std::move(backend));
|
||||
|
||||
EXPECT_TRUE(runtime.Initialize("D:/Xuanchi/Main/XCEngine/project"));
|
||||
EXPECT_EQ(backendPtr->ensureStartupSceneCallCount, 1);
|
||||
EXPECT_EQ(
|
||||
backendPtr->lastProjectRoot,
|
||||
std::filesystem::path("D:/Xuanchi/Main/XCEngine/project"));
|
||||
EXPECT_EQ(runtime.GetStartupResult().sceneName, "Main");
|
||||
}
|
||||
|
||||
TEST(EditorSceneRuntimeBackendTests, FindGameObjectUsesBoundBackend) {
|
||||
auto backend = std::make_unique<FakeEditorSceneBackend>();
|
||||
Scene scene("Main");
|
||||
GameObject probe("Probe");
|
||||
backend->startupSceneResult.ready = true;
|
||||
backend->activeScene = &scene;
|
||||
backend->foundGameObject = &probe;
|
||||
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");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "Hierarchy/HierarchyModel.h"
|
||||
#include "Scene/EditorSceneBridge.h"
|
||||
#include "Scene/EngineEditorSceneBackend.h"
|
||||
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
namespace {
|
||||
@@ -61,6 +62,10 @@ private:
|
||||
std::filesystem::path m_root = {};
|
||||
};
|
||||
|
||||
std::unique_ptr<EditorSceneBackend> CreateTestSceneBackend() {
|
||||
return CreateEngineEditorSceneBackend();
|
||||
}
|
||||
|
||||
TEST(HierarchySceneBindingTests, BuildFromSceneUsesRealGameObjectIds) {
|
||||
ScopedSceneManagerReset reset = {};
|
||||
|
||||
@@ -97,8 +102,9 @@ TEST(HierarchySceneBindingTests, DuplicateGameObjectClonesHierarchyIntoScene) {
|
||||
GameObject* child = scene->CreateGameObject("Child", root);
|
||||
ASSERT_NE(child, nullptr);
|
||||
|
||||
const std::unique_ptr<EditorSceneBackend> backend = CreateTestSceneBackend();
|
||||
const std::string duplicateId =
|
||||
DuplicateEditorGameObject(MakeEditorGameObjectItemId(root->GetID()));
|
||||
backend->DuplicateGameObject(MakeEditorGameObjectItemId(root->GetID()));
|
||||
ASSERT_FALSE(duplicateId.empty());
|
||||
|
||||
const HierarchyModel model = HierarchyModel::BuildFromScene(scene);
|
||||
@@ -124,7 +130,8 @@ TEST(HierarchySceneBindingTests, RenameGameObjectUpdatesRealSceneAndProjection)
|
||||
|
||||
const std::string itemId =
|
||||
MakeEditorGameObjectItemId(gameObject->GetID());
|
||||
ASSERT_TRUE(RenameEditorGameObject(itemId, "PlayerCamera"));
|
||||
const std::unique_ptr<EditorSceneBackend> backend = CreateTestSceneBackend();
|
||||
ASSERT_TRUE(backend->RenameGameObject(itemId, "PlayerCamera"));
|
||||
EXPECT_EQ(gameObject->GetName(), "PlayerCamera");
|
||||
|
||||
const HierarchyModel model = HierarchyModel::BuildFromScene(scene);
|
||||
@@ -147,7 +154,8 @@ TEST(HierarchySceneBindingTests, ReparentAndMoveToRootOperateOnRealScene) {
|
||||
GameObject* child = scene->CreateGameObject("Child", parentA);
|
||||
ASSERT_NE(child, nullptr);
|
||||
|
||||
ASSERT_TRUE(ReparentEditorGameObject(
|
||||
const std::unique_ptr<EditorSceneBackend> backend = CreateTestSceneBackend();
|
||||
ASSERT_TRUE(backend->ReparentGameObject(
|
||||
MakeEditorGameObjectItemId(child->GetID()),
|
||||
MakeEditorGameObjectItemId(parentB->GetID())));
|
||||
EXPECT_EQ(child->GetParent(), parentB);
|
||||
@@ -159,7 +167,7 @@ TEST(HierarchySceneBindingTests, ReparentAndMoveToRootOperateOnRealScene) {
|
||||
ASSERT_EQ(parentBNode->children.size(), 1u);
|
||||
EXPECT_EQ(parentBNode->children.front().label, "Child");
|
||||
|
||||
ASSERT_TRUE(MoveEditorGameObjectToRoot(
|
||||
ASSERT_TRUE(backend->MoveGameObjectToRoot(
|
||||
MakeEditorGameObjectItemId(child->GetID())));
|
||||
EXPECT_EQ(child->GetParent(), nullptr);
|
||||
|
||||
@@ -181,15 +189,16 @@ TEST(HierarchySceneBindingTests, EnsureStartupSceneLoadsMainSceneAndSetsActive)
|
||||
scene.Save(scenePath.string());
|
||||
}
|
||||
|
||||
const std::unique_ptr<EditorSceneBackend> backend = CreateTestSceneBackend();
|
||||
const EditorStartupSceneResult result =
|
||||
EnsureEditorStartupScene(projectRoot.Root());
|
||||
backend->EnsureStartupScene(projectRoot.Root());
|
||||
EXPECT_TRUE(result.ready);
|
||||
EXPECT_TRUE(result.loadedFromDisk);
|
||||
ASSERT_NE(GetActiveEditorScene(), nullptr);
|
||||
EXPECT_EQ(GetActiveEditorScene()->GetName(), "Main");
|
||||
ASSERT_NE(backend->GetActiveScene(), nullptr);
|
||||
EXPECT_EQ(backend->GetActiveScene()->GetName(), "Main");
|
||||
|
||||
const HierarchyModel model =
|
||||
HierarchyModel::BuildFromScene(GetActiveEditorScene());
|
||||
HierarchyModel::BuildFromScene(backend->GetActiveScene());
|
||||
EXPECT_FALSE(model.Empty());
|
||||
|
||||
SceneManager& sceneManager = SceneManager::Get();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Inspector/InspectorPresentationModel.h"
|
||||
#include "Inspector/InspectorSubject.h"
|
||||
#include "Scene/EditorSceneRuntime.h"
|
||||
#include "Scene/EngineEditorSceneBackend.h"
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
@@ -87,6 +88,10 @@ void SaveMainScene(const TemporaryProjectRoot& projectRoot) {
|
||||
scene.Save(scenePath.string());
|
||||
}
|
||||
|
||||
void BindEngineSceneBackend(EditorSceneRuntime& runtime) {
|
||||
runtime.SetBackend(CreateEngineEditorSceneBackend());
|
||||
}
|
||||
|
||||
const UIEditorPropertyGridSection* FindSection(
|
||||
const InspectorPresentationModel& model,
|
||||
std::string_view title) {
|
||||
@@ -126,6 +131,7 @@ const InspectorPresentationComponentBinding* FindBinding(
|
||||
|
||||
TEST(InspectorPresentationModelTests, EmptySubjectBuildsDefaultEmptyState) {
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
const InspectorPresentationModel model =
|
||||
BuildInspectorPresentationModel(
|
||||
{},
|
||||
@@ -150,6 +156,7 @@ TEST(InspectorPresentationModelTests, ProjectAssetSubjectBuildsIdentityAndLocati
|
||||
std::filesystem::path("D:/Xuanchi/Main/XCEngine/project/Assets/Materials/Test.mat");
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
const InspectorPresentationModel model =
|
||||
BuildInspectorPresentationModel(
|
||||
subject,
|
||||
@@ -190,6 +197,7 @@ TEST(InspectorPresentationModelTests, SceneObjectSubjectBuildsRegisteredComponen
|
||||
SaveMainScene(projectRoot);
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
@@ -261,6 +269,7 @@ TEST(InspectorPresentationModelTests, CameraSkyboxMaterialBuildsAssetField) {
|
||||
SaveMainScene(projectRoot);
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "Scene/EditorSceneRuntime.h"
|
||||
#include "Scene/EngineEditorSceneBackend.h"
|
||||
#include "Scene/SceneViewportController.h"
|
||||
#include "Inspector/InspectorSubject.h"
|
||||
#include "Viewport/SceneViewportRenderService.h"
|
||||
@@ -106,6 +107,10 @@ void SaveMainScene(const TemporaryProjectRoot& projectRoot, const Math::Vector3&
|
||||
scene.Save(scenePath.string());
|
||||
}
|
||||
|
||||
void BindEngineSceneBackend(EditorSceneRuntime& runtime) {
|
||||
runtime.SetBackend(CreateEngineEditorSceneBackend());
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerEvent(
|
||||
UIInputEventType type,
|
||||
float x,
|
||||
@@ -207,6 +212,7 @@ TEST(SceneViewportRuntimeTests, ApplySceneViewportCameraInputUpdatesCameraTransf
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
|
||||
auto* camera = runtime.GetSceneViewCamera();
|
||||
@@ -233,6 +239,7 @@ TEST(SceneViewportRuntimeTests, FocusSceneSelectionRepositionsCameraAroundSelect
|
||||
SaveMainScene(projectRoot, Math::Vector3(12.0f, 3.0f, -8.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
@@ -261,6 +268,7 @@ TEST(SceneViewportRuntimeTests, BuildSceneViewportRenderRequestIncludesSelectedO
|
||||
SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
@@ -284,6 +292,7 @@ TEST(SceneViewportRuntimeTests, SelectedComponentsExposeTransformAndAttachedCame
|
||||
SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
@@ -314,6 +323,7 @@ TEST(SceneViewportRuntimeTests, RemoveSelectedComponentDropsRemovableDescriptorB
|
||||
SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
Scene* scene = runtime.GetActiveScene();
|
||||
ASSERT_NE(scene, nullptr);
|
||||
@@ -340,6 +350,7 @@ TEST(SceneViewportRuntimeTests, TransformSetterApisWriteLocalValuesOnSelectedTra
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.EnsureSceneSelection();
|
||||
|
||||
@@ -380,6 +391,7 @@ TEST(SceneViewportRuntimeTests, SelectionStampAdvancesOnSceneSelectionChanges) {
|
||||
SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.EnsureSceneSelection();
|
||||
|
||||
@@ -409,6 +421,7 @@ TEST(SceneViewportRuntimeTests, InspectorSelectionResolverFollowsUnifiedSelectio
|
||||
|
||||
EditorSelectionService selectionService = {};
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.BindSelectionService(&selectionService);
|
||||
runtime.EnsureSceneSelection();
|
||||
@@ -476,6 +489,7 @@ TEST(SceneViewportRuntimeTests, RightMouseDragRotatesSceneCameraThroughViewportC
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
|
||||
auto* camera = runtime.GetSceneViewCamera();
|
||||
@@ -536,6 +550,7 @@ TEST(SceneViewportRuntimeTests, MoveRightInputMovesSceneCameraTowardPositiveCame
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
|
||||
auto* camera = runtime.GetSceneViewCamera();
|
||||
@@ -560,6 +575,7 @@ TEST(SceneViewportRuntimeTests, MiddleMouseDragPansSceneCameraWithGrabSemantics)
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
|
||||
auto* camera = runtime.GetSceneViewCamera();
|
||||
@@ -618,6 +634,7 @@ TEST(SceneViewportRuntimeTests, ViewToolLeftMouseDragPansSceneCameraWithGrabSema
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.SetToolMode(SceneToolMode::View);
|
||||
|
||||
@@ -677,6 +694,7 @@ TEST(SceneViewportRuntimeTests, MouseWheelUsesSingleNotchNormalizationForSceneZo
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
const float beforeDistance =
|
||||
runtime.BuildSceneViewportRenderRequest().orbitDistance;
|
||||
@@ -723,6 +741,7 @@ TEST(SceneViewportRuntimeTests, ToolShortcutSwitchesFocusedSceneViewportIntoTran
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.SetToolMode(SceneToolMode::View);
|
||||
EXPECT_EQ(runtime.GetToolMode(), SceneToolMode::View);
|
||||
@@ -781,6 +800,7 @@ TEST(SceneViewportRuntimeTests, SceneToolOverlayClickSwitchesModeOnPointerDown)
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.SetToolMode(SceneToolMode::Translate);
|
||||
|
||||
@@ -819,6 +839,7 @@ TEST(SceneViewportRuntimeTests, SceneToolOverlayIncludesTransformButtonAndSwitch
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.SetToolMode(SceneToolMode::Translate);
|
||||
|
||||
@@ -860,6 +881,7 @@ TEST(SceneViewportRuntimeTests, SceneToolOverlayHandlesCoalescedClickInSingleFra
|
||||
SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f));
|
||||
|
||||
EditorSceneRuntime runtime = {};
|
||||
BindEngineSceneBackend(runtime);
|
||||
ASSERT_TRUE(runtime.Initialize(projectRoot.Root()));
|
||||
runtime.SetToolMode(SceneToolMode::Translate);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user