kitgit

tirbofish/dropbear · commit

d8ab63e6f99d2592bb6df1b23e8d456e055eb3ee

spewed something that resembled a component tree. i mean, it sorta works? a lot of changed need to be made, such as a proper ID, and implementing SerializableComponent for everything else + some proper plugin work. otherwise, it is pretty nice.

Unverified

tk <4tkbytes@pm.me> · 2025-11-21 14:37

view full diff

diff --git a/Cargo.toml b/Cargo.toml
index 4811015..b3b006b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -77,6 +77,7 @@ typetag = "0.2"
 syn = { version = "2.0", features = ["full"] }
 quote = "1.0"
 egui_ltreeview = { version = "0.6", features = ["doc"] }
+dyn-hash = "1.0"
 
 [workspace.dependencies.image]
 version = "0.25"
diff --git a/dropbear-traits/Cargo.toml b/dropbear-traits/Cargo.toml
index f869fb8..df14e7e 100644
--- a/dropbear-traits/Cargo.toml
+++ b/dropbear-traits/Cargo.toml
@@ -8,3 +8,7 @@ readme = "README.md"
 
 [dependencies]
 typetag.workspace = true
+hecs.workspace = true
+log.workspace = true
+dyn-hash.workspace = true
+anyhow.workspace = true
\ No newline at end of file
diff --git a/dropbear-traits/src/component_registry.rs b/dropbear-traits/src/component_registry.rs
new file mode 100644
index 0000000..bad6ba2
--- /dev/null
+++ b/dropbear-traits/src/component_registry.rs
@@ -0,0 +1,99 @@
+use std::any::TypeId;
+use std::collections::HashMap;
+use anyhow::Result;
+use hecs::{Entity, EntityBuilder, World};
+use crate::{
+    ComponentConverter,
+    ComponentDeserializer,
+    CustomConverter,
+    CustomDeserializer,
+    DirectConverter,
+    DirectDeserializer,
+    SerializableComponent,
+};
+
+#[derive(Default)]
+pub struct ComponentRegistry {
+    converters: HashMap<TypeId, Box<dyn ComponentConverter>>,
+    deserializers: HashMap<TypeId, Box<dyn ComponentDeserializer>>,
+}
+
+impl ComponentRegistry {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    // Register a component that's already SerializableComponent
+    pub fn register<T>(&mut self)
+    where
+        T: SerializableComponent + hecs::Component + Clone + 'static,
+    {
+        let type_id = TypeId::of::<T>();
+        self.converters
+            .insert(type_id, Box::new(DirectConverter::<T>::new()));
+        self.deserializers
+            .insert(type_id, Box::new(DirectDeserializer::<T>::new()));
+    }
+
+    // Register a custom converter for special cases
+    pub fn register_converter<From, To, F>(&mut self, converter_fn: F)
+    where
+        From: hecs::Component + 'static,
+        To: SerializableComponent + 'static,
+        F: Fn(&From) -> To + Send + Sync + 'static,
+    {
+        let type_id = TypeId::of::<From>();
+        self.converters.insert(
+            type_id,
+            Box::new(CustomConverter::new(converter_fn))
+        );
+    }
+
+    pub fn register_deserializer<From, To, F>(&mut self, converter_fn: F)
+    where
+        From: SerializableComponent + 'static,
+        To: hecs::Component + Clone + 'static,
+        F: Fn(&From) -> To + Send + Sync + 'static,
+    {
+        let type_id = TypeId::of::<From>();
+        self.deserializers.insert(
+            type_id,
+            Box::new(CustomDeserializer::new(converter_fn)),
+        );
+    }
+
+    // Extract all serializable components from an entity
+    pub fn extract_all_components(
+        &self,
+        world: &World,
+        entity: Entity,
+    ) -> Vec<Box<dyn SerializableComponent>> {
+        let mut vec = vec![];
+        for (k, v) in &self.converters
+        {
+            match v.extract_serializable(world, entity) {
+                Some(v) => vec.push(v),
+                None => {log::error!("Unable to extract the serializable value for entity [typeid: {:?}]", k); continue;}
+            }
+        }
+        return vec;
+    }
+
+    /// Attempts to deserialize a [`SerializableComponent`] back into an
+    /// ECS component and insert it into the provided [`EntityBuilder`].
+    /// Returns `Ok(true)` if the component was handled, `Ok(false)` if no
+    /// deserializer was registered, and `Err` if deserialization failed.
+    pub fn deserialize_into_builder(
+        &self,
+        component: &dyn SerializableComponent,
+        builder: &mut EntityBuilder,
+    ) -> Result<bool> {
+        let type_id = component.as_any().type_id();
+        if let Some(deserializer) = self.deserializers.get(&type_id) {
+            deserializer.insert_into_builder(component, builder)?;
+            Ok(true)
+        } else {
+            Ok(false)
+        }
+    }
+}
\ No newline at end of file
diff --git a/dropbear-traits/src/lib.rs b/dropbear-traits/src/lib.rs
index 02df2c5..94a16df 100644
--- a/dropbear-traits/src/lib.rs
+++ b/dropbear-traits/src/lib.rs
@@ -1,5 +1,9 @@
-use std::any::Any;
+pub mod component_registry;
+
+use std::any::{Any, TypeId};
 use std::fmt::Debug;
+use anyhow::{anyhow, Result};
+use hecs::{Entity, EntityBuilder, World};
 
 /// A type of component that gets serialized and deserialized into a scene config file.
 #[typetag::serde(tag = "type")]
@@ -14,8 +18,194 @@ pub trait SerializableComponent: Send + Sync + Debug {
     fn clone_boxed(&self) -> Box<dyn SerializableComponent>;
 }
 
+
 impl Clone for Box<dyn SerializableComponent> {
     fn clone(&self) -> Self {
         self.clone_boxed()
     }
+
+    fn clone_from(&mut self, source: &Self)
+    {
+        *self = source.clone_boxed();
+    }
+}
+
+pub trait ComponentConverter: Send + Sync {
+    fn type_id(&self) -> TypeId;
+    fn type_name(&self) -> &'static str;
+
+    fn extract_serializable(
+        &self,
+        world: &World,
+        entity: Entity
+    ) -> Option<Box<dyn SerializableComponent>>;
+}
+
+pub trait ComponentDeserializer: Send + Sync {
+    fn serializable_type_id(&self) -> TypeId;
+    fn serializable_type_name(&self) -> &'static str;
+
+    fn insert_into_builder(
+        &self,
+        component: &dyn SerializableComponent,
+        builder: &mut EntityBuilder,
+    ) -> Result<()>;
+}
+
+struct DirectConverter<T: SerializableComponent + hecs::Component + Clone> {
+    _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T: SerializableComponent + hecs::Component + Clone + 'static> DirectConverter<T> {
+    fn new() -> Self {
+        Self { _phantom: std::marker::PhantomData }
+    }
+}
+
+impl<T: SerializableComponent + hecs::Component + Clone + 'static> ComponentConverter for DirectConverter<T> {
+    fn type_id(&self) -> TypeId {
+        TypeId::of::<T>()
+    }
+
+    fn type_name(&self) -> &'static str {
+        std::any::type_name::<T>()
+    }
+
+    fn extract_serializable(&self, world: &World, entity: Entity) -> Option<Box<dyn SerializableComponent>> {
+        if let Ok(mut q) = world.query_one::<&T>(entity) && let Some(ty) = q.get() {
+            return Some(ty.clone_boxed());
+        }
+        None
+    }
+}
+
+/// Custom converter that has special logic for converting `T` to a [`SerializableComponent`]
+struct CustomConverter<From, To, F>
+where
+    From: hecs::Component,
+    To: SerializableComponent,
+    F: Fn(&From) -> To + Send + Sync,
+{
+    converter_fn: F,
+    _phantom: std::marker::PhantomData<(From, To)>,
+}
+
+impl<From, To, F> CustomConverter<From, To, F>
+where
+    From: hecs::Component + 'static,
+    To: SerializableComponent + 'static,
+    F: Fn(&From) -> To + Send + Sync,
+{
+    fn new(converter_fn: F) -> Self {
+        Self {
+            converter_fn,
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<From, To, F> ComponentConverter for CustomConverter<From, To, F>
+where
+    From: hecs::Component + 'static,
+    To: SerializableComponent + 'static,
+    F: Fn(&From) -> To + Send + Sync,
+{
+    fn type_id(&self) -> TypeId {
+        TypeId::of::<From>()
+    }
+
+    fn type_name(&self) -> &'static str {
+        std::any::type_name::<From>()
+    }
+
+    fn extract_serializable(&self, world: &World, entity: Entity) -> Option<Box<dyn SerializableComponent>> {
+        world.get::<&From>(entity).ok()
+            .as_ref().map(|component| Box::new((self.converter_fn)(component)) as Box<dyn SerializableComponent>)
+    }
+}
+
+struct DirectDeserializer<T: SerializableComponent + hecs::Component + Clone> {
+    _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T: SerializableComponent + hecs::Component + Clone + 'static> DirectDeserializer<T> {
+    pub fn new() -> Self {
+        Self { _phantom: std::marker::PhantomData }
+    }
+}
+
+impl<T: SerializableComponent + hecs::Component + Clone + 'static> ComponentDeserializer for DirectDeserializer<T> {
+    fn serializable_type_id(&self) -> TypeId {
+        TypeId::of::<T>()
+    }
+
+    fn serializable_type_name(&self) -> &'static str {
+        std::any::type_name::<T>()
+    }
+
+    fn insert_into_builder(&self, component: &dyn SerializableComponent, builder: &mut EntityBuilder) -> Result<()> {
+        let typed = component
+            .as_any()
+            .downcast_ref::<T>()
+            .ok_or_else(|| anyhow!(
+                "Component '{}' does not match registered type '{}'",
+                component.type_name(),
+                std::any::type_name::<T>()
+            ))?;
+        builder.add(typed.clone());
+        Ok(())
+    }
+}
+
+struct CustomDeserializer<From, To, F>
+where
+    From: SerializableComponent,
+    To: hecs::Component,
+    F: Fn(&From) -> To + Send + Sync,
+{
+    converter_fn: F,
+    _phantom: std::marker::PhantomData<(From, To)>,
+}
+
+impl<From, To, F> CustomDeserializer<From, To, F>
+where
+    From: SerializableComponent + 'static,
+    To: hecs::Component + Clone + 'static,
+    F: Fn(&From) -> To + Send + Sync + 'static,
+{
+    pub fn new(converter_fn: F) -> Self {
+        Self {
+            converter_fn,
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<From, To, F> ComponentDeserializer for CustomDeserializer<From, To, F>
+where
+    From: SerializableComponent + 'static,
+    To: hecs::Component + Clone + 'static,
+    F: Fn(&From) -> To + Send + Sync + 'static,
+{
+    fn serializable_type_id(&self) -> TypeId {
+        TypeId::of::<From>()
+    }
+
+    fn serializable_type_name(&self) -> &'static str {
+        std::any::type_name::<From>()
+    }
+
+    fn insert_into_builder(&self, component: &dyn SerializableComponent, builder: &mut EntityBuilder) -> Result<()> {
+        let typed = component
+            .as_any()
+            .downcast_ref::<From>()
+            .ok_or_else(|| anyhow!(
+                "Component '{}' cannot be deserialized by '{}'",
+                component.type_name(),
+                std::any::type_name::<From>()
+            ))?;
+        let rebuild = (self.converter_fn)(typed);
+        builder.add(rebuild);
+        Ok(())
+    }
 }
\ No newline at end of file
diff --git a/eucalyptus-core/src/hierarchy.rs b/eucalyptus-core/src/hierarchy.rs
index e230c80..06e38cb 100644
--- a/eucalyptus-core/src/hierarchy.rs
+++ b/eucalyptus-core/src/hierarchy.rs
@@ -3,12 +3,12 @@ use serde::{Deserialize, Serialize};
 use dropbear_engine::entity::{EntityTransform, Transform};
 use crate::states::Label;
 
-/// A component that is added to all entities to show all child entities
+/// A component that tracks all child entities of a parent entity
 #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
-pub struct Parent(Vec<hecs::Entity>);
+pub struct Children(Vec<hecs::Entity>);
 
-impl Parent {
-    /// Creates a new parent component with the provided child entities.
+impl Children {
+    /// Creates a new children component with the provided child entities.
     pub fn new(children: Vec<hecs::Entity>) -> Self {
         Self(children)
     }
@@ -23,12 +23,19 @@ impl Parent {
         &mut self.0
     }
 
-    /// Adds a new child entity to this parent component.
+    /// Adds a new child entity to this component.
     pub fn push(&mut self, child: hecs::Entity) {
-        self.0.push(child);
+        if !self.0.contains(&child) {
+            self.0.push(child);
+        }
+    }
+
+    /// Removes a specific child entity.
+    pub fn remove(&mut self, child: hecs::Entity) {
+        self.0.retain(|&e| e != child);
     }
 
-    /// Removes all children from this parent component.
+    /// Removes all children from this component.
     pub fn clear(&mut self) {
         self.0.clear();
     }
@@ -41,20 +48,109 @@ impl Parent {
 
 /// A component that points to the parent entity of an entity.
 #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
-pub struct Child(hecs::Entity);
+pub struct Parent(hecs::Entity);
 
-impl Child {
-    /// Creates a new child component with the provided parent entity.
+impl Parent {
+    /// Creates a new parent component with the provided parent entity.
     pub fn new(parent: hecs::Entity) -> Self {
         Self(parent)
     }
 
-    /// Returns the parent entity of this child component.
+    /// Returns the parent entity of this component.
     pub fn parent(&self) -> hecs::Entity {
         self.0
     }
 }
 
+/// Helper functions for managing entity hierarchies
+pub struct Hierarchy;
+
+impl Hierarchy {
+    /// Set the parent of a child entity, updating both Parent and Children components
+    pub fn set_parent(world: &mut hecs::World, child: hecs::Entity, parent: hecs::Entity) {
+        // Remove old parent relationship if it exists
+        if let Ok(old_parent) = world.get::<&Parent>(child) {
+            let old_parent_entity = old_parent.parent();
+            if let Ok(mut children) = world.get::<&mut Children>(old_parent_entity) {
+                children.remove(child);
+            }
+        }
+
+        // Add Parent component to child
+        let _ = world.insert_one(child, Parent::new(parent));
+
+        // Add child to parent's Children component
+        if let Ok(mut children) = world.get::<&mut Children>(parent) {
+            children.push(child);
+        } else {
+            let _ = world.insert_one(parent, Children::new(vec![child]));
+        }
+    }
+
+    /// Remove parent relationship from a child entity
+    pub fn remove_parent(world: &mut hecs::World, child: hecs::Entity) {
+        let mut local_remove_signal = false;
+        if let Ok(parent) = world.get::<&Parent>(child) {
+            let parent_entity = parent.parent();
+
+            local_remove_signal = true;
+
+            if let Ok(mut children) = world.get::<&mut Children>(parent_entity) {
+                children.remove(child);
+            }
+        }
+
+        if local_remove_signal {
+            let _ = world.remove_one::<Parent>(child);
+        }
+    }
+
+    /// Get all children of an entity
+    pub fn get_children(world: &hecs::World, entity: hecs::Entity) -> Vec<hecs::Entity> {
+        world.get::<&Children>(entity)
+            .map(|c| c.children().to_vec())
+            .unwrap_or_default()
+    }
+
+    /// Get the parent of an entity
+    pub fn get_parent(world: &hecs::World, entity: hecs::Entity) -> Option<hecs::Entity> {
+        world.get::<&Parent>(entity)
+            .ok()
+            .map(|p| p.parent())
+    }
+
+    /// Get all ancestors of an entity (parent, grandparent, etc.)
+    pub fn get_ancestors(world: &hecs::World, entity: hecs::Entity) -> Vec<hecs::Entity> {
+        let mut ancestors = Vec::new();
+        let mut current = entity;
+
+        while let Some(parent) = Self::get_parent(world, current) {
+            ancestors.push(parent);
+            current = parent;
+        }
+
+        ancestors
+    }
+
+    /// Check if an entity is a descendant of another
+    pub fn is_descendant_of(
+        world: &hecs::World,
+        entity: hecs::Entity,
+        potential_ancestor: hecs::Entity,
+    ) -> bool {
+        let mut current = entity;
+
+        while let Some(parent) = Self::get_parent(world, current) {
+            if parent == potential_ancestor {
+                return true;
+            }
+            current = parent;
+        }
+
+        false
+    }
+}
+
 /// An extension trait for [EntityTransform] that allows for propagation of entities into a target transform.
 pub trait EntityTransformExt {
     /// Walks up the [`hecs::World`] and calculates the final [Transform] for the entity based off its parents.
@@ -66,8 +162,8 @@ impl EntityTransformExt for EntityTransform {
         let mut result = self.local().clone();
 
         let mut current = target_entity;
-        while let Ok(child) = world.get::<&Child>(current) {
-            let parent_entity = child.parent();
+        while let Ok(parent_comp) = world.get::<&Parent>(current) {
+            let parent_entity = parent_comp.parent();
 
             if let Ok(parent_transform) = world.get::<&EntityTransform>(parent_entity) {
                 let parent_world = parent_transform.world();
@@ -86,6 +182,7 @@ impl EntityTransformExt for EntityTransform {
     }
 }
 
+/// A serializable scene hierarchy based on entity labels
 #[derive(Default, Serialize, Deserialize, Clone, Debug)]
 pub struct SceneHierarchy {
     /// Maps entity labels to their parent label
@@ -102,6 +199,31 @@ impl SceneHierarchy {
         }
     }
 
+    /// Build hierarchy from world entities
+    pub fn from_world(world: &hecs::World) -> Self {
+        let mut hierarchy = Self::new();
+
+        for (_entity, (label, parent)) in world.query::<(&Label, &Parent)>().iter() {
+            if let Ok(parent_label) = world.get::<&Label>(parent.parent()) {
+                hierarchy.set_parent(label.clone(), Label::new(parent_label.as_str()));
+            }
+        }
+
+        hierarchy
+    }
+
+    /// Apply this hierarchy to a world
+    pub fn apply_to_world(&self, world: &mut hecs::World, label_to_entity: &HashMap<Label, hecs::Entity>) {
+        for (child_label, parent_label) in &self.parent_map {
+            if let (Some(&child_entity), Some(&parent_entity)) = (
+                label_to_entity.get(child_label),
+                label_to_entity.get(parent_label),
+            ) {
+                Hierarchy::set_parent(world, child_entity, parent_entity);
+            }
+        }
+    }
+
     /// Set the parent of an entity
     pub fn set_parent(&mut self, child: Label, parent: Label) {
         if let Some(old_parent) = self.parent_map.get(&child) {
diff --git a/eucalyptus-core/src/scene.rs b/eucalyptus-core/src/scene.rs
index 71a7b81..a4d0938 100644
--- a/eucalyptus-core/src/scene.rs
+++ b/eucalyptus-core/src/scene.rs
@@ -14,8 +14,10 @@ use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterato
 use dropbear_engine::asset::ASSET_REGISTRY;
 use dropbear_engine::model::Model;
 use dropbear_engine::utils::ResourceReferenceType;
+use dropbear_traits::component_registry::ComponentRegistry;
+use dropbear_traits::SerializableComponent;
 use crate::camera::{CameraComponent, CameraType};
-use crate::hierarchy::{Parent, SceneHierarchy};
+use crate::hierarchy::{Children, Parent, SceneHierarchy};
 use crate::states::{CameraConfig, Label, LightConfig, ModelProperties, ScriptComponent, SerializedMeshRenderer, WorldLoadingStatus, PROJECT};
 use crate::utils::ResolveReference;
 
@@ -23,13 +25,36 @@ use crate::utils::ResolveReference;
 pub struct SceneEntity {
     #[serde(default)]
     pub label: Label,
+
     #[serde(default)]
-    pub components: Vec<Box<dyn dropbear_traits::SerializableComponent>>,
+    pub components: Vec<Box<dyn SerializableComponent>>,
 
     #[serde(skip)]
     pub entity_id: Option<hecs::Entity>,
 }
 
+impl SceneEntity {
+    pub fn from_world(
+        world: &hecs::World,
+        entity: hecs::Entity,
+        registry: &ComponentRegistry,
+    ) -> Option<Self> {
+        let label= if let Ok(mut q) = world.query_one::<&Label>(entity) && let Some(label) = q.get() {
+            label.clone()
+        } else {
+            return None;
+        };
+
+        let components = registry.extract_all_components(world, entity);
+
+        Some(Self {
+            label,
+            components,
+            entity_id: Some(entity),
+        })
+    }
+}
+
 #[derive(Default, Debug, Serialize, Deserialize, Clone)]
 pub struct SceneSettings { /* *crickets* */ }
 
@@ -74,6 +99,7 @@ impl SceneConfig {
         component: Box<dyn dropbear_traits::SerializableComponent>,
         builder: &mut hecs::EntityBuilder,
         graphics: Arc<SharedGraphicsContext>,
+        registry: Option<&ComponentRegistry>,
         label: &str,
     ) -> anyhow::Result<()> {
         if let Some(transform) = component.as_any().downcast_ref::<EntityTransform>() {
@@ -181,6 +207,14 @@ impl SceneConfig {
             builder.add(script.clone());
         } else if component.as_any().downcast_ref::<Parent>().is_some() {
             log::debug!("Skipping Parent component for '{}' - will be rebuilt from hierarchy_map", label);
+        } else if let Some(registry) = registry {
+            if !registry.deserialize_into_builder(component.as_ref(), builder)? {
+                log::warn!(
+                    "Unknown component type '{}' for entity '{}' - skipping",
+                    component.type_name(),
+                    label
+                );
+            }
         } else {
             log::warn!(
                 "Unknown component type '{}' for entity '{}' - skipping",
@@ -219,6 +253,7 @@ impl SceneConfig {
         &self,
         world: &mut hecs::World,
         graphics: Arc<SharedGraphicsContext>,
+        registry: Option<&ComponentRegistry>,
         progress_sender: Option<UnboundedSender<WorldLoadingStatus>>,
     ) -> anyhow::Result<hecs::Entity> {
         if let Some(ref s) = progress_sender {
@@ -285,7 +320,14 @@ impl SceneConfig {
                     has_entity_transform = true;
                 }
                 
-                Self::load_component(component, &mut builder, graphics.clone(), &label_for_logs).await?;
+                Self::load_component(
+                    component,
+                    &mut builder,
+                    graphics.clone(),
+                    registry,
+                    &label_for_logs,
+                )
+                .await?;
             }
 
             let entity = world.spawn(builder.build());
@@ -356,11 +398,11 @@ impl SceneConfig {
 
             let mut local_insert_one: Option<hecs::Entity> = None;
 
-            match world.query_one::<&mut Parent>(parent_entity) {
+            match world.query_one::<&mut Children>(parent_entity) {
                 Ok(mut parent_query) => {
-                    if let Some(parent_component) = parent_query.get() {
-                        parent_component.clear();
-                        parent_component
+                    if let Some(child_component) = parent_query.get() {
+                        child_component.clear();
+                        child_component
                             .children_mut()
                             .extend(resolved_children.iter().copied());
                     } else {
@@ -378,7 +420,7 @@ impl SceneConfig {
             }
 
             if let Some(parent_entity) = local_insert_one
-                && let Err(e) = world.insert_one(parent_entity, Parent::new(resolved_children))
+                && let Err(e) = world.insert_one(parent_entity, Children::new(resolved_children))
             {
                 log::error!(
                     "Failed to attach Parent component to entity {:?}: {}",
@@ -401,6 +443,34 @@ impl SceneConfig {
 
             if !has_light {
                 log::info!("No lights in scene, spawning default light");
+
+                let legacy_lights: Vec<hecs::Entity> = world
+                    .query::<&Label>()
+                    .iter()
+                    .filter_map(|(entity, label)| {
+                        if label.as_str() == "Default Light" {
+                            Some(entity)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect();
+
+                for entity in legacy_lights {
+                    if let Err(err) = world.despawn(entity) {
+                        log::warn!(
+                            "Failed to remove legacy 'Default Light' entity {:?}: {}",
+                            entity,
+                            err
+                        );
+                    } else {
+                        log::debug!(
+                            "Removed legacy 'Default Light' placeholder entity {:?}",
+                            entity
+                        );
+                    }
+                }
+
                 if let Some(ref s) = progress_sender {
                     let _ = s.send(WorldLoadingStatus::LoadingEntity {
                         index: 0,
@@ -458,6 +528,34 @@ impl SceneConfig {
                     Ok(camera_entity)
                 } else {
                     log::info!("No debug camera found, creating viewport camera for editor");
+
+                    let legacy_cameras: Vec<hecs::Entity> = world
+                        .query::<&Label>()
+                        .iter()
+                        .filter_map(|(entity, label)| {
+                            if label.as_str() == "Viewport Camera" {
+                                Some(entity)
+                            } else {
+                                None
+                            }
+                        })
+                        .collect();
+
+                    for entity in legacy_cameras {
+                        if let Err(err) = world.despawn(entity) {
+                            log::warn!(
+                                "Failed to remove legacy 'Viewport Camera' entity {:?}: {}",
+                                entity,
+                                err
+                            );
+                        } else {
+                            log::debug!(
+                                "Removed legacy 'Viewport Camera' placeholder entity {:?}",
+                                entity
+                            );
+                        }
+                    }
+
                     if let Some(ref s) = progress_sender {
                         let _ = s.send(WorldLoadingStatus::LoadingEntity {
                             index: 0,
@@ -467,7 +565,8 @@ impl SceneConfig {
                     }
                     let camera = Camera::predetermined(graphics.clone(), Some("Viewport Camera"));
                     let component = crate::camera::DebugCamera::new();
-                    let camera_entity = { world.spawn((camera, component)) };
+                    let label = Label::new("Viewport Camera");
+                    let camera_entity = { world.spawn((label, camera, component)) };
                     Ok(camera_entity)
                 }
             }
diff --git a/eucalyptus-core/src/scripting/error.rs b/eucalyptus-core/src/scripting/error.rs
index 274ac35..bdc22b3 100644
--- a/eucalyptus-core/src/scripting/error.rs
+++ b/eucalyptus-core/src/scripting/error.rs
@@ -3,6 +3,7 @@ use crate::scripting::ScriptTarget;
 
 /// A trait implemented by the different script types ([ScriptTarget::JVM], [ScriptTarget::Native]) which allow
 /// for populating the last error and getting the contents of the last error.
+#[allow(dead_code)]
 pub trait LastErrorMessage {
     /// Fetches the last error message.
     ///
diff --git a/eucalyptus-core/src/scripting/native.rs b/eucalyptus-core/src/scripting/native.rs
index ef3d249..2703aa2 100644
--- a/eucalyptus-core/src/scripting/native.rs
+++ b/eucalyptus-core/src/scripting/native.rs
@@ -26,7 +26,9 @@ pub struct NativeLibrary {
     destroy_tagged_fn: Symbol<'static, DestroyTagged>,
     
     // err msg
+    #[allow(dead_code)]
     pub(crate) get_last_err_msg_fn: Symbol<'static, sig::GetLastErrorMessage>,
+    #[allow(dead_code)]
     pub(crate) set_last_err_msg_fn: Symbol<'static, sig::SetLastErrorMessage>,
 }
 
diff --git a/eucalyptus-editor/src/editor/dock.rs b/eucalyptus-editor/src/editor/dock.rs
index 478db68..d20e172 100644
--- a/eucalyptus-editor/src/editor/dock.rs
+++ b/eucalyptus-editor/src/editor/dock.rs
@@ -18,6 +18,7 @@ use dropbear_engine::{
 };
 use egui::{self, CollapsingHeader, Margin, RichText};
 use egui_dock::TabViewer;
+use egui_ltreeview::{NodeBuilder, TreeViewBuilder};
 use indexmap::Equivalent;
 use eucalyptus_core::states::{Label};
 use log;
@@ -37,7 +38,9 @@ pub struct EditorTabViewer<'a> {
     pub editor_mode: &'a mut EditorState,
     pub active_camera: &'a mut Arc<Mutex<Option<Entity>>>,
     pub plugin_registry: &'a mut PluginRegistry,
+    pub component_registry: &'a ComponentRegistry,
     pub build_logs: &'a mut Vec<String>,
+
     // "wah wah its unsafe, its using raw pointers" shut the fuck up if it breaks i will know
     pub editor: *mut Editor,
 }
@@ -253,12 +256,99 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                 }
             }
             EditorTab::ModelEntityList => {
-                // todo: use egui_ltreeview to create a component graph
-                // egui_ltreeview::TreeView::new(egui::Id::new("model_entity_list")).show(ui, |builder| {
-                //     builder.dir()
-                //     // entity is entity.id()
-                //     // component is entity.id() * -100 *
-                // });
+                let (_response, action) = egui_ltreeview::TreeView::new(egui::Id::new("model_entity_list"))
+                    .show(ui, |builder| {
+                    let current_scene_name = {PROJECT.read().last_opened_scene.clone().unwrap_or("Scene".to_string())};
+                    builder.node(
+                        NodeBuilder::dir(u64::MAX)
+                            .label(format!("Scene: {}", current_scene_name))
+                            .context_menu(|ui| {
+                                if ui.button("New Empty Entity").clicked() {
+                                    self.world.spawn((Label::new("Blank Entity"), ));
+                                }
+                            })
+                    );
+                    // the root scene must be the biggest number possible to remove any ambiguity
+
+                    fn add_entity_to_tree(
+                        builder: &mut TreeViewBuilder<u64>,
+                        entity: Entity,
+                        world: &World,
+                        registry: &ComponentRegistry,
+                    ) -> anyhow::Result<()> {
+                        let entity_id = entity.id() as u64;
+                        let label = if let Ok(mut q) = world.query_one::<&Label>(entity) && let Some(label) = q.get() {
+                            label.clone()
+                        } else {
+                            anyhow::bail!("This entity [{}] is expected to contain Label", entity_id);
+                        };
+
+                        builder.node(
+                            NodeBuilder::dir(entity_id)
+                                .label(label.as_str())
+                                .context_menu(|ui| {
+                                    ui.menu_button("New", |ui| {
+                                        if ui.button("Child").clicked() {
+                                            // todo
+                                        }
+                                    });
+                                })
+                        );
+
+                        let components = registry.extract_all_components(world, entity);
+
+                        for (i, component) in components.iter().enumerate() {
+                            // todo: make this a uuid instead
+                            let component_id = entity_id.wrapping_mul(1_000_000).wrapping_add(i as u64);
+
+                            let component_name = component.type_name();
+
+                            builder.node(
+                                NodeBuilder::leaf(component_id)
+                                    .label(component_name)
+                                    .context_menu(|ui| {
+                                        if ui.button("Remove Component").clicked() {
+                                            // complete this
+                                        }
+                                    })
+                            );
+                        }
+
+                        if let Ok(children) = world.get::<&Children>(entity) {
+                            for &child in children.children() {
+                                if let Err(e) = add_entity_to_tree(builder, child, world, registry) {
+                                    log_once::error_once!("Failed to add child entity to tree, skipping: {}", e);
+                                    continue;
+                                }
+                            }
+                        }
+
+                        builder.close_dir();
+                        Ok(())
+                    }
+
+                    for (entity, ()) in self.world.query::<()>()
+                        .without::<&Parent>()
+                        .iter()
+                    {
+                        if let Err(e) = add_entity_to_tree(builder, entity, &self.world, &self.component_registry) {
+                            log_once::error_once!("Failed to add child entity to tree, skipping: {}", e);
+                        }
+                    }
+
+                    builder.close_dir();
+                });
+
+                for i in action {
+                    match i {
+                        egui_ltreeview::Action::SetSelected(_items) => {},
+                        egui_ltreeview::Action::Move(_drag_and_drop) => {},
+                        egui_ltreeview::Action::Drag(_drag_and_drop) => {},
+                        egui_ltreeview::Action::Activate(_activate) => {},
+                        egui_ltreeview::Action::DragExternal(_drag_and_drop_external) => {},
+                        egui_ltreeview::Action::MoveExternal(_drag_and_drop_external) => {},
+                    }
+                }
             }
             EditorTab::AssetViewer => {
                 // // todo: use egui_ltreeview to create a asset viewer
@@ -592,83 +682,6 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
     }
 }
 
-pub(crate) fn import_object() -> anyhow::Result<()> {
-    let model_ext = vec!["glb", "fbx", "obj"];
-    let texture_ext = vec!["png"];
-
-    let files = rfd::FileDialog::new()
-        .add_filter("All Files", &["*"])
-        .add_filter("Model", &model_ext)
-        .add_filter("Texture", &texture_ext)
-        .pick_files();
-    if let Some(files) = files {
-        for file in files {
-            let ext = file.extension().unwrap().to_str().unwrap();
-            let mut copied = false;
-            for mde in model_ext.iter() {
-                if ext.contains(mde) {
-                    // copy over to models folder
-                    {
-                        let project = PROJECT.read();
-                        let models_dir = project
-                            .project_path
-                            .clone()
-                            .join("resources")
-                            .join("models");
-                        if !models_dir.exists() {
-                            std::fs::create_dir_all(&models_dir)?;
-                        }
-                        let dest = models_dir.join(file.file_name().unwrap());
-                        std::fs::copy(&file, &dest)?;
-                        log::info!("Copied model file to {:?}", dest);
-                        copied = true;
-                    }
-                }
-            }
-            for tex in texture_ext.iter() {
-                if ext.contains(tex) {
-                    // copy over to textures folder
-                    {
-                        let project = PROJECT.read();
-                        let textures_dir = project
-                            .project_path
-                            .clone()
-                            .join("resources")
-                            .join("textures");
-                        if !textures_dir.exists() {
-                            std::fs::create_dir_all(&textures_dir)?;
-                        }
-                        let dest = textures_dir.join(file.file_name().unwrap());
-                        std::fs::copy(&file, &dest)?;
-                        log::info!("Copied texture file to {:?}", dest);
-                        copied = true;
-                    }
-                }
-            }
-
-            if !copied {
-                {
-                    let project = PROJECT.read();
-                    // everything else copies over to resources root dir
-                    let resources_dir = project.project_path.clone().join("resources");
-                    if !resources_dir.exists() {
-                        std::fs::create_dir_all(&resources_dir)?;
-                    }
-                    let dest = resources_dir.join(file.file_name().unwrap());
-                    std::fs::copy(&file, &dest)?;
-                    log::info!("Copied other resource file to {:?}", dest);
-                }
-            }
-        }
-        // save it all to ensure the eucc recognises it
-        let mut proj = PROJECT.write();
-        proj.write_to_all()?;
-        Ok(())
-    } else {
-        Err(anyhow::anyhow!("File dialogue returned None"))
-    }
-}
-
 #[derive(Debug, Clone, Copy)]
 #[allow(dead_code)]
 // todo: provide a purpose to RemoveComponent
diff --git a/eucalyptus-editor/src/editor/mod.rs b/eucalyptus-editor/src/editor/mod.rs
index eeb4539..9fbccb7 100644
--- a/eucalyptus-editor/src/editor/mod.rs
+++ b/eucalyptus-editor/src/editor/mod.rs
@@ -19,14 +19,14 @@ use dropbear_engine::{
     entity::{MeshRenderer, Transform},
     future::FutureHandle,
     graphics::{RenderContext, SharedGraphicsContext},
-    lighting::{Light, LightManager},
+    lighting::{LightManager},
     model::{ModelId, MODEL_CACHE},
     scene::SceneCommand,
 };
 use egui::{self, Context};
 use egui_dock::{DockArea, DockState, NodeIndex, Style};
 use eucalyptus_core::APP_INFO;
-use eucalyptus_core::hierarchy::{Parent, SceneHierarchy};
+use eucalyptus_core::hierarchy::{Children, Parent, SceneHierarchy};
 use eucalyptus_core::states::{Label, SerializedMeshRenderer};
 use eucalyptus_core::{
     camera::{CameraComponent, CameraType, DebugCamera},
@@ -62,8 +62,8 @@ use wgpu::{Color, Extent3d, RenderPipeline};
 use winit::window::CursorGrabMode;
 use winit::{keyboard::KeyCode, window::Window};
 use dropbear_engine::entity::EntityTransform;
-use dropbear_engine::lighting::LightComponent;
 use eucalyptus_core::scene::{SceneConfig, SceneEntity};
+use eucalyptus_core::traits::component_registry::ComponentRegistry;
 use eucalyptus_core::traits::SerializableComponent;
 
 pub struct Editor {
@@ -144,6 +144,9 @@ pub struct Editor {
     // about
     show_about: bool,
     nerd_stats: NerdStats,
+
+    // component registry
+    component_registry: Arc<ComponentRegistry>,
 }
 
 impl Editor {
@@ -182,7 +185,47 @@ impl Editor {
             }
         });
 
-        let plugin_registry = PluginRegistry::new();
+        let mut plugin_registry = PluginRegistry::new();
+        let mut component_registry = ComponentRegistry::new();
+
+        fn register_components(plugin_registry: &mut PluginRegistry, component_registry: &mut ComponentRegistry) {
+            component_registry.register::<EntityTransform>();
+            component_registry.register::<ModelProperties>();
+            component_registry.register::<CameraConfig>();
+            component_registry.register::<LightConfig>();
+            component_registry.register::<ScriptComponent>();
+
+            component_registry.register_converter::<MeshRenderer, SerializedMeshRenderer, _>(
+                |renderer| {
+                    SerializedMeshRenderer {
+                        handle: renderer.handle().path.clone(),
+                        material_override: renderer.material_overrides().to_vec(),
+                    }
+                }
+            );
+
+            // register plugin defined structs
+            if let Err(e) = plugin_registry.load_plugins() {
+                fatal!("Failed to load plugins: {}", e);
+                return;
+            }
+
+            for p in plugin_registry.list_plugins() {
+                log::info!("Plugin {} has been loaded", p.display_name);
+            }
+
+            log::info!("Total plugins added: {}", plugin_registry.plugins.len());
+
+            for plugin in plugin_registry.list_plugins() {
+                if let Some(p) = plugin_registry.get_mut(&plugin.display_name) {
+                    p.register_component(component_registry);
+                    log::info!("Components for plugin [{}] has been registered to component registry", plugin.display_name);
+                }
+            }
+        }
+
+        register_components(&mut plugin_registry, &mut component_registry);
+        let component_registry = Arc::new(component_registry);
 
         Ok(Self {
             scene_command: SceneCommand::None,
@@ -235,6 +278,7 @@ impl Editor {
             pending_scene_creation: None,
             show_about: false,
             nerd_stats: NerdStats::default(),
+            component_registry,
         })
     }
 
@@ -272,66 +316,24 @@ impl Editor {
             .iter_mut()
             .find(|scene| scene.scene_name == target_scene_name)
             .ok_or_else(|| anyhow::anyhow!("Active scene '{}' is not loaded", target_scene_name))?;
-        
+
         scene.entities.clear();
         scene.hierarchy_map = SceneHierarchy::new();
+        log::debug!("Reset internal hierarchy map for scene {}", scene.scene_name);
 
-        let labels = self.world.query::<&Label>().iter().map(|(e, l)| (e, l.clone())).collect::<Vec<_>>();
+        let labels = self.world.query::<&Label>().iter()
+            .map(|(e, l)| (e, l.clone()))
+            .collect::<Vec<_>>();
 
         for (id, label) in labels {
             let entity_label = label.clone();
-            let mut components: Vec<Box<dyn SerializableComponent>> = Vec::new();
 
-            if let Ok(transform) = self.world.get::<&EntityTransform>(id) {
-                components.push(Box::new(*transform));
-            }
+            let components = self.component_registry.extract_all_components(&self.world, id);
 
-            if let Ok(renderer) = self.world.get::<&MeshRenderer>(id) {
-                let serialized = SerializedMeshRenderer {
-                    handle: renderer.handle().path.clone(),
-                    material_override: renderer.material_overrides().to_vec(),
-                };
-                components.push(Box::new(serialized));
-            }
-
-            if let Ok(mut q) = self.world.query_one::<&ModelProperties>(id) && let Some(properties) = q.get() {
-                components.push(Box::new(properties.clone()));
-            }
-
-            if let Ok(mut camera_query) = self.world.query_one::<(&Camera, &CameraComponent)>(id)
-                && let Some((camera, component)) = camera_query.get()
-            {
-                let camera_config = CameraConfig::from_ecs_camera(camera, component);
-                components.push(Box::new(camera_config));
-            }
-
-            if let Ok(mut light_query) = self.world.query_one::<(&Light, &LightComponent, &EntityTransform)>(id) {
-                if let Some((light, light_component, entity_transform)) = light_query.get() {
-                    let light_config = LightConfig {
-                        label: light.cube_model.label.to_string(),
-                        transform: entity_transform.sync(),
-                        light_component: light_component.clone(),
-                        enabled: light_component.enabled,
-                        entity_id: Some(id),
-                    };
-                    components.push(Box::new(light_config));
-                }
-            }
-
-            if let Ok(mut q) = self.world.query_one::<&ScriptComponent>(id) && let Some(script) = q.get() {
-                components.push(Box::new(script.clone()));
-            }
-
-            let mut parent_component = None;
-
-            if let Ok(comp) = self.world.query_one_mut::<&Parent>(id) {
-                parent_component = Some(comp.clone());
-            }
-
-            if let Some(parent_component) = parent_component {
-                for &child_entity in parent_component.children() {
-                    if let Ok(child_label) = self.world.query_one_mut::<&Label>(child_entity) {
-                        scene.hierarchy_map.set_parent(child_label.clone(), entity_label.clone());
+            if let Ok(children_comp) = self.world.get::<&Children>(id) {
+                for &child_entity in children_comp.children() {
+                    if let Ok(child_label) = self.world.get::<&Label>(child_entity) {
+                        scene.hierarchy_map.set_parent(Label::new(child_label.as_str()), entity_label.clone());
                     } else {
                         log::warn!(
                             "Unable to resolve child entity {:?} for parent '{}' when saving scene",
@@ -480,7 +482,6 @@ impl Editor {
     /// It uses an unbounded sender to send messages back to the receiver so it can
     /// be used within threads.
     pub async fn load_project_config(
-        // &mut self,
         graphics: Arc<SharedGraphicsContext>,
         sender: Option<UnboundedSender<WorldLoadingStatus>>,
         world: &mut World,
@@ -488,6 +489,7 @@ impl Editor {
         active_camera: Arc<Mutex<Option<hecs::Entity>>>,
         project_path: Arc<Mutex<Option<PathBuf>>>,
         dock_state: Arc<Mutex<DockState<EditorTab>>>,
+        component_registry: Arc<ComponentRegistry>,
     ) -> anyhow::Result<()> {
         {
             let config = PROJECT.read();
@@ -521,7 +523,12 @@ impl Editor {
         {
             if let Some(first_scene) = first_scene_opt {
                 let cam = first_scene
-                    .load_into_world(world, graphics, sender.clone())
+                    .load_into_world(
+                        world,
+                        graphics,
+                        Some(component_registry.as_ref()),
+                        sender.clone(),
+                    )
                     .await?;
                 let mut a_c = active_camera.lock();
                 *a_c = Some(cam);
@@ -661,6 +668,7 @@ impl Editor {
         let graphics_shared = graphics.shared.clone();
         let active_camera = self.active_camera.clone();
         let scene_name = scene.scene_name.clone();
+        let component_registry_clone = self.component_registry.clone();
 
         let handle = graphics.shared.future_queue.push(async move {
             let mut temp_world = World::new();
@@ -669,6 +677,7 @@ impl Editor {
                 .load_into_world(
                     &mut temp_world,
                     graphics_shared.clone(),
+                    Some(component_registry_clone.as_ref()),
                     Some(progress_sender.clone()),
                 )
                 .await;
@@ -1031,6 +1040,7 @@ impl Editor {
                         plugin_registry: &mut self.plugin_registry,
                         editor: editor_ptr,
                         build_logs: &mut self.build_logs,
+                        component_registry: &self.component_registry
                     },
                 );
         });
diff --git a/eucalyptus-editor/src/editor/scene.rs b/eucalyptus-editor/src/editor/scene.rs
index c7fd9f2..7a01e98 100644
--- a/eucalyptus-editor/src/editor/scene.rs
+++ b/eucalyptus-editor/src/editor/scene.rs
@@ -48,6 +48,8 @@ impl Scene for Editor {
         let dock_state_shared = Arc::new(Mutex::new(self.dock_state.clone()));
         let dock_state_for_loading = dock_state_shared.clone();
 
+        let component_registry = self.component_registry.clone();
+
         let handle = graphics.shared.future_queue.push(async move {
             let mut temp_world = World::new();
             if let Err(e) = Self::load_project_config(
@@ -58,6 +60,7 @@ impl Scene for Editor {
                 active_camera_clone,
                 project_path_clone,
                 dock_state_for_loading,
+                component_registry
             )
             .await
             {
@@ -163,14 +166,6 @@ impl Scene for Editor {
             }
         }
 
-        if !self.plugin_registry.plugins_loaded {
-            if let Err(e) = self.plugin_registry.load_plugins() {
-                fatal!("Failed to load plugins: {}", e);
-            } else {
-                log::info!("Plugins loaded");
-            }
-        }
-
         let cache_mutex_ptr = std::sync::LazyLock::force(&MODEL_CACHE) as *const _;
         ASSET_REGISTRY.add_pointer(PointerKind::Const("model_cache"), cache_mutex_ptr as usize);
 
diff --git a/eucalyptus-editor/src/main.rs b/eucalyptus-editor/src/main.rs
index f764258..c96bd03 100644
--- a/eucalyptus-editor/src/main.rs
+++ b/eucalyptus-editor/src/main.rs
@@ -93,6 +93,7 @@ async fn main() -> anyhow::Result<()> {
                 LevelFilter::Debug,
             )
             .filter(Some("eucalyptus_core"), LevelFilter::Debug)
+            .filter(Some("dropbear_traits"), LevelFilter::Debug)
             .init();
         log::info!("Initialised logger");
     }
diff --git a/eucalyptus-editor/src/plugin.rs b/eucalyptus-editor/src/plugin.rs
index 930a1ed..c761934 100644
--- a/eucalyptus-editor/src/plugin.rs
+++ b/eucalyptus-editor/src/plugin.rs
@@ -3,6 +3,7 @@ use app_dirs2::AppDataType;
 use egui::Ui;
 use eucalyptus_core::APP_INFO;
 use eucalyptus_core::states::PluginInfo;
+use eucalyptus_core::traits::component_registry::ComponentRegistry;
 use indexmap::IndexMap;
 use libloading as lib;
 use std::fs::ReadDir;
@@ -14,8 +15,10 @@ pub trait EditorPlugin: Send + Sync {
     fn display_name(&self) -> &str;
     fn ui(&mut self, ui: &mut Ui, editor: &mut Editor);
     fn tab_title(&self) -> &str;
-    fn context_menu(&mut self, _ui: &mut Ui, _editor: &mut Editor) { /* No context */
-    }
+    
+    /// Allows you to register the struct [Self] as a component to the component registry. 
+    /// This will then be listed as an option for a potential component a user could add. 
+    fn register_component(&mut self, registry: &mut ComponentRegistry);
 }
 
 pub type PluginConstructor = fn() -> Box<dyn EditorPlugin>;