diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md index 9780606..badf8f1 100644 --- a/src/features/editScene/AGENTS.md +++ b/src/features/editScene/AGENTS.md @@ -196,15 +196,23 @@ restored to the spawner name so the character respawns correctly. ## Adding a New Component 1. Define the component struct in `components/MyComponent.hpp`. -2. Register it for UI editing in `components/MyComponentModule.cpp` using - `REGISTER_COMPONENT_GROUP(...)` and `registry.registerComponent(...)`. +2. Create `components/MyComponentModule.cpp` with a + `REGISTER_COMPONENT_GROUP(...)` block that calls + `registry.registerComponent(name, group, editor, adder, remover, + onModified, order)`. The registry builds a type-erased renderer from this, + so the property panel and Add/Remove menus pick it up automatically — no + changes to `EditorUISystem.cpp` are required. 3. Implement an editor in `ui/MyComponentEditor.hpp` / `.cpp` deriving from `ComponentEditor`. 4. If the component should be saved/loaded, add serialization support in - `systems/SceneSerializer.cpp`. + `systems/SceneSerializer.cpp` (see `ComponentArchitectureImprovement.md`, + step C, for the planned registry-driven serializer). 5. Add a Lua binding in `lua/LuaComponentApi.cpp` if needed. 6. Add a component-module test case in `tests/component_lua_test.cpp`. +See `ComponentArchitectureImprovement.md` for the long-term plan to remove the +remaining per-component hardcoding in serialization, lifecycle hooks and Lua. + ## Adding a New System 1. Create `systems/MySystem.hpp` and `systems/MySystem.cpp`. diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 7fe40f0..86e5faa 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -168,6 +168,12 @@ set(EDITSCENE_SOURCES components/TerrainModule.cpp components/SunModule.cpp components/SkyboxModule.cpp + components/TransformModule.cpp + components/RenderableModule.cpp + components/RigidBodyModule.cpp + components/PhysicsColliderModule.cpp + components/PrefabInstanceModule.cpp + components/CharacterIdentityModule.cpp camera/EditorCamera.cpp gizmo/Gizmo.cpp gizmo/Cursor3D.cpp diff --git a/src/features/editScene/ComponentArchitectureImprovement.md b/src/features/editScene/ComponentArchitectureImprovement.md new file mode 100644 index 0000000..64dbebf --- /dev/null +++ b/src/features/editScene/ComponentArchitectureImprovement.md @@ -0,0 +1,256 @@ +# Component Architecture Improvement — Implementation Specification + +This document is the working specification for removing the remaining +per-component hardcoding in `src/features/editScene`. It records what has +already been implemented and, step by step, what remains. + +## Goal + +Adding a new component to the editor should require **one** place to describe +the component (name, group, editor, add/remove, render, serialize, Lua) instead +of editing six or seven files with scattered special-case code. + +## Status + +| Step | Description | Status | +|------|-------------|--------| +| A | Type-erased editor renderer (`ComponentRenderer`) | Done | +| B | Unified registration (extract hardcoded components into `*Module.cpp`) | Done | +| C | Serialization hooks (drive `SceneSerializer` from the registry) | To do | +| D | Lifecycle hooks (hierarchy badges, delete/duplicate cleanup) | To do | +| E | Unified Lua component table (single source of truth) | To do | +| F | Flecs meta reflection for auto-generated editors/serde (optional) | To do | + +--- + +## What is already done (A + B) + +### A — Type-erased renderer + +`ui/ComponentRegistry.hpp` now stores a `ComponentRenderer` closure per +component in `ComponentInfo`: + +```cpp +struct ComponentInfo { + const char *name; + const char *group; + int order = 0; // lower renders first; Transform = -1 + std::unique_ptr editor; + ComponentRenderer renderer; // type-erased render + ComponentAdder adder; + ComponentRemover remover; + ComponentChecker checker; + ComponentModified onModified; // optional post-render side effect +}; +``` + +`registerComponent()` builds `renderer` from the (templated) type, handling +empty/tag components via `std::is_empty_v` so `get_mut()` is never called +on a zero-sized type. + +`EditorUISystem::renderComponentList()` is now a generic loop: it collects the +registered components present on the entity, sorts them by `(order, name)`, and +invokes each `renderer`; if the renderer returns `true` it also runs the +optional `onModified` hook. + +### B — Unified registration + +The six previously hardcoded components were extracted into module files that +use the same `REGISTER_COMPONENT_GROUP` mechanism as every other component: + +- `components/TransformModule.cpp` +- `components/RenderableModule.cpp` +- `components/RigidBodyModule.cpp` +- `components/PhysicsColliderModule.cpp` +- `components/PrefabInstanceModule.cpp` +- `components/CharacterIdentityModule.cpp` + +`EditorUISystem::registerComponentEditors()` was removed; the constructor now +calls `registerModularComponents()` directly, which invokes +`ComponentRegistration::registerAll()`. + +Cross-component post-render side effects were moved into `onModified` hooks +co-located with each component's registration: + +- Transform → marks `StaticGeometryMemberComponent` dirty. +- Sun / Skybox / WaterPlane / Room / ClearArea → `markDirty()`. +- NavMesh → syncs `debugDraw` with `NavMeshSystem`. + +--- + +## Suggestion C — Serialization hooks + +**Problem.** `systems/SceneSerializer.cpp` (~4366 lines) hardcodes a +`serializeXxx`/`deserializeXxx` method pair per component plus two giant +`if (entity.has())` / `if (json.contains("key"))` dispatch chains in +`serializeEntity()` and `deserializeEntityComponents()`. Adding a component +today still requires edits to the serializer. + +**Design.** Extend `ComponentInfo` (or a parallel serde table) with two closures +and drive the serializer from the registry: + +```cpp +using ComponentSerialize = std::function; +using ComponentDeserialize = std::function; + +struct ComponentInfo { + ... + ComponentSerialize serialize; + ComponentDeserialize deserialize; + ComponentModified postLoad; // second-pass reference resolution +}; +``` + +The serializer then: + +1. In `serializeEntity`, iterate registered components, call `serialize(e)` for + those that have it, and write the result under a per-component JSON key. +2. In `deserializeEntityComponents`, for each key in the entity JSON that + matches a registered component, call `deserialize(e, value)`. +3. Keep the two-pass reference resolution (material references, + procedural-texture references, character spawner) as `postLoad` hooks that + run after all entities are loaded. + +**Steps.** + +1. Add the serde type aliases + fields to `ComponentRegistry.hpp` (keep the + `nlohmann::json` dependency out of the header if possible; prefer a + `void*`/`std::function` boundary or move the serde table into + `SceneSerializer` so the registry stays UI-only). +2. Migrate simple components first (Transform, Renderable, RigidBody, + Collider, Light, Camera, Lod, etc.) — move their `serialize/deserialize` + bodies from `SceneSerializer.cpp` into their `*Module.cpp` registration. +3. Leave complex components (Town/District/Lot, ProceduralMaterial, + CharacterSpawner, NavMesh) as explicit overrides initially; the registry is + a fallback. Then migrate them one at a time, adding `postLoad` where the + current two-pass logic lives. +4. Remove the now-dead `serializeXxx`/`deserializeXxx` declarations from + `SceneSerializer.hpp` and their dispatch blocks from `.cpp`. + +**Acceptance criteria.** + +- `serializeEntity` / `deserializeEntityComponents` contain no + `if (entity.has())` component dispatch (or only a tiny, documented set of + explicit overrides for two-pass components). +- Round-trip save/load tests still pass (see `tests/` and + `docs/SaveLoadSystem.md`). +- Adding a new serializable component touches only its `*Module.cpp`. + +--- + +## Suggestion D — Lifecycle hooks + +**Problem.** Three UI hot-spots still enumerate components by hand: + +1. `EditorUISystem::renderEntityNode()` — ~26 hardcoded hierarchy badge lines + (`indicators += " [T]"`). +2. `EditorUISystem::deleteEntity()` — hardcoded cleanup for RigidBody + (physics body), Transform (scene node), Renderable (Ogre entity). +3. `EditorUISystem::duplicateEntity()` — hardcoded copy logic for name, + transform, renderable. + +**Design.** Add nullable hooks to `ComponentInfo` (following the `onModified` +pattern already established): + +```cpp +struct ComponentInfo { + ... + std::function badge; // hierarchy indicator + std::function onDelete; // cleanup on delete + std::function onDuplicate; // copy +}; +``` + +**Steps.** + +1. Add the fields + optional parameters to `registerComponent()` (or a + post-registration setter to avoid growing the signature further). +2. Move the badge strings into each `*Module.cpp` (Transform `[T]`, Renderable + `[R]`, Light `[L]`, …). `renderEntityNode` then appends + `info.badge(entity)` for each component the entity has. +3. Move `deleteEntity` cleanup into `onDelete` hooks (RigidBody removes its + physics body, Transform destroys its scene node, Renderable destroys its + Ogre entity). `deleteEntity` becomes a generic "invoke onDelete for every + registered component present, then destruct". +4. Move `duplicateEntity` copy logic into `onDuplicate` hooks, or — for most + components — a generic `entity.set(other.get())` copy that falls back + to a hook for components with non-trivial ownership. + +**Acceptance criteria.** + +- `renderEntityNode`, `deleteEntity`, `duplicateEntity` contain no + `if (entity.has())` component dispatch. +- Deleting/duplicating an entity with a renderable + rigid body + transform + behaves identically to before. + +--- + +## Suggestion E — Unified Lua component table + +**Problem.** `lua/LuaComponentApi.cpp` maintains its own `s_components` map and +its own `REGISTER_COMPONENT` macro with hand-written push/read field lambdas — +a third independent list of components. + +**Design.** Make one shared descriptor the single source of truth. The editor +registry, the serializer (C) and the Lua binder (E) all read name/group/fields +from it. + +**Steps.** + +1. Introduce a `ComponentTraits` (or `ComponentDescriptor`) struct in a + neutral header (not UI-, not Lua-specific) holding at least `name`, `group`, + and optional `luaPush` / `luaRead` closures (or field-table reflection). +2. Have the `REGISTER_COMPONENT_GROUP` macro (or a new unified macro) populate + both the `ComponentRegistry` entry and the Lua `s_components` entry from the + same traits. +3. Migrate the existing hand-written `REGISTER_COMPONENT(...)` entries in + `LuaComponentApi.cpp` to the shared traits one component at a time, keeping + `lua-examples/` and `tests/*_lua_test.cpp` green. + +**Acceptance criteria.** + +- There is exactly one place that lists a component's name/group/serde/Lua + behaviour. +- `component_lua_test` and the other `*_lua_test` executables still pass. + +--- + +## Suggestion F — Flecs meta reflection (optional, long term) + +**Problem.** Even after C/D/E, "dumb data" components still need a hand-written +editor + serializer + Lua binding. + +**Design.** Register plain-data components with Flecs meta (`flecs::meta`) and +generate the ImGui editor, JSON serde and Lua accessor from the reflected +fields. Keep custom editors only for behaviour-heavy components (Camera +preview, NavMesh, CharacterSlots, etc.). + +**Steps.** + +1. Add `flecs::meta` to the project and register a pilot component with it. +2. Write a generic `MetaComponentEditor` that walks `flecs::meta` cursor + fields and renders an ImGui widget per field. +3. Write a generic meta-based serializer (or use the built-in `flecs::json` + exporter) and a meta-based Lua binding. +4. Migrate the simplest components; validate against existing round-trip tests. + +**Acceptance criteria.** + +- A new plain-data component can be added without writing an editor, a + serializer, or a Lua binding. +- OGRE math types (`Ogre::Vector3`, `Quaternion`, `ColourValue`) have registered + meta type handlers so they render and serialize correctly. + +--- + +## Conventions and caveats + +- Keep `order` on `ComponentInfo` for deterministic render order; only Transform + needs a non-zero value today. Do not rely on `unordered_map` iteration order. +- Empty/tag components must render via a stack-local dummy (handled by + `std::is_empty_v` in the renderer). Do not call `get_mut()` on them. +- Two-pass scene load (material/character/procedural-texture references) must + stay explicit; fold it into a `postLoad` hook, never into a single pass. +- The source code is the source of truth. Update `AGENTS.md` ("Adding a New + Component"), `docs/`, and `lua-examples/` whenever the registration API + changes. diff --git a/src/features/editScene/components/CellGridEditorsModule.cpp b/src/features/editScene/components/CellGridEditorsModule.cpp index f5cee3c..54ddcff 100644 --- a/src/features/editScene/components/CellGridEditorsModule.cpp +++ b/src/features/editScene/components/CellGridEditorsModule.cpp @@ -120,6 +120,10 @@ REGISTER_COMPONENT_GROUP("Room", "Room Layout", RoomComponent, RoomEditor) if (e.has()) { e.remove(); } + }, + // On-modified: mark dirty so RoomLayoutSystem rebuilds the room + [](flecs::entity e) { + e.get_mut().markDirty(); } ); } @@ -158,6 +162,10 @@ REGISTER_COMPONENT_GROUP("Clear Area", "Room Layout", ClearAreaComponent, ClearA if (e.has()) { e.remove(); } + }, + // On-modified: mark dirty so RoomLayoutSystem rebuilds the clear area + [](flecs::entity e) { + e.get_mut().markDirty(); } ); } diff --git a/src/features/editScene/components/CharacterIdentityModule.cpp b/src/features/editScene/components/CharacterIdentityModule.cpp new file mode 100644 index 0000000..8beedfc --- /dev/null +++ b/src/features/editScene/components/CharacterIdentityModule.cpp @@ -0,0 +1,23 @@ +#include "CharacterIdentity.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/CharacterIdentityEditor.hpp" + +// Register CharacterIdentity component +REGISTER_COMPONENT_GROUP("Character Identity", "Character", + CharacterIdentityComponent, CharacterIdentityEditor) +{ + registry.registerComponent( + "Character Identity", "Character", + std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) + e.set( + CharacterIdentityComponent{}); + }, + // Remover + [](flecs::entity e) { + if (e.has()) + e.remove(); + }); +} diff --git a/src/features/editScene/components/NavMeshModule.cpp b/src/features/editScene/components/NavMeshModule.cpp index e090bdc..80b6f76 100644 --- a/src/features/editScene/components/NavMeshModule.cpp +++ b/src/features/editScene/components/NavMeshModule.cpp @@ -2,6 +2,7 @@ #include "../ui/ComponentRegistration.hpp" #include "../ui/NavMeshEditor.hpp" #include "../ui/NavMeshGeometrySourceEditor.hpp" +#include "../systems/NavMeshSystem.hpp" REGISTER_COMPONENT_GROUP("NavMesh", "Navigation", NavMeshComponent, NavMeshEditor) @@ -18,6 +19,13 @@ REGISTER_COMPONENT_GROUP("NavMesh", "Navigation", NavMeshComponent, [](flecs::entity e) { if (e.has()) e.remove(); + }, + // On-modified: sync the debug-draw toggle with the NavMesh system + [](flecs::entity e) { + auto &nav = e.get_mut(); + if (nav.debugDraw && NavMeshSystem::getInstance()) + NavMeshSystem::getInstance()->setDebugDraw(e, + nav.debugDraw); }); } diff --git a/src/features/editScene/components/PhysicsColliderModule.cpp b/src/features/editScene/components/PhysicsColliderModule.cpp new file mode 100644 index 0000000..e85019a --- /dev/null +++ b/src/features/editScene/components/PhysicsColliderModule.cpp @@ -0,0 +1,22 @@ +#include "PhysicsCollider.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/PhysicsColliderEditor.hpp" + +// Register PhysicsCollider component +REGISTER_COMPONENT_GROUP("Physics Collider", "Physics", + PhysicsColliderComponent, PhysicsColliderEditor) +{ + registry.registerComponent( + "Physics Collider", "Physics", + std::make_unique(sceneMgr), + // Adder + [](flecs::entity e) { + if (!e.has()) + e.set({}); + }, + // Remover + [](flecs::entity e) { + if (e.has()) + e.remove(); + }); +} diff --git a/src/features/editScene/components/PrefabInstanceModule.cpp b/src/features/editScene/components/PrefabInstanceModule.cpp new file mode 100644 index 0000000..ee2db4d --- /dev/null +++ b/src/features/editScene/components/PrefabInstanceModule.cpp @@ -0,0 +1,21 @@ +#include "PrefabInstance.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/PrefabInstanceEditor.hpp" + +// Register PrefabInstance component +REGISTER_COMPONENT_GROUP("Prefab Instance", "Scene", PrefabInstanceComponent, + PrefabInstanceEditor) +{ + registry.registerComponent( + "Prefab Instance", "Scene", std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) + e.set({}); + }, + // Remover + [](flecs::entity e) { + if (e.has()) + e.remove(); + }); +} diff --git a/src/features/editScene/components/RenderableModule.cpp b/src/features/editScene/components/RenderableModule.cpp new file mode 100644 index 0000000..c6834ae --- /dev/null +++ b/src/features/editScene/components/RenderableModule.cpp @@ -0,0 +1,28 @@ +#include "Renderable.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/RenderableEditor.hpp" + +// Register Renderable component +REGISTER_COMPONENT_GROUP("Renderable", "Rendering", RenderableComponent, + RenderableEditor) +{ + registry.registerComponent( + "Renderable", "Rendering", + std::make_unique(sceneMgr), + // Adder + [](flecs::entity e) { + if (!e.has()) + e.set({}); + }, + // Remover + [sceneMgr](flecs::entity e) { + if (e.has()) { + auto &renderable = e.get_mut(); + if (renderable.entity) { + sceneMgr->destroyEntity(renderable.entity); + renderable.entity = nullptr; + } + e.remove(); + } + }); +} diff --git a/src/features/editScene/components/RigidBodyModule.cpp b/src/features/editScene/components/RigidBodyModule.cpp new file mode 100644 index 0000000..ebd8a67 --- /dev/null +++ b/src/features/editScene/components/RigidBodyModule.cpp @@ -0,0 +1,21 @@ +#include "RigidBody.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/RigidBodyEditor.hpp" + +// Register RigidBody component +REGISTER_COMPONENT_GROUP("Rigid Body", "Physics", RigidBodyComponent, + RigidBodyEditor) +{ + registry.registerComponent( + "Rigid Body", "Physics", std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) + e.set({}); + }, + // Remover + [](flecs::entity e) { + if (e.has()) + e.remove(); + }); +} diff --git a/src/features/editScene/components/SkyboxModule.cpp b/src/features/editScene/components/SkyboxModule.cpp index f697422..7fcc853 100644 --- a/src/features/editScene/components/SkyboxModule.cpp +++ b/src/features/editScene/components/SkyboxModule.cpp @@ -20,5 +20,9 @@ REGISTER_COMPONENT_GROUP("Skybox", "Environment", SkyboxComponent, SkyboxEditor) if (e.has()) { e.remove(); } + }, + // On-modified: mark dirty so EditorSkyboxSystem rebuilds the skybox + [](flecs::entity e) { + e.get_mut().markDirty(); }); } diff --git a/src/features/editScene/components/SunModule.cpp b/src/features/editScene/components/SunModule.cpp index c1def45..349b42c 100644 --- a/src/features/editScene/components/SunModule.cpp +++ b/src/features/editScene/components/SunModule.cpp @@ -20,5 +20,9 @@ REGISTER_COMPONENT_GROUP("Sun", "Environment", SunComponent, SunEditor) if (e.has()) { e.remove(); } + }, + // On-modified: mark dirty so EditorSunSystem rebuilds light/material + [](flecs::entity e) { + e.get_mut().markDirty(); }); } diff --git a/src/features/editScene/components/TransformModule.cpp b/src/features/editScene/components/TransformModule.cpp new file mode 100644 index 0000000..156cef3 --- /dev/null +++ b/src/features/editScene/components/TransformModule.cpp @@ -0,0 +1,42 @@ +#include "Transform.hpp" +#include "StaticGeometryMember.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/TransformEditor.hpp" + +// Register Transform component +REGISTER_COMPONENT_GROUP("Transform", "Transform", TransformComponent, + TransformEditor) +{ + registry.registerComponent( + "Transform", "Transform", std::make_unique(), + // Adder + [sceneMgr](flecs::entity e) { + if (!e.has()) { + TransformComponent transform; + transform.node = sceneMgr->getRootSceneNode() + ->createChildSceneNode(); + transform.position = Ogre::Vector3::ZERO; + transform.rotation = Ogre::Quaternion::IDENTITY; + transform.scale = Ogre::Vector3::UNIT_SCALE; + e.set(transform); + } + }, + // Remover + [sceneMgr](flecs::entity e) { + if (e.has()) { + auto &transform = e.get_mut(); + if (transform.node) { + sceneMgr->destroySceneNode(transform.node); + transform.node = nullptr; + } + e.remove(); + } + }, + // On-modified: keep a StaticGeometryMember in sync with its transform + [](flecs::entity e) { + if (e.has()) + e.get_mut().markDirty(); + }, + // Render order: Transform always renders first + -1); +} diff --git a/src/features/editScene/components/WaterPlaneModule.cpp b/src/features/editScene/components/WaterPlaneModule.cpp index c72ff64..e360ce8 100644 --- a/src/features/editScene/components/WaterPlaneModule.cpp +++ b/src/features/editScene/components/WaterPlaneModule.cpp @@ -21,5 +21,9 @@ REGISTER_COMPONENT_GROUP("Water Plane", "Water", WaterPlane, WaterPlaneEditor) if (e.has()) { e.remove(); } + }, + // On-modified: mark dirty so EditorWaterPlaneSystem rebuilds the plane + [](flecs::entity e) { + e.get_mut().markDirty(); }); } diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 75835f3..cb61db5 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -52,13 +52,7 @@ #include "../components/Inventory.hpp" #include "CharacterClassSystem.hpp" -#include "../ui/TransformEditor.hpp" -#include "../ui/RenderableEditor.hpp" -#include "../ui/PhysicsColliderEditor.hpp" -#include "../ui/RigidBodyEditor.hpp" #include "../ui/ComponentRegistration.hpp" -#include "../ui/CharacterIdentityEditor.hpp" -#include "../ui/PrefabInstanceEditor.hpp" #include "PhysicsSystem.hpp" #include "BuoyancySystem.hpp" #include "NavMeshSystem.hpp" @@ -79,7 +73,7 @@ EditorUISystem::EditorUISystem(flecs::world &world, , m_selectedEntity(flecs::entity::null()) , m_nameQuery(world.query()) { - registerComponentEditors(); + registerModularComponents(); m_gizmo = std::make_unique(m_sceneMgr); m_roadGizmo = std::make_unique(m_sceneMgr); m_cursor3D = std::make_unique(m_sceneMgr); @@ -249,135 +243,6 @@ bool EditorUISystem::onMouseReleased() return false; } -void EditorUISystem::registerComponentEditors() -{ - // Register Transform component - auto transformEditor = std::make_unique(); - m_componentRegistry.registerComponent( - "Transform", "Transform", std::move(transformEditor), - // Adder - [this](flecs::entity e) { - if (!e.has()) { - TransformComponent transform; - transform.node = - m_sceneMgr->getRootSceneNode() - ->createChildSceneNode(); - transform.position = Ogre::Vector3::ZERO; - transform.rotation = Ogre::Quaternion::IDENTITY; - transform.scale = Ogre::Vector3::UNIT_SCALE; - e.set(transform); - } - }, - // Remover - [this](flecs::entity e) { - if (e.has()) { - auto &transform = - e.get_mut(); - if (transform.node) { - m_sceneMgr->destroySceneNode( - transform.node); - transform.node = nullptr; - } - e.remove(); - } - }); - - // Register Renderable component - auto renderableEditor = std::make_unique(m_sceneMgr); - m_componentRegistry.registerComponent( - "Renderable", "Rendering", std::move(renderableEditor), - // Adder - [this](flecs::entity e) { - if (!e.has()) { - e.set({}); - } - }, - // Remover - [this](flecs::entity e) { - if (e.has()) { - auto &renderable = - e.get_mut(); - if (renderable.entity) { - m_sceneMgr->destroyEntity( - renderable.entity); - renderable.entity = nullptr; - } - e.remove(); - } - }); - - // Register RigidBody component - auto rigidBodyEditor = std::make_unique(); - m_componentRegistry.registerComponent( - "Rigid Body", "Physics", std::move(rigidBodyEditor), - // Adder - [this](flecs::entity e) { - if (!e.has()) { - e.set({}); - } - }, - // Remover - [this](flecs::entity e) { - if (e.has()) { - e.remove(); - } - }); - - // Register PhysicsCollider component - auto colliderEditor = - std::make_unique(m_sceneMgr); - m_componentRegistry.registerComponent( - "Physics Collider", "Physics", std::move(colliderEditor), - // Adder - [this](flecs::entity e) { - if (!e.has()) { - e.set({}); - } - }, - // Remover - [this](flecs::entity e) { - if (e.has()) { - e.remove(); - } - }); - - // Register PrefabInstance component - auto prefabEditor = std::make_unique(); - m_componentRegistry.registerComponent( - "Prefab Instance", "Scene", std::move(prefabEditor), - // Adder - [this](flecs::entity e) { - if (!e.has()) { - e.set({}); - } - }, - // Remover - [this](flecs::entity e) { - if (e.has()) { - e.remove(); - } - }); - - // Register CharacterIdentity component - auto characterIdentityEditor = - std::make_unique(); - m_componentRegistry.registerComponent( - "Character Identity", "Character", - std::move(characterIdentityEditor), - [](flecs::entity e) { - if (!e.has()) - e.set( - CharacterIdentityComponent{}); - }, - [](flecs::entity e) { - if (e.has()) - e.remove(); - }); - - // Register modular components (Light, Camera, etc.) - registerModularComponents(); -} - void EditorUISystem::registerModularComponents() { // This calls all component modules registered via REGISTER_COMPONENT macro @@ -1056,387 +921,33 @@ void EditorUISystem::renderComponentList(flecs::entity entity) // Render component editors ImGui::BeginChild("Components", ImVec2(0, 0), true); - // Dynamically render all components the entity has + // Collect the components present on the entity and render them through + // their registered type-erased renderers. Sort by (order, name) so + // Transform (order -1) always renders first. + std::vector infos; + m_componentRegistry.forEach( + [&](const std::type_index &, + const ComponentRegistry::ComponentInfo &info) { + if (info.renderer && info.checker(entity)) + infos.push_back(&info); + }); + std::sort(infos.begin(), infos.end(), + [](const ComponentRegistry::ComponentInfo *a, + const ComponentRegistry::ComponentInfo *b) { + if (a->order != b->order) + return a->order < b->order; + return std::string(a->name) < std::string(b->name); + }); + int componentCount = 0; - - // Render Transform first if present (it's the base component) - if (entity.has()) { - auto &transform = entity.get_mut(); - if (m_componentRegistry.render(entity, - transform)) { - // Transform changed - mark StaticGeometryMember dirty if present - if (entity.has()) { - entity.get_mut() - .markDirty(); - } - } - componentCount++; - } - - // Render Renderable - if (entity.has()) { - auto &renderable = entity.get_mut(); - m_componentRegistry.render(entity, - renderable); - componentCount++; - } - - // Render Light if present - if (entity.has()) { - auto &light = entity.get_mut(); - m_componentRegistry.render(entity, light); - componentCount++; - } - - // Render Camera if present - if (entity.has()) { - auto &camera = entity.get_mut(); - m_componentRegistry.render(entity, camera); - componentCount++; - } - - // Render RigidBody - if (entity.has()) { - auto &rigidBody = entity.get_mut(); - m_componentRegistry.render(entity, - rigidBody); - componentCount++; - } - - // Render PhysicsCollider - if (entity.has()) { - auto &collider = entity.get_mut(); - m_componentRegistry.render(entity, - collider); - componentCount++; - } - - // Render BuoyancyInfo if present - if (entity.has()) { - auto &buoyancy = entity.get_mut(); - m_componentRegistry.render(entity, buoyancy); - componentCount++; - } - - // Render WaterPhysics if present - if (entity.has()) { - auto &wp = entity.get_mut(); - m_componentRegistry.render(entity, wp); - componentCount++; - } - - // Render Sun if present - if (entity.has()) { - auto &sun = entity.get_mut(); - if (m_componentRegistry.render(entity, sun)) { - sun.markDirty(); - } - componentCount++; - } - - // Render Skybox if present - if (entity.has()) { - auto &sky = entity.get_mut(); - if (m_componentRegistry.render(entity, sky)) { - sky.markDirty(); - } - componentCount++; - } - - // Render WaterPlane if present - if (entity.has()) { - auto &wp = entity.get_mut(); - if (m_componentRegistry.render(entity, wp)) { - wp.markDirty(); - } - componentCount++; - } - - // Render Terrain if present - if (entity.has()) { - auto &tc = entity.get_mut(); - m_componentRegistry.render(entity, tc); - componentCount++; - } - - // Render LOD Settings if present - if (entity.has()) { - auto &lodSettings = entity.get_mut(); - m_componentRegistry.render(entity, - lodSettings); - componentCount++; - } - - // Render LOD if present - if (entity.has()) { - auto &lod = entity.get_mut(); - m_componentRegistry.render(entity, lod); - componentCount++; - } - - // Render StaticGeometry Region if present - if (entity.has()) { - auto ®ion = entity.get_mut(); - m_componentRegistry.render(entity, - region); - componentCount++; - } - - // Render StaticGeometry Member if present - if (entity.has()) { - auto &member = entity.get_mut(); - m_componentRegistry.render( - entity, member); - componentCount++; - } - - // Render ProceduralTexture if present - if (entity.has()) { - auto &texture = entity.get_mut(); - m_componentRegistry.render(entity, - texture); - componentCount++; - } - - // Render ProceduralMaterial if present - if (entity.has()) { - auto &material = entity.get_mut(); - m_componentRegistry.render( - entity, material); - componentCount++; - } - - // Render Primitive if present - if (entity.has()) { - auto &primitive = entity.get_mut(); - m_componentRegistry.render(entity, - primitive); - componentCount++; - } - - // Render TriangleBuffer if present - if (entity.has()) { - auto &tb = entity.get_mut(); - m_componentRegistry.render(entity, tb); - componentCount++; - } - - // Render Character if present - if (entity.has()) { - auto &cc = entity.get_mut(); - m_componentRegistry.render(entity, cc); - componentCount++; - } - - // Render CharacterSlots if present - if (entity.has()) { - /* CharacterSlotsComponent is now an empty tag; get_mut is illegal - * for zero-sized types in flecs. */ - CharacterSlotsComponent cs; - m_componentRegistry.render(entity, cs); - componentCount++; - } - - // Render CharacterIdentity if present - if (entity.has()) { - auto &ci = entity.get_mut(); - m_componentRegistry.render(entity, - ci); - componentCount++; - } - - // Render CharacterSpawner if present - if (entity.has()) { - auto &spawner = entity.get_mut(); - m_componentRegistry.render(entity, - spawner); - componentCount++; - } - - // Render AnimationTree if present - if (entity.has()) { - auto &at = entity.get_mut(); - m_componentRegistry.render(entity, at); - componentCount++; - } - - // Render StartupMenu if present - if (entity.has()) { - auto &sm = entity.get_mut(); - m_componentRegistry.render(entity, sm); - componentCount++; - } - - // Render PlayerController if present - if (entity.has()) { - auto &pc = entity.get_mut(); - m_componentRegistry.render(entity, - pc); - 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(); - if (m_componentRegistry.render(entity, room)) { - room.markDirty(); - } - componentCount++; - } - - // Render ClearArea if present - if (entity.has()) { - auto &clearArea = entity.get_mut(); - if (m_componentRegistry.render(entity, - clearArea)) { - clearArea.markDirty(); - } - componentCount++; - } - - // Render FurnitureTemplate if present - if (entity.has()) { - auto &furniture = entity.get_mut(); - m_componentRegistry.render( - entity, furniture); - componentCount++; - } - - // Render ActionDatabaseComponent if present - if (entity.has()) { - auto &db = entity.get_mut(); - m_componentRegistry.render(entity, db); - componentCount++; - } - - // Render ActionDebug if present - - if (entity.has()) { - auto &debug = entity.get_mut(); - m_componentRegistry.render(entity, debug); - componentCount++; - } - - // Render BehaviorTree if present - if (entity.has()) { - auto &bt = entity.get_mut(); - m_componentRegistry.render(entity, bt); - componentCount++; - } - - // Render NavMesh if present - if (entity.has()) { - auto &nav = entity.get_mut(); - if (m_componentRegistry.render(entity, nav)) { - if (nav.debugDraw && NavMeshSystem::getInstance()) - NavMeshSystem::getInstance()->setDebugDraw( - entity, nav.debugDraw); - } - componentCount++; - } - - // Render NavMeshGeometrySource if present - if (entity.has()) { - auto &src = entity.get_mut(); - m_componentRegistry.render(entity, src); - componentCount++; - } - - // Render SmartObject if present - if (entity.has()) { - auto &so = entity.get_mut(); - m_componentRegistry.render(entity, so); - componentCount++; - } - - // Render GoapPlanner if present - if (entity.has()) { - auto &planner = entity.get_mut(); - m_componentRegistry.render(entity, - planner); - componentCount++; - } - - // Render GoapRunner if present - if (entity.has()) { - auto &runner = entity.get_mut(); - m_componentRegistry.render(entity, runner); - componentCount++; - } - - // Render PathFollowing if present - if (entity.has()) { - auto &pf = entity.get_mut(); - m_componentRegistry.render(entity, pf); - componentCount++; - } - - // Render Actuator if present - if (entity.has()) { - auto &actuator = entity.get_mut(); - m_componentRegistry.render(entity, actuator); - componentCount++; - } - - // Render EventHandler if present - if (entity.has()) { - auto &handler = entity.get_mut(); - m_componentRegistry.render(entity, - handler); - componentCount++; - } - - // Render Item if present - if (entity.has()) { - auto &item = entity.get_mut(); - m_componentRegistry.render(entity, item); - componentCount++; - } - - // Render Inventory if present - if (entity.has()) { - auto &inv = entity.get_mut(); - m_componentRegistry.render(entity, inv); + for (const auto *info : infos) { + bool modified = info->renderer(entity); + if (modified && info->onModified) + info->onModified(entity); componentCount++; } // Show message if no components - if (componentCount == 0) { ImGui::TextDisabled("No components"); ImGui::Text("Click 'Add Component' to add components"); diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index eeff804..07c2689 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -243,7 +243,6 @@ private: void renderRemoveComponentMenu(flecs::entity entity); // Helper functions - void registerComponentEditors(); void registerModularComponents(); flecs::entity findEntityParent(flecs::entity entity); std::vector getEntityChildren(flecs::entity entity); diff --git a/src/features/editScene/ui/ComponentRegistry.hpp b/src/features/editScene/ui/ComponentRegistry.hpp index 36cbf2c..1388a17 100644 --- a/src/features/editScene/ui/ComponentRegistry.hpp +++ b/src/features/editScene/ui/ComponentRegistry.hpp @@ -2,6 +2,7 @@ #define EDITSCENE_COMPONENTREGISTRY_HPP #pragma once #include +#include #include #include #include @@ -26,6 +27,24 @@ using ComponentRemover = std::function; */ using ComponentChecker = std::function; +/** + * Function type for rendering a component editor from an entity. + * + * The closure performs the type-erased fetch of the component pointer + * (via e.get_mut() for data components, or a stack-local instance for + * empty/tag components) and forwards it to the registered editor. + */ +using ComponentRenderer = std::function; + +/** + * Function type invoked after a component editor reports a modification. + * + * Used for cross-component/system side effects that cannot live inside a + * single editor (e.g. marking a StaticGeometryMember dirty when its + * Transform changed, or syncing NavMesh debug-draw state). + */ +using ComponentModified = std::function; + /** * Registry for component editors and their add/remove functions */ @@ -34,10 +53,13 @@ public: struct ComponentInfo { const char *name; const char *group; // Group name for menu organization (nullptr = "Miscellaneous") + int order = 0; // Render order (lower renders first; Transform is -1) std::unique_ptr editor; + ComponentRenderer renderer; // Type-erased render closure ComponentAdder adder; ComponentRemover remover; ComponentChecker checker; // Checks if entity has this component + ComponentModified onModified; // Optional post-render side effect }; ComponentRegistry() = default; @@ -58,19 +80,38 @@ public: * @param editor The editor for this component * @param adder Function to add the component to an entity * @param remover Function to remove the component from an entity + * @param onModified Optional callback invoked after the editor reports a + * modification (for cross-component side effects) + * @param order Render order; lower values render first (Transform uses -1) */ template void registerComponent(const char *name, const char *group, std::unique_ptr> editor, - ComponentAdder adder, ComponentRemover remover) + ComponentAdder adder, ComponentRemover remover, + ComponentModified onModified = nullptr, + int order = 0) { ComponentInfo info; info.name = name; info.group = group ? group : "Miscellaneous"; + info.order = order; + IComponentEditor *rawEditor = editor.get(); info.editor = std::move(editor); + info.renderer = [rawEditor](flecs::entity e) -> bool { + if (!e.has()) + return false; + if constexpr (std::is_empty_v) { + /* Empty/tag components are zero-sized in flecs, so + * get_mut() is illegal; render against a dummy. */ + T dummy{}; + return rawEditor->render(e, &dummy); + } + return rawEditor->render(e, &e.get_mut()); + }; info.adder = adder; info.remover = remover; info.checker = [](flecs::entity e) { return e.has(); }; + info.onModified = onModified; m_components[std::type_index(typeid(T))] = std::move(info); }