Prefab placement at cursor
This commit is contained in:
@@ -120,6 +120,7 @@ set(EDITSCENE_SOURCES
|
||||
components/SkyboxModule.cpp
|
||||
camera/EditorCamera.cpp
|
||||
gizmo/Gizmo.cpp
|
||||
gizmo/Cursor3D.cpp
|
||||
physics/physics.cpp
|
||||
)
|
||||
|
||||
@@ -239,6 +240,7 @@ set(EDITSCENE_HEADERS
|
||||
components/ActionDebug.hpp
|
||||
camera/EditorCamera.hpp
|
||||
gizmo/Gizmo.hpp
|
||||
gizmo/Cursor3D.hpp
|
||||
physics/physics.h
|
||||
)
|
||||
|
||||
|
||||
252
src/features/editScene/gizmo/Cursor3D.cpp
Normal file
252
src/features/editScene/gizmo/Cursor3D.cpp
Normal file
@@ -0,0 +1,252 @@
|
||||
#include "Cursor3D.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include <cmath>
|
||||
|
||||
// Colors for cursor - bright cyan/white for visibility
|
||||
static const float COLOR_CYAN[3] = { 0.0f, 1.0f, 1.0f };
|
||||
static const float COLOR_WHITE[3] = { 1.0f, 1.0f, 1.0f };
|
||||
static const float COLOR_RED[3] = { 1.0f, 0.2f, 0.2f };
|
||||
static const float COLOR_GREEN[3] = { 0.2f, 1.0f, 0.2f };
|
||||
static const float COLOR_BLUE[3] = { 0.2f, 0.4f, 1.0f };
|
||||
|
||||
Cursor3D::Cursor3D(Ogre::SceneManager *sceneMgr)
|
||||
: m_sceneMgr(sceneMgr)
|
||||
, m_cursorNode(nullptr)
|
||||
, m_axesObj(nullptr)
|
||||
, m_markerObj(nullptr)
|
||||
, m_position(Ogre::Vector3::ZERO)
|
||||
, m_orientation(Ogre::Quaternion::IDENTITY)
|
||||
, m_size(1.0f)
|
||||
, m_visible(false)
|
||||
{
|
||||
m_cursorNode = m_sceneMgr->getRootSceneNode()->createChildSceneNode(
|
||||
"Cursor3DNode");
|
||||
|
||||
m_axesObj = m_sceneMgr->createManualObject("Cursor3DAxes");
|
||||
m_markerObj = m_sceneMgr->createManualObject("Cursor3DMarker");
|
||||
|
||||
m_cursorNode->attachObject(m_axesObj);
|
||||
m_cursorNode->attachObject(m_markerObj);
|
||||
|
||||
// Draw on top of everything
|
||||
m_axesObj->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
m_markerObj->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
|
||||
m_cursorNode->setVisible(false);
|
||||
|
||||
createGeometry();
|
||||
}
|
||||
|
||||
void Cursor3D::shutdown()
|
||||
{
|
||||
if (!m_sceneMgr)
|
||||
return;
|
||||
|
||||
if (m_cursorNode && m_axesObj) {
|
||||
try {
|
||||
m_cursorNode->detachObject(m_axesObj);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
if (m_cursorNode && m_markerObj) {
|
||||
try {
|
||||
m_cursorNode->detachObject(m_markerObj);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
if (m_axesObj) {
|
||||
try {
|
||||
m_sceneMgr->destroyManualObject(m_axesObj);
|
||||
} catch (...) {
|
||||
}
|
||||
m_axesObj = nullptr;
|
||||
}
|
||||
if (m_markerObj) {
|
||||
try {
|
||||
m_sceneMgr->destroyManualObject(m_markerObj);
|
||||
} catch (...) {
|
||||
}
|
||||
m_markerObj = nullptr;
|
||||
}
|
||||
|
||||
if (m_cursorNode) {
|
||||
try {
|
||||
m_sceneMgr->destroySceneNode(m_cursorNode);
|
||||
} catch (...) {
|
||||
}
|
||||
m_cursorNode = nullptr;
|
||||
}
|
||||
|
||||
m_sceneMgr = nullptr;
|
||||
}
|
||||
|
||||
Cursor3D::~Cursor3D()
|
||||
{
|
||||
if (m_sceneMgr)
|
||||
shutdown();
|
||||
}
|
||||
|
||||
void Cursor3D::setPosition(const Ogre::Vector3 &pos)
|
||||
{
|
||||
m_position = pos;
|
||||
updateNodeTransform();
|
||||
}
|
||||
|
||||
void Cursor3D::setOrientation(const Ogre::Quaternion &rot)
|
||||
{
|
||||
m_orientation = rot;
|
||||
updateNodeTransform();
|
||||
}
|
||||
|
||||
void Cursor3D::setVisible(bool visible)
|
||||
{
|
||||
m_visible = visible;
|
||||
if (m_cursorNode)
|
||||
m_cursorNode->setVisible(visible);
|
||||
}
|
||||
|
||||
bool Cursor3D::isVisible() const
|
||||
{
|
||||
return m_visible;
|
||||
}
|
||||
|
||||
void Cursor3D::setSize(float size)
|
||||
{
|
||||
m_size = size;
|
||||
createGeometry();
|
||||
}
|
||||
|
||||
void Cursor3D::snapToTransform(const TransformComponent &transform)
|
||||
{
|
||||
if (transform.node) {
|
||||
m_position = transform.node->_getDerivedPosition();
|
||||
m_orientation = transform.node->_getDerivedOrientation();
|
||||
} else {
|
||||
m_position = transform.position;
|
||||
m_orientation = transform.rotation;
|
||||
}
|
||||
updateNodeTransform();
|
||||
}
|
||||
|
||||
void Cursor3D::applyToTransform(TransformComponent &transform) const
|
||||
{
|
||||
if (!transform.node)
|
||||
return;
|
||||
|
||||
Ogre::SceneNode *parent = static_cast<Ogre::SceneNode*>(
|
||||
transform.node->getParent());
|
||||
if (parent) {
|
||||
transform.position = parent->convertWorldToLocalPosition(
|
||||
m_position);
|
||||
transform.rotation = parent->convertWorldToLocalOrientation(
|
||||
m_orientation);
|
||||
} else {
|
||||
transform.position = m_position;
|
||||
transform.rotation = m_orientation;
|
||||
}
|
||||
transform.applyToNode();
|
||||
transform.markChanged();
|
||||
}
|
||||
|
||||
bool Cursor3D::hitTest(const Ogre::Ray &mouseRay) const
|
||||
{
|
||||
if (!m_cursorNode || !m_visible)
|
||||
return false;
|
||||
|
||||
// Check if ray passes near cursor center
|
||||
Ogre::Vector3 toCursor = m_position - mouseRay.getOrigin();
|
||||
float tca = toCursor.dotProduct(mouseRay.getDirection());
|
||||
if (tca < 0.01f)
|
||||
return false;
|
||||
|
||||
float d2 = toCursor.dotProduct(toCursor) - tca * tca;
|
||||
float threshold = 0.3f * m_size;
|
||||
return d2 <= threshold * threshold;
|
||||
}
|
||||
|
||||
void Cursor3D::updateNodeTransform()
|
||||
{
|
||||
if (m_cursorNode) {
|
||||
m_cursorNode->setPosition(m_position);
|
||||
m_cursorNode->setOrientation(m_orientation);
|
||||
}
|
||||
}
|
||||
|
||||
void Cursor3D::createGeometry()
|
||||
{
|
||||
float len = 0.5f * m_size;
|
||||
float half = 0.05f * m_size;
|
||||
|
||||
// Axes - short lines in RGB
|
||||
m_axesObj->clear();
|
||||
m_axesObj->begin("Ogre/AxisGizmo",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
|
||||
// X axis - red
|
||||
m_axesObj->colour(COLOR_RED[0], COLOR_RED[1], COLOR_RED[2]);
|
||||
m_axesObj->position(0, 0, 0);
|
||||
m_axesObj->position(len, 0, 0);
|
||||
|
||||
// Y axis - green
|
||||
m_axesObj->colour(COLOR_GREEN[0], COLOR_GREEN[1], COLOR_GREEN[2]);
|
||||
m_axesObj->position(0, 0, 0);
|
||||
m_axesObj->position(0, len, 0);
|
||||
|
||||
// Z axis - blue
|
||||
m_axesObj->colour(COLOR_BLUE[0], COLOR_BLUE[1], COLOR_BLUE[2]);
|
||||
m_axesObj->position(0, 0, 0);
|
||||
m_axesObj->position(0, 0, len);
|
||||
|
||||
m_axesObj->end();
|
||||
|
||||
// Center marker - small wireframe cube in cyan
|
||||
m_markerObj->clear();
|
||||
m_markerObj->begin("Ogre/AxisGizmo",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
m_markerObj->colour(COLOR_CYAN[0], COLOR_CYAN[1], COLOR_CYAN[2]);
|
||||
|
||||
// Cube corners
|
||||
Ogre::Vector3 corners[8] = {
|
||||
Ogre::Vector3(-half, -half, -half),
|
||||
Ogre::Vector3(half, -half, -half),
|
||||
Ogre::Vector3(half, half, -half),
|
||||
Ogre::Vector3(-half, half, -half),
|
||||
Ogre::Vector3(-half, -half, half),
|
||||
Ogre::Vector3(half, -half, half),
|
||||
Ogre::Vector3(half, half, half),
|
||||
Ogre::Vector3(-half, half, half),
|
||||
};
|
||||
|
||||
// Bottom face
|
||||
m_markerObj->position(corners[0]);
|
||||
m_markerObj->position(corners[1]);
|
||||
m_markerObj->position(corners[1]);
|
||||
m_markerObj->position(corners[2]);
|
||||
m_markerObj->position(corners[2]);
|
||||
m_markerObj->position(corners[3]);
|
||||
m_markerObj->position(corners[3]);
|
||||
m_markerObj->position(corners[0]);
|
||||
|
||||
// Top face
|
||||
m_markerObj->position(corners[4]);
|
||||
m_markerObj->position(corners[5]);
|
||||
m_markerObj->position(corners[5]);
|
||||
m_markerObj->position(corners[6]);
|
||||
m_markerObj->position(corners[6]);
|
||||
m_markerObj->position(corners[7]);
|
||||
m_markerObj->position(corners[7]);
|
||||
m_markerObj->position(corners[4]);
|
||||
|
||||
// Vertical edges
|
||||
m_markerObj->position(corners[0]);
|
||||
m_markerObj->position(corners[4]);
|
||||
m_markerObj->position(corners[1]);
|
||||
m_markerObj->position(corners[5]);
|
||||
m_markerObj->position(corners[2]);
|
||||
m_markerObj->position(corners[6]);
|
||||
m_markerObj->position(corners[3]);
|
||||
m_markerObj->position(corners[7]);
|
||||
|
||||
m_markerObj->end();
|
||||
}
|
||||
88
src/features/editScene/gizmo/Cursor3D.hpp
Normal file
88
src/features/editScene/gizmo/Cursor3D.hpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#ifndef EDITSCENE_CURSOR3D_HPP
|
||||
#define EDITSCENE_CURSOR3D_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <OgreManualObject.h>
|
||||
|
||||
// Forward declarations
|
||||
struct TransformComponent;
|
||||
|
||||
/**
|
||||
* 3D Cursor - a visual marker for prefab placement and transform reference
|
||||
* Shows a small crosshair with axis indicators in world space
|
||||
*/
|
||||
class Cursor3D {
|
||||
public:
|
||||
Cursor3D(Ogre::SceneManager *sceneMgr);
|
||||
~Cursor3D();
|
||||
|
||||
/**
|
||||
* Shutdown and cleanup - must be called before SceneManager is destroyed
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
/**
|
||||
* Set world position
|
||||
*/
|
||||
void setPosition(const Ogre::Vector3 &pos);
|
||||
Ogre::Vector3 getPosition() const
|
||||
{
|
||||
return m_position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set world orientation
|
||||
*/
|
||||
void setOrientation(const Ogre::Quaternion &rot);
|
||||
Ogre::Quaternion getOrientation() const
|
||||
{
|
||||
return m_orientation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show/hide cursor
|
||||
*/
|
||||
void setVisible(bool visible);
|
||||
bool isVisible() const;
|
||||
|
||||
/**
|
||||
* Set cursor size/scale
|
||||
*/
|
||||
void setSize(float size);
|
||||
float getSize() const
|
||||
{
|
||||
return m_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy position and orientation from a TransformComponent (world space)
|
||||
*/
|
||||
void snapToTransform(const TransformComponent &transform);
|
||||
|
||||
/**
|
||||
* Apply cursor position and orientation to a TransformComponent
|
||||
*/
|
||||
void applyToTransform(TransformComponent &transform) const;
|
||||
|
||||
/**
|
||||
* Simple hit test - returns true if mouse ray passes near cursor center
|
||||
*/
|
||||
bool hitTest(const Ogre::Ray &mouseRay) const;
|
||||
|
||||
private:
|
||||
void createGeometry();
|
||||
void updateNodeTransform();
|
||||
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
Ogre::SceneNode *m_cursorNode;
|
||||
Ogre::ManualObject *m_axesObj;
|
||||
Ogre::ManualObject *m_markerObj;
|
||||
|
||||
Ogre::Vector3 m_position;
|
||||
Ogre::Quaternion m_orientation;
|
||||
float m_size;
|
||||
bool m_visible;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CURSOR3D_HPP
|
||||
@@ -165,6 +165,11 @@ void CharacterSystem::setupEntity(flecs::entity e, CharacterComponent &cc)
|
||||
cc.hasFloor = false;
|
||||
std::cout << "CharacterSystem::setupEntity: entity=" << e.id()
|
||||
<< " nodePos=" << transform.node->_getDerivedPosition()
|
||||
<< " parent=" << (transform.node->getParent() ?
|
||||
transform.node->getParent()->getName() :
|
||||
"<root>")
|
||||
<< " radius=" << cc.radius << " height=" << cc.height
|
||||
<< " dirty=" << cc.dirty << " hasFloor=" << cc.hasFloor
|
||||
<< std::endl;
|
||||
|
||||
CharacterState state;
|
||||
|
||||
@@ -63,6 +63,7 @@ EditorUISystem::EditorUISystem(flecs::world &world,
|
||||
{
|
||||
registerComponentEditors();
|
||||
m_gizmo = std::make_unique<Gizmo>(m_sceneMgr);
|
||||
m_cursor3D = std::make_unique<Cursor3D>(m_sceneMgr);
|
||||
m_serializer = std::make_unique<SceneSerializer>(m_world, m_sceneMgr);
|
||||
}
|
||||
|
||||
@@ -70,17 +71,44 @@ EditorUISystem::~EditorUISystem() = default;
|
||||
|
||||
void EditorUISystem::shutdown()
|
||||
{
|
||||
// Shutdown gizmo before SceneManager is destroyed
|
||||
// Shutdown gizmo and cursor before SceneManager is destroyed
|
||||
if (m_gizmo) {
|
||||
m_gizmo->shutdown();
|
||||
}
|
||||
if (m_cursor3D) {
|
||||
m_cursor3D->shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
bool EditorUISystem::onMousePressed(const Ogre::Ray &mouseRay)
|
||||
{
|
||||
if (m_gizmo) {
|
||||
return m_gizmo->onMousePressed(mouseRay);
|
||||
// Try gizmo first
|
||||
if (m_gizmo && m_gizmo->onMousePressed(mouseRay)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If cursor placement mode is active, raycast and place cursor
|
||||
// (skip if ImGui wants the mouse for UI interaction)
|
||||
if (m_cursor3D && m_cursorPlaceMode && m_physicsSystem &&
|
||||
m_physicsSystem->isInitialized() &&
|
||||
!ImGui::GetIO().WantCaptureMouse) {
|
||||
JoltPhysicsWrapper *physics =
|
||||
m_physicsSystem->getPhysicsWrapper();
|
||||
if (physics) {
|
||||
Ogre::Vector3 start = mouseRay.getOrigin();
|
||||
Ogre::Vector3 end =
|
||||
start + mouseRay.getDirection() * 1000.0f;
|
||||
Ogre::Vector3 hitPos;
|
||||
JPH::BodyID hitBody;
|
||||
if (physics->raycastQuery(start, end, hitPos,
|
||||
hitBody)) {
|
||||
m_cursor3D->setPosition(hitPos);
|
||||
m_cursorPlaceMode = false; // disable after placement
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -250,6 +278,7 @@ void EditorUISystem::update(float deltaTime)
|
||||
}
|
||||
|
||||
renderPrefabBrowser();
|
||||
renderCursorPanel();
|
||||
|
||||
// Render FPS overlay
|
||||
renderFPSOverlay(deltaTime);
|
||||
@@ -308,6 +337,14 @@ void EditorUISystem::renderHierarchyWindow()
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
// Tools menu
|
||||
if (ImGui::BeginMenu("Tools")) {
|
||||
if (ImGui::MenuItem("3D Cursor")) {
|
||||
m_showCursorPanel = true;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
// Entity menu
|
||||
if (ImGui::BeginMenu("Entity")) {
|
||||
if (ImGui::MenuItem("New Entity", "Ctrl+N")) {
|
||||
@@ -1582,6 +1619,11 @@ void EditorUISystem::renderPrefabBrowser()
|
||||
if (ImGui::Button("Create Instance")) {
|
||||
// Will show file picker or use selected prefab
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Root", &m_prefabInstAtRoot);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Instantiate at root level (no parent)");
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
@@ -1607,20 +1649,72 @@ void EditorUISystem::renderPrefabBrowser()
|
||||
|
||||
std::string instId = "Inst##" + file;
|
||||
if (ImGui::Button(instId.c_str())) {
|
||||
Ogre::Vector3 pos(0, 0, 0);
|
||||
if (m_selectedEntity.is_alive() &&
|
||||
m_selectedEntity.has<TransformComponent>()) {
|
||||
pos = m_selectedEntity
|
||||
.get<TransformComponent>()
|
||||
.position +
|
||||
Ogre::Vector3(2, 0, 0);
|
||||
}
|
||||
PrefabSystem prefabSys(m_world,
|
||||
m_sceneMgr);
|
||||
flecs::entity parent =
|
||||
m_selectedEntity.is_alive()
|
||||
? m_selectedEntity
|
||||
: flecs::entity::null();
|
||||
flecs::entity::null();
|
||||
Ogre::Vector3 pos(0, 0, 0);
|
||||
|
||||
if (m_prefabUseCursor && m_cursor3D &&
|
||||
m_cursor3D->isVisible()) {
|
||||
// Use cursor position as base
|
||||
pos = m_cursor3D->getPosition();
|
||||
|
||||
// Optional downward raycast with margin
|
||||
if (m_prefabRaycastMargin > 0.0f &&
|
||||
m_physicsSystem &&
|
||||
m_physicsSystem->isInitialized()) {
|
||||
JoltPhysicsWrapper *physics =
|
||||
m_physicsSystem
|
||||
->getPhysicsWrapper();
|
||||
if (physics) {
|
||||
Ogre::Vector3 start =
|
||||
pos + Ogre::Vector3(
|
||||
0,
|
||||
m_prefabRaycastMargin,
|
||||
0);
|
||||
Ogre::Vector3 end =
|
||||
pos + Ogre::Vector3(
|
||||
0,
|
||||
-1000.0f,
|
||||
0);
|
||||
Ogre::Vector3 hitPos;
|
||||
JPH::BodyID hitBody;
|
||||
if (physics->raycastQuery(
|
||||
start, end,
|
||||
hitPos,
|
||||
hitBody)) {
|
||||
pos = hitPos + Ogre::Vector3(
|
||||
0,
|
||||
m_prefabRaycastMargin,
|
||||
0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not at root, parent under selected entity
|
||||
if (!m_prefabInstAtRoot &&
|
||||
m_selectedEntity.is_alive() &&
|
||||
m_selectedEntity.has<
|
||||
TransformComponent>()) {
|
||||
parent = m_selectedEntity;
|
||||
// Convert world pos to local
|
||||
auto &pt = m_selectedEntity.get<
|
||||
TransformComponent>();
|
||||
if (pt.node) {
|
||||
pos = pt.node->convertWorldToLocalPosition(
|
||||
pos);
|
||||
}
|
||||
}
|
||||
} else if (!m_prefabInstAtRoot &&
|
||||
m_selectedEntity.is_alive() &&
|
||||
m_selectedEntity.has<
|
||||
TransformComponent>()) {
|
||||
parent = m_selectedEntity;
|
||||
// Local offset from parent
|
||||
pos = Ogre::Vector3(2, 0, 0);
|
||||
}
|
||||
|
||||
auto instance = prefabSys.createInstance(
|
||||
path, parent, pos,
|
||||
file.substr(0, file.find_last_of('.'))
|
||||
@@ -1634,3 +1728,124 @@ void EditorUISystem::renderPrefabBrowser()
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void EditorUISystem::renderCursorPanel()
|
||||
{
|
||||
if (!m_showCursorPanel)
|
||||
return;
|
||||
|
||||
if (!m_cursor3D)
|
||||
return;
|
||||
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(LEFT_PANEL_WIDTH, 100), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(280, 350),
|
||||
ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
if (ImGui::Begin("3D Cursor", &m_showCursorPanel, flags)) {
|
||||
// Visibility toggle
|
||||
bool visible = m_cursor3D->isVisible();
|
||||
if (ImGui::Checkbox("Visible", &visible)) {
|
||||
m_cursor3D->setVisible(visible);
|
||||
}
|
||||
|
||||
// Placement mode
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Place on Click", &m_cursorPlaceMode)) {
|
||||
if (m_cursorPlaceMode && !m_cursor3D->isVisible())
|
||||
m_cursor3D->setVisible(true);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Click in the 3D viewport to place cursor on surface");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Position editor
|
||||
Ogre::Vector3 pos = m_cursor3D->getPosition();
|
||||
float posArr[3] = { pos.x, pos.y, pos.z };
|
||||
if (ImGui::DragFloat3("Position", posArr, 0.01f)) {
|
||||
m_cursor3D->setPosition(Ogre::Vector3(
|
||||
posArr[0], posArr[1], posArr[2]));
|
||||
}
|
||||
|
||||
// Rotation editor (Euler angles)
|
||||
Ogre::Quaternion rot = m_cursor3D->getOrientation();
|
||||
float yaw = Ogre::Radian(rot.getYaw()).valueDegrees();
|
||||
float pitch = Ogre::Radian(rot.getPitch()).valueDegrees();
|
||||
float roll = Ogre::Radian(rot.getRoll()).valueDegrees();
|
||||
float rotArr[3] = { yaw, pitch, roll };
|
||||
if (ImGui::DragFloat3("Rotation (Y/P/R)", rotArr, 0.5f)) {
|
||||
Ogre::Quaternion q1(Ogre::Degree(rotArr[0]),
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
Ogre::Quaternion q2(Ogre::Degree(rotArr[1]),
|
||||
Ogre::Vector3::UNIT_X);
|
||||
Ogre::Quaternion q3(Ogre::Degree(rotArr[2]),
|
||||
Ogre::Vector3::UNIT_Z);
|
||||
m_cursor3D->setOrientation(q1 * q2 * q3);
|
||||
}
|
||||
|
||||
// Size
|
||||
float size = m_cursor3D->getSize();
|
||||
if (ImGui::DragFloat("Size", &size, 0.01f, 0.1f, 10.0f)) {
|
||||
m_cursor3D->setSize(size);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Snap to selected entity
|
||||
if (ImGui::Button("Snap to Selected", ImVec2(120, 0))) {
|
||||
if (m_selectedEntity.is_alive() &&
|
||||
m_selectedEntity.has<TransformComponent>()) {
|
||||
m_cursor3D->snapToTransform(
|
||||
m_selectedEntity.get<TransformComponent>());
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Copy selected entity's world position/rotation to cursor");
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// Apply to selected entity
|
||||
if (ImGui::Button("Apply to Selected", ImVec2(120, 0))) {
|
||||
if (m_selectedEntity.is_alive() &&
|
||||
m_selectedEntity.has<TransformComponent>()) {
|
||||
auto &transform =
|
||||
m_selectedEntity.get_mut<TransformComponent>();
|
||||
m_cursor3D->applyToTransform(transform);
|
||||
if (m_selectedEntity.has<
|
||||
StaticGeometryMemberComponent>()) {
|
||||
m_selectedEntity.get_mut<
|
||||
StaticGeometryMemberComponent>()
|
||||
.markDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Copy cursor position/rotation to selected entity");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Prefab placement settings
|
||||
ImGui::Text("Prefab Placement");
|
||||
ImGui::Checkbox("Use Cursor Position", &m_prefabUseCursor);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Place prefabs at cursor position instead of selected entity");
|
||||
}
|
||||
ImGui::DragFloat("Raycast Margin", &m_prefabRaycastMargin,
|
||||
0.01f, 0.0f, 10.0f);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Vertical offset: raycast from cursor+margin downward, "
|
||||
"place prefab at hit point + margin");
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "../ui/ComponentRegistry.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../gizmo/Gizmo.hpp"
|
||||
#include "../gizmo/Cursor3D.hpp"
|
||||
#include "SceneSerializer.hpp"
|
||||
|
||||
// Forward declarations
|
||||
@@ -92,6 +93,14 @@ public:
|
||||
return m_gizmo.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 3D cursor for external interaction
|
||||
*/
|
||||
Cursor3D *getCursor3D() const
|
||||
{
|
||||
return m_cursor3D.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown UI system - must be called before SceneManager destruction
|
||||
*/
|
||||
@@ -171,6 +180,7 @@ public:
|
||||
*/
|
||||
void showCreatePrefabDialog(flecs::entity entity);
|
||||
void renderPrefabBrowser();
|
||||
void renderCursorPanel();
|
||||
|
||||
private:
|
||||
// File menu
|
||||
@@ -209,6 +219,7 @@ private:
|
||||
ComponentRegistry m_componentRegistry;
|
||||
std::vector<flecs::entity> m_allEntities;
|
||||
std::unique_ptr<Gizmo> m_gizmo;
|
||||
std::unique_ptr<Cursor3D> m_cursor3D;
|
||||
std::unique_ptr<SceneSerializer> m_serializer;
|
||||
|
||||
// Settings
|
||||
@@ -255,6 +266,13 @@ private:
|
||||
bool m_showPrefabBrowser = false;
|
||||
std::vector<std::string> m_prefabFiles;
|
||||
bool m_refreshPrefabList = true;
|
||||
bool m_prefabInstAtRoot = false;
|
||||
bool m_prefabUseCursor = true; // Use 3D cursor for prefab placement
|
||||
float m_prefabRaycastMargin = 0.0f; // Vertical margin for raycast placement
|
||||
|
||||
// 3D Cursor state
|
||||
bool m_showCursorPanel = false;
|
||||
bool m_cursorPlaceMode = false; // Click in viewport to place cursor
|
||||
|
||||
// Queries
|
||||
flecs::query<EntityNameComponent> m_nameQuery;
|
||||
|
||||
@@ -46,13 +46,53 @@ flecs::entity PrefabSystem::createInstance(const std::string &prefabPath,
|
||||
const std::string &name,
|
||||
EditorUISystem *uiSystem)
|
||||
{
|
||||
// Read prefab root transform so we can preserve it as the base
|
||||
Ogre::Vector3 prefabPos(0, 0, 0);
|
||||
Ogre::Quaternion prefabRot(Ogre::Quaternion::IDENTITY);
|
||||
Ogre::Vector3 prefabScale(1, 1, 1);
|
||||
try {
|
||||
std::ifstream file(prefabPath);
|
||||
if (file.is_open()) {
|
||||
nlohmann::json prefabJson;
|
||||
file >> prefabJson;
|
||||
file.close();
|
||||
if (prefabJson.contains("transform")) {
|
||||
auto &t = prefabJson["transform"];
|
||||
if (t.contains("position")) {
|
||||
auto &p = t["position"];
|
||||
prefabPos = Ogre::Vector3(
|
||||
p.value("x", 0.0f),
|
||||
p.value("y", 0.0f),
|
||||
p.value("z", 0.0f));
|
||||
}
|
||||
if (t.contains("rotation")) {
|
||||
auto &r = t["rotation"];
|
||||
prefabRot = Ogre::Quaternion(
|
||||
r.value("w", 1.0f),
|
||||
r.value("x", 0.0f),
|
||||
r.value("y", 0.0f),
|
||||
r.value("z", 0.0f));
|
||||
}
|
||||
if (t.contains("scale")) {
|
||||
auto &s = t["scale"];
|
||||
prefabScale = Ogre::Vector3(
|
||||
s.value("x", 1.0f),
|
||||
s.value("y", 1.0f),
|
||||
s.value("z", 1.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
// Ignore read errors — defaults are safe
|
||||
}
|
||||
|
||||
flecs::entity instance = m_world.entity();
|
||||
instance.add<EditorMarkerComponent>();
|
||||
instance.set<EntityNameComponent>(EntityNameComponent(name));
|
||||
instance.set<PrefabInstanceComponent>(
|
||||
PrefabInstanceComponent{ prefabPath, false });
|
||||
|
||||
// Create transform
|
||||
// Create transform: caller offset + prefab root transform
|
||||
TransformComponent transform;
|
||||
Ogre::SceneNode *parentNode = m_sceneMgr->getRootSceneNode();
|
||||
if (parent.is_valid() && parent != 0 &&
|
||||
@@ -61,12 +101,28 @@ flecs::entity PrefabSystem::createInstance(const std::string &prefabPath,
|
||||
instance.child_of(parent);
|
||||
}
|
||||
transform.node = parentNode->createChildSceneNode();
|
||||
transform.position = position;
|
||||
transform.rotation = Ogre::Quaternion::IDENTITY;
|
||||
transform.scale = Ogre::Vector3::UNIT_SCALE;
|
||||
transform.position = position + prefabPos;
|
||||
transform.rotation = prefabRot;
|
||||
transform.scale = prefabScale;
|
||||
transform.applyToNode();
|
||||
instance.set<TransformComponent>(transform);
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"PrefabSystem::createInstance: name=" + name +
|
||||
" offset=" + Ogre::StringConverter::toString(position) +
|
||||
" prefabPos=" + Ogre::StringConverter::toString(prefabPos) +
|
||||
" finalPos=" +
|
||||
Ogre::StringConverter::toString(
|
||||
transform.node->_getDerivedPosition()) +
|
||||
" parent=" +
|
||||
(parent.is_valid() && parent != 0 ?
|
||||
std::to_string(parent.id()) :
|
||||
"<root>") +
|
||||
" nodeParent=" +
|
||||
(parentNode != m_sceneMgr->getRootSceneNode() ?
|
||||
parentNode->getName() :
|
||||
"<root>"));
|
||||
|
||||
if (uiSystem)
|
||||
uiSystem->addEntity(instance);
|
||||
|
||||
|
||||
@@ -795,7 +795,7 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
flecs::entity child = m_world.entity();
|
||||
if (addEditorMarker)
|
||||
child.add<EditorMarkerComponent>();
|
||||
if (json.contains("id")) {
|
||||
if (childJson.contains("id")) {
|
||||
uint64_t cid = childJson.value("id", 0ULL);
|
||||
if (cid)
|
||||
m_entityMap[cid] = child;
|
||||
|
||||
Reference in New Issue
Block a user