refactor(editor): isolate scene backend boundary
This commit is contained in:
420
editor/app/Services/Scene/EngineEditorSceneBackend.cpp
Normal file
420
editor/app/Services/Scene/EngineEditorSceneBackend.cpp
Normal file
@@ -0,0 +1,420 @@
|
||||
#include "Scene/EngineEditorSceneBackend.h"
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
#include <XCEngine/Components/ComponentFactoryRegistry.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Components/TransformComponent.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
#include <XCEngine/Scene/SceneManager.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::Components::Component;
|
||||
using ::XCEngine::Components::ComponentFactoryRegistry;
|
||||
using ::XCEngine::Components::GameObject;
|
||||
using ::XCEngine::Components::Scene;
|
||||
using ::XCEngine::Components::SceneManager;
|
||||
using ::XCEngine::Components::TransformComponent;
|
||||
using ::XCEngine::Resources::ResourceManager;
|
||||
|
||||
void TraceSceneStartup(std::string message) {
|
||||
::XCEngine::UI::Editor::AppendUIEditorRuntimeTrace("startup", std::move(message));
|
||||
}
|
||||
|
||||
struct ClipboardNode {
|
||||
std::string name = {};
|
||||
std::string transformPayload = {};
|
||||
std::vector<std::pair<std::string, std::string>> components = {};
|
||||
std::vector<ClipboardNode> children = {};
|
||||
};
|
||||
|
||||
Scene* ResolvePrimaryScene() {
|
||||
SceneManager& sceneManager = SceneManager::Get();
|
||||
if (Scene* activeScene = sceneManager.GetActiveScene();
|
||||
activeScene != nullptr) {
|
||||
return activeScene;
|
||||
}
|
||||
|
||||
const std::vector<Scene*> scenes = sceneManager.GetAllScenes();
|
||||
if (!scenes.empty() && scenes.front() != nullptr) {
|
||||
sceneManager.SetActiveScene(scenes.front());
|
||||
return scenes.front();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string> SerializeComponent(
|
||||
const Component* component) {
|
||||
std::ostringstream payload = {};
|
||||
component->Serialize(payload);
|
||||
return { component->GetName(), payload.str() };
|
||||
}
|
||||
|
||||
ClipboardNode CopyGameObjectRecursive(const GameObject& gameObject) {
|
||||
ClipboardNode node = {};
|
||||
node.name = gameObject.GetName();
|
||||
|
||||
if (const TransformComponent* transform = gameObject.GetTransform();
|
||||
transform != nullptr) {
|
||||
std::ostringstream payload = {};
|
||||
transform->Serialize(payload);
|
||||
node.transformPayload = payload.str();
|
||||
}
|
||||
|
||||
const auto components = gameObject.GetComponents<Component>();
|
||||
for (const Component* component : components) {
|
||||
if (component == nullptr || component == gameObject.GetTransform()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
node.components.push_back(SerializeComponent(component));
|
||||
}
|
||||
|
||||
for (GameObject* child : gameObject.GetChildren()) {
|
||||
if (child == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
node.children.push_back(CopyGameObjectRecursive(*child));
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
GameObject* PasteGameObjectRecursive(
|
||||
Scene& scene,
|
||||
const ClipboardNode& node,
|
||||
GameObject* parent) {
|
||||
GameObject* gameObject = scene.CreateGameObject(node.name, parent);
|
||||
if (gameObject == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!node.transformPayload.empty()) {
|
||||
if (TransformComponent* transform = gameObject->GetTransform();
|
||||
transform != nullptr) {
|
||||
std::istringstream transformStream(node.transformPayload);
|
||||
transform->Deserialize(transformStream);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& componentData : node.components) {
|
||||
Component* component =
|
||||
ComponentFactoryRegistry::Get().CreateComponent(
|
||||
gameObject,
|
||||
componentData.first);
|
||||
if (component == nullptr || componentData.second.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::istringstream payloadStream(componentData.second);
|
||||
component->Deserialize(payloadStream);
|
||||
}
|
||||
|
||||
for (const ClipboardNode& child : node.children) {
|
||||
PasteGameObjectRecursive(scene, child, gameObject);
|
||||
}
|
||||
|
||||
return gameObject;
|
||||
}
|
||||
|
||||
bool WouldCreateCycle(
|
||||
const GameObject& source,
|
||||
const GameObject& targetParent) {
|
||||
const GameObject* current = &targetParent;
|
||||
while (current != nullptr) {
|
||||
if (current == &source) {
|
||||
return true;
|
||||
}
|
||||
current = current->GetParent();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<GameObject*> ResolveSiblingSequence(
|
||||
Scene& scene,
|
||||
GameObject* parent) {
|
||||
return parent != nullptr
|
||||
? parent->GetChildren()
|
||||
: scene.GetRootGameObjects();
|
||||
}
|
||||
|
||||
std::optional<std::size_t> FindSiblingIndex(
|
||||
const std::vector<GameObject*>& siblings,
|
||||
const GameObject* gameObject) {
|
||||
if (gameObject == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto it =
|
||||
std::find(siblings.begin(), siblings.end(), gameObject);
|
||||
if (it == siblings.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return static_cast<std::size_t>(std::distance(siblings.begin(), it));
|
||||
}
|
||||
|
||||
GameObject* FindGameObjectByItemId(std::string_view itemId) {
|
||||
Scene* scene = ResolvePrimaryScene();
|
||||
const std::optional<GameObject::ID> gameObjectId =
|
||||
ParseEditorGameObjectItemId(itemId);
|
||||
if (scene == nullptr || !gameObjectId.has_value()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return scene->FindByID(gameObjectId.value());
|
||||
}
|
||||
|
||||
bool MoveGameObjectRelativeToTarget(
|
||||
std::string_view itemId,
|
||||
std::string_view targetItemId,
|
||||
bool placeAfterTarget) {
|
||||
Scene* scene = ResolvePrimaryScene();
|
||||
GameObject* gameObject = FindGameObjectByItemId(itemId);
|
||||
GameObject* target = FindGameObjectByItemId(targetItemId);
|
||||
if (scene == nullptr ||
|
||||
gameObject == nullptr ||
|
||||
target == nullptr ||
|
||||
gameObject == target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GameObject* targetParent = target->GetParent();
|
||||
if (targetParent != nullptr &&
|
||||
WouldCreateCycle(*gameObject, *targetParent)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::vector<GameObject*> siblings =
|
||||
ResolveSiblingSequence(*scene, targetParent);
|
||||
const std::optional<std::size_t> targetIndex =
|
||||
FindSiblingIndex(siblings, target);
|
||||
if (!targetIndex.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t finalIndex =
|
||||
targetIndex.value() + (placeAfterTarget ? 1u : 0u);
|
||||
if (gameObject->GetParent() == targetParent) {
|
||||
const std::optional<std::size_t> sourceIndex =
|
||||
FindSiblingIndex(siblings, gameObject);
|
||||
if (sourceIndex.has_value()) {
|
||||
if ((!placeAfterTarget && sourceIndex.value() < targetIndex.value()) ||
|
||||
(placeAfterTarget && sourceIndex.value() <= targetIndex.value())) {
|
||||
--finalIndex;
|
||||
}
|
||||
|
||||
if (finalIndex == sourceIndex.value()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gameObject->SetParent(targetParent, finalIndex, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
class EngineEditorSceneBackend final : public EditorSceneBackend {
|
||||
public:
|
||||
EditorStartupSceneResult EnsureStartupScene(
|
||||
const std::filesystem::path& projectRoot) override {
|
||||
EditorStartupSceneResult result = {};
|
||||
TraceSceneStartup("EnsureStartupScene begin projectRoot=" + projectRoot.string());
|
||||
if (projectRoot.empty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
ResourceManager::Get().SetResourceRoot(projectRoot.string().c_str());
|
||||
TraceSceneStartup("ResourceManager::SetResourceRoot complete");
|
||||
|
||||
if (Scene* activeScene = ResolvePrimaryScene();
|
||||
activeScene != nullptr) {
|
||||
result.ready = true;
|
||||
result.sceneName = activeScene->GetName();
|
||||
TraceSceneStartup("EnsureStartupScene reused active scene=" + result.sceneName);
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::filesystem::path startupScenePath =
|
||||
(projectRoot / "Assets" / "Scenes" / "Main.xc").lexically_normal();
|
||||
SceneManager& sceneManager = SceneManager::Get();
|
||||
|
||||
if (std::filesystem::exists(startupScenePath) &&
|
||||
std::filesystem::is_regular_file(startupScenePath)) {
|
||||
TraceSceneStartup(
|
||||
"SceneManager::LoadScene begin path=" + startupScenePath.string());
|
||||
{
|
||||
ResourceManager::ScopedDeferredSceneLoad deferredSceneLoad(
|
||||
ResourceManager::Get());
|
||||
sceneManager.LoadScene(startupScenePath.string());
|
||||
}
|
||||
TraceSceneStartup("SceneManager::LoadScene end");
|
||||
Scene* loadedScene =
|
||||
sceneManager.GetScene(startupScenePath.stem().string());
|
||||
if (loadedScene == nullptr) {
|
||||
loadedScene = ResolvePrimaryScene();
|
||||
} else {
|
||||
sceneManager.SetActiveScene(loadedScene);
|
||||
}
|
||||
|
||||
if (loadedScene != nullptr) {
|
||||
result.ready = true;
|
||||
result.loadedFromDisk = true;
|
||||
result.scenePath = startupScenePath;
|
||||
result.sceneName = loadedScene->GetName();
|
||||
TraceSceneStartup(
|
||||
"EnsureStartupScene loaded scene=" + result.sceneName);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (Scene* scene = sceneManager.CreateScene("Main");
|
||||
scene != nullptr) {
|
||||
sceneManager.SetActiveScene(scene);
|
||||
result.ready = true;
|
||||
result.sceneName = scene->GetName();
|
||||
TraceSceneStartup(
|
||||
"EnsureStartupScene created scene=" + result.sceneName);
|
||||
}
|
||||
|
||||
TraceSceneStartup(
|
||||
std::string("EnsureStartupScene end ready=") +
|
||||
(result.ready ? "1" : "0"));
|
||||
return result;
|
||||
}
|
||||
|
||||
Scene* GetActiveScene() const override {
|
||||
return ResolvePrimaryScene();
|
||||
}
|
||||
|
||||
bool OpenSceneAsset(const std::filesystem::path& scenePath) override {
|
||||
if (scenePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code errorCode = {};
|
||||
if (!std::filesystem::exists(scenePath, errorCode) ||
|
||||
errorCode ||
|
||||
!std::filesystem::is_regular_file(scenePath, errorCode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SceneManager& sceneManager = SceneManager::Get();
|
||||
{
|
||||
ResourceManager::ScopedDeferredSceneLoad deferredSceneLoad(
|
||||
ResourceManager::Get());
|
||||
sceneManager.LoadScene(scenePath.string());
|
||||
}
|
||||
Scene* loadedScene = sceneManager.GetScene(scenePath.stem().string());
|
||||
if (loadedScene == nullptr) {
|
||||
loadedScene = ResolvePrimaryScene();
|
||||
} else {
|
||||
sceneManager.SetActiveScene(loadedScene);
|
||||
}
|
||||
|
||||
return loadedScene != nullptr;
|
||||
}
|
||||
|
||||
GameObject* FindGameObject(std::string_view itemId) const override {
|
||||
return FindGameObjectByItemId(itemId);
|
||||
}
|
||||
|
||||
bool RenameGameObject(
|
||||
std::string_view itemId,
|
||||
std::string_view newName) override {
|
||||
GameObject* gameObject = FindGameObject(itemId);
|
||||
if (gameObject == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
gameObject->SetName(std::string(newName));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeleteGameObject(std::string_view itemId) override {
|
||||
Scene* scene = ResolvePrimaryScene();
|
||||
GameObject* gameObject = FindGameObject(itemId);
|
||||
if (scene == nullptr || gameObject == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
scene->DestroyGameObject(gameObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string DuplicateGameObject(std::string_view itemId) override {
|
||||
Scene* scene = ResolvePrimaryScene();
|
||||
GameObject* gameObject = FindGameObject(itemId);
|
||||
if (scene == nullptr || gameObject == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ClipboardNode clipboard = CopyGameObjectRecursive(*gameObject);
|
||||
GameObject* duplicate =
|
||||
PasteGameObjectRecursive(*scene, clipboard, gameObject->GetParent());
|
||||
return duplicate != nullptr
|
||||
? MakeEditorGameObjectItemId(duplicate->GetID())
|
||||
: std::string();
|
||||
}
|
||||
|
||||
bool ReparentGameObject(
|
||||
std::string_view itemId,
|
||||
std::string_view parentItemId) override {
|
||||
GameObject* gameObject = FindGameObject(itemId);
|
||||
GameObject* newParent = FindGameObject(parentItemId);
|
||||
if (gameObject == nullptr || newParent == nullptr ||
|
||||
gameObject == newParent ||
|
||||
gameObject->GetParent() == newParent ||
|
||||
WouldCreateCycle(*gameObject, *newParent)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
gameObject->SetParent(newParent);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoveGameObjectBefore(
|
||||
std::string_view itemId,
|
||||
std::string_view targetItemId) override {
|
||||
return MoveGameObjectRelativeToTarget(itemId, targetItemId, false);
|
||||
}
|
||||
|
||||
bool MoveGameObjectAfter(
|
||||
std::string_view itemId,
|
||||
std::string_view targetItemId) override {
|
||||
return MoveGameObjectRelativeToTarget(itemId, targetItemId, true);
|
||||
}
|
||||
|
||||
bool MoveGameObjectToRoot(std::string_view itemId) override {
|
||||
GameObject* gameObject = FindGameObject(itemId);
|
||||
if (gameObject == nullptr || gameObject->GetParent() == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
gameObject->SetParent(nullptr);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<EditorSceneBackend> CreateEngineEditorSceneBackend() {
|
||||
return std::make_unique<EngineEditorSceneBackend>();
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
Reference in New Issue
Block a user