diff --git a/src/features/CMakeLists.txt b/src/features/CMakeLists.txt index d4c4717..3eddef8 100644 --- a/src/features/CMakeLists.txt +++ b/src/features/CMakeLists.txt @@ -1,3 +1,4 @@ project(features) -# ... add_subdirectory(characters) +add_subdirectory(sceneEditor) +add_subdirectory(editScene) diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt new file mode 100644 index 0000000..f2b9944 --- /dev/null +++ b/src/features/editScene/CMakeLists.txt @@ -0,0 +1,62 @@ +project(editScene) +set(CMAKE_CXX_STANDARD 17) + +find_package(OGRE REQUIRED COMPONENTS Bites Overlay CONFIG) +find_package(flecs REQUIRED CONFIG) +find_package(SDL2 REQUIRED) + +# Collect all source files +set(EDITSCENE_SOURCES + main.cpp + EditorApp.cpp + systems/EditorUISystem.cpp + ui/TransformEditor.cpp + ui/RenderableEditor.cpp + camera/EditorCamera.cpp + gizmo/Gizmo.cpp +) + +set(EDITSCENE_HEADERS + EditorApp.hpp + components/Transform.hpp + components/Renderable.hpp + components/EntityName.hpp + components/Relationship.hpp + systems/EditorUISystem.hpp + ui/ComponentEditor.hpp + ui/ComponentRegistry.hpp + ui/TransformEditor.hpp + ui/RenderableEditor.hpp + camera/EditorCamera.hpp + gizmo/Gizmo.hpp +) + +add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS}) + +target_link_libraries(editSceneEditor + OgreMain + OgreBites + OgreOverlay + flecs::flecs_static +) + +target_include_directories(editSceneEditor PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# Copy local resources (materials, etc.) +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources" + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") +endif() + +# Copy resources from main build +add_custom_command(TARGET editSceneEditor POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_BINARY_DIR}/resources" + "${CMAKE_CURRENT_BINARY_DIR}/resources" + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_SOURCE_DIR}/resources.cfg" + "${CMAKE_CURRENT_BINARY_DIR}/resources.cfg" + COMMENT "Copying resources to editSceneEditor build directory" +) diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp new file mode 100644 index 0000000..b99c251 --- /dev/null +++ b/src/features/editScene/EditorApp.cpp @@ -0,0 +1,372 @@ +#include +#include "EditorApp.hpp" +#include "systems/EditorUISystem.hpp" +#include "camera/EditorCamera.hpp" +#include "components/EntityName.hpp" +#include "components/Transform.hpp" +#include "components/Renderable.hpp" +#include "components/EditorMarker.hpp" +#include +#include + +//============================================================================= +// ImGuiRenderListener Implementation +//============================================================================= + +ImGuiRenderListener::ImGuiRenderListener(Ogre::ImGuiOverlay *imguiOverlay, + EditorUISystem *uiSystem) + : m_imguiOverlay(imguiOverlay) + , m_uiSystem(uiSystem) +{ +} + +void ImGuiRenderListener::preViewportUpdate( + const Ogre::RenderTargetViewportEvent &evt) +{ + (void)evt; + // Start ImGui frame before viewport renders + Ogre::ImGuiOverlay::NewFrame(); + + // Render editor UI + if (m_uiSystem) { + m_uiSystem->update(); + } +} + +void ImGuiRenderListener::postViewportUpdate( + const Ogre::RenderTargetViewportEvent &evt) +{ + (void)evt; + // End ImGui frame after viewport renders + ImGui::EndFrame(); +} + +//============================================================================= +// EditorApp Implementation +//============================================================================= + +EditorApp::EditorApp() + : OgreBites::ApplicationContext("EditSceneEditor") + , m_sceneMgr(nullptr) + , m_overlaySystem(nullptr) + , m_imguiOverlay(nullptr) + , m_currentModifiers(0) +{ +} + +EditorApp::~EditorApp() +{ + // Singletons are managed by OGRE, don't delete them +} + +void EditorApp::setup() +{ + // Base setup + OgreBites::ApplicationContext::setup(); + + // Get root and create scene manager + Ogre::Root *root = getRoot(); + m_sceneMgr = root->createSceneManager(); + m_sceneMgr->setAmbientLight(Ogre::ColourValue(0.3f, 0.3f, 0.3f)); + + // Add scene manager to RTShader generator + Ogre::RTShader::ShaderGenerator *shadergen = + Ogre::RTShader::ShaderGenerator::getSingletonPtr(); + if (shadergen) { + shadergen->addSceneManager(m_sceneMgr); + } + + // Setup overlay system - get from ApplicationContext + m_overlaySystem = getOverlaySystem(); + OgreAssert(m_overlaySystem, "OverlaySystem not available"); + m_sceneMgr->addRenderQueueListener(m_overlaySystem); + + // Setup ImGui overlay + m_imguiOverlay = initialiseImGui(); + if (m_imguiOverlay) { + m_imguiOverlay->setZOrder(300); + m_imguiOverlay->show(); + ImGui::StyleColorsDark(); + } + + // Setup camera + m_camera = + std::make_unique(m_sceneMgr, getRenderWindow()); + + // Setup ECS and scene + setupECS(); + setupLights(); + createGrid(); + createAxes(); + createDefaultEntities(); + + // Setup UI system + m_uiSystem = std::make_unique(m_world, m_sceneMgr); + + // Add default entities to UI cache + for (auto &e : m_defaultEntities) { + m_uiSystem->addEntity(e); + } + + // Create and register ImGui render listener + m_imguiListener = std::make_unique( + m_imguiOverlay, m_uiSystem.get()); + getRenderWindow()->addListener(m_imguiListener.get()); + + // Register input listeners + addInputListener(this); + addInputListener(getImGuiInputListener()); +} + +void EditorApp::setupECS() +{ + // Register components + m_world.component(); + m_world.component(); + m_world.component(); + m_world.component(); +} + +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); +} + +void EditorApp::setupLights() +{ + // Directional light + Ogre::Light *dirLight = m_sceneMgr->createLight("DirectionalLight"); + dirLight->setType(Ogre::Light::LT_DIRECTIONAL); + dirLight->setDiffuseColour(Ogre::ColourValue(1.0f, 1.0f, 1.0f)); + dirLight->setSpecularColour(Ogre::ColourValue(0.5f, 0.5f, 0.5f)); + + Ogre::SceneNode *dirNode = + m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + dirNode->attachObject(dirLight); + dirNode->setDirection(Ogre::Vector3(1, -1, 0), Ogre::Node::TS_WORLD); + + // Fill light + Ogre::Light *fillLight = m_sceneMgr->createLight("FillLight"); + fillLight->setType(Ogre::Light::LT_DIRECTIONAL); + fillLight->setDiffuseColour(Ogre::ColourValue(0.4f, 0.4f, 0.4f)); + + Ogre::SceneNode *fillNode = + m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + fillNode->attachObject(fillLight); + fillNode->setDirection(Ogre::Vector3(-0.5f, -1, 0.5f), + Ogre::Node::TS_WORLD); +} + +void EditorApp::createGrid() +{ + try { + Ogre::ManualObject *grid = + m_sceneMgr->createManualObject("Grid"); + // Use Ogre/AxisGizmo which properly supports vertex colors + grid->begin("Ogre/AxisGizmo", + Ogre::RenderOperation::OT_LINE_LIST); + + // Draw grid lines - grey color set once + float size = 10.0f; + int divisions = 20; + float step = size * 2.0f / divisions; + + for (int i = 0; i <= divisions; ++i) { + float pos = -size + i * step; + + // Lines along X + grid->position(pos, 0, -size); + grid->position(pos, 0, size); + + // Lines along Z + grid->position(-size, 0, pos); + grid->position(size, 0, pos); + } + + grid->end(); + + Ogre::SceneNode *gridNode = + m_sceneMgr->getRootSceneNode()->createChildSceneNode( + "GridNode"); + gridNode->attachObject(grid); + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "Grid creation failed: " + Ogre::String(e.what())); + } +} + +void EditorApp::createAxes() +{ + try { + // X axis (red) + Ogre::ManualObject *axisX = + m_sceneMgr->createManualObject("AxisX"); + axisX->begin("Ogre/AxisGizmo", + Ogre::RenderOperation::OT_LINE_LIST); + axisX->colour(1.0f, 0.0f, 0.0f); + axisX->position(0, 0, 0); + axisX->position(2, 0, 0); + axisX->end(); + + // Y axis (green) + Ogre::ManualObject *axisY = + m_sceneMgr->createManualObject("AxisY"); + axisY->begin("Ogre/AxisGizmo", + Ogre::RenderOperation::OT_LINE_LIST); + axisY->colour(0.0f, 1.0f, 0.0f); + axisY->position(0, 0, 0); + axisY->position(0, 2, 0); + axisY->end(); + + // Z axis (blue) + Ogre::ManualObject *axisZ = + m_sceneMgr->createManualObject("AxisZ"); + axisZ->begin("Ogre/AxisGizmo", + Ogre::RenderOperation::OT_LINE_LIST); + axisZ->colour(0.0f, 0.0f, 1.0f); + axisZ->position(0, 0, 0); + axisZ->position(0, 0, 2); + axisZ->end(); + + // Create axis node + Ogre::SceneNode *axisNode = + m_sceneMgr->getRootSceneNode()->createChildSceneNode( + "AxisNode"); + axisNode->attachObject(axisX); + axisNode->attachObject(axisY); + axisNode->attachObject(axisZ); + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "Axis creation failed: " + Ogre::String(e.what())); + } +} + +bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) +{ + // Update camera + if (m_camera) { + m_camera->update(evt.timeSinceLastFrame); + } + + // Don't call base class - it crashes when iterating input listeners + return true; +} + +bool EditorApp::mouseMoved(const OgreBites::MouseMotionEvent &evt) +{ + // Skip if ImGui wants to capture mouse (for gizmo, we still want to process even if over UI) + // But we need to update hover state + if (m_camera && m_uiSystem) { + Ogre::Ray mouseRay = m_camera->getMouseRay(evt.x, evt.y); + Ogre::Vector2 delta(evt.xrel, evt.yrel); + + // Try gizmo first - gizmo handles its own visibility checks + if (m_uiSystem->onMouseMoved(mouseRay, delta)) { + return true; + } + } + + // Only pass to camera if ImGui doesn't want the mouse + ImGuiIO &io = ImGui::GetIO(); + if (!io.WantCaptureMouse && m_camera) { + m_camera->handleMouseMove(evt); + } + return true; +} + +bool EditorApp::mousePressed(const OgreBites::MouseButtonEvent &evt) +{ + // Get mouse ray for gizmo interaction FIRST (before ImGui check) + // This allows clicking on 3D gizmo even when mouse is over empty UI areas + if (m_camera && m_uiSystem) { + Ogre::Ray mouseRay = m_camera->getMouseRay(evt.x, evt.y); + std::cout << "click: " << mouseRay.getOrigin() << " " + << mouseRay.getDirection() << std::endl; + // Try gizmo first - if it handles the event, don't pass to camera or UI + if (m_uiSystem->onMousePressed(mouseRay)) { + return true; + } + } + + // Then check if ImGui wants the mouse + ImGuiIO &io = ImGui::GetIO(); + if (io.WantCaptureMouse) { + return false; // Let ImGui handle it + } + + if (m_camera) { + m_camera->handleMousePress(evt); + } + return true; +} + +bool EditorApp::mouseReleased(const OgreBites::MouseButtonEvent &evt) +{ + // Handle gizmo mouse release (always process to end dragging) + if (m_uiSystem) { + if (m_uiSystem->onMouseReleased()) { + return true; + } + } + + // Only pass to camera if ImGui doesn't want the mouse + ImGuiIO &io = ImGui::GetIO(); + if (!io.WantCaptureMouse && m_camera) { + m_camera->handleMouseRelease(evt); + } + return true; +} + +bool EditorApp::keyPressed(const OgreBites::KeyboardEvent &evt) +{ + m_currentModifiers = evt.keysym.mod; + + // Delete key to delete selected entity + if (evt.keysym.sym == 127) { // Delete key + // Handled in UI + } + + // F key to focus camera on selected entity + if (evt.keysym.sym == 'f' || evt.keysym.sym == 'F') { + if (m_camera && m_uiSystem && + m_uiSystem->getSelectedEntity().is_alive()) { + auto entity = m_uiSystem->getSelectedEntity(); + if (entity.has()) { + auto &transform = + entity.get(); + m_camera->focusOn(transform.position); + } + } + } + + return true; +} + +bool EditorApp::keyReleased(const OgreBites::KeyboardEvent &evt) +{ + m_currentModifiers = evt.keysym.mod; + return true; +} + +flecs::entity EditorApp::getSelectedEntity() const +{ + if (m_uiSystem) { + return m_uiSystem->getSelectedEntity(); + } + return flecs::entity::null(); +} diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp new file mode 100644 index 0000000..5260694 --- /dev/null +++ b/src/features/editScene/EditorApp.hpp @@ -0,0 +1,86 @@ +#ifndef EDITSCENE_EDITORAPP_HPP +#define EDITSCENE_EDITORAPP_HPP +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +// Forward declarations +class EditorUISystem; +class EditorCamera; + +/** + * RenderTargetListener for ImGui frame management + * Handles NewFrame() in preViewportUpdate and EndFrame() in postViewportUpdate + */ +class ImGuiRenderListener : public Ogre::RenderTargetListener { +public: + ImGuiRenderListener(Ogre::ImGuiOverlay *imguiOverlay, + EditorUISystem *uiSystem); + + void preViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override; + void postViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override; + +private: + Ogre::ImGuiOverlay *m_imguiOverlay; + EditorUISystem *m_uiSystem; +}; + +/** + * Main application class for the scene editor + */ +class EditorApp : public OgreBites::ApplicationContext, + public OgreBites::InputListener { +public: + EditorApp(); + virtual ~EditorApp(); + + // OgreBites::ApplicationContext overrides + void setup() override; + bool frameRenderingQueued(const Ogre::FrameEvent &evt) override; + + // OgreBites::InputListener overrides + bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override; + bool mousePressed(const OgreBites::MouseButtonEvent &evt) override; + bool mouseReleased(const OgreBites::MouseButtonEvent &evt) override; + bool keyPressed(const OgreBites::KeyboardEvent &evt) override; + bool keyReleased(const OgreBites::KeyboardEvent &evt) override; + + // Scene setup + void setupLights(); + void createGrid(); + void createAxes(); + + // ECS setup + void setupECS(); + void createDefaultEntities(); + + // Getters + flecs::entity getSelectedEntity() const; + Ogre::SceneManager *getSceneManager() const { return m_sceneMgr; } + +private: + // Ogre objects + Ogre::SceneManager *m_sceneMgr; + Ogre::OverlaySystem *m_overlaySystem; + Ogre::ImGuiOverlay *m_imguiOverlay; + + // ECS + flecs::world m_world; + std::vector m_defaultEntities; + + // Editor systems + std::unique_ptr m_uiSystem; + std::unique_ptr m_camera; + std::unique_ptr m_imguiListener; + + // State + uint16_t m_currentModifiers; +}; + +#endif // EDITSCENE_EDITORAPP_HPP diff --git a/src/features/editScene/camera/EditorCamera.cpp b/src/features/editScene/camera/EditorCamera.cpp new file mode 100644 index 0000000..5536e06 --- /dev/null +++ b/src/features/editScene/camera/EditorCamera.cpp @@ -0,0 +1,155 @@ +#include "EditorCamera.hpp" +#include + +EditorCamera::EditorCamera(Ogre::SceneManager *sceneMgr, + Ogre::RenderWindow *window) + : m_sceneMgr(sceneMgr) + , m_camera(nullptr) + , m_cameraNode(nullptr) + , m_targetNode(nullptr) + , m_position(0, 5, 15) + , m_target(0, 0, 0) + , m_distance(15.0f) + , m_yaw(0.0f) + , m_pitch(-20.0f) + , m_rotating(false) + , m_panning(false) + , m_zooming(false) + , m_lastMouseX(0) + , m_lastMouseY(0) +{ + // Create camera + m_camera = sceneMgr->createCamera("EditorCamera"); + m_camera->setNearClipDistance(0.1f); + m_camera->setFarClipDistance(1000.0f); + m_camera->setAutoAspectRatio(true); + + // Create camera node + m_cameraNode = sceneMgr->getRootSceneNode()->createChildSceneNode( + "EditorCameraNode"); + m_cameraNode->attachObject(m_camera); + + // Create target node + m_targetNode = sceneMgr->getRootSceneNode()->createChildSceneNode( + "EditorCameraTarget"); + + // Setup viewport + Ogre::Viewport *vp = window->addViewport(m_camera); + vp->setBackgroundColour(Ogre::ColourValue(0.1f, 0.1f, 0.1f)); + + updateCameraPosition(); +} + +EditorCamera::~EditorCamera() = default; + +void EditorCamera::update(float deltaTime) +{ + (void)deltaTime; + updateCameraPosition(); +} + +void EditorCamera::handleMouseMove(const OgreBites::MouseMotionEvent &evt) +{ + int dx = evt.x - m_lastMouseX; + int dy = evt.y - m_lastMouseY; + m_lastMouseX = evt.x; + m_lastMouseY = evt.y; + + if (m_rotating) { + m_yaw -= dx * ROTATION_SPEED; + m_pitch -= dy * ROTATION_SPEED; + + // Clamp pitch + if (m_pitch > 89.0f) + m_pitch = 89.0f; + if (m_pitch < -89.0f) + m_pitch = -89.0f; + } + + if (m_panning) { + // Get right and up vectors from camera's orientation + Ogre::Quaternion orientation = m_camera->getDerivedOrientation(); + Ogre::Vector3 right = orientation * Ogre::Vector3::UNIT_X; + Ogre::Vector3 up = orientation * Ogre::Vector3::UNIT_Y; + + Ogre::Vector3 pan = right * (-dx * PAN_SPEED * m_distance) + + up * (dy * PAN_SPEED * m_distance); + + m_target += pan; + } +} + +void EditorCamera::handleMousePress(const OgreBites::MouseButtonEvent &evt) +{ + m_lastMouseX = evt.x; + m_lastMouseY = evt.y; + + if (evt.button == OgreBites::BUTTON_RIGHT) { + m_rotating = true; + } else if (evt.button == OgreBites::BUTTON_MIDDLE) { + m_panning = true; + } else if (evt.button == OgreBites::BUTTON_LEFT) { + // Left click is for selection, handled by app + } +} + +void EditorCamera::handleMouseRelease(const OgreBites::MouseButtonEvent &evt) +{ + if (evt.button == OgreBites::BUTTON_RIGHT) { + m_rotating = false; + } else if (evt.button == OgreBites::BUTTON_MIDDLE) { + m_panning = false; + } +} + +void EditorCamera::handleKeyboard(const OgreBites::KeyboardEvent &evt) +{ + // Camera keyboard controls can be added here + // For now, mouse controls are sufficient + (void)evt; +} + +void EditorCamera::focusOn(const Ogre::Vector3 &point) +{ + m_target = point; + updateCameraPosition(); +} + +void EditorCamera::setPosition(const Ogre::Vector3 &pos) +{ + m_target = pos; + updateCameraPosition(); +} + +Ogre::Ray EditorCamera::getMouseRay(float screenX, float screenY) const +{ + // Convert pixel coordinates to normalized viewport coordinates (0-1) + Ogre::Viewport *viewport = m_camera->getViewport(); + if (!viewport) + return Ogre::Ray(); + + float normX = screenX / viewport->getActualWidth(); + float normY = screenY / viewport->getActualHeight(); + + return m_camera->getCameraToViewportRay(normX, normY); +} + +void EditorCamera::updateCameraPosition() +{ + // Calculate camera position based on spherical coordinates + float yawRad = Ogre::Degree(m_yaw).valueRadians(); + float pitchRad = Ogre::Degree(m_pitch).valueRadians(); + + Ogre::Vector3 offset; + offset.x = m_distance * Ogre::Math::Cos(pitchRad) * + Ogre::Math::Sin(yawRad); + offset.y = m_distance * Ogre::Math::Sin(pitchRad); + offset.z = m_distance * Ogre::Math::Cos(pitchRad) * + Ogre::Math::Cos(yawRad); + + m_position = m_target + offset; + + m_cameraNode->setPosition(m_position); + m_cameraNode->lookAt(m_target, Ogre::Node::TS_WORLD); + m_targetNode->setPosition(m_target); +} diff --git a/src/features/editScene/camera/EditorCamera.hpp b/src/features/editScene/camera/EditorCamera.hpp new file mode 100644 index 0000000..9764b83 --- /dev/null +++ b/src/features/editScene/camera/EditorCamera.hpp @@ -0,0 +1,83 @@ +#ifndef EDITSCENE_EDITORCAMERA_HPP +#define EDITSCENE_EDITORCAMERA_HPP +#pragma once +#include +#include +#include +#include +#include + +/** + * Simple editor camera controller + * Supports orbit and fly modes + */ +class EditorCamera { +public: + EditorCamera(Ogre::SceneManager *sceneMgr, + Ogre::RenderWindow *window); + ~EditorCamera(); + + /** + * Update camera movement + */ + void update(float deltaTime); + + /** + * Handle input events + */ + void handleMouseMove(const OgreBites::MouseMotionEvent &evt); + void handleMousePress(const OgreBites::MouseButtonEvent &evt); + void handleMouseRelease(const OgreBites::MouseButtonEvent &evt); + void handleKeyboard(const OgreBites::KeyboardEvent &evt); + + /** + * Get the camera + */ + Ogre::Camera *getCamera() const { return m_camera; } + + /** + * Focus camera on a point + */ + void focusOn(const Ogre::Vector3 &point); + + /** + * Set camera position + */ + void setPosition(const Ogre::Vector3 &pos); + + /** + * Get ray from mouse position + */ + Ogre::Ray getMouseRay(float screenX, float screenY) const; + +private: + void updateCameraPosition(); + + Ogre::SceneManager *m_sceneMgr; + Ogre::Camera *m_camera; + Ogre::SceneNode *m_cameraNode; + Ogre::SceneNode *m_targetNode; + + // Camera state + Ogre::Vector3 m_position; + Ogre::Vector3 m_target; + float m_distance; + float m_yaw; + float m_pitch; + + // Input state + bool m_rotating; + bool m_panning; + bool m_zooming; + int m_lastMouseX; + int m_lastMouseY; + + // Movement speeds + static constexpr float ROTATION_SPEED = 0.3f; + static constexpr float PAN_SPEED = 0.01f; + static constexpr float ZOOM_SPEED = 0.5f; + static constexpr float MIN_DISTANCE = 1.0f; + static constexpr float MAX_DISTANCE = 1000.0f; +}; + +#endif // EDITSCENE_EDITORCAMERA_HPP diff --git a/src/features/editScene/components/EditorMarker.hpp b/src/features/editScene/components/EditorMarker.hpp new file mode 100644 index 0000000..8ff2d24 --- /dev/null +++ b/src/features/editScene/components/EditorMarker.hpp @@ -0,0 +1,13 @@ +#ifndef EDITSCENE_EDITORMARKER_HPP +#define EDITSCENE_EDITORMARKER_HPP +#pragma once + +/** + * Marker component for entities created in the editor + * Used to filter out flecs internal entities (components, systems, etc.) + */ +struct EditorMarkerComponent { + // Empty marker component +}; + +#endif // EDITSCENE_EDITORMARKER_HPP diff --git a/src/features/editScene/components/EntityName.hpp b/src/features/editScene/components/EntityName.hpp new file mode 100644 index 0000000..ebb27e9 --- /dev/null +++ b/src/features/editScene/components/EntityName.hpp @@ -0,0 +1,21 @@ +#ifndef EDITSCENE_ENTITYNAME_HPP +#define EDITSCENE_ENTITYNAME_HPP +#pragma once +#include + +/** + * Name component for Flecs entities + * Used for display in the hierarchy window + */ +struct EntityNameComponent { + Ogre::String name; + + EntityNameComponent() = default; + + explicit EntityNameComponent(const Ogre::String &n) + : name(n) + { + } +}; + +#endif // EDITSCENE_ENTITYNAME_HPP diff --git a/src/features/editScene/components/Relationship.hpp b/src/features/editScene/components/Relationship.hpp new file mode 100644 index 0000000..64d4262 --- /dev/null +++ b/src/features/editScene/components/Relationship.hpp @@ -0,0 +1,21 @@ +#ifndef EDITSCENE_RELATIONSHIP_HPP +#define EDITSCENE_RELATIONSHIP_HPP +#pragma once +#include + +/** + * Parent-child relationship component + * Tracks the parent entity for hierarchy + */ +struct ParentComponent { + flecs::entity parent; +}; + +/** + * Component to mark an entity as modified (for UI updates) + */ +struct ModifiedComponent { + bool modified = true; +}; + +#endif // EDITSCENE_RELATIONSHIP_HPP diff --git a/src/features/editScene/components/Renderable.hpp b/src/features/editScene/components/Renderable.hpp new file mode 100644 index 0000000..502568c --- /dev/null +++ b/src/features/editScene/components/Renderable.hpp @@ -0,0 +1,23 @@ +#ifndef EDITSCENE_RENDERABLE_HPP +#define EDITSCENE_RENDERABLE_HPP +#pragma once +#include + +/** + * Renderable component for Flecs entities + * Links a mesh to an entity + */ +struct RenderableComponent { + Ogre::Entity *entity = nullptr; + Ogre::String meshName; + bool visible = true; + + RenderableComponent() = default; + + explicit RenderableComponent(const Ogre::String &mesh) + : meshName(mesh) + { + } +}; + +#endif // EDITSCENE_RENDERABLE_HPP diff --git a/src/features/editScene/components/Transform.hpp b/src/features/editScene/components/Transform.hpp new file mode 100644 index 0000000..7a7b011 --- /dev/null +++ b/src/features/editScene/components/Transform.hpp @@ -0,0 +1,41 @@ +#ifndef EDITSCENE_TRANSFORM_HPP +#define EDITSCENE_TRANSFORM_HPP +#pragma once +#include + +/** + * Transform component for Flecs entities + * Links a Flecs entity to an Ogre SceneNode + */ +struct TransformComponent { + Ogre::SceneNode *node = nullptr; + Ogre::Vector3 position = Ogre::Vector3::ZERO; + Ogre::Quaternion rotation = Ogre::Quaternion::IDENTITY; + Ogre::Vector3 scale = Ogre::Vector3::UNIT_SCALE; + + /** + * Apply component values to the scene node + */ + void applyToNode() const + { + if (node) { + node->setPosition(position); + node->setOrientation(rotation); + node->setScale(scale); + } + } + + /** + * Update component values from the scene node + */ + void updateFromNode() + { + if (node) { + position = node->getPosition(); + rotation = node->getOrientation(); + scale = node->getScale(); + } + } +}; + +#endif // EDITSCENE_TRANSFORM_HPP diff --git a/src/features/editScene/gizmo/Gizmo.cpp b/src/features/editScene/gizmo/Gizmo.cpp new file mode 100644 index 0000000..3deccc6 --- /dev/null +++ b/src/features/editScene/gizmo/Gizmo.cpp @@ -0,0 +1,304 @@ +#include "Gizmo.hpp" +#include "../components/Transform.hpp" +#include + +// Simple colors for axes - not using vertex colors since RTSS has issues +static const float COLOR_RED[3] = {1.0f, 0.0f, 0.0f}; +static const float COLOR_GREEN[3] = {0.0f, 1.0f, 0.0f}; +static const float COLOR_BLUE[3] = {0.0f, 0.0f, 1.0f}; +static const float COLOR_YELLOW[3] = {1.0f, 1.0f, 0.0f}; + +Gizmo::Gizmo(Ogre::SceneManager *sceneMgr) + : m_sceneMgr(sceneMgr) + , m_gizmoNode(nullptr) + , m_axisX(nullptr) + , m_axisY(nullptr) + , m_axisZ(nullptr) + , m_mode(Mode::Translate) + , m_selectedAxis(Axis::None) + , m_hoveredAxis(Axis::None) + , m_size(1.0f) + , m_axisLength(2.0f) + , m_isDragging(false) + , m_dragStartT(0.0f) +{ + m_gizmoNode = m_sceneMgr->getRootSceneNode()->createChildSceneNode("GizmoNode"); + + m_axisX = m_sceneMgr->createManualObject("GizmoAxisX"); + m_axisY = m_sceneMgr->createManualObject("GizmoAxisY"); + m_axisZ = m_sceneMgr->createManualObject("GizmoAxisZ"); + + m_gizmoNode->attachObject(m_axisX); + m_gizmoNode->attachObject(m_axisY); + m_gizmoNode->attachObject(m_axisZ); + + // Set render queue to overlay so gizmo draws on top of everything + m_axisX->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); + m_axisY->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); + m_axisZ->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); + + m_gizmoNode->setVisible(false); +} + +Gizmo::~Gizmo() +{ + if (m_axisX) m_sceneMgr->destroyManualObject(m_axisX); + if (m_axisY) m_sceneMgr->destroyManualObject(m_axisY); + if (m_axisZ) m_sceneMgr->destroyManualObject(m_axisZ); + if (m_gizmoNode) m_sceneMgr->destroySceneNode(m_gizmoNode); +} + +void Gizmo::attachTo(flecs::entity entity) +{ + m_attachedEntity = entity; + + if (entity.is_alive() && entity.has()) { + createGizmoGeometry(); + update(); + m_gizmoNode->setVisible(true); + } else { + m_gizmoNode->setVisible(false); + } +} + +void Gizmo::detach() +{ + m_attachedEntity = flecs::entity::null(); + m_gizmoNode->setVisible(false); + m_selectedAxis = Axis::None; + m_hoveredAxis = Axis::None; + m_isDragging = false; +} + +void Gizmo::update() +{ + if (!m_attachedEntity.is_alive() || !m_attachedEntity.has()) { + m_gizmoNode->setVisible(false); + return; + } + + auto &transform = m_attachedEntity.get(); + if (transform.node) { + m_gizmoNode->setPosition(transform.position); + m_gizmoNode->setOrientation(transform.rotation); + m_gizmoNode->setVisible(true); + } +} + +void Gizmo::setMode(Mode mode) +{ + if (m_mode != mode) { + m_mode = mode; + createGizmoGeometry(); + } +} + +void Gizmo::createGizmoGeometry() +{ + float len = m_axisLength * m_size; + float arrowSize = 0.3f * m_size; + + // X Axis - Red (or Yellow if selected/hovered) + bool xSelected = (m_selectedAxis == Axis::X || m_hoveredAxis == Axis::X); + m_axisX->clear(); + m_axisX->begin("Ogre/AxisGizmo", Ogre::RenderOperation::OT_LINE_LIST); + if (xSelected) { + m_axisX->colour(1.0f, 1.0f, 0.0f); // Yellow + } else { + m_axisX->colour(1.0f, 0.0f, 0.0f); // Red + } + // Main line + m_axisX->position(0, 0, 0); + m_axisX->position(len, 0, 0); + // Arrow head lines + m_axisX->position(len, 0, 0); + m_axisX->position(len - arrowSize, arrowSize, 0); + m_axisX->position(len, 0, 0); + m_axisX->position(len - arrowSize, -arrowSize, 0); + m_axisX->end(); + + // Y Axis - Green + bool ySelected = (m_selectedAxis == Axis::Y || m_hoveredAxis == Axis::Y); + m_axisY->clear(); + m_axisY->begin("Ogre/AxisGizmo", Ogre::RenderOperation::OT_LINE_LIST); + if (ySelected) { + m_axisY->colour(1.0f, 1.0f, 0.0f); // Yellow + } else { + m_axisY->colour(0.0f, 1.0f, 0.0f); // Green + } + // Main line + m_axisY->position(0, 0, 0); + m_axisY->position(0, len, 0); + // Arrow head lines + m_axisY->position(0, len, 0); + m_axisY->position(arrowSize, len - arrowSize, 0); + m_axisY->position(0, len, 0); + m_axisY->position(-arrowSize, len - arrowSize, 0); + m_axisY->end(); + + // Z Axis - Blue + bool zSelected = (m_selectedAxis == Axis::Z || m_hoveredAxis == Axis::Z); + m_axisZ->clear(); + m_axisZ->begin("Ogre/AxisGizmo", Ogre::RenderOperation::OT_LINE_LIST); + if (zSelected) { + m_axisZ->colour(1.0f, 1.0f, 0.0f); // Yellow + } else { + m_axisZ->colour(0.0f, 0.0f, 1.0f); // Blue + } + // Main line + m_axisZ->position(0, 0, 0); + m_axisZ->position(0, 0, len); + // Arrow head lines + m_axisZ->position(0, 0, len); + m_axisZ->position(arrowSize, 0, len - arrowSize); + m_axisZ->position(0, 0, len); + m_axisZ->position(-arrowSize, 0, len - arrowSize); + m_axisZ->end(); +} + +// Project ray onto axis line and return the parameter t along the axis +static bool projectRayOntoAxis(const Ogre::Ray& ray, const Ogre::Vector3& axisOrigin, + const Ogre::Vector3& axisDir, float& outT) +{ + Ogre::Vector3 rayOrigin = ray.getOrigin(); + Ogre::Vector3 rayDir = ray.getDirection(); + + Ogre::Vector3 w0 = rayOrigin - axisOrigin; + + float a = rayDir.dotProduct(rayDir); + float b = rayDir.dotProduct(axisDir); + float c = axisDir.dotProduct(axisDir); + float d = rayDir.dotProduct(w0); + float e = axisDir.dotProduct(w0); + + float denom = a * c - b * b; + + if (std::abs(denom) < 0.0001f) { + return false; + } + + outT = (a * e - b * d) / denom; + return true; +} + +bool Gizmo::onMousePressed(const Ogre::Ray &mouseRay) +{ + if (!m_axisX->isVisible()) return false; + + m_selectedAxis = hitTest(mouseRay); + + if (m_selectedAxis != Axis::None && m_attachedEntity.is_alive() + && m_attachedEntity.has()) { + + m_isDragging = true; + auto &transform = m_attachedEntity.get_mut(); + m_dragStartPosition = transform.position; + + 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; + case Axis::Z: m_dragAxisDir = m_gizmoNode->getOrientation() * Ogre::Vector3::UNIT_Z; break; + default: m_dragAxisDir = Ogre::Vector3::UNIT_X; break; + } + + projectRayOntoAxis(mouseRay, m_dragStartPosition, m_dragAxisDir, m_dragStartT); + + return true; + } + + return false; +} + +bool Gizmo::onMouseMoved(const Ogre::Ray &mouseRay, const Ogre::Vector2 &mouseDelta) +{ + if (m_isDragging && m_selectedAxis != Axis::None + && m_attachedEntity.is_alive() + && m_attachedEntity.has()) { + + auto &transform = m_attachedEntity.get_mut(); + + float currentT; + if (!projectRayOntoAxis(mouseRay, m_dragStartPosition, m_dragAxisDir, currentT)) { + return true; + } + + float deltaT = currentT - m_dragStartT; + + transform.position = m_dragStartPosition + m_dragAxisDir * deltaT; + transform.applyToNode(); + + m_gizmoNode->setPosition(transform.position); + + return true; + } else if (m_axisX->isVisible()) { + Axis prevHover = m_hoveredAxis; + m_hoveredAxis = hitTest(mouseRay); + if (prevHover != m_hoveredAxis) { + // Could update visualization here + } + } + + return false; +} + +bool Gizmo::onMouseReleased() +{ + if (m_isDragging) { + m_isDragging = false; + m_selectedAxis = Axis::None; + return true; + } + return false; +} + +Gizmo::Axis Gizmo::hitTest(const Ogre::Ray &mouseRay) +{ + if (!m_gizmoNode || !m_axisX->isVisible()) + return Axis::None; + + Ogre::Vector3 gizmoPos = m_gizmoNode->getPosition(); + float len = m_axisLength * m_size; + float threshold = 0.5f * m_size; + + float bestDist = 1000000.0f; + Axis bestAxis = Axis::None; + + for (int i = 0; i < 3; ++i) { + Ogre::Vector3 axisDir; + if (i == 0) axisDir = m_gizmoNode->getOrientation() * Ogre::Vector3::UNIT_X; + else if (i == 1) axisDir = m_gizmoNode->getOrientation() * Ogre::Vector3::UNIT_Y; + else axisDir = m_gizmoNode->getOrientation() * Ogre::Vector3::UNIT_Z; + + for (int j = 0; j <= 8; ++j) { + float t = len * j / 8.0f; + Ogre::Vector3 pointOnAxis = gizmoPos + axisDir * t; + + Ogre::Vector3 L = pointOnAxis - mouseRay.getOrigin(); + float tca = L.dotProduct(mouseRay.getDirection()); + + if (tca < 0.01f) continue; + + float d2 = L.dotProduct(L) - tca * tca; + if (d2 <= threshold * threshold) { + if (tca < bestDist) { + bestDist = tca; + bestAxis = static_cast(i + 1); + } + } + } + } + + return bestAxis; +} + +void Gizmo::setVisible(bool visible) +{ + if (m_gizmoNode) { + m_gizmoNode->setVisible(visible); + } +} + +bool Gizmo::isVisible() const +{ + return m_gizmoNode && m_axisX && m_axisX->isVisible(); +} diff --git a/src/features/editScene/gizmo/Gizmo.hpp b/src/features/editScene/gizmo/Gizmo.hpp new file mode 100644 index 0000000..074550a --- /dev/null +++ b/src/features/editScene/gizmo/Gizmo.hpp @@ -0,0 +1,107 @@ +#ifndef EDITSCENE_GIZMO_HPP +#define EDITSCENE_GIZMO_HPP +#pragma once + +#include +#include +#include + +/** + * 3D Gizmo for editing entity transforms + * Shows X/Y/Z axes and allows mouse interaction for translation + */ +class Gizmo { +public: + enum class Mode { + Translate, + Rotate, + Scale + }; + + enum class Axis { + None, + X, + Y, + Z + }; + + Gizmo(Ogre::SceneManager *sceneMgr); + ~Gizmo(); + + /** + * Attach to an entity's transform + */ + void attachTo(flecs::entity entity); + + /** + * Detach from current entity + */ + void detach(); + + /** + * Update gizmo position/rotation to match attached entity + */ + void update(); + + /** + * Check if gizmo is attached to an entity + */ + bool isAttached() const { return m_attachedEntity.is_alive(); } + + /** + * Get attached entity + */ + flecs::entity getAttachedEntity() const { return m_attachedEntity; } + + /** + * Set gizmo mode (translate/rotate/scale) + */ + void setMode(Mode mode); + Mode getMode() const { return m_mode; } + + /** + * Handle mouse input for gizmo interaction + * Returns true if gizmo handled the input + */ + bool onMousePressed(const Ogre::Ray &mouseRay); + bool onMouseMoved(const Ogre::Ray &mouseRay, const Ogre::Vector2 &mouseDelta); + bool onMouseReleased(); + + /** + * Show/hide gizmo + */ + void setVisible(bool visible); + bool isVisible() const; + + /** + * Set gizmo size/scale + */ + void setSize(float size) { m_size = size; createGizmoGeometry(); } + float getSize() const { return m_size; } + +private: + void createGizmoGeometry(); + Axis hitTest(const Ogre::Ray &mouseRay); + + Ogre::SceneManager *m_sceneMgr; + Ogre::SceneNode *m_gizmoNode; + Ogre::ManualObject *m_axisX; + Ogre::ManualObject *m_axisY; + Ogre::ManualObject *m_axisZ; + + flecs::entity m_attachedEntity; + Mode m_mode; + Axis m_selectedAxis; + Axis m_hoveredAxis; + + float m_size; + float m_axisLength; + bool m_isDragging; + + // Drag state - stored at drag start and reused during drag + Ogre::Vector3 m_dragStartPosition; + Ogre::Vector3 m_dragAxisDir; + float m_dragStartT; +}; + +#endif // EDITSCENE_GIZMO_HPP diff --git a/src/features/editScene/main.cpp b/src/features/editScene/main.cpp new file mode 100644 index 0000000..47dc080 --- /dev/null +++ b/src/features/editScene/main.cpp @@ -0,0 +1,20 @@ +#include +#include "EditorApp.hpp" + +int main(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + + try { + EditorApp app; + app.initApp(); + app.getRoot()->startRendering(); + app.closeApp(); + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/src/features/editScene/resources.cfg b/src/features/editScene/resources.cfg new file mode 100644 index 0000000..6cfb5b8 --- /dev/null +++ b/src/features/editScene/resources.cfg @@ -0,0 +1,17 @@ +# Ogre Resources Configuration for EditScene Editor + +[General] +FileSystem=resources +FileSystem=resources/materials +FileSystem=resources/meshes +FileSystem=resources/textures +FileSystem=resources/buildings +FileSystem=resources/vehicles + +[Popular] +FileSystem=resources/materials/programs +FileSystem=resources/materials/scripts +FileSystem=resources/materials/textures + +[Essential] +Zip=/usr/share/OGRE-14/media/packs/SdkTrays.zip diff --git a/src/features/editScene/resources/materials/scripts/gizmo.material b/src/features/editScene/resources/materials/scripts/gizmo.material new file mode 100644 index 0000000..07cd2d5 --- /dev/null +++ b/src/features/editScene/resources/materials/scripts/gizmo.material @@ -0,0 +1,55 @@ +material GizmoRed +{ + technique + { + pass + { + diffuse 1 0 0 + ambient 1 0 0 + specular 0 0 0 0 + lighting on + } + } +} + +material GizmoGreen +{ + technique + { + pass + { + diffuse 0 1 0 + ambient 0 1 0 + specular 0 0 0 0 + lighting on + } + } +} + +material GizmoBlue +{ + technique + { + pass + { + diffuse 0 0 1 + ambient 0 0 1 + specular 0 0 0 0 + lighting on + } + } +} + +material GizmoYellow +{ + technique + { + pass + { + diffuse 1 1 0 + ambient 1 1 0 + specular 0 0 0 0 + lighting on + } + } +} diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp new file mode 100644 index 0000000..5c7346a --- /dev/null +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -0,0 +1,637 @@ +#include "EditorUISystem.hpp" +#include "../components/EntityName.hpp" +#include "../components/Transform.hpp" +#include "../components/Renderable.hpp" +#include "../components/EditorMarker.hpp" +#include "../ui/TransformEditor.hpp" +#include "../ui/RenderableEditor.hpp" +#include +#include + +EditorUISystem::EditorUISystem(flecs::world &world, + Ogre::SceneManager *sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_selectedEntity(flecs::entity::null()) + , m_nameQuery(world.query()) +{ + registerComponentEditors(); + m_gizmo = std::make_unique(m_sceneMgr); +} + +EditorUISystem::~EditorUISystem() = default; + +bool EditorUISystem::onMousePressed(const Ogre::Ray &mouseRay) +{ + if (m_gizmo) { + return m_gizmo->onMousePressed(mouseRay); + } + return false; +} + +bool EditorUISystem::onMouseMoved(const Ogre::Ray &mouseRay, const Ogre::Vector2 &mouseDelta) +{ + if (m_gizmo) { + return m_gizmo->onMouseMoved(mouseRay, mouseDelta); + } + return false; +} + +bool EditorUISystem::onMouseReleased() +{ + if (m_gizmo) { + return m_gizmo->onMouseReleased(); + } + return false; +} + +void EditorUISystem::registerComponentEditors() +{ + // Register Transform component + auto transformEditor = std::make_unique(); + m_componentRegistry.registerComponent( + "Transform", std::move(transformEditor), + // Adder + [this](flecs::entity e) { + if (!e.has()) { + TransformComponent transform; + transform.node = + m_sceneMgr->getRootSceneNode() + ->createChildSceneNode(); + transform.position = Ogre::Vector3::ZERO; + transform.rotation = Ogre::Quaternion::IDENTITY; + transform.scale = Ogre::Vector3::UNIT_SCALE; + e.set(transform); + } + }, + // Remover + [this](flecs::entity e) { + if (e.has()) { + auto &transform = + e.get_mut(); + if (transform.node) { + m_sceneMgr->destroySceneNode( + transform.node); + transform.node = nullptr; + } + e.remove(); + } + }); + + // Register Renderable component + auto renderableEditor = std::make_unique(m_sceneMgr); + m_componentRegistry.registerComponent( + "Renderable", std::move(renderableEditor), + // Adder + [this](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [this](flecs::entity e) { + if (e.has()) { + auto &renderable = + e.get_mut(); + if (renderable.entity) { + m_sceneMgr->destroyEntity( + renderable.entity); + renderable.entity = nullptr; + } + e.remove(); + } + }); +} + +void EditorUISystem::update() +{ + // Render UI windows + // Note: NewFrame() is called by ImGuiRenderListener::preViewportUpdate + renderHierarchyWindow(); + renderPropertyWindow(); + + // Update gizmo position to match selected entity + if (m_gizmo && m_gizmo->isAttached()) { + m_gizmo->update(); + } +} + +void EditorUISystem::setSelectedEntity(flecs::entity entity) +{ + m_selectedEntity = entity; + + // Update gizmo attachment + if (m_gizmo) { + if (entity.is_alive() && entity.has()) { + m_gizmo->attachTo(entity); + } else { + m_gizmo->detach(); + } + } +} + +void EditorUISystem::renderHierarchyWindow() +{ + ImVec2 viewportSize = ImGui::GetMainViewport()->Size; + + // Left window - fixed to left side + ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(LEFT_PANEL_WIDTH, viewportSize.y), + ImGuiCond_Always); + + ImGuiWindowFlags windowFlags = + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_MenuBar; + + if (ImGui::Begin("Entity Hierarchy", nullptr, windowFlags)) { + // Menu bar + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("Entity")) { + if (ImGui::MenuItem("New Entity", "Ctrl+N")) { + createNewEntity(); + } + if (ImGui::MenuItem("New Child Entity")) { + if (m_selectedEntity.is_alive()) { + createChildEntity( + m_selectedEntity); + } + } + ImGui::Separator(); + if (ImGui::MenuItem( + "Duplicate", "Ctrl+D", false, + m_selectedEntity.is_alive())) { + duplicateEntity(m_selectedEntity); + } + ImGui::Separator(); + if (ImGui::MenuItem( + "Delete", "Delete", false, + m_selectedEntity.is_alive())) { + deleteEntity(m_selectedEntity); + } + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + // Create Entity button + if (ImGui::Button("+ New Entity", ImVec2(120, 0))) { + createNewEntity(); + } + ImGui::SameLine(); + if (ImGui::Button("+ New Child", ImVec2(120, 0))) { + if (m_selectedEntity.is_alive()) { + createChildEntity(m_selectedEntity); + } + } + + ImGui::Separator(); + + // Root entities query - entities without a parent + std::vector rootEntities; + m_world.query_builder<>() + .with() + .build() + .each([&](flecs::entity entity) { + // Check if entity has no parent (flecs::ChildOf) + bool isRoot = true; + entity.children([&](flecs::entity child) { + // Just checking if we can iterate children + }); + // Alternative: use parent() function + if (entity.parent().is_valid() && entity.parent() != 0) { + isRoot = false; + } + if (isRoot) { + rootEntities.push_back(entity); + } + }); + + // Render tree for each root entity + for (auto &entity : rootEntities) { + renderEntityNode(entity, 0); + } + + // If no entities, show message + if (rootEntities.empty()) { + ImGui::TextDisabled("No entities in scene"); + } + } + ImGui::End(); +} + +void EditorUISystem::renderEntityNode(flecs::entity entity, int depth) +{ + if (!entity.is_alive()) + return; + + // Get entity name + std::string name = "Unnamed"; + if (entity.has()) { + name = entity.get().name; + } + + // Collect children using flecs::ChildOf relationship + std::vector children; + entity.children([&](flecs::entity child) { + children.push_back(child); + }); + bool hasChildren = !children.empty(); + + // Tree node flags + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_OpenOnDoubleClick | + ImGuiTreeNodeFlags_SpanAvailWidth; + + // Leaf nodes (no children) have bullet style + if (!hasChildren) { + flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + } + + // Highlight selected + if (m_selectedEntity == entity) { + flags |= ImGuiTreeNodeFlags_Selected; + } + + // Create label with optional component indicators + char label[256]; + std::string indicators; + if (entity.has()) + indicators += " [T]"; + if (entity.has()) + indicators += " [R]"; + + snprintf(label, sizeof(label), "%s%s##%llu", name.c_str(), + indicators.c_str(), (unsigned long long)entity.id()); + + // Draw tree node + bool isOpen = ImGui::TreeNodeEx(label, flags); + + // Handle selection on click + if (ImGui::IsItemClicked()) { + setSelectedEntity(entity); + } + + // Context menu + if (ImGui::BeginPopupContextItem()) { + renderEntityContextMenu(entity); + ImGui::EndPopup(); + } + + // Render children recursively + if (isOpen && hasChildren) { + for (auto &child : children) { + renderEntityNode(child, depth + 1); + } + ImGui::TreePop(); + } +} + +void EditorUISystem::renderEntityContextMenu(flecs::entity entity) +{ + if (ImGui::MenuItem("New Child Entity")) { + createChildEntity(entity); + } + ImGui::Separator(); + if (ImGui::MenuItem("Duplicate")) { + duplicateEntity(entity); + } + ImGui::Separator(); + if (ImGui::MenuItem("Delete")) { + deleteEntity(entity); + } +} + +void EditorUISystem::renderPropertyWindow() +{ + ImVec2 viewportSize = ImGui::GetMainViewport()->Size; + + // Right window - fixed to right side + ImGui::SetNextWindowPos(ImVec2(viewportSize.x - RIGHT_PANEL_WIDTH, 0), + ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(RIGHT_PANEL_WIDTH, viewportSize.y), + ImGuiCond_Always); + + ImGuiWindowFlags windowFlags = + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_MenuBar; + + if (ImGui::Begin("Property Editor", nullptr, windowFlags)) { + if (m_selectedEntity.is_alive()) { + // Entity name header + if (m_selectedEntity.has()) { + auto &nameComp = + m_selectedEntity + .get_mut(); + char nameBuffer[256]; + strcpy(nameBuffer, nameComp.name.c_str()); + if (ImGui::InputText("Name", nameBuffer, + sizeof(nameBuffer))) { + nameComp.name = nameBuffer; + } + } + + // Entity ID + ImGui::TextDisabled( + "ID: %llu", + (unsigned long long)m_selectedEntity.id()); + + ImGui::Separator(); + + // Component operations + renderComponentList(m_selectedEntity); + } else { + ImGui::TextDisabled("No entity selected"); + ImGui::Text("Select an entity from the hierarchy"); + } + } + ImGui::End(); +} + +void EditorUISystem::renderComponentList(flecs::entity entity) +{ + // Component operations menu bar + if (ImGui::Button("Add Component")) { + ImGui::OpenPopup("AddComponentMenu"); + } + renderAddComponentMenu(entity); + + ImGui::SameLine(); + + if (ImGui::Button("Remove Component")) { + ImGui::OpenPopup("RemoveComponentMenu"); + } + renderRemoveComponentMenu(entity); + + ImGui::Separator(); + + // Render component editors + ImGui::BeginChild("Components", ImVec2(0, 0), true); + + if (entity.has()) { + auto &transform = entity.get_mut(); + m_componentRegistry.render(entity, + transform); + } + + if (entity.has()) { + auto &renderable = entity.get_mut(); + m_componentRegistry.render(entity, + renderable); + } + + // Show message if no components + if (!entity.has() && + !entity.has()) { + ImGui::TextDisabled("No components"); + ImGui::Text("Click 'Add Component' to add components"); + } + + ImGui::EndChild(); +} + +void EditorUISystem::renderAddComponentMenu(flecs::entity entity) +{ + if (ImGui::BeginPopup("AddComponentMenu")) { + ImGui::Text("Add Component:"); + ImGui::Separator(); + + bool hasTransform = entity.has(); + bool hasRenderable = entity.has(); + + if (!hasTransform) { + if (ImGui::MenuItem("Transform")) { + m_componentRegistry + .addComponent( + entity); + } + } + + if (!hasRenderable) { + if (ImGui::MenuItem("Renderable")) { + m_componentRegistry + .addComponent( + entity); + } + } + + if (hasTransform && hasRenderable) { + ImGui::TextDisabled("All components added"); + } + + ImGui::EndPopup(); + } +} + +void EditorUISystem::renderRemoveComponentMenu(flecs::entity entity) +{ + { + if (ImGui::BeginPopup("RemoveComponentMenu")) { + ImGui::Text("Remove Component:"); + ImGui::Separator(); + + bool hasAny = false; + + if (entity.has()) { + hasAny = true; + if (ImGui::MenuItem("Transform")) { + m_componentRegistry.removeComponent< + TransformComponent>(entity); + } + } + + if (entity.has()) { + hasAny = true; + if (ImGui::MenuItem("Renderable")) { + m_componentRegistry.removeComponent< + RenderableComponent>(entity); + } + } + + if (!hasAny) { + ImGui::TextDisabled("No components to remove"); + } + + ImGui::EndPopup(); + } + } +} + +void EditorUISystem::createNewEntity() +{ + flecs::entity entity = m_world.entity(); + entity.set(EntityNameComponent("New Entity")); + entity.add(); + + // Add transform by default + TransformComponent transform; + transform.node = m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + transform.position = Ogre::Vector3::ZERO; + transform.rotation = Ogre::Quaternion::IDENTITY; + transform.scale = Ogre::Vector3::UNIT_SCALE; + entity.set(transform); + + setSelectedEntity(entity); +} + +void EditorUISystem::createChildEntity(flecs::entity parent) +{ + if (!parent.is_alive() || !parent.has()) + return; + + // Create entity as child of parent using flecs::ChildOf + flecs::entity entity = m_world.entity(); + entity.child_of(parent); + entity.set(EntityNameComponent("Child Entity")); + entity.add(); + + // Create transform with parent as scene node parent + TransformComponent transform; + auto &parentTransform = parent.get_mut(); + transform.node = parentTransform.node->createChildSceneNode(); + transform.position = Ogre::Vector3::ZERO; + transform.rotation = Ogre::Quaternion::IDENTITY; + transform.scale = Ogre::Vector3::UNIT_SCALE; + entity.set(transform); + + setSelectedEntity(entity); +} + +void EditorUISystem::deleteEntity(flecs::entity entity) +{ + if (!entity.is_alive()) + return; + + // First delete all children recursively + auto children = getEntityChildren(entity); + for (auto &child : children) { + deleteEntity(child); + } + + // Clean up transform node + if (entity.has()) { + auto &transform = entity.get_mut(); + if (transform.node) { + m_sceneMgr->destroySceneNode(transform.node); + } + } + + // Clean up renderable + if (entity.has()) { + auto &renderable = entity.get_mut(); + if (renderable.entity) { + m_sceneMgr->destroyEntity(renderable.entity); + } + } + + if (m_selectedEntity == entity) { + setSelectedEntity(flecs::entity::null()); + } + + // Remove from cached list + auto it = std::find(m_allEntities.begin(), m_allEntities.end(), entity); + if (it != m_allEntities.end()) { + m_allEntities.erase(it); + } + + entity.destruct(); +} + +void EditorUISystem::duplicateEntity(flecs::entity entity) +{ + if (!entity.is_alive()) + return; + + flecs::entity newEntity = m_world.entity(); + + // Copy name + if (entity.has()) { + auto &name = entity.get().name; + newEntity.set( + EntityNameComponent(name + " (Copy)")); + } + + // Copy parent relationship using flecs::ChildOf + flecs::entity parent = entity.parent(); + if (parent.is_valid() && parent != 0) { + newEntity.child_of(parent); + } + + // Copy transform + if (entity.has()) { + auto &oldTransform = entity.get(); + TransformComponent newTransform; + + // Find parent node + Ogre::SceneNode *parentNode = m_sceneMgr->getRootSceneNode(); + flecs::entity parent = entity.parent(); + 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 + newTransform.rotation = oldTransform.rotation; + newTransform.scale = oldTransform.scale; + newTransform.applyToNode(); + newEntity.set(newTransform); + } + + // Copy renderable (but not the mesh instance) + if (entity.has()) { + auto &oldRenderable = entity.get(); + RenderableComponent newRenderable; + newRenderable.meshName = oldRenderable.meshName; + newRenderable.visible = oldRenderable.visible; + // Entity will be created when mesh is loaded + newEntity.set(newRenderable); + } + + setSelectedEntity(newEntity); +} + +flecs::entity EditorUISystem::findEntityParent(flecs::entity entity) +{ + if (!entity.is_alive()) + return flecs::entity::null(); + + // Use flecs parent() function + flecs::entity parent = entity.parent(); + if (parent.is_valid() && parent != 0) { + return parent; + } + return flecs::entity::null(); +} + +std::vector +EditorUISystem::getEntityChildren(flecs::entity parent) +{ + std::vector children; + + if (!parent.is_alive()) + return children; + + // Use flecs::ChildOf relationship to get children + parent.children([&](flecs::entity child) { + children.push_back(child); + }); + + return children; +} + +bool EditorUISystem::isDescendantOf(flecs::entity potentialChild, + flecs::entity potentialParent) +{ + if (!potentialChild.is_alive() || !potentialParent.is_alive()) + return false; + + flecs::entity current = findEntityParent(potentialChild); + while (current.is_alive()) { + if (current == potentialParent) + return true; + current = findEntityParent(current); + } + return false; +} diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp new file mode 100644 index 0000000..dfb214e --- /dev/null +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -0,0 +1,103 @@ +#ifndef EDITSCENE_EDITORUISYSTEM_HPP +#define EDITSCENE_EDITORUISYSTEM_HPP +#pragma once +#include +#include +#include +#include "../ui/ComponentRegistry.hpp" +#include "../components/EntityName.hpp" +#include "../gizmo/Gizmo.hpp" + +/** + * Main UI system for the scene editor + * Handles rendering of: + * - Left window: Entity hierarchy + * - Right window: Property editor + * - 3D Gizmo for transform editing + */ +class EditorUISystem { +public: + EditorUISystem(flecs::world &world, Ogre::SceneManager *sceneMgr); + ~EditorUISystem(); + + /** + * Update and render all UI windows + * Call this once per frame + */ + void update(); + + /** + * Set the currently selected entity + */ + void setSelectedEntity(flecs::entity entity); + + /** + * Add existing entity to cache (for entities created before UI) + */ + void addEntity(flecs::entity entity) { m_allEntities.push_back(entity); } + + /** + * Get the currently selected entity + */ + flecs::entity getSelectedEntity() const + { + return m_selectedEntity; + } + + /** + * Handle mouse events for gizmo interaction + * Returns true if gizmo handled the event + */ + bool onMousePressed(const Ogre::Ray &mouseRay); + bool onMouseMoved(const Ogre::Ray &mouseRay, const Ogre::Vector2 &mouseDelta); + bool onMouseReleased(); + + /** + * Get the gizmo for external interaction + */ + Gizmo *getGizmo() const { return m_gizmo.get(); } + +private: + // Window rendering functions + void renderHierarchyWindow(); + void renderPropertyWindow(); + + // Entity operations + void renderEntityNode(flecs::entity entity, int depth); + void renderEntityContextMenu(flecs::entity entity); + void createNewEntity(); + void createChildEntity(flecs::entity parent); + void deleteEntity(flecs::entity entity); + void duplicateEntity(flecs::entity entity); + + // Component operations + void renderComponentList(flecs::entity entity); + void renderAddComponentMenu(flecs::entity entity); + void renderRemoveComponentMenu(flecs::entity entity); + + // Helper functions + void registerComponentEditors(); + flecs::entity findEntityParent(flecs::entity entity); + std::vector getEntityChildren(flecs::entity entity); + bool isDescendantOf(flecs::entity potentialChild, + flecs::entity potentialParent); + + // Core references + flecs::world m_world; + Ogre::SceneManager *m_sceneMgr; + + // State + flecs::entity m_selectedEntity; + ComponentRegistry m_componentRegistry; + std::vector m_allEntities; + std::unique_ptr m_gizmo; + + // Queries + flecs::query m_nameQuery; + + // Constants + static constexpr float LEFT_PANEL_WIDTH = 300.0f; + static constexpr float RIGHT_PANEL_WIDTH = 350.0f; +}; + +#endif // EDITSCENE_EDITORUISYSTEM_HPP diff --git a/src/features/editScene/ui/ComponentEditor.hpp b/src/features/editScene/ui/ComponentEditor.hpp new file mode 100644 index 0000000..3e2bcf8 --- /dev/null +++ b/src/features/editScene/ui/ComponentEditor.hpp @@ -0,0 +1,50 @@ +#ifndef EDITSCENE_COMPONENTEDITOR_HPP +#define EDITSCENE_COMPONENTEDITOR_HPP +#pragma once +#include +#include + +/** + * Base interface for component editors + */ +class IComponentEditor { +public: + virtual ~IComponentEditor() = default; + + /** + * Render the editor UI + * @param entity The flecs entity being edited + * @param component Pointer to the component data + * @return true if the component was modified + */ + virtual bool render(flecs::entity entity, void *component) = 0; + + /** + * Get the display name for this component type + */ + virtual const char *getName() const = 0; +}; + +/** + * Template base class for component editors + * @tparam T The component type to edit + */ +template +class ComponentEditor : public IComponentEditor { +public: + bool render(flecs::entity entity, void *component) override + { + return renderComponent(entity, *static_cast(component)); + } + +protected: + /** + * Implement this method to render the editor for your component + * @param entity The flecs entity being edited + * @param component Reference to the component data + * @return true if the component was modified + */ + virtual bool renderComponent(flecs::entity entity, T &component) = 0; +}; + +#endif // EDITSCENE_COMPONENTEDITOR_HPP diff --git a/src/features/editScene/ui/ComponentRegistry.hpp b/src/features/editScene/ui/ComponentRegistry.hpp new file mode 100644 index 0000000..ef5349e --- /dev/null +++ b/src/features/editScene/ui/ComponentRegistry.hpp @@ -0,0 +1,157 @@ +#ifndef EDITSCENE_COMPONENTREGISTRY_HPP +#define EDITSCENE_COMPONENTREGISTRY_HPP +#pragma once +#include +#include +#include +#include +#include "ComponentEditor.hpp" + +/** + * Function type for adding a component to an entity + */ +using ComponentAdder = std::function; + +/** + * Function type for removing a component from an entity + */ +using ComponentRemover = std::function; + +/** + * Registry for component editors and their add/remove functions + */ +class ComponentRegistry { +public: + struct ComponentInfo { + const char *name; + std::unique_ptr editor; + ComponentAdder adder; + ComponentRemover remover; + }; + + ComponentRegistry() = default; + ~ComponentRegistry() = default; + + // Delete copy + ComponentRegistry(const ComponentRegistry &) = delete; + ComponentRegistry &operator=(const ComponentRegistry &) = delete; + + // Allow move + ComponentRegistry(ComponentRegistry &&) = default; + ComponentRegistry &operator=(ComponentRegistry &&) = default; + + /** + * Register a component type with its editor and add/remove functions + */ + template + void registerComponent(const char *name, + std::unique_ptr> editor, + ComponentAdder adder, ComponentRemover remover) + { + ComponentInfo info; + info.name = name; + info.editor = std::move(editor); + info.adder = adder; + info.remover = remover; + m_components[std::type_index(typeid(T))] = std::move(info); + } + + /** + * Check if a component type is registered + */ + template + bool hasComponent() const + { + return m_components.find(std::type_index(typeid(T))) != + m_components.end(); + } + + /** + * Render a component editor + */ + template + bool render(flecs::entity entity, T &component) + { + auto it = m_components.find(std::type_index(typeid(T))); + if (it != m_components.end() && it->second.editor) { + return it->second.editor->render(entity, &component); + } + return false; + } + + /** + * Add a component to an entity + */ + template + void addComponent(flecs::entity entity) + { + auto it = m_components.find(std::type_index(typeid(T))); + if (it != m_components.end() && it->second.adder) { + it->second.adder(entity); + } + } + + /** + * Remove a component from an entity + */ + template + void removeComponent(flecs::entity entity) + { + auto it = m_components.find(std::type_index(typeid(T))); + if (it != m_components.end() && it->second.remover) { + it->second.remover(entity); + } + } + + /** + * Get all registered component types + */ + std::vector getRegisteredTypes() const + { + std::vector types; + for (const auto &pair : m_components) { + types.push_back(pair.first); + } + return types; + } + + /** + * Get component name for a type + */ + const char *getComponentName(const std::type_index &type) const + { + auto it = m_components.find(type); + if (it != m_components.end()) { + return it->second.name; + } + return "Unknown"; + } + + /** + * Check if entity has a specific component type + */ + bool entityHasComponent(flecs::entity entity, + const std::type_index &type) const + { + // This is a simplified check - in practice you'd need to + // store flecs component IDs and check those + return false; + } + + /** + * Render add component menu for all registered components + * Returns true if a component was added + */ + bool renderAddComponentMenu(flecs::entity entity); + + /** + * Render remove component menu for components the entity has + * Returns true if a component was removed + */ + bool renderRemoveComponentMenu(flecs::entity entity); + +private: + std::unordered_map m_components; +}; + +#endif // EDITSCENE_COMPONENTREGISTRY_HPP diff --git a/src/features/editScene/ui/RenderableEditor.cpp b/src/features/editScene/ui/RenderableEditor.cpp new file mode 100644 index 0000000..610a7cf --- /dev/null +++ b/src/features/editScene/ui/RenderableEditor.cpp @@ -0,0 +1,233 @@ +#include "RenderableEditor.hpp" +#include +#include "../components/Transform.hpp" +#include +#include + +char RenderableEditor::s_meshNameBuffer[256] = { 0 }; +char RenderableEditor::s_searchBuffer[256] = { 0 }; +bool RenderableEditor::s_showMeshBrowser = false; + +RenderableEditor::RenderableEditor(Ogre::SceneManager *sceneMgr) + : m_sceneMgr(sceneMgr) +{ +} + +void RenderableEditor::scanMeshFiles() +{ + if (m_meshesScanned) + return; + + m_meshFiles.clear(); + + // Get all resource groups + Ogre::ResourceGroupManager &rgm = Ogre::ResourceGroupManager::getSingleton(); + Ogre::StringVector groups = rgm.getResourceGroups(); + + for (const auto &group : groups) { + // Get file info for this group + Ogre::FileInfoListPtr fileList = rgm.findResourceFileInfo(group, "*"); + + if (fileList) { + for (const auto &fileInfo : *fileList) { + const std::string &filename = fileInfo.filename; + // Check for supported extensions + if (filename.size() > 5) { + std::string ext = filename.substr(filename.find_last_of('.') + 1); + // Convert to lowercase + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + + if (ext == "mesh" || ext == "glb" || ext == "gltf") { + m_meshFiles.push_back(filename); + } + } + } + } + } + + // Sort and remove duplicates + std::sort(m_meshFiles.begin(), m_meshFiles.end()); + m_meshFiles.erase(std::unique(m_meshFiles.begin(), m_meshFiles.end()), + m_meshFiles.end()); + + m_meshesScanned = true; +} + +bool RenderableEditor::matchesSearch(const std::string &meshName, + const std::string &search) +{ + if (search.empty()) + return true; + + std::string lowerMesh = meshName; + std::string lowerSearch = search; + std::transform(lowerMesh.begin(), lowerMesh.end(), lowerMesh.begin(), + ::tolower); + std::transform(lowerSearch.begin(), lowerSearch.end(), lowerSearch.begin(), + ::tolower); + + return lowerMesh.find(lowerSearch) != std::string::npos; +} + +void RenderableEditor::loadMesh(RenderableComponent &renderable, + flecs::entity entity) +{ + if (renderable.meshName.empty() || !m_sceneMgr) + return; + + try { + // Destroy old entity if exists + if (renderable.entity) { + m_sceneMgr->destroyEntity(renderable.entity); + renderable.entity = nullptr; + } + + // Create new entity + renderable.entity = m_sceneMgr->createEntity(renderable.meshName); + + // Attach to transform node if available + if (entity.has()) { + auto &transform = entity.get_mut(); + if (transform.node && renderable.entity) { + transform.node->attachObject(renderable.entity); + } + } + } catch (const std::exception &e) { + // Error will be displayed in UI + } +} + +bool RenderableEditor::renderComponent(flecs::entity entity, + RenderableComponent &renderable) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Renderable", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Mesh name input + strcpy(s_meshNameBuffer, renderable.meshName.c_str()); + if (ImGui::InputText("Mesh Name", s_meshNameBuffer, + sizeof(s_meshNameBuffer))) { + renderable.meshName = s_meshNameBuffer; + } + + ImGui::SameLine(); + if (ImGui::Button("Browse...")) { + scanMeshFiles(); + s_showMeshBrowser = true; + strcpy(s_searchBuffer, ""); + } + + // Mesh Browser Popup + if (s_showMeshBrowser) { + ImGui::OpenPopup("Mesh Browser"); + } + + if (ImGui::BeginPopupModal("Mesh Browser", &s_showMeshBrowser, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Search:"); + ImGui::SameLine(); + ImGui::InputText("##search", s_searchBuffer, + sizeof(s_searchBuffer)); + + ImGui::Separator(); + + // Show filtered mesh list + ImGui::BeginChild("MeshList", ImVec2(400, 300), true); + + std::string searchStr(s_searchBuffer); + + for (const auto &meshName : m_meshFiles) { + if (!matchesSearch(meshName, searchStr)) + continue; + + // Highlight current selection + bool isCurrent = (renderable.meshName == meshName); + if (isCurrent) { + ImGui::PushStyleColor( + ImGuiCol_Text, + ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); + } + + if (ImGui::Selectable(meshName.c_str(), isCurrent)) { + renderable.meshName = meshName; + strcpy(s_meshNameBuffer, meshName.c_str()); + } + + if (isCurrent) { + ImGui::PopStyleColor(); + } + } + + if (m_meshFiles.empty()) { + ImGui::TextDisabled("No mesh files found"); + } + + ImGui::EndChild(); + + ImGui::Separator(); + + // Show count + int visibleCount = 0; + for (const auto &meshName : m_meshFiles) { + if (matchesSearch(meshName, searchStr)) + visibleCount++; + } + ImGui::Text("Showing %d of %zu meshes", visibleCount, + m_meshFiles.size()); + + // Buttons + if (ImGui::Button("Load", ImVec2(120, 0))) { + loadMesh(renderable, entity); + modified = true; + s_showMeshBrowser = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + s_showMeshBrowser = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Refresh", ImVec2(120, 0))) { + m_meshesScanned = false; + scanMeshFiles(); + } + + ImGui::EndPopup(); + } + + // Load mesh button + if (ImGui::Button("Load Mesh")) { + loadMesh(renderable, entity); + modified = true; + } + + // Visibility toggle + if (renderable.entity) { + bool visible = renderable.entity->getVisible(); + if (ImGui::Checkbox("Visible", &visible)) { + renderable.entity->setVisible(visible); + renderable.visible = visible; + modified = true; + } + } + + // Entity info + if (renderable.entity) { + ImGui::Text("Entity: %p", renderable.entity); + } else { + ImGui::TextDisabled("No entity loaded"); + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/RenderableEditor.hpp b/src/features/editScene/ui/RenderableEditor.hpp new file mode 100644 index 0000000..f37b4d9 --- /dev/null +++ b/src/features/editScene/ui/RenderableEditor.hpp @@ -0,0 +1,36 @@ +#ifndef EDITSCENE_RENDERABLEEDITOR_HPP +#define EDITSCENE_RENDERABLEEDITOR_HPP +#pragma once +#include "ComponentEditor.hpp" +#include "../components/Renderable.hpp" +#include +#include +#include + +/** + * Editor for RenderableComponent + */ +class RenderableEditor : public ComponentEditor { +public: + explicit RenderableEditor(Ogre::SceneManager *sceneMgr); + + const char *getName() const override { return "Renderable"; } + +protected: + bool renderComponent(flecs::entity entity, + RenderableComponent &renderable) override; + +private: + void scanMeshFiles(); + void loadMesh(RenderableComponent &renderable, flecs::entity entity); + bool matchesSearch(const std::string &meshName, const std::string &search); + + Ogre::SceneManager *m_sceneMgr; + std::vector m_meshFiles; + bool m_meshesScanned = false; + static char s_meshNameBuffer[256]; + static char s_searchBuffer[256]; + static bool s_showMeshBrowser; +}; + +#endif // EDITSCENE_RENDERABLEEDITOR_HPP diff --git a/src/features/editScene/ui/TransformEditor.cpp b/src/features/editScene/ui/TransformEditor.cpp new file mode 100644 index 0000000..2203714 --- /dev/null +++ b/src/features/editScene/ui/TransformEditor.cpp @@ -0,0 +1,69 @@ +#include "TransformEditor.hpp" +#include + +bool TransformEditor::renderComponent(flecs::entity entity, + TransformComponent &transform) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Transform", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Position + float pos[3] = { transform.position.x, transform.position.y, + transform.position.z }; + if (ImGui::DragFloat3("Position", pos, 0.1f)) { + transform.position = + Ogre::Vector3(pos[0], pos[1], pos[2]); + modified = true; + } + + // Rotation (using Ogre's getYaw/Pitch/Roll) + float yaw = Ogre::Radian(transform.rotation.getYaw()).valueDegrees(); + float pitch = Ogre::Radian(transform.rotation.getPitch()).valueDegrees(); + float roll = Ogre::Radian(transform.rotation.getRoll()).valueDegrees(); + float rot[3] = { yaw, pitch, roll }; + if (ImGui::DragFloat3("Rotation (Y/P/R)", rot, 0.5f)) { + Ogre::Quaternion q1(Ogre::Degree(rot[0]), Ogre::Vector3::UNIT_Y); + Ogre::Quaternion q2(Ogre::Degree(rot[1]), Ogre::Vector3::UNIT_X); + Ogre::Quaternion q3(Ogre::Degree(rot[2]), Ogre::Vector3::UNIT_Z); + transform.rotation = q1 * q2 * q3; + modified = true; + } + + // Scale + float scale[3] = { transform.scale.x, transform.scale.y, + transform.scale.z }; + if (ImGui::DragFloat3("Scale", scale, 0.01f)) { + transform.scale = + Ogre::Vector3(scale[0], scale[1], scale[2]); + modified = true; + } + + // Reset buttons + if (ImGui::Button("Reset Position")) { + transform.position = Ogre::Vector3::ZERO; + modified = true; + } + ImGui::SameLine(); + if (ImGui::Button("Reset Rotation")) { + transform.rotation = Ogre::Quaternion::IDENTITY; + modified = true; + } + ImGui::SameLine(); + if (ImGui::Button("Reset Scale")) { + transform.scale = Ogre::Vector3::UNIT_SCALE; + modified = true; + } + + // Apply changes to scene node + if (modified && transform.node) { + transform.applyToNode(); + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/TransformEditor.hpp b/src/features/editScene/ui/TransformEditor.hpp new file mode 100644 index 0000000..5e7e10d --- /dev/null +++ b/src/features/editScene/ui/TransformEditor.hpp @@ -0,0 +1,19 @@ +#ifndef EDITSCENE_TRANSFORMEDITOR_HPP +#define EDITSCENE_TRANSFORMEDITOR_HPP +#pragma once +#include "ComponentEditor.hpp" +#include "../components/Transform.hpp" + +/** + * Editor for TransformComponent + */ +class TransformEditor : public ComponentEditor { +public: + const char *getName() const override { return "Transform"; } + +protected: + bool renderComponent(flecs::entity entity, + TransformComponent &transform) override; +}; + +#endif // EDITSCENE_TRANSFORMEDITOR_HPP diff --git a/src/features/sceneEditor/CMakeLists.txt b/src/features/sceneEditor/CMakeLists.txt new file mode 100644 index 0000000..300e716 --- /dev/null +++ b/src/features/sceneEditor/CMakeLists.txt @@ -0,0 +1,35 @@ +project(sceneEditor) +find_package(OGRE REQUIRED COMPONENTS Bites Paging Terrain CONFIG) +find_package(ZLIB) +find_package(SDL2) +find_package(assimp REQUIRED CONFIG) +find_package(OgreProcedural REQUIRED CONFIG) +find_package(pugixml REQUIRED CONFIG) +find_package(flecs REQUIRED CONFIG) +find_package(Tracy REQUIRED CONFIG) +add_executable(editorFeatureDemo main.cpp EditorApp.cpp systems/EditorUISystem.cpp camera/EditorCamera.cpp) +target_link_libraries(editorFeatureDemo OgreMain OgreBites + flecs::flecs_static + ) +file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources.cfg" + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") +add_custom_command(TARGET editorFeatureDemo POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_BINARY_DIR}/resources" + "${CMAKE_CURRENT_BINARY_DIR}/resources" +# COMMAND ${CMAKE_COMMAND} -E copy +# "${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.mesh" +# "${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.mesh" +# COMMAND ${CMAKE_COMMAND} -E copy +# "${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.skeleton" +# "${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.skeleton" +# COMMAND ${CMAKE_COMMAND} -E copy +# "${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.material" +# "${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.material" +# COMMAND ${CMAKE_COMMAND} -E copy +# "${CMAKE_CURRENT_SOURCE_DIR}/resources/main/blue_jaiqua.jpg" +# "${CMAKE_CURRENT_BINARY_DIR}/resources/main/blue_jaiqua.jpg" + DEPENDS "${CMAKE_BINARY_DIR}/resources" + COMMENT "Copying generated resources from root build dir to local build dir" +) + diff --git a/src/features/sceneEditor/EditorApp.cpp b/src/features/sceneEditor/EditorApp.cpp new file mode 100644 index 0000000..9bbe282 --- /dev/null +++ b/src/features/sceneEditor/EditorApp.cpp @@ -0,0 +1,451 @@ +#include "EditorApp.hpp" +#include "systems/EditorUISystem.hpp" +#include "camera/EditorCamera.hpp" +#include "components/Transform.hpp" +#include "components/Renderable.hpp" +#include "components/EditorComponents.hpp" +#include "components/EulerUtils.hpp" + +EditorApp::EditorApp() + : OgreBites::ApplicationContext("OgreEditor") + , mSceneMgr(nullptr) + , mOverlaySystem(nullptr) + , mImGuiOverlay(nullptr) + , mIsDraggingCursor(false) + , mCurrentModifiers(0) + , mGridVisible(true) + , mAxesVisible(true) + , m3DCursorPosition(Ogre::Vector3::ZERO) +{ +} + +EditorApp::~EditorApp() +{ + if (mImGuiOverlay) { + delete mImGuiOverlay; + mImGuiOverlay = nullptr; + } + if (mOverlaySystem) { + delete mOverlaySystem; + mOverlaySystem = nullptr; + } +} + +void EditorApp::setup() +{ + // Call base class setup first + OgreBites::ApplicationContext::setup(); + + // Get root and setup scene + Ogre::Root *root = getRoot(); + if (!root) { + OGRE_EXCEPT(Ogre::Exception::ERR_RT_ASSERTION_FAILED, + "Failed to get Ogre::Root", "EditorApp::setup"); + return; + } + + mSceneMgr = root->createSceneManager(); + if (!mSceneMgr) { + OGRE_EXCEPT(Ogre::Exception::ERR_RT_ASSERTION_FAILED, + "Failed to create SceneManager", + "EditorApp::setup"); + return; + } + + // Setup overlay system + mOverlaySystem = new Ogre::OverlaySystem(); + mSceneMgr->addRenderQueueListener(mOverlaySystem); + + // Setup render window + Ogre::RenderWindow *window = getRenderWindow(); + if (!window) { + OGRE_EXCEPT(Ogre::Exception::ERR_RT_ASSERTION_FAILED, + "Failed to get RenderWindow", "EditorApp::setup"); + return; + } + + // Setup ImGui overlay + setupImGui(); + + setupFlecs(); + setupScene(); + + // Setup editor camera + mEditorCamera = std::make_unique(mSceneMgr, window); + + // Setup UI system + mUISystem = std::make_unique(mWorld, mSceneMgr, + m3DCursorPosition); + + // Set the cursor moved callback + mUISystem->setOnCursorMoved( + [this](const Ogre::Vector3 &pos) { m3DCursorPosition = pos; }); + + // Register input listeners + addInputListener(this); + + // Add ImGui input listener - this is a method of ApplicationContext + OgreBites::InputListener *imguiListener = getImGuiInputListener(); + if (imguiListener) { + addInputListener(imguiListener); + } +} + +void EditorApp::setupFlecs() +{ + // Configure flecs + mWorld.set_entity_range(1, 10000); + + // Register components using the helper function + registerComponents(mWorld); + + // Create default entities + flecs::entity rootEntity = mWorld.entity(); + rootEntity.set({ "Root" }); + + // Create a test entity with mesh + flecs::entity testEntity = mWorld.entity(); + testEntity.set({ "Test Cube" }); + + // Create transform with scene node + TransformComponent transform; + transform.node = mSceneMgr->getRootSceneNode()->createChildSceneNode(); + transform.position = Ogre::Vector3(0, 0, 0); + transform.applyToNode(); + testEntity.set(transform); + + // Try to load a mesh + try { + RenderableComponent renderable("cube.mesh"); + renderable.entity = mSceneMgr->createEntity("cube.mesh"); + testEntity.set(renderable); + + // Attach to node + if (testEntity.has()) { + auto &testTransform = + testEntity.get_mut(); + if (testTransform.node && renderable.entity) { + testTransform.node->attachObject( + renderable.entity); + } + } + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "Failed to load cube.mesh: " + std::string(e.what())); + } + + // Optional: Add a system for auto-updating transforms + mWorld.system().each( + [](flecs::entity e, TransformComponent &transform) { + // Auto-sync transforms if needed + // transform.updateFromNode(); + }); +} + +void EditorApp::setupScene() +{ + // Setup ambient light + mSceneMgr->setAmbientLight(Ogre::ColourValue(0.3f, 0.3f, 0.3f)); + + // Create directional light with scene node + Ogre::Light *directionalLight = mSceneMgr->createLight("MainLight"); + directionalLight->setType(Ogre::Light::LT_DIRECTIONAL); + directionalLight->setDiffuseColour(Ogre::ColourValue(1.0f, 1.0f, 1.0f)); + directionalLight->setSpecularColour( + Ogre::ColourValue(0.5f, 0.5f, 0.5f)); + + // Create scene node for directional light and set direction + Ogre::SceneNode *lightNode = + mSceneMgr->getRootSceneNode()->createChildSceneNode(); + lightNode->attachObject(directionalLight); + lightNode->setDirection(Ogre::Vector3(1, -1, 0), Ogre::Node::TS_WORLD); + + // Create a fill light from below + Ogre::Light *fillLight = mSceneMgr->createLight("FillLight"); + fillLight->setType(Ogre::Light::LT_DIRECTIONAL); + fillLight->setDiffuseColour(Ogre::ColourValue(0.4f, 0.4f, 0.4f)); + fillLight->setSpecularColour(Ogre::ColourValue(0.2f, 0.2f, 0.2f)); + + Ogre::SceneNode *fillLightNode = + mSceneMgr->getRootSceneNode()->createChildSceneNode(); + fillLightNode->attachObject(fillLight); + fillLightNode->setDirection(Ogre::Vector3(-0.5f, -1.0f, 0.5f), + Ogre::Node::TS_WORLD); + + // Optional: Add a grid helper with named node for toggling + try { + Ogre::Plane plane(Ogre::Vector3::UNIT_Y, -1); + Ogre::MeshManager::getSingleton().createPlane( + "grid_plane", + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + plane, 20, 20, 20, 20, true, 1, 5, 5, + Ogre::Vector3::UNIT_Z); + + Ogre::Entity *gridEntity = + mSceneMgr->createEntity("grid_plane"); + + // Try to set a grid material if available + if (Ogre::MaterialManager::getSingleton().resourceExists( + "GridMaterial")) { + gridEntity->setMaterialName("GridMaterial"); + } + + // Create node with a name for easy access + Ogre::SceneNode *gridNode = + mSceneMgr->getRootSceneNode()->createChildSceneNode( + "GridNode"); + gridNode->attachObject(gridEntity); + gridNode->setPosition(0, -1, 0); + + Ogre::LogManager::getSingleton().logMessage( + "Grid created successfully"); + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "Grid plane creation failed: " + std::string(e.what())); + } + + // Add a simple axis helper + createAxisHelper(); +} + +void EditorApp::createAxisHelper() +{ + try { + // X-axis (red) + Ogre::ManualObject *axisX = + mSceneMgr->createManualObject("AxisX"); + axisX->begin("BaseWhiteNoLighting", + Ogre::RenderOperation::OT_LINE_LIST); + axisX->colour(Ogre::ColourValue(1.0f, 0.0f, 0.0f)); + axisX->position(0, 0, 0); + axisX->position(2, 0, 0); + axisX->end(); + + // Y-axis (green) + Ogre::ManualObject *axisY = + mSceneMgr->createManualObject("AxisY"); + axisY->begin("BaseWhiteNoLighting", + Ogre::RenderOperation::OT_LINE_LIST); + axisY->colour(Ogre::ColourValue(0.0f, 1.0f, 0.0f)); + axisY->position(0, 0, 0); + axisY->position(0, 2, 0); + axisY->end(); + + // Z-axis (blue) + Ogre::ManualObject *axisZ = + mSceneMgr->createManualObject("AxisZ"); + axisZ->begin("BaseWhiteNoLighting", + Ogre::RenderOperation::OT_LINE_LIST); + axisZ->colour(Ogre::ColourValue(0.0f, 0.0f, 1.0f)); + axisZ->position(0, 0, 0); + axisZ->position(0, 0, 2); + axisZ->end(); + + // Create named node for easy access + Ogre::SceneNode *axisNode = + mSceneMgr->getRootSceneNode()->createChildSceneNode( + "AxisNode"); + axisNode->attachObject(axisX); + axisNode->attachObject(axisY); + axisNode->attachObject(axisZ); + + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "Axis helper creation failed: " + + std::string(e.what())); + } +} + +void EditorApp::setupImGui() +{ + // Create ImGui overlay using Ogre's built-in integration + mImGuiOverlay = new Ogre::ImGuiOverlay(); + + // Show the overlay - this enables it in the overlay system + mImGuiOverlay->show(); + + // Setup ImGui style + ImGui::StyleColorsDark(); + + // Configure ImGui IO (standard features only) + ImGuiIO &io = ImGui::GetIO(); + io.ConfigWindowsMoveFromTitleBarOnly = true; + + Ogre::LogManager::getSingleton().logMessage( + "ImGui overlay initialized"); +} + +bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) +{ + // Update camera + if (mEditorCamera) { + mEditorCamera->update(evt.timeSinceLastFrame); + } + + // Start ImGui frame + if (mImGuiOverlay) { + mImGuiOverlay->NewFrame(); + } + + // Update UI system (this will render all ImGui windows) + if (mUISystem) { + mUISystem->update(); + } + + return OgreBites::ApplicationContext::frameRenderingQueued(evt); +} + +bool EditorApp::mouseMoved(const OgreBites::MouseMotionEvent &evt) +{ + if (mEditorCamera) { + mEditorCamera->handleMouseMove(evt); + } + return true; +} + +bool EditorApp::mousePressed(const OgreBites::MouseButtonEvent &evt) +{ + if (mEditorCamera) { + mEditorCamera->handleMousePress(evt); + + // Check for Ctrl key using current modifier state + if (evt.button == OgreBites::BUTTON_LEFT && isCtrlPressed()) { + Ogre::Viewport *vp = + mEditorCamera->getCamera()->getViewport(); + if (vp) { + float screenX = static_cast(evt.x) / + static_cast( + vp->getActualWidth()); + float screenY = static_cast(evt.y) / + static_cast( + vp->getActualHeight()); + + Ogre::Vector3 cursorPos = + mEditorCamera->getMouseRay(screenX, + screenY); + m3DCursorPosition = cursorPos; + } + } + } + return true; +} + +bool EditorApp::mouseReleased(const OgreBites::MouseButtonEvent &evt) +{ + if (mEditorCamera) { + mEditorCamera->handleMouseRelease(evt); + } + return true; +} + +bool EditorApp::keyPressed(const OgreBites::KeyboardEvent &evt) +{ + if (mEditorCamera) { + mEditorCamera->handleKeyboard(evt); + } + + // Update current modifiers from the event + mCurrentModifiers = evt.keysym.mod; + + // Handle delete key + if (evt.keysym.sym == OgreBites::SDLK_DELETE) { + if (mUISystem && mSelectedEntity) { + // Clean up scene node + if (mSelectedEntity.has()) { + auto &transform = + mSelectedEntity + .get_mut(); + if (transform.node) { + mSceneMgr->destroySceneNode( + transform.node); + transform.node = nullptr; + } + } + + // Clean up renderable + if (mSelectedEntity.has()) { + auto &renderable = + mSelectedEntity + .get_mut(); + if (renderable.entity) { + mSceneMgr->destroyEntity( + renderable.entity); + renderable.entity = nullptr; + } + } + + mSelectedEntity.destruct(); + mUISystem->setSelectedEntity(flecs::entity::null()); + } + } + + // Handle F5 for reloading resources + if (evt.keysym.sym == OgreBites::SDLK_F5) { + reloadResources(); + } + + // Handle F3 for showing/hiding grid + if (evt.keysym.sym == OgreBites::SDLK_F3) { + toggleGrid(); + } + + // Handle F4 for showing/hiding axes + if (evt.keysym.sym == OgreBites::SDLK_F4) { + toggleAxes(); + } + + return true; +} + +bool EditorApp::keyReleased(const OgreBites::KeyboardEvent &evt) +{ + // Update current modifiers from the event + mCurrentModifiers = evt.keysym.mod; + + return true; +} + +void EditorApp::toggleGrid() +{ + // Use getChild and cast to SceneNode + Ogre::Node *node = mSceneMgr->getRootSceneNode()->getChild("GridNode"); + if (node) { + Ogre::SceneNode *gridNode = + static_cast(node); + mGridVisible = !mGridVisible; + gridNode->setVisible(mGridVisible); + Ogre::LogManager::getSingleton().logMessage( + "Grid visibility: " + + std::string(mGridVisible ? "ON" : "OFF")); + } else { + Ogre::LogManager::getSingleton().logMessage( + "Grid node not found"); + } +} + +void EditorApp::toggleAxes() +{ + // Use getChild and cast to SceneNode + Ogre::Node *node = mSceneMgr->getRootSceneNode()->getChild("AxisNode"); + if (node) { + Ogre::SceneNode *axisNode = + static_cast(node); + mAxesVisible = !mAxesVisible; + axisNode->setVisible(mAxesVisible); + Ogre::LogManager::getSingleton().logMessage( + "Axes visibility: " + + std::string(mAxesVisible ? "ON" : "OFF")); + } else { + Ogre::LogManager::getSingleton().logMessage( + "Axis node not found"); + } +} + +void EditorApp::reloadResources() +{ + // Reload all materials + Ogre::LogManager::getSingleton().logMessage("Reloading materials..."); + Ogre::MaterialManager::getSingleton().reloadAll(); + + Ogre::LogManager::getSingleton().logMessage("Resources reloaded"); +} diff --git a/src/features/sceneEditor/EditorApp.hpp b/src/features/sceneEditor/EditorApp.hpp new file mode 100644 index 0000000..3b1e0a3 --- /dev/null +++ b/src/features/sceneEditor/EditorApp.hpp @@ -0,0 +1,73 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +// Forward declarations +class EditorUISystem; +class EditorCamera; + +class EditorApp : public OgreBites::ApplicationContext, + public OgreBites::InputListener +{ +public: + EditorApp(); + virtual ~EditorApp(); + + void setup() override; + bool frameRenderingQueued(const Ogre::FrameEvent& evt) override; + + // Ogre 14.5 input handling + bool mouseMoved(const OgreBites::MouseMotionEvent& evt) override; + bool mousePressed(const OgreBites::MouseButtonEvent& evt) override; + bool mouseReleased(const OgreBites::MouseButtonEvent& evt) override; + bool keyPressed(const OgreBites::KeyboardEvent& evt) override; + bool keyReleased(const OgreBites::KeyboardEvent& evt) override; + + // Setup methods + void setupScene(); + void setupImGui(); + void setupFlecs(); + void createAxisHelper(); + + // Helper methods + void toggleGrid(); + void toggleAxes(); + void reloadResources(); + + // Getters + flecs::entity getSelectedEntity() const { return mSelectedEntity; } + Ogre::SceneManager* getSceneManager() const { return mSceneMgr; } + + // Helper to check modifier keys using current state + bool isCtrlPressed() const { return (mCurrentModifiers & OgreBites::KMOD_CTRL) != 0; } + bool isShiftPressed() const { return (mCurrentModifiers & OgreBites::KMOD_SHIFT) != 0; } + bool isAltPressed() const { return (mCurrentModifiers & OgreBites::KMOD_ALT) != 0; } + +private: + // Ogre objects + Ogre::SceneManager* mSceneMgr; + Ogre::OverlaySystem* mOverlaySystem; + Ogre::ImGuiOverlay* mImGuiOverlay; + + // ECS + flecs::world mWorld; + flecs::entity mSelectedEntity; + + // Editor state + Ogre::Vector3 m3DCursorPosition; + bool mIsDraggingCursor; + bool mGridVisible; + bool mAxesVisible; + + // Modifier key tracking + uint16_t mCurrentModifiers; + + // Editor systems + std::unique_ptr mUISystem; + std::unique_ptr mEditorCamera; +}; diff --git a/src/features/sceneEditor/camera/EditorCamera.cpp b/src/features/sceneEditor/camera/EditorCamera.cpp new file mode 100644 index 0000000..bf03966 --- /dev/null +++ b/src/features/sceneEditor/camera/EditorCamera.cpp @@ -0,0 +1,259 @@ +#include "EditorCamera.hpp" +#include +#include +#include +#include +#include + +EditorCamera::EditorCamera(Ogre::SceneManager *sceneMgr, + Ogre::RenderWindow *window) + : mSceneMgr(sceneMgr) + , mWindow(window) + , mYaw(0.0f) + , mPitch(Ogre::Math::DegreesToRadians( + 45.0f)) // Convert 45 degrees to radians + , mDistance(15.0f) + , mTargetPosition(0, 0, 0) + , mCursorPosition(0, 0, 0) + , mActive(true) + , mMiddleMouseDown(false) + , mRightMouseDown(false) + , mLastMouseX(0) + , mLastMouseY(0) + , mMoveSpeed(10.0f) + , mRotateSpeed(0.005f) + , mZoomSpeed(0.5f) +{ + // Create camera + mCamera = mSceneMgr->createCamera("EditorCamera"); + mCamera->setNearClipDistance(0.1f); + mCamera->setFarClipDistance(1000.0f); + mCamera->setAutoAspectRatio(true); + + // Create camera node and attach camera + mCameraNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(); + mCameraNode->attachObject(mCamera); + + // Create target node for looking at + mTargetNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(); + mTargetNode->setPosition(mTargetPosition); + + // Set viewport + Ogre::Viewport *vp = mWindow->addViewport(mCamera); + vp->setBackgroundColour(Ogre::ColourValue(0.1f, 0.1f, 0.1f)); + mCamera->setAspectRatio(Ogre::Real(vp->getActualWidth()) / + Ogre::Real(vp->getActualHeight())); + + // Initialize camera position + updateCameraTransform(); +} + +EditorCamera::~EditorCamera() +{ + if (mSceneMgr) { + if (mCamera) { + mCameraNode->detachObject(mCamera); + mSceneMgr->destroyCamera(mCamera); + } + if (mCameraNode) { + mSceneMgr->destroySceneNode(mCameraNode); + } + if (mTargetNode) { + mSceneMgr->destroySceneNode(mTargetNode); + } + } +} + +void EditorCamera::update(float deltaTime) +{ + if (!mActive) + return; + // Camera transform is updated in real-time through input handlers +} + +void EditorCamera::handleMouseMove(const OgreBites::MouseMotionEvent &evt) +{ + if (!mActive) + return; + + int deltaX = evt.x - mLastMouseX; + int deltaY = evt.y - mLastMouseY; + + if (mMiddleMouseDown) { + // Pan - move target position + Ogre::Vector3 right = + mCameraNode->getOrientation() * Ogre::Vector3::UNIT_X; + Ogre::Vector3 up = + mCameraNode->getOrientation() * Ogre::Vector3::UNIT_Y; + + float moveScale = mMoveSpeed * 0.01f; + Ogre::Vector3 panDelta = (right * static_cast(-deltaX) + + up * static_cast(deltaY)) * + moveScale; + + mTargetPosition += panDelta; + mTargetNode->setPosition(mTargetPosition); + updateCameraTransform(); + } else if (mRightMouseDown) { + // Orbit - rotate around target + // Convert delta to radians (delta is in pixels, mRotateSpeed is in radians per pixel) + orbit(static_cast(deltaX) * mRotateSpeed, + static_cast(deltaY) * mRotateSpeed); + } + + mLastMouseX = evt.x; + mLastMouseY = evt.y; +} + +void EditorCamera::handleMousePress(const OgreBites::MouseButtonEvent &evt) +{ + if (evt.button == OgreBites::BUTTON_MIDDLE) { + mMiddleMouseDown = true; + } else if (evt.button == OgreBites::BUTTON_RIGHT) { + mRightMouseDown = true; + } + + mLastMouseX = evt.x; + mLastMouseY = evt.y; +} + +void EditorCamera::handleMouseRelease(const OgreBites::MouseButtonEvent &evt) +{ + if (evt.button == OgreBites::BUTTON_MIDDLE) { + mMiddleMouseDown = false; + } else if (evt.button == OgreBites::BUTTON_RIGHT) { + mRightMouseDown = false; + } +} + +void EditorCamera::handleKeyboard(const OgreBites::KeyboardEvent &evt) +{ + if (!mActive) + return; + + float moveDelta = mMoveSpeed * 0.016f; // Assume 60fps + + Ogre::Vector3 forward = + mCameraNode->getOrientation() * Ogre::Vector3::NEGATIVE_UNIT_Z; + Ogre::Vector3 right = + mCameraNode->getOrientation() * Ogre::Vector3::UNIT_X; + forward.y = 0; // Keep movement horizontal + right.y = 0; + forward.normalise(); + right.normalise(); + + Ogre::Vector3 moveDeltaVec = Ogre::Vector3::ZERO; + + // Use character constants instead of SDLK_ constants + if (evt.keysym.sym == 'w') { + moveDeltaVec += forward * moveDelta; + } else if (evt.keysym.sym == 's') { + moveDeltaVec -= forward * moveDelta; + } else if (evt.keysym.sym == 'a') { + moveDeltaVec -= right * moveDelta; + } else if (evt.keysym.sym == 'd') { + moveDeltaVec += right * moveDelta; + } else if (evt.keysym.sym == 'q') { + moveDeltaVec.y += moveDelta; + } else if (evt.keysym.sym == 'e') { + moveDeltaVec.y -= moveDelta; + } + + if (moveDeltaVec != Ogre::Vector3::ZERO) { + mTargetPosition += moveDeltaVec; + mTargetNode->setPosition(mTargetPosition); + updateCameraTransform(); + } +} + +void EditorCamera::setPosition(const Ogre::Vector3 &pos) +{ + // Calculate distance and angles from target + Ogre::Vector3 offset = pos - mTargetPosition; + mDistance = offset.length(); + + // Calculate yaw and pitch from the offset + mYaw = std::atan2(offset.z, offset.x); + mPitch = std::asin(offset.y / mDistance); + + updateCameraTransform(); +} + +void EditorCamera::setTarget(const Ogre::Vector3 &target) +{ + mTargetPosition = target; + mTargetNode->setPosition(mTargetPosition); + updateCameraTransform(); +} + +void EditorCamera::orbit(float deltaYaw, float deltaPitch) +{ + mYaw += deltaYaw; + mPitch += deltaPitch; + + // Clamp pitch to avoid gimbal lock (-89 to 89 degrees) + // Convert degree limits to radians using Ogre::Math::DegreesToRadians + float maxPitch = Ogre::Math::DegreesToRadians(89.0f); + float minPitch = Ogre::Math::DegreesToRadians(-89.0f); + mPitch = std::clamp(mPitch, minPitch, maxPitch); + + updateCameraTransform(); +} + +void EditorCamera::pan(const Ogre::Vector3 &delta) +{ + mTargetPosition += delta; + mTargetNode->setPosition(mTargetPosition); + updateCameraTransform(); +} + +void EditorCamera::zoom(float delta) +{ + mDistance -= delta; + mDistance = std::clamp(mDistance, 1.0f, 500.0f); + updateCameraTransform(); +} + +void EditorCamera::updateCameraTransform() +{ + // Calculate camera position based on spherical coordinates + float cosPitch = std::cos(mPitch); + float sinPitch = std::sin(mPitch); + float cosYaw = std::cos(mYaw); + float sinYaw = std::sin(mYaw); + + Ogre::Vector3 offset; + offset.x = mDistance * cosPitch * cosYaw; + offset.y = mDistance * sinPitch; + offset.z = mDistance * cosPitch * sinYaw; + + // Set camera node position + Ogre::Vector3 cameraPosition = mTargetPosition + offset; + mCameraNode->setPosition(cameraPosition); + + // Make camera look at target + mCameraNode->lookAt(mTargetPosition, Ogre::Node::TS_WORLD); +} + +Ogre::Vector3 EditorCamera::getMouseRay(float screenX, float screenY) +{ + if (!mCamera) + return Ogre::Vector3::ZERO; + + // Create ray from camera through mouse position + Ogre::Ray ray = mCamera->getCameraToViewportRay(screenX, screenY); + + // Create a plane at Y=0 for ground intersection + Ogre::Plane groundPlane(Ogre::Vector3::UNIT_Y, 0); + + // Calculate intersection with ground plane + std::pair intersection = ray.intersects(groundPlane); + + if (intersection.first) { + // Return the intersection point + return ray.getPoint(intersection.second); + } + + // If no intersection, return a point at a default distance along the ray + return ray.getPoint(100.0f); +} diff --git a/src/features/sceneEditor/camera/EditorCamera.hpp b/src/features/sceneEditor/camera/EditorCamera.hpp new file mode 100644 index 0000000..f8699dc --- /dev/null +++ b/src/features/sceneEditor/camera/EditorCamera.hpp @@ -0,0 +1,67 @@ +#ifndef EDITORCAMERA_HPP +#define EDITORCAMERA_HPP +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +class EditorCamera { +public: + EditorCamera(Ogre::SceneManager* sceneMgr, Ogre::RenderWindow* window); + ~EditorCamera(); + + void update(float deltaTime); + void setActive(bool active); + Ogre::Camera* getCamera() const { return mCamera; } + Ogre::SceneNode* getCameraNode() const { return mCameraNode; } + + void handleMouseMove(const OgreBites::MouseMotionEvent& evt); + void handleMousePress(const OgreBites::MouseButtonEvent& evt); + void handleMouseRelease(const OgreBites::MouseButtonEvent& evt); + void handleKeyboard(const OgreBites::KeyboardEvent& evt); + + Ogre::Vector3 getMouseRay(float screenX, float screenY); + Ogre::Vector3 getCursorPosition() const { return mCursorPosition; } + void setCursorPosition(const Ogre::Vector3& pos) { mCursorPosition = pos; } + + // Camera control methods + void setPosition(const Ogre::Vector3& pos); + void setTarget(const Ogre::Vector3& target); + void orbit(float deltaYaw, float deltaPitch); + void pan(const Ogre::Vector3& delta); + void zoom(float delta); + +private: + Ogre::SceneManager* mSceneMgr; + Ogre::Camera* mCamera; + Ogre::SceneNode* mCameraNode; + Ogre::SceneNode* mTargetNode; // Node for target position + Ogre::RenderWindow* mWindow; + + // Camera parameters + float mYaw; + float mPitch; + float mDistance; + Ogre::Vector3 mTargetPosition; + Ogre::Vector3 mCursorPosition; + + // Input state + bool mActive; + bool mMiddleMouseDown; + bool mRightMouseDown; + int mLastMouseX; + int mLastMouseY; + + // Control speeds + float mMoveSpeed; + float mRotateSpeed; + float mZoomSpeed; + + void updateCameraTransform(); +}; +#endif // EDITORCAMERA_HPP diff --git a/src/features/sceneEditor/components/EditorComponents.hpp b/src/features/sceneEditor/components/EditorComponents.hpp new file mode 100644 index 0000000..a28d902 --- /dev/null +++ b/src/features/sceneEditor/components/EditorComponents.hpp @@ -0,0 +1,4 @@ +#ifndef EDITORCOMPONENTS_HPP +#define EDITORCOMPONENTS_HPP + +#endif // EDITORCOMPONENTS_HPP diff --git a/src/features/sceneEditor/components/EulerUtils.hpp b/src/features/sceneEditor/components/EulerUtils.hpp new file mode 100644 index 0000000..71205dc --- /dev/null +++ b/src/features/sceneEditor/components/EulerUtils.hpp @@ -0,0 +1,102 @@ +#ifndef EULERUTILS_HPP +#define EULERUTILS_HPP +#pragma once +#include +#include +#include + +namespace EulerUtils { + + // Convert Euler angles (in degrees) to quaternion + // Order: Pitch (X), Yaw (Y), Roll (Z) + inline Ogre::Quaternion fromEulerDegrees(const Ogre::Vector3& degrees) { + Ogre::Radian pitch(Ogre::Degree(degrees.x)); + Ogre::Radian yaw(Ogre::Degree(degrees.y)); + Ogre::Radian roll(Ogre::Degree(degrees.z)); + + // Create quaternions for each axis + Ogre::Quaternion qPitch(pitch, Ogre::Vector3::UNIT_X); + Ogre::Quaternion qYaw(yaw, Ogre::Vector3::UNIT_Y); + Ogre::Quaternion qRoll(roll, Ogre::Vector3::UNIT_Z); + + // Combine in ZYX order (roll, yaw, pitch) + return qYaw * qPitch * qRoll; + } + + // Convert quaternion to Euler angles (in degrees) + // Returns Vector3 (pitch, yaw, roll) in degrees + inline Ogre::Vector3 toEulerDegrees(const Ogre::Quaternion& quat) { + // Extract rotation matrix from quaternion + Ogre::Matrix3 rotMat; + quat.ToRotationMatrix(rotMat); + + // Extract Euler angles from rotation matrix + // Using XYZ (pitch, yaw, roll) order + Ogre::Radian pitch, yaw, roll; + rotMat.ToEulerAnglesXYZ(pitch, yaw, roll); + + return Ogre::Vector3( + pitch.valueDegrees(), + yaw.valueDegrees(), + roll.valueDegrees() + ); + } + + // Alternative implementation using quaternion math (more stable) + inline Ogre::Vector3 toEulerDegreesStable(const Ogre::Quaternion& quat) { + // Extract quaternion components + float w = quat.w; + float x = quat.x; + float y = quat.y; + float z = quat.z; + + // Pitch (X-axis rotation) + float sinr_cosp = 2.0f * (w * x + y * z); + float cosr_cosp = 1.0f - 2.0f * (x * x + y * y); + float pitch = std::atan2(sinr_cosp, cosr_cosp); + + // Yaw (Y-axis rotation) + float sinp = 2.0f * (w * y - z * x); + float yaw; + if (std::abs(sinp) >= 1.0f) { + yaw = std::copysign(Ogre::Math::HALF_PI, sinp); + } else { + yaw = std::asin(sinp); + } + + // Roll (Z-axis rotation) + float siny_cosp = 2.0f * (w * z + x * y); + float cosy_cosp = 1.0f - 2.0f * (y * y + z * z); + float roll = std::atan2(siny_cosp, cosy_cosp); + + return Ogre::Vector3( + Ogre::Radian(pitch).valueDegrees(), + Ogre::Radian(yaw).valueDegrees(), + Ogre::Radian(roll).valueDegrees() + ); + } + + // Apply rotation to a vector using quaternion + inline Ogre::Vector3 rotateVector(const Ogre::Quaternion& quat, const Ogre::Vector3& vec) { + return quat * vec; + } + + // Clamp Euler angles to reasonable ranges + inline Ogre::Vector3 clampEulerDegrees(const Ogre::Vector3& euler, float minDeg = -180.0f, float maxDeg = 180.0f) { + return Ogre::Vector3( + std::clamp(euler.x, minDeg, maxDeg), + std::clamp(euler.y, minDeg, maxDeg), + std::clamp(euler.z, minDeg, maxDeg) + ); + } + + // Normalize Euler angles to [-180, 180] range + inline Ogre::Vector3 normalizeEulerDegrees(const Ogre::Vector3& euler) { + return Ogre::Vector3( + std::fmod(euler.x + 180.0f, 360.0f) - 180.0f, + std::fmod(euler.y + 180.0f, 360.0f) - 180.0f, + std::fmod(euler.z + 180.0f, 360.0f) - 180.0f + ); + } +} +#endif // EULERUTILS_HPP diff --git a/src/features/sceneEditor/components/Renderable.hpp b/src/features/sceneEditor/components/Renderable.hpp new file mode 100644 index 0000000..d4061f1 --- /dev/null +++ b/src/features/sceneEditor/components/Renderable.hpp @@ -0,0 +1,4 @@ +#ifndef RENDERABLE_HPP +#define RENDERABLE_HPP + +#endif // RENDERABLE_HPP diff --git a/src/features/sceneEditor/components/Transform.hpp b/src/features/sceneEditor/components/Transform.hpp new file mode 100644 index 0000000..cd1c26e --- /dev/null +++ b/src/features/sceneEditor/components/Transform.hpp @@ -0,0 +1,77 @@ +#pragma once +#include +#include +#include +#include "EulerUtils.hpp" + +// Component definitions +struct TransformComponent { + Ogre::SceneNode* node; + Ogre::Vector3 position; + Ogre::Quaternion orientation; + Ogre::Vector3 scale; + + TransformComponent() : node(nullptr), position(Ogre::Vector3::ZERO), + orientation(Ogre::Quaternion::IDENTITY), + scale(Ogre::Vector3::UNIT_SCALE) {} + + void applyToNode() { + if (node) { + node->setPosition(position); + node->setOrientation(orientation); + node->setScale(scale); + } + } + + void updateFromNode() { + if (node) { + position = node->getPosition(); + orientation = node->getOrientation(); + scale = node->getScale(); + } + } + + void setEulerDegrees(const Ogre::Vector3& degrees) { + orientation = EulerUtils::fromEulerDegrees(degrees); + applyToNode(); + } + + Ogre::Vector3 getEulerDegrees() const { + return EulerUtils::toEulerDegrees(orientation); + } + + void rotateEulerDegrees(const Ogre::Vector3& deltaDegrees) { + Ogre::Vector3 current = getEulerDegrees(); + Ogre::Vector3 newEuler = current + deltaDegrees; + newEuler = EulerUtils::normalizeEulerDegrees(newEuler); + setEulerDegrees(newEuler); + } +}; + +struct RenderableComponent { + Ogre::Entity* entity; + std::string meshName; + + RenderableComponent() : entity(nullptr) {} + RenderableComponent(const std::string& mesh) : meshName(mesh), entity(nullptr) {} +}; + +struct EditorTag { + bool isSelected; + EditorTag() : isSelected(false) {} +}; + +struct EntityNameComponent { + std::string name; + EntityNameComponent() : name("Entity") {} + EntityNameComponent(const std::string& n) : name(n) {} +}; + +// Register components with flecs 4.1.4 +inline void registerComponents(flecs::world& world) { + // Simple component registration - this is all that's needed + world.component(); + world.component(); + world.component(); + world.component(); +} diff --git a/src/features/sceneEditor/main.cpp b/src/features/sceneEditor/main.cpp new file mode 100644 index 0000000..2de70e4 --- /dev/null +++ b/src/features/sceneEditor/main.cpp @@ -0,0 +1,17 @@ +#include +#include "EditorApp.hpp" + +int main(int argc, char *argv[]) +{ + try { + EditorApp app; + app.initApp(); + app.getRoot()->startRendering(); + app.closeApp(); + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/src/features/sceneEditor/resources.cfg b/src/features/sceneEditor/resources.cfg new file mode 100644 index 0000000..e2b30cc --- /dev/null +++ b/src/features/sceneEditor/resources.cfg @@ -0,0 +1,82 @@ +# Ogre Core Resources +[OgreInternal] +#FileSystem=./Media/Main +FileSystem=resources/main +FileSystem=resources/shaderlib +#FileSystem=./Media/RTShaderLib +FileSystem=resources/terrain + +# Resources required by OgreBites::Trays +[Essential] +#Zip=./Media/packs/SdkTrays.zip +#Zip=./Media/packs/profiler.zip + +## this line will end up in the [Essential] group +#FileSystem=./Media/thumbnails + +# Common sample resources needed by many of the samples. +# Rarely used resources should be separately loaded by the +# samples which require them. +[General] +FileSystem=skybox +FileSystem=resources/buildings +FileSystem=resources/buildings/parts/pier +FileSystem=resources/buildings/parts/furniture +FileSystem=resources/vehicles +FileSystem=resources/debug +FileSystem=resources/fonts +# PBR media must come before the scripts that reference it +#FileSystem=./Media/PBR +#FileSystem=./Media/PBR/filament + +#FileSystem=./Media/materials/programs/GLSL +#FileSystem=./Media/materials/programs/GLSL120 +#FileSystem=./Media/materials/programs/GLSL150 +#FileSystem=./Media/materials/programs/GLSL400 +#FileSystem=./Media/materials/programs/GLSLES +#FileSystem=./Media/materials/programs/SPIRV +#FileSystem=./Media/materials/programs/Cg +#FileSystem=./Media/materials/programs/HLSL +#FileSystem=./Media/materials/programs/HLSL_Cg +#FileSystem=./Media/materials/scripts +#FileSystem=./Media/materials/textures +#FileSystem=./Media/materials/textures/terrain +#FileSystem=./Media/models +#FileSystem=./Media/particle +#FileSystem=./Media/DeferredShadingMedia +#FileSystem=./Media/DeferredShadingMedia/DeferredShading/post +#FileSystem=./Media/PCZAppMedia +#FileSystem=./Media/materials/scripts/SSAO +#FileSystem=./Media/materials/textures/SSAO +#FileSystem=./Media/volumeTerrain +#FileSystem=./Media/CSMShadows + +#Zip=./Media/packs/cubemap.zip +#Zip=./Media/packs/cubemapsJS.zip +#Zip=./Media/packs/dragon.zip +#Zip=./Media/packs/fresneldemo.zip +#Zip=./Media/packs/ogredance.zip +#Zip=./Media/packs/Sinbad.zip +#Zip=./Media/packs/skybox.zip +#Zip=./Media/volumeTerrain/volumeTerrainBig.zip + +#Zip=./Media/packs/DamagedHelmet.zip +#Zip=./Media/packs/filament_shaders.zip + +#[BSPWorld] +#Zip=./Media/packs/oa_rpg3dm2.pk3 +#Zip=./Media/packs/ogretestmap.zip + +# Materials for visual tests +#[Tests] +#FileSystem=/media/slapin/library/ogre/ogre-sdk/Tests/Media +[LuaScripts] +FileSystem=lua-scripts + +#[Characters] +#FileSystem=./characters +[Audio] +FileSystem=./audio/gui + +[Water] +FileSystem=water diff --git a/src/features/sceneEditor/systems/EditorUISystem.cpp b/src/features/sceneEditor/systems/EditorUISystem.cpp new file mode 100644 index 0000000..e7d5123 --- /dev/null +++ b/src/features/sceneEditor/systems/EditorUISystem.cpp @@ -0,0 +1,260 @@ +#include "EditorUISystem.hpp" +#include "../ui/ComponentEditorRegistry.hpp" +#include "../ui/TransformEditor.hpp" +#include "../ui/RenderableEditor.hpp" +#include +#include + +EditorUISystem::EditorUISystem(flecs::world &world, + Ogre::SceneManager *sceneMgr, + Ogre::Vector3 &cursorPos) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_cursorPosition(cursorPos) + , m_nameQuery(world.query()) + , m_editorRegistry() +{ + registerEditors(); + refreshEntityList(); +} + +EditorUISystem::~EditorUISystem() +{ +} + +void EditorUISystem::registerEditors() +{ + // Register transform editor - using the template version + auto transformEditor = + std::make_unique(m_cursorPosition, [this]() { + if (m_onCursorMoved) { + m_onCursorMoved(m_cursorPosition); + } + }); + m_editorRegistry.registerEditor( + std::move(transformEditor)); + + // Register renderable editor - using the template version + auto renderableEditor = std::make_unique(m_sceneMgr); + m_editorRegistry.registerEditor( + std::move(renderableEditor)); +} + +void EditorUISystem::setOnCursorMoved( + std::function callback) +{ + m_onCursorMoved = callback; +} + +void EditorUISystem::update() +{ + renderHierarchyWindow(); + renderPropertyWindow(); +} + +void EditorUISystem::renderHierarchyWindow() +{ + ImGui::Begin("Scene Hierarchy", nullptr, ImGuiWindowFlags_NoCollapse); + + if (ImGui::Button("Create Entity")) { + flecs::entity entity = m_world.entity(); + entity.set({ "New Entity" }); + + // Create transform component with scene node + TransformComponent transform; + transform.node = + m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + transform.position = Ogre::Vector3::ZERO; + entity.set(transform); + + refreshEntityList(); + } + + ImGui::Separator(); + + static float lastRefresh = 0; + if (ImGui::GetTime() - lastRefresh > 1.0f) { + refreshEntityList(); + lastRefresh = ImGui::GetTime(); + } + + // Display entities using query + m_nameQuery.each([this](flecs::entity e, EntityNameComponent &name) { + if (!e.is_alive()) + return; + + std::string displayName = + name.name.empty() ? + ("Entity " + std::to_string(e.id())) : + name.name; + + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | + ImGuiTreeNodeFlags_NoTreePushOnOpen; + if (m_selectedEntity == e) { + flags |= ImGuiTreeNodeFlags_Selected; + } + + ImGui::TreeNodeEx(displayName.c_str(), flags); + + if (ImGui::IsItemClicked()) { + setSelectedEntity(e); + } + + if (ImGui::BeginPopupContextItem()) { + renderEntityContextMenu(e); + ImGui::EndPopup(); + } + + // Component indicators + if (e.has()) { + ImGui::SameLine(); + ImGui::TextDisabled("[Transform]"); + } + if (e.has()) { + ImGui::SameLine(); + ImGui::TextDisabled("[Mesh]"); + } + }); + + ImGui::End(); +} + +void EditorUISystem::renderEntityContextMenu(flecs::entity entity) +{ + if (ImGui::MenuItem("Delete")) { + // Clean up scene node + if (entity.has()) { + auto &transform = entity.get_mut(); + if (transform.node) { + m_sceneMgr->destroySceneNode(transform.node); + transform.node = nullptr; + } + } + + // Clean up renderable + if (entity.has()) { + auto &renderable = + entity.get_mut(); + if (renderable.entity) { + m_sceneMgr->destroyEntity(renderable.entity); + renderable.entity = nullptr; + } + } + + if (m_selectedEntity == entity) { + setSelectedEntity(flecs::entity::null()); + } + entity.destruct(); + refreshEntityList(); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Add Transform Component")) { + if (!entity.has()) { + TransformComponent transform; + transform.node = m_sceneMgr->getRootSceneNode() + ->createChildSceneNode(); + transform.position = Ogre::Vector3::ZERO; + entity.set(transform); + } + } + + if (ImGui::MenuItem("Add Mesh Component")) { + if (!entity.has()) { + entity.set({}); + } + } +} + +void EditorUISystem::renderPropertyWindow() +{ + ImGui::Begin("Properties", nullptr, ImGuiWindowFlags_NoCollapse); + + if (m_selectedEntity && m_selectedEntity.is_alive()) { + // Entity name editing + if (m_selectedEntity.has()) { + auto &name = + m_selectedEntity.get_mut(); + static char nameBuffer[256]; + strcpy(nameBuffer, name.name.c_str()); + if (ImGui::InputText("Entity Name", nameBuffer, + sizeof(nameBuffer))) { + name.name = nameBuffer; + } + } + + ImGui::Separator(); + + render3DCursorControls(); + + ImGui::Separator(); + + renderComponentEditors(m_selectedEntity); + } else { + ImGui::Text("No entity selected"); + render3DCursorControls(); + } + + ImGui::End(); +} + +void EditorUISystem::render3DCursorControls() +{ + if (ImGui::CollapsingHeader("3D Cursor", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + float cursorPos[3] = { m_cursorPosition.x, m_cursorPosition.y, + m_cursorPosition.z }; + if (ImGui::DragFloat3("Cursor Position", cursorPos, 0.1f)) { + m_cursorPosition = Ogre::Vector3( + cursorPos[0], cursorPos[1], cursorPos[2]); + if (m_onCursorMoved) { + m_onCursorMoved(m_cursorPosition); + } + } + + ImGui::Text("Tip: Ctrl+Click in scene view to move cursor"); + + if (ImGui::Button("Reset Cursor")) { + m_cursorPosition = Ogre::Vector3::ZERO; + if (m_onCursorMoved) { + m_onCursorMoved(m_cursorPosition); + } + } + + ImGui::Unindent(); + } +} + +void EditorUISystem::renderComponentEditors(flecs::entity entity) +{ + // Render Transform component if present + if (entity.has()) { + auto &transform = entity.get_mut(); + m_editorRegistry.renderComponent(entity, + transform); + } + + // Render Renderable component if present + if (entity.has()) { + auto &renderable = entity.get_mut(); + m_editorRegistry.renderComponent( + entity, renderable); + } +} + +void EditorUISystem::setSelectedEntity(flecs::entity entity) +{ + m_selectedEntity = entity; +} + +void EditorUISystem::refreshEntityList() +{ + m_entityList.clear(); + + m_nameQuery.each([this](flecs::entity e, EntityNameComponent &name) { + m_entityList.push_back(e); + }); +} diff --git a/src/features/sceneEditor/systems/EditorUISystem.hpp b/src/features/sceneEditor/systems/EditorUISystem.hpp new file mode 100644 index 0000000..6dcea79 --- /dev/null +++ b/src/features/sceneEditor/systems/EditorUISystem.hpp @@ -0,0 +1,44 @@ +#ifndef EDITORUISYSTEM_HPP +#define EDITORUISYSTEM_HPP +#pragma once +#include +#include +#include +#include +#include +#include "../ui/ComponentEditorRegistry.hpp" +#include "../components/Transform.hpp" +#include "../components/Renderable.hpp" +#include "../components/EditorComponents.hpp" + +class EditorUISystem { +public: + EditorUISystem(flecs::world& world, Ogre::SceneManager* sceneMgr, Ogre::Vector3& cursorPos); + ~EditorUISystem(); + + void update(); + void setSelectedEntity(flecs::entity entity); + void setOnCursorMoved(std::function callback); + +private: + void renderHierarchyWindow(); + void renderPropertyWindow(); + void renderEntityContextMenu(flecs::entity entity); + void renderComponentEditors(flecs::entity entity); + void render3DCursorControls(); + void refreshEntityList(); + void registerEditors(); + + flecs::world& m_world; + Ogre::SceneManager* m_sceneMgr; + Ogre::Vector3& m_cursorPosition; + flecs::entity m_selectedEntity; + + ComponentEditorRegistry m_editorRegistry; + std::vector m_entityList; + std::function m_onCursorMoved; + + // flecs 4.1.4 query for entities with name component + flecs::query m_nameQuery; +}; +#endif // EDITORUISYSTEM_HPP diff --git a/src/features/sceneEditor/systems/RenderSystem.hpp b/src/features/sceneEditor/systems/RenderSystem.hpp new file mode 100644 index 0000000..c7facfc --- /dev/null +++ b/src/features/sceneEditor/systems/RenderSystem.hpp @@ -0,0 +1,4 @@ +#ifndef RENDERSYSTEM_HPP +#define RENDERSYSTEM_HPP + +#endif // RENDERSYSTEM_HPP diff --git a/src/features/sceneEditor/ui/ComponentEditor.hpp b/src/features/sceneEditor/ui/ComponentEditor.hpp new file mode 100644 index 0000000..d71032b --- /dev/null +++ b/src/features/sceneEditor/ui/ComponentEditor.hpp @@ -0,0 +1,70 @@ +#ifndef COMPONENTEDITOR_HPP +#define COMPONENTEDITOR_HPP +#pragma once +#include +#include +#include +#include +#include +#include +#include + +class IComponentEditor { +public: + virtual ~IComponentEditor() = default; + virtual bool render(flecs::entity entity, void* component) = 0; + virtual std::type_index getType() const = 0; +}; + +template +class ComponentEditor : public IComponentEditor { +public: + bool render(flecs::entity entity, void* component) override { + return renderComponent(entity, *static_cast(component)); + } + + std::type_index getType() const override { + return std::type_index(typeid(T)); + } + +protected: + virtual bool renderComponent(flecs::entity entity, T& component) = 0; +}; + +class ComponentEditorRegistry { +public: + template + void registerEditor(std::unique_ptr> editor) { + m_editors[std::type_index(typeid(T))] = std::move(editor); + } + + bool renderComponent(flecs::entity entity, flecs::type_t componentType, void* component) { + // In flecs 4.1.4, we need to get the type ID properly + // For now, we'll use a simpler approach - store editors by component ID + auto it = m_editors_by_id.find(componentType); + if (it != m_editors_by_id.end()) { + return it->second->render(entity, component); + } + return false; + } + + // Alternative: Register by type directly + template + void registerEditor(std::unique_ptr> editor) { + m_editors_by_type[typeid(T)] = std::move(editor); + } + + template + bool renderComponent(flecs::entity entity, T& component) { + auto it = m_editors_by_type.find(typeid(T)); + if (it != m_editors_by_type.end()) { + return it->second->render(entity, &component); + } + return false; + } + +private: + std::unordered_map> m_editors_by_type; + std::unordered_map> m_editors_by_id; +}; +#endif // COMPONENTEDITOR_HPP diff --git a/src/features/sceneEditor/ui/ComponentEditorRegistry.hpp b/src/features/sceneEditor/ui/ComponentEditorRegistry.hpp new file mode 100644 index 0000000..d3212f4 --- /dev/null +++ b/src/features/sceneEditor/ui/ComponentEditorRegistry.hpp @@ -0,0 +1,196 @@ +#ifndef COMPONENTEDITORREGISTRY_HPP +#define COMPONENTEDITORREGISTRY_HPP +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +// Forward declarations +class IComponentEditor; + +/** + * Base interface for component editors + */ +class IComponentEditor { +public: + virtual ~IComponentEditor() = default; + + /** + * Render the editor UI + * @param entity The flecs entity being edited + * @param component Pointer to the component data + * @return true if the component was modified + */ + virtual bool render(flecs::entity entity, void* component) = 0; + + /** + * Get the type of component this editor handles + * @return type_index of the component + */ + virtual std::type_index getType() const = 0; +}; + +/** + * Template base class for component editors + * @tparam T The component type to edit + */ +template +class ComponentEditor : public IComponentEditor { +public: + bool render(flecs::entity entity, void* component) override { + return renderComponent(entity, *static_cast(component)); + } + + std::type_index getType() const override { + return std::type_index(typeid(T)); + } + +protected: + /** + * Implement this method to render the editor for your component + * @param entity The flecs entity being edited + * @param component Reference to the component data + * @return true if the component was modified + */ + virtual bool renderComponent(flecs::entity entity, T& component) = 0; +}; + +/** + * Registry for component editors + * Manages the registration and rendering of editors for different component types + */ +class ComponentEditorRegistry { +public: + ComponentEditorRegistry() = default; + ~ComponentEditorRegistry() = default; + + // Delete copy constructor and assignment + ComponentEditorRegistry(const ComponentEditorRegistry&) = delete; + ComponentEditorRegistry& operator=(const ComponentEditorRegistry&) = delete; + + // Allow move operations + ComponentEditorRegistry(ComponentEditorRegistry&&) = default; + ComponentEditorRegistry& operator=(ComponentEditorRegistry&&) = default; + + /** + * Register an editor for a specific component type using type_index + * @param typeIndex The type_index of the component + * @param editor The editor instance + */ + void registerEditor(std::type_index typeIndex, std::unique_ptr editor) { + m_editors_by_type[typeIndex] = std::move(editor); + } + + /** + * Register an editor for a component type using template + * @tparam T The component type + * @param editor The editor instance + */ + template + void registerEditor(std::unique_ptr> editor) { + m_editors_by_type[std::type_index(typeid(T))] = std::move(editor); + } + + /** + * Render a component editor for the given component + * @tparam T The component type + * @param entity The flecs entity + * @param component Reference to the component data + * @return true if the component was modified + */ + template + bool renderComponent(flecs::entity entity, T& component) { + auto it = m_editors_by_type.find(std::type_index(typeid(T))); + if (it != m_editors_by_type.end()) { + return it->second->render(entity, &component); + } + + // If no specific editor, show a default editor + return renderDefaultEditor(entity, component); + } + + /** + * Check if a component type has a registered editor + * @tparam T The component type + * @return true if an editor is registered + */ + template + bool hasEditor() const { + return m_editors_by_type.find(std::type_index(typeid(T))) != m_editors_by_type.end(); + } + +private: + /** + * Default editor for components without custom editors + * Displays raw component data as text + */ + template + bool renderDefaultEditor(flecs::entity entity, T& component) { + if (ImGui::CollapsingHeader("Component (No Editor)", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Get component name from type info + const char* typeName = typeid(T).name(); + + // Simple demangling for common compilers + #ifdef _MSC_VER + // MSVC: Remove "struct " or "class " prefix + std::string name = typeName; + if (name.find("struct ") == 0) name = name.substr(7); + if (name.find("class ") == 0) name = name.substr(6); + ImGui::Text("Type: %s", name.c_str()); + #else + ImGui::Text("Type: %s", typeName); + #endif + + ImGui::Text("Size: %zu bytes", sizeof(T)); + ImGui::Text("Address: %p", &component); + + // Show raw bytes option + if (ImGui::TreeNode("Show Raw Data")) { + unsigned char* bytes = reinterpret_cast(&component); + for (size_t i = 0; i < sizeof(T); ++i) { + ImGui::Text("%02X ", bytes[i]); + if ((i + 1) % 16 == 0 && i + 1 < sizeof(T)) { + ImGui::NewLine(); + } else if ((i + 1) % 8 == 0) { + ImGui::SameLine(); + ImGui::Text(" "); + } else { + ImGui::SameLine(); + } + } + ImGui::TreePop(); + } + + ImGui::Unindent(); + } + return false; + } + + // Store editors by C++ type + std::unordered_map> m_editors_by_type; +}; + +/** + * Helper macro to create a simple component editor + * Use this for quick creation of editors for simple components + */ +#define CREATE_SIMPLE_EDITOR(ComponentType, ...) \ +class SimpleEditor_##ComponentType : public ComponentEditor { \ +protected: \ + bool renderComponent(flecs::entity entity, ComponentType& comp) override { \ + bool modified = false; \ + __VA_ARGS__ \ + return modified; \ + } \ +}; \ +auto register_##ComponentType = []() { \ + return std::make_unique(); \ +} +#endif // COMPONENTEDITORREGISTRY_HPP diff --git a/src/features/sceneEditor/ui/PropertyEditor.hpp b/src/features/sceneEditor/ui/PropertyEditor.hpp new file mode 100644 index 0000000..3a1a0aa --- /dev/null +++ b/src/features/sceneEditor/ui/PropertyEditor.hpp @@ -0,0 +1,4 @@ +#ifndef PROPERTYEDITOR_HPP +#define PROPERTYEDITOR_HPP + +#endif // PROPERTYEDITOR_HPP diff --git a/src/features/sceneEditor/ui/RenderableEditor.hpp b/src/features/sceneEditor/ui/RenderableEditor.hpp new file mode 100644 index 0000000..daf3f92 --- /dev/null +++ b/src/features/sceneEditor/ui/RenderableEditor.hpp @@ -0,0 +1,81 @@ +#ifndef RENDERABLEEDITOR_HPP +#define RENDERABLEEDITOR_HPP +#pragma once +#include "ComponentEditorRegistry.hpp" +#include "../components/Transform.hpp" +#include +#include +#include +#include + +class RenderableEditor : public ComponentEditor { +public: + RenderableEditor(Ogre::SceneManager* sceneMgr) : m_sceneMgr(sceneMgr) {} + +protected: + bool renderComponent(flecs::entity entity, RenderableComponent& renderable) override { + bool modified = false; + + if (ImGui::CollapsingHeader("Renderable", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Mesh selection + static char meshName[128] = ""; + ImGui::InputText("Mesh Name", meshName, sizeof(meshName)); + + if (ImGui::Button("Load Mesh")) { + if (strlen(meshName) > 0) { + if (renderable.entity) { + m_sceneMgr->destroyEntity(renderable.entity); + } + + renderable.meshName = meshName; + try { + renderable.entity = m_sceneMgr->createEntity(meshName); + + // Attach to scene node if transform exists + if (entity.has()) { + auto& transform = entity.get_mut(); + if (transform.node) { + transform.node->attachObject(renderable.entity); + } + } + + modified = true; + } catch (const std::exception& e) { + Ogre::LogManager::getSingleton().logMessage( + "Failed to load mesh: " + std::string(e.what()) + ); + } + } + } + + // Show current mesh info + if (!renderable.meshName.empty()) { + ImGui::Text("Current Mesh: %s", renderable.meshName.c_str()); + } + + // Detach button + if (renderable.entity && ImGui::Button("Detach Mesh")) { + if (entity.has()) { + auto& transform = entity.get_mut(); + if (transform.node) { + transform.node->detachObject(renderable.entity); + } + } + m_sceneMgr->destroyEntity(renderable.entity); + renderable.entity = nullptr; + renderable.meshName.clear(); + modified = true; + } + + ImGui::Unindent(); + } + + return modified; + } + +private: + Ogre::SceneManager* m_sceneMgr; +}; +#endif // RENDERABLEEDITOR_HPP diff --git a/src/features/sceneEditor/ui/TransformEditor.hpp b/src/features/sceneEditor/ui/TransformEditor.hpp new file mode 100644 index 0000000..a658554 --- /dev/null +++ b/src/features/sceneEditor/ui/TransformEditor.hpp @@ -0,0 +1,86 @@ +#ifndef TRANSFORMEDITOR_HPP +#define TRANSFORMEDITOR_HPP +#pragma once +#include "ComponentEditorRegistry.hpp" +#include "../components/Transform.hpp" +#include "../components/EulerUtils.hpp" +#include +#include +#include +#include + +class TransformEditor : public ComponentEditor { +public: + TransformEditor(Ogre::Vector3& cursorPos, std::function onCursorSet) + : m_cursorPosition(cursorPos), m_onCursorSet(onCursorSet) {} + +protected: + bool renderComponent(flecs::entity entity, TransformComponent& transform) override { + bool modified = false; + + if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Position editing + float pos[3] = {transform.position.x, transform.position.y, transform.position.z}; + if (ImGui::DragFloat3("Position", pos, 0.1f)) { + transform.position = Ogre::Vector3(pos[0], pos[1], pos[2]); + transform.applyToNode(); + modified = true; + } + + // Rotation editing using Euler angles + Ogre::Vector3 euler = EulerUtils::toEulerDegrees(transform.orientation); + float rot[3] = {euler.x, euler.y, euler.z}; + + if (ImGui::DragFloat3("Rotation (deg)", rot, 1.0f, -180.0f, 180.0f)) { + transform.orientation = EulerUtils::fromEulerDegrees(Ogre::Vector3(rot[0], rot[1], rot[2])); + transform.applyToNode(); + modified = true; + } + + // Scale editing + float scale[3] = {transform.scale.x, transform.scale.y, transform.scale.z}; + if (ImGui::DragFloat3("Scale", scale, 0.1f, 0.01f, 100.0f)) { + transform.scale = Ogre::Vector3(scale[0], scale[1], scale[2]); + transform.applyToNode(); + modified = true; + } + + ImGui::Separator(); + + // Reset button + if (ImGui::Button("Reset Transform")) { + transform.position = Ogre::Vector3::ZERO; + transform.orientation = Ogre::Quaternion::IDENTITY; + transform.scale = Ogre::Vector3::UNIT_SCALE; + transform.applyToNode(); + modified = true; + } + + ImGui::SameLine(); + + // 3D Cursor controls + if (ImGui::Button("Set Cursor from Position")) { + m_cursorPosition = transform.position; + if (m_onCursorSet) m_onCursorSet(); + } + + ImGui::SameLine(); + if (ImGui::Button("Set Position from Cursor")) { + transform.position = m_cursorPosition; + transform.applyToNode(); + modified = true; + } + + ImGui::Unindent(); + } + + return modified; + } + +private: + Ogre::Vector3& m_cursorPosition; + std::function m_onCursorSet; +}; +#endif // TRANSFORMEDITOR_HPP