Proper scene editor implementation
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
project(features)
|
||||
# ...
|
||||
add_subdirectory(characters)
|
||||
add_subdirectory(sceneEditor)
|
||||
add_subdirectory(editScene)
|
||||
|
||||
62
src/features/editScene/CMakeLists.txt
Normal file
62
src/features/editScene/CMakeLists.txt
Normal file
@@ -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"
|
||||
)
|
||||
372
src/features/editScene/EditorApp.cpp
Normal file
372
src/features/editScene/EditorApp.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
#include <iostream>
|
||||
#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 <OgreRTShaderSystem.h>
|
||||
#include <imgui.h>
|
||||
|
||||
//=============================================================================
|
||||
// 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<EditorCamera>(m_sceneMgr, getRenderWindow());
|
||||
|
||||
// Setup ECS and scene
|
||||
setupECS();
|
||||
setupLights();
|
||||
createGrid();
|
||||
createAxes();
|
||||
createDefaultEntities();
|
||||
|
||||
// Setup UI system
|
||||
m_uiSystem = std::make_unique<EditorUISystem>(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<ImGuiRenderListener>(
|
||||
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<EntityNameComponent>();
|
||||
m_world.component<TransformComponent>();
|
||||
m_world.component<RenderableComponent>();
|
||||
m_world.component<EditorMarkerComponent>();
|
||||
}
|
||||
|
||||
void EditorApp::createDefaultEntities()
|
||||
{
|
||||
// Create root entity
|
||||
flecs::entity root = m_world.entity("Root");
|
||||
root.set<EntityNameComponent>(EntityNameComponent("Root"));
|
||||
root.add<EditorMarkerComponent>();
|
||||
|
||||
// Create child using flecs::ChildOf relationship
|
||||
flecs::entity child1 = m_world.entity("Child1");
|
||||
child1.set<EntityNameComponent>(EntityNameComponent("Child 1"));
|
||||
child1.add<EditorMarkerComponent>();
|
||||
child1.child_of(root);
|
||||
|
||||
// Create grandchild using flecs::ChildOf relationship
|
||||
flecs::entity grandchild = m_world.entity("Grandchild");
|
||||
grandchild.set<EntityNameComponent>(EntityNameComponent("Grandchild"));
|
||||
grandchild.add<EditorMarkerComponent>();
|
||||
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<TransformComponent>()) {
|
||||
auto &transform =
|
||||
entity.get<TransformComponent>();
|
||||
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();
|
||||
}
|
||||
86
src/features/editScene/EditorApp.hpp
Normal file
86
src/features/editScene/EditorApp.hpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#ifndef EDITSCENE_EDITORAPP_HPP
|
||||
#define EDITSCENE_EDITORAPP_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <OgreApplicationContext.h>
|
||||
#include <OgreInput.h>
|
||||
#include <OgreOverlaySystem.h>
|
||||
#include <OgreImGuiOverlay.h>
|
||||
#include <OgreRenderTargetListener.h>
|
||||
#include <flecs.h>
|
||||
#include <memory>
|
||||
|
||||
// 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<flecs::entity> m_defaultEntities;
|
||||
|
||||
// Editor systems
|
||||
std::unique_ptr<EditorUISystem> m_uiSystem;
|
||||
std::unique_ptr<EditorCamera> m_camera;
|
||||
std::unique_ptr<ImGuiRenderListener> m_imguiListener;
|
||||
|
||||
// State
|
||||
uint16_t m_currentModifiers;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_EDITORAPP_HPP
|
||||
155
src/features/editScene/camera/EditorCamera.cpp
Normal file
155
src/features/editScene/camera/EditorCamera.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
#include "EditorCamera.hpp"
|
||||
#include <OgreViewport.h>
|
||||
|
||||
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);
|
||||
}
|
||||
83
src/features/editScene/camera/EditorCamera.hpp
Normal file
83
src/features/editScene/camera/EditorCamera.hpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#ifndef EDITSCENE_EDITORCAMERA_HPP
|
||||
#define EDITSCENE_EDITORCAMERA_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <OgreCamera.h>
|
||||
#include <OgreSceneManager.h>
|
||||
#include <OgreRenderWindow.h>
|
||||
#include <OgreInput.h>
|
||||
|
||||
/**
|
||||
* 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
|
||||
13
src/features/editScene/components/EditorMarker.hpp
Normal file
13
src/features/editScene/components/EditorMarker.hpp
Normal file
@@ -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
|
||||
21
src/features/editScene/components/EntityName.hpp
Normal file
21
src/features/editScene/components/EntityName.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef EDITSCENE_ENTITYNAME_HPP
|
||||
#define EDITSCENE_ENTITYNAME_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* 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
|
||||
21
src/features/editScene/components/Relationship.hpp
Normal file
21
src/features/editScene/components/Relationship.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef EDITSCENE_RELATIONSHIP_HPP
|
||||
#define EDITSCENE_RELATIONSHIP_HPP
|
||||
#pragma once
|
||||
#include <flecs.h>
|
||||
|
||||
/**
|
||||
* 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
|
||||
23
src/features/editScene/components/Renderable.hpp
Normal file
23
src/features/editScene/components/Renderable.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#ifndef EDITSCENE_RENDERABLE_HPP
|
||||
#define EDITSCENE_RENDERABLE_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* 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
|
||||
41
src/features/editScene/components/Transform.hpp
Normal file
41
src/features/editScene/components/Transform.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifndef EDITSCENE_TRANSFORM_HPP
|
||||
#define EDITSCENE_TRANSFORM_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* 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
|
||||
304
src/features/editScene/gizmo/Gizmo.cpp
Normal file
304
src/features/editScene/gizmo/Gizmo.cpp
Normal file
@@ -0,0 +1,304 @@
|
||||
#include "Gizmo.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include <cmath>
|
||||
|
||||
// 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<TransformComponent>()) {
|
||||
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<TransformComponent>()) {
|
||||
m_gizmoNode->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
auto &transform = m_attachedEntity.get<TransformComponent>();
|
||||
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<TransformComponent>()) {
|
||||
|
||||
m_isDragging = true;
|
||||
auto &transform = m_attachedEntity.get_mut<TransformComponent>();
|
||||
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<TransformComponent>()) {
|
||||
|
||||
auto &transform = m_attachedEntity.get_mut<TransformComponent>();
|
||||
|
||||
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<Axis>(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();
|
||||
}
|
||||
107
src/features/editScene/gizmo/Gizmo.hpp
Normal file
107
src/features/editScene/gizmo/Gizmo.hpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#ifndef EDITSCENE_GIZMO_HPP
|
||||
#define EDITSCENE_GIZMO_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <OgreManualObject.h>
|
||||
#include <flecs.h>
|
||||
|
||||
/**
|
||||
* 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
|
||||
20
src/features/editScene/main.cpp
Normal file
20
src/features/editScene/main.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include <iostream>
|
||||
#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;
|
||||
}
|
||||
17
src/features/editScene/resources.cfg
Normal file
17
src/features/editScene/resources.cfg
Normal file
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
637
src/features/editScene/systems/EditorUISystem.cpp
Normal file
637
src/features/editScene/systems/EditorUISystem.cpp
Normal file
@@ -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 <imgui.h>
|
||||
#include <algorithm>
|
||||
|
||||
EditorUISystem::EditorUISystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_selectedEntity(flecs::entity::null())
|
||||
, m_nameQuery(world.query<EntityNameComponent>())
|
||||
{
|
||||
registerComponentEditors();
|
||||
m_gizmo = std::make_unique<Gizmo>(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<TransformEditor>();
|
||||
m_componentRegistry.registerComponent<TransformComponent>(
|
||||
"Transform", std::move(transformEditor),
|
||||
// Adder
|
||||
[this](flecs::entity e) {
|
||||
if (!e.has<TransformComponent>()) {
|
||||
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<TransformComponent>(transform);
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[this](flecs::entity e) {
|
||||
if (e.has<TransformComponent>()) {
|
||||
auto &transform =
|
||||
e.get_mut<TransformComponent>();
|
||||
if (transform.node) {
|
||||
m_sceneMgr->destroySceneNode(
|
||||
transform.node);
|
||||
transform.node = nullptr;
|
||||
}
|
||||
e.remove<TransformComponent>();
|
||||
}
|
||||
});
|
||||
|
||||
// Register Renderable component
|
||||
auto renderableEditor = std::make_unique<RenderableEditor>(m_sceneMgr);
|
||||
m_componentRegistry.registerComponent<RenderableComponent>(
|
||||
"Renderable", std::move(renderableEditor),
|
||||
// Adder
|
||||
[this](flecs::entity e) {
|
||||
if (!e.has<RenderableComponent>()) {
|
||||
e.set<RenderableComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[this](flecs::entity e) {
|
||||
if (e.has<RenderableComponent>()) {
|
||||
auto &renderable =
|
||||
e.get_mut<RenderableComponent>();
|
||||
if (renderable.entity) {
|
||||
m_sceneMgr->destroyEntity(
|
||||
renderable.entity);
|
||||
renderable.entity = nullptr;
|
||||
}
|
||||
e.remove<RenderableComponent>();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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<TransformComponent>()) {
|
||||
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<flecs::entity> rootEntities;
|
||||
m_world.query_builder<>()
|
||||
.with<EditorMarkerComponent>()
|
||||
.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<EntityNameComponent>()) {
|
||||
name = entity.get<EntityNameComponent>().name;
|
||||
}
|
||||
|
||||
// Collect children using flecs::ChildOf relationship
|
||||
std::vector<flecs::entity> 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<TransformComponent>())
|
||||
indicators += " [T]";
|
||||
if (entity.has<RenderableComponent>())
|
||||
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<EntityNameComponent>()) {
|
||||
auto &nameComp =
|
||||
m_selectedEntity
|
||||
.get_mut<EntityNameComponent>();
|
||||
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<TransformComponent>()) {
|
||||
auto &transform = entity.get_mut<TransformComponent>();
|
||||
m_componentRegistry.render<TransformComponent>(entity,
|
||||
transform);
|
||||
}
|
||||
|
||||
if (entity.has<RenderableComponent>()) {
|
||||
auto &renderable = entity.get_mut<RenderableComponent>();
|
||||
m_componentRegistry.render<RenderableComponent>(entity,
|
||||
renderable);
|
||||
}
|
||||
|
||||
// Show message if no components
|
||||
if (!entity.has<TransformComponent>() &&
|
||||
!entity.has<RenderableComponent>()) {
|
||||
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<TransformComponent>();
|
||||
bool hasRenderable = entity.has<RenderableComponent>();
|
||||
|
||||
if (!hasTransform) {
|
||||
if (ImGui::MenuItem("Transform")) {
|
||||
m_componentRegistry
|
||||
.addComponent<TransformComponent>(
|
||||
entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasRenderable) {
|
||||
if (ImGui::MenuItem("Renderable")) {
|
||||
m_componentRegistry
|
||||
.addComponent<RenderableComponent>(
|
||||
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<TransformComponent>()) {
|
||||
hasAny = true;
|
||||
if (ImGui::MenuItem("Transform")) {
|
||||
m_componentRegistry.removeComponent<
|
||||
TransformComponent>(entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.has<RenderableComponent>()) {
|
||||
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>(EntityNameComponent("New Entity"));
|
||||
entity.add<EditorMarkerComponent>();
|
||||
|
||||
// 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<TransformComponent>(transform);
|
||||
|
||||
setSelectedEntity(entity);
|
||||
}
|
||||
|
||||
void EditorUISystem::createChildEntity(flecs::entity parent)
|
||||
{
|
||||
if (!parent.is_alive() || !parent.has<TransformComponent>())
|
||||
return;
|
||||
|
||||
// Create entity as child of parent using flecs::ChildOf
|
||||
flecs::entity entity = m_world.entity();
|
||||
entity.child_of(parent);
|
||||
entity.set<EntityNameComponent>(EntityNameComponent("Child Entity"));
|
||||
entity.add<EditorMarkerComponent>();
|
||||
|
||||
// Create transform with parent as scene node parent
|
||||
TransformComponent transform;
|
||||
auto &parentTransform = parent.get_mut<TransformComponent>();
|
||||
transform.node = parentTransform.node->createChildSceneNode();
|
||||
transform.position = Ogre::Vector3::ZERO;
|
||||
transform.rotation = Ogre::Quaternion::IDENTITY;
|
||||
transform.scale = Ogre::Vector3::UNIT_SCALE;
|
||||
entity.set<TransformComponent>(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<TransformComponent>()) {
|
||||
auto &transform = entity.get_mut<TransformComponent>();
|
||||
if (transform.node) {
|
||||
m_sceneMgr->destroySceneNode(transform.node);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up renderable
|
||||
if (entity.has<RenderableComponent>()) {
|
||||
auto &renderable = entity.get_mut<RenderableComponent>();
|
||||
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<EntityNameComponent>()) {
|
||||
auto &name = entity.get<EntityNameComponent>().name;
|
||||
newEntity.set<EntityNameComponent>(
|
||||
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<TransformComponent>()) {
|
||||
auto &oldTransform = entity.get<TransformComponent>();
|
||||
TransformComponent newTransform;
|
||||
|
||||
// Find parent node
|
||||
Ogre::SceneNode *parentNode = m_sceneMgr->getRootSceneNode();
|
||||
flecs::entity parent = entity.parent();
|
||||
if (parent.is_valid() && parent != 0 &&
|
||||
parent.has<TransformComponent>()) {
|
||||
parentNode = parent.get<TransformComponent>().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<TransformComponent>(newTransform);
|
||||
}
|
||||
|
||||
// Copy renderable (but not the mesh instance)
|
||||
if (entity.has<RenderableComponent>()) {
|
||||
auto &oldRenderable = entity.get<RenderableComponent>();
|
||||
RenderableComponent newRenderable;
|
||||
newRenderable.meshName = oldRenderable.meshName;
|
||||
newRenderable.visible = oldRenderable.visible;
|
||||
// Entity will be created when mesh is loaded
|
||||
newEntity.set<RenderableComponent>(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<flecs::entity>
|
||||
EditorUISystem::getEntityChildren(flecs::entity parent)
|
||||
{
|
||||
std::vector<flecs::entity> 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;
|
||||
}
|
||||
103
src/features/editScene/systems/EditorUISystem.hpp
Normal file
103
src/features/editScene/systems/EditorUISystem.hpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#ifndef EDITSCENE_EDITORUISYSTEM_HPP
|
||||
#define EDITSCENE_EDITORUISYSTEM_HPP
|
||||
#pragma once
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <memory>
|
||||
#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<flecs::entity> 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<flecs::entity> m_allEntities;
|
||||
std::unique_ptr<Gizmo> m_gizmo;
|
||||
|
||||
// Queries
|
||||
flecs::query<EntityNameComponent> m_nameQuery;
|
||||
|
||||
// Constants
|
||||
static constexpr float LEFT_PANEL_WIDTH = 300.0f;
|
||||
static constexpr float RIGHT_PANEL_WIDTH = 350.0f;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_EDITORUISYSTEM_HPP
|
||||
50
src/features/editScene/ui/ComponentEditor.hpp
Normal file
50
src/features/editScene/ui/ComponentEditor.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef EDITSCENE_COMPONENTEDITOR_HPP
|
||||
#define EDITSCENE_COMPONENTEDITOR_HPP
|
||||
#pragma once
|
||||
#include <imgui.h>
|
||||
#include <flecs.h>
|
||||
|
||||
/**
|
||||
* 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<typename T>
|
||||
class ComponentEditor : public IComponentEditor {
|
||||
public:
|
||||
bool render(flecs::entity entity, void *component) override
|
||||
{
|
||||
return renderComponent(entity, *static_cast<T *>(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
|
||||
157
src/features/editScene/ui/ComponentRegistry.hpp
Normal file
157
src/features/editScene/ui/ComponentRegistry.hpp
Normal file
@@ -0,0 +1,157 @@
|
||||
#ifndef EDITSCENE_COMPONENTREGISTRY_HPP
|
||||
#define EDITSCENE_COMPONENTREGISTRY_HPP
|
||||
#pragma once
|
||||
#include <typeindex>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include "ComponentEditor.hpp"
|
||||
|
||||
/**
|
||||
* Function type for adding a component to an entity
|
||||
*/
|
||||
using ComponentAdder = std::function<void(flecs::entity)>;
|
||||
|
||||
/**
|
||||
* Function type for removing a component from an entity
|
||||
*/
|
||||
using ComponentRemover = std::function<void(flecs::entity)>;
|
||||
|
||||
/**
|
||||
* Registry for component editors and their add/remove functions
|
||||
*/
|
||||
class ComponentRegistry {
|
||||
public:
|
||||
struct ComponentInfo {
|
||||
const char *name;
|
||||
std::unique_ptr<IComponentEditor> 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<typename T>
|
||||
void registerComponent(const char *name,
|
||||
std::unique_ptr<ComponentEditor<T>> 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<typename T>
|
||||
bool hasComponent() const
|
||||
{
|
||||
return m_components.find(std::type_index(typeid(T))) !=
|
||||
m_components.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a component editor
|
||||
*/
|
||||
template<typename T>
|
||||
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<typename T>
|
||||
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<typename T>
|
||||
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<std::type_index> getRegisteredTypes() const
|
||||
{
|
||||
std::vector<std::type_index> 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<std::type_index, ComponentInfo> m_components;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_COMPONENTREGISTRY_HPP
|
||||
233
src/features/editScene/ui/RenderableEditor.cpp
Normal file
233
src/features/editScene/ui/RenderableEditor.cpp
Normal file
@@ -0,0 +1,233 @@
|
||||
#include "RenderableEditor.hpp"
|
||||
#include <imgui.h>
|
||||
#include "../components/Transform.hpp"
|
||||
#include <OgreResourceGroupManager.h>
|
||||
#include <algorithm>
|
||||
|
||||
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<TransformComponent>()) {
|
||||
auto &transform = entity.get_mut<TransformComponent>();
|
||||
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;
|
||||
}
|
||||
36
src/features/editScene/ui/RenderableEditor.hpp
Normal file
36
src/features/editScene/ui/RenderableEditor.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#ifndef EDITSCENE_RENDERABLEEDITOR_HPP
|
||||
#define EDITSCENE_RENDERABLEEDITOR_HPP
|
||||
#pragma once
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/Renderable.hpp"
|
||||
#include <OgreSceneManager.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Editor for RenderableComponent
|
||||
*/
|
||||
class RenderableEditor : public ComponentEditor<RenderableComponent> {
|
||||
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<std::string> m_meshFiles;
|
||||
bool m_meshesScanned = false;
|
||||
static char s_meshNameBuffer[256];
|
||||
static char s_searchBuffer[256];
|
||||
static bool s_showMeshBrowser;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_RENDERABLEEDITOR_HPP
|
||||
69
src/features/editScene/ui/TransformEditor.cpp
Normal file
69
src/features/editScene/ui/TransformEditor.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
#include "TransformEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
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;
|
||||
}
|
||||
19
src/features/editScene/ui/TransformEditor.hpp
Normal file
19
src/features/editScene/ui/TransformEditor.hpp
Normal file
@@ -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<TransformComponent> {
|
||||
public:
|
||||
const char *getName() const override { return "Transform"; }
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
TransformComponent &transform) override;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_TRANSFORMEDITOR_HPP
|
||||
35
src/features/sceneEditor/CMakeLists.txt
Normal file
35
src/features/sceneEditor/CMakeLists.txt
Normal file
@@ -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"
|
||||
)
|
||||
|
||||
451
src/features/sceneEditor/EditorApp.cpp
Normal file
451
src/features/sceneEditor/EditorApp.cpp
Normal file
@@ -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<EditorCamera>(mSceneMgr, window);
|
||||
|
||||
// Setup UI system
|
||||
mUISystem = std::make_unique<EditorUISystem>(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<EntityNameComponent>({ "Root" });
|
||||
|
||||
// Create a test entity with mesh
|
||||
flecs::entity testEntity = mWorld.entity();
|
||||
testEntity.set<EntityNameComponent>({ "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<TransformComponent>(transform);
|
||||
|
||||
// Try to load a mesh
|
||||
try {
|
||||
RenderableComponent renderable("cube.mesh");
|
||||
renderable.entity = mSceneMgr->createEntity("cube.mesh");
|
||||
testEntity.set<RenderableComponent>(renderable);
|
||||
|
||||
// Attach to node
|
||||
if (testEntity.has<TransformComponent>()) {
|
||||
auto &testTransform =
|
||||
testEntity.get_mut<TransformComponent>();
|
||||
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<TransformComponent>().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<float>(evt.x) /
|
||||
static_cast<float>(
|
||||
vp->getActualWidth());
|
||||
float screenY = static_cast<float>(evt.y) /
|
||||
static_cast<float>(
|
||||
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<TransformComponent>()) {
|
||||
auto &transform =
|
||||
mSelectedEntity
|
||||
.get_mut<TransformComponent>();
|
||||
if (transform.node) {
|
||||
mSceneMgr->destroySceneNode(
|
||||
transform.node);
|
||||
transform.node = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up renderable
|
||||
if (mSelectedEntity.has<RenderableComponent>()) {
|
||||
auto &renderable =
|
||||
mSelectedEntity
|
||||
.get_mut<RenderableComponent>();
|
||||
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<Ogre::SceneNode *>(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<Ogre::SceneNode *>(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");
|
||||
}
|
||||
73
src/features/sceneEditor/EditorApp.hpp
Normal file
73
src/features/sceneEditor/EditorApp.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <OgreApplicationContext.h>
|
||||
#include <OgreInput.h>
|
||||
#include <OgreOverlaySystem.h>
|
||||
#include <OgreImGuiOverlay.h>
|
||||
#include <flecs.h>
|
||||
#include <memory>
|
||||
|
||||
// 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<EditorUISystem> mUISystem;
|
||||
std::unique_ptr<EditorCamera> mEditorCamera;
|
||||
};
|
||||
259
src/features/sceneEditor/camera/EditorCamera.cpp
Normal file
259
src/features/sceneEditor/camera/EditorCamera.cpp
Normal file
@@ -0,0 +1,259 @@
|
||||
#include "EditorCamera.hpp"
|
||||
#include <OgreRay.h>
|
||||
#include <OgrePlane.h>
|
||||
#include <OgreMath.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
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<float>(-deltaX) +
|
||||
up * static_cast<float>(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<float>(deltaX) * mRotateSpeed,
|
||||
static_cast<float>(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<bool, Ogre::Real> 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);
|
||||
}
|
||||
67
src/features/sceneEditor/camera/EditorCamera.hpp
Normal file
67
src/features/sceneEditor/camera/EditorCamera.hpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#ifndef EDITORCAMERA_HPP
|
||||
#define EDITORCAMERA_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <OgreInput.h>
|
||||
#include <OgreCamera.h>
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreSceneManager.h>
|
||||
#include <OgreRenderWindow.h>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
|
||||
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
|
||||
4
src/features/sceneEditor/components/EditorComponents.hpp
Normal file
4
src/features/sceneEditor/components/EditorComponents.hpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#ifndef EDITORCOMPONENTS_HPP
|
||||
#define EDITORCOMPONENTS_HPP
|
||||
|
||||
#endif // EDITORCOMPONENTS_HPP
|
||||
102
src/features/sceneEditor/components/EulerUtils.hpp
Normal file
102
src/features/sceneEditor/components/EulerUtils.hpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#ifndef EULERUTILS_HPP
|
||||
#define EULERUTILS_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
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
|
||||
4
src/features/sceneEditor/components/Renderable.hpp
Normal file
4
src/features/sceneEditor/components/Renderable.hpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#ifndef RENDERABLE_HPP
|
||||
#define RENDERABLE_HPP
|
||||
|
||||
#endif // RENDERABLE_HPP
|
||||
77
src/features/sceneEditor/components/Transform.hpp
Normal file
77
src/features/sceneEditor/components/Transform.hpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
#include <string>
|
||||
#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<TransformComponent>();
|
||||
world.component<RenderableComponent>();
|
||||
world.component<EditorTag>();
|
||||
world.component<EntityNameComponent>();
|
||||
}
|
||||
17
src/features/sceneEditor/main.cpp
Normal file
17
src/features/sceneEditor/main.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include <iostream>
|
||||
#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;
|
||||
}
|
||||
82
src/features/sceneEditor/resources.cfg
Normal file
82
src/features/sceneEditor/resources.cfg
Normal file
@@ -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
|
||||
260
src/features/sceneEditor/systems/EditorUISystem.cpp
Normal file
260
src/features/sceneEditor/systems/EditorUISystem.cpp
Normal file
@@ -0,0 +1,260 @@
|
||||
#include "EditorUISystem.hpp"
|
||||
#include "../ui/ComponentEditorRegistry.hpp"
|
||||
#include "../ui/TransformEditor.hpp"
|
||||
#include "../ui/RenderableEditor.hpp"
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
|
||||
EditorUISystem::EditorUISystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
Ogre::Vector3 &cursorPos)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_cursorPosition(cursorPos)
|
||||
, m_nameQuery(world.query<EntityNameComponent>())
|
||||
, m_editorRegistry()
|
||||
{
|
||||
registerEditors();
|
||||
refreshEntityList();
|
||||
}
|
||||
|
||||
EditorUISystem::~EditorUISystem()
|
||||
{
|
||||
}
|
||||
|
||||
void EditorUISystem::registerEditors()
|
||||
{
|
||||
// Register transform editor - using the template version
|
||||
auto transformEditor =
|
||||
std::make_unique<TransformEditor>(m_cursorPosition, [this]() {
|
||||
if (m_onCursorMoved) {
|
||||
m_onCursorMoved(m_cursorPosition);
|
||||
}
|
||||
});
|
||||
m_editorRegistry.registerEditor<TransformComponent>(
|
||||
std::move(transformEditor));
|
||||
|
||||
// Register renderable editor - using the template version
|
||||
auto renderableEditor = std::make_unique<RenderableEditor>(m_sceneMgr);
|
||||
m_editorRegistry.registerEditor<RenderableComponent>(
|
||||
std::move(renderableEditor));
|
||||
}
|
||||
|
||||
void EditorUISystem::setOnCursorMoved(
|
||||
std::function<void(const Ogre::Vector3 &)> 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<EntityNameComponent>({ "New Entity" });
|
||||
|
||||
// Create transform component with scene node
|
||||
TransformComponent transform;
|
||||
transform.node =
|
||||
m_sceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
transform.position = Ogre::Vector3::ZERO;
|
||||
entity.set<TransformComponent>(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<TransformComponent>()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("[Transform]");
|
||||
}
|
||||
if (e.has<RenderableComponent>()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("[Mesh]");
|
||||
}
|
||||
});
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void EditorUISystem::renderEntityContextMenu(flecs::entity entity)
|
||||
{
|
||||
if (ImGui::MenuItem("Delete")) {
|
||||
// Clean up scene node
|
||||
if (entity.has<TransformComponent>()) {
|
||||
auto &transform = entity.get_mut<TransformComponent>();
|
||||
if (transform.node) {
|
||||
m_sceneMgr->destroySceneNode(transform.node);
|
||||
transform.node = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up renderable
|
||||
if (entity.has<RenderableComponent>()) {
|
||||
auto &renderable =
|
||||
entity.get_mut<RenderableComponent>();
|
||||
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>()) {
|
||||
TransformComponent transform;
|
||||
transform.node = m_sceneMgr->getRootSceneNode()
|
||||
->createChildSceneNode();
|
||||
transform.position = Ogre::Vector3::ZERO;
|
||||
entity.set<TransformComponent>(transform);
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::MenuItem("Add Mesh Component")) {
|
||||
if (!entity.has<RenderableComponent>()) {
|
||||
entity.set<RenderableComponent>({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EditorUISystem::renderPropertyWindow()
|
||||
{
|
||||
ImGui::Begin("Properties", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
|
||||
if (m_selectedEntity && m_selectedEntity.is_alive()) {
|
||||
// Entity name editing
|
||||
if (m_selectedEntity.has<EntityNameComponent>()) {
|
||||
auto &name =
|
||||
m_selectedEntity.get_mut<EntityNameComponent>();
|
||||
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<TransformComponent>()) {
|
||||
auto &transform = entity.get_mut<TransformComponent>();
|
||||
m_editorRegistry.renderComponent<TransformComponent>(entity,
|
||||
transform);
|
||||
}
|
||||
|
||||
// Render Renderable component if present
|
||||
if (entity.has<RenderableComponent>()) {
|
||||
auto &renderable = entity.get_mut<RenderableComponent>();
|
||||
m_editorRegistry.renderComponent<RenderableComponent>(
|
||||
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);
|
||||
});
|
||||
}
|
||||
44
src/features/sceneEditor/systems/EditorUISystem.hpp
Normal file
44
src/features/sceneEditor/systems/EditorUISystem.hpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef EDITORUISYSTEM_HPP
|
||||
#define EDITORUISYSTEM_HPP
|
||||
#pragma once
|
||||
#include <flecs.h>
|
||||
#include <imgui.h>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#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<void(const Ogre::Vector3&)> 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<flecs::entity> m_entityList;
|
||||
std::function<void(const Ogre::Vector3&)> m_onCursorMoved;
|
||||
|
||||
// flecs 4.1.4 query for entities with name component
|
||||
flecs::query<EntityNameComponent> m_nameQuery;
|
||||
};
|
||||
#endif // EDITORUISYSTEM_HPP
|
||||
4
src/features/sceneEditor/systems/RenderSystem.hpp
Normal file
4
src/features/sceneEditor/systems/RenderSystem.hpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#ifndef RENDERSYSTEM_HPP
|
||||
#define RENDERSYSTEM_HPP
|
||||
|
||||
#endif // RENDERSYSTEM_HPP
|
||||
70
src/features/sceneEditor/ui/ComponentEditor.hpp
Normal file
70
src/features/sceneEditor/ui/ComponentEditor.hpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#ifndef COMPONENTEDITOR_HPP
|
||||
#define COMPONENTEDITOR_HPP
|
||||
#pragma once
|
||||
#include <imgui.h>
|
||||
#include <flecs.h>
|
||||
#include <typeindex>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class IComponentEditor {
|
||||
public:
|
||||
virtual ~IComponentEditor() = default;
|
||||
virtual bool render(flecs::entity entity, void* component) = 0;
|
||||
virtual std::type_index getType() const = 0;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class ComponentEditor : public IComponentEditor {
|
||||
public:
|
||||
bool render(flecs::entity entity, void* component) override {
|
||||
return renderComponent(entity, *static_cast<T*>(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<typename T>
|
||||
void registerEditor(std::unique_ptr<ComponentEditor<T>> 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<typename T>
|
||||
void registerEditor(std::unique_ptr<ComponentEditor<T>> editor) {
|
||||
m_editors_by_type[typeid(T)] = std::move(editor);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
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<std::type_index, std::unique_ptr<IComponentEditor>> m_editors_by_type;
|
||||
std::unordered_map<flecs::type_t, std::unique_ptr<IComponentEditor>> m_editors_by_id;
|
||||
};
|
||||
#endif // COMPONENTEDITOR_HPP
|
||||
196
src/features/sceneEditor/ui/ComponentEditorRegistry.hpp
Normal file
196
src/features/sceneEditor/ui/ComponentEditorRegistry.hpp
Normal file
@@ -0,0 +1,196 @@
|
||||
#ifndef COMPONENTEDITORREGISTRY_HPP
|
||||
#define COMPONENTEDITORREGISTRY_HPP
|
||||
#pragma once
|
||||
#include <imgui.h>
|
||||
#include <flecs.h>
|
||||
#include <typeindex>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
|
||||
// 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<typename T>
|
||||
class ComponentEditor : public IComponentEditor {
|
||||
public:
|
||||
bool render(flecs::entity entity, void* component) override {
|
||||
return renderComponent(entity, *static_cast<T*>(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<IComponentEditor> 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<typename T>
|
||||
void registerEditor(std::unique_ptr<ComponentEditor<T>> 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<typename T>
|
||||
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<typename T>
|
||||
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<typename T>
|
||||
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<unsigned char*>(&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<std::type_index, std::unique_ptr<IComponentEditor>> 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<ComponentType> { \
|
||||
protected: \
|
||||
bool renderComponent(flecs::entity entity, ComponentType& comp) override { \
|
||||
bool modified = false; \
|
||||
__VA_ARGS__ \
|
||||
return modified; \
|
||||
} \
|
||||
}; \
|
||||
auto register_##ComponentType = []() { \
|
||||
return std::make_unique<SimpleEditor_##ComponentType>(); \
|
||||
}
|
||||
#endif // COMPONENTEDITORREGISTRY_HPP
|
||||
4
src/features/sceneEditor/ui/PropertyEditor.hpp
Normal file
4
src/features/sceneEditor/ui/PropertyEditor.hpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#ifndef PROPERTYEDITOR_HPP
|
||||
#define PROPERTYEDITOR_HPP
|
||||
|
||||
#endif // PROPERTYEDITOR_HPP
|
||||
81
src/features/sceneEditor/ui/RenderableEditor.hpp
Normal file
81
src/features/sceneEditor/ui/RenderableEditor.hpp
Normal file
@@ -0,0 +1,81 @@
|
||||
#ifndef RENDERABLEEDITOR_HPP
|
||||
#define RENDERABLEEDITOR_HPP
|
||||
#pragma once
|
||||
#include "ComponentEditorRegistry.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include <imgui.h>
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
class RenderableEditor : public ComponentEditor<RenderableComponent> {
|
||||
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<TransformComponent>()) {
|
||||
auto& transform = entity.get_mut<TransformComponent>();
|
||||
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<TransformComponent>()) {
|
||||
auto& transform = entity.get_mut<TransformComponent>();
|
||||
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
|
||||
86
src/features/sceneEditor/ui/TransformEditor.hpp
Normal file
86
src/features/sceneEditor/ui/TransformEditor.hpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#ifndef TRANSFORMEDITOR_HPP
|
||||
#define TRANSFORMEDITOR_HPP
|
||||
#pragma once
|
||||
#include "ComponentEditorRegistry.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/EulerUtils.hpp"
|
||||
#include <imgui.h>
|
||||
#include <Ogre.h>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
class TransformEditor : public ComponentEditor<TransformComponent> {
|
||||
public:
|
||||
TransformEditor(Ogre::Vector3& cursorPos, std::function<void()> 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<void()> m_onCursorSet;
|
||||
};
|
||||
#endif // TRANSFORMEDITOR_HPP
|
||||
Reference in New Issue
Block a user