diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index f2b9944..d5d2585 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -3,6 +3,7 @@ set(CMAKE_CXX_STANDARD 17) find_package(OGRE REQUIRED COMPONENTS Bites Overlay CONFIG) find_package(flecs REQUIRED CONFIG) +find_package(nlohmann_json REQUIRED) find_package(SDL2 REQUIRED) # Collect all source files @@ -10,6 +11,7 @@ set(EDITSCENE_SOURCES main.cpp EditorApp.cpp systems/EditorUISystem.cpp + systems/SceneSerializer.cpp ui/TransformEditor.cpp ui/RenderableEditor.cpp camera/EditorCamera.cpp @@ -23,6 +25,7 @@ set(EDITSCENE_HEADERS components/EntityName.hpp components/Relationship.hpp systems/EditorUISystem.hpp + systems/SceneSerializer.hpp ui/ComponentEditor.hpp ui/ComponentRegistry.hpp ui/TransformEditor.hpp @@ -38,6 +41,7 @@ target_link_libraries(editSceneEditor OgreBites OgreOverlay flecs::flecs_static + nlohmann_json::nlohmann_json ) target_include_directories(editSceneEditor PRIVATE diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index b99c251..9e6bd26 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -64,7 +64,7 @@ void EditorApp::setup() // Base setup OgreBites::ApplicationContext::setup(); - // Get root and create scene manager + // Create scene manager Ogre::Root *root = getRoot(); m_sceneMgr = root->createSceneManager(); m_sceneMgr->setAmbientLight(Ogre::ColourValue(0.3f, 0.3f, 0.3f)); @@ -129,22 +129,7 @@ void EditorApp::setupECS() void EditorApp::createDefaultEntities() { - // Create root entity - flecs::entity root = m_world.entity("Root"); - root.set(EntityNameComponent("Root")); - root.add(); - - // Create child using flecs::ChildOf relationship - flecs::entity child1 = m_world.entity("Child1"); - child1.set(EntityNameComponent("Child 1")); - child1.add(); - child1.child_of(root); - - // Create grandchild using flecs::ChildOf relationship - flecs::entity grandchild = m_world.entity("Grandchild"); - grandchild.set(EntityNameComponent("Grandchild")); - grandchild.add(); - grandchild.child_of(child1); + // Start with empty scene - user creates entities as needed } void EditorApp::setupLights() diff --git a/src/features/editScene/gizmo/Gizmo.cpp b/src/features/editScene/gizmo/Gizmo.cpp index 3deccc6..3243ed1 100644 --- a/src/features/editScene/gizmo/Gizmo.cpp +++ b/src/features/editScene/gizmo/Gizmo.cpp @@ -79,8 +79,9 @@ void Gizmo::update() auto &transform = m_attachedEntity.get(); if (transform.node) { - m_gizmoNode->setPosition(transform.position); - m_gizmoNode->setOrientation(transform.rotation); + // Use derived (world) position and orientation + m_gizmoNode->setPosition(transform.node->_getDerivedPosition()); + m_gizmoNode->setOrientation(transform.node->_getDerivedOrientation()); m_gizmoNode->setVisible(true); } } @@ -192,8 +193,11 @@ bool Gizmo::onMousePressed(const Ogre::Ray &mouseRay) m_isDragging = true; auto &transform = m_attachedEntity.get_mut(); - m_dragStartPosition = transform.position; + // Use derived (world) position for dragging + m_dragStartPosition = transform.node->_getDerivedPosition(); + + // Get axis direction in world space from gizmo orientation switch (m_selectedAxis) { case Axis::X: m_dragAxisDir = m_gizmoNode->getOrientation() * Ogre::Vector3::UNIT_X; break; case Axis::Y: m_dragAxisDir = m_gizmoNode->getOrientation() * Ogre::Vector3::UNIT_Y; break; @@ -224,10 +228,20 @@ bool Gizmo::onMouseMoved(const Ogre::Ray &mouseRay, const Ogre::Vector2 &mouseDe float deltaT = currentT - m_dragStartT; - transform.position = m_dragStartPosition + m_dragAxisDir * deltaT; + // Calculate new world position + Ogre::Vector3 newWorldPos = m_dragStartPosition + m_dragAxisDir * deltaT; + + // Convert to local position if node has a parent + if (transform.node->getParent()) { + transform.position = transform.node->getParent()->convertWorldToLocalPosition(newWorldPos); + } else { + transform.position = newWorldPos; + } + transform.applyToNode(); - m_gizmoNode->setPosition(transform.position); + // Update gizmo to follow (use derived position) + m_gizmoNode->setPosition(transform.node->_getDerivedPosition()); return true; } else if (m_axisX->isVisible()) { diff --git a/src/features/editScene/resources.cfg b/src/features/editScene/resources.cfg index 6cfb5b8..160de71 100644 --- a/src/features/editScene/resources.cfg +++ b/src/features/editScene/resources.cfg @@ -3,6 +3,7 @@ [General] FileSystem=resources FileSystem=resources/materials +FileSystem=resources/materials/scripts FileSystem=resources/meshes FileSystem=resources/textures FileSystem=resources/buildings @@ -10,7 +11,6 @@ FileSystem=resources/vehicles [Popular] FileSystem=resources/materials/programs -FileSystem=resources/materials/scripts FileSystem=resources/materials/textures [Essential] diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 5c7346a..f6668c4 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -17,6 +17,7 @@ EditorUISystem::EditorUISystem(flecs::world &world, { registerComponentEditors(); m_gizmo = std::make_unique(m_sceneMgr); + m_serializer = std::make_unique(m_world, m_sceneMgr); } EditorUISystem::~EditorUISystem() = default; @@ -146,6 +147,18 @@ void EditorUISystem::renderHierarchyWindow() if (ImGui::Begin("Entity Hierarchy", nullptr, windowFlags)) { // Menu bar if (ImGui::BeginMenuBar()) { + // File menu + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Save Scene", "Ctrl+S")) { + saveScene("scene.json"); + } + if (ImGui::MenuItem("Load Scene", "Ctrl+O")) { + loadScene("scene.json"); + } + ImGui::EndMenu(); + } + + // Entity menu if (ImGui::BeginMenu("Entity")) { if (ImGui::MenuItem("New Entity", "Ctrl+N")) { createNewEntity(); @@ -170,6 +183,17 @@ void EditorUISystem::renderHierarchyWindow() } ImGui::EndMenu(); } + + // Settings menu + if (ImGui::BeginMenu("Settings")) { + if (ImGui::Checkbox("Parent SceneNodes", &m_parentSceneNodes)) { + // Toggle applied immediately to new entities + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("When enabled, child entities' SceneNodes are parented to their parent's SceneNode, inheriting transforms. When disabled, SceneNodes are created at root level."); + } + ImGui::EndMenu(); + } ImGui::EndMenuBar(); } @@ -484,11 +508,21 @@ void EditorUISystem::createChildEntity(flecs::entity parent) entity.set(EntityNameComponent("Child Entity")); entity.add(); - // Create transform with parent as scene node parent + // Create transform TransformComponent transform; auto &parentTransform = parent.get_mut(); - transform.node = parentTransform.node->createChildSceneNode(); - transform.position = Ogre::Vector3::ZERO; + + // SceneNode parenting depends on setting + if (m_parentSceneNodes) { + // Child SceneNode inherits parent's transform + transform.node = parentTransform.node->createChildSceneNode(); + transform.position = Ogre::Vector3::ZERO; + } else { + // Child SceneNode at root level, position relative to parent + transform.node = m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + transform.position = parentTransform.position; + } + transform.rotation = Ogre::Quaternion::IDENTITY; transform.scale = Ogre::Vector3::UNIT_SCALE; entity.set(transform); @@ -561,18 +595,26 @@ void EditorUISystem::duplicateEntity(flecs::entity entity) auto &oldTransform = entity.get(); TransformComponent newTransform; - // Find parent node - Ogre::SceneNode *parentNode = m_sceneMgr->getRootSceneNode(); + // Find parent entity and node flecs::entity parent = entity.parent(); + Ogre::SceneNode *parentNode = m_sceneMgr->getRootSceneNode(); + if (parent.is_valid() && parent != 0 && parent.has()) { parentNode = parent.get().node; } - newTransform.node = parentNode->createChildSceneNode(); - newTransform.position = - oldTransform.position + - Ogre::Vector3(1, 0, 0); // Offset slightly + // SceneNode parenting depends on setting + if (m_parentSceneNodes && parent.is_valid() && parent != 0) { + // Create as child of parent's SceneNode + newTransform.node = parentNode->createChildSceneNode(); + newTransform.position = oldTransform.position + Ogre::Vector3(1, 0, 0); + } else { + // Create at root level + newTransform.node = m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + newTransform.position = oldTransform.position + Ogre::Vector3(1, 0, 0); + } + newTransform.rotation = oldTransform.rotation; newTransform.scale = oldTransform.scale; newTransform.applyToNode(); @@ -635,3 +677,29 @@ bool EditorUISystem::isDescendantOf(flecs::entity potentialChild, } return false; } + +void EditorUISystem::saveScene(const std::string& filepath) +{ + if (!m_serializer) return; + + if (m_serializer->saveToFile(filepath)) { + Ogre::LogManager::getSingleton().logMessage( + "Scene saved to: " + filepath); + } else { + Ogre::LogManager::getSingleton().logMessage( + "Failed to save scene: " + m_serializer->getLastError()); + } +} + +void EditorUISystem::loadScene(const std::string& filepath) +{ + if (!m_serializer) return; + + if (m_serializer->loadFromFile(filepath, this)) { + Ogre::LogManager::getSingleton().logMessage( + "Scene loaded from: " + filepath); + } else { + Ogre::LogManager::getSingleton().logMessage( + "Failed to load scene: " + m_serializer->getLastError()); + } +} diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index dfb214e..503fe47 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -4,9 +4,11 @@ #include #include #include +#include #include "../ui/ComponentRegistry.hpp" #include "../components/EntityName.hpp" #include "../gizmo/Gizmo.hpp" +#include "SceneSerializer.hpp" /** * Main UI system for the scene editor @@ -57,7 +59,25 @@ public: */ Gizmo *getGizmo() const { return m_gizmo.get(); } + /** + * Get/set SceneNode parenting mode + */ + bool getParentSceneNodes() const { return m_parentSceneNodes; } + void setParentSceneNodes(bool value) { m_parentSceneNodes = value; } + + /** + * Save scene to file + */ + void saveScene(const std::string& filepath); + + /** + * Load scene from file + */ + void loadScene(const std::string& filepath); + private: + // File menu + void renderFileMenu(); // Window rendering functions void renderHierarchyWindow(); void renderPropertyWindow(); @@ -91,6 +111,10 @@ private: ComponentRegistry m_componentRegistry; std::vector m_allEntities; std::unique_ptr m_gizmo; + std::unique_ptr m_serializer; + + // Settings + bool m_parentSceneNodes = true; // Whether child entities inherit parent's SceneNode // Queries flecs::query m_nameQuery; diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp new file mode 100644 index 0000000..f8f2ad0 --- /dev/null +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -0,0 +1,285 @@ +#include "SceneSerializer.hpp" +#include "../components/Transform.hpp" +#include "../components/Renderable.hpp" +#include "../components/EntityName.hpp" +#include "../components/EditorMarker.hpp" +#include "EditorUISystem.hpp" +#include +#include + +SceneSerializer::SceneSerializer(flecs::world& world, Ogre::SceneManager* sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) +{ +} + +bool SceneSerializer::saveToFile(const std::string& filepath) +{ + try { + nlohmann::json scene; + scene["version"] = "1.0"; + scene["entities"] = nlohmann::json::array(); + + // Collect all entities with EditorMarkerComponent + m_world.query_builder<>() + .with() + .build() + .each([&](flecs::entity entity) { + // Only save root entities (children will be saved recursively) + flecs::entity parent = entity.parent(); + if (!parent.is_valid() || parent == 0) { + scene["entities"].push_back(serializeEntity(entity)); + } + }); + + // Write to file + std::ofstream file(filepath); + if (!file.is_open()) { + m_lastError = "Failed to open file for writing: " + filepath; + return false; + } + + file << scene.dump(4); // Pretty print with 4-space indent + file.close(); + + return true; + } catch (const std::exception& e) { + m_lastError = std::string("Save error: ") + e.what(); + return false; + } +} + +bool SceneSerializer::loadFromFile(const std::string& filepath, EditorUISystem* uiSystem) +{ + try { + // Read from file + std::ifstream file(filepath); + if (!file.is_open()) { + m_lastError = "Failed to open file for reading: " + filepath; + return false; + } + + nlohmann::json scene; + file >> scene; + file.close(); + + // Clear existing entities (optional - could be made configurable) + // For now, we'll just add to the existing scene + + // Validate version + if (scene.contains("version")) { + std::string version = scene["version"]; + if (version != "1.0") { + m_lastError = "Unsupported scene version: " + version; + return false; + } + } + + // Clear entity map for new load + m_entityMap.clear(); + + // Load entities + if (scene.contains("entities") && scene["entities"].is_array()) { + for (const auto& entityJson : scene["entities"]) { + deserializeEntity(entityJson, flecs::entity::null(), uiSystem); + } + } + + m_entityMap.clear(); + return true; + } catch (const std::exception& e) { + m_lastError = std::string("Load error: ") + e.what(); + return false; + } +} + +nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) +{ + nlohmann::json json; + + // Store entity ID for parent/child relationships + json["id"] = entity.id(); + + // Serialize components + if (entity.has()) { + json["name"] = serializeEntityName(entity); + } + + if (entity.has()) { + json["transform"] = serializeTransform(entity); + } + + if (entity.has()) { + json["renderable"] = serializeRenderable(entity); + } + + // Serialize children + json["children"] = nlohmann::json::array(); + entity.children([&](flecs::entity child) { + if (child.has()) { + json["children"].push_back(serializeEntity(child)); + } + }); + + return json; +} + +void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entity parent, EditorUISystem* uiSystem) +{ + // Create new entity + flecs::entity entity = m_world.entity(); + entity.add(); + + // Store in map for potential future reference + if (json.contains("id")) { + uint64_t id = json["id"]; + m_entityMap[id] = entity; + } + + // Set parent relationship + if (parent.is_valid() && parent != 0) { + entity.child_of(parent); + } + + // Deserialize components + if (json.contains("name")) { + deserializeEntityName(entity, json["name"]); + } else { + entity.set(EntityNameComponent("Entity")); + } + + if (json.contains("transform")) { + deserializeTransform(entity, json["transform"], parent); + } + + if (json.contains("renderable")) { + deserializeRenderable(entity, json["renderable"]); + } + + // Add to UI system if provided + if (uiSystem) { + uiSystem->addEntity(entity); + } + + // Deserialize children + if (json.contains("children") && json["children"].is_array()) { + for (const auto& childJson : json["children"]) { + deserializeEntity(childJson, entity, uiSystem); + } + } +} + +nlohmann::json SceneSerializer::serializeTransform(flecs::entity entity) +{ + auto& transform = entity.get(); + nlohmann::json json; + + json["position"] = { + {"x", transform.position.x}, + {"y", transform.position.y}, + {"z", transform.position.z} + }; + + json["rotation"] = { + {"w", transform.rotation.w}, + {"x", transform.rotation.x}, + {"y", transform.rotation.y}, + {"z", transform.rotation.z} + }; + + json["scale"] = { + {"x", transform.scale.x}, + {"y", transform.scale.y}, + {"z", transform.scale.z} + }; + + return json; +} + +nlohmann::json SceneSerializer::serializeRenderable(flecs::entity entity) +{ + auto& renderable = entity.get(); + nlohmann::json json; + json["meshName"] = renderable.meshName; + json["visible"] = renderable.visible; + return json; +} + +nlohmann::json SceneSerializer::serializeEntityName(flecs::entity entity) +{ + auto& name = entity.get(); + nlohmann::json json; + json["name"] = name.name; + return json; +} + +void SceneSerializer::deserializeTransform(flecs::entity entity, const nlohmann::json& json, flecs::entity parentEntity) +{ + TransformComponent transform; + + // Read position + if (json.contains("position")) { + auto& pos = json["position"]; + transform.position = Ogre::Vector3( + pos.value("x", 0.0f), + pos.value("y", 0.0f), + pos.value("z", 0.0f) + ); + } + + // Read rotation + if (json.contains("rotation")) { + auto& rot = json["rotation"]; + transform.rotation = Ogre::Quaternion( + rot.value("w", 1.0f), + rot.value("x", 0.0f), + rot.value("y", 0.0f), + rot.value("z", 0.0f) + ); + } + + // Read scale + if (json.contains("scale")) { + auto& scl = json["scale"]; + transform.scale = Ogre::Vector3( + scl.value("x", 1.0f), + scl.value("y", 1.0f), + scl.value("z", 1.0f) + ); + } + + // Create scene node + if (parentEntity.is_valid() && parentEntity != 0 && parentEntity.has()) { + // Child of parent entity's node + auto& parentTransform = parentEntity.get(); + if (parentTransform.node) { + transform.node = parentTransform.node->createChildSceneNode(); + } else { + transform.node = m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + } + } else { + // Root level + transform.node = m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + } + + transform.applyToNode(); + entity.set(transform); +} + +void SceneSerializer::deserializeRenderable(flecs::entity entity, const nlohmann::json& json) +{ + RenderableComponent renderable; + renderable.meshName = json.value("meshName", ""); + renderable.visible = json.value("visible", true); + + // Don't create the Ogre::Entity here - it will be created when mesh is loaded + renderable.entity = nullptr; + + entity.set(renderable); +} + +void SceneSerializer::deserializeEntityName(flecs::entity entity, const nlohmann::json& json) +{ + std::string name = json.value("name", "Entity"); + entity.set(EntityNameComponent(name)); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp new file mode 100644 index 0000000..7377d95 --- /dev/null +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -0,0 +1,59 @@ +#ifndef EDITSCENE_SCENESERIALIZER_HPP +#define EDITSCENE_SCENESERIALIZER_HPP + +#pragma once +#include +#include +#include +#include +#include + +// Forward declarations +class EditorUISystem; + +/** + * Scene serializer for saving/loading scenes in JSON format + */ +class SceneSerializer { +public: + SceneSerializer(flecs::world& world, Ogre::SceneManager* sceneMgr); + + /** + * Save scene to JSON file + */ + bool saveToFile(const std::string& filepath); + + /** + * Load scene from JSON file + */ + bool loadFromFile(const std::string& filepath, EditorUISystem* uiSystem = nullptr); + + /** + * Get last error message + */ + const std::string& getLastError() const { return m_lastError; } + +private: + // Serialization helpers + nlohmann::json serializeEntity(flecs::entity entity); + void deserializeEntity(const nlohmann::json& json, flecs::entity parent, EditorUISystem* uiSystem); + + // Component serialization + nlohmann::json serializeTransform(flecs::entity entity); + nlohmann::json serializeRenderable(flecs::entity entity); + nlohmann::json serializeEntityName(flecs::entity entity); + + // Component deserialization + void deserializeTransform(flecs::entity entity, const nlohmann::json& json, flecs::entity parentEntity); + void deserializeRenderable(flecs::entity entity, const nlohmann::json& json); + void deserializeEntityName(flecs::entity entity, const nlohmann::json& json); + + flecs::world& m_world; + Ogre::SceneManager* m_sceneMgr; + std::string m_lastError; + + // Track entity ID mapping for parent/child relationships + std::unordered_map m_entityMap; +}; + +#endif // EDITSCENE_SCENESERIALIZER_HPP