diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index fbfb98e..f6fa7e5 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -22,6 +22,7 @@ set(EDITSCENE_SOURCES systems/ProceduralTextureSystem.cpp systems/ProceduralMaterialSystem.cpp systems/ProceduralMeshSystem.cpp + systems/CellGridSystem.cpp ui/TransformEditor.cpp ui/RenderableEditor.cpp ui/PhysicsColliderEditor.cpp @@ -36,6 +37,13 @@ set(EDITSCENE_SOURCES ui/ProceduralMaterialEditor.cpp ui/PrimitiveEditor.cpp ui/TriangleBufferEditor.cpp + ui/CellGridEditor.cpp + ui/LotEditor.cpp + ui/DistrictEditor.cpp + ui/TownEditor.cpp + ui/RoofEditor.cpp + ui/RoomEditor.cpp + ui/FurnitureTemplateEditor.cpp ui/ComponentRegistration.cpp components/LightModule.cpp components/CameraModule.cpp @@ -45,6 +53,9 @@ set(EDITSCENE_SOURCES components/ProceduralMaterialModule.cpp components/PrimitiveModule.cpp components/TriangleBufferModule.cpp + components/CellGridModule.cpp + components/CellGridEditorsModule.cpp + components/CellGrid.cpp camera/EditorCamera.cpp gizmo/Gizmo.cpp physics/physics.cpp @@ -68,7 +79,9 @@ set(EDITSCENE_HEADERS components/ProceduralMaterial.hpp components/Primitive.hpp components/TriangleBuffer.hpp + components/CellGrid.hpp systems/EditorUISystem.hpp + systems/CellGridSystem.hpp systems/ProceduralMaterialSystem.hpp systems/ProceduralMeshSystem.hpp systems/ProceduralTextureSystem.hpp @@ -95,6 +108,13 @@ set(EDITSCENE_HEADERS ui/ProceduralMaterialEditor.hpp ui/PrimitiveEditor.hpp ui/TriangleBufferEditor.hpp + ui/CellGridEditor.hpp + ui/LotEditor.hpp + ui/DistrictEditor.hpp + ui/TownEditor.hpp + ui/RoofEditor.hpp + ui/RoomEditor.hpp + ui/FurnitureTemplateEditor.hpp camera/EditorCamera.hpp gizmo/Gizmo.hpp physics/physics.h diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 41e9a4f..7e8a7c1 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -9,6 +9,7 @@ #include "systems/ProceduralTextureSystem.hpp" #include "systems/ProceduralMaterialSystem.hpp" #include "systems/ProceduralMeshSystem.hpp" +#include "systems/CellGridSystem.hpp" #include "camera/EditorCamera.hpp" #include "components/EntityName.hpp" #include "components/Transform.hpp" @@ -26,6 +27,8 @@ #include "components/ProceduralMaterial.hpp" #include "components/Primitive.hpp" #include "components/TriangleBuffer.hpp" +#include "components/CellGrid.hpp" +#include "components/CellGridModule.hpp" #include #include @@ -186,6 +189,10 @@ void EditorApp::setup() // Setup ProceduralMesh system m_proceduralMeshSystem = std::make_unique(m_world, m_sceneMgr); m_proceduralMeshSystem->initialize(); + + // Setup CellGrid system + m_cellGridSystem = std::make_unique(m_world, m_sceneMgr); + m_cellGridSystem->initialize(); // Add default entities to UI cache for (auto &e : m_defaultEntities) { @@ -240,6 +247,9 @@ void EditorApp::setupECS() // Register Primitive and TriangleBuffer components m_world.component(); m_world.component(); + + // Register CellGrid/Town components + CellGridModule::registerComponents(m_world); } void EditorApp::createDefaultEntities() @@ -402,6 +412,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) if (m_proceduralMeshSystem) { m_proceduralMeshSystem->update(); } + + // Update CellGrid system + if (m_cellGridSystem) { + m_cellGridSystem->update(); + } // Don't call base class - it crashes when iterating input listeners return true; diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index 969b43b..583507a 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -21,6 +21,7 @@ class StaticGeometrySystem; class ProceduralTextureSystem; class ProceduralMaterialSystem; class ProceduralMeshSystem; +class CellGridSystem; /** * RenderTargetListener for ImGui frame management @@ -94,6 +95,7 @@ private: std::unique_ptr m_proceduralTextureSystem; std::unique_ptr m_proceduralMaterialSystem; std::unique_ptr m_proceduralMeshSystem; + std::unique_ptr m_cellGridSystem; // State uint16_t m_currentModifiers; diff --git a/src/features/editScene/components/CellGrid.cpp b/src/features/editScene/components/CellGrid.cpp new file mode 100644 index 0000000..0d8184e --- /dev/null +++ b/src/features/editScene/components/CellGrid.cpp @@ -0,0 +1,178 @@ +#include "CellGrid.hpp" +#include + +Cell* CellGridComponent::findCell(int x, int y, int z) +{ + int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z); + for (auto& cell : cells) { + if (cell.getKey() == key) { + return &cell; + } + } + return nullptr; +} + +const Cell* CellGridComponent::findCell(int x, int y, int z) const +{ + int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z); + for (const auto& cell : cells) { + if (cell.getKey() == key) { + return &cell; + } + } + return nullptr; +} + +Cell& CellGridComponent::getOrCreateCell(int x, int y, int z) +{ + Cell* existing = findCell(x, y, z); + if (existing) { + return *existing; + } + Cell newCell; + newCell.x = x; + newCell.y = y; + newCell.z = z; + cells.push_back(newCell); + return cells.back(); +} + +void CellGridComponent::removeCell(int x, int y, int z) +{ + int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z); + auto it = std::remove_if(cells.begin(), cells.end(), + [key](const Cell& c) { return c.getKey() == key; }); + cells.erase(it, cells.end()); +} + +FurnitureCell* CellGridComponent::findFurnitureCell(int x, int y, int z) +{ + int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z); + for (auto& cell : furnitureCells) { + if (cell.getKey() == key) { + return &cell; + } + } + return nullptr; +} + +FurnitureCell& CellGridComponent::getOrCreateFurnitureCell(int x, int y, int z) +{ + FurnitureCell* existing = findFurnitureCell(x, y, z); + if (existing) { + return *existing; + } + FurnitureCell newCell; + newCell.x = x; + newCell.y = y; + newCell.z = z; + furnitureCells.push_back(newCell); + return furnitureCells.back(); +} + +void CellGridComponent::removeFurnitureCell(int x, int y, int z) +{ + int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z); + auto it = std::remove_if(furnitureCells.begin(), furnitureCells.end(), + [key](const FurnitureCell& c) { return c.getKey() == key; }); + furnitureCells.erase(it, furnitureCells.end()); +} + +void CellGridComponent::clear() +{ + cells.clear(); + furnitureCells.clear(); + markDirty(); +} + +Ogre::Vector3 CellGridComponent::cellToWorld(int x, int y, int z) const +{ + return Ogre::Vector3( + x * cellSize, + y * cellHeight, + z * cellSize + ); +} + +void CellGridComponent::worldToCell(const Ogre::Vector3& worldPos, int& x, int& y, int& z) const +{ + x = Ogre::Math::Floor(worldPos.x / cellSize); + y = Ogre::Math::Floor(worldPos.y / cellHeight); + z = Ogre::Math::Floor(worldPos.z / cellSize); +} + +const char* CellGridComponent::getFlagName(uint64_t flag) +{ + switch (flag) { + case CellFlags::Floor: return "Floor"; + case CellFlags::Ceiling: return "Ceiling"; + case CellFlags::WallXNeg: return "Ext Wall X-"; + case CellFlags::WallXPos: return "Ext Wall X+"; + case CellFlags::WallZPos: return "Ext Wall Z+"; + case CellFlags::WallZNeg: return "Ext Wall Z-"; + case CellFlags::DoorXNeg: return "Ext Door X-"; + case CellFlags::DoorXPos: return "Ext Door X+"; + case CellFlags::DoorZPos: return "Ext Door Z+"; + case CellFlags::DoorZNeg: return "Ext Door Z-"; + case CellFlags::WindowXNeg: return "Ext Window X-"; + case CellFlags::WindowXPos: return "Ext Window X+"; + case CellFlags::WindowZPos: return "Ext Window Z+"; + case CellFlags::WindowZNeg: return "Ext Window Z-"; + case CellFlags::IntWallXNeg: return "Int Wall X-"; + case CellFlags::IntWallXPos: return "Int Wall X+"; + case CellFlags::IntWallZPos: return "Int Wall Z+"; + case CellFlags::IntWallZNeg: return "Int Wall Z-"; + case CellFlags::IntDoorXNeg: return "Int Door X-"; + case CellFlags::IntDoorXPos: return "Int Door X+"; + case CellFlags::IntDoorZPos: return "Int Door Z+"; + case CellFlags::IntDoorZNeg: return "Int Door Z-"; + case CellFlags::IntWindowXNeg: return "Int Window X-"; + case CellFlags::IntWindowXPos: return "Int Window X+"; + case CellFlags::IntWindowZPos: return "Int Window Z+"; + case CellFlags::IntWindowZNeg: return "Int Window Z-"; + default: return "Unknown"; + } +} + +uint64_t CellGridComponent::getFlagByName(const std::string& name) +{ + auto flags = getAllFlags(); + for (const auto& [flag, flagName] : flags) { + if (name == flagName) { + return flag; + } + } + return 0; +} + +std::vector> CellGridComponent::getAllFlags() +{ + return { + { CellFlags::Floor, "Floor" }, + { CellFlags::Ceiling, "Ceiling" }, + { CellFlags::WallXNeg, "Ext Wall X-" }, + { CellFlags::WallXPos, "Ext Wall X+" }, + { CellFlags::WallZPos, "Ext Wall Z+" }, + { CellFlags::WallZNeg, "Ext Wall Z-" }, + { CellFlags::DoorXNeg, "Ext Door X-" }, + { CellFlags::DoorXPos, "Ext Door X+" }, + { CellFlags::DoorZPos, "Ext Door Z+" }, + { CellFlags::DoorZNeg, "Ext Door Z-" }, + { CellFlags::WindowXNeg, "Ext Window X-" }, + { CellFlags::WindowXPos, "Ext Window X+" }, + { CellFlags::WindowZPos, "Ext Window Z+" }, + { CellFlags::WindowZNeg, "Ext Window Z-" }, + { CellFlags::IntWallXNeg, "Int Wall X-" }, + { CellFlags::IntWallXPos, "Int Wall X+" }, + { CellFlags::IntWallZPos, "Int Wall Z+" }, + { CellFlags::IntWallZNeg, "Int Wall Z-" }, + { CellFlags::IntDoorXNeg, "Int Door X-" }, + { CellFlags::IntDoorXPos, "Int Door X+" }, + { CellFlags::IntDoorZPos, "Int Door Z+" }, + { CellFlags::IntDoorZNeg, "Int Door Z-" }, + { CellFlags::IntWindowXNeg, "Int Window X-" }, + { CellFlags::IntWindowXPos, "Int Window X+" }, + { CellFlags::IntWindowZPos, "Int Window Z+" }, + { CellFlags::IntWindowZNeg, "Int Window Z-" }, + }; +} diff --git a/src/features/editScene/components/CellGrid.hpp b/src/features/editScene/components/CellGrid.hpp new file mode 100644 index 0000000..2da6a7f --- /dev/null +++ b/src/features/editScene/components/CellGrid.hpp @@ -0,0 +1,321 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +/** + * @brief Cell flags for building geometry + * + * Each cell in the 3D grid can have these features: + * - Floor/Ceiling + * - External walls (4 directions) + * - External doors/windows (4 directions each) + * - Internal walls (4 directions) + * - Internal doors/windows (4 directions each) + */ +namespace CellFlags { + constexpr uint64_t Floor = 1ULL << 0; + constexpr uint64_t Ceiling = 1ULL << 1; + // External walls + constexpr uint64_t WallXNeg = 1ULL << 2; // Wall on -X side + constexpr uint64_t WallXPos = 1ULL << 3; // Wall on +X side + constexpr uint64_t WallZPos = 1ULL << 4; // Wall on +Z side + constexpr uint64_t WallZNeg = 1ULL << 5; // Wall on -Z side + // External doors + constexpr uint64_t DoorXNeg = 1ULL << 6; + constexpr uint64_t DoorXPos = 1ULL << 7; + constexpr uint64_t DoorZPos = 1ULL << 8; + constexpr uint64_t DoorZNeg = 1ULL << 9; + // External windows + constexpr uint64_t WindowXNeg = 1ULL << 10; + constexpr uint64_t WindowXPos = 1ULL << 11; + constexpr uint64_t WindowZPos = 1ULL << 12; + constexpr uint64_t WindowZNeg = 1ULL << 13; + // Internal walls + constexpr uint64_t IntWallXNeg = 1ULL << 14; + constexpr uint64_t IntWallXPos = 1ULL << 15; + constexpr uint64_t IntWallZPos = 1ULL << 16; + constexpr uint64_t IntWallZNeg = 1ULL << 17; + // Internal doors + constexpr uint64_t IntDoorXNeg = 1ULL << 18; + constexpr uint64_t IntDoorXPos = 1ULL << 19; + constexpr uint64_t IntDoorZPos = 1ULL << 20; + constexpr uint64_t IntDoorZNeg = 1ULL << 21; + // Internal windows + constexpr uint64_t IntWindowXNeg = 1ULL << 22; + constexpr uint64_t IntWindowXPos = 1ULL << 23; + constexpr uint64_t IntWindowZPos = 1ULL << 24; + constexpr uint64_t IntWindowZNeg = 1ULL << 25; + + // Masks + constexpr uint64_t AllExternalWalls = WallXNeg | WallXPos | WallZPos | WallZNeg; + constexpr uint64_t AllInternalWalls = IntWallXNeg | IntWallXPos | IntWallZPos | IntWallZNeg; + constexpr uint64_t AllWalls = AllExternalWalls | AllInternalWalls; + constexpr uint64_t AllDoors = DoorXNeg | DoorXPos | DoorZPos | DoorZNeg | + IntDoorXNeg | IntDoorXPos | IntDoorZPos | IntDoorZNeg; + constexpr uint64_t AllWindows = WindowXNeg | WindowXPos | WindowZPos | WindowZNeg | + IntWindowXNeg | IntWindowXPos | IntWindowZPos | IntWindowZNeg; +} + +/** + * @brief A single cell in the 3D grid + */ +struct Cell { + int x = 0, y = 0, z = 0; + uint64_t flags = 0; + + // Generate unique key for this cell + int64_t getKey() const { + // Support -1024 to 1024 range for each axis + return (int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z; + } + + bool hasFlag(uint64_t flag) const { return (flags & flag) != 0; } + void setFlag(uint64_t flag) { flags |= flag; } + void clearFlag(uint64_t flag) { flags &= ~flag; } + void toggleFlag(uint64_t flag) { flags ^= flag; } +}; + +/** + * @brief Furniture placement in a cell + */ +struct FurnitureCell { + int x = 0, y = 0, z = 0; + std::vector tags; + std::string furnitureType; + int rotation = 0; // 0-3, representing 0, 90, 180, 270 degrees + + int64_t getKey() const { + return (int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z; + } +}; + +/** + * @brief Cell grid for procedural building generation + * + * This component stores a sparse 3D grid of cells that define + * building geometry (walls, floors, doors, windows). + * + * Used by: House/Lot generation, dungeon generation + */ +struct CellGridComponent { + // Grid dimensions (in cells) + int width = 10; // X dimension + int height = 1; // Y dimension (floors) + int depth = 10; // Z dimension + + // Cell size in world units + float cellSize = 4.0f; + float cellHeight = 4.0f; + + // The cell data (sparse storage) + std::vector cells; + std::vector furnitureCells; + + // Generation script (Lua or custom format) + std::string generationScript; + + // Dirty flag - triggers rebuild + bool dirty = true; + unsigned int version = 0; + + // Find cell at position (returns nullptr if not found) + Cell* findCell(int x, int y, int z); + const Cell* findCell(int x, int y, int z) const; + + // Get or create cell at position + Cell& getOrCreateCell(int x, int y, int z); + + // Remove cell at position + void removeCell(int x, int y, int z); + + // Find furniture cell + FurnitureCell* findFurnitureCell(int x, int y, int z); + + // Get or create furniture cell + FurnitureCell& getOrCreateFurnitureCell(int x, int y, int z); + + // Remove furniture cell + void removeFurnitureCell(int x, int y, int z); + + // Clear all cells + void clear(); + + // Mark for rebuild + void markDirty() { dirty = true; version++; } + + // Convert local cell position to world position + Ogre::Vector3 cellToWorld(int x, int y, int z) const; + + // Convert world position to cell coordinates + void worldToCell(const Ogre::Vector3& worldPos, int& x, int& y, int& z) const; + + // Get bit name for editor display + static const char* getFlagName(uint64_t flag); + static uint64_t getFlagByName(const std::string& name); + static std::vector> getAllFlags(); +}; + +/** + * @brief Room definition within a cell grid + * + * Rooms are rectangular areas that can be connected by doors + */ +struct RoomComponent { + // Room bounds (in cell coordinates, inclusive) + int minX = 0, minY = 0, minZ = 0; + int maxX = 0, maxY = 0, maxZ = 0; + + // Room name/tag for furniture placement rules + std::string roomType; // e.g., "bedroom", "kitchen", "hallway" + std::vector tags; + + // Connected room indices + std::vector connectedRooms; + + // Exit directions (0-3 = Z-, Z+, X-, X+) + std::vector exits; +}; + +/** + * @brief Roof definition for a lot + */ +struct RoofComponent { + enum Type { + Flat = 0, + Normal = 1, + Normal2 = 2, + Cone = 3, + Cylinder = 4 + }; + + Type type = Flat; + + // Position (in cell coordinates) + int posX = 0, posY = 0, posZ = 0; + + // Size (in cells) + int sizeX = 1, sizeZ = 1; + + // Offset from cell position (world units) + float offsetX = 0.0f, offsetY = 0.0f, offsetZ = 0.0f; + + // Height parameters + float baseHeight = 0.5f; + float maxHeight = 0.5f; +}; + +/** + * @brief Lot component - represents a single house/building lot + * + * A lot contains: + * - Cell grid (walls, floors, etc.) + * - Room definitions + * - Roof definitions + * - Furniture placement rules + */ +struct LotComponent { + // Lot dimensions (in cells) + int width = 10; + int depth = 10; + + // Elevation offset from district + float elevation = 0.0f; + + // Rotation around district center (degrees) + float angle = 0.0f; + + // Position offset (for manual placement) + float offsetX = 0.0f, offsetZ = 0.0f; + + // Template name if this lot was created from a template + std::string templateName; + + // Dirty flag + bool dirty = true; + + void markDirty() { dirty = true; } +}; + +/** + * @brief District component - contains multiple lots arranged in a circle + */ +struct DistrictComponent { + // District radius (distance from center to lots) + float radius = 50.0f; + + // Elevation offset + float elevation = 0.0f; + + // Height of district base (for plaza) + float height = 0.2f; + + // Is this a plaza (circular base)? + bool isPlaza = false; + + // Lot templates for this district + std::vector lotTemplateNames; + + // Procedural material for plaza (references entity with ProceduralMaterialComponent) + flecs::entity proceduralMaterialEntity; + std::string proceduralMaterialEntityId; // For serialization + + // Texture rectangle name from ProceduralTexture for UV mapping + std::string textureRectName; + + // Dirty flag + bool dirty = true; + + void markDirty() { dirty = true; } +}; + +/** + * @brief Town component - top-level container + */ +struct TownComponent { + std::string townName = "New Town"; + + // Color rectangles for texture atlas + struct ColorRect { + float left = 0.0f, top = 0.0f, right = 1.0f, bottom = 1.0f; + Ogre::ColourValue color = Ogre::ColourValue::White; + }; + std::unordered_map colorRects; + + // Material name (created from color rects) + std::string materialName; + + // Dirty flag + bool dirty = true; + bool materialDirty = true; + + void markDirty() { dirty = true; } + void markMaterialDirty() { materialDirty = true; } +}; + +/** + * @brief Furniture template - defines a furniture item + */ +struct FurnitureTemplateComponent { + std::string templateName; + std::vector tags; // e.g., "bed", "essential", "bedroom" + + // Mesh/model info + std::string meshName; + std::string materialName; + + // Placement rules + bool requiresWall = false; + bool requiresFloor = true; + bool blocksPath = true; + + // Offset from cell center + float offsetX = 0.0f, offsetY = 0.0f, offsetZ = 0.0f; + + // Random selection weight + float weight = 1.0f; +}; diff --git a/src/features/editScene/components/CellGridEditorsModule.cpp b/src/features/editScene/components/CellGridEditorsModule.cpp new file mode 100644 index 0000000..41da562 --- /dev/null +++ b/src/features/editScene/components/CellGridEditorsModule.cpp @@ -0,0 +1,142 @@ +#include "CellGrid.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/CellGridEditor.hpp" +#include "../ui/LotEditor.hpp" +#include "../ui/DistrictEditor.hpp" +#include "../ui/TownEditor.hpp" +#include "../ui/RoofEditor.hpp" +#include "../ui/RoomEditor.hpp" +#include "../ui/FurnitureTemplateEditor.hpp" + +// Register CellGrid component +REGISTER_COMPONENT("Cell Grid", CellGridComponent, CellGridEditor) +{ + registry.registerComponent( + "Cell Grid", + std::make_unique(), + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} + +// Register Lot component +REGISTER_COMPONENT("Lot", LotComponent, LotEditor) +{ + registry.registerComponent( + "Lot", + std::make_unique(), + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} + +// Register District component +REGISTER_COMPONENT("District", DistrictComponent, DistrictEditor) +{ + registry.registerComponent( + "District", + std::make_unique(), + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} + +// Register Town component +REGISTER_COMPONENT("Town", TownComponent, TownEditor) +{ + registry.registerComponent( + "Town", + std::make_unique(), + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} + +// Register Roof component +REGISTER_COMPONENT("Roof", RoofComponent, RoofEditor) +{ + registry.registerComponent( + "Roof", + std::make_unique(), + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} + +// Register Room component +REGISTER_COMPONENT("Room", RoomComponent, RoomEditor) +{ + registry.registerComponent( + "Room", + std::make_unique(), + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} + +// Register FurnitureTemplate component +REGISTER_COMPONENT("Furniture Template", FurnitureTemplateComponent, FurnitureTemplateEditor) +{ + registry.registerComponent( + "Furniture Template", + std::make_unique(), + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} diff --git a/src/features/editScene/components/CellGridModule.cpp b/src/features/editScene/components/CellGridModule.cpp new file mode 100644 index 0000000..6966bb7 --- /dev/null +++ b/src/features/editScene/components/CellGridModule.cpp @@ -0,0 +1,46 @@ +#include "CellGridModule.hpp" +#include "CellGrid.hpp" +#include +#include + +namespace CellGridModule { + +using json = nlohmann::json; + +void registerComponents(flecs::world& world) +{ + // Register components without .member<> for complex types (std::string, std::vector) + // Flecs doesn't have built-in reflection for these, but JSON serialization handles them + + // CellGridComponent + world.component(); + + // Cell struct + world.component(); + + // FurnitureCell struct + world.component(); + + // LotComponent + world.component(); + + // DistrictComponent + world.component(); + + // TownComponent + world.component(); + + // TownComponent::ColorRect + world.component(); + + // RoomComponent + world.component(); + + // RoofComponent + world.component(); + + // FurnitureTemplateComponent + world.component(); +} + +} // namespace CellGridModule diff --git a/src/features/editScene/components/CellGridModule.hpp b/src/features/editScene/components/CellGridModule.hpp new file mode 100644 index 0000000..982ddbd --- /dev/null +++ b/src/features/editScene/components/CellGridModule.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace CellGridModule { + void registerComponents(flecs::world& world); +} diff --git a/src/features/editScene/systems/CellGridSystem.cpp b/src/features/editScene/systems/CellGridSystem.cpp new file mode 100644 index 0000000..61a1361 --- /dev/null +++ b/src/features/editScene/systems/CellGridSystem.cpp @@ -0,0 +1,682 @@ +#include "CellGridSystem.hpp" +#include "../components/CellGrid.hpp" +#include "../components/TriangleBuffer.hpp" +#include "../components/Transform.hpp" +#include "../components/StaticGeometry.hpp" +#include "../components/StaticGeometryMember.hpp" +#include "../components/ProceduralMaterial.hpp" +#include "../components/ProceduralTexture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CellGridSystem::CellGridSystem(flecs::world& world, Ogre::SceneManager* sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_cellGridQuery(world.query()) + , m_townQuery(world.query()) + , m_districtQuery(world.query()) +{ +} + +CellGridSystem::~CellGridSystem() = default; + +void CellGridSystem::initialize() +{ + if (m_initialized) return; + m_initialized = true; + Ogre::LogManager::getSingleton().logMessage("CellGridSystem initialized"); +} + +void CellGridSystem::update() +{ + if (!m_initialized) return; + + // Process dirty cell grids + m_cellGridQuery.each([&](flecs::entity entity, CellGridComponent& grid) { + if (grid.dirty) { + buildCellGrid(entity, grid); + grid.dirty = false; + } + }); + + // Update town materials + m_townQuery.each([&](flecs::entity entity, TownComponent& town) { + if (town.materialDirty) { + updateTownMaterial(entity); + town.materialDirty = false; + } + }); + + // Process dirty districts (for plaza generation) + int districtCount = 0; + int dirtyDistrictCount = 0; + m_districtQuery.each([&](flecs::entity entity, DistrictComponent& district) { + districtCount++; + if (district.dirty) { + dirtyDistrictCount++; + Ogre::LogManager::getSingleton().logMessage("CellGrid: Processing dirty district " + + std::to_string(entity.id()) + " isPlaza=" + (district.isPlaza ? "true" : "false")); + buildDistrictPlaza(entity, district); + district.dirty = false; + } + }); + + static int frameCount = 0; + if (++frameCount % 60 == 0) { + Ogre::LogManager::getSingleton().logMessage("CellGrid: update() - " + + std::to_string(districtCount) + " districts, " + + std::to_string(dirtyDistrictCount) + " dirty"); + } +} + +void CellGridSystem::buildCellGrid(flecs::entity entity, CellGridComponent& grid) +{ + Ogre::LogManager::getSingleton().logMessage("CellGrid: buildCellGrid for entity " + std::to_string(entity.id()) + + " cells=" + std::to_string(grid.cells.size())); + + // Destroy existing meshes + destroyCellGridMeshes(grid); + + // Find parent town for material + std::string materialName = "Ogre/StandardFloor"; // default + flecs::entity townEntity = flecs::entity::null(); + + // Look for TownComponent in parent hierarchy + flecs::entity parent = entity.parent(); + while (parent.is_alive()) { + if (parent.has()) { + townEntity = parent; + auto& town = parent.get(); + materialName = town.materialName.empty() ? "TownMaterial_" + std::to_string(parent.id()) : town.materialName; + break; + } + parent = parent.parent(); + } + + if (!townEntity.is_alive()) { + Ogre::LogManager::getSingleton().logMessage("CellGrid: No parent town found for entity " + std::to_string(entity.id()) + ", using default material"); + } else { + Ogre::LogManager::getSingleton().logMessage("CellGrid: Found town " + std::to_string(townEntity.id()) + + " for entity " + std::to_string(entity.id()) + " material=" + materialName); + } + + MeshData& meshData = m_entityMeshes[entity.id()]; + + // Build geometry parts + Procedural::TriangleBuffer floorTb, ceilingTb, extWallTb, intWallTb, intWindowsTb, roofTb; + std::vector doorTbs, windowTbs; + + buildFloorsAndCeilings(grid, floorTb, ceilingTb); + buildWalls(grid, extWallTb, intWallTb, intWindowsTb); + buildCorners(grid, extWallTb); + buildDoors(grid, doorTbs); + buildWindows(grid, windowTbs); + buildRoofs(entity, grid, roofTb); + + // Generate unique base name + std::string baseName = "CellGrid_" + std::to_string(entity.id()) + "_" + std::to_string(m_meshCount++); + + // Convert to meshes and create entities + if (!entity.has()) { + Ogre::LogManager::getSingleton().logMessage("CellGrid: No TransformComponent for entity " + std::to_string(entity.id())); + return; + } + auto& transform = entity.get_mut(); + if (!transform.node) { + Ogre::LogManager::getSingleton().logMessage("CellGrid: No SceneNode for entity " + std::to_string(entity.id())); + return; + } + + // Create meshes for each part + if (floorTb.getVertices().size() >= 3) { + meshData.floorMesh = baseName + "_floor"; + auto mesh = convertToMesh(meshData.floorMesh, floorTb, materialName); + auto entity3d = m_sceneMgr->createEntity(meshData.floorMesh); + transform.node->attachObject(entity3d); + meshData.entities.push_back(entity3d); + } + + if (ceilingTb.getVertices().size() >= 3) { + meshData.ceilingMesh = baseName + "_ceiling"; + auto mesh = convertToMesh(meshData.ceilingMesh, ceilingTb, materialName); + auto entity3d = m_sceneMgr->createEntity(meshData.ceilingMesh); + transform.node->attachObject(entity3d); + meshData.entities.push_back(entity3d); + } + + if (extWallTb.getVertices().size() >= 3) { + meshData.extWallMesh = baseName + "_extWall"; + auto mesh = convertToMesh(meshData.extWallMesh, extWallTb, materialName); + auto entity3d = m_sceneMgr->createEntity(meshData.extWallMesh); + transform.node->attachObject(entity3d); + meshData.entities.push_back(entity3d); + } + + if (intWallTb.getVertices().size() >= 3) { + meshData.intWallMesh = baseName + "_intWall"; + auto mesh = convertToMesh(meshData.intWallMesh, intWallTb, materialName); + auto entity3d = m_sceneMgr->createEntity(meshData.intWallMesh); + transform.node->attachObject(entity3d); + meshData.entities.push_back(entity3d); + } + + if (intWindowsTb.getVertices().size() >= 3) { + meshData.intWindowsMesh = baseName + "_intWindows"; + auto mesh = convertToMesh(meshData.intWindowsMesh, intWindowsTb, materialName); + auto entity3d = m_sceneMgr->createEntity(meshData.intWindowsMesh); + transform.node->attachObject(entity3d); + meshData.entities.push_back(entity3d); + } + + // Roof + if (roofTb.getVertices().size() >= 3) { + meshData.roofMesh = baseName + "_roof"; + auto mesh = convertToMesh(meshData.roofMesh, roofTb, materialName); + auto entity3d = m_sceneMgr->createEntity(meshData.roofMesh); + transform.node->attachObject(entity3d); + meshData.entities.push_back(entity3d); + } + + Ogre::LogManager::getSingleton().logMessage("CellGrid: Built geometry for entity " + std::to_string(entity.id())); +} + +void CellGridSystem::buildFloorsAndCeilings(const CellGridComponent& grid, + Procedural::TriangleBuffer& floorTb, + Procedural::TriangleBuffer& ceilingTb) +{ + const float cellSize = grid.cellSize; + const float floorHeight = 0.1f; + const float ceilingOffset = grid.cellHeight - 0.3f; + + for (const auto& cell : grid.cells) { + Ogre::Vector3 origin = grid.cellToWorld(cell.x, cell.y, cell.z); + + // Floor + if (cell.hasFlag(CellFlags::Floor)) { + Procedural::BoxGenerator box; + box.setSizeX(cellSize - 0.1f); + box.setSizeY(floorHeight); + box.setSizeZ(cellSize - 0.1f); + box.setPosition(Ogre::Vector3(origin.x, origin.y + floorHeight/2.0f, origin.z)); + box.addToTriangleBuffer(floorTb); + } + + // Ceiling + if (cell.hasFlag(CellFlags::Ceiling)) { + Procedural::BoxGenerator box; + box.setSizeX(cellSize - 0.1f); + box.setSizeY(0.1f); + box.setSizeZ(cellSize - 0.1f); + box.setPosition(Ogre::Vector3(origin.x, origin.y + ceilingOffset, origin.z)); + box.addToTriangleBuffer(ceilingTb); + } + } +} + +void CellGridSystem::buildWalls(const CellGridComponent& grid, + Procedural::TriangleBuffer& extWallTb, + Procedural::TriangleBuffer& intWallTb, + Procedural::TriangleBuffer& intWindowsTb) +{ + const float cellSize = grid.cellSize; + const float solidExtHeight = grid.cellHeight; + const float solidExtOffset = 0.0f; + const float solidIntOffset = 0.1f; + const float solidIntHeight = grid.cellHeight - 0.3f - solidIntOffset; + + for (const auto& cell : grid.cells) { + Ogre::Vector3 origin = grid.cellToWorld(cell.x, cell.y, cell.z); + + // External walls + if (cell.hasFlag(CellFlags::WallXNeg)) { + Procedural::BoxGenerator box; + box.setSizeX(0.2f); + box.setSizeY(solidExtHeight); + box.setSizeZ(cellSize); + box.setPosition(Ogre::Vector3(origin.x - cellSize/2.0f - 0.1f, + origin.y + solidExtHeight/2.0f, origin.z)); + box.addToTriangleBuffer(extWallTb); + } + if (cell.hasFlag(CellFlags::WallXPos)) { + Procedural::BoxGenerator box; + box.setSizeX(0.2f); + box.setSizeY(solidExtHeight); + box.setSizeZ(cellSize); + box.setPosition(Ogre::Vector3(origin.x + cellSize/2.0f + 0.1f, + origin.y + solidExtHeight/2.0f, origin.z)); + box.addToTriangleBuffer(extWallTb); + } + if (cell.hasFlag(CellFlags::WallZPos)) { + Procedural::BoxGenerator box; + box.setSizeX(cellSize); + box.setSizeY(solidExtHeight); + box.setSizeZ(0.2f); + box.setPosition(Ogre::Vector3(origin.x, origin.y + solidExtHeight/2.0f, + origin.z + cellSize/2.0f + 0.1f)); + box.addToTriangleBuffer(extWallTb); + } + if (cell.hasFlag(CellFlags::WallZNeg)) { + Procedural::BoxGenerator box; + box.setSizeX(cellSize); + box.setSizeY(solidExtHeight); + box.setSizeZ(0.2f); + box.setPosition(Ogre::Vector3(origin.x, origin.y + solidExtHeight/2.0f, + origin.z - cellSize/2.0f - 0.1f)); + box.addToTriangleBuffer(extWallTb); + } + + // Internal walls + if (cell.hasFlag(CellFlags::IntWallXNeg)) { + Procedural::BoxGenerator box; + box.setSizeX(0.1f); + box.setSizeY(solidIntHeight); + box.setSizeZ(cellSize - 0.4f); + box.setPosition(Ogre::Vector3(origin.x - cellSize/2.0f + 0.05f, + origin.y + solidIntHeight/2.0f + solidIntOffset, origin.z)); + box.addToTriangleBuffer(intWallTb); + } + if (cell.hasFlag(CellFlags::IntWallXPos)) { + Procedural::BoxGenerator box; + box.setSizeX(0.1f); + box.setSizeY(solidIntHeight); + box.setSizeZ(cellSize - 0.4f); + box.setPosition(Ogre::Vector3(origin.x + cellSize/2.0f - 0.05f, + origin.y + solidIntHeight/2.0f + solidIntOffset, origin.z)); + box.addToTriangleBuffer(intWallTb); + } + if (cell.hasFlag(CellFlags::IntWallZPos)) { + Procedural::BoxGenerator box; + box.setSizeX(cellSize - 0.4f); + box.setSizeY(solidIntHeight); + box.setSizeZ(0.1f); + box.setPosition(Ogre::Vector3(origin.x, origin.y + solidIntHeight/2.0f + solidIntOffset, + origin.z + cellSize/2.0f - 0.05f)); + box.addToTriangleBuffer(intWallTb); + } + if (cell.hasFlag(CellFlags::IntWallZNeg)) { + Procedural::BoxGenerator box; + box.setSizeX(cellSize - 0.4f); + box.setSizeY(solidIntHeight); + box.setSizeZ(0.1f); + box.setPosition(Ogre::Vector3(origin.x, origin.y + solidIntHeight/2.0f + solidIntOffset, + origin.z - cellSize/2.0f + 0.05f)); + box.addToTriangleBuffer(intWallTb); + } + } +} + +void CellGridSystem::buildCorners(const CellGridComponent& grid, Procedural::TriangleBuffer& extWallTb) +{ + // Build external wall corners where walls meet + const float cellSize = grid.cellSize; + const float solidExtHeight = grid.cellHeight; + + for (const auto& cell : grid.cells) { + Ogre::Vector3 origin = grid.cellToWorld(cell.x, cell.y, cell.z); + uint64_t wallFlags = cell.flags & CellFlags::AllExternalWalls; + + // Corner cases: X- with Z+, X- with Z-, X+ with Z+, X+ with Z- + if ((cell.hasFlag(CellFlags::WallXNeg) || cell.hasFlag(CellFlags::WallZPos)) && + (wallFlags & (CellFlags::WallXNeg | CellFlags::WallZPos)) == (CellFlags::WallXNeg | CellFlags::WallZPos)) { + // Corner at X-, Z+ + Procedural::BoxGenerator box; + box.setSizeX(0.2f); + box.setSizeY(solidExtHeight); + box.setSizeZ(0.2f); + box.setPosition(Ogre::Vector3(origin.x - cellSize/2.0f - 0.1f, + origin.y + solidExtHeight/2.0f, + origin.z + cellSize/2.0f + 0.1f)); + box.addToTriangleBuffer(extWallTb); + } + + if ((cell.hasFlag(CellFlags::WallXNeg) || cell.hasFlag(CellFlags::WallZNeg)) && + (wallFlags & (CellFlags::WallXNeg | CellFlags::WallZNeg)) == (CellFlags::WallXNeg | CellFlags::WallZNeg)) { + // Corner at X-, Z- + Procedural::BoxGenerator box; + box.setSizeX(0.2f); + box.setSizeY(solidExtHeight); + box.setSizeZ(0.2f); + box.setPosition(Ogre::Vector3(origin.x - cellSize/2.0f - 0.1f, + origin.y + solidExtHeight/2.0f, + origin.z - cellSize/2.0f - 0.1f)); + box.addToTriangleBuffer(extWallTb); + } + + if ((cell.hasFlag(CellFlags::WallXPos) || cell.hasFlag(CellFlags::WallZPos)) && + (wallFlags & (CellFlags::WallXPos | CellFlags::WallZPos)) == (CellFlags::WallXPos | CellFlags::WallZPos)) { + // Corner at X+, Z+ + Procedural::BoxGenerator box; + box.setSizeX(0.2f); + box.setSizeY(solidExtHeight); + box.setSizeZ(0.2f); + box.setPosition(Ogre::Vector3(origin.x + cellSize/2.0f + 0.1f, + origin.y + solidExtHeight/2.0f, + origin.z + cellSize/2.0f + 0.1f)); + box.addToTriangleBuffer(extWallTb); + } + + if ((cell.hasFlag(CellFlags::WallXPos) || cell.hasFlag(CellFlags::WallZNeg)) && + (wallFlags & (CellFlags::WallXPos | CellFlags::WallZNeg)) == (CellFlags::WallXPos | CellFlags::WallZNeg)) { + // Corner at X+, Z- + Procedural::BoxGenerator box; + box.setSizeX(0.2f); + box.setSizeY(solidExtHeight); + box.setSizeZ(0.2f); + box.setPosition(Ogre::Vector3(origin.x + cellSize/2.0f + 0.1f, + origin.y + solidExtHeight/2.0f, + origin.z - cellSize/2.0f - 0.1f)); + box.addToTriangleBuffer(extWallTb); + } + } +} + +void CellGridSystem::buildDoors(const CellGridComponent& grid, std::vector& doorTbs) +{ + // Doors are cutouts in walls - we build the wall around them + // For now, use wall geometry with gaps + // This is simplified - full implementation needs more complex geometry +} + +void CellGridSystem::buildWindows(const CellGridComponent& grid, std::vector& windowTbs) +{ + // Windows are cutouts in walls - similar to doors +} + +void CellGridSystem::buildRoofs(flecs::entity lotEntity, const CellGridComponent& grid, Procedural::TriangleBuffer& roofTb) +{ + if (!lotEntity.has()) return; + + // Get roof components from lot's children + lotEntity.children([&](flecs::entity child) { + if (child.has()) { + auto& roof = child.get(); + Ogre::Vector3 origin = grid.cellToWorld(roof.posX, roof.posY, roof.posZ); + origin += Ogre::Vector3(roof.offsetX, roof.offsetY, roof.offsetZ); + + float width = roof.sizeX * grid.cellSize; + float depth = roof.sizeZ * grid.cellSize; + float height = roof.maxHeight - roof.baseHeight; + + switch (roof.type) { + case RoofComponent::Flat: { + Procedural::BoxGenerator box; + box.setSizeX(width); + box.setSizeY(roof.baseHeight); + box.setSizeZ(depth); + box.setPosition(Ogre::Vector3(origin.x + width/2.0f - grid.cellSize/2.0f, + origin.y + roof.baseHeight/2.0f, + origin.z + depth/2.0f - grid.cellSize/2.0f)); + box.addToTriangleBuffer(roofTb); + break; + } + case RoofComponent::Normal: + case RoofComponent::Normal2: { + // Gable roof using two rotated boxes or a prism + // Simplified as a box for now + Procedural::BoxGenerator box; + box.setSizeX(width); + box.setSizeY(roof.baseHeight + height/2.0f); + box.setSizeZ(depth); + box.setPosition(Ogre::Vector3(origin.x + width/2.0f - grid.cellSize/2.0f, + origin.y + (roof.baseHeight + height/2.0f)/2.0f, + origin.z + depth/2.0f - grid.cellSize/2.0f)); + box.addToTriangleBuffer(roofTb); + break; + } + case RoofComponent::Cone: { + Procedural::ConeGenerator cone; + cone.setRadius(width/2.0f); + cone.setHeight(roof.baseHeight + height); + cone.setPosition(Ogre::Vector3(origin.x, origin.y, origin.z)); + cone.addToTriangleBuffer(roofTb); + break; + } + case RoofComponent::Cylinder: { + Procedural::CylinderGenerator cyl; + cyl.setRadius(width/2.0f); + cyl.setHeight(roof.baseHeight); + cyl.setPosition(Ogre::Vector3(origin.x, origin.y + roof.baseHeight/2.0f, origin.z)); + cyl.addToTriangleBuffer(roofTb); + break; + } + } + } + }); +} + +Ogre::MeshPtr CellGridSystem::convertToMesh(const std::string& name, Procedural::TriangleBuffer& tb, const std::string& materialName) +{ + try { + Ogre::MeshPtr mesh = tb.transformToMesh(name); + if (!materialName.empty() && mesh->getNumSubMeshes() > 0) { + mesh->getSubMesh(0)->setMaterialName(materialName); + } + return mesh; + } catch (const Ogre::Exception& e) { + Ogre::LogManager::getSingleton().logMessage("CellGrid: Failed to create mesh '" + name + "': " + e.getDescription()); + return Ogre::MeshPtr(); + } +} + +void CellGridSystem::destroyCellGridMeshes(CellGridComponent& grid) +{ + // Cleanup handled by entity destruction, but we track mesh names +} + +void CellGridSystem::updateTownMaterial(flecs::entity townEntity) +{ + if (!townEntity.has()) return; + + auto& town = townEntity.get_mut(); + if (town.materialName.empty()) { + town.materialName = "TownMaterial_" + std::to_string(townEntity.id()); + } + + // Create or update material + auto& matMgr = Ogre::MaterialManager::getSingleton(); + Ogre::MaterialPtr mat; + + if (matMgr.resourceExists(town.materialName, Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME)) { + mat = matMgr.getByName(town.materialName); + mat->unload(); + } else { + mat = matMgr.create(town.materialName, Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + } + + // For now, create a simple colored material + // Full implementation would create texture atlas from color rects + auto pass = mat->getTechnique(0)->getPass(0); + pass->setDiffuse(Ogre::ColourValue(0.8f, 0.8f, 0.8f)); + pass->setAmbient(Ogre::ColourValue(0.4f, 0.4f, 0.4f)); + pass->setSpecular(Ogre::ColourValue(0.1f, 0.1f, 0.1f)); + + Ogre::LogManager::getSingleton().logMessage("CellGrid: Updated town material '" + town.materialName + "'"); +} + +void CellGridSystem::rebuildCellGrid(flecs::entity entity) +{ + if (!entity.is_alive() || !entity.has()) return; + entity.get_mut().markDirty(); +} + +void CellGridSystem::buildDistrictPlaza(flecs::entity entity, DistrictComponent& district) +{ + // Destroy existing plaza if any + auto it = m_plazaMeshes.find(entity.id()); + if (it != m_plazaMeshes.end()) { + if (it->second.entity) { + try { + m_sceneMgr->destroyEntity(it->second.entity); + } catch (...) {} + } + if (!it->second.meshName.empty()) { + try { + Ogre::MeshManager::getSingleton().remove(it->second.meshName); + } catch (...) {} + } + m_plazaMeshes.erase(it); + } + + if (!district.isPlaza) { + return; + } + + // Get transform from entity + if (!entity.has()) { + Ogre::LogManager::getSingleton().logMessage("CellGrid: District has no TransformComponent"); + return; + } + auto& transform = entity.get_mut(); + if (!transform.node) { + Ogre::LogManager::getSingleton().logMessage("CellGrid: District has no SceneNode"); + return; + } + + // Get material from district's procedural material entity + std::string materialName = "Ogre/StandardFloor"; // fallback + + if (district.proceduralMaterialEntity.is_alive() && + district.proceduralMaterialEntity.has()) { + const auto& mat = district.proceduralMaterialEntity.get(); + if (mat.created && !mat.materialName.empty()) { + materialName = mat.materialName; + } + } + + Ogre::LogManager::getSingleton().logMessage("CellGrid: Building plaza for district " + std::to_string(entity.id()) + + " with material: " + materialName); + + // Create plaza mesh using Lathe + Procedural::TriangleBuffer tb; + Procedural::Shape plazaShape; + + float radius = district.radius; + float height = district.height; + float elevation = district.elevation; + float mh = 4.0f; // margin height + + // Build the plaza shape profile (based on original code) + plazaShape.addPoint(0, -height - mh - mh); + plazaShape.addPoint(radius * 0.5f + mh, -height - mh - mh); + plazaShape.addPoint(radius + mh, -height - mh); + plazaShape.addPoint(radius, -height); + plazaShape.addPoint(radius, 0.0f); + plazaShape.addPoint(radius - 0.1f, 0.1f); + plazaShape.addPoint(radius - mh + 0.1f, height); + plazaShape.addPoint(radius - mh, height + 0.1f); + plazaShape.addPoint(radius * 0.5f + mh, height); + plazaShape.addPoint(0, height); + + // Generate lathe geometry + Procedural::Lathe() + .setShapeToExtrude(&plazaShape) + .setEnableNormals(true) + .setPosition(Ogre::Vector3(0.0f, height / 2.0f + elevation, 0.0f)) + .setNumSeg(24) + .addToTriangleBuffer(tb); + + // Check if we have valid geometry + size_t vertexCount = tb.getVertices().size(); + size_t indexCount = tb.getIndices().size(); + Ogre::LogManager::getSingleton().logMessage("CellGrid: Plaza triangle buffer has " + + std::to_string(vertexCount) + " vertices, " + std::to_string(indexCount) + " indices"); + + if (vertexCount < 3) { + Ogre::LogManager::getSingleton().logMessage("CellGrid: Not enough vertices for plaza mesh"); + return; + } + + // Apply UV mapping if texture rect is specified + if (!district.textureRectName.empty()) { + // Find the texture entity from the material + flecs::entity textureEntity = flecs::entity::null(); + if (district.proceduralMaterialEntity.is_alive() && + district.proceduralMaterialEntity.has()) { + const auto& mat = district.proceduralMaterialEntity.get(); + // Search for the texture that is referenced by this material + if (mat.diffuseTextureEntity.is_alive()) { + textureEntity = mat.diffuseTextureEntity; + } + } + + if (textureEntity.is_alive() && textureEntity.has()) { + const auto& texture = textureEntity.get(); + const TextureRectInfo* rect = texture.getNamedRect(district.textureRectName); + if (rect) { + // Apply UV mapping with 0.01 margin + const float margin = 0.01f; + float uRange = (rect->u2 - rect->u1) * (1.0f - 2.0f * margin); + float vRange = (rect->v2 - rect->v1) * (1.0f - 2.0f * margin); + float uOffset = rect->u1 + (rect->u2 - rect->u1) * margin; + float vOffset = rect->v1 + (rect->v2 - rect->v1) * margin; + + auto& vertices = tb.getVertices(); + for (auto& vertex : vertices) { + vertex.mUV.x = uOffset + vertex.mUV.x * uRange; + vertex.mUV.y = vOffset + vertex.mUV.y * vRange; + } + + Ogre::LogManager::getSingleton().logMessage("CellGrid: Applied UV mapping for rect '" + + district.textureRectName + "'"); + } + } + } + + // Generate mesh name + std::string meshName = "Plaza_" + std::to_string(entity.id()) + "_" + std::to_string(m_meshCount++); + + // Create mesh and entity + try { + Ogre::MeshPtr mesh = tb.transformToMesh(meshName); + + if (mesh.isNull()) { + Ogre::LogManager::getSingleton().logMessage("CellGrid: transformToMesh returned null"); + return; + } + + Ogre::LogManager::getSingleton().logMessage("CellGrid: Created mesh '" + meshName + + "' with " + std::to_string(mesh->getNumSubMeshes()) + " submeshes"); + + if (!materialName.empty() && mesh->getNumSubMeshes() > 0) { + mesh->getSubMesh(0)->setMaterialName(materialName); + Ogre::LogManager::getSingleton().logMessage("CellGrid: Set material to '" + materialName + "'"); + } + + Ogre::Entity* plazzaEntity = m_sceneMgr->createEntity(meshName); + if (!plazzaEntity) { + Ogre::LogManager::getSingleton().logMessage("CellGrid: createEntity returned null"); + return; + } + + transform.node->attachObject(plazzaEntity); + + // Force visibility + plazzaEntity->setVisible(true); + + // Store for cleanup + PlazaData data; + data.meshName = meshName; + data.entity = plazzaEntity; + m_plazaMeshes[entity.id()] = data; + + Ogre::LogManager::getSingleton().logMessage("CellGrid: Created plaza mesh for district " + std::to_string(entity.id()) + + " attached to node '" + transform.node->getName() + "'"); + + } catch (const Ogre::Exception& e) { + Ogre::LogManager::getSingleton().logMessage("CellGrid: Failed to create plaza mesh: " + e.getDescription()); + } +} diff --git a/src/features/editScene/systems/CellGridSystem.hpp b/src/features/editScene/systems/CellGridSystem.hpp new file mode 100644 index 0000000..524f66e --- /dev/null +++ b/src/features/editScene/systems/CellGridSystem.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include + +namespace Procedural { + class TriangleBuffer; +} + +class CellGridSystem { +public: + CellGridSystem(flecs::world& world, Ogre::SceneManager* sceneMgr); + ~CellGridSystem(); + + void initialize(); + void update(); + + // Force rebuild of a specific cell grid + void rebuildCellGrid(flecs::entity entity); + + // Create/Update town material + void updateTownMaterial(flecs::entity townEntity); + +private: + flecs::world& m_world; + Ogre::SceneManager* m_sceneMgr; + + flecs::query m_cellGridQuery; + flecs::query m_townQuery; + flecs::query m_districtQuery; + + bool m_initialized = false; + int m_meshCount = 0; + + // Build geometry from cell grid + void buildCellGrid(flecs::entity entity, struct CellGridComponent& grid); + + // Create triangle buffers for different parts + void buildFloorsAndCeilings(const struct CellGridComponent& grid, Procedural::TriangleBuffer& floorTb, Procedural::TriangleBuffer& ceilingTb); + void buildWalls(const struct CellGridComponent& grid, + Procedural::TriangleBuffer& extWallTb, + Procedural::TriangleBuffer& intWallTb, + Procedural::TriangleBuffer& intWindowsTb); + void buildDoors(const struct CellGridComponent& grid, std::vector& doorTbs); + void buildWindows(const struct CellGridComponent& grid, std::vector& windowTbs); + void buildCorners(const struct CellGridComponent& grid, Procedural::TriangleBuffer& extWallTb); + + // Build roofs + void buildRoofs(flecs::entity lotEntity, const struct CellGridComponent& grid, Procedural::TriangleBuffer& roofTb); + + // Build plaza for district + void buildDistrictPlaza(flecs::entity entity, struct DistrictComponent& district); + + // Convert triangle buffer to mesh + Ogre::MeshPtr convertToMesh(const std::string& name, Procedural::TriangleBuffer& tb, const std::string& materialName); + + // Apply UV mapping from color rects + void applyUVMapping(Procedural::TriangleBuffer& tb, const struct TownComponent& town, const std::string& rectName); + + // Destroy existing mesh + void destroyCellGridMeshes(struct CellGridComponent& grid); + + // Mesh storage per entity + struct MeshData { + std::string floorMesh; + std::string ceilingMesh; + std::string extWallMesh; + std::string intWallMesh; + std::string intWindowsMesh; + std::vector doorMeshes; + std::vector windowMeshes; + std::string roofMesh; + std::vector entities; + }; + std::unordered_map m_entityMeshes; + + // Plaza mesh storage per district entity + struct PlazaData { + std::string meshName; + Ogre::Entity* entity = nullptr; + }; + std::unordered_map m_plazaMeshes; +}; diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 08b29ea..e98a242 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -15,6 +15,7 @@ #include "../components/Primitive.hpp" #include "../components/TriangleBuffer.hpp" #include "../components/LodSettings.hpp" +#include "../components/CellGrid.hpp" #include "../ui/TransformEditor.hpp" #include "../ui/RenderableEditor.hpp" #include "../ui/PhysicsColliderEditor.hpp" @@ -594,6 +595,55 @@ void EditorUISystem::renderComponentList(flecs::entity entity) m_componentRegistry.render(entity, tb); componentCount++; } + + // Render CellGrid if present + if (entity.has()) { + auto &grid = entity.get_mut(); + m_componentRegistry.render(entity, grid); + componentCount++; + } + + // Render Lot if present + if (entity.has()) { + auto &lot = entity.get_mut(); + m_componentRegistry.render(entity, lot); + componentCount++; + } + + // Render District if present + if (entity.has()) { + auto &district = entity.get_mut(); + m_componentRegistry.render(entity, district); + componentCount++; + } + + // Render Town if present + if (entity.has()) { + auto &town = entity.get_mut(); + m_componentRegistry.render(entity, town); + componentCount++; + } + + // Render Roof if present + if (entity.has()) { + auto &roof = entity.get_mut(); + m_componentRegistry.render(entity, roof); + componentCount++; + } + + // Render Room if present + if (entity.has()) { + auto &room = entity.get_mut(); + m_componentRegistry.render(entity, room); + componentCount++; + } + + // Render FurnitureTemplate if present + if (entity.has()) { + auto &furniture = entity.get_mut(); + m_componentRegistry.render(entity, furniture); + componentCount++; + } // Show message if no components if (componentCount == 0) { diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 6a91c04..fdfb512 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -15,6 +15,7 @@ #include "../components/ProceduralMaterial.hpp" #include "../components/Primitive.hpp" #include "../components/TriangleBuffer.hpp" +#include "../components/CellGrid.hpp" #include "EditorUISystem.hpp" #include #include @@ -175,6 +176,29 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) if (entity.has()) { json["triangleBuffer"] = serializeTriangleBuffer(entity); } + + // CellGrid/Town components + if (entity.has()) { + json["cellGrid"] = serializeCellGrid(entity); + } + if (entity.has()) { + json["town"] = serializeTown(entity); + } + if (entity.has()) { + json["district"] = serializeDistrict(entity); + } + if (entity.has()) { + json["lot"] = serializeLot(entity); + } + if (entity.has()) { + json["room"] = serializeRoom(entity); + } + if (entity.has()) { + json["roof"] = serializeRoof(entity); + } + if (entity.has()) { + json["furnitureTemplate"] = serializeFurnitureTemplate(entity); + } // Serialize children json["children"] = nlohmann::json::array(); @@ -268,6 +292,29 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit if (json.contains("triangleBuffer")) { deserializeTriangleBuffer(entity, json["triangleBuffer"]); } + + // CellGrid/Town components + if (json.contains("cellGrid")) { + deserializeCellGrid(entity, json["cellGrid"]); + } + if (json.contains("town")) { + deserializeTown(entity, json["town"]); + } + if (json.contains("district")) { + deserializeDistrict(entity, json["district"]); + } + if (json.contains("lot")) { + deserializeLot(entity, json["lot"]); + } + if (json.contains("room")) { + deserializeRoom(entity, json["room"]); + } + if (json.contains("roof")) { + deserializeRoof(entity, json["roof"]); + } + if (json.contains("furnitureTemplate")) { + deserializeFurnitureTemplate(entity, json["furnitureTemplate"]); + } // Add to UI system if provided if (uiSystem) { @@ -1204,3 +1251,383 @@ void SceneSerializer::deserializeTriangleBuffer(flecs::entity entity, const nloh entity.set(tb); } + + +// ============================================================================ +// CellGrid/Town Component Serialization +// ============================================================================ + +nlohmann::json SceneSerializer::serializeCellGrid(flecs::entity entity) +{ + auto& grid = entity.get(); + nlohmann::json json; + + json["width"] = grid.width; + json["height"] = grid.height; + json["depth"] = grid.depth; + json["cellSize"] = grid.cellSize; + json["cellHeight"] = grid.cellHeight; + json["generationScript"] = grid.generationScript; + + // Serialize cells + nlohmann::json cellsJson = nlohmann::json::array(); + for (const auto& cell : grid.cells) { + cellsJson.push_back({ + {"x", cell.x}, + {"y", cell.y}, + {"z", cell.z}, + {"flags", cell.flags} + }); + } + json["cells"] = cellsJson; + + // Serialize furniture cells + nlohmann::json furnitureJson = nlohmann::json::array(); + for (const auto& furn : grid.furnitureCells) { + nlohmann::json furnJson; + furnJson["x"] = furn.x; + furnJson["y"] = furn.y; + furnJson["z"] = furn.z; + furnJson["furnitureType"] = furn.furnitureType; + furnJson["rotation"] = furn.rotation; + furnJson["tags"] = furn.tags; + furnitureJson.push_back(furnJson); + } + json["furnitureCells"] = furnitureJson; + + return json; +} + +nlohmann::json SceneSerializer::serializeTown(flecs::entity entity) +{ + auto& town = entity.get(); + nlohmann::json json; + + json["townName"] = town.townName; + json["materialName"] = town.materialName; + + // Serialize color rects + nlohmann::json colorRectsJson = nlohmann::json::object(); + for (const auto& pair : town.colorRects) { + colorRectsJson[pair.first] = { + {"left", pair.second.left}, + {"top", pair.second.top}, + {"right", pair.second.right}, + {"bottom", pair.second.bottom}, + {"color", { + {"r", pair.second.color.r}, + {"g", pair.second.color.g}, + {"b", pair.second.color.b}, + {"a", pair.second.color.a} + }} + }; + } + json["colorRects"] = colorRectsJson; + + return json; +} + +nlohmann::json SceneSerializer::serializeDistrict(flecs::entity entity) +{ + auto& district = entity.get(); + nlohmann::json json; + + json["radius"] = district.radius; + json["elevation"] = district.elevation; + json["height"] = district.height; + json["isPlaza"] = district.isPlaza; + json["lotTemplateNames"] = district.lotTemplateNames; + json["proceduralMaterialEntityId"] = district.proceduralMaterialEntityId; + json["textureRectName"] = district.textureRectName; + + return json; +} + +nlohmann::json SceneSerializer::serializeLot(flecs::entity entity) +{ + auto& lot = entity.get(); + nlohmann::json json; + + json["width"] = lot.width; + json["depth"] = lot.depth; + json["elevation"] = lot.elevation; + json["angle"] = lot.angle; + json["offsetX"] = lot.offsetX; + json["offsetZ"] = lot.offsetZ; + json["templateName"] = lot.templateName; + + return json; +} + +nlohmann::json SceneSerializer::serializeRoom(flecs::entity entity) +{ + auto& room = entity.get(); + nlohmann::json json; + + json["minX"] = room.minX; + json["minY"] = room.minY; + json["minZ"] = room.minZ; + json["maxX"] = room.maxX; + json["maxY"] = room.maxY; + json["maxZ"] = room.maxZ; + json["roomType"] = room.roomType; + json["tags"] = room.tags; + json["connectedRooms"] = room.connectedRooms; + json["exits"] = room.exits; + + return json; +} + +nlohmann::json SceneSerializer::serializeRoof(flecs::entity entity) +{ + auto& roof = entity.get(); + nlohmann::json json; + + json["type"] = static_cast(roof.type); + json["posX"] = roof.posX; + json["posY"] = roof.posY; + json["posZ"] = roof.posZ; + json["sizeX"] = roof.sizeX; + json["sizeZ"] = roof.sizeZ; + json["offsetX"] = roof.offsetX; + json["offsetY"] = roof.offsetY; + json["offsetZ"] = roof.offsetZ; + json["baseHeight"] = roof.baseHeight; + json["maxHeight"] = roof.maxHeight; + + return json; +} + +nlohmann::json SceneSerializer::serializeFurnitureTemplate(flecs::entity entity) +{ + auto& furn = entity.get(); + nlohmann::json json; + + json["templateName"] = furn.templateName; + json["tags"] = furn.tags; + json["meshName"] = furn.meshName; + json["materialName"] = furn.materialName; + json["requiresWall"] = furn.requiresWall; + json["requiresFloor"] = furn.requiresFloor; + json["blocksPath"] = furn.blocksPath; + json["offsetX"] = furn.offsetX; + json["offsetY"] = furn.offsetY; + json["offsetZ"] = furn.offsetZ; + json["weight"] = furn.weight; + + return json; +} + +void SceneSerializer::deserializeCellGrid(flecs::entity entity, const nlohmann::json& json) +{ + CellGridComponent grid; + + grid.width = json.value("width", 10); + grid.height = json.value("height", 1); + grid.depth = json.value("depth", 10); + grid.cellSize = json.value("cellSize", 4.0f); + grid.cellHeight = json.value("cellHeight", 4.0f); + grid.generationScript = json.value("generationScript", ""); + + // Deserialize cells + if (json.contains("cells") && json["cells"].is_array()) { + for (const auto& cellJson : json["cells"]) { + Cell cell; + cell.x = cellJson.value("x", 0); + cell.y = cellJson.value("y", 0); + cell.z = cellJson.value("z", 0); + cell.flags = cellJson.value("flags", 0ULL); + grid.cells.push_back(cell); + } + } + + // Deserialize furniture cells + if (json.contains("furnitureCells") && json["furnitureCells"].is_array()) { + for (const auto& furnJson : json["furnitureCells"]) { + FurnitureCell furn; + furn.x = furnJson.value("x", 0); + furn.y = furnJson.value("y", 0); + furn.z = furnJson.value("z", 0); + furn.furnitureType = furnJson.value("furnitureType", ""); + furn.rotation = furnJson.value("rotation", 0); + if (furnJson.contains("tags") && furnJson["tags"].is_array()) { + for (const auto& tag : furnJson["tags"]) { + if (tag.is_string()) { + furn.tags.push_back(tag); + } + } + } + grid.furnitureCells.push_back(furn); + } + } + + grid.dirty = true; + entity.set(grid); +} + +void SceneSerializer::deserializeTown(flecs::entity entity, const nlohmann::json& json) +{ + TownComponent town; + + town.townName = json.value("townName", "New Town"); + town.materialName = json.value("materialName", ""); + + // Deserialize color rects + if (json.contains("colorRects") && json["colorRects"].is_object()) { + for (auto& [name, rectJson] : json["colorRects"].items()) { + TownComponent::ColorRect rect; + rect.left = rectJson.value("left", 0.0f); + rect.top = rectJson.value("top", 0.0f); + rect.right = rectJson.value("right", 1.0f); + rect.bottom = rectJson.value("bottom", 1.0f); + if (rectJson.contains("color")) { + auto& c = rectJson["color"]; + rect.color.r = c.value("r", 1.0f); + rect.color.g = c.value("g", 1.0f); + rect.color.b = c.value("b", 1.0f); + rect.color.a = c.value("a", 1.0f); + } + town.colorRects[name] = rect; + } + } + + town.dirty = true; + town.materialDirty = true; + entity.set(town); +} + +void SceneSerializer::deserializeDistrict(flecs::entity entity, const nlohmann::json& json) +{ + DistrictComponent district; + + district.radius = json.value("radius", 50.0f); + district.elevation = json.value("elevation", 0.0f); + district.height = json.value("height", 0.2f); + district.isPlaza = json.value("isPlaza", false); + district.proceduralMaterialEntityId = json.value("proceduralMaterialEntityId", ""); + district.textureRectName = json.value("textureRectName", ""); + + // Resolve material entity reference + if (!district.proceduralMaterialEntityId.empty()) { + try { + uint64_t matId = std::stoull(district.proceduralMaterialEntityId); + flecs::entity matEntity(m_world, matId); + if (matEntity.is_alive() && matEntity.has()) { + district.proceduralMaterialEntity = matEntity; + } + } catch (...) { + // Invalid ID format, ignore + } + } + + if (json.contains("lotTemplateNames") && json["lotTemplateNames"].is_array()) { + for (const auto& name : json["lotTemplateNames"]) { + if (name.is_string()) { + district.lotTemplateNames.push_back(name); + } + } + } + + district.dirty = true; + entity.set(district); +} + +void SceneSerializer::deserializeLot(flecs::entity entity, const nlohmann::json& json) +{ + LotComponent lot; + + lot.width = json.value("width", 10); + lot.depth = json.value("depth", 10); + lot.elevation = json.value("elevation", 0.0f); + lot.angle = json.value("angle", 0.0f); + lot.offsetX = json.value("offsetX", 0.0f); + lot.offsetZ = json.value("offsetZ", 0.0f); + lot.templateName = json.value("templateName", ""); + + lot.dirty = true; + entity.set(lot); +} + +void SceneSerializer::deserializeRoom(flecs::entity entity, const nlohmann::json& json) +{ + RoomComponent room; + + room.minX = json.value("minX", 0); + room.minY = json.value("minY", 0); + room.minZ = json.value("minZ", 0); + room.maxX = json.value("maxX", 0); + room.maxY = json.value("maxY", 0); + room.maxZ = json.value("maxZ", 0); + room.roomType = json.value("roomType", ""); + + if (json.contains("tags") && json["tags"].is_array()) { + for (const auto& tag : json["tags"]) { + if (tag.is_string()) { + room.tags.push_back(tag); + } + } + } + + if (json.contains("connectedRooms") && json["connectedRooms"].is_array()) { + for (const auto& idx : json["connectedRooms"]) { + if (idx.is_number()) { + room.connectedRooms.push_back(idx); + } + } + } + + if (json.contains("exits") && json["exits"].is_array()) { + for (const auto& exit : json["exits"]) { + if (exit.is_number()) { + room.exits.push_back(exit); + } + } + } + + entity.set(room); +} + +void SceneSerializer::deserializeRoof(flecs::entity entity, const nlohmann::json& json) +{ + RoofComponent roof; + + roof.type = static_cast(json.value("type", 0)); + roof.posX = json.value("posX", 0); + roof.posY = json.value("posY", 0); + roof.posZ = json.value("posZ", 0); + roof.sizeX = json.value("sizeX", 1); + roof.sizeZ = json.value("sizeZ", 1); + roof.offsetX = json.value("offsetX", 0.0f); + roof.offsetY = json.value("offsetY", 0.0f); + roof.offsetZ = json.value("offsetZ", 0.0f); + roof.baseHeight = json.value("baseHeight", 0.5f); + roof.maxHeight = json.value("maxHeight", 0.5f); + + entity.set(roof); +} + +void SceneSerializer::deserializeFurnitureTemplate(flecs::entity entity, const nlohmann::json& json) +{ + FurnitureTemplateComponent furn; + + furn.templateName = json.value("templateName", ""); + furn.meshName = json.value("meshName", ""); + furn.materialName = json.value("materialName", ""); + furn.requiresWall = json.value("requiresWall", false); + furn.requiresFloor = json.value("requiresFloor", true); + furn.blocksPath = json.value("blocksPath", true); + furn.offsetX = json.value("offsetX", 0.0f); + furn.offsetY = json.value("offsetY", 0.0f); + furn.offsetZ = json.value("offsetZ", 0.0f); + furn.weight = json.value("weight", 1.0f); + + if (json.contains("tags") && json["tags"].is_array()) { + for (const auto& tag : json["tags"]) { + if (tag.is_string()) { + furn.tags.push_back(tag); + } + } + } + + entity.set(furn); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 3fa821f..b9314b6 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -55,6 +55,15 @@ private: nlohmann::json serializePrimitive(flecs::entity entity); nlohmann::json serializeTriangleBuffer(flecs::entity entity); + // CellGrid/Town component serialization + nlohmann::json serializeCellGrid(flecs::entity entity); + nlohmann::json serializeTown(flecs::entity entity); + nlohmann::json serializeDistrict(flecs::entity entity); + nlohmann::json serializeLot(flecs::entity entity); + nlohmann::json serializeRoom(flecs::entity entity); + nlohmann::json serializeRoof(flecs::entity entity); + nlohmann::json serializeFurnitureTemplate(flecs::entity entity); + // Component deserialization void deserializeTransform(flecs::entity entity, const nlohmann::json& json, flecs::entity parentEntity); void deserializeRenderable(flecs::entity entity, const nlohmann::json& json); @@ -71,6 +80,15 @@ private: void deserializeProceduralMaterial(flecs::entity entity, const nlohmann::json& json); void deserializePrimitive(flecs::entity entity, const nlohmann::json& json); void deserializeTriangleBuffer(flecs::entity entity, const nlohmann::json& json); + + // CellGrid/Town component deserialization + void deserializeCellGrid(flecs::entity entity, const nlohmann::json& json); + void deserializeTown(flecs::entity entity, const nlohmann::json& json); + void deserializeDistrict(flecs::entity entity, const nlohmann::json& json); + void deserializeLot(flecs::entity entity, const nlohmann::json& json); + void deserializeRoom(flecs::entity entity, const nlohmann::json& json); + void deserializeRoof(flecs::entity entity, const nlohmann::json& json); + void deserializeFurnitureTemplate(flecs::entity entity, const nlohmann::json& json); flecs::world& m_world; Ogre::SceneManager* m_sceneMgr; diff --git a/src/features/editScene/ui/CellGridEditor.cpp b/src/features/editScene/ui/CellGridEditor.cpp new file mode 100644 index 0000000..b02ae98 --- /dev/null +++ b/src/features/editScene/ui/CellGridEditor.cpp @@ -0,0 +1,225 @@ +#include "CellGridEditor.hpp" +#include "../components/CellGrid.hpp" +#include + +bool CellGridEditor::renderComponent(flecs::entity entity, CellGridComponent& grid) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Cell Grid", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Grid dimensions + int dims[3] = { grid.width, grid.height, grid.depth }; + if (ImGui::InputInt3("Grid Size (W/H/D)", dims)) { + grid.width = std::max(1, dims[0]); + grid.height = std::max(1, dims[1]); + grid.depth = std::max(1, dims[2]); + modified = true; + } + + // Cell size + if (ImGui::DragFloat("Cell Size", &grid.cellSize, 0.1f, 0.5f, 20.0f)) { + modified = true; + } + if (ImGui::DragFloat("Cell Height", &grid.cellHeight, 0.1f, 0.5f, 20.0f)) { + modified = true; + } + + ImGui::Separator(); + + // Statistics + ImGui::Text("Cells: %zu", grid.cells.size()); + ImGui::Text("Furniture: %zu", grid.furnitureCells.size()); + + // Cell editor + if (ImGui::CollapsingHeader("Cells")) { + renderCellEditor(grid); + } + + // Furniture editor + if (ImGui::CollapsingHeader("Furniture")) { + renderFurnitureEditor(grid); + } + + // Script editor + if (ImGui::CollapsingHeader("Generation Script")) { + renderScriptEditor(grid); + } + + ImGui::Separator(); + + // Actions + if (ImGui::Button("Clear All Cells")) { + grid.clear(); + modified = true; + } + ImGui::SameLine(); + if (ImGui::Button("Rebuild Geometry")) { + grid.markDirty(); + modified = true; + } + + if (modified) { + grid.markDirty(); + } + + ImGui::Unindent(); + } + + return modified; +} + +void CellGridEditor::renderCellEditor(CellGridComponent& grid) +{ + // New cell input + ImGui::InputInt3("New Cell Pos", &newCellX); + + if (ImGui::Button("Add/Edit Cell")) { + auto& cell = grid.getOrCreateCell(newCellX, newCellY, newCellZ); + selectedCellX = newCellX; + selectedCellY = newCellY; + selectedCellZ = newCellZ; + } + + ImGui::Separator(); + + // Selected cell editor + Cell* cell = grid.findCell(selectedCellX, selectedCellY, selectedCellZ); + if (cell) { + ImGui::Text("Editing Cell: %d, %d, %d", selectedCellX, selectedCellY, selectedCellZ); + + auto flags = CellGridComponent::getAllFlags(); + for (const auto& [flag, name] : flags) { + bool hasFlag = cell->hasFlag(flag); + if (ImGui::Checkbox(name, &hasFlag)) { + if (hasFlag) { + cell->setFlag(flag); + } else { + cell->clearFlag(flag); + } + grid.markDirty(); + } + } + + if (ImGui::Button("Delete Cell")) { + grid.removeCell(selectedCellX, selectedCellY, selectedCellZ); + grid.markDirty(); + } + } else { + ImGui::Text("No cell selected"); + } + + ImGui::Separator(); + + // List existing cells + if (ImGui::BeginListBox("Cells", ImVec2(-FLT_MIN, 150))) { + for (const auto& cell : grid.cells) { + std::string label = std::to_string(cell.x) + ", " + + std::to_string(cell.y) + ", " + + std::to_string(cell.z) + " (flags: " + + std::to_string(cell.flags) + ")"; + bool isSelected = (cell.x == selectedCellX && + cell.y == selectedCellY && + cell.z == selectedCellZ); + if (ImGui::Selectable(label.c_str(), isSelected)) { + selectedCellX = cell.x; + selectedCellY = cell.y; + selectedCellZ = cell.z; + } + } + ImGui::EndListBox(); + } +} + +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 rotation = 0; + + ImGui::InputInt3("Furniture Pos", &fx); + ImGui::InputText("Tags (comma separated)", tagsBuffer, sizeof(tagsBuffer)); + ImGui::InputText("Furniture Type", furnitureType, sizeof(furnitureType)); + ImGui::SliderInt("Rotation", &rotation, 0, 3); + + if (ImGui::Button("Add/Update Furniture")) { + auto& fcell = grid.getOrCreateFurnitureCell(fx, fy, fz); + fcell.furnitureType = furnitureType; + fcell.rotation = rotation; + fcell.tags.clear(); + // Parse tags + std::string tagsStr(tagsBuffer); + size_t start = 0; + 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()) { + fcell.tags.push_back(tag); + } + 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); + if (!lastTag.empty()) { + fcell.tags.push_back(lastTag); + } + grid.markDirty(); + } + + ImGui::Separator(); + + // List furniture cells + if (ImGui::BeginListBox("Furniture Cells", ImVec2(-FLT_MIN, 100))) { + for (const auto& fcell : grid.furnitureCells) { + std::string label = std::to_string(fcell.x) + ", " + + std::to_string(fcell.y) + ", " + + std::to_string(fcell.z) + ": " + fcell.furnitureType; + if (ImGui::Selectable(label.c_str())) { + fx = fcell.x; + fy = fcell.y; + fz = fcell.z; + rotation = fcell.rotation; + strncpy(furnitureType, fcell.furnitureType.c_str(), sizeof(furnitureType) - 1); + // Build tags string + std::string tags; + for (size_t i = 0; i < fcell.tags.size(); ++i) { + if (i > 0) tags += ", "; + tags += fcell.tags[i]; + } + strncpy(tagsBuffer, tags.c_str(), sizeof(tagsBuffer) - 1); + } + } + ImGui::EndListBox(); + } +} + +void CellGridEditor::renderScriptEditor(CellGridComponent& grid) +{ + // Copy script to buffer if needed + if (scriptBuffer[0] == '\0' && !grid.generationScript.empty()) { + strncpy(scriptBuffer, grid.generationScript.c_str(), sizeof(scriptBuffer) - 1); + } + + ImGui::InputTextMultiline("Script", scriptBuffer, sizeof(scriptBuffer), + ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 20), + ImGuiInputTextFlags_AllowTabInput); + + if (ImGui::IsItemDeactivatedAfterEdit()) { + grid.generationScript = scriptBuffer; + grid.markDirty(); + } + + if (ImGui::Button("Execute Script")) { + grid.generationScript = scriptBuffer; + // TODO: Execute Lua script + grid.markDirty(); + } +} diff --git a/src/features/editScene/ui/CellGridEditor.hpp b/src/features/editScene/ui/CellGridEditor.hpp new file mode 100644 index 0000000..872170d --- /dev/null +++ b/src/features/editScene/ui/CellGridEditor.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "ComponentEditor.hpp" + +struct CellGridComponent; + +class CellGridEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, CellGridComponent& grid) override; + const char* getName() const override { return "Cell Grid"; } + +private: + void renderCellEditor(CellGridComponent& grid); + void renderFurnitureEditor(CellGridComponent& grid); + void renderScriptEditor(CellGridComponent& grid); + + // State for UI + int selectedCellX = 0, selectedCellY = 0, selectedCellZ = 0; + int newCellX = 0, newCellY = 0, newCellZ = 0; + char scriptBuffer[16384] = {0}; + bool showGrid = false; +}; diff --git a/src/features/editScene/ui/DistrictEditor.cpp b/src/features/editScene/ui/DistrictEditor.cpp new file mode 100644 index 0000000..70694b6 --- /dev/null +++ b/src/features/editScene/ui/DistrictEditor.cpp @@ -0,0 +1,153 @@ +#include "DistrictEditor.hpp" +#include "../components/CellGrid.hpp" +#include "../components/ProceduralTexture.hpp" +#include "../components/ProceduralMaterial.hpp" +#include + +bool DistrictEditor::renderComponent(flecs::entity entity, DistrictComponent& district) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("District", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + if (ImGui::DragFloat("Radius", &district.radius, 1.0f, 10.0f, 500.0f)) { + modified = true; + } + if (ImGui::DragFloat("Elevation", &district.elevation, 0.1f, -10.0f, 10.0f)) { + modified = true; + } + if (ImGui::DragFloat("Height", &district.height, 0.05f, 0.0f, 10.0f)) { + modified = true; + } + if (ImGui::Checkbox("Is Plaza", &district.isPlaza)) { + modified = true; + Ogre::LogManager::getSingleton().logMessage( + "DistrictEditor: Is Plaza changed to " + std::string(district.isPlaza ? "true" : "false") + + " for entity " + std::to_string(entity.id())); + } + + // Plaza material and texture settings + if (district.isPlaza) { + ImGui::Separator(); + ImGui::Text("Plaza Material:"); + + // Material selector + flecs::world world = entity.world(); + std::vector materialEntities; + std::vector materialNames; + + int currentMatIndex = -1; + int noneMatIndex = 0; + + materialEntities.push_back(flecs::entity::null()); + materialNames.push_back("None"); + + world.query().each([&](flecs::entity e, ProceduralMaterialComponent& mat) { + materialEntities.push_back(e); + std::string name = mat.materialName.empty() ? "Material " + std::to_string(e.id()) : mat.materialName; + materialNames.push_back(name); + + if (district.proceduralMaterialEntity == e) { + currentMatIndex = (int)materialEntities.size() - 1; + } + }); + + if (currentMatIndex == -1) currentMatIndex = noneMatIndex; + + std::string matComboItems; + for (size_t i = 0; i < materialNames.size(); ++i) { + if (i > 0) matComboItems += '\0'; + matComboItems += materialNames[i]; + } + matComboItems += '\0'; + + int newMatIndex = currentMatIndex; + if (ImGui::Combo("Material", &newMatIndex, matComboItems.c_str())) { + if (newMatIndex == noneMatIndex) { + district.proceduralMaterialEntity = flecs::entity::null(); + district.proceduralMaterialEntityId.clear(); + } else { + district.proceduralMaterialEntity = materialEntities[newMatIndex]; + district.proceduralMaterialEntityId = std::to_string(materialEntities[newMatIndex].id()); + } + modified = true; + } + + // Texture rectangle selector + if (district.proceduralMaterialEntity.is_alive()) { + // Find the texture entity from the material + flecs::entity textureEntity = flecs::entity::null(); + if (district.proceduralMaterialEntity.has()) { + const auto& mat = district.proceduralMaterialEntity.get(); + // Use the material's diffuse texture entity directly + if (mat.diffuseTextureEntity.is_alive()) { + textureEntity = mat.diffuseTextureEntity; + } + } + + if (textureEntity.is_alive() && textureEntity.has()) { + const auto& texture = textureEntity.get(); + const auto& namedRects = texture.getAllNamedRects(); + + if (!namedRects.empty()) { + std::vector rectNames; + int currentRectIndex = -1; + int noneRectIndex = 0; + + rectNames.push_back("None (use full texture)"); + + for (const auto& pair : namedRects) { + rectNames.push_back(pair.first); + if (district.textureRectName == pair.first) { + currentRectIndex = (int)rectNames.size() - 1; + } + } + + if (currentRectIndex == -1) currentRectIndex = noneRectIndex; + + std::string rectComboItems; + for (size_t i = 0; i < rectNames.size(); ++i) { + if (i > 0) rectComboItems += '\0'; + rectComboItems += rectNames[i]; + } + rectComboItems += '\0'; + + int newRectIndex = currentRectIndex; + if (ImGui::Combo("Texture Rectangle", &newRectIndex, rectComboItems.c_str())) { + if (newRectIndex == noneRectIndex) { + district.textureRectName.clear(); + } else { + district.textureRectName = rectNames[newRectIndex]; + } + modified = true; + } + } else { + ImGui::TextDisabled("No named rectangles in texture"); + } + } else { + ImGui::TextDisabled("Material has no associated ProceduralTexture"); + } + } + } + + // Count lots + int lotCount = 0; + entity.children([&](flecs::entity child) { + if (child.has()) { + lotCount++; + } + }); + ImGui::Text("Lots: %d", lotCount); + + if (modified) { + district.markDirty(); + Ogre::LogManager::getSingleton().logMessage( + "DistrictEditor: markDirty() called for entity " + std::to_string(entity.id())); + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/DistrictEditor.hpp b/src/features/editScene/ui/DistrictEditor.hpp new file mode 100644 index 0000000..313107e --- /dev/null +++ b/src/features/editScene/ui/DistrictEditor.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComponentEditor.hpp" + +struct DistrictComponent; + +class DistrictEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, DistrictComponent& district) override; + const char* getName() const override { return "District"; } +}; diff --git a/src/features/editScene/ui/FurnitureTemplateEditor.cpp b/src/features/editScene/ui/FurnitureTemplateEditor.cpp new file mode 100644 index 0000000..ff48fee --- /dev/null +++ b/src/features/editScene/ui/FurnitureTemplateEditor.cpp @@ -0,0 +1,81 @@ +#include "FurnitureTemplateEditor.hpp" +#include "../components/CellGrid.hpp" +#include + +bool FurnitureTemplateEditor::renderComponent(flecs::entity entity, FurnitureTemplateComponent& furniture) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Furniture Template", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + char nameBuf[128] = {0}; + strncpy(nameBuf, furniture.templateName.c_str(), sizeof(nameBuf) - 1); + if (ImGui::InputText("Template Name", nameBuf, sizeof(nameBuf))) { + furniture.templateName = nameBuf; + modified = true; + } + + char meshBuf[128] = {0}; + strncpy(meshBuf, furniture.meshName.c_str(), sizeof(meshBuf) - 1); + if (ImGui::InputText("Mesh", meshBuf, sizeof(meshBuf))) { + furniture.meshName = meshBuf; + modified = true; + } + + char matBuf[128] = {0}; + strncpy(matBuf, furniture.materialName.c_str(), sizeof(matBuf) - 1); + if (ImGui::InputText("Material", matBuf, sizeof(matBuf))) { + furniture.materialName = matBuf; + modified = true; + } + + // Tags + std::string tagsStr; + for (size_t i = 0; i < furniture.tags.size(); ++i) { + if (i > 0) tagsStr += ", "; + tagsStr += furniture.tags[i]; + } + char tagsBuf[256] = {0}; + strncpy(tagsBuf, tagsStr.c_str(), sizeof(tagsBuf) - 1); + if (ImGui::InputText("Tags", tagsBuf, sizeof(tagsBuf))) { + furniture.tags.clear(); + std::string newTags(tagsBuf); + size_t start = 0; + size_t end = newTags.find(','); + while (end != std::string::npos) { + std::string tag = newTags.substr(start, end - start); + tag.erase(0, tag.find_first_not_of(" \t")); + tag.erase(tag.find_last_not_of(" \t") + 1); + if (!tag.empty()) furniture.tags.push_back(tag); + start = end + 1; + end = newTags.find(',', start); + } + std::string lastTag = newTags.substr(start); + lastTag.erase(0, lastTag.find_first_not_of(" \t")); + lastTag.erase(lastTag.find_last_not_of(" \t") + 1); + if (!lastTag.empty()) furniture.tags.push_back(lastTag); + modified = true; + } + + ImGui::Checkbox("Requires Wall", &furniture.requiresWall); + ImGui::Checkbox("Requires Floor", &furniture.requiresFloor); + ImGui::Checkbox("Blocks Path", &furniture.blocksPath); + + float offset[3] = { furniture.offsetX, furniture.offsetY, furniture.offsetZ }; + if (ImGui::DragFloat3("Offset", offset, 0.01f)) { + furniture.offsetX = offset[0]; + furniture.offsetY = offset[1]; + furniture.offsetZ = offset[2]; + modified = true; + } + + if (ImGui::DragFloat("Weight", &furniture.weight, 0.1f, 0.0f, 100.0f)) { + modified = true; + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/FurnitureTemplateEditor.hpp b/src/features/editScene/ui/FurnitureTemplateEditor.hpp new file mode 100644 index 0000000..a0b9f44 --- /dev/null +++ b/src/features/editScene/ui/FurnitureTemplateEditor.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComponentEditor.hpp" + +struct FurnitureTemplateComponent; + +class FurnitureTemplateEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, FurnitureTemplateComponent& furniture) override; + const char* getName() const override { return "Furniture Template"; } +}; diff --git a/src/features/editScene/ui/LotEditor.cpp b/src/features/editScene/ui/LotEditor.cpp new file mode 100644 index 0000000..dea04b8 --- /dev/null +++ b/src/features/editScene/ui/LotEditor.cpp @@ -0,0 +1,72 @@ +#include "LotEditor.hpp" +#include "../components/CellGrid.hpp" +#include + +bool LotEditor::renderComponent(flecs::entity entity, LotComponent& lot) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Lot", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Dimensions + int dims[2] = { lot.width, lot.depth }; + if (ImGui::InputInt2("Lot Size (W/D)", dims)) { + lot.width = std::max(4, dims[0]); + lot.depth = std::max(4, dims[1]); + modified = true; + } + + // Position/rotation + if (ImGui::SliderFloat("Angle (degrees)", &lot.angle, -180.0f, 180.0f)) { + modified = true; + } + if (ImGui::DragFloat("Elevation", &lot.elevation, 0.1f, -10.0f, 10.0f)) { + modified = true; + } + if (ImGui::DragFloat("Offset X", &lot.offsetX, 0.1f)) { + modified = true; + } + if (ImGui::DragFloat("Offset Z", &lot.offsetZ, 0.1f)) { + modified = true; + } + + // Template + char templateName[128] = {0}; + strncpy(templateName, lot.templateName.c_str(), sizeof(templateName) - 1); + if (ImGui::InputText("Template", templateName, sizeof(templateName))) { + lot.templateName = templateName; + modified = true; + } + + // Statistics + int cellCount = 0; + int roomCount = 0; + int roofCount = 0; + + entity.children([&](flecs::entity child) { + if (child.has()) { + cellCount = child.get().cells.size(); + } + if (child.has()) { + roomCount++; + } + if (child.has()) { + roofCount++; + } + }); + + ImGui::Separator(); + ImGui::Text("Cells: %d", cellCount); + ImGui::Text("Rooms: %d", roomCount); + ImGui::Text("Roofs: %d", roofCount); + + if (modified) { + lot.markDirty(); + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/LotEditor.hpp b/src/features/editScene/ui/LotEditor.hpp new file mode 100644 index 0000000..fa130de --- /dev/null +++ b/src/features/editScene/ui/LotEditor.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComponentEditor.hpp" + +struct LotComponent; + +class LotEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, LotComponent& lot) override; + const char* getName() const override { return "Lot"; } +}; diff --git a/src/features/editScene/ui/RoofEditor.cpp b/src/features/editScene/ui/RoofEditor.cpp new file mode 100644 index 0000000..369bb15 --- /dev/null +++ b/src/features/editScene/ui/RoofEditor.cpp @@ -0,0 +1,53 @@ +#include "RoofEditor.hpp" +#include "../components/CellGrid.hpp" +#include + +bool RoofEditor::renderComponent(flecs::entity entity, RoofComponent& roof) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Roof", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + const char* roofTypes[] = { "Flat", "Normal", "Normal2", "Cone", "Cylinder" }; + int typeIndex = static_cast(roof.type); + if (ImGui::Combo("Type", &typeIndex, roofTypes, IM_ARRAYSIZE(roofTypes))) { + roof.type = static_cast(typeIndex); + modified = true; + } + + int pos[3] = { roof.posX, roof.posY, roof.posZ }; + if (ImGui::InputInt3("Position (cells)", pos)) { + roof.posX = pos[0]; + roof.posY = pos[1]; + roof.posZ = pos[2]; + modified = true; + } + + int size[2] = { roof.sizeX, roof.sizeZ }; + if (ImGui::InputInt2("Size (cells)", size)) { + roof.sizeX = std::max(1, size[0]); + roof.sizeZ = std::max(1, size[1]); + modified = true; + } + + float offset[3] = { roof.offsetX, roof.offsetY, roof.offsetZ }; + if (ImGui::DragFloat3("Offset", offset, 0.1f)) { + roof.offsetX = offset[0]; + roof.offsetY = offset[1]; + roof.offsetZ = offset[2]; + modified = true; + } + + if (ImGui::DragFloat("Base Height", &roof.baseHeight, 0.1f, 0.1f, 100.0f)) { + modified = true; + } + if (ImGui::DragFloat("Max Height", &roof.maxHeight, 0.1f, roof.baseHeight, 100.0f)) { + modified = true; + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/RoofEditor.hpp b/src/features/editScene/ui/RoofEditor.hpp new file mode 100644 index 0000000..05bc86d --- /dev/null +++ b/src/features/editScene/ui/RoofEditor.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComponentEditor.hpp" + +struct RoofComponent; + +class RoofEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, RoofComponent& roof) override; + const char* getName() const override { return "Roof"; } +}; diff --git a/src/features/editScene/ui/RoomEditor.cpp b/src/features/editScene/ui/RoomEditor.cpp new file mode 100644 index 0000000..d9cea84 --- /dev/null +++ b/src/features/editScene/ui/RoomEditor.cpp @@ -0,0 +1,73 @@ +#include "RoomEditor.hpp" +#include "../components/CellGrid.hpp" +#include + +bool RoomEditor::renderComponent(flecs::entity entity, RoomComponent& room) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Room", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + int minPos[3] = { room.minX, room.minY, room.minZ }; + if (ImGui::InputInt3("Min (X/Y/Z)", minPos)) { + room.minX = minPos[0]; + room.minY = minPos[1]; + room.minZ = minPos[2]; + modified = true; + } + + int maxPos[3] = { room.maxX, room.maxY, room.maxZ }; + if (ImGui::InputInt3("Max (X/Y/Z)", maxPos)) { + room.maxX = maxPos[0]; + room.maxY = maxPos[1]; + room.maxZ = maxPos[2]; + modified = true; + } + + char roomType[128] = {0}; + strncpy(roomType, room.roomType.c_str(), sizeof(roomType) - 1); + if (ImGui::InputText("Room Type", roomType, sizeof(roomType))) { + room.roomType = roomType; + modified = true; + } + + // Tags + std::string tagsStr; + for (size_t i = 0; i < room.tags.size(); ++i) { + if (i > 0) tagsStr += ", "; + tagsStr += room.tags[i]; + } + char tagsBuf[256] = {0}; + strncpy(tagsBuf, tagsStr.c_str(), sizeof(tagsBuf) - 1); + if (ImGui::InputText("Tags", tagsBuf, sizeof(tagsBuf))) { + room.tags.clear(); + std::string newTags(tagsBuf); + size_t start = 0; + size_t end = newTags.find(','); + while (end != std::string::npos) { + std::string tag = newTags.substr(start, end - start); + tag.erase(0, tag.find_first_not_of(" \t")); + tag.erase(tag.find_last_not_of(" \t") + 1); + if (!tag.empty()) room.tags.push_back(tag); + start = end + 1; + end = newTags.find(',', start); + } + std::string lastTag = newTags.substr(start); + lastTag.erase(0, lastTag.find_first_not_of(" \t")); + lastTag.erase(lastTag.find_last_not_of(" \t") + 1); + if (!lastTag.empty()) room.tags.push_back(lastTag); + modified = true; + } + + // Connected rooms + ImGui::Text("Connected Rooms: %zu", room.connectedRooms.size()); + for (size_t i = 0; i < room.connectedRooms.size(); ++i) { + ImGui::Text(" Room %d", room.connectedRooms[i]); + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/RoomEditor.hpp b/src/features/editScene/ui/RoomEditor.hpp new file mode 100644 index 0000000..efda3c7 --- /dev/null +++ b/src/features/editScene/ui/RoomEditor.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComponentEditor.hpp" + +struct RoomComponent; + +class RoomEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, RoomComponent& room) override; + const char* getName() const override { return "Room"; } +}; diff --git a/src/features/editScene/ui/TownEditor.cpp b/src/features/editScene/ui/TownEditor.cpp new file mode 100644 index 0000000..d051078 --- /dev/null +++ b/src/features/editScene/ui/TownEditor.cpp @@ -0,0 +1,68 @@ +#include "TownEditor.hpp" +#include "../components/CellGrid.hpp" +#include + +bool TownEditor::renderComponent(flecs::entity entity, TownComponent& town) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Town", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + char nameBuf[128] = {0}; + strncpy(nameBuf, town.townName.c_str(), sizeof(nameBuf) - 1); + if (ImGui::InputText("Town Name", nameBuf, sizeof(nameBuf))) { + town.townName = nameBuf; + modified = true; + } + + // Count districts + int districtCount = 0; + entity.children([&](flecs::entity child) { + if (child.has()) { + districtCount++; + } + }); + ImGui::Text("Districts: %d", districtCount); + + // Color rects + if (ImGui::CollapsingHeader("Color Rects")) { + for (auto& [name, rect] : town.colorRects) { + ImGui::PushID(name.c_str()); + ImGui::Text("%s", name.c_str()); + float l = rect.left, t = rect.top, r = rect.right, b = rect.bottom; + if (ImGui::SliderFloat("Left", &l, 0.0f, 1.0f)) { rect.left = l; modified = true; town.markMaterialDirty(); } + if (ImGui::SliderFloat("Top", &t, 0.0f, 1.0f)) { rect.top = t; modified = true; town.markMaterialDirty(); } + if (ImGui::SliderFloat("Right", &r, 0.0f, 1.0f)) { rect.right = r; modified = true; town.markMaterialDirty(); } + if (ImGui::SliderFloat("Bottom", &b, 0.0f, 1.0f)) { rect.bottom = b; modified = true; town.markMaterialDirty(); } + float color[4] = { rect.color.r, rect.color.g, rect.color.b, rect.color.a }; + if (ImGui::ColorEdit4("Color", color)) { + rect.color.r = color[0]; + rect.color.g = color[1]; + rect.color.b = color[2]; + rect.color.a = color[3]; + modified = true; + town.markMaterialDirty(); + } + ImGui::PopID(); + } + + // Add new rect + static char newName[64] = {0}; + ImGui::InputText("New Rect Name", newName, sizeof(newName)); + if (ImGui::Button("Add Color Rect") && strlen(newName) > 0) { + town.colorRects[newName] = TownComponent::ColorRect(); + modified = true; + town.markMaterialDirty(); + } + } + + if (modified) { + town.markDirty(); + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/TownEditor.hpp b/src/features/editScene/ui/TownEditor.hpp new file mode 100644 index 0000000..0e21042 --- /dev/null +++ b/src/features/editScene/ui/TownEditor.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComponentEditor.hpp" + +struct TownComponent; + +class TownEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, TownComponent& town) override; + const char* getName() const override { return "Town"; } +};