From febeb8ff8dcf6c9eda5885d73970345c08c0b137 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Tue, 14 Apr 2026 18:54:28 +0300 Subject: [PATCH] Furniture enabled --- src/features/editScene/CMakeLists.txt | 2 + .../editScene/systems/CellGridSystem.cpp | 365 +++++++++++++++++- .../editScene/systems/CellGridSystem.hpp | 22 ++ .../editScene/systems/FurnitureLibrary.cpp | 126 ++++++ .../editScene/systems/FurnitureLibrary.hpp | 39 ++ .../editScene/systems/RoomLayoutSystem.cpp | 245 ++++++++++++ .../editScene/systems/RoomLayoutSystem.hpp | 3 + src/features/editScene/ui/CellGridEditor.cpp | 50 ++- src/features/editScene/ui/CellGridEditor.hpp | 1 + src/features/editScene/ui/RoomEditor.cpp | 49 +++ 10 files changed, 878 insertions(+), 24 deletions(-) create mode 100644 src/features/editScene/systems/FurnitureLibrary.cpp create mode 100644 src/features/editScene/systems/FurnitureLibrary.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 19365e1..f25b80d 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -24,6 +24,7 @@ set(EDITSCENE_SOURCES systems/ProceduralMeshSystem.cpp systems/CellGridSystem.cpp systems/RoomLayoutSystem.cpp + systems/FurnitureLibrary.cpp ui/TransformEditor.cpp ui/RenderableEditor.cpp ui/PhysicsColliderEditor.cpp @@ -87,6 +88,7 @@ set(EDITSCENE_HEADERS systems/EditorUISystem.hpp systems/CellGridSystem.hpp systems/RoomLayoutSystem.hpp + systems/FurnitureLibrary.hpp systems/ProceduralMaterialSystem.hpp systems/ProceduralMeshSystem.hpp systems/ProceduralTextureSystem.hpp diff --git a/src/features/editScene/systems/CellGridSystem.cpp b/src/features/editScene/systems/CellGridSystem.cpp index 1bbdbe0..197d7b9 100644 --- a/src/features/editScene/systems/CellGridSystem.cpp +++ b/src/features/editScene/systems/CellGridSystem.cpp @@ -1,9 +1,11 @@ #include "CellGridSystem.hpp" +#include "FurnitureLibrary.hpp" #include "../components/CellGrid.hpp" #include "../components/TriangleBuffer.hpp" #include "../components/Transform.hpp" #include "../components/StaticGeometry.hpp" #include "../components/StaticGeometryMember.hpp" +#include #include "../components/ProceduralMaterial.hpp" #include "../components/ProceduralTexture.hpp" #include @@ -485,6 +487,18 @@ void CellGridSystem::buildCellGrid(flecs::entity entity, " roofTop=" + std::to_string(roofTopTb.getVertices().size()) + " roofSide=" + std::to_string(roofSideTb.getVertices().size())); + // Build furniture + try { + buildFurniture(entity, grid); + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "CellGrid: Error building furniture: " + + std::string(e.what())); + } catch (...) { + Ogre::LogManager::getSingleton().logMessage( + "CellGrid: Unknown error building furniture"); + } + // Build window and door frames try { buildFrames(entity, grid, materialName); @@ -2251,6 +2265,108 @@ Ogre::MeshPtr CellGridSystem::convertToMesh(const std::string &name, } } +void CellGridSystem::buildFurniture(flecs::entity entity, + const CellGridComponent &grid) +{ + destroyFurniture(entity); + + if (!entity.has()) + return; + auto &transform = entity.get_mut(); + if (!transform.node) + return; + + FurnitureLibrary::getInstance().loadAll(); + auto &meshData = m_entityMeshes[entity.id()]; + + for (const auto &fcell : grid.furnitureCells) { + const FurnitureDefinition *def = + FurnitureLibrary::getInstance().findByName( + fcell.furnitureType); + if (!def) { + // Fallback: try to find by exact mesh name + for (const auto &d : + FurnitureLibrary::getInstance().getDefinitions()) { + if (d.meshName == fcell.furnitureType || + d.name == fcell.furnitureType) { + def = &d; + break; + } + } + } + if (!def) + continue; + + Ogre::MeshPtr mesh = + Ogre::MeshManager::getSingleton().getByName( + def->meshName); + if (!mesh) { + Ogre::String group = + Ogre::ResourceGroupManager::getSingleton() + .findGroupContainingResource( + def->meshName); + if (!group.empty()) { + mesh = Ogre::MeshManager::getSingleton().load( + def->meshName, group); + } + } + if (!mesh) { + Ogre::LogManager::getSingleton().logMessage( + "CellGrid: Could not load furniture mesh '" + + def->meshName + "'"); + continue; + } + + Ogre::Entity *ent = + m_sceneMgr->createEntity(def->meshName); + if (!ent) + continue; + + Ogre::SceneNode *fnode = + transform.node->createChildSceneNode(); + Ogre::Vector3 pos = grid.cellToWorld(fcell.x, fcell.y, + fcell.z); + // Apply small Y offset so furniture sits on floor + pos.y += 0.05f; + fnode->setPosition(pos); + fnode->setOrientation(Ogre::Quaternion( + Ogre::Degree(90.0f * fcell.rotation), + Ogre::Vector3::UNIT_Y)); + fnode->attachObject(ent); + ent->setRenderingDistance(100.0f); + + // Try to set material if mesh has submesh material + if (!mesh->getSubMesh(0)->getMaterialName().empty()) { + ent->setMaterialName( + mesh->getSubMesh(0)->getMaterialName()); + } + + meshData.furnitureEntities.push_back(ent); + } +} + +void CellGridSystem::destroyFurniture(flecs::entity entity) +{ + auto it = m_entityMeshes.find(entity.id()); + if (it == m_entityMeshes.end()) + return; + + for (auto *ent : it->second.furnitureEntities) { + if (!ent) + continue; + Ogre::SceneNode *node = ent->getParentSceneNode(); + if (node) { + node->detachObject(ent); + m_sceneMgr->destroySceneNode(node); + } + try { + m_sceneMgr->destroyEntity(ent); + } catch (...) { + } + } + it->second.furnitureEntities.clear(); +} + void CellGridSystem::destroyCellGridMeshes(CellGridComponent &grid) { // This is called from buildCellGrid which uses the entity ID as key in m_entityMeshes @@ -2305,6 +2421,9 @@ void CellGridSystem::destroyCellGridMeshes(flecs::entity entity) removeMesh(name); } + // Destroy furniture + destroyFurniture(entity); + // Destroy frames destroyFrames(entity); @@ -3100,7 +3219,7 @@ void CellGridSystem::buildFrames(flecs::entity entity, parent = parent.parent(); } - // Get the transform node to attach frames to + // Get the transform for frame positioning Ogre::SceneNode *transformNode = nullptr; if (entity.has()) { auto &transform = entity.get(); @@ -3111,32 +3230,51 @@ void CellGridSystem::buildFrames(flecs::entity entity, "CellGrid: No transform node for frames!"); return; } + + // Get parent transform for world position calculation + Ogre::Vector3 parentPos = transformNode->_getDerivedPosition(); + Ogre::Quaternion parentRot = transformNode->_getDerivedOrientation(); + Ogre::Vector3 parentScale = transformNode->_getDerivedScale(); // Create frame meshes with unique names per entity std::string meshPrefix = "CellGrid_" + std::to_string(entity.id()) + "_"; createWindowFrameMeshes(grid, materialName, meshPrefix, materialEntity); createDoorFrameMeshes(grid, materialName, meshPrefix, materialEntity); - - // Place frames as child entities of the transform node - // This way they move automatically when the parent transform changes - std::vector frameEntities; - placeWindowFrames(entity, grid, transformNode, frameEntities); - placeDoorFrames(entity, grid, transformNode, frameEntities); + + // Create or get StaticGeometry region for frames + std::string regionName = "FrameRegion_" + std::to_string(entity.id()); + Ogre::StaticGeometry* staticGeom = nullptr; + + auto it = m_entityMeshes.find(entity.id()); + if (it != m_entityMeshes.end() && it->second.frameStaticGeometry) { + // Reuse existing static geometry - destroy it first + m_sceneMgr->destroyStaticGeometry(it->second.frameStaticGeometry); + it->second.frameStaticGeometry = nullptr; + } + + staticGeom = m_sceneMgr->createStaticGeometry(regionName); + staticGeom->setCastShadows(true); + staticGeom->setRegionDimensions(Ogre::Vector3(1000.0f)); // Large region for all frames + + // Place frames into StaticGeometry (batched into single draw call) + int frameCount = 0; + placeWindowFramesInStaticGeometry(entity, grid, parentPos, parentRot, parentScale, staticGeom, frameCount); + placeDoorFramesInStaticGeometry(entity, grid, parentPos, parentRot, parentScale, staticGeom, frameCount); + + // Build the static geometry + staticGeom->build(); Ogre::LogManager::getSingleton().logMessage( - "CellGrid: Placed " + std::to_string(frameEntities.size()) + - " frame entities"); + "CellGrid: Batched " + std::to_string(frameCount) + + " frames into StaticGeometry"); // Store reference - auto it = m_entityMeshes.find(entity.id()); if (it != m_entityMeshes.end()) { - it->second.frameEntities = std::move(frameEntities); + it->second.frameStaticGeometry = staticGeom; } else { - // Clean up if we can't store them - for (auto *ent : frameEntities) { - m_sceneMgr->destroyEntity(ent); - } + // Clean up if we can't store it + m_sceneMgr->destroyStaticGeometry(staticGeom); } } @@ -3704,7 +3842,16 @@ void CellGridSystem::destroyFrames(flecs::entity entity) { auto it = m_entityMeshes.find(entity.id()); if (it != m_entityMeshes.end()) { - // Destroy frame entities (and their child nodes) + // Destroy StaticGeometry if present (replaces individual entities) + if (it->second.frameStaticGeometry) { + try { + m_sceneMgr->destroyStaticGeometry(it->second.frameStaticGeometry); + } catch (...) { + } + it->second.frameStaticGeometry = nullptr; + } + + // Destroy old frame entities (legacy cleanup) for (auto *ent : it->second.frameEntities) { if (ent) { // Get the node that owns this entity @@ -3748,3 +3895,189 @@ void CellGridSystem::destroyFrames(flecs::entity entity) } } } + +// ============================================================================ +// StaticGeometry Frame Placement (Batched for better performance) +// ============================================================================ + +void CellGridSystem::placeWindowFramesInStaticGeometry( + flecs::entity entity, const CellGridComponent &grid, + const Ogre::Vector3 &parentPos, const Ogre::Quaternion &parentRot, + const Ogre::Vector3 &parentScale, Ogre::StaticGeometry *staticGeom, + int &frameCount) +{ + std::string meshPrefix = + "CellGrid_" + std::to_string(entity.id()) + "_"; + Ogre::MeshPtr extMesh = Ogre::MeshManager::getSingleton().getByName( + meshPrefix + "window_external"); + Ogre::MeshPtr intMesh = Ogre::MeshManager::getSingleton().getByName( + meshPrefix + "window_internal"); + + if (!extMesh && !intMesh) + return; + + float halfCell = grid.cellSize / 2.0f; + float frameOffset = 0.0f; + float windowHeight = 2.0f * (grid.cellHeight / 4.0f); + float windowBottomOffset = 0.8f * (grid.cellHeight / 4.0f); + float windowY = windowBottomOffset; + + for (const auto &cell : grid.cells) { + Ogre::Vector3 origin = grid.cellToWorld(cell.x, cell.y, cell.z); + + auto addFrame = [&](Ogre::MeshPtr mesh, const Ogre::Vector3 &localPos, + const Ogre::Quaternion &localRot) { + if (!mesh) + return; + + // Calculate world position relative to parent + Ogre::Vector3 worldPos = parentPos + (parentRot * (localPos * parentScale)); + Ogre::Quaternion worldRot = parentRot * localRot; + + // Create temporary entity to add to StaticGeometry + Ogre::Entity* tempEnt = m_sceneMgr->createEntity(mesh->getName()); + staticGeom->addEntity(tempEnt, worldPos, worldRot, parentScale); + m_sceneMgr->destroyEntity(tempEnt); // StaticGeometry copies the data + frameCount++; + }; + + if (extMesh) { + frameOffset = 0.1f; + if (cell.hasFlag(CellFlags::WindowXNeg)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(-halfCell - frameOffset, windowY, 0); + Ogre::Quaternion rot(Ogre::Degree(-90), Ogre::Vector3::UNIT_Y); + addFrame(extMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::WindowXPos)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(halfCell + frameOffset, windowY, 0); + Ogre::Quaternion rot(Ogre::Degree(90), Ogre::Vector3::UNIT_Y); + addFrame(extMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::WindowZNeg)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(0, windowY, -halfCell - frameOffset); + Ogre::Quaternion rot(Ogre::Degree(180), Ogre::Vector3::UNIT_Y); + addFrame(extMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::WindowZPos)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(0, windowY, halfCell + frameOffset); + Ogre::Quaternion rot(Ogre::Degree(0), Ogre::Vector3::UNIT_Y); + addFrame(extMesh, pos, rot); + } + } + + if (intMesh) { + frameOffset = 0.1f; + if (cell.hasFlag(CellFlags::IntWindowXNeg)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(-halfCell + frameOffset, windowY, 0); + Ogre::Quaternion rot(Ogre::Degree(-90), Ogre::Vector3::UNIT_Y); + addFrame(intMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::IntWindowXPos)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(halfCell - frameOffset, windowY, 0); + Ogre::Quaternion rot(Ogre::Degree(90), Ogre::Vector3::UNIT_Y); + addFrame(intMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::IntWindowZNeg)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(0, windowY, -halfCell + frameOffset); + Ogre::Quaternion rot(Ogre::Degree(180), Ogre::Vector3::UNIT_Y); + addFrame(intMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::IntWindowZPos)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(0, windowY, halfCell - frameOffset); + Ogre::Quaternion rot(Ogre::Degree(0), Ogre::Vector3::UNIT_Y); + addFrame(intMesh, pos, rot); + } + } + } +} + +void CellGridSystem::placeDoorFramesInStaticGeometry( + flecs::entity entity, const CellGridComponent &grid, + const Ogre::Vector3 &parentPos, const Ogre::Quaternion &parentRot, + const Ogre::Vector3 &parentScale, Ogre::StaticGeometry *staticGeom, + int &frameCount) +{ + std::string meshPrefix = + "CellGrid_" + std::to_string(entity.id()) + "_"; + Ogre::MeshPtr extMesh = Ogre::MeshManager::getSingleton().getByName( + meshPrefix + "door_external"); + Ogre::MeshPtr intMesh = Ogre::MeshManager::getSingleton().getByName( + meshPrefix + "door_internal"); + + if (!extMesh && !intMesh) + return; + + float halfCell = grid.cellSize / 2.0f; + float frameOffset = 0.1f; + float solidExtOffset = 0.0f; + float solidIntOffset = 0.1f; + float doorHeightBase = 3.0f * (grid.cellHeight / 4.0f); + float extDoorFrameY = solidExtOffset; + float intDoorFrameY = solidIntOffset; + + auto addFrame = [&](Ogre::MeshPtr mesh, const Ogre::Vector3 &localPos, + const Ogre::Quaternion &localRot) { + if (!mesh) + return; + + // Calculate world position relative to parent + Ogre::Vector3 worldPos = parentPos + (parentRot * (localPos * parentScale)); + Ogre::Quaternion worldRot = parentRot * localRot; + + // Create temporary entity to add to StaticGeometry + Ogre::Entity* tempEnt = m_sceneMgr->createEntity(mesh->getName()); + staticGeom->addEntity(tempEnt, worldPos, worldRot, parentScale); + m_sceneMgr->destroyEntity(tempEnt); // StaticGeometry copies the data + frameCount++; + }; + + for (const auto &cell : grid.cells) { + Ogre::Vector3 origin = grid.cellToWorld(cell.x, cell.y, cell.z); + + if (extMesh) { + if (cell.hasFlag(CellFlags::DoorXNeg)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(-halfCell - frameOffset, extDoorFrameY, 0); + Ogre::Quaternion rot(Ogre::Degree(-90), Ogre::Vector3::UNIT_Y); + addFrame(extMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::DoorXPos)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(halfCell + frameOffset, extDoorFrameY, 0); + Ogre::Quaternion rot(Ogre::Degree(90), Ogre::Vector3::UNIT_Y); + addFrame(extMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::DoorZNeg)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(0, extDoorFrameY, -halfCell - frameOffset); + Ogre::Quaternion rot(Ogre::Degree(180), Ogre::Vector3::UNIT_Y); + addFrame(extMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::DoorZPos)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(0, extDoorFrameY, halfCell + frameOffset); + Ogre::Quaternion rot(Ogre::Degree(0), Ogre::Vector3::UNIT_Y); + addFrame(extMesh, pos, rot); + } + } + + if (intMesh) { + if (cell.hasFlag(CellFlags::IntDoorXNeg)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(-halfCell + frameOffset, intDoorFrameY, 0); + Ogre::Quaternion rot(Ogre::Degree(-90), Ogre::Vector3::UNIT_Y); + addFrame(intMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::IntDoorXPos)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(halfCell - frameOffset, intDoorFrameY, 0); + Ogre::Quaternion rot(Ogre::Degree(90), Ogre::Vector3::UNIT_Y); + addFrame(intMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::IntDoorZNeg)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(0, intDoorFrameY, -halfCell + frameOffset); + Ogre::Quaternion rot(Ogre::Degree(180), Ogre::Vector3::UNIT_Y); + addFrame(intMesh, pos, rot); + } + if (cell.hasFlag(CellFlags::IntDoorZPos)) { + Ogre::Vector3 pos = origin + Ogre::Vector3(0, intDoorFrameY, halfCell - frameOffset); + Ogre::Quaternion rot(Ogre::Degree(0), Ogre::Vector3::UNIT_Y); + addFrame(intMesh, pos, rot); + } + } + } +} diff --git a/src/features/editScene/systems/CellGridSystem.hpp b/src/features/editScene/systems/CellGridSystem.hpp index ef9d73e..f2989f2 100644 --- a/src/features/editScene/systems/CellGridSystem.hpp +++ b/src/features/editScene/systems/CellGridSystem.hpp @@ -8,6 +8,10 @@ namespace Procedural { class TriangleBuffer; } +namespace Ogre { + class StaticGeometry; +} + class CellGridSystem { public: CellGridSystem(flecs::world& world, Ogre::SceneManager* sceneMgr); @@ -58,6 +62,10 @@ private: // Build lot base geometry void buildLotBase(flecs::entity entity, struct LotComponent& lot); + // Build furniture meshes + void buildFurniture(flecs::entity entity, const struct CellGridComponent& grid); + void destroyFurniture(flecs::entity entity); + // Build window and door frames (3D frame meshes) void buildFrames(flecs::entity entity, const struct CellGridComponent& grid, const std::string& materialName); void createWindowFrameMeshes(const struct CellGridComponent& grid, const std::string& materialName, @@ -68,6 +76,14 @@ private: void placeDoorFrames(flecs::entity entity, const struct CellGridComponent& grid, Ogre::SceneNode* parentNode, std::vector& frameEntities); void destroyFrames(flecs::entity entity); + // StaticGeometry versions (batched for better performance) + void placeWindowFramesInStaticGeometry(flecs::entity entity, const struct CellGridComponent& grid, + const Ogre::Vector3& parentPos, const Ogre::Quaternion& parentRot, const Ogre::Vector3& parentScale, + Ogre::StaticGeometry* staticGeom, int& frameCount); + void placeDoorFramesInStaticGeometry(flecs::entity entity, const struct CellGridComponent& grid, + const Ogre::Vector3& parentPos, const Ogre::Quaternion& parentRot, const Ogre::Vector3& parentScale, + Ogre::StaticGeometry* staticGeom, int& frameCount); + // Convert triangle buffer to mesh Ogre::MeshPtr convertToMesh(const std::string& name, Procedural::TriangleBuffer& tb, const std::string& materialName); @@ -106,6 +122,12 @@ private: // Frame entities attached to the SceneNode (so they move with transform) std::vector frameEntities; + + // StaticGeometry for frames (batches all frames into single draw call) + Ogre::StaticGeometry* frameStaticGeometry = nullptr; + + // Furniture entities + std::vector furnitureEntities; }; std::unordered_map m_entityMeshes; diff --git a/src/features/editScene/systems/FurnitureLibrary.cpp b/src/features/editScene/systems/FurnitureLibrary.cpp new file mode 100644 index 0000000..f84eb11 --- /dev/null +++ b/src/features/editScene/systems/FurnitureLibrary.cpp @@ -0,0 +1,126 @@ +#include "FurnitureLibrary.hpp" +#include +#include +#include +#include +#include + +FurnitureLibrary &FurnitureLibrary::getInstance() +{ + static FurnitureLibrary instance; + return instance; +} + +void FurnitureLibrary::loadAll() +{ + if (m_loaded) + return; + m_loaded = true; + m_definitions.clear(); + + std::vector names; + const std::vector &groups = + Ogre::ResourceGroupManager::getSingleton() + .getResourceGroups(); + for (const auto &group : groups) { + std::vector groupNames = + *Ogre::ResourceGroupManager::getSingleton() + .findResourceNames(group, "furniture-*.json"); + names.insert(names.end(), groupNames.begin(), + groupNames.end()); + } + + for (const auto &name : names) { + Ogre::String group = + Ogre::ResourceGroupManager::getSingleton() + .findGroupContainingResource(name); + Ogre::DataStreamPtr stream = + Ogre::ResourceGroupManager::getSingleton() + .openResource(name, group); + Ogre::String jsonStr = stream->getAsString(); + try { + nlohmann::json jdata = nlohmann::json::parse(jsonStr); + FurnitureDefinition def; + def.name = jdata.value("name", name); + def.meshName = jdata.value("mesh", ""); + def.jsonData = jdata; + if (jdata.contains("tags") && jdata["tags"].is_array()) { + for (auto &tag : jdata["tags"]) { + def.tags.push_back(tag.get()); + } + } + if (!def.meshName.empty()) { + m_definitions.push_back(std::move(def)); + } + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "FurnitureLibrary: Failed to parse " + name + + ": " + e.what()); + } + } + + Ogre::LogManager::getSingleton().logMessage( + "FurnitureLibrary: Loaded " + + std::to_string(m_definitions.size()) + + " furniture definitions"); +} + +const std::vector &FurnitureLibrary::getDefinitions() + const +{ + return m_definitions; +} + +const FurnitureDefinition *FurnitureLibrary::findByName( + const std::string &name) const +{ + for (const auto &def : m_definitions) { + if (def.name == name) + return &def; + } + return nullptr; +} + +std::vector FurnitureLibrary::findByTags( + const std::vector &requiredTags, + const std::vector &forbiddenTags) const +{ + std::vector results; + for (const auto &def : m_definitions) { + bool match = true; + for (const auto &req : requiredTags) { + if (std::find(def.tags.begin(), def.tags.end(), req) == + def.tags.end()) { + match = false; + break; + } + } + if (!match) + continue; + for (const auto &forb : forbiddenTags) { + if (std::find(def.tags.begin(), def.tags.end(), forb) != + def.tags.end()) { + match = false; + break; + } + } + if (match) + results.push_back(&def); + } + return results; +} + +const FurnitureDefinition *FurnitureLibrary::selectByTags( + const std::vector &requiredTags, + const std::vector &forbiddenTags) const +{ + auto candidates = findByTags(requiredTags, forbiddenTags); + if (candidates.empty()) + return nullptr; + + static std::mt19937 rng((unsigned)std::chrono::steady_clock::now() + .time_since_epoch() + .count()); + std::uniform_int_distribution dist(0, candidates.size() - 1); + return candidates[dist(rng)]; +} diff --git a/src/features/editScene/systems/FurnitureLibrary.hpp b/src/features/editScene/systems/FurnitureLibrary.hpp new file mode 100644 index 0000000..2bcd717 --- /dev/null +++ b/src/features/editScene/systems/FurnitureLibrary.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +struct FurnitureDefinition { + std::string name; + std::string meshName; + std::vector tags; + nlohmann::json jsonData; +}; + +class FurnitureLibrary { +public: + static FurnitureLibrary &getInstance(); + + void loadAll(); + bool isLoaded() const { return m_loaded; } + void reload() { m_loaded = false; loadAll(); } + + const std::vector &getDefinitions() const; + + const FurnitureDefinition *findByName(const std::string &name) const; + + std::vector findByTags( + const std::vector &requiredTags, + const std::vector &forbiddenTags = {}) const; + + const FurnitureDefinition *selectByTags( + const std::vector &requiredTags, + const std::vector &forbiddenTags = {}) const; + +private: + FurnitureLibrary() = default; + std::vector m_definitions; + bool m_loaded = false; +}; diff --git a/src/features/editScene/systems/RoomLayoutSystem.cpp b/src/features/editScene/systems/RoomLayoutSystem.cpp index 9806cd7..4395874 100644 --- a/src/features/editScene/systems/RoomLayoutSystem.cpp +++ b/src/features/editScene/systems/RoomLayoutSystem.cpp @@ -1,9 +1,11 @@ #include "RoomLayoutSystem.hpp" +#include "FurnitureLibrary.hpp" #include "../components/CellGrid.hpp" #include #include #include #include +#include RoomLayoutSystem::RoomLayoutSystem(flecs::world& world, Ogre::SceneManager* sceneMgr) : m_world(world) @@ -131,6 +133,15 @@ void RoomLayoutSystem::reprocessAll() processExteriorGeneration(*grid); } }); + + // STEP 7: Generate furniture for all rooms + FurnitureLibrary::getInstance().loadAll(); + m_roomQuery.each([&](flecs::entity roomEntity, RoomComponent& room) { + CellGridComponent* grid = findParentGrid(roomEntity); + if (grid) { + generateRoomFurniture(room, *grid); + } + }); Ogre::LogManager::getSingleton().logMessage("RoomLayoutSystem: Full reprocess complete."); } @@ -842,3 +853,237 @@ void RoomLayoutSystem::clearWallFlags(CellGridComponent& grid, int x, int y, int cell->flags &= ~flagsToClear; } + +void RoomLayoutSystem::generateRoomFurniture(RoomComponent& room, + CellGridComponent& grid) +{ + if (room.tags.empty()) + return; + + // Clear existing furniture in room bounds + for (auto it = grid.furnitureCells.begin(); + it != grid.furnitureCells.end();) { + if (it->x >= room.minX && it->x < room.maxX && + it->y >= room.minY && it->y < room.maxY && + it->z >= room.minZ && it->z < room.maxZ) { + it = grid.furnitureCells.erase(it); + } else { + ++it; + } + } + + int midX = (room.minX + room.maxX - 1) / 2; + int midZ = (room.minZ + room.maxZ - 1) / 2; + + // Side: 0=Z- (minZ), 1=Z+ (maxZ), 2=X- (minX), 3=X+ (maxX) + const int ROTATION_BY_SIDE[4] = { 2, 0, 3, 1 }; + const uint64_t WALL_FLAGS_BY_SIDE[4] = { + CellFlags::IntWallZNeg | CellFlags::WallZNeg, + CellFlags::IntWallZPos | CellFlags::WallZPos, + CellFlags::IntWallXNeg | CellFlags::WallXNeg, + CellFlags::IntWallXPos | CellFlags::WallXPos + }; + const uint64_t WINDOW_FLAGS_BY_SIDE[4] = { + CellFlags::IntWindowZNeg | CellFlags::WindowZNeg, + CellFlags::IntWindowZPos | CellFlags::WindowZPos, + CellFlags::IntWindowXNeg | CellFlags::WindowXNeg, + CellFlags::IntWindowXPos | CellFlags::WindowXPos + }; + const uint64_t DOOR_FLAGS_BY_SIDE[4] = { + CellFlags::IntDoorZNeg | CellFlags::DoorZNeg, + CellFlags::IntDoorZPos | CellFlags::DoorZPos, + CellFlags::IntDoorXNeg | CellFlags::DoorXNeg, + CellFlags::IntDoorXPos | CellFlags::DoorXPos + }; + + auto cellHasAnyDoor = [&](int x, int y, int z) -> bool { + const Cell *cell = grid.findCell(x, y, z); + if (!cell) + return false; + return (cell->flags & CellFlags::AllDoors) != 0; + }; + auto cellHasAnyWindow = [&](int x, int y, int z) -> bool { + const Cell *cell = grid.findCell(x, y, z); + if (!cell) + return false; + return (cell->flags & CellFlags::AllWindows) != 0; + }; + auto cellHasAnyWall = [&](int x, int y, int z) -> bool { + const Cell *cell = grid.findCell(x, y, z); + if (!cell) + return false; + return (cell->flags & CellFlags::AllWalls) != 0; + }; + + auto canPlaceFurniture = [&](const FurnitureDefinition *def, + int x, int y, int z) -> bool { + if (!def) + return false; + // If cell has any door, furniture must have "door" tag + if (cellHasAnyDoor(x, y, z)) { + if (std::find(def->tags.begin(), def->tags.end(), + "door") == def->tags.end()) + return false; + } + // If cell has any window, furniture must NOT have "nowindow" tag + if (cellHasAnyWindow(x, y, z)) { + if (std::find(def->tags.begin(), def->tags.end(), + "nowindow") != def->tags.end()) + return false; + } + // If cell has no walls, furniture must have "nowall" tag + if (!cellHasAnyWall(x, y, z)) { + if (std::find(def->tags.begin(), def->tags.end(), + "nowall") == def->tags.end()) + return false; + } + return true; + }; + + auto placeOne = [&](int cx, int cy, int cz, int side, + const std::vector &tags, + const std::vector ¬ags, + bool onWindow) -> bool { + if (grid.findFurnitureCell(cx, cy, cz)) + return false; + const Cell *cell = grid.findCell(cx, cy, cz); + if (!cell) + return false; + + uint64_t sideFlags = onWindow ? WINDOW_FLAGS_BY_SIDE[side] : + WALL_FLAGS_BY_SIDE[side]; + if ((cell->flags & sideFlags) == 0) + return false; + + auto *def = FurnitureLibrary::getInstance().selectByTags( + tags, notags); + if (!def) + return false; + + if (!canPlaceFurniture(def, cx, cy, cz)) + return false; + + auto &fcell = grid.getOrCreateFurnitureCell(cx, cy, cz); + fcell.furnitureType = def->name; + fcell.rotation = ROTATION_BY_SIDE[side]; + fcell.tags = tags; + return true; + }; + + std::vector baseTags = room.tags; + std::vector etags = baseTags; + std::vector itags = baseTags; + std::vector otags = baseTags; + std::vector ftags = baseTags; + etags.push_back("essential"); + itags.push_back("important"); + otags.push_back("optional"); + ftags.push_back("filler"); + + struct TagData { + std::vector tags; + std::vector notagsWalls; + std::vector notagsWindows; + bool essential; + bool filler; + }; + TagData taglist[] = { + { etags, { "nowall", "nowindow" }, { "nowindow" }, true, + false }, + { itags, { "nowall", "nowindow" }, { "nowindow" }, false, + false }, + { otags, { "nowall", "nowindow" }, { "nowindow" }, false, + false }, + { ftags, { "nowall", "nowindow" }, { "nowindow" }, false, + true }, + }; + + // Midpoints for each side + std::tuple mpoints[4] = { + { midX, room.minY, room.minZ }, // side 0 (Z-) + { midX, room.minY, room.maxZ - 1 }, // side 1 (Z+) + { room.minX, room.minY, midZ }, // side 2 (X-) + { room.maxX - 1, room.minY, midZ }, // side 3 (X+) + }; + + for (const auto &mtags : taglist) { + bool placed = false; + + // Try wall midpoints + for (int side = 0; side < 4 && !placed; ++side) { + int cx = std::get<0>(mpoints[side]); + int cy = std::get<1>(mpoints[side]); + int cz = std::get<2>(mpoints[side]); + placed = placeOne(cx, cy, cz, side, mtags.tags, + mtags.notagsWalls, false); + if (placed && !mtags.filler) + break; + if (mtags.filler) + placed = false; + } + + // Try wall ranges (all edge cells on each side) + if (!placed) { + for (int side = 0; side < 4 && !placed; ++side) { + auto edgeCells = getRoomEdgeCells(room, side); + for (const auto &cell : edgeCells) { + int cx = cell.first; + int cz = cell.second; + int cy = room.minY; + placed = placeOne(cx, cy, cz, side, + mtags.tags, + mtags.notagsWalls, + false); + if (placed && !mtags.filler) + break; + } + if (placed && !mtags.filler) + break; + } + } + + if (mtags.filler) + placed = false; + + // Try window midpoints + if (!placed) { + for (int side = 0; side < 4 && !placed; ++side) { + int cx = std::get<0>(mpoints[side]); + int cy = std::get<1>(mpoints[side]); + int cz = std::get<2>(mpoints[side]); + placed = placeOne(cx, cy, cz, side, mtags.tags, + mtags.notagsWindows, true); + if (placed && !mtags.filler) + break; + if (mtags.filler) + placed = false; + } + } + + // Try window ranges + if (!placed) { + for (int side = 0; side < 4 && !placed; ++side) { + auto edgeCells = getRoomEdgeCells(room, side); + for (const auto &cell : edgeCells) { + int cx = cell.first; + int cz = cell.second; + int cy = room.minY; + placed = placeOne(cx, cy, cz, side, + mtags.tags, + mtags.notagsWindows, + true); + if (placed && !mtags.filler) + break; + } + if (placed && !mtags.filler) + break; + } + } + + if (!placed && !mtags.filler) { + break; // do not place anything if essentials were not placed + } + } + + grid.markDirty(); +} diff --git a/src/features/editScene/systems/RoomLayoutSystem.hpp b/src/features/editScene/systems/RoomLayoutSystem.hpp index b074505..cd0fd40 100644 --- a/src/features/editScene/systems/RoomLayoutSystem.hpp +++ b/src/features/editScene/systems/RoomLayoutSystem.hpp @@ -62,6 +62,9 @@ private: // This always runs at the end of the pipeline for all rooms void processExteriorGeneration(class CellGridComponent& grid); + // Generate furniture for a room based on its tags + void generateRoomFurniture(class RoomComponent& room, class CellGridComponent& grid); + // Helper: Get the parent CellGridComponent for an entity class CellGridComponent* findParentGrid(flecs::entity entity); diff --git a/src/features/editScene/ui/CellGridEditor.cpp b/src/features/editScene/ui/CellGridEditor.cpp index d5cdf76..35eecfa 100644 --- a/src/features/editScene/ui/CellGridEditor.cpp +++ b/src/features/editScene/ui/CellGridEditor.cpp @@ -2,10 +2,13 @@ #include "../components/CellGrid.hpp" #include "../components/ProceduralMaterial.hpp" #include "../components/ProceduralTexture.hpp" +#include "../systems/FurnitureLibrary.hpp" #include + bool CellGridEditor::renderComponent(flecs::entity entity, CellGridComponent& grid) { + m_currentEntity = entity; bool modified = false; if (ImGui::CollapsingHeader("Cell Grid", ImGuiTreeNodeFlags_DefaultOpen)) { @@ -145,17 +148,38 @@ void CellGridEditor::renderFurnitureEditor(CellGridComponent& grid) { static int fx = 0, fy = 0, fz = 0; static char tagsBuffer[256] = {0}; - static char furnitureType[128] = {0}; + static int selectedFurnitureIndex = -1; static int rotation = 0; + FurnitureLibrary::getInstance().loadAll(); + const auto& defs = FurnitureLibrary::getInstance().getDefinitions(); + ImGui::InputInt3("Furniture Pos", &fx); ImGui::InputText("Tags (comma separated)", tagsBuffer, sizeof(tagsBuffer)); - ImGui::InputText("Furniture Type", furnitureType, sizeof(furnitureType)); + + // Furniture type dropdown + std::string preview = selectedFurnitureIndex >= 0 && selectedFurnitureIndex < (int)defs.size() + ? defs[selectedFurnitureIndex].name + " (" + defs[selectedFurnitureIndex].meshName + ")" + : "Select furniture type..."; + + if (ImGui::BeginCombo("Furniture Type", preview.c_str())) { + for (int i = 0; i < (int)defs.size(); ++i) { + std::string label = defs[i].name + " (" + defs[i].meshName + ")"; + bool isSelected = (selectedFurnitureIndex == i); + if (ImGui::Selectable(label.c_str(), isSelected)) { + selectedFurnitureIndex = i; + } + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + ImGui::SliderInt("Rotation", &rotation, 0, 3); - if (ImGui::Button("Add/Update Furniture")) { + if (ImGui::Button("Add/Update Furniture") && selectedFurnitureIndex >= 0) { auto& fcell = grid.getOrCreateFurnitureCell(fx, fy, fz); - fcell.furnitureType = furnitureType; + fcell.furnitureType = defs[selectedFurnitureIndex].name; fcell.rotation = rotation; fcell.tags.clear(); // Parse tags @@ -164,7 +188,6 @@ void CellGridEditor::renderFurnitureEditor(CellGridComponent& grid) size_t end = tagsStr.find(','); while (end != std::string::npos) { std::string tag = tagsStr.substr(start, end - start); - // Trim whitespace tag.erase(0, tag.find_first_not_of(" \t")); tag.erase(tag.find_last_not_of(" \t") + 1); if (!tag.empty()) { @@ -173,7 +196,6 @@ void CellGridEditor::renderFurnitureEditor(CellGridComponent& grid) start = end + 1; end = tagsStr.find(',', start); } - // Last tag std::string lastTag = tagsStr.substr(start); lastTag.erase(0, lastTag.find_first_not_of(" \t")); lastTag.erase(lastTag.find_last_not_of(" \t") + 1); @@ -183,6 +205,12 @@ void CellGridEditor::renderFurnitureEditor(CellGridComponent& grid) grid.markDirty(); } + ImGui::SameLine(); + if (ImGui::Button("Delete Furniture")) { + grid.removeFurnitureCell(fx, fy, fz); + grid.markDirty(); + } + ImGui::Separator(); // List furniture cells @@ -196,8 +224,14 @@ void CellGridEditor::renderFurnitureEditor(CellGridComponent& grid) fy = fcell.y; fz = fcell.z; rotation = fcell.rotation; - strncpy(furnitureType, fcell.furnitureType.c_str(), sizeof(furnitureType) - 1); - // Build tags string + // Find index in defs + selectedFurnitureIndex = -1; + for (int i = 0; i < (int)defs.size(); ++i) { + if (defs[i].name == fcell.furnitureType) { + selectedFurnitureIndex = i; + break; + } + } std::string tags; for (size_t i = 0; i < fcell.tags.size(); ++i) { if (i > 0) tags += ", "; diff --git a/src/features/editScene/ui/CellGridEditor.hpp b/src/features/editScene/ui/CellGridEditor.hpp index 5ab0125..19e9add 100644 --- a/src/features/editScene/ui/CellGridEditor.hpp +++ b/src/features/editScene/ui/CellGridEditor.hpp @@ -20,4 +20,5 @@ private: int newCellX = 0, newCellY = 0, newCellZ = 0; char scriptBuffer[16384] = {0}; bool showGrid = false; + flecs::entity m_currentEntity = flecs::entity::null(); }; diff --git a/src/features/editScene/ui/RoomEditor.cpp b/src/features/editScene/ui/RoomEditor.cpp index 828d4df..fcbc5a3 100644 --- a/src/features/editScene/ui/RoomEditor.cpp +++ b/src/features/editScene/ui/RoomEditor.cpp @@ -1,8 +1,10 @@ #include "RoomEditor.hpp" #include "../components/CellGrid.hpp" #include "../components/EntityName.hpp" +#include "../systems/FurnitureLibrary.hpp" #include #include +#include bool RoomEditor::renderComponent(flecs::entity entity, RoomComponent& room) { @@ -80,6 +82,53 @@ bool RoomEditor::renderComponent(flecs::entity entity, RoomComponent& room) if (!lastTag.empty()) room.tags.push_back(lastTag); modified = true; } + + // Validate that furniture exists for these tags + if (!room.tags.empty()) { + FurnitureLibrary::getInstance().loadAll(); + auto matches = FurnitureLibrary::getInstance().findByTags(room.tags, {}); + if (matches.empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), + "Warning: No furniture found for these tags"); + } else { + ImGui::TextDisabled("%zu furniture item(s) match", matches.size()); + } + } + + if (ImGui::CollapsingHeader("Available Furniture", ImGuiTreeNodeFlags_None)) { + ImGui::Indent(); + FurnitureLibrary::getInstance().loadAll(); + const auto& defs = FurnitureLibrary::getInstance().getDefinitions(); + if (defs.empty()) { + ImGui::TextDisabled("No furniture definitions loaded"); + } else { + // Build a sorted set of all tags + std::set allTags; + for (const auto& def : defs) { + for (const auto& tag : def.tags) { + allTags.insert(tag); + } + } + ImGui::TextDisabled("All tags:"); + std::string tagList; + for (const auto& tag : allTags) { + if (!tagList.empty()) tagList += ", "; + tagList += tag; + } + ImGui::TextWrapped("%s", tagList.c_str()); + ImGui::Separator(); + + for (const auto& def : defs) { + std::string tagStr; + for (size_t i = 0; i < def.tags.size(); ++i) { + if (i > 0) tagStr += ", "; + tagStr += def.tags[i]; + } + ImGui::BulletText("%s [%s]", def.name.c_str(), tagStr.c_str()); + } + } + ImGui::Unindent(); + } ImGui::Unindent(); }