kitgit

tirbofish/dropbear · diff

a0917a8 · Thribhu K

wip: finally got eucalyptus-core all smoothened out, need to fix up the editor now.

Unverified

diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
index 135b58a..26634a0 100644
--- a/crates/dropbear-engine/src/animation.rs
+++ b/crates/dropbear-engine/src/animation.rs
@@ -1,13 +1,14 @@
-use dropbear_traits::SerializableComponent;
+use dropbear_traits::{Component, ComponentDescriptor, ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent};
 use std::collections::HashMap;
 use std::sync::Arc;
+use egui::{CollapsingHeader, Ui};
 use glam::Mat4;
 use wgpu::util::DeviceExt;
-use dropbear_macro::SerializableComponent;
 use crate::graphics::SharedGraphicsContext;
 use crate::model::{AnimationInterpolation, ChannelValues, Model, NodeTransform};
+use std::any::Any;
 
-#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, SerializableComponent)]
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 pub struct AnimationComponent {
     #[serde(default)]
     pub active_animation_index: Option<usize>,
@@ -31,6 +32,49 @@ pub struct AnimationComponent {
     pub bind_group: Option<wgpu::BindGroup>,
 }
 
+impl Component for AnimationComponent {
+    type Serialized = AnimationComponent;
+
+    fn static_descriptor() -> ComponentDescriptor {
+        ComponentDescriptor {
+            fqtn: "dropbear_engine::animation::AnimationComponent".to_string(),
+            type_name: "AnimationComponent".to_string(),
+            category: Some("Animation".to_string()),
+            description: Some("A component that allows playback of a component".to_string()),
+        }
+    }
+
+    fn deserialize(serialized: &Self::Serialized) -> Self {
+        serialized.clone()
+    }
+
+    fn serialize(&self) -> Self::Serialized {
+        self.clone()
+    }
+
+    fn inspect(&mut self, ui: &mut Ui) {
+        CollapsingHeader::new("Animation").default_open(true).show(ui, |ui| {
+            ui.label("Active animation:");
+            let mut enabled = self.active_animation_index.is_some();
+            let mut value = self.active_animation_index.unwrap_or(0);
+
+            ui.horizontal(|ui| {
+                if ui.checkbox(&mut enabled, "Enable").changed() {
+                    self.active_animation_index = if enabled { Some(value) } else { None };
+                }
+
+                ui.add_enabled(enabled, egui::DragValue::new(&mut value));
+
+                if enabled && self.active_animation_index != Some(value) {
+                    self.active_animation_index = Some(value);
+                }
+            });
+
+            // todo: complete some more
+        });
+    }
+}
+
 impl Default for AnimationComponent {
     fn default() -> Self {
         Self {
@@ -47,6 +91,26 @@ impl Default for AnimationComponent {
     }
 }
 
+#[typetag::serde]
+impl SerializableComponent for AnimationComponent {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn clone_box(&self) -> Box<dyn SerializableComponent> {
+        Box::new(self.clone())
+    }
+
+    fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture {
+        let value = self.clone();
+        Box::pin(async move {
+            let insert: Box<dyn dropbear_traits::ComponentInsert> =
+                Box::new(InsertBundle((value,)));
+            Ok(insert)
+        })
+    }
+}
+
 impl AnimationComponent {
     pub fn new() -> Self {
         Self::default()
@@ -77,7 +141,6 @@ impl AnimationComponent {
             let count = channel.times.len();
             if count == 0 { continue; }
 
-            // Handle out of bounds / single keyframe
             if count == 1 || self.time <= channel.times[0] {
                 Self::apply_single_keyframe(channel, 0, &mut self.local_pose, model);
                 continue;
diff --git a/crates/dropbear-engine/src/entity.rs b/crates/dropbear-engine/src/entity.rs
index 61621c0..2b96893 100644
--- a/crates/dropbear-engine/src/entity.rs
+++ b/crates/dropbear-engine/src/entity.rs
@@ -1,4 +1,3 @@
-use dropbear_traits::SerializableComponent;
 use glam::{DMat4, DQuat, DVec3, Mat4, Quat, Vec3};
 use parking_lot::Mutex;
 use serde::{Deserialize, Serialize};
@@ -11,16 +10,18 @@ use std::{
 use crate::{
     asset::{ASSET_REGISTRY, AssetRegistry},
     graphics::{Instance, SharedGraphicsContext},
-    model::{Model},
-    utils::ResourceReference,
+    model::Model,
     texture::Texture,
+    utils::{ResourceReference, ResourceReferenceType, EUCA_SCHEME},
 };
 use anyhow::anyhow;
-use dropbear_macro::SerializableComponent;
+use egui::{CollapsingHeader, Ui};
+use dropbear_traits::{Component, ComponentDescriptor, ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent};
+use std::any::Any;
 use crate::asset::Handle;
 
 /// A type of transform that is attached to all entities. It contains the local and world transforms.
-#[derive(Default, Debug, Deserialize, Serialize, Copy, PartialEq, Clone, SerializableComponent)]
+#[derive(Default, Debug, Deserialize, Serialize, Copy, PartialEq, Clone)]
 pub struct EntityTransform {
     local: Transform,
     world: Transform,
@@ -77,6 +78,71 @@ impl EntityTransform {
     }
 }
 
+impl Component for EntityTransform {
+    type Serialized = EntityTransform;
+
+    fn static_descriptor() -> ComponentDescriptor {
+        ComponentDescriptor {
+            fqtn: "dropbear_engine::entity::EntityTransform".to_string(),
+            type_name: "EntityTransform".to_string(),
+            category: Some("Transform".to_string()),
+            description: Some("A component that allows the entity to be transformed both locally and globally".to_string()),
+        }
+    }
+
+    fn deserialize(serialized: &Self::Serialized) -> Self {
+        serialized.clone()
+    }
+
+    fn serialize(&self) -> Self::Serialized {
+        self.clone()
+    }
+
+    fn inspect(&mut self, ui: &mut Ui) {
+        CollapsingHeader::new("Entity Transform")
+            .default_open(true)
+            .show(ui, |ui| {
+                ui.set_min_width(300.0);
+
+                ui.group(|ui| {
+                    ui.strong("Local Transform");
+                    ui.add_space(4.0);
+
+                    self.local.inspect(ui);
+                });
+
+                ui.add_space(8.0);
+
+                ui.group(|ui| {
+                    ui.strong("World Transform");
+                    ui.add_space(4.0);
+
+                    self.world.inspect(ui);
+                });
+            });
+    }
+}
+
+#[typetag::serde]
+impl SerializableComponent for EntityTransform {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn clone_box(&self) -> Box<dyn SerializableComponent> {
+        Box::new(*self)
+    }
+
+    fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture {
+        let value = *self;
+        Box::pin(async move {
+            let insert: Box<dyn dropbear_traits::ComponentInsert> =
+                Box::new(InsertBundle((value,)));
+            Ok(insert)
+        })
+    }
+}
+
 /// A type that represents a position, rotation and scale of an entity
 ///
 /// This type is the most primitive model, as it implements most traits.
@@ -159,6 +225,118 @@ impl Transform {
     pub fn scale(&mut self, scale: DVec3) {
         self.scale *= scale;
     }
+
+    fn inspect(&mut self, ui: &mut Ui) {
+        ui.horizontal(|ui| {
+            ui.label("Position:");
+        });
+        ui.horizontal(|ui| {
+            ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:");
+            ui.add(egui::DragValue::new(&mut self.position.x)
+                .speed(0.1)
+                .fixed_decimals(2));
+
+            ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:");
+            ui.add(egui::DragValue::new(&mut self.position.y)
+                .speed(0.1)
+                .fixed_decimals(2));
+
+            ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:");
+            ui.add(egui::DragValue::new(&mut self.position.z)
+                .speed(0.1)
+                .fixed_decimals(2));
+        });
+
+        ui.add_space(4.0);
+
+        ui.horizontal(|ui| {
+            ui.label("Rotation:");
+        });
+
+        let (mut x, mut y, mut z) = self.rotation.to_euler(glam::EulerRot::XYZ);
+        x = x.to_degrees();
+        y = y.to_degrees();
+        z = z.to_degrees();
+
+        let changed = ui.horizontal(|ui| {
+            ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:");
+            let cx = ui.add(egui::DragValue::new(&mut x)
+                .speed(1.0)
+                .suffix("°")
+                .fixed_decimals(1)).changed();
+
+            ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:");
+            let cy = ui.add(egui::DragValue::new(&mut y)
+                .speed(1.0)
+                .suffix("°")
+                .fixed_decimals(1)).changed();
+
+            ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:");
+            let cz = ui.add(egui::DragValue::new(&mut z)
+                .speed(1.0)
+                .suffix("°")
+                .fixed_decimals(1)).changed();
+
+            cx || cy || cz
+        }).inner;
+
+        if changed {
+            self.rotation = DQuat::from_euler(
+                glam::EulerRot::XYZ,
+                x.to_radians(),
+                y.to_radians(),
+                z.to_radians()
+            );
+        }
+
+        ui.add_space(4.0);
+
+        // Scale
+        ui.horizontal(|ui| {
+            ui.label("Scale:");
+        });
+
+        let mut uniform_scale = self.scale.x == self.scale.y
+            && self.scale.y == self.scale.z;
+
+        ui.horizontal(|ui| {
+            if ui.checkbox(&mut uniform_scale, "Uniform").changed() {
+                if uniform_scale {
+                    let avg = (self.scale.x + self.scale.y + self.scale.z) / 3.0;
+                    self.scale = DVec3::splat(avg);
+                }
+            }
+        });
+
+        if uniform_scale {
+            ui.horizontal(|ui| {
+                ui.label("XYZ:");
+                if ui.add(egui::DragValue::new(&mut self.scale.x)
+                    .speed(0.01)
+                    .fixed_decimals(3)).changed()
+                {
+                    self.scale = DVec3::splat(self.scale.x);
+                }
+            });
+        } else {
+            ui.horizontal(|ui| {
+                ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:");
+                ui.add(egui::DragValue::new(&mut self.scale.x)
+                    .speed(0.01)
+                    .fixed_decimals(3));
+
+                ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:");
+                ui.add(egui::DragValue::new(&mut self.scale.y)
+                    .speed(0.01)
+                    .fixed_decimals(3));
+
+                ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:");
+                ui.add(egui::DragValue::new(&mut self.scale.z)
+                    .speed(0.01)
+                    .fixed_decimals(3));
+            });
+        }
+    }
 }
 
 #[derive(Clone)]
@@ -170,7 +348,7 @@ pub struct MeshRenderer {
     import_scale: f32,
 
     handle: Handle<Model>,
-    instance: Instance,
+    pub instance: Instance,
     previous_matrix: DMat4,
     texture_override: Option<Handle<Texture>>,
 }
@@ -282,6 +460,81 @@ impl MeshRenderer {
     }
 }
 
+#[derive(Default, Debug, Clone, Serialize, Deserialize)]
+pub struct SerializedMeshRenderer {
+    pub handle: ResourceReference,
+    pub import_scale: Option<f32>,
+    pub texture_override: Option<ResourceReference>,
+}
+
+impl Component for MeshRenderer {
+    type Serialized = SerializedMeshRenderer;
+
+    fn static_descriptor() -> ComponentDescriptor {
+        ComponentDescriptor {
+            fqtn: "dropbear_engine::entity::MeshRenderer".to_string(),
+            type_name: "MeshRenderer".to_string(),
+            category: Some("Meshes".to_string()),
+            description: Some("Renders a 3D model".to_string()),
+        }
+    }
+
+    fn deserialize(serialized: &Self::Serialized) -> Self {
+        let handle = match serialized.handle.ref_type {
+            ResourceReferenceType::Unassigned { id } => Handle::new(id),
+            _ => Handle::NULL,
+        };
+
+        let mut renderer = MeshRenderer::from_handle(handle);
+        if let Some(scale) = serialized.import_scale {
+            renderer.set_import_scale(scale);
+        }
+
+        renderer
+    }
+
+    fn serialize(&self) -> Self::Serialized {
+        let handle = self.model();
+        let handle_ref = if handle.is_null() {
+            ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: handle.id })
+        } else {
+            let registry = ASSET_REGISTRY.read();
+            registry
+                .get_model(handle)
+                .map(|model| model.path.clone())
+                .unwrap_or_else(|| {
+                    ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: handle.id })
+                })
+        };
+
+        let texture_override = self.texture_override().map(|handle| {
+            let registry = ASSET_REGISTRY.read();
+            let label = registry.get_label_from_texture_handle(handle);
+            let reference = label.and_then(|value| {
+                if value.starts_with(EUCA_SCHEME) {
+                    Some(ResourceReference::from_reference(ResourceReferenceType::File(value)))
+                } else {
+                    None
+                }
+            });
+
+            reference.unwrap_or_else(|| {
+                ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: handle.id })
+            })
+        });
+
+        SerializedMeshRenderer {
+            handle: handle_ref,
+            import_scale: Some(self.import_scale()),
+            texture_override,
+        }
+    }
+
+    fn inspect(&mut self, ui: &mut Ui) {
+        let _ = ui;
+    }
+}
+
 #[repr(C)]
 #[derive(Debug, Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct ModelUniform {
diff --git a/crates/dropbear-engine/src/lighting.rs b/crates/dropbear-engine/src/lighting.rs
index ce76cd5..aef3065 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -1,4 +1,4 @@
-use crate::attenuation::{Attenuation, RANGE_50};
+use crate::attenuation::{Attenuation, RANGE_50, ATTENUATION_PRESETS};
 use crate::buffer::{ResizableBuffer, UniformBuffer};
 use crate::graphics::SharedGraphicsContext;
 use crate::pipelines::light_cube::InstanceInput;
@@ -6,12 +6,12 @@ use crate::{
     entity::Transform,
     model::Model,
 };
-use dropbear_macro::SerializableComponent;
-use dropbear_traits::SerializableComponent;
 use glam::{DMat4, DVec3};
 use std::fmt::{Display, Formatter};
 use std::sync::Arc;
+use egui::{CollapsingHeader, Ui};
 use wgpu::{BindGroup};
+use dropbear_traits::{Component, ComponentDescriptor};
 use crate::asset::{Handle, ASSET_REGISTRY};
 use crate::model::Material;
 use crate::procedural::{ProcObj, ProcedurallyGeneratedObject};
@@ -120,7 +120,7 @@ impl From<LightType> for u32 {
     }
 }
 
-#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, SerializableComponent)]
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 pub struct LightComponent {
     #[serde(default)]
     pub position: DVec3,          // point, spot
@@ -178,6 +178,112 @@ impl Default for LightComponent {
     }
 }
 
+impl Component for LightComponent {
+    type Serialized = LightComponent;
+
+    fn static_descriptor() -> ComponentDescriptor {
+        ComponentDescriptor {
+            fqtn: "dropbear_engine::lighting::LightComponent".to_string(),
+            type_name: "LightComponent".to_string(),
+            category: Some("Lighting".to_string()),
+            description: Some("Light parameters used by the renderer".to_string()),
+        }
+    }
+
+    fn deserialize(serialized: &Self::Serialized) -> Self {
+        serialized.clone()
+    }
+
+    fn serialize(&self) -> Self::Serialized {
+        self.clone()
+    }
+
+    fn inspect(&mut self, ui: &mut Ui) {
+        CollapsingHeader::new("Light")
+            .default_open(true)
+            .show(ui, |ui| {
+                egui::ComboBox::from_id_salt("light_type")
+                    .selected_text(self.light_type.to_string())
+                    .show_ui(ui, |ui| {
+                        ui.selectable_value(
+                            &mut self.light_type,
+                            LightType::Directional,
+                            "Directional",
+                        );
+                        ui.selectable_value(&mut self.light_type, LightType::Point, "Point");
+                        ui.selectable_value(&mut self.light_type, LightType::Spot, "Spot");
+                    });
+
+                ui.separator();
+
+                let mut colour = self.colour.as_vec3().to_array();
+                ui.horizontal(|ui| {
+                    ui.label("Colour");
+                    egui::color_picker::color_edit_button_rgb(ui, &mut colour);
+                });
+                self.colour = glam::Vec3::from_array(colour).as_dvec3();
+
+                ui.horizontal(|ui| {
+                    ui.label("Intensity");
+                    ui.add(egui::Slider::new(&mut self.intensity, 0.0..=10.0));
+                });
+
+                ui.horizontal(|ui| {
+                    ui.checkbox(&mut self.enabled, "Enabled");
+                    ui.checkbox(&mut self.visible, "Visible");
+                });
+
+                let is_point = matches!(self.light_type, LightType::Point);
+                let is_spot = matches!(self.light_type, LightType::Spot);
+
+                if is_point || is_spot {
+                    ui.separator();
+                    egui::ComboBox::from_id_salt("attenuation_range")
+                        .selected_text(format!("Range {}", self.attenuation.range))
+                        .show_ui(ui, |ui| {
+                            for (preset, label) in ATTENUATION_PRESETS {
+                                ui.selectable_value(&mut self.attenuation, *preset, *label);
+                            }
+                        });
+                }
+
+                if is_spot {
+                    ui.separator();
+                    ui.add(
+                        egui::Slider::new(&mut self.cutoff_angle, 1.0..=89.0)
+                            .text("Inner")
+                            .suffix("\u{00B0}")
+                            .step_by(0.1),
+                    );
+                    ui.add(
+                        egui::Slider::new(&mut self.outer_cutoff_angle, 1.0..=90.0)
+                            .text("Outer")
+                            .suffix("\u{00B0}")
+                            .step_by(0.1),
+                    );
+
+                    if self.outer_cutoff_angle <= self.cutoff_angle {
+                        self.outer_cutoff_angle = self.cutoff_angle + 1.0;
+                    }
+                }
+
+                ui.separator();
+                ui.label("Shadows");
+                ui.checkbox(&mut self.cast_shadows, "Cast Shadows");
+                ui.horizontal(|ui| {
+                    ui.label("Depth");
+                    ui.add(egui::DragValue::new(&mut self.depth.start).speed(0.1));
+                    ui.label("..");
+                    ui.add(egui::DragValue::new(&mut self.depth.end).speed(0.1));
+                });
+
+                if self.depth.end < self.depth.start {
+                    self.depth.end = self.depth.start;
+                }
+            });
+    }
+}
+
 impl LightComponent {
     pub fn default_direction() -> DVec3 {
         let dir = DVec3::new(-0.35, -1.0, -0.25);
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 58837b3..3cb8897 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -22,7 +22,7 @@ use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt, 
 
 #[derive(Clone)]
 pub struct Model {
-    pub(crate) hash: u64, // also the id related to the handle
+    pub hash: u64, // also the id related to the handle
     pub label: String,
     pub path: ResourceReference,
     pub meshes: Vec<Mesh>,
diff --git a/crates/dropbear-macro/src/lib.rs b/crates/dropbear-macro/src/lib.rs
index 13fa41b..f840604 100644
--- a/crates/dropbear-macro/src/lib.rs
+++ b/crates/dropbear-macro/src/lib.rs
@@ -9,88 +9,6 @@ use syn::{
 };
 use std::path::Path;
 
-/// A `derive` macro that converts a struct to a usable [SerializableComponent].
-///
-/// You have to implement `serde::Serialize`, `serde::Deserialize` and `Clone` for the
-/// struct to be usable (it will throw errors anyway).
-///
-/// # Usage
-/// ```
-/// use dropbear_macro::SerializableComponent;
-///
-/// #[derive(Serialize, Deserialize, Clone, SerializableComponent)] // required to be implemented
-/// struct MyComponent {
-///     value1: String,
-///     value2: i32,
-/// }
-/// ```
-#[proc_macro_derive(SerializableComponent)]
-pub fn derive_component(input: TokenStream) -> TokenStream {
-    let input = parse_macro_input!(input as DeriveInput);
-    let name = &input.ident;
-    let name_str = name.to_string();
-
-    let expanded = quote! {
-        #[typetag::serde]
-        impl SerializableComponent for #name {
-            fn as_any(&self) -> &dyn std::any::Any {
-                self
-            }
-
-            fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
-                self
-            }
-
-            fn clone_boxed(&self) -> Box<dyn SerializableComponent> {
-                Box::new(self.clone())
-            }
-
-            fn type_name(&self) -> &'static str {
-                #name_str
-            }
-        }
-    };
-
-    TokenStream::from(expanded)
-}
-
-/// Converts a module's functions into C API compatible functions.
-/// 
-/// Each function must require a return type of `DropbearNativeResult<T>`. If the return type
-/// is not `DropbearNativeResult<T>`, it is recommended that you move it to another module (such as `super::shared`). 
-/// 
-/// A function like that of:
-/// ```
-/// pub fn dropbear_mesh_renderer_exists_for_entity(
-///      world_ptr: *mut hecs::World,
-///      entity_id: u64,
-///  ) -> DropbearNativeResult<bool> {...}
-/// ``` 
-/// will get converted to something like:
-/// ```
-/// pub unsafe extern "C" fn dropbear_mesh_renderer_exists_for_entity(world_ptr: WorldPtr, entity_id: u64, out_result: *mut bool) -> i32 {...}
-/// ```
-#[proc_macro_attribute]
-pub fn impl_c_api(_attr: TokenStream, item: TokenStream) -> TokenStream {
-    let mut module = parse_macro_input!(item as ItemMod);
-
-    let mut new_content = Vec::new();
-
-    if let Some((brace, content)) = module.content {
-        for item in content {
-            match item {
-                Item::Fn(func) => {
-                    new_content.push(Item::Fn(transform_function(func)));
-                }
-                _ => new_content.push(item),
-            }
-        }
-        module.content = Some((brace, new_content));
-    }
-
-    TokenStream::from(quote! { #module })
-}
-
 struct ExportArgs {
     c: Option<CArgs>,
     kotlin: Option<KotlinArgs>,
diff --git a/crates/dropbear-traits/Cargo.toml b/crates/dropbear-traits/Cargo.toml
index df14e7e..bbd0fdd 100644
--- a/crates/dropbear-traits/Cargo.toml
+++ b/crates/dropbear-traits/Cargo.toml
@@ -7,8 +7,8 @@ repository.workspace = true
 readme = "README.md"
 
 [dependencies]
-typetag.workspace = true
+serde.workspace = true
+egui.workspace = true
+anyhow.workspace = true
 hecs.workspace = true
-log.workspace = true
-dyn-hash.workspace = true
-anyhow.workspace = true
\ No newline at end of file
+typetag.workspace = true
\ No newline at end of file
diff --git a/crates/dropbear-traits/src/lib.rs b/crates/dropbear-traits/src/lib.rs
index c5a62b3..459f85f 100644
--- a/crates/dropbear-traits/src/lib.rs
+++ b/crates/dropbear-traits/src/lib.rs
@@ -1,262 +1,133 @@
-pub mod registry;
-
-use anyhow::{Result, anyhow};
-use hecs::{Entity, EntityBuilder, World};
 use std::any::{Any, TypeId};
-use std::fmt::Debug;
-
-/// A type of component that gets serialized and deserialized into a scene config file.
-#[typetag::serde(tag = "type")]
-pub trait SerializableComponent: Send + Sync + Debug {
-    /// Converts a [SerializableComponent] to an [Any] type.
-    fn as_any(&self) -> &dyn Any;
-    /// Converts a [SerializableComponent] to a mutable [Any] type
-    fn as_any_mut(&mut self) -> &mut dyn Any;
-    /// Fetches the type name of that component
-    fn type_name(&self) -> &'static str;
-    /// Allows you to clone the dynamic object.
-    fn clone_boxed(&self) -> Box<dyn SerializableComponent>;
-
-    /// Returns the display name of the component.
-    fn display_name(&self) -> String {
-        let type_name = self.type_name();
-        type_name
-            .split("::")
-            .last()
-            .unwrap_or(type_name)
-            .to_string()
-    }
-}
-
-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 serializable_type_id(&self) -> TypeId;
+use std::collections::HashMap;
+use std::future::Future;
+use std::pin::Pin;
+use hecs::{Entity, World, DynamicBundle};
+use std::sync::Arc;
+use serde::{Deserialize, Serialize};
 
-    fn extract_serializable(
-        &self,
-        world: &World,
-        entity: Entity,
-    ) -> Option<Box<dyn SerializableComponent>>;
-
-    fn remove_component(&self, world: &mut World, entity: Entity);
+pub struct ComponentDescriptor {
+    pub fqtn: String,
+    pub type_name: String,
+    pub category: Option<String>,
+    pub description: Option<String>,
 }
 
-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<()>;
+pub struct ComponentRepository {
+    components: HashMap<TypeId, ComponentDescriptor>,
 }
 
-struct DirectConverter<T: SerializableComponent + hecs::Component + Clone> {
-    _phantom: std::marker::PhantomData<T>,
-}
-
-impl<T: SerializableComponent + hecs::Component + Clone + 'static> DirectConverter<T> {
-    fn new() -> Self {
+impl ComponentRepository {
+    pub fn new() -> Self {
         Self {
-            _phantom: std::marker::PhantomData,
+            components: Default::default(),
         }
     }
-}
 
-impl<T: SerializableComponent + hecs::Component + Clone + 'static> ComponentConverter
-    for DirectConverter<T>
-{
-    fn type_id(&self) -> TypeId {
-        TypeId::of::<T>()
-    }
+    pub fn register<T: Component + 'static>(&mut self) {
+        let type_id = TypeId::of::<T>();
 
-    fn type_name(&self) -> &'static str {
-        std::any::type_name::<T>()
-    }
+        let descriptor = T::static_descriptor();
 
-    fn serializable_type_id(&self) -> TypeId {
-        TypeId::of::<T>()
+        self.components.insert(type_id, descriptor);
     }
 
-    fn extract_serializable(
-        &self,
-        world: &World,
-        entity: Entity,
-    ) -> Option<Box<dyn SerializableComponent>> {
-        if let Ok(ty) = world.query_one::<&T>(entity).get()
-        {
-            return Some(ty.clone_boxed());
-        }
-        None
+    pub fn get_descriptor<T: Component + 'static>(&self) -> Option<&ComponentDescriptor> {
+        self.components.get(&TypeId::of::<T>())
     }
 
-    fn remove_component(&self, world: &mut World, entity: Entity) {
-        let _ = world.remove_one::<T>(entity);
+    pub fn iter(&self) -> impl Iterator<Item = (&TypeId, &ComponentDescriptor)> {
+        self.components.iter()
     }
-}
-
-/// 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(&World, Entity, &From) -> Option<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(&World, Entity, &From) -> Option<To> + Send + Sync,
-{
-    fn new(converter_fn: F) -> Self {
-        Self {
-            converter_fn,
-            _phantom: std::marker::PhantomData,
-        }
+    pub fn descriptors(&self) -> impl Iterator<Item = &ComponentDescriptor> {
+        self.components.values()
     }
 }
 
-impl<From, To, F> ComponentConverter for CustomConverter<From, To, F>
-where
-    From: hecs::Component + 'static,
-    To: SerializableComponent + 'static,
-    F: Fn(&World, Entity, &From) -> Option<To> + Send + Sync,
-{
-    fn type_id(&self) -> TypeId {
-        TypeId::of::<From>()
-    }
+pub trait Component {
+    type Serialized: Serialize + for<'de> Deserialize<'de>;
 
-    fn type_name(&self) -> &'static str {
-        std::any::type_name::<From>()
-    }
+    fn static_descriptor() -> ComponentDescriptor;
 
-    fn serializable_type_id(&self) -> TypeId {
-        TypeId::of::<To>()
-    }
-
-    fn extract_serializable(
-        &self,
-        world: &World,
-        entity: Entity,
-    ) -> Option<Box<dyn SerializableComponent>> {
-        let component = world.get::<&From>(entity).ok()?;
-        (self.converter_fn)(world, entity, &component)
-            .map(|converted| Box::new(converted) as Box<dyn SerializableComponent>)
-    }
-
-    fn remove_component(&self, world: &mut World, entity: Entity) {
-        let _ = world.remove_one::<From>(entity);
-    }
+    fn deserialize(serialized: &Self::Serialized) -> Self;
+    fn serialize(&self) -> Self::Serialized;
+    fn inspect(&mut self, ui: &mut egui::Ui);
 }
 
-struct DirectDeserializer<T: SerializableComponent + hecs::Component + Clone> {
-    _phantom: std::marker::PhantomData<T>,
+pub mod registry;
+
+pub struct ComponentResources {
+    map: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
 }
 
-impl<T: SerializableComponent + hecs::Component + Clone + 'static> DirectDeserializer<T> {
+impl ComponentResources {
     pub fn new() -> Self {
         Self {
-            _phantom: std::marker::PhantomData,
+            map: HashMap::new(),
         }
     }
-}
 
-impl<T: SerializableComponent + hecs::Component + Clone + 'static> ComponentDeserializer
-    for DirectDeserializer<T>
-{
-    fn serializable_type_id(&self) -> TypeId {
-        TypeId::of::<T>()
+    pub fn insert<T: Any + Send + Sync>(&mut self, value: T) {
+        self.map.insert(TypeId::of::<T>(), Box::new(value));
     }
 
-    fn serializable_type_name(&self) -> &'static str {
-        std::any::type_name::<T>()
+    pub fn get<T: Any + Send + Sync>(&self) -> Option<&T> {
+        self.map
+            .get(&TypeId::of::<T>())
+            .and_then(|boxed| boxed.downcast_ref::<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(())
+    pub fn get_mut<T: Any + Send + Sync>(&mut self) -> Option<&mut T> {
+        self.map
+            .get_mut(&TypeId::of::<T>())
+            .and_then(|boxed| boxed.downcast_mut::<T>())
     }
 }
 
-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)>,
+#[derive(Clone)]
+pub struct ComponentInitContext {
+    pub entity: Entity,
+    pub resources: Arc<ComponentResources>,
 }
 
-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,
-        }
-    }
+pub type ComponentInitFuture = Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn ComponentInsert>>> + Send + 'static>>;
+
+pub trait ComponentInsert: Send {
+    fn insert(self: Box<Self>, world: &mut World, entity: Entity) -> anyhow::Result<()>;
 }
 
-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>()
+pub struct InsertBundle<T: DynamicBundle + Send + 'static>(pub T);
+
+impl<T: DynamicBundle + Send + 'static> ComponentInsert for InsertBundle<T> {
+    fn insert(self: Box<Self>, world: &mut World, entity: Entity) -> anyhow::Result<()> {
+        world.insert(entity, self.0).map_err(|e| anyhow::anyhow!(e.to_string()))?;
+        Ok(())
     }
+}
 
-    fn serializable_type_name(&self) -> &'static str {
-        std::any::type_name::<From>()
+#[typetag::serde(tag = "type")]
+pub trait SerializableComponent: Send + Sync {
+    fn as_any(&self) -> &dyn Any;
+    fn clone_box(&self) -> Box<dyn SerializableComponent>;
+
+    fn type_name(&self) -> &'static str {
+        std::any::type_name::<Self>()
     }
 
-    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(())
+    fn display_name(&self) -> String {
+        self.type_name()
+            .split("::")
+            .last()
+            .unwrap_or(self.type_name())
+            .to_string()
     }
+
+    fn init(&self, ctx: ComponentInitContext) -> ComponentInitFuture;
 }
+
+impl Clone for Box<dyn SerializableComponent> {
+    fn clone(&self) -> Self {
+        self.clone_box()
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-traits/src/registry.rs b/crates/dropbear-traits/src/registry.rs
index 5508a29..03e14b6 100644
--- a/crates/dropbear-traits/src/registry.rs
+++ b/crates/dropbear-traits/src/registry.rs
@@ -1,367 +1,147 @@
-use crate::{
-    ComponentConverter, ComponentDeserializer, CustomConverter, CustomDeserializer,
-    DirectConverter, DirectDeserializer, SerializableComponent,
-};
-use anyhow::Result;
-use hecs::{Entity, EntityBuilder, World};
 use std::any::TypeId;
 use std::collections::HashMap;
 
-// note: to anyone viewing this, yes i did use AI to generate the documentation for this module because
-// sometimes i kept forgetting what function some did. mb
+use hecs::{Entity, World};
+
+use crate::SerializableComponent;
 
-/// Registry of conversions between ECS components (`hecs`) and [`SerializableComponent`]
-/// values.
-///
-/// # What this type does
-///
-/// `ComponentRegistry` is an adapter layer around a `hecs::World` that lets you:
-///
-/// - **Extract** one or more [`SerializableComponent`] values from an entity.
-/// - **Deserialize** a [`SerializableComponent`] back into a `hecs` component
-///   and insert it into an [`EntityBuilder`].
-/// - **Address component "kinds" by numeric IDs** (useful for editor UI, network
-///   messages, prefab formats, etc.).
-/// - **Provide default/factory construction** for editor "Add component" flows.
-///
-/// # Numeric IDs and stability
-///
-/// Component IDs are assigned lazily (on registration) starting from `1`.
-///
-/// - IDs are **stable only for the lifetime of this registry instance**.
-/// - IDs are **not guaranteed to be stable across runs** (or across different
-///   registration orders), because they're assigned incrementally.
-/// - Display / editor lists returned by [`iter_available_components`] will be in
-///   **arbitrary order**, because `HashMap` iteration order is not deterministic.
-///
-/// If you need cross-run stable identifiers (e.g., long-lived save files), you
-/// should layer an explicit, user-assigned ID scheme on top.
-///
-/// # Converters vs deserializers
-///
-/// A **converter** looks at an entity and tries to produce a serializable value.
-/// A **deserializer** takes a serializable value and inserts a real ECS component
-/// into an [`EntityBuilder`].
-///
-/// For directly-serializable components, [`register`] wires up both.
-/// For custom flows, [`register_converter`] and [`register_deserializer`] can be
-/// used independently.
 pub struct ComponentRegistry {
-    converters: HashMap<TypeId, Box<dyn ComponentConverter>>,
-    deserializers: HashMap<TypeId, Box<dyn ComponentDeserializer>>,
-    serializable_ids: HashMap<TypeId, u64>,
-    id_to_serializable: HashMap<u64, TypeId>,
-    default_creators: HashMap<u64, Box<dyn Fn() -> Box<dyn SerializableComponent> + Send + Sync>>,
-    next_component_id: u64,
+    next_id: u32,
+    entries: HashMap<u32, ComponentEntry>,
+    type_to_id: HashMap<TypeId, u32>,
+    converters: Vec<ConverterEntry>,
+}
+
+struct ComponentEntry {
+    type_id: TypeId,
+    fqtn: &'static str,
+    display_name: String,
+    create_default: Option<fn() -> Box<dyn SerializableComponent>>,
+    extract_fn: Option<fn(&World, Entity) -> Option<Box<dyn SerializableComponent>>>,
+    remove_fn: Option<fn(&mut World, Entity)>,
+}
+
+struct ConverterEntry {
+    from_type: TypeId,
+    convert_fn: Box<dyn Fn(&World, Entity) -> Option<Box<dyn SerializableComponent>> + Send + Sync>,
 }
 
 impl ComponentRegistry {
-    /// Creates an empty registry.
-    ///
-    /// No components can be extracted or deserialized until they are registered.
     pub fn new() -> Self {
         Self {
-            converters: HashMap::new(),
-            deserializers: HashMap::new(),
-            serializable_ids: HashMap::new(),
-            id_to_serializable: HashMap::new(),
-            default_creators: HashMap::new(),
-            next_component_id: 1,
+            next_id: 1,
+            entries: HashMap::new(),
+            type_to_id: HashMap::new(),
+            converters: Vec::new(),
         }
     }
 
-    /// Registers a component type that is already a [`SerializableComponent`].
-    ///
-    /// This is the common case: `T` is both a `hecs` component and a serializable
-    /// value. The registry will:
-    ///
-    /// - Assign a numeric ID to `T` (if it doesn't already have one).
-    /// - Register a direct converter (extract `T` from an entity and clone it).
-    /// - Register a direct deserializer (insert a cloned `T` into a builder).
-    ///
-    /// Note: numeric IDs are assigned based on registration order; see the type
-    /// docs for stability caveats.
-    pub fn register<T>(&mut self)
-    where
-        T: SerializableComponent + hecs::Component + Clone + 'static,
-    {
-        let type_id = TypeId::of::<T>();
-        self.ensure_serializable_id(type_id);
-        self.converters
-            .insert(type_id, Box::new(DirectConverter::<T>::new()));
-        self.deserializers
-            .insert(type_id, Box::new(DirectDeserializer::<T>::new()));
-    }
-
-    /// Registers `T` and also exposes it as an "available" component with a
-    /// default constructor.
-    ///
-    /// This is primarily meant for editor tooling: values registered via this
-    /// method will appear in [`iter_available_components`] and can be created via
-    /// [`create_default_component`].
     pub fn register_with_default<T>(&mut self)
     where
-        T: SerializableComponent + hecs::Component + Clone + Default + 'static,
-    {
-        self.register::<T>();
-        let id = self.id_for_type::<T>().unwrap();
-        self.default_creators
-            .insert(id, Box::new(|| Box::new(T::default())));
-    }
-
-    /// Registers `T` for extraction/deserialization (if needed) and associates a
-    /// custom factory used to create new instances of `T`.
-    ///
-    /// Like [`register_with_default`], this is intended for editor tooling.
-    /// Use this when the best "empty" value can't be expressed as `Default`.
-    pub fn register_factory<T, F>(&mut self, factory: F)
-    where
-        T: SerializableComponent + hecs::Component + Clone + 'static,
-        F: Fn() -> Box<dyn SerializableComponent> + Send + Sync + 'static,
+        T: SerializableComponent + Default + Clone + 'static,
     {
-        // Ensure registered first
-        if self.id_for_type::<T>().is_none() {
-            self.register::<T>();
-        }
-        let id = self.id_for_type::<T>().unwrap();
-        self.default_creators.insert(id, Box::new(factory));
-    }
-
-    /// Creates a new component instance using the default constructor/factory
-    /// registered for `component_id`.
-    ///
-    /// Returns `None` if no factory/default was registered for that ID.
-    pub fn create_default_component(
-        &self,
-        component_id: u64,
-    ) -> Option<Box<dyn SerializableComponent>> {
-        self.default_creators.get(&component_id).map(|f| f())
-    }
-
-    /// Removes the (source) ECS component associated with the given numeric ID
-    /// from `entity`.
-    ///
-    /// The registry resolves `component_id` to a *serializable* type, then finds
-    /// the first registered converter whose output type matches and asks it to
-    /// remove the underlying ECS component.
-    ///
-    /// ## Notes / edge cases
-    ///
-    /// - If no ID mapping exists or no converter matches, this is a no-op.
-    /// - If multiple converters map to the same serializable type, only the first
-    ///   match (in arbitrary `HashMap` order) will be used.
-    pub fn remove_component_by_id(&self, world: &mut World, entity: Entity, component_id: u64) {
-        if let Some(expected_type) = self.serializable_type_from_numeric(component_id) {
-            // Find the converter that produces this serializable type
-            for converter in self.converters.values() {
-                if converter.serializable_type_id() == expected_type {
-                    converter.remove_component(world, entity);
-                    // We can stop after finding one, assuming one-to-one mapping for removal
-                    // Or should we continue? Usually one component type per entity.
-                    // But multiple converters might produce same serializable type?
-                    // If so, we might try to remove all possible source components.
-                    // But usually it's 1:1.
-                    return;
-                }
-            }
+        let type_id = TypeId::of::<T>();
+        if self.type_to_id.contains_key(&type_id) {
+            return;
         }
-    }
 
-    /// Iterates the set of components that are considered "addable" via defaults
-    /// or factories.
-    ///
-    /// Yields pairs of `(numeric_id, type_name)`.
-    ///
-    /// The returned iterator only includes components for which:
-    ///
-    /// - a default/factory was registered (via [`register_with_default`] or
-    ///   [`register_factory`]), and
-    /// - a deserializer exists to provide a human-friendly type name.
-    ///
-    /// Ordering is arbitrary.
-    pub fn iter_available_components(&self) -> impl Iterator<Item = (u64, &str)> {
-        self.default_creators.keys().filter_map(move |id| {
-            let type_id = self.id_to_serializable.get(id)?;
-            let deserializer = self.deserializers.get(type_id)?;
-            Some((*id, deserializer.serializable_type_name()))
-        })
-    }
-
-    /// Registers a custom converter that extracts a serializable value `To` from
-    /// an ECS component `From`.
-    ///
-    /// Use this when the runtime ECS component isn't directly serializable, but
-    /// you can derive a serializable representation from it.
-    ///
-    /// The provided function receives `(world, entity, &From)` and may return
-    /// `None` to indicate "not present / not applicable".
-    ///
-    /// Note: this registers an ID for `To` (the serializable output type), not for
-    /// `From`.
-    pub fn register_converter<From, To, F>(&mut self, converter_fn: F)
+        let id = self.next_id;
+        self.next_id = self.next_id.saturating_add(1);
+
+        let entry = ComponentEntry {
+            type_id,
+            fqtn: std::any::type_name::<T>(),
+            display_name: short_name(std::any::type_name::<T>()),
+            create_default: Some(|| Box::new(T::default())),
+            extract_fn: Some(|world, entity| {
+                world
+                    .get::<&T>(entity)
+                    .ok()
+                    .map(|component| component.clone_box())
+            }),
+            remove_fn: Some(|world, entity| {
+                let _ = world.remove_one::<T>(entity);
+            }),
+        };
+
+        self.entries.insert(id, entry);
+        self.type_to_id.insert(type_id, id);
+    }
+
+    pub fn register_converter<From, To, F>(&mut self, converter: F)
     where
-        From: hecs::Component + 'static,
+        From: Sync + Send + 'static,
         To: SerializableComponent + 'static,
-        F: Fn(&World, Entity, &From) -> Option<To> + Send + Sync + 'static,
+        F: Fn(&World, Entity, &From) -> Option<To> + Sync + Send + 'static,
     {
-        let type_id = TypeId::of::<From>();
-        // converter output is To, so track its serializable id
-        self.ensure_serializable_id(TypeId::of::<To>());
-        self.converters
-            .insert(type_id, Box::new(CustomConverter::new(converter_fn)));
-    }
+        let from_id = TypeId::of::<From>();
+        let convert_fn = move |world: &World, entity: Entity| -> Option<Box<dyn SerializableComponent>> {
+            world
+                .get::<&From>(entity)
+                .ok()
+                .and_then(|component| converter(world, entity, &component))
+                .map(|converted| Box::new(converted) as Box<dyn SerializableComponent>)
+        };
 
-    /// Registers a custom deserializer that converts a serializable `From` into
-    /// a concrete ECS component `To`.
-    ///
-    /// This is the inverse of [`register_converter`]. Use it when `From` is the
-    /// type you store/transport, and `To` is the type you actually attach to an
-    /// entity.
-    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.ensure_serializable_id(type_id);
-        self.deserializers
-            .insert(type_id, Box::new(CustomDeserializer::new(converter_fn)));
+        self.converters.push(ConverterEntry {
+            from_type: from_id,
+            convert_fn: Box::new(convert_fn),
+        });
     }
 
-    /// Extracts all registered serializable components from `entity`.
-    ///
-    /// This calls every registered converter and collects the values it returns.
-    ///
-    /// Ordering is arbitrary (depends on `HashMap` iteration order).
-    pub fn extract_all_components(
-        &self,
-        world: &World,
-        entity: Entity,
-    ) -> Vec<Box<dyn SerializableComponent>> {
-        let mut vec = vec![];
-        for converter in self.converters.values() {
-            if let Some(component) = converter.extract_serializable(world, entity) {
-                vec.push(component);
-            }
-        }
-        return vec;
-    }
-
-    /// Ensures a numeric ID exists for the given serializable `TypeId` and
-    /// returns it.
-    ///
-    /// IDs are assigned incrementally and wrap on overflow. `0` is never used.
-    fn ensure_serializable_id(&mut self, type_id: TypeId) -> u64 {
-        if let Some(id) = self.serializable_ids.get(&type_id) {
-            *id
-        } else {
-            let id = self.next_component_id;
-            self.next_component_id = self.next_component_id.wrapping_add(1).max(1);
-            self.serializable_ids.insert(type_id, id);
-            self.id_to_serializable.insert(id, type_id);
-            id
-        }
+    pub fn iter_available_components(&self) -> impl Iterator<Item = (u32, &str)> {
+        self.entries
+            .iter()
+            .map(|(id, entry)| (*id, entry.fqtn))
     }
 
-    /// Returns the numeric identifier assigned to the dynamic component value.
-    ///
-    /// Returns `None` if the component's concrete type has not been registered.
-    pub fn id_for_component(&self, component: &dyn SerializableComponent) -> Option<u64> {
-        let type_id = component.as_any().type_id();
-        self.serializable_ids.get(&type_id).copied()
+    pub fn create_default_component(&self, id: u32) -> Option<Box<dyn SerializableComponent>> {
+        self.entries
+            .get(&id)
+            .and_then(|entry| entry.create_default.map(|ctor| ctor()))
     }
 
-    /// Returns the numeric identifier assigned to the serializable type `T`.
-    ///
-    /// Returns `None` if `T` has not been registered (directly or as a converter
-    /// output).
-    pub fn id_for_type<T>(&self) -> Option<u64>
-    where
-        T: SerializableComponent + 'static,
-    {
-        self.serializable_ids.get(&TypeId::of::<T>()).copied()
+    pub fn id_for_component(&self, component: &dyn SerializableComponent) -> Option<u32> {
+        self.type_to_id.get(&component.as_any().type_id()).copied()
     }
 
-    /// Looks up the serializable `TypeId` associated with a numeric identifier.
-    fn serializable_type_from_numeric(&self, component_id: u64) -> Option<TypeId> {
-        self.id_to_serializable.get(&component_id).copied()
+    pub fn remove_component_by_id(&self, world: &mut World, entity: Entity, id: u32) {
+        if let Some(entry) = self.entries.get(&id) {
+            if let Some(remove_fn) = entry.remove_fn {
+                remove_fn(world, entity);
+            }
+        }
     }
 
-    /// Extracts a single serializable component from `entity` by numeric ID.
-    ///
-    /// Returns `None` if:
-    ///
-    /// - `component_id` is unknown, or
-    /// - none of the registered converters produce a value of that type for the
-    ///   given entity.
-    ///
-    /// If multiple converters can produce the same serializable type, the first
-    /// match (in arbitrary order) wins.
-    pub fn extract_component_by_numeric_id(
+    pub fn extract_all_components(
         &self,
         world: &World,
         entity: Entity,
-        component_id: u64,
-    ) -> Option<Box<dyn SerializableComponent>> {
-        let expected_type = self.serializable_type_from_numeric(component_id)?;
+    ) -> Vec<Box<dyn SerializableComponent>> {
+        let mut components = Vec::new();
 
-        for converter in self.converters.values() {
-            if let Some(component) = converter.extract_serializable(world, entity) {
-                if component.as_any().type_id() == expected_type {
-                    return Some(component);
+        for entry in self.entries.values() {
+            if let Some(extract_fn) = entry.extract_fn {
+                if let Some(component) = extract_fn(world, entity) {
+                    components.push(component);
                 }
             }
         }
 
-        None
-    }
-
-    /// Finds every entity in `world` that has a component matching `component_id`.
-    ///
-    /// This is a convenience wrapper that iterates all entities and uses
-    /// [`extract_component_by_numeric_id`] to test each one.
-    pub fn find_components_by_numeric_id(
-        &self,
-        world: &World,
-        component_id: u64,
-    ) -> Vec<(Entity, Box<dyn SerializableComponent>)> {
-        let mut matches = Vec::new();
-        for entity in world.query::<Entity>().iter() {
-            if let Some(component) =
-                self.extract_component_by_numeric_id(world, entity, component_id)
-            {
-                matches.push((entity, component));
+        for converter in &self.converters {
+            if let Some(component) = (converter.convert_fn)(world, entity) {
+                components.push(component);
             }
         }
-        matches
+
+        components
     }
 
-    /// Deserializes a [`SerializableComponent`] into an ECS component and inserts
-    /// it into `builder`.
-    ///
-    /// Returns:
-    ///
-    /// - `Ok(true)` if a deserializer was found and insertion succeeded.
-    /// - `Ok(false)` if no deserializer is registered for this component type.
-    /// - `Err(_)` if a deserializer was found but it 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)
-        }
+    pub fn iter(&self) -> impl Iterator<Item = (u32, &str)> {
+        self.iter_available_components()
     }
 }
 
-impl Default for ComponentRegistry {
-    fn default() -> Self {
-        Self::new()
-    }
+fn short_name(fqtn: &str) -> String {
+    fqtn.split("::").last().unwrap_or(fqtn).to_string()
 }
diff --git a/crates/eucalyptus-core/src/camera.rs b/crates/eucalyptus-core/src/camera.rs
index 4bc70aa..9b9d48c 100644
--- a/crates/eucalyptus-core/src/camera.rs
+++ b/crates/eucalyptus-core/src/camera.rs
@@ -1,15 +1,15 @@
 //! Additional information and context for cameras from the [`dropbear_engine::camera`]
 use crate::states::Camera3D;
-use crate::traits::SerializableComponent;
 use dropbear_engine::camera::{Camera, CameraBuilder, CameraSettings};
-use dropbear_macro::SerializableComponent;
+use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent};
 use glam::DVec3;
 use serde::{Deserialize, Serialize};
+use std::any::Any;
 use crate::ptr::WorldPtr;
 use crate::scripting::result::DropbearNativeResult;
 use crate::types::NVector3;
 
-#[derive(Debug, Clone, Serialize, Deserialize, SerializableComponent)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct CameraComponent {
     pub settings: CameraSettings,
     pub camera_type: CameraType,
@@ -36,6 +36,26 @@ impl CameraComponent {
     }
 }
 
+#[typetag::serde]
+impl SerializableComponent for CameraComponent {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn clone_box(&self) -> Box<dyn SerializableComponent> {
+        Box::new(self.clone())
+    }
+
+    fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture {
+        let value = self.clone();
+        Box::pin(async move {
+            let insert: Box<dyn dropbear_traits::ComponentInsert> =
+                Box::new(InsertBundle((value,)));
+            Ok(insert)
+        })
+    }
+}
+
 impl From<Camera3D> for CameraBuilder {
     fn from(value: Camera3D) -> Self {
         let forward = value.transform.rotation * DVec3::Z;
diff --git a/crates/eucalyptus-core/src/physics/collider.rs b/crates/eucalyptus-core/src/physics/collider.rs
index f5a7cc2..76470a0 100644
--- a/crates/eucalyptus-core/src/physics/collider.rs
+++ b/crates/eucalyptus-core/src/physics/collider.rs
@@ -21,8 +21,8 @@ use crate::states::Label;
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::wgpu::util::{BufferInitDescriptor, DeviceExt};
 use dropbear_engine::wgpu::{Buffer, BufferUsages};
-use dropbear_macro::SerializableComponent;
-use dropbear_traits::SerializableComponent;
+use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent};
+use std::any::Any;
 use ::jni::objects::{JObject, JValue};
 use ::jni::JNIEnv;
 use rapier3d::prelude::ColliderBuilder;
@@ -37,7 +37,7 @@ use rapier3d::prelude::{Rotation, SharedShape, TypedShape, Vector};
 use crate::physics::PhysicsState;
 use crate::ptr::PhysicsStatePtr;
 
-#[derive(Debug, Default, Serialize, Deserialize, Clone, SerializableComponent)]
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
 pub struct ColliderGroup {
     #[serde(default)]
     pub colliders: Vec<Collider>,
@@ -53,6 +53,26 @@ impl ColliderGroup {
     }
 }
 
+#[typetag::serde]
+impl SerializableComponent for ColliderGroup {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn clone_box(&self) -> Box<dyn SerializableComponent> {
+        Box::new(self.clone())
+    }
+
+    fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture {
+        let value = self.clone();
+        Box::pin(async move {
+            let insert: Box<dyn dropbear_traits::ComponentInsert> =
+                Box::new(InsertBundle((value,)));
+            Ok(insert)
+        })
+    }
+}
+
 #[repr(C)]
 #[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct Collider {
diff --git a/crates/eucalyptus-core/src/physics/kcc.rs b/crates/eucalyptus-core/src/physics/kcc.rs
index 540b212..f2a6811 100644
--- a/crates/eucalyptus-core/src/physics/kcc.rs
+++ b/crates/eucalyptus-core/src/physics/kcc.rs
@@ -3,11 +3,11 @@
 
 pub mod character_collision;
 
-use crate::traits::SerializableComponent;
 use rapier3d::control::{CharacterCollision, KinematicCharacterController};
 use serde::{Deserialize, Serialize};
-use dropbear_macro::SerializableComponent;
+use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent};
 use crate::states::Label;
+use std::any::Any;
 use crate::physics::PhysicsState;
 use crate::scripting::jni::utils::ToJObject;
 use crate::scripting::native::DropbearNativeError;
@@ -21,7 +21,7 @@ use rapier3d::prelude::QueryFilter;
 use crate::ptr::WorldPtr;
 
 /// The kinematic character controller (kcc) component.
-#[derive(Debug, Default, Serialize, Deserialize, Clone, SerializableComponent)]
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
 pub struct KCC {
     pub entity: Label,
     pub controller: KinematicCharacterController,
@@ -29,6 +29,26 @@ pub struct KCC {
     pub collisions: Vec<CharacterCollision>,
 }
 
+#[typetag::serde]
+impl SerializableComponent for KCC {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn clone_box(&self) -> Box<dyn SerializableComponent> {
+        Box::new(self.clone())
+    }
+
+    fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture {
+        let value = self.clone();
+        Box::pin(async move {
+            let insert: Box<dyn dropbear_traits::ComponentInsert> =
+                Box::new(InsertBundle((value,)));
+            Ok(insert)
+        })
+    }
+}
+
 impl KCC {
     pub fn new(label: &Label) -> Self {
         KCC {
diff --git a/crates/eucalyptus-core/src/physics/rigidbody.rs b/crates/eucalyptus-core/src/physics/rigidbody.rs
index 306a031..6744ae2 100644
--- a/crates/eucalyptus-core/src/physics/rigidbody.rs
+++ b/crates/eucalyptus-core/src/physics/rigidbody.rs
@@ -4,8 +4,8 @@ use crate::scripting::jni::utils::{FromJObject, ToJObject};
 use crate::scripting::native::DropbearNativeError;
 use crate::scripting::result::DropbearNativeResult;
 use crate::states::Label;
-use dropbear_macro::SerializableComponent;
-use dropbear_traits::SerializableComponent;
+use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent};
+use std::any::Any;
 use ::jni::objects::{JObject, JValue};
 use ::jni::JNIEnv;
 use rapier3d::prelude::RigidBodyType;
@@ -116,7 +116,7 @@ impl ToJObject for AxisLock {
 /// - The body's initial pose should typically come from your `EntityTransform`/`Transform`.
 /// - Colliders/material (shape, friction, restitution, sensor, etc.) should usually be a separate
 ///   component (e.g. `Collider`).
-#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct RigidBody {
 	/// The entity this component is attached to.
 	#[serde(default)]
@@ -190,6 +190,26 @@ impl Default for RigidBody {
 	}
 }
 
+#[typetag::serde]
+impl SerializableComponent for RigidBody {
+	fn as_any(&self) -> &dyn Any {
+		self
+	}
+
+	fn clone_box(&self) -> Box<dyn SerializableComponent> {
+		Box::new(self.clone())
+	}
+
+	fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture {
+		let value = self.clone();
+		Box::pin(async move {
+			let insert: Box<dyn dropbear_traits::ComponentInsert> =
+				Box::new(InsertBundle((value,)));
+			Ok(insert)
+		})
+	}
+}
+
 impl RigidBody {
 	const fn default_gravity_scale() -> f32 {
 		1.0
diff --git a/crates/eucalyptus-core/src/properties.rs b/crates/eucalyptus-core/src/properties.rs
index a850c24..692e008 100644
--- a/crates/eucalyptus-core/src/properties.rs
+++ b/crates/eucalyptus-core/src/properties.rs
@@ -1,8 +1,8 @@
 use std::fmt;
 use std::fmt::{Display, Formatter};
 use serde::{Deserialize, Serialize};
-use dropbear_macro::SerializableComponent;
-use dropbear_traits::SerializableComponent;
+use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent};
+use std::any::Any;
 use egui::Ui;
 use hecs::{Entity, World};
 use crate::ptr::WorldPtr;
@@ -12,7 +12,7 @@ use crate::states::Property;
 use crate::types::NVector3;
 
 /// Properties for an entity, typically queries with `entity.getProperty<Float>` and `entity.setProperty(21)`
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, SerializableComponent)]
+#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
 pub struct CustomProperties {
     pub custom_properties: Vec<Property>,
     pub next_id: u64,
@@ -128,6 +128,26 @@ impl Default for CustomProperties {
     }
 }
 
+#[typetag::serde]
+impl SerializableComponent for CustomProperties {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn clone_box(&self) -> Box<dyn SerializableComponent> {
+        Box::new(self.clone())
+    }
+
+    fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture {
+        let value = self.clone();
+        Box::pin(async move {
+            let insert: Box<dyn dropbear_traits::ComponentInsert> =
+                Box::new(InsertBundle((value,)));
+            Ok(insert)
+        })
+    }
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 pub enum Value {
     String(String),
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index a0b787f..3aa7d99 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -6,16 +6,11 @@ pub mod scripting;
 use crate::camera::CameraComponent;
 use crate::hierarchy::{Children, Parent, SceneHierarchy};
 use crate::states::{Camera3D, Label, Light, Script, SerializedMeshRenderer, WorldLoadingStatus, PROJECT};
-use crate::utils::ResolveReference;
-use dropbear_engine::asset::ASSET_REGISTRY;
-use dropbear_engine::camera::{Camera, CameraBuilder};
+use dropbear_engine::camera::Camera;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
-use dropbear_engine::texture::Texture;
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::lighting::{Light as EngineLight, LightComponent};
-use dropbear_engine::model::{Material, Model};
-use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
-use dropbear_traits::SerializableComponent;
+use dropbear_traits::{ComponentInitContext, ComponentResources, SerializableComponent};
 use dropbear_traits::registry::ComponentRegistry;
 use glam::{DQuat, DVec3};
 use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
@@ -26,15 +21,15 @@ use std::fs;
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
 use crossbeam_channel::Sender;
+use egui::Ui;
 use hecs::Entity;
-use dropbear_engine::procedural::ProcObj;
 use crate::physics::collider::ColliderGroup;
 use crate::physics::kcc::KCC;
 use crate::physics::PhysicsState;
 use crate::physics::rigidbody::RigidBody;
 use crate::properties::CustomProperties;
 
-#[derive(Default, Debug, Serialize, Deserialize, Clone)]
+#[derive(Default, Serialize, Deserialize, Clone)]
 pub struct SceneEntity {
     #[serde(default)]
     pub label: Label,
@@ -130,241 +125,24 @@ impl SceneConfig {
         }
     }
 
-    /// Helper function to load a component and add it to the entity builder
-    async fn load_component(
-        component: Box<dyn SerializableComponent>,
-        builder: &mut hecs::EntityBuilder,
-        graphics: Arc<SharedGraphicsContext>,
-        registry: Option<&ComponentRegistry>,
+    /// Helper function to init a component and insert it into the world.
+    async fn init_component(
+        component: &Box<dyn SerializableComponent>,
+        entity: Entity,
+        world: &mut hecs::World,
+        resources: Arc<ComponentResources>,
         label: &str,
     ) -> anyhow::Result<()> {
-        if let Some(transform) = component.as_any().downcast_ref::<EntityTransform>() {
-            builder.add(*transform);
-        } else if let Some(renderer) = component.as_any().downcast_ref::<SerializedMeshRenderer>() {
-            let renderer = renderer.clone();
-            let import_scale = renderer.import_scale.unwrap_or(1.0);
-            let mut model = match &renderer.handle.ref_type {
-                ResourceReferenceType::None => {
-                    log::error!(
-                        "Resource reference type is None for entity '{}', not supported, skipping",
-                        label
-                    );
-                    return Ok(());
-                }
-                ResourceReferenceType::Unassigned { id } => {
-                    log::debug!("Loading entity '{}' with no model selected", label);
-
-                    let model = std::sync::Arc::new(Model {
-                        label: "None".to_string(),
-                        hash: *id,
-                        path: ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: *id }),
-                        meshes: Vec::new(),
-                        materials: Vec::new(),
-                        skins: Vec::new(),
-                        animations: Vec::new(),
-                        nodes: Vec::new(),
-                    });
-                    let loaded = LoadedModel::new_raw(&ASSET_REGISTRY, model);
-                    MeshRenderer::from_handle_with_import_scale(loaded, import_scale)
-                }
-                ResourceReferenceType::File(reference) => {
-                    let path = &renderer.handle.resolve()?;
-
-                    log::debug!(
-                            "Path for entity {} is {} from reference {}",
-                            label,
-                            path.display(),
-                            reference
-                        );
-
-                    let loaded = Model::load(graphics.clone(), path, Some(label), None).await?;
-                    MeshRenderer::from_handle_with_import_scale(loaded, import_scale)
-                }
-                ResourceReferenceType::Bytes(bytes) => {
-                    log::info!("Loading entity from bytes [Len: {}]", bytes.len());
-
-                    let loaded =
-                        Model::load_from_memory(graphics.clone(), bytes.clone(), Some(label), None)
-                            .await?;
-                    MeshRenderer::from_handle_with_import_scale(loaded, import_scale)
-                }
-                ResourceReferenceType::ProcObj(obj) => {
-                    match obj {
-                        ProcObj::Cuboid { size_bits } => {
-                            let size = [
-                                f32::from_bits(size_bits[0]),
-                                f32::from_bits(size_bits[1]),
-                                f32::from_bits(size_bits[2]),
-                            ];
-                            log::info!("Loading entity from cuboid: {:?}", size);
-                            {
-                                let mut cache_guard = MODEL_CACHE.lock();
-                                cache_guard.remove(label);
-                            }
-
-                            let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64);
-                            let mut loaded_model = dropbear_engine::procedural::ProcedurallyGeneratedObject::cuboid(size_vec)
-                                .build_model(graphics.clone(), None, Some(label));
-
-                            let model = loaded_model.make_mut();
-                            model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits: *size_bits }));
-
-                            loaded_model.refresh_registry();
-
-                            MeshRenderer::from_handle_with_import_scale(loaded_model, import_scale)
-                        }
-                    }
-                }
-            };
-
-            if !renderer.material_override.is_empty() {
-                for override_entry in &renderer.material_override {
-                    if ASSET_REGISTRY
-                        .model_handle_from_reference(&override_entry.source_model)
-                        .is_none()
-                    {
-                        if matches!(
-                            override_entry.source_model.ref_type,
-                            ResourceReferenceType::File(_)
-                        ) {
-                            let source_path = override_entry.source_model.resolve()?;
-                            let label_hint = override_entry.source_model.as_uri();
-                            Model::load(graphics.clone(), &source_path, label_hint, None).await?;
-                        } else {
-                            log::warn!(
-                                "Material override for '{}' references unsupported resource {:?}",
-                                label,
-                                override_entry.source_model
-                            );
-                            continue;
-                        }
-                    }
-
-                    if let Err(err) = model.apply_material_override(
-                        &override_entry.target_material,
-                        override_entry.source_model.clone(),
-                        &override_entry.source_material,
-                    ) {
-                        log::warn!(
-                            "Failed to apply material override '{}' on '{}': {}",
-                            override_entry.target_material,
-                            label,
-                            err
-                        );
-                    }
-                }
-            }
-
-            if !renderer.material_customisation.is_empty() {
-                for custom in &renderer.material_customisation {
-                    {
-                        let model_mut = model.make_model_mut();
-                        let name_index = model_mut
-                            .materials
-                            .iter()
-                            .position(|mat| mat.name == custom.target_material);
-
-                        let index = name_index.or(custom.material_index);
-
-                        if let Some(material) = index.and_then(|idx| model_mut.materials.get_mut(idx)) {
-                            material.set_tint(graphics.as_ref(), custom.tint);
-                            material.set_uv_tiling(graphics.as_ref(), custom.uv_tiling);
-
-                            if let Some(reference) = &custom.diffuse_texture {
-                                if let Ok(path) = reference.resolve() {
-                                    if let Ok(bytes) = std::fs::read(&path) {
-                                        let diffuse = Texture::from_bytes_verbose_mipmapped(
-                                            graphics.clone(),
-                                            bytes.as_slice(),
-                                            None,
-                                            None,
-                                            Some(Texture::sampler_from_wrap(custom.wrap_mode)),
-                                            Some(material.name.as_str())
-                                        );
-                                        let flat_normal = (*ASSET_REGISTRY
-                                            .solid_texture_rgba8(
-                                                graphics.clone(),
-                                                [128, 128, 255, 255],
-                                            ))
-                                        .clone();
-
-                                        material.diffuse_texture = diffuse;
-                                        material.normal_texture = flat_normal;
-                                        material.bind_group = Material::create_bind_group(
-                                            graphics.as_ref(),
-                                            &material.diffuse_texture,
-                                            &material.normal_texture,
-                                            &material.name,
-                                        );
-                                        material.texture_tag = reference.as_uri().map(|s| s.to_string());
-                                        material.wrap_mode = custom.wrap_mode;
-                                        material.set_uv_tiling(graphics.as_ref(), custom.uv_tiling);
-                                    } else {
-                                        log::warn!(
-                                            "Failed to read custom texture '{}' for '{}'",
-                                            path.display(),
-                                            label
-                                        );
-                                    }
-                                } else {
-                                    log::warn!(
-                                        "Failed to resolve custom texture reference {:?} for '{}'",
-                                        reference,
-                                        label
-                                    );
-                                }
-                            }
-                        }
-                    }
-
-                }
-
-                model.sync_asset_registry();
-            }
-
-            builder.add(model);
-        } else if let Some(props) = component.as_any().downcast_ref::<CustomProperties>() {
-            builder.add(props.clone());
-        } else if let Some(camera_comp) = component.as_any().downcast_ref::<Camera3D>() {
-            let cam_builder = CameraBuilder::from(camera_comp.clone());
-            let comp = CameraComponent::from(camera_comp.clone());
-            let camera = Camera::new(graphics.clone(), cam_builder, Some(label));
-            builder.add_bundle((camera, comp));
-        } else if let Some(light_conf) = component.as_any().downcast_ref::<Light>() {
-            let light = EngineLight::new(
-                graphics.clone(),
-                light_conf.light_component.clone(),
-                light_conf.transform,
-                Some(label),
-            )
-            .await;
-            builder.add_bundle((light_conf.light_component.clone(), light));
-            builder.add(light_conf.clone());
-            builder.add(light_conf.transform);
-        } else if let Some(script) = component.as_any().downcast_ref::<Script>() {
-            builder.add(script.clone());
-        } else if let Some(body) = component.as_any().downcast_ref::<RigidBody>() {
-            builder.add(body.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",
+        let ctx = ComponentInitContext { entity, resources };
+        let insert = component.init(ctx).await?;
+        insert.insert(world, entity).map_err(|e| {
+            anyhow::anyhow!(
+                "Failed to insert component '{}' for '{}': {}",
                 component.type_name(),
-                label
-            );
-        }
+                label,
+                e
+            )
+        })?;
 
         Ok(())
     }
@@ -402,7 +180,7 @@ impl SceneConfig {
         &mut self,
         world: &mut hecs::World,
         graphics: Arc<SharedGraphicsContext>,
-        registry: Option<&ComponentRegistry>,
+        _registry: Option<&ComponentRegistry>,
         progress_sender: Option<Sender<WorldLoadingStatus>>,
         is_play_mode: bool,
     ) -> anyhow::Result<hecs::Entity> {
@@ -428,6 +206,10 @@ impl SceneConfig {
 
         log::info!("World cleared, now has {} entities", world.len());
 
+        let mut resources = ComponentResources::new();
+        resources.insert(graphics.clone());
+        let resources = Arc::new(resources);
+
         let entity_configs: Vec<(usize, SceneEntity)> = {
             let cloned = self.entities.clone();
             cloned
@@ -461,9 +243,7 @@ impl SceneConfig {
                 });
             }
 
-            let mut builder = hecs::EntityBuilder::new();
-
-            builder.add(label_for_map.clone());
+            let entity = world.spawn((label_for_map.clone(),));
 
             let mut has_entity_transform = false;
 
@@ -476,18 +256,24 @@ impl SceneConfig {
                     has_entity_transform = true;
                 }
 
-                Self::load_component(
-                    component,
-                    &mut builder,
-                    graphics.clone(),
-                    registry,
+                if component.as_any().downcast_ref::<Parent>().is_some() {
+                    log::debug!(
+                        "Skipping Parent component for '{}' - will be rebuilt from hierarchy_map",
+                        label_for_logs
+                    );
+                    continue;
+                }
+
+                Self::init_component(
+                    &component,
+                    entity,
+                    world,
+                    resources.clone(),
                     &label_for_logs,
                 )
                 .await?;
             }
 
-            let entity = world.spawn(builder.build());
-
             if has_entity_transform {
                 if let Ok((
                               entity_transform, 
diff --git a/crates/eucalyptus-core/src/scripting/native/utils.rs b/crates/eucalyptus-core/src/scripting/native/utils.rs
index edff3ad..1cc67f6 100644
--- a/crates/eucalyptus-core/src/scripting/native/utils.rs
+++ b/crates/eucalyptus-core/src/scripting/native/utils.rs
@@ -24,20 +24,4 @@ pub fn button_from_ordinal(ordinal: i32) -> Result<gilrs::Button, ()> {
         19 => Ok(gilrs::Button::DPadRight),
         _ => Err(()),
     }
-}
-
-#[dropbear_macro::impl_c_api]
-mod string {
-    use crate::scripting::result::DropbearNativeResult;
-    use std::ffi::{c_char, CString};
-
-    pub fn dropbear_free_string(ptr: *mut c_char) -> DropbearNativeResult<()> {
-        if ptr.is_null() {
-            return Ok(());
-        }
-        unsafe {
-            let _ = CString::from_raw(ptr);
-        }
-        Ok(())
-    }
 }
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/spawn.rs b/crates/eucalyptus-core/src/spawn.rs
index 63ccb40..ccb44ca 100644
--- a/crates/eucalyptus-core/src/spawn.rs
+++ b/crates/eucalyptus-core/src/spawn.rs
@@ -11,7 +11,7 @@ pub static PENDING_SPAWNS: LazyLock<Mutex<Vec<PendingSpawn>>> =
     LazyLock::new(|| Mutex::new(Vec::new()));
 
 /// A spawn that's waiting to be added into the world.
-#[derive(Clone, Debug)]
+#[derive(Clone)]
 pub struct PendingSpawn {
     pub scene_entity: SceneEntity,
     /// An optional future handle to an object.
diff --git a/crates/eucalyptus-core/src/states.rs b/crates/eucalyptus-core/src/states.rs
index 4adc248..4980327 100644
--- a/crates/eucalyptus-core/src/states.rs
+++ b/crates/eucalyptus-core/src/states.rs
@@ -5,22 +5,27 @@
 use crate::camera::{CameraComponent, CameraType};
 use crate::config::{ProjectConfig, ResourceConfig, SourceConfig};
 use crate::scene::SceneConfig;
-use crate::traits::SerializableComponent;
 use dropbear_engine::camera::Camera;
+use dropbear_engine::camera::CameraBuilder;
 use dropbear_engine::entity::{MeshRenderer, Transform};
 use dropbear_engine::lighting::LightComponent;
+use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::asset::ASSET_REGISTRY;
 use dropbear_engine::utils::{ResourceReference, ResourceReferenceType, EUCA_SCHEME};
-use dropbear_macro::SerializableComponent;
+use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent};
 use once_cell::sync::Lazy;
 use parking_lot::RwLock;
 use serde::{Deserialize, Serialize};
 use std::borrow::Borrow;
+use std::any::Any;
 use std::fmt;
 use std::fmt::{Display, Formatter};
 use std::ops::{Deref, DerefMut};
 use std::path::PathBuf;
+use std::sync::Arc;
+use egui::Ui;
 use hecs::{Entity, World};
+use dropbear_traits::{Component, ComponentDescriptor};
 use crate::properties::Value;
 
 /// A global "singleton" that contains the configuration of a project.
@@ -146,12 +151,12 @@ impl Display for ResourceType {
     }
 }
 
-#[derive(Default, Debug, Serialize, Deserialize, Clone, SerializableComponent)]
+#[derive(Default, Debug, Serialize, Deserialize, Clone)]
 pub struct Script {
     pub tags: Vec<String>,
 }
 
-#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct Camera3D {
     pub label: String,
     pub transform: Transform,
@@ -227,7 +232,7 @@ pub struct Property {
 }
 
 // A serializable configuration struct for the [Light] type
-#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct Light {
     pub label: String,
     pub transform: Transform,
@@ -280,7 +285,7 @@ pub enum WorldLoadingStatus {
     Completed,
 }
 
-#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, SerializableComponent)]
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
 pub struct Label(String);
 
 impl Default for Label {
@@ -370,7 +375,8 @@ impl DerefMut for Label {
 }
 
 /// A [MeshRenderer] that is serialized into a file to be stored as a value for config.
-#[derive(Default, Debug, Clone, Serialize, Deserialize, SerializableComponent)]
+
+#[derive(Default, Debug, Clone, Serialize, Deserialize)]
 pub struct SerializedMeshRenderer {
     pub handle: ResourceReference,
     pub import_scale: Option<f32>,
@@ -415,4 +421,143 @@ impl SerializedMeshRenderer {
             texture_override,
         }
     }
+}
+
+#[typetag::serde]
+impl SerializableComponent for Script {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn clone_box(&self) -> Box<dyn SerializableComponent> {
+        Box::new(self.clone())
+    }
+
+    fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture {
+        let value = self.clone();
+        Box::pin(async move {
+            let insert: Box<dyn dropbear_traits::ComponentInsert> =
+                Box::new(InsertBundle((value,)));
+            Ok(insert)
+        })
+    }
+}
+
+#[typetag::serde]
+impl SerializableComponent for Camera3D {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn clone_box(&self) -> Box<dyn SerializableComponent> {
+        Box::new(self.clone())
+    }
+
+    fn init(&self, ctx: ComponentInitContext) -> ComponentInitFuture {
+        let value = self.clone();
+        Box::pin(async move {
+            let graphics = ctx
+                .resources
+                .get::<Arc<SharedGraphicsContext>>()
+                .ok_or_else(|| anyhow::anyhow!("SharedGraphicsContext missing for Camera3D init"))?;
+
+            let builder = CameraBuilder::from(value.clone());
+            let camera = Camera::new(graphics.clone(), builder, Some(value.label.as_str()));
+            let component = CameraComponent::from(value);
+
+            let insert: Box<dyn dropbear_traits::ComponentInsert> =
+                Box::new(InsertBundle((camera, component)));
+            Ok(insert)
+        })
+    }
+}
+
+#[typetag::serde]
+impl SerializableComponent for Light {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn clone_box(&self) -> Box<dyn SerializableComponent> {
+        Box::new(self.clone())
+    }
+
+    fn init(&self, ctx: ComponentInitContext) -> ComponentInitFuture {
+        let value = self.clone();
+        Box::pin(async move {
+            let graphics = ctx
+                .resources
+                .get::<Arc<SharedGraphicsContext>>()
+                .ok_or_else(|| anyhow::anyhow!("SharedGraphicsContext missing for Light init"))?;
+
+            let engine_light = dropbear_engine::lighting::Light::new(
+                graphics.clone(),
+                value.light_component.clone(),
+                value.transform,
+                Some(value.label.as_str()),
+            )
+            .await;
+
+            let insert: Box<dyn dropbear_traits::ComponentInsert> =
+                Box::new(InsertBundle((
+                    value.light_component.clone(),
+                    engine_light,
+                    value.clone(),
+                    value.transform,
+                )));
+            Ok(insert)
+        })
+    }
+}
+
+#[typetag::serde]
+impl SerializableComponent for Label {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn clone_box(&self) -> Box<dyn SerializableComponent> {
+        Box::new(self.clone())
+    }
+
+    fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture {
+        let value = self.clone();
+        Box::pin(async move {
+            let insert: Box<dyn dropbear_traits::ComponentInsert> =
+                Box::new(InsertBundle((value,)));
+            Ok(insert)
+        })
+    }
+}
+
+#[typetag::serde]
+impl SerializableComponent for SerializedMeshRenderer {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn clone_box(&self) -> Box<dyn SerializableComponent> {
+        Box::new(self.clone())
+    }
+
+    fn init(&self, ctx: ComponentInitContext) -> ComponentInitFuture {
+        let value = self.clone();
+        Box::pin(async move {
+            let graphics = ctx
+                .resources
+                .get::<Arc<SharedGraphicsContext>>()
+                .ok_or_else(|| anyhow::anyhow!("SharedGraphicsContext missing for MeshRenderer init"))?;
+
+            let label = format!("Entity {:?}", ctx.entity);
+            let renderer = crate::utils::mesh_loader::load_mesh_renderer_from_serialized(
+                &value,
+                graphics.clone(),
+                &label,
+            )
+            .await?;
+            let insert: Box<dyn dropbear_traits::ComponentInsert> =
+                Box::new(InsertBundle((renderer,)));
+            Ok(insert)
+        })
+    }
 }
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/transform.rs b/crates/eucalyptus-core/src/transform.rs
index f4c1dcc..bbac6db 100644
--- a/crates/eucalyptus-core/src/transform.rs
+++ b/crates/eucalyptus-core/src/transform.rs
@@ -1,3 +1,5 @@
+use crate::hierarchy::EntityTransformExt;
+use crate::ptr::WorldPtr;
 use crate::scripting::jni::utils::{FromJObject, ToJObject};
 use crate::scripting::native::DropbearNativeError;
 use crate::scripting::result::DropbearNativeResult;
@@ -5,7 +7,7 @@ use dropbear_engine::entity::{EntityTransform, Transform};
 use glam::{DQuat, DVec3};
 use ::jni::objects::{JObject, JValue};
 use ::jni::JNIEnv;
-use crate::types::NVector3;
+use crate::types::{NTransform, NVector3};
 
 impl FromJObject for Transform {
     fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
@@ -131,247 +133,107 @@ impl ToJObject for EntityTransform {
     }
 }
 
-pub mod shared {
-    use dropbear_engine::entity::EntityTransform;
-    use hecs::{Entity, World};
-
-    pub fn entity_transform_exists_for_entity(world: &World, entity: Entity) -> bool {
-        world.get::<&EntityTransform>(entity).is_ok()
-    }
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.EntityTransformNative", func = "entityTransformExistsForEntity"),
+    c
+)]
+fn exists_for_entity(
+    #[dropbear_macro::define(WorldPtr)]
+    world: &hecs::World,
+    #[dropbear_macro::entity]
+    entity: hecs::Entity,
+) -> DropbearNativeResult<bool> {
+    Ok(world.get::<&EntityTransform>(entity).is_ok())
 }
 
-pub mod jni {
-    #![allow(non_snake_case)]
-    use crate::convert_jlong_to_entity;
-    use crate::hierarchy::EntityTransformExt;
-    use crate::scripting::jni::utils::{FromJObject, ToJObject};
-    use dropbear_engine::entity::EntityTransform;
-    use hecs::World;
-    use jni::objects::{JClass, JObject};
-    use jni::sys::{jboolean, jlong, jobject};
-    use jni::JNIEnv;
-
-    #[unsafe(no_mangle)]
-    pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_entityTransformExistsForEntity(
-        _env: JNIEnv,
-        _class: JClass,
-        world_ptr: jlong,
-        entity_id: jlong,
-    ) -> jboolean {
-        let world = crate::convert_ptr!(world_ptr => World);
-        let entity = convert_jlong_to_entity!(entity_id);
-
-        if world.get::<&EntityTransform>(entity).is_ok() { 1 } else { 0 }
-    }
-
-    #[unsafe(no_mangle)]
-    pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_getLocalTransform(
-        mut env: JNIEnv,
-        _class: JClass,
-        world_ptr: jlong,
-        entity_id: jlong,
-    ) -> jobject {
-        let world = crate::convert_ptr!(world_ptr => World);
-        let entity = convert_jlong_to_entity!(entity_id);
-
-        if let Ok(et) = world.get::<&EntityTransform>(entity) {
-            match et.local().to_jobject(&mut env) {
-                Ok(obj) => obj.into_raw(),
-                Err(_) => std::ptr::null_mut(),
-            }
-        } else {
-            let _ = env.throw_new("java/lang/RuntimeException", "Entity missing EntityTransform");
-            std::ptr::null_mut()
-        }
-    }
-
-    #[unsafe(no_mangle)]
-    pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_setLocalTransform(
-        mut env: JNIEnv,
-        _class: JClass,
-        world_ptr: jlong,
-        entity_id: jlong,
-        transform_obj: JObject,
-    ) {
-        let world = crate::convert_ptr!(world_ptr => World);
-        let entity = convert_jlong_to_entity!(entity_id);
-
-        let new_transform = match dropbear_engine::entity::Transform::from_jobject(&mut env, &transform_obj) {
-            Ok(t) => t,
-            Err(e) => {
-                let _ = env.throw_new("java/lang/IllegalArgumentException", format!("Invalid Transform: {:?}", e));
-                return;
-            }
-        };
-
-        if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
-            *et.local_mut() = new_transform;
-        } else {
-            let _ = env.throw_new("java/lang/RuntimeException", "Entity missing EntityTransform");
-        }
-    }
-
-    #[unsafe(no_mangle)]
-    pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_getWorldTransform(
-        mut env: JNIEnv,
-        _class: JClass,
-        world_ptr: jlong,
-        entity_id: jlong,
-    ) -> jobject {
-        let world = crate::convert_ptr!(world_ptr => World);
-        let entity = convert_jlong_to_entity!(entity_id);
-
-        if let Ok(et) = world.get::<&EntityTransform>(entity) {
-            match et.world().to_jobject(&mut env) {
-                Ok(obj) => obj.into_raw(),
-                Err(_) => std::ptr::null_mut(),
-            }
-        } else {
-            let _ = env.throw_new("java/lang/RuntimeException", "Entity missing EntityTransform");
-            std::ptr::null_mut()
-        }
-    }
-
-    #[unsafe(no_mangle)]
-    pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_setWorldTransform(
-        mut env: JNIEnv,
-        _class: JClass,
-        world_ptr: jlong,
-        entity_id: jlong,
-        transform_obj: JObject,
-    ) {
-        let world = crate::convert_ptr!(world_ptr => World);
-        let entity = convert_jlong_to_entity!(entity_id);
-
-        let new_transform = match dropbear_engine::entity::Transform::from_jobject(&mut env, &transform_obj) {
-            Ok(t) => t,
-            Err(_) => return,
-        };
-
-        if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
-            *et.world_mut() = new_transform;
-        } else {
-            let _ = env.throw_new("java/lang/RuntimeException", "Entity missing EntityTransform");
-        }
-    }
-
-    #[unsafe(no_mangle)]
-    pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_propagateTransform(
-        mut env: JNIEnv,
-        _class: JClass,
-        world_ptr: jlong,
-        entity_id: jlong,
-    ) -> jobject {
-        let world = crate::convert_ptr!(world_ptr => World);
-        let entity = convert_jlong_to_entity!(entity_id);
-
-        if let Ok(et) = world.get::<&EntityTransform>(entity) {
-            let propagated = et.propagate(&world, entity);
-            match propagated.to_jobject(&mut env) {
-                Ok(obj) => obj.into_raw(),
-                Err(_) => std::ptr::null_mut(),
-            }
-        } else {
-            let _ = env.throw_new("java/lang/RuntimeException", "Entity missing EntityTransform");
-            std::ptr::null_mut()
-        }
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.EntityTransformNative", func = "getLocalTransform"),
+    c
+)]
+fn get_local_transform(
+    #[dropbear_macro::define(WorldPtr)]
+    world: &hecs::World,
+    #[dropbear_macro::entity]
+    entity: hecs::Entity,
+) -> DropbearNativeResult<NTransform> {
+    if let Ok(et) = world.get::<&EntityTransform>(entity) {
+        Ok((*et.local()).into())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
     }
 }
 
-#[dropbear_macro::impl_c_api]
-pub mod native {
-    use crate::hierarchy::EntityTransformExt;
-    use crate::types::{NTransform as Transform};
-    use dropbear_engine::entity::EntityTransform;
-    use hecs::{Entity, World};
-
-    use crate::convert_ptr;
-    use crate::ptr::WorldPtr;
-    use crate::scripting::native::DropbearNativeError;
-    use crate::scripting::result::DropbearNativeResult;
-
-    pub fn dropbear_entity_transform_exists_for_entity(
-        world_ptr: WorldPtr,
-        entity_id: u64,
-    ) -> DropbearNativeResult<bool> {
-        let world = convert_ptr!(world_ptr => World);
-        let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?;
-        DropbearNativeResult::Ok(world.get::<&EntityTransform>(entity).is_ok())
-    }
-
-    pub fn dropbear_get_local_transform(
-        world_ptr: WorldPtr,
-        entity_id: u64,
-    ) -> DropbearNativeResult<Transform> {
-        let world = convert_ptr!(world_ptr => World);
-        let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?;
-
-        if let Ok(et) = world.get::<&EntityTransform>(entity) {
-            DropbearNativeResult::Ok(Transform::from(et.local().clone()))
-        } else {
-            DropbearNativeResult::Err(DropbearNativeError::NoSuchComponent)
-        }
-    }
-
-
-    pub fn dropbear_set_local_transform(
-        world_ptr: WorldPtr,
-        entity_id: u64,
-        value: Transform,
-    ) -> DropbearNativeResult<()> {
-        let world = convert_ptr!(world_ptr => World);
-        let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?;
-
-        if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
-            *et.local_mut() = dropbear_engine::entity::Transform::from(value);
-            DropbearNativeResult::Ok(())
-        } else {
-            DropbearNativeResult::Err(DropbearNativeError::NoSuchComponent)
-        }
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.EntityTransformNative", func = "setLocalTransform"),
+    c
+)]
+fn set_local_transform(
+    #[dropbear_macro::define(WorldPtr)]
+    world: &hecs::World,
+    #[dropbear_macro::entity]
+    entity: hecs::Entity,
+    transform: &NTransform
+) -> DropbearNativeResult<()> {
+    if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
+        *et.local_mut() = (*transform).into();
+
+        Ok(())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
     }
+}
 
-    pub fn dropbear_get_world_transform(
-        world_ptr: WorldPtr,
-        entity_id: u64,
-    ) -> DropbearNativeResult<Transform> {
-        let world = convert_ptr!(world_ptr => World);
-        let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?;
-
-        if let Ok(et) = world.get::<&EntityTransform>(entity) {
-            DropbearNativeResult::Ok(Transform::from(et.world().clone()))
-        } else {
-            DropbearNativeResult::Err(DropbearNativeError::NoSuchComponent)
-        }
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.EntityTransformNative", func = "getWorldTransform"),
+    c
+)]
+fn get_world_transform(
+    #[dropbear_macro::define(WorldPtr)]
+    world: &hecs::World,
+    #[dropbear_macro::entity]
+    entity: hecs::Entity,
+) -> DropbearNativeResult<NTransform> {
+    if let Ok(et) = world.get::<&EntityTransform>(entity) {
+        Ok((*et.world()).into())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
     }
+}
 
-    pub fn dropbear_set_world_transform(
-        world_ptr: WorldPtr,
-        entity_id: u64,
-        value: Transform,
-    ) -> DropbearNativeResult<()> {
-        let world = convert_ptr!(world_ptr => World);
-        let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?;
-
-        if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
-            *et.world_mut() = dropbear_engine::entity::Transform::from(value);
-            DropbearNativeResult::Ok(())
-        } else {
-            DropbearNativeResult::Err(DropbearNativeError::NoSuchComponent)
-        }
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.EntityTransformNative", func = "setWorldTransform"),
+    c
+)]
+fn set_world_transform(
+    #[dropbear_macro::define(WorldPtr)]
+    world: &hecs::World,
+    #[dropbear_macro::entity]
+    entity: hecs::Entity,
+    transform: &NTransform
+) -> DropbearNativeResult<()> {
+    if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
+        *et.world_mut() = (*transform).into();
+
+        Ok(())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
     }
+}
 
-    pub fn dropbear_propagate_transform(
-        world_ptr: WorldPtr,
-        entity_id: u64,
-    ) -> DropbearNativeResult<Transform> {
-        let world = convert_ptr!(world_ptr => World);
-        let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?;
-
-        if let Ok(et) = world.get::<&EntityTransform>(entity) {
-            let propagated = et.propagate(world, entity);
-            DropbearNativeResult::Ok(Transform::from(propagated))
-        } else {
-            DropbearNativeResult::Err(DropbearNativeError::NoSuchComponent)
-        }
+#[dropbear_macro::export(
+    kotlin(class = "com.dropbear.components.EntityTransformNative", func = "propogateTransform"),
+    c
+)]
+fn propogate_transform(
+    #[dropbear_macro::define(WorldPtr)]
+    world: &hecs::World,
+    #[dropbear_macro::entity]
+    entity: hecs::Entity,
+) -> DropbearNativeResult<NTransform> {
+    if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) {
+        let result = et.propagate(world, entity);
+        Ok(result.into())
+    } else {
+        Err(DropbearNativeError::MissingComponent)
     }
 }
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/types.rs b/crates/eucalyptus-core/src/types.rs
index 0ce4d72..9d8365b 100644
--- a/crates/eucalyptus-core/src/types.rs
+++ b/crates/eucalyptus-core/src/types.rs
@@ -472,6 +472,51 @@ impl From<NTransform> for Transform {
     }
 }
 
+impl FromJObject for NTransform {
+    fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> {
+        let pos_val = env.get_field(obj, "position", "Lcom/dropbear/math/Vector3d;")
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
+
+        let pos_obj = pos_val.l()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let rot_val = env.get_field(obj, "rotation", "Lcom/dropbear/math/Quaterniond;")
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
+
+        let rot_obj = rot_val.l()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let scale_val = env.get_field(obj, "scale", "Lcom/dropbear/math/Vector3d;")
+            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?;
+
+        let scale_obj = scale_val.l()
+            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;
+
+        let position: DVec3 = NVector3::from_jobject(env, &pos_obj)?.into();
+        let scale: DVec3 = NVector3::from_jobject(env, &scale_obj)?.into();
+
+        let mut get_double = |field: &str| -> DropbearNativeResult<f64> {
+            env.get_field(&rot_obj, field, "D")
+                .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
+                .d()
+                .map_err(|_| DropbearNativeError::JNIUnwrapFailed)
+        };
+
+        let rx = get_double("x")?;
+        let ry = get_double("y")?;
+        let rz = get_double("z")?;
+        let rw = get_double("w")?;
+
+        let rotation = DQuat::from_xyzw(rx, ry, rz, rw);
+
+        Ok(NTransform {
+            position: position.into(),
+            rotation: rotation.into(),
+            scale: scale.into(),
+        })
+    }
+}
+
 impl ToJObject for NTransform {
     fn to_jobject<'a>(&self, env: &mut JNIEnv<'a>) -> DropbearNativeResult<JObject<'a>> {
         let class = env
diff --git a/crates/eucalyptus-core/src/utils.rs b/crates/eucalyptus-core/src/utils.rs
index 6214acc..2ba682f 100644
--- a/crates/eucalyptus-core/src/utils.rs
+++ b/crates/eucalyptus-core/src/utils.rs
@@ -2,6 +2,7 @@
 
 pub mod option;
 pub mod hashmap;
+pub mod mesh_loader;
 
 use crate::states::Node;
 use dropbear_engine::utils::{ResourceReference, ResourceReferenceType, relative_path_from_euca};
diff --git a/crates/eucalyptus-core/src/utils/mesh_loader.rs b/crates/eucalyptus-core/src/utils/mesh_loader.rs
new file mode 100644
index 0000000..f42281a
--- /dev/null
+++ b/crates/eucalyptus-core/src/utils/mesh_loader.rs
@@ -0,0 +1,84 @@
+use std::sync::Arc;
+
+use dropbear_engine::asset::ASSET_REGISTRY;
+use dropbear_engine::entity::MeshRenderer;
+use dropbear_engine::graphics::SharedGraphicsContext;
+use dropbear_engine::model::Model;
+use dropbear_engine::procedural::ProcedurallyGeneratedObject;
+use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
+
+use crate::states::SerializedMeshRenderer;
+use crate::utils::ResolveReference;
+
+pub async fn load_mesh_renderer_from_serialized(
+    serialized: &SerializedMeshRenderer,
+    graphics: Arc<SharedGraphicsContext>,
+    label: &str,
+) -> anyhow::Result<MeshRenderer> {
+    let import_scale = serialized.import_scale.unwrap_or(1.0);
+
+    let handle = match &serialized.handle.ref_type {
+        ResourceReferenceType::None => {
+            anyhow::bail!("Resource reference type is None for '{}'", label);
+        }
+        ResourceReferenceType::Unassigned { id } => {
+            let model = Model {
+                label: "None".to_string(),
+                hash: *id,
+                path: ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: *id }),
+                meshes: Vec::new(),
+                materials: Vec::new(),
+                skins: Vec::new(),
+                animations: Vec::new(),
+                nodes: Vec::new(),
+            };
+
+            let mut registry = ASSET_REGISTRY.write();
+            if let Some(existing) = registry.model_handle_by_hash(*id) {
+                existing
+            } else {
+                registry.add_model(model)
+            }
+        }
+        ResourceReferenceType::File(reference) => {
+            let path = serialized.handle.resolve()?;
+            log::debug!("Path for entity {} is {} from reference {}", label, path.display(), reference);
+            let buffer = std::fs::read(&path)?;
+            Model::load_from_memory_raw(graphics.clone(), buffer, Some(label), ASSET_REGISTRY.clone()).await?
+        }
+        ResourceReferenceType::Bytes(bytes) => {
+            log::info!("Loading entity '{}' from bytes [Len: {}]", label, bytes.len());
+            Model::load_from_memory_raw(graphics.clone(), bytes, Some(label), ASSET_REGISTRY.clone()).await?
+        }
+        ResourceReferenceType::ProcObj(obj) => match obj {
+            dropbear_engine::procedural::ProcObj::Cuboid { size_bits } => {
+                let size = [
+                    f32::from_bits(size_bits[0]),
+                    f32::from_bits(size_bits[1]),
+                    f32::from_bits(size_bits[2]),
+                ];
+                log::info!("Loading entity '{}' from cuboid: {:?}", label, size);
+
+                let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64);
+                ProcedurallyGeneratedObject::cuboid(size_vec).build_model(
+                    graphics.clone(),
+                    None,
+                    Some(label),
+                    ASSET_REGISTRY.clone(),
+                )
+            }
+        },
+    };
+
+    let mut renderer = MeshRenderer::from_handle(handle);
+    renderer.set_import_scale(import_scale);
+
+    if serialized.texture_override.is_some() {
+        log::debug!(
+            "texture_override is set for '{}' but not applied yet", 
+            label
+        );
+    }
+
+    Ok(renderer)
+}
diff --git a/crates/eucalyptus-editor/src/editor/build_console.rs b/crates/eucalyptus-editor/src/editor/build_console.rs
new file mode 100644
index 0000000..5242498
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/build_console.rs
@@ -0,0 +1,137 @@
+impl<'a> EditorTabViewer<'a> {
+    pub fn build_console(&mut self, ui: &mut egui::Ui) {
+        fn analyse_error(log: &Vec<String>) -> Vec<ConsoleItem> {
+                    fn parse_compiler_location(
+                        line: &str,
+                    ) -> Option<(ErrorLevel, PathBuf, String)> {
+                        let trimmed = line.trim_start();
+                        let (error_level, rest) =
+                            if let Some(r) = trimmed.strip_prefix("e: file:///") {
+                                (ErrorLevel::Error, r)
+                            } else if let Some(r) = trimmed.strip_prefix("w: file:///") {
+                                (ErrorLevel::Warn, r)
+                            } else {
+                                return None;
+                            };
+
+                        let location = rest.split_whitespace().next()?;
+
+                        let mut segments = location.rsplitn(3, ':');
+                        let column = segments.next()?;
+                        let row = segments.next()?;
+                        let path = segments.next()?;
+
+                        Some((error_level, PathBuf::from(path), format!("{row}:{column}")))
+                    }
+
+                    let mut list: Vec<ConsoleItem> = Vec::new();
+                    for (index, line) in log.iter().enumerate() {
+                        if line.contains("The required library") {
+                            list.push(ConsoleItem {
+                                error_level: ErrorLevel::Error,
+                                msg: line.clone(),
+                                file_location: None,
+                                line_ref: None,
+                                id: index as u64,
+                            });
+                        } else if let Some((error_level, path, loc)) = parse_compiler_location(line) {
+                            list.push(ConsoleItem {
+                                error_level,
+                                msg: line.clone(),
+                                file_location: Some(path),
+                                line_ref: Some(loc),
+                                id: index as u64,
+                            });
+                        } else {
+                            list.push(ConsoleItem {
+                                error_level: ErrorLevel::Info,
+                                msg: line.clone(),
+                                file_location: None,
+                                line_ref: None,
+                                id: index as u64,
+                            });
+                        }
+                    }
+                    list
+                }
+
+                let logs = analyse_error(&self.build_logs);
+
+                egui::ScrollArea::vertical()
+                    .auto_shrink([false, false])
+                    .stick_to_bottom(true)
+                    .show(ui, |ui| {
+                        if logs.is_empty() {
+                            ui.label("Build output will appear here once available.");
+                            return;
+                        }
+
+                        for item in &logs {
+                            let (bg_color, text_color, stroke_color) = match item.error_level {
+                                ErrorLevel::Error => (
+                                    egui::Color32::from_rgb(60, 20, 20),
+                                    egui::Color32::from_rgb(255, 200, 200),
+                                    egui::Color32::from_rgb(255, 200, 200),
+                                ),
+                                ErrorLevel::Warn => (
+                                    egui::Color32::from_rgb(40, 40, 10),
+                                    egui::Color32::from_rgb(255, 255, 200),
+                                    egui::Color32::from_rgb(255, 255, 200),
+                                ),
+                                ErrorLevel::Info => (
+                                    egui::Color32::TRANSPARENT,
+                                    ui.style().visuals.text_color(),
+                                    egui::Color32::TRANSPARENT,
+                                ),
+                            };
+
+                            if matches!(item.error_level, ErrorLevel::Info) {
+                                ui.label(RichText::new(&item.msg).monospace());
+                            } else {
+                                let available_width = ui.available_width();
+                                let frame = egui::Frame::new()
+                                    .inner_margin(Margin::symmetric(8, 6))
+                                    .fill(bg_color)
+                                    .stroke(egui::Stroke::new(1.0, stroke_color));
+
+                                let response = frame
+                                    .show(ui, |ui| {
+                                        ui.set_width(available_width - 10.0);
+                                        ui.horizontal(|ui| {
+                                            ui.label(RichText::new(&item.msg).color(text_color).monospace());
+                                        });
+                                    })
+                                    .response;
+
+                                if response.clicked() {
+                                    log::debug!("Log item clicked: {}", &item.id);
+                                    if let (Some(path), Some(loc)) =
+                                        (&item.file_location, &item.line_ref)
+                                    {
+                                        let location_arg = format!("{}:{}", path.display(), loc);
+
+                                        match std::process::Command::new("code")
+                                            .args(["-g", &location_arg])
+                                            .spawn()
+                                            .map(|_| ())
+                                        {
+                                            Ok(()) => {
+                                                log::info!(
+                                                    "Launched Visual Studio Code at the error: {}",
+                                                    &location_arg
+                                                );
+                                            }
+                                            Err(e) => {
+                                                warn!(
+                                                    "Failed to open '{}' in VS Code: {}",
+                                                    location_arg, e
+                                                );
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    });
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/dock.rs b/crates/eucalyptus-editor/src/editor/dock.rs
index b09122e..8d2ca87 100644
--- a/crates/eucalyptus-editor/src/editor/dock.rs
+++ b/crates/eucalyptus-editor/src/editor/dock.rs
@@ -214,751 +214,16 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
 
         match tab {
             EditorTab::Viewport => {
-                log_once::debug_once!("Viewport focused");
-
-                let available_rect = ui.available_rect_before_wrap();
-                let available_size = available_rect.size();
-
-                let tex_aspect = self.tex_size.width as f32 / self.tex_size.height as f32;
-                let available_aspect = available_size.x / available_size.y;
-
-                let (display_width, display_height) = if available_aspect > tex_aspect {
-                    let height = available_size.y * 0.95;
-                    let width = height * tex_aspect;
-                    (width, height)
-                } else {
-                    let width = available_size.x * 0.95;
-                    let height = width / tex_aspect;
-                    (width, height)
-                };
-
-                let center_x = available_rect.center().x;
-                let center_y = available_rect.center().y;
-
-                let image_rect = egui::Rect::from_center_size(
-                    egui::pos2(center_x, center_y),
-                    egui::vec2(display_width, display_height),
-                );
-
-                let (_rect, _response) =
-                    ui.allocate_exact_size(available_size, egui::Sense::click_and_drag());
-
-                let _image_response = ui.allocate_rect(image_rect, egui::Sense::click_and_drag());
-
-                ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| {
-                    ui.add_sized(
-                        [display_width, display_height],
-                        egui::Image::new((self.view, [display_width, display_height].into()))
-                            .fit_to_exact_size([display_width, display_height].into()),
-                    )
-                });
-
-                let snapping = ui.input(|input| input.modifiers.shift);
-
-                // Note to self: fuck you >:(
-                // Note to self: ok wow thats pretty rude im trying my best >﹏<
-                // Note to self: finally holy shit i got it working
-                let active_cam = self.active_camera.lock();
-                if let Some(active_camera) = *active_cam {
-                    let camera_data = {
-                        if let Ok((cam, _comp)) = self
-                            .world
-                            .query_one::<(&Camera, &CameraComponent)>(active_camera).get()
-                        {
-                            Some(cam.clone())
-                        } else {
-                            log::warn!("Queried camera but found no value");
-                            None
-                        }
-                    };
-
-                    if let Some(camera) = camera_data {
-                        self.gizmo.update_config(GizmoConfig {
-                            view_matrix: camera.view_mat.into(),
-                            projection_matrix: camera.proj_mat.into(),
-                            viewport: image_rect,
-                            modes: *self.gizmo_mode,
-                            orientation: *self.gizmo_orientation,
-                            snapping,
-                            snap_distance: 1.0,
-                            ..Default::default()
-                        });
-                    }
-                }
-                if !matches!(self.viewport_mode, ViewportMode::None)
-                    && let Some(entity_id) = self.selected_entity
-                {
-                    let mut handled = false;
-
-                    if let Ok(entity_transform) = self.world.query_one::<&mut EntityTransform>(*entity_id).get()
-                    {
-                        let was_focused = cfg.is_focused;
-                        cfg.is_focused = self.gizmo.is_focused();
-
-                        if cfg.is_focused && !was_focused {
-                            cfg.entity_transform_original = Some(*entity_transform);
-                        }
-
-                        let synced = entity_transform.sync();
-                        let gizmo_transform =
-                            transform_gizmo_egui::math::Transform::from_scale_rotation_translation(
-                                synced.scale,
-                                synced.rotation,
-                                synced.position,
-                            );
-
-                        if let Some((_result, new_transforms)) =
-                            self.gizmo.interact(ui, &[gizmo_transform])
-                            && let Some(new_transform) = new_transforms.first()
-                        {
-                            let new_synced_pos: glam::DVec3 = new_transform.translation.into();
-                            let new_synced_rot: glam::DQuat = new_transform.rotation.into();
-                            let new_synced_scale: glam::DVec3 = new_transform.scale.into();
-
-                            match *self.gizmo_orientation {
-                                GizmoOrientation::Global => {
-                                    let local = *entity_transform.local();
-
-                                    let safe_local_scale = glam::DVec3::new(
-                                        if local.scale.x.abs() < 1e-6 { 1.0 } else { local.scale.x },
-                                        if local.scale.y.abs() < 1e-6 { 1.0 } else { local.scale.y },
-                                        if local.scale.z.abs() < 1e-6 { 1.0 } else { local.scale.z },
-                                    );
-
-                                    let new_world_scale = new_synced_scale / safe_local_scale;
-                                    let new_world_rot = new_synced_rot * local.rotation.inverse();
-
-                                    let scaled_local_pos = local.position * new_world_scale;
-                                    let rotated_local_pos = new_world_rot * scaled_local_pos;
-                                    let new_world_pos = new_synced_pos - rotated_local_pos;
-
-                                    let world_transform = entity_transform.world_mut();
-                                    world_transform.position = new_world_pos;
-                                    world_transform.rotation = new_world_rot;
-                                    world_transform.scale = new_world_scale;
-                                }
-                                GizmoOrientation::Local => {
-                                    let world_transform = entity_transform.world();
-                                    let world_scale = world_transform.scale;
-                                    let world_rot = world_transform.rotation;
-                                    let world_pos = world_transform.position;
-
-                                    let safe_world_scale = glam::DVec3::new(
-                                        if world_scale.x.abs() < 1e-6 { 1.0 } else { world_scale.x },
-                                        if world_scale.y.abs() < 1e-6 { 1.0 } else { world_scale.y },
-                                        if world_scale.z.abs() < 1e-6 { 1.0 } else { world_scale.z },
-                                    );
-
-                                    let local_transform = entity_transform.local_mut();
-                                    local_transform.scale = new_synced_scale / safe_world_scale;
-                                    local_transform.rotation = world_rot.inverse() * new_synced_rot;
-
-                                    let delta_pos = new_synced_pos - world_pos;
-                                    let unrotated_delta = world_rot.inverse() * delta_pos;
-                                    local_transform.position = unrotated_delta / safe_world_scale;
-                                }
-                            }
-                        }
-
-                        if was_focused && !cfg.is_focused {
-                            if let Some(original) = cfg.entity_transform_original {
-                                if original != *entity_transform {
-                                    UndoableAction::push_to_undo(
-                                        self.undo_stack,
-                                        UndoableAction::EntityTransform(*entity_id, original),
-                                    );
-                                    log::debug!("Pushed entity transform action to stack");
-                                }
-                            }
-                        }
-                        handled = true;
-                    }
-
-                    if !handled {
-                        if let Ok(transform) = self.world.query_one::<&mut Transform>(*entity_id).get()
-                        {
-                            let was_focused = cfg.is_focused;
-                            cfg.is_focused = self.gizmo.is_focused();
-
-                            if cfg.is_focused && !was_focused {
-                                cfg.old_pos = *transform;
-                            }
-
-                            let gizmo_transform =
-                                        transform_gizmo_egui::math::Transform::from_scale_rotation_translation(
-                                            transform.scale,
-                                            transform.rotation,
-                                            transform.position,
-                                        );
-
-                            if let Some((_result, new_transforms)) =
-                                self.gizmo.interact(ui, &[gizmo_transform])
-                                && let Some(new_transform) = new_transforms.first()
-                            {
-                                transform.position = new_transform.translation.into();
-                                transform.rotation = new_transform.rotation.into();
-                                transform.scale = new_transform.scale.into();
-                            }
-
-                            if was_focused && !cfg.is_focused {
-                                let transform_changed = cfg.old_pos.position != transform.position
-                                    || cfg.old_pos.rotation != transform.rotation
-                                    || cfg.old_pos.scale != transform.scale;
-
-                                if transform_changed {
-                                    UndoableAction::push_to_undo(
-                                        self.undo_stack,
-                                        UndoableAction::Transform(*entity_id, cfg.old_pos),
-                                    );
-                                    log::debug!("Pushed transform action to stack");
-                                }
-                            }
-                        }
-                    }
-                }
+                self.viewport_tab(ui);
             }
             EditorTab::ModelEntityList => {
-                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"),));
-                                    ui.close();
-                                }
-                            }),
-                    );
-                    // 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: &mut World,
-                        registry: &ComponentRegistry,
-                        cfg: &mut StaticallyKept,
-                        signal: &mut Signal,
-                    ) -> anyhow::Result<()> {
-                        let entity_id = entity.to_bits().get();
-                        let label = if let Ok(label) = world.query_one::<&Label>(entity).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() {
-                                            let child = world.spawn((Label::new("New Entity"),));
-                                            Hierarchy::set_parent(world, child, entity);
-                                            ui.close();
-                                        }
-                                    });
-                                    ui.menu_button("Add", |ui| {
-                                        log_once::debug_once!("Available components: ");
-                                        for (id, name) in registry.iter_available_components() {
-                                            log_once::debug_once!("id: {}, name: {}", id, name);
-                                            // if name.contains("EntityTransform") {
-                                            //     continue;
-                                            // }
-                                            let short_name =
-                                                name.split("::").last().unwrap_or(name);
-                                            let display_name =
-                                                if short_name == "SerializedMeshRenderer" {
-                                                    "MeshRenderer"
-                                                } else {
-                                                    short_name
-                                                };
-
-                                            if ui.button(display_name).clicked() {
-                                                if name.contains("MeshRenderer") {
-                                                    *signal = Signal::AddComponent(
-                                                        entity,
-                                                        "MeshRenderer".to_string(),
-                                                    );
-                                                } else if name.contains("CameraComponent")
-                                                    || name.contains("Camera3D")
-                                                {
-                                                    *signal = Signal::AddComponent(
-                                                        entity,
-                                                        "CameraComponent".to_string(),
-                                                    );
-                                                } else if name.contains("Light") {
-                                                    *signal = Signal::AddComponent(
-                                                        entity,
-                                                        "Light".to_string(),
-                                                    );
-                                                } else {
-                                                    if let Some(comp) =
-                                                        registry.create_default_component(id)
-                                                    {
-                                                        let mut builder = EntityBuilder::new();
-                                                        if let Ok(_) = registry
-                                                            .deserialize_into_builder(
-                                                                comp.as_ref(),
-                                                                &mut builder,
-                                                            )
-                                                        {
-                                                            let _ = world
-                                                                .insert(entity, builder.build());
-                                                        }
-                                                    }
-                                                }
-                                                ui.close();
-                                            }
-                                        }
-                                    });
-                                }),
-                        );
-
-                        let components = registry.extract_all_components(world, entity);
-
-                        for component in components.iter() {
-                            // if component.type_name().contains("EntityTransform") {
-                            //     continue;
-                            // }
-                            let Some(component_type_id) =
-                                registry.id_for_component(component.as_ref())
-                            else {
-                                log_once::warn_once!(
-                                    "Component '{}' missing registry id, skipping tree entry",
-                                    component.type_name()
-                                );
-                                continue;
-                            };
-                            let component_node_id =
-                                cfg.component_node_id(entity, component_type_id);
-                            let display =
-                                format!("{} (id #{component_type_id})", component.display_name());
-
-                            let has_rigidbody = world.get::<&RigidBody>(entity).is_ok();
-                            let has_collider = world.get::<&ColliderGroup>(entity).is_ok();
-
-                            let node = NodeBuilder::leaf(component_node_id)
-                                .label_ui(|ui| {
-                                    ui.label(display.clone());
-
-                                    if has_rigidbody && !has_collider && component.type_name().contains("RigidBody") {
-                                        ui.add_space(4.0);
-                                        ui.small_button("⚠")
-                                            .on_hover_text("RigidBody has no colliders! Add the ColliderGroup component");
-                                    }
-                                })
-                                .context_menu(|ui| {
-                                    if ui.button("Remove Component").clicked() {
-                                        registry.remove_component_by_id(
-                                            world,
-                                            entity,
-                                            component_type_id,
-                                        );
-                                        ui.close();
-                                    }
-                                });
-
-                            builder.node(node);
-                        }
-
-                        let children_entities = if let Ok(children) = world.get::<&Children>(entity) {
-                            children.children().to_vec()
-                        } else {
-                            Vec::new()
-                        };
-
-                        for child in children_entities {
-                            if let Err(e) =
-                                add_entity_to_tree(builder, child, world, registry, cfg, signal)
-                            {
-                                log_once::error_once!(
-                                    "Failed to add child entity to tree, skipping: {}",
-                                    e
-                                );
-                                continue;
-                            }
-                        }
-
-                        builder.close_dir();
-                        Ok(())
-                    }
-
-                    let root_entities: Vec<Entity> = self
-                        .world
-                        .query::<Entity>()
-                        .without::<&Parent>()
-                        .iter()
-                        .map(|e| e)
-                        .collect();
-
-                    for entity in root_entities {
-                        if let Err(e) = add_entity_to_tree(
-                            builder,
-                            entity,
-                            &mut self.world,
-                            &self.component_registry,
-                            &mut cfg,
-                            self.signal,
-                        ) {
-                            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) => {
-                            log_once::debug_once!("Selected: {:?}", items);
-                            self.handle_tree_selection(&mut cfg, &items);
-                        }
-                        egui_ltreeview::Action::Move(drag_and_drop) => {
-                            log_once::debug_once!("Moved: {:?}", drag_and_drop);
-                            self.handle_tree_move(&mut cfg, &drag_and_drop);
-                        }
-                        egui_ltreeview::Action::Drag(drag_and_drop) => {
-                            log_once::debug_once!("Dragged: {:?}", drag_and_drop);
-                            self.handle_tree_drag(&mut cfg, &drag_and_drop);
-                        }
-                        egui_ltreeview::Action::Activate(activate) => {
-                            log_once::debug_once!("Activated: {:?}", activate);
-                            self.handle_tree_activate(&mut cfg, &activate);
-                        }
-                        egui_ltreeview::Action::DragExternal(_drag_and_drop_external) => {}
-                        egui_ltreeview::Action::MoveExternal(_drag_and_drop_external) => {}
-                    }
-                }
+                self.entity_list(ui);
             }
             EditorTab::AssetViewer => {
                 self.show_asset_viewer(&mut cfg, ui);
             }
             EditorTab::ResourceInspector => {
-                let local_scene_settings = cfg.root_node_selected;
-                let mut local_add_collider: Option<Entity> = None;
-                
-                if let Some(entity) = self.selected_entity {
-                    let mut local_set_initial_camera = false;
-                    let mut inspect_entity = *entity;
-                    let world = &self.world;
-
-                    if !cfg.root_node_selected {
-                        if let Ok((label, )) = world.query_one::<(&mut Label,)>(inspect_entity).get() {
-                            label.inspect(
-                                &mut inspect_entity,
-                                &mut cfg,
-                                ui,
-                                self.undo_stack,
-                                self.signal,
-                                &mut String::new(),
-                            );
-
-                            ui.label(format!("Entity ID: {}", inspect_entity.id()));
-
-                            ui.separator();
-
-                            // mesh renderer
-                            if let Ok(e) = world.query_one::<&mut MeshRenderer>(inspect_entity).get()
-                            {
-                                CollapsingHeader::new("MeshRenderer").default_open(true).show(ui, |ui| {
-                                    e.inspect(
-                                        &mut inspect_entity,
-                                        &mut cfg,
-                                        ui,
-                                        self.undo_stack,
-                                        self.signal,
-                                        &mut String::new(),
-                                    );
-                                });
-                            }
-
-                            // entity transform
-                            if let Ok(t) = world.query_one::<&mut EntityTransform>(inspect_entity).get()
-                            {
-                                CollapsingHeader::new("Transform").default_open(true).show(ui, |ui| {
-                                    t.inspect(
-                                        &mut inspect_entity,
-                                        &mut cfg,
-                                        ui,
-                                        self.undo_stack,
-                                        self.signal,
-                                        &mut String::new(),
-                                    );
-                                });
-                                ui.separator();
-                            }
-
-                            // custom properties
-                            if let Ok(props) = world.query_one::<&mut CustomProperties>(inspect_entity).get()
-                            {
-                                CollapsingHeader::new("Custom Properties").default_open(true).show(ui, |ui| {
-                                    props.inspect(
-                                        &mut inspect_entity,
-                                        &mut cfg,
-                                        ui,
-                                        self.undo_stack,
-                                        self.signal,
-                                        label.as_mut_string(),
-                                    );
-                                });
-                                ui.separator();
-                            }
-
-                            // camera
-                            if let Ok((camera, camera_component)) = world
-                                .query_one::<(&mut Camera, &mut CameraComponent)>(inspect_entity).get()
-                            {
-                                CollapsingHeader::new("Camera").default_open(true).show(ui, |ui| {
-                                    camera.inspect(
-                                        &mut inspect_entity,
-                                        &mut cfg,
-                                        ui,
-                                        self.undo_stack,
-                                        self.signal,
-                                        &mut String::new(),
-                                    );
-
-                                    ui.separator();
-
-                                    camera_component.inspect(
-                                        &mut inspect_entity,
-                                        &mut cfg,
-                                        ui,
-                                        self.undo_stack,
-                                        self.signal,
-                                        &mut camera.label.clone(),
-                                    );
-
-                                    ui.separator();
-
-                                    // camera controller
-                                    let mut active_camera = self.active_camera.lock();
-
-                                    if active_camera.equivalent(&Some(*entity)) {
-                                        ui.label("Status: Currently viewing through camera");
-                                    } else {
-                                        ui.label("Status: Not viewing through this camera");
-                                    }
-
-                                    if ui.button("Set as active camera").clicked() {
-                                        *active_camera = Some(*entity);
-                                        log::info!(
-                                            "Currently viewing from camera angle '{}'",
-                                            camera.label
-                                        );
-                                    }
-
-                                    if camera_component.starting_camera {
-                                        if ui.button("Stop making camera initial").clicked() {
-                                            log::debug!("'Stop making camera initial' button clicked");
-                                            camera_component.starting_camera = false;
-                                            success!("Removed {} from starting camera", camera.label);
-                                        }
-                                    } else if ui.button("Set as initial camera").clicked() {
-                                        log::debug!("'Set as initial camera' button clicked");
-                                        if matches!(camera_component.camera_type, CameraType::Debug) {
-                                            warn!(
-                                                "Cannot set any cameras of type 'Debug' to initial camera"
-                                            );
-                                        } else {
-                                            local_set_initial_camera = true
-                                        }
-                                    }
-                                });
-                                ui.separator();
-                            }
-
-                            // light
-                            if let Ok((light, comp, transform)) = world.query_one::<(&mut Light, &mut LightComponent, &mut Transform)>(inspect_entity).get()
-                            {
-                                light.transform = *transform;
-                                light.light_component = comp.clone();
-
-                                light.inspect(
-                                    entity,
-                                    &mut cfg,
-                                    ui,
-                                    self.undo_stack,
-                                    self.signal,
-                                    &mut String::new(),
-                                );
-
-                                *transform = light.transform;
-                                *comp = light.light_component.clone();
-                                ui.separator();
-                            }
-
-                            // script
-                            if let Ok((script, ui_c)) = world.query_one::<(Option<&mut Script>, Option<&mut UIComponent>)>(*entity).get()
-                            {
-                                CollapsingHeader::new("Script").default_open(true).show(ui, |ui| {
-                                    if let Some(s) = script {
-                                        s.inspect(
-                                            entity,
-                                            &mut cfg,
-                                            ui,
-                                            self.undo_stack,
-                                            self.signal,
-                                            label.as_mut_string(),
-                                        );
-                                    }
-                                    
-                                    if let Some(ui_c) = ui_c {
-                                        CollapsingHeader::new("UI").default_open(true).show(ui, |ui| {
-                                            ui_c.inspect(
-                                                entity,
-                                                &mut cfg,
-                                                ui,
-                                                self.undo_stack,
-                                                self.signal,
-                                                label.as_mut_string(),
-                                            );
-                                        });
-                                    }
-                                });
-                                ui.separator();
-                            }
-
-                            // physics
-                            if let Ok((rigid, colliders, kcc)) = world.query_one::<(Option<&mut RigidBody>, Option<&mut ColliderGroup>, Option<&mut KCC>)>(*entity).get()
-                            {
-                                if rigid.is_some() || colliders.is_some() || kcc.is_some() {
-                                    CollapsingHeader::new("Physics").default_open(true).show(ui, |ui| {
-
-                                        if let Some(kcc) = kcc {
-                                            CollapsingHeader::new("Kinematic Character Controller").default_open(true).show(ui, |ui| {
-                                                kcc.inspect(
-                                                    entity,
-                                                    &mut cfg,
-                                                    ui,
-                                                    self.undo_stack,
-                                                    self.signal,
-                                                    label.as_mut_string(),
-                                                );
-                                            });
-                                            ui.separator();
-                                        }
-
-                                        if let Some(rigid) = rigid {
-                                            CollapsingHeader::new("RigidBody").default_open(true).show(ui, |ui| {
-                                                rigid.inspect(
-                                                    entity,
-                                                    &mut cfg,
-                                                    ui,
-                                                    self.undo_stack,
-                                                    self.signal,
-                                                    label.as_mut_string(),
-                                                );
-                                            });
-                                            ui.separator();
-                                        }
-
-                                        if let Some(col) = colliders {
-                                            CollapsingHeader::new("Colliders").default_open(true).show(ui, |ui| {
-                                                let mut to_remove: Option<usize> = None;
-
-                                                for (index, c) in col.colliders.iter_mut().enumerate() {
-                                                    ui.horizontal(|ui| {
-                                                        let header = CollapsingHeader::new(format!("Collider {}", index + 1))
-                                                            .default_open(true);
-
-                                                        header.show(ui, |ui| {
-                                                            c.inspect(
-                                                                entity,
-                                                                &mut cfg,
-                                                                ui,
-                                                                self.undo_stack,
-                                                                self.signal,
-                                                                label.as_mut_string(),
-                                                            );
-                                                        });
-
-                                                        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
-                                                            if ui.button("🗑").on_hover_text("Remove collider").clicked() {
-                                                                to_remove = Some(index);
-                                                            }
-                                                        });
-                                                    });
-
-                                                    ui.separator();
-                                                }
-
-                                                if let Some(index) = to_remove {
-                                                    col.colliders.remove(index);
-                                                }
-
-                                                if ui.button("➕ Add new collider").clicked() {
-                                                    local_add_collider = Some(*entity);
-                                                }
-                                            });
-                                        }
-
-                                        ui.separator();
-                                    });
-                                }
-                                ui.separator();
-                            }
-                        } else {
-                            log_once::debug_once!("Unable to query entity inside resource inspector");
-                        }
-                    }
-
-                    if local_set_initial_camera {
-                        for (id, comp) in self.world.query::<(Entity, &mut CameraComponent)>().iter() {
-                            comp.starting_camera = false;
-                            self.undo_stack
-                                .push(UndoableAction::RemoveStartingCamera(id))
-                        }
-
-                        if let Ok(comp) = self.world.query_one_mut::<&mut CameraComponent>(*entity)
-                        {
-                            success!("This camera is currently set as the initial camera");
-                            comp.starting_camera = true;
-                        }
-                    }
-                } else if !local_scene_settings {
-                    ui.label("No entity selected, therefore no info to provide. Go on, what are you waiting for? Click an entity!");
-                }
-                
-                if local_scene_settings {
-                    log_once::debug_once!("Rendering scene settings");
-                    self.scene_settings(&mut cfg, ui);
-                }
-
-                if let Some(e) = local_add_collider {
-                    let mut manual_edit = false;
-                    if let Ok(col) = self.world.query_one::<Option<&mut ColliderGroup>>(e).get()
-                    {
-                        if let Some(col) = col {
-                            let mut collider = Collider::new();
-                            collider.id = col.colliders.len() as u32 + 1;
-                            col.insert(collider);
-                        } else {
-                            manual_edit = true;
-                        }
-                    }
-
-                    if manual_edit {
-                        let _ = self.world.insert_one(e, ColliderGroup::new());
-                    }
-                }
+                self.resource_inspector(ui);
             }
             EditorTab::Plugin(dock_info) => {
                 if self.editor.is_null() {
@@ -975,139 +240,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                 }
             }
             EditorTab::ErrorConsole => {
-                fn analyse_error(log: &Vec<String>) -> Vec<ConsoleItem> {
-                    fn parse_compiler_location(
-                        line: &str,
-                    ) -> Option<(ErrorLevel, PathBuf, String)> {
-                        let trimmed = line.trim_start();
-                        let (error_level, rest) =
-                            if let Some(r) = trimmed.strip_prefix("e: file:///") {
-                                (ErrorLevel::Error, r)
-                            } else if let Some(r) = trimmed.strip_prefix("w: file:///") {
-                                (ErrorLevel::Warn, r)
-                            } else {
-                                return None;
-                            };
-
-                        let location = rest.split_whitespace().next()?;
-
-                        let mut segments = location.rsplitn(3, ':');
-                        let column = segments.next()?;
-                        let row = segments.next()?;
-                        let path = segments.next()?;
-
-                        Some((error_level, PathBuf::from(path), format!("{row}:{column}")))
-                    }
-
-                    let mut list: Vec<ConsoleItem> = Vec::new();
-                    for (index, line) in log.iter().enumerate() {
-                        if line.contains("The required library") {
-                            list.push(ConsoleItem {
-                                error_level: ErrorLevel::Error,
-                                msg: line.clone(),
-                                file_location: None,
-                                line_ref: None,
-                                id: index as u64,
-                            });
-                        } else if let Some((error_level, path, loc)) = parse_compiler_location(line) {
-                            list.push(ConsoleItem {
-                                error_level,
-                                msg: line.clone(),
-                                file_location: Some(path),
-                                line_ref: Some(loc),
-                                id: index as u64,
-                            });
-                        } else {
-                            list.push(ConsoleItem {
-                                error_level: ErrorLevel::Info,
-                                msg: line.clone(),
-                                file_location: None,
-                                line_ref: None,
-                                id: index as u64,
-                            });
-                        }
-                    }
-                    list
-                }
-
-                let logs = analyse_error(&self.build_logs);
-
-                egui::ScrollArea::vertical()
-                    .auto_shrink([false, false])
-                    .stick_to_bottom(true)
-                    .show(ui, |ui| {
-                        if logs.is_empty() {
-                            ui.label("Build output will appear here once available.");
-                            return;
-                        }
-
-                        for item in &logs {
-                            let (bg_color, text_color, stroke_color) = match item.error_level {
-                                ErrorLevel::Error => (
-                                    egui::Color32::from_rgb(60, 20, 20),
-                                    egui::Color32::from_rgb(255, 200, 200),
-                                    egui::Color32::from_rgb(255, 200, 200),
-                                ),
-                                ErrorLevel::Warn => (
-                                    egui::Color32::from_rgb(40, 40, 10),
-                                    egui::Color32::from_rgb(255, 255, 200),
-                                    egui::Color32::from_rgb(255, 255, 200),
-                                ),
-                                ErrorLevel::Info => (
-                                    egui::Color32::TRANSPARENT,
-                                    ui.style().visuals.text_color(),
-                                    egui::Color32::TRANSPARENT,
-                                ),
-                            };
-
-                            if matches!(item.error_level, ErrorLevel::Info) {
-                                ui.label(RichText::new(&item.msg).monospace());
-                            } else {
-                                let available_width = ui.available_width();
-                                let frame = egui::Frame::new()
-                                    .inner_margin(Margin::symmetric(8, 6))
-                                    .fill(bg_color)
-                                    .stroke(egui::Stroke::new(1.0, stroke_color));
-
-                                let response = frame
-                                    .show(ui, |ui| {
-                                        ui.set_width(available_width - 10.0);
-                                        ui.horizontal(|ui| {
-                                            ui.label(RichText::new(&item.msg).color(text_color).monospace());
-                                        });
-                                    })
-                                    .response;
-
-                                if response.clicked() {
-                                    log::debug!("Log item clicked: {}", &item.id);
-                                    if let (Some(path), Some(loc)) =
-                                        (&item.file_location, &item.line_ref)
-                                    {
-                                        let location_arg = format!("{}:{}", path.display(), loc);
-
-                                        match std::process::Command::new("code")
-                                            .args(["-g", &location_arg])
-                                            .spawn()
-                                            .map(|_| ())
-                                        {
-                                            Ok(()) => {
-                                                log::info!(
-                                                    "Launched Visual Studio Code at the error: {}",
-                                                    &location_arg
-                                                );
-                                            }
-                                            Err(e) => {
-                                                warn!(
-                                                    "Failed to open '{}' in VS Code: {}",
-                                                    location_arg, e
-                                                );
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    });
+                self.build_console(ui);
             }
             EditorTab::Console => {
                 ui.heading("Console");
diff --git a/crates/eucalyptus-editor/src/editor/entity_list.rs b/crates/eucalyptus-editor/src/editor/entity_list.rs
index a84db8d..88d5552 100644
--- a/crates/eucalyptus-editor/src/editor/entity_list.rs
+++ b/crates/eucalyptus-editor/src/editor/entity_list.rs
@@ -74,38 +74,8 @@ impl<'a> EditorTabViewer<'a> {
                                             };
 
                                         if ui.button(display_name).clicked() {
-                                            if name.contains("MeshRenderer") {
-                                                *signal = Signal::AddComponent(
-                                                    entity,
-                                                    "MeshRenderer".to_string(),
-                                                );
-                                            } else if name.contains("CameraComponent")
-                                                || name.contains("Camera3D")
-                                            {
-                                                *signal = Signal::AddComponent(
-                                                    entity,
-                                                    "CameraComponent".to_string(),
-                                                );
-                                            } else if name.contains("Light") {
-                                                *signal = Signal::AddComponent(
-                                                    entity,
-                                                    "Light".to_string(),
-                                                );
-                                            } else {
-                                                if let Some(comp) =
-                                                    registry.create_default_component(id)
-                                                {
-                                                    let mut builder = EntityBuilder::new();
-                                                    if let Ok(_) = registry
-                                                        .deserialize_into_builder(
-                                                            comp.as_ref(),
-                                                            &mut builder,
-                                                        )
-                                                    {
-                                                        let _ = world
-                                                            .insert(entity, builder.build());
-                                                    }
-                                                }
+                                            if let Some(component) = registry.create_default_component(id) {
+                                                *signal = Signal::AddComponent(entity, component);
                                             }
                                             ui.close();
                                         }
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 5303b74..67d3075 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -1,10 +1,15 @@
+pub mod asset_viewer;
+pub mod build_console;
 pub mod component;
 pub mod console_error;
+pub mod console;
 pub mod dock;
+pub mod entity_list;
 pub mod input;
+pub mod resource;
 pub mod scene;
 pub mod settings;
-mod console;
+pub mod viewport;
 
 pub(crate) use crate::editor::dock::*;
 
@@ -1531,10 +1536,8 @@ pub enum Signal {
     Play,
     StopPlaying,
     LogEntities,
-    /// This only applies to builders with specific behaviours that the standard component
-    /// registry is unable to have. Most don't apply to this signal, however some are supported,
-    /// such as [`MeshRenderer`] (which uses async loading). 
-    AddComponent(hecs::Entity, String),
+    /// Adds a new component instance using the async init pipeline.
+    AddComponent(hecs::Entity, Box<dyn SerializableComponent>),
 
     /// Loads a model from a URI/path and swaps it onto an existing MeshRenderer (or adds one if missing).
     ReplaceModel(hecs::Entity, String),
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index 763150a..7cc26dc 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -1,25 +1,27 @@
 use crate::editor::{Editor, EditorState, Signal};
+use dropbear_engine::asset::ASSET_REGISTRY;
 use dropbear_engine::camera::Camera;
-use dropbear_engine::entity::{MeshRenderer, Transform};
-use dropbear_engine::graphics::{SharedGraphicsContext};
+use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
+use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::lighting::{Light as EngineLight, LightComponent};
-use dropbear_engine::model::{LoadedModel, Material, Model, ModelId, MODEL_CACHE};
+use dropbear_engine::model::{LoadedModel, Material, Model, MODEL_CACHE};
 use dropbear_engine::procedural::{ProcedurallyGeneratedObject, ProcObj};
 use dropbear_engine::texture::{Texture, TextureWrapMode};
-use dropbear_engine::utils::{relative_path_from_euca, EUCA_SCHEME, ResourceReference, ResourceReferenceType};
+use dropbear_engine::utils::{
+    relative_path_from_euca, EUCA_SCHEME, ResourceReference, ResourceReferenceType,
+};
+use dropbear_traits::{ComponentInitContext, ComponentInsert, ComponentResources};
 use egui::Align2;
 use eucalyptus_core::camera::{CameraComponent, CameraType};
+use eucalyptus_core::properties::CustomProperties;
 use eucalyptus_core::scripting::{build_jvm, BuildStatus};
 use eucalyptus_core::spawn::{push_pending_spawn, PendingSpawn};
-use eucalyptus_core::states::{
-    EditorTab, Label, Light, Script, PROJECT,
-};
+use eucalyptus_core::states::{EditorTab, Label, Script, PROJECT};
 use eucalyptus_core::{fatal, info, success, success_without_console, warn, warn_without_console};
 use std::any::TypeId;
 use std::path::PathBuf;
 use std::sync::Arc;
 use winit::keyboard::KeyCode;
-use eucalyptus_core::properties::CustomProperties;
 
 fn resolve_editor_path(uri: &str) -> PathBuf {
     if uri.starts_with(EUCA_SCHEME) {
@@ -32,6 +34,33 @@ fn resolve_editor_path(uri: &str) -> PathBuf {
     }
 }
 
+struct InsertMeshRenderer {
+    renderer: MeshRenderer,
+    ensure_transform: bool,
+}
+
+impl ComponentInsert for InsertMeshRenderer {
+    fn insert(
+        self: Box<Self>,
+        world: &mut hecs::World,
+        entity: hecs::Entity,
+    ) -> anyhow::Result<()> {
+        if world.get::<&MeshRenderer>(entity).is_ok() {
+            let _ = world.remove_one::<MeshRenderer>(entity);
+        }
+
+        world
+            .insert_one(entity, self.renderer)
+            .map_err(|e| anyhow::anyhow!(e.to_string()))?;
+
+        if self.ensure_transform && world.get::<&EntityTransform>(entity).is_err() {
+            let _ = world.insert_one(entity, EntityTransform::default());
+        }
+
+        Ok(())
+    }
+}
+
 pub trait SignalController {
     fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()>;
 }
@@ -455,15 +484,15 @@ impl SignalController for Editor {
             }
             Signal::StopPlaying => {
                 self.editor_state = EditorState::Editing;
-                
+
                 if let Some(pid) = self.play_mode_pid {
                     log::debug!("Play mode requested to be exited, killing processes [{}]", pid);
                     let _ = crate::process::kill_process(pid);
                 }
-                
+
                 self.play_mode_pid = None;
                 self.play_mode_exit_rx = None;
-                
+
                 success!("Exited play mode");
                 log::info!("Back to the editor you go...");
 
@@ -543,79 +572,19 @@ impl SignalController for Editor {
                 self.signal = Signal::None;
                 Ok(())
             }
-            Signal::AddComponent(entity, component_name) => {
-                if component_name == "MeshRenderer" {
-                    let unassigned_id = (*entity).to_bits().get();
-                    let reference = ResourceReference::from_reference(
-                        ResourceReferenceType::Unassigned { id: unassigned_id },
-                    );
-
-                    let model = std::sync::Arc::new(Model {
-                        label: "None".to_string(),
-                        hash: unassigned_id,
-                        path: reference,
-                        meshes: Vec::new(),
-                        materials: Vec::new(),
-                        skins: Vec::new(),
-                        animations: Vec::new(),
-                        nodes: Vec::new(),
-                    });
-
-                    let loaded_model = LoadedModel::new_raw(
-                        &dropbear_engine::asset::ASSET_REGISTRY,
-                        model,
-                    );
+            Signal::AddComponent(entity, component) => {
+                let mut resources = ComponentResources::new();
+                resources.insert(graphics.clone());
+                let ctx = ComponentInitContext {
+                    entity: *entity,
+                    resources: Arc::new(resources),
+                };
 
-                    let renderer = dropbear_engine::entity::MeshRenderer::from_handle(loaded_model);
-                    let _ = self.world.insert_one(*entity, renderer);
-                    success!("Added MeshRenderer (unassigned) for entity {:?}", entity);
-                } else if component_name == "CameraComponent" {
-                    let graphics_clone = graphics.clone();
-                    let future = async move {
-                        let camera = Camera::predetermined(graphics_clone, Some("New Camera"));
-                        let component = CameraComponent::new();
-                        Ok::<(Camera, CameraComponent), anyhow::Error>((camera, component))
-                    };
-                    let handle = graphics.future_queue.push(Box::pin(future));
-                    self.pending_components.push((*entity, handle));
-                    success!("Queued Camera addition for entity {:?}", entity);
-                } else if component_name == "Light" {
-                    let graphics_clone = graphics.clone();
-                    let future = async move {
-                        let light_comp = LightComponent::default();
-                        let transform = Transform::default();
-                        let engine_light = EngineLight::new(
-                            graphics_clone,
-                            light_comp.clone(),
-                            transform,
-                            Some("New Light"),
-                        )
-                        .await;
-
-                        let light_config = Light {
-                            label: "New Light".to_string(),
-                            transform,
-                            light_component: light_comp.clone(),
-                            enabled: true,
-                            entity_id: None,
-                        };
+                let init_future = component.init(ctx);
+                let handle = graphics.future_queue.push(init_future);
+                self.pending_components.push((*entity, handle));
 
-                        Ok::<(LightComponent, EngineLight, Light, Transform), anyhow::Error>((
-                            light_comp,
-                            engine_light,
-                            light_config,
-                            transform,
-                        ))
-                    };
-                    let handle = graphics.future_queue.push(Box::pin(future));
-                    self.pending_components.push((*entity, handle));
-                    success!("Queued Light addition for entity {:?}", entity);
-                } else {
-                    warn!(
-                        "Unknown component type for AddComponent signal: {}",
-                        component_name
-                    );
-                }
+                success!("Queued component addition for entity {:?}", entity);
                 self.signal = Signal::None;
                 Ok(())
             }
@@ -656,7 +625,7 @@ impl SignalController for Editor {
                         let size = glam::DVec3::new(1.0, 1.0, 1.0);
                         let size_bits = [1.0f32.to_bits(), 1.0f32.to_bits(), 1.0f32.to_bits()];
                         let mut loaded = ProcedurallyGeneratedObject::cuboid(size)
-                            .build_model(graphics_clone.clone(), None, Some("Cuboid"));
+                            .build_model(graphics_clone.clone(), None, Some("Cuboid"), ASSET_REGISTRY.clone());
 
                         let model = loaded.make_mut();
                         model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }));
@@ -690,11 +659,11 @@ impl SignalController for Editor {
                 let graphics_clone = graphics.clone();
                 let uri_clone = uri.clone();
                 let future = async move {
-                    if is_legacy_internal_cube_uri(&uri_clone) {
+                    let renderer = if is_legacy_internal_cube_uri(&uri_clone) {
                         let size = glam::DVec3::new(1.0, 1.0, 1.0);
                         let size_bits = [1.0f32.to_bits(), 1.0f32.to_bits(), 1.0f32.to_bits()];
                         let mut loaded_model = ProcedurallyGeneratedObject::cuboid(size)
-                            .build_model(graphics_clone.clone(), None, Some("Cuboid"));
+                            .build_model(graphics_clone.clone(), None, Some("Cuboid"), ASSET_REGISTRY.clone());
 
                         {
                             let model = loaded_model.make_mut();
@@ -705,7 +674,7 @@ impl SignalController for Editor {
                         }
                         loaded_model.refresh_registry();
 
-                        Ok::<MeshRenderer, anyhow::Error>(MeshRenderer::from_handle(loaded_model))
+                        MeshRenderer::from_handle(loaded_model)
                     } else {
                         let path = resolve_editor_path(&uri_clone);
                         let mut model = dropbear_engine::model::Model::load(
@@ -724,11 +693,13 @@ impl SignalController for Editor {
                         }
 
                         model.refresh_registry();
+                        dropbear_engine::entity::MeshRenderer::from_handle(model)
+                    };
 
-                        Ok::<MeshRenderer, anyhow::Error>(
-                            dropbear_engine::entity::MeshRenderer::from_handle(model),
-                        )
-                    }
+                    Ok::<Box<dyn ComponentInsert>, anyhow::Error>(Box::new(InsertMeshRenderer {
+                        renderer,
+                        ensure_transform: true,
+                    }))
                 };
 
                 let handle = graphics.future_queue.push(Box::pin(future));
@@ -738,7 +709,8 @@ impl SignalController for Editor {
                 Ok(())
             }
 
-            Signal::SetProceduralCuboid(entity, size) | Signal::UpdateProceduralCuboid(entity, size) => {
+            Signal::SetProceduralCuboid(entity, size)
+            | Signal::UpdateProceduralCuboid(entity, size) => {
                 let previous_customisation: Option<
                     Vec<(String, [f32; 4], Option<String>, TextureWrapMode, [f32; 2])>,
                 > =
@@ -777,7 +749,7 @@ impl SignalController for Editor {
                 let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64);
 
                 let mut loaded_model = ProcedurallyGeneratedObject::cuboid(size_vec)
-                    .build_model(graphics.clone(), None, Some(&label));
+                    .build_model(graphics.clone(), None, Some(&label), ASSET_REGISTRY.clone());
 
                 {
                     let model = loaded_model.make_mut();
@@ -791,7 +763,9 @@ impl SignalController for Editor {
                     if let Some(previous) = previous_customisation {
                         let model = renderer.make_model_mut();
                         for (mat_name, tint, texture_tag, wrap_mode, uv_tiling) in previous {
-                            if let Some(material) = model.materials.iter_mut().find(|m| m.name == mat_name) {
+                            if let Some(material) =
+                                model.materials.iter_mut().find(|m| m.name == mat_name)
+                            {
                                 material.wrap_mode = wrap_mode;
                                 material.set_tint(graphics.as_ref(), tint);
                                 material.set_uv_tiling(graphics.as_ref(), uv_tiling);
@@ -812,7 +786,7 @@ impl SignalController for Editor {
                                                 None,
                                                 None,
                                                 Some(Texture::sampler_from_wrap(wrap_mode)),
-                                                Some(mat_name.as_str())
+                                                Some(mat_name.as_str()),
                                             );
                                             let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY
                                                 .solid_texture_rgba8(
@@ -823,12 +797,14 @@ impl SignalController for Editor {
 
                                             material.diffuse_texture = diffuse;
                                             material.normal_texture = flat_normal;
-                                            material.bind_group = dropbear_engine::model::Material::create_bind_group(
-                                                graphics.as_ref(),
-                                                &material.diffuse_texture,
-                                                &material.normal_texture,
-                                                &material.name,
-                                            );
+                                            material.bind_group =
+                                                dropbear_engine::model::Material::create_bind_group(
+                                                    graphics.as_ref(),
+                                                    &material.diffuse_texture,
+                                                    &material.normal_texture,
+                                                    &material.tint_buffer,
+                                                    &material.name,
+                                                );
                                             material.texture_tag = Some(uri);
                                         }
                                     } else {
@@ -867,7 +843,7 @@ impl SignalController for Editor {
                     None,
                     None,
                     Some(Texture::sampler_from_wrap(wrap_mode.clone())),
-                    Some(target_material)
+                    Some(target_material),
                 );
                 let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY
                     .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]))
@@ -886,6 +862,7 @@ impl SignalController for Editor {
                             graphics.as_ref(),
                             &material.diffuse_texture,
                             &material.normal_texture,
+                            &material.tint_buffer,
                             &material.name,
                         );
                         material.texture_tag = Some(uri.clone());
@@ -921,13 +898,14 @@ impl SignalController for Editor {
                                     None,
                                     None,
                                     Some(Texture::sampler_from_wrap(wrap_mode.clone())),
-                                    Some(target_material)
+                                    Some(target_material),
                                 );
                                 material.diffuse_texture = diffuse;
                                 material.bind_group = Material::create_bind_group(
                                     graphics.as_ref(),
                                     &material.diffuse_texture,
                                     &material.normal_texture,
+                                    &material.tint_buffer,
                                     &material.name,
                                 );
                             } else {
@@ -969,7 +947,9 @@ impl SignalController for Editor {
             }
 
             Signal::ClearMaterialTexture(entity, target_material) => {
-                let diffuse = (*dropbear_engine::asset::ASSET_REGISTRY.grey_texture(graphics.clone())).clone();
+                let diffuse =
+                    (*dropbear_engine::asset::ASSET_REGISTRY.grey_texture(graphics.clone()))
+                        .clone();
                 let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY
                     .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]))
                     .clone();
@@ -987,6 +967,7 @@ impl SignalController for Editor {
                             graphics.as_ref(),
                             &material.diffuse_texture,
                             &material.normal_texture,
+                            &material.tint_buffer,
                             &material.name,
                         );
                         material.texture_tag = None;
diff --git a/crates/eucalyptus-editor/src/spawn.rs b/crates/eucalyptus-editor/src/spawn.rs
index 91d3e2a..489812f 100644
--- a/crates/eucalyptus-editor/src/spawn.rs
+++ b/crates/eucalyptus-editor/src/spawn.rs
@@ -1,36 +1,13 @@
 use crate::editor::Editor;
-use dropbear_engine::asset::ASSET_REGISTRY;
-use dropbear_engine::camera::Camera;
-use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
+use dropbear_engine::entity::{EntityTransform, MeshRenderer};
 use dropbear_engine::future::FutureQueue;
 use dropbear_engine::graphics::{SharedGraphicsContext};
-use dropbear_engine::lighting::{Light, LightComponent};
-use dropbear_engine::model::{LoadedModel, Material, Model, ModelId};
-use dropbear_engine::texture::Texture;
-use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
-use dropbear_engine::procedural::ProcObj;
-use eucalyptus_core::camera::CameraComponent;
-use eucalyptus_core::scene::SceneEntity;
+use dropbear_engine::model::LoadedModel;
+use dropbear_traits::{ComponentInitContext, ComponentInsert, ComponentResources};
 pub(crate) use eucalyptus_core::spawn::{PendingSpawnController, PENDING_SPAWNS};
-use eucalyptus_core::states::{
-    Light as LightConfig, Script, SerializedMeshRenderer,
-};
-use eucalyptus_core::utils::ResolveReference;
 use eucalyptus_core::{fatal, success};
-use hecs::EntityBuilder;
+use hecs::Entity;
 use std::sync::Arc;
-use eucalyptus_core::properties::CustomProperties;
-
-fn component_ref<'a, T: 'static>(entity: &'a SceneEntity) -> Option<&'a T> {
-    entity
-        .components
-        .iter()
-        .find_map(|component| component.as_any().downcast_ref::<T>())
-}
-
-fn component_cloned<T: Clone + 'static>(entity: &SceneEntity) -> Option<T> {
-    component_ref::<T>(entity).cloned()
-}
 
 impl PendingSpawnController for Editor {
     fn check_up(
@@ -48,52 +25,58 @@ impl PendingSpawnController for Editor {
                 spawn.scene_entity.label
             );
 
-            let serialized_renderer =
-                component_cloned::<SerializedMeshRenderer>(&spawn.scene_entity);
+            if spawn.handle.is_none() {
+                let entity = self.world.spawn((spawn.scene_entity.label.clone(),));
+                let components = spawn.scene_entity.components.clone();
+
+                let mut resources = ComponentResources::new();
+                resources.insert(graphics.clone());
+                let resources = Arc::new(resources);
+
+                let future = async move {
+                    let mut inserts: Vec<Box<dyn ComponentInsert>> = Vec::new();
+                    for component in components {
+                        let ctx = ComponentInitContext {
+                            entity,
+                            resources: resources.clone(),
+                        };
+                        let insert = component.init(ctx).await?;
+                        inserts.push(insert);
+                    }
 
-            if serialized_renderer.is_none() && spawn.handle.is_none() {
-                log::debug!(
-                    "No renderer component found for '{}', spawning immediately",
-                    spawn.scene_entity.label
-                );
-                self.spawn_scene_entity(&spawn.scene_entity, None);
-                completed.push(index);
-                continue;
-            }
+                    Ok::<(Entity, Vec<Box<dyn ComponentInsert>>), anyhow::Error>((entity, inserts))
+                };
 
-            if spawn.handle.is_none() {
-                if let Some(renderer) = serialized_renderer.clone() {
-                    let graphics_clone = graphics.clone();
-                    let label = spawn.scene_entity.label.to_string();
-                    let future = async move {
-                        load_renderer_from_serialized(renderer, graphics_clone, label).await
-                    };
-                    let handle = queue.push(Box::pin(future));
-                    spawn.handle = Some(handle);
-                }
+                let handle = queue.push(Box::pin(future));
+                spawn.handle = Some(handle);
             }
 
             if let Some(handle) = &spawn.handle {
                 if let Some(result) = queue.exchange_owned(handle) {
-                    if let Ok(r) = result.downcast::<anyhow::Result<MeshRenderer>>() {
+                    if let Ok(r) = result.downcast::<anyhow::Result<(Entity, Vec<Box<dyn ComponentInsert>>)>>() {
                         match Arc::try_unwrap(r) {
-                            Ok(outcome) => match outcome {
-                                Ok(renderer) => {
-                                    self.spawn_scene_entity(&spawn.scene_entity, Some(renderer));
-                                    success!(
-                                        "Spawned '{}' from pending queue",
-                                        spawn.scene_entity.label
-                                    );
-                                    completed.push(index);
+                            Ok(Ok((entity, inserts))) => {
+                                for insert in inserts {
+                                    insert.insert(&mut self.world, entity)?;
                                 }
-                                Err(err) => {
-                                    fatal!("Unable to load mesh renderer: {}", err);
-                                    completed.push(index);
+
+                                if self.world.get::<&EntityTransform>(entity).is_err() {
+                                    let _ = self.world.insert_one(entity, EntityTransform::default());
                                 }
-                            },
+
+                                success!(
+                                    "Spawned '{}' from pending queue",
+                                    spawn.scene_entity.label
+                                );
+                                completed.push(index);
+                            }
+                            Ok(Err(err)) => {
+                                fatal!("Unable to init components for '{}': {}", spawn.scene_entity.label, err);
+                                completed.push(index);
+                            }
                             Err(_) => {
                                 log_once::warn_once!(
-                                    "Renderer future for '{}' still shared, deferring",
+                                    "Spawn future for '{}' still shared, deferring",
                                     spawn.scene_entity.label
                                 );
                             }
@@ -116,69 +99,22 @@ impl PendingSpawnController for Editor {
         let mut completed_components = Vec::new();
         for (index, (entity, handle)) in self.pending_components.iter().enumerate() {
             if let Some(result) = queue.exchange_owned(handle) {
-                match result.downcast::<anyhow::Result<MeshRenderer>>() {
-                    Ok(r) => {
-                        match Arc::try_unwrap(r) {
-                            Ok(Ok(renderer)) => {
-                                let _ = self.world.insert_one(*entity, renderer);
-                                let _ = self.world.insert_one(*entity, EntityTransform::default());
-                                success!("Added MeshRenderer to entity {:?}", entity);
-                                completed_components.push(index);
-                            }
-                            Ok(Err(e)) => {
-                                fatal!("Failed to load MeshRenderer: {}", e);
-                                completed_components.push(index);
-                            }
-                            Err(_) => {} // Still shared
+                if let Ok(r) = result.downcast::<anyhow::Result<Box<dyn ComponentInsert>>>() {
+                    match Arc::try_unwrap(r) {
+                        Ok(Ok(insert)) => {
+                            insert.insert(&mut self.world, *entity)?;
+                            success!("Added component to entity {:?}", entity);
+                            completed_components.push(index);
                         }
-                    }
-                    Err(result) => {
-                        match result.downcast::<anyhow::Result<(Camera, CameraComponent)>>() {
-                            Ok(r) => {
-                                match Arc::try_unwrap(r) {
-                                    Ok(Ok((camera, component))) => {
-                                        let _ = self.world.insert(*entity, (camera, component));
-                                        success!("Added Camera to entity {:?}", entity);
-                                        completed_components.push(index);
-                                    }
-                                    Ok(Err(e)) => {
-                                        fatal!("Failed to create Camera: {}", e);
-                                        completed_components.push(index);
-                                    }
-                                    Err(_) => {} // Still shared
-                                }
-                            }
-                            Err(result) => {
-                                if let Ok(r) = result.downcast::<anyhow::Result<(
-                                    LightComponent,
-                                    Light,
-                                    LightConfig,
-                                    Transform,
-                                )>>() {
-                                    match Arc::try_unwrap(r) {
-                                        Ok(Ok((
-                                            light_comp,
-                                            engine_light,
-                                            light_config,
-                                            transform,
-                                        ))) => {
-                                            let _ = self.world.insert(
-                                                *entity,
-                                                (light_comp, engine_light, light_config, transform),
-                                            );
-                                            success!("Added Light to entity {:?}", entity);
-                                            completed_components.push(index);
-                                        }
-                                        Ok(Err(e)) => {
-                                            fatal!("Failed to create Light: {}", e);
-                                            completed_components.push(index);
-                                        }
-                                        Err(_) => {} // Still shared
-                                    }
-                                }
-                            }
+                        Ok(Err(e)) => {
+                            fatal!("Failed to add component: {}", e);
+                            completed_components.push(index);
                         }
+                        Err(_) => {} // Still shared
                     }
+                } else {
+                    fatal!("Pending component result could not be downcasted");
+                    completed_components.push(index);
                 }
             }
         }
@@ -224,227 +160,3 @@ impl PendingSpawnController for Editor {
         Ok(())
     }
 }
-
-impl Editor {
-    fn spawn_scene_entity(
-        &mut self,
-        scene_entity: &SceneEntity,
-        mesh_renderer: Option<MeshRenderer>,
-    ) {
-        let mut builder = EntityBuilder::new();
-        builder.add(scene_entity.label.clone());
-
-        if let Some(transform) = component_ref::<EntityTransform>(scene_entity).copied() {
-            builder.add(transform);
-        } else {
-            builder.add(EntityTransform::default());
-        }
-
-        if let Some(renderer) = mesh_renderer {
-            builder.add(renderer);
-        }
-
-        if let Some(props) = component_cloned::<CustomProperties>(scene_entity) {
-            builder.add(props);
-        }
-
-        if let Some(script) = component_cloned::<Script>(scene_entity) {
-            builder.add(script);
-        }
-
-        if let Some(camera) = component_cloned::<CameraComponent>(scene_entity) {
-            builder.add(camera);
-        }
-
-        self.world.spawn(builder.build());
-    }
-}
-
-async fn load_renderer_from_serialized(
-    renderer: SerializedMeshRenderer,
-    graphics: Arc<SharedGraphicsContext>,
-    label: String,
-) -> anyhow::Result<MeshRenderer> {
-    fn is_legacy_internal_cube_uri(uri: &str) -> bool {
-        let uri = uri.replace('\\', "/");
-        uri.ends_with("internal/dropbear/models/cube")
-    }
-
-    let import_scale = renderer.import_scale.unwrap_or(1.0);
-
-    let mut mesh_renderer = match &renderer.handle.ref_type {
-        ResourceReferenceType::None => anyhow::bail!(
-            "Renderer for '{}' does not specify an asset reference",
-            label
-        ),
-        ResourceReferenceType::Unassigned { id } => {
-            let model = std::sync::Arc::new(Model {
-                label: "None".to_string(),
-                hash: *id,
-                path: ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: *id }),
-                meshes: Vec::new(),
-                materials: Vec::new(),
-                skins: Vec::new(),
-                animations: Vec::new(),
-                nodes: Vec::new(),
-            });
-
-            let loaded = LoadedModel::new_raw(&ASSET_REGISTRY, model);
-            MeshRenderer::from_handle_with_import_scale(loaded, import_scale)
-        }
-        ResourceReferenceType::File(reference) => {
-            if is_legacy_internal_cube_uri(reference) {
-                let size = glam::DVec3::new(1.0, 1.0, 1.0);
-                let size_bits = [1.0f32.to_bits(), 1.0f32.to_bits(), 1.0f32.to_bits()];
-                let mut loaded_model = dropbear_engine::procedural::ProcedurallyGeneratedObject::cuboid(size)
-                    .build_model(graphics.clone(), None, Some(&label));
-
-                let model = loaded_model.make_mut();
-                model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }));
-
-                loaded_model.refresh_registry();
-
-                MeshRenderer::from_handle(loaded_model)
-            } else {
-                let path = renderer.handle.resolve()?;
-                let loaded = Model::load(graphics.clone(), &path, Some(&label), None).await?;
-                MeshRenderer::from_handle_with_import_scale(loaded, import_scale)
-            }
-        }
-        ResourceReferenceType::Bytes(bytes) => {
-            let loaded =
-                Model::load_from_memory(graphics.clone(), bytes.clone(), Some(&label), None)
-                    .await?;
-            MeshRenderer::from_handle_with_import_scale(loaded, import_scale)
-        }
-        ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) => {
-            let size = [
-                f32::from_bits(size_bits[0]),
-                f32::from_bits(size_bits[1]),
-                f32::from_bits(size_bits[2]),
-            ];
-            let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64);
-            let mut loaded_model = dropbear_engine::procedural::ProcedurallyGeneratedObject::cuboid(size_vec)
-                .build_model(graphics.clone(), None, Some(&label));
-
-            let model = loaded_model.make_mut();
-            model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits: *size_bits }));
-
-            loaded_model.refresh_registry();
-
-            MeshRenderer::from_handle_with_import_scale(loaded_model, import_scale)
-        }
-    };
-
-    for override_entry in renderer.material_override {
-        if ASSET_REGISTRY
-            .model_handle_from_reference(&override_entry.source_model)
-            .is_none()
-        {
-            if matches!(
-                override_entry.source_model.ref_type,
-                ResourceReferenceType::File(_)
-            ) {
-                let source_path = override_entry.source_model.resolve()?;
-                let label_hint = override_entry.source_model.as_uri();
-                if let Err(err) = Model::load(graphics.clone(), &source_path, label_hint, None).await {
-                    log::warn!(
-                        "Failed to preload source model {:?} for override '{}': {}",
-                        override_entry.source_model,
-                        override_entry.target_material,
-                        err
-                    );
-                    continue;
-                }
-            } else {
-                log::warn!(
-                    "Unsupported override source {:?} for '{}'",
-                    override_entry.source_model,
-                    label
-                );
-                continue;
-            }
-        }
-
-        if let Err(err) = mesh_renderer.apply_material_override(
-            &override_entry.target_material,
-            override_entry.source_model.clone(),
-            &override_entry.source_material,
-        ) {
-            log::warn!(
-                "Failed to apply material override '{}' on '{}': {}",
-                override_entry.target_material,
-                label,
-                err
-            );
-        }
-    }
-
-    if !renderer.material_customisation.is_empty() {
-        for custom in &renderer.material_customisation {
-            let model_mut = mesh_renderer.make_model_mut();
-            let name_index = model_mut
-                .materials
-                .iter()
-                .position(|mat| mat.name == custom.target_material);
-            let index = name_index.or(custom.material_index);
-
-            if let Some(material) = index.and_then(|idx| model_mut.materials.get_mut(idx)) {
-                material.set_tint(graphics.as_ref(), custom.tint);
-                material.set_uv_tiling(graphics.as_ref(), custom.uv_tiling);
-
-                if let Some(reference) = &custom.diffuse_texture {
-                    if let Ok(path) = reference.resolve() {
-                        match std::fs::read(&path) {
-                            Ok(bytes) => {
-                                let diffuse = Texture::from_bytes_verbose_mipmapped(
-                                    graphics.clone(),
-                                    &bytes,
-                                    None,
-                                    None,
-                                    Some(Texture::sampler_from_wrap(custom.wrap_mode)),
-                                    Some(material.name.as_str()),
-                                );
-                                let flat_normal = (*ASSET_REGISTRY
-                                    .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]))
-                                .clone();
-
-                                material.diffuse_texture = diffuse;
-                                material.normal_texture = flat_normal;
-                                material.bind_group = Material::create_bind_group(
-                                    graphics.as_ref(),
-                                    &material.diffuse_texture,
-                                    &material.normal_texture,
-                                    &material.name,
-                                );
-                                material.texture_tag = reference.as_uri().map(|s| s.to_string());
-                                material.wrap_mode = custom.wrap_mode;
-                                material.set_uv_tiling(graphics.as_ref(), custom.uv_tiling);
-                            }
-                            Err(err) => {
-                                log::warn!(
-                                    "Failed to read custom texture '{}' for '{}': {}",
-                                    path.display(),
-                                    label,
-                                    err
-                                );
-                            }
-                        }
-                    } else {
-                        log::warn!(
-                            "Failed to resolve custom texture reference {:?} for '{}'",
-                            reference,
-                            label
-                        );
-                    }
-                }
-            }
-        }
-
-        mesh_renderer.sync_asset_registry();
-    }
-
-    mesh_renderer.set_import_scale(import_scale);
-
-    Ok(mesh_renderer)
-}
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index a878420..0535e9e 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -126,6 +126,8 @@ pub struct PlayMode {
     shader_globals: Option<GlobalsUniform>,
     collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
     sky_pipeline: Option<SkyPipeline>,
+    default_skinning_buffer: Option<wgpu::Buffer>,
+    default_skinning_bind_group: Option<wgpu::BindGroup>,
 
     initial_scene: Option<String>,
     current_scene: Option<String>,
@@ -211,6 +213,8 @@ impl PlayMode {
             },
             kino: None,
             sky_pipeline: None,
+            default_skinning_buffer: None,
+            default_skinning_bind_group: None,
         };
 
         log::debug!("Created new play mode instance");
@@ -234,7 +238,7 @@ impl PlayMode {
                     graphics.viewport_texture.size.height as f32,
                 ],
             ),
-            KinoWinitWindowing::new(graphics.window.clone()),
+            KinoWinitWindowing::new(graphics.window.clone(), None),
         ));
 
         let sky_texture = HdrLoader::from_equirectangular_bytes(
@@ -438,7 +442,7 @@ impl PlayMode {
         progress.camera_received = true;
         self.scene_progress = Some(progress);
 
-        self.load_wgpu_nerdy_stuff(graphics);
+        self.load_wgpu_nerdy_stuff(graphics.clone());
 
         self.reload_scripts_for_current_world(graphics.clone());
 
@@ -461,7 +465,7 @@ impl PlayMode {
                 self.active_camera = Some(new_camera);
             }
 
-            self.load_wgpu_nerdy_stuff(graphics);
+            self.load_wgpu_nerdy_stuff(graphics.clone());
             self.reload_scripts_for_current_world(graphics.clone());
 
             self.current_scene = Some(scene_progress.requested_scene.clone());
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index a8989a5..67afe4e 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -7,19 +7,21 @@ use eucalyptus_core::egui::CentralPanel;
 use eucalyptus_core::physics::collider::ColliderGroup;
 use eucalyptus_core::physics::collider::ColliderShapeKey;
 use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw;
-use glam::{vec2, DMat4, DQuat, DVec3, Quat, Vec2};
+use glam::{vec2, DMat4, DQuat, DVec3, Mat4, Quat, Vec2};
 use hecs::Entity;
 use wgpu::Color;
 use wgpu::util::DeviceExt;
 use winit::event_loop::ActiveEventLoop;
 use winit::event::WindowEvent;
+use dropbear_engine::animation::AnimationComponent;
+use dropbear_engine::asset::{ASSET_REGISTRY, Handle};
 use dropbear_engine::camera::Camera;
 use dropbear_engine::buffer::ResizableBuffer;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
 use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext};
 use dropbear_engine::lighting::{Light, LightComponent};
 use dropbear_engine::lighting::MAX_LIGHTS;
-use dropbear_engine::model::{DrawLight, DrawModel, ModelId, MODEL_CACHE};
+use dropbear_engine::model::{DrawLight, DrawModel, Model};
 use dropbear_engine::scene::{Scene, SceneCommand};
 use eucalyptus_core::camera::CameraComponent;
 use eucalyptus_core::command::CommandBufferPoller;
@@ -32,7 +34,6 @@ use eucalyptus_core::states::SCENES;
 use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOADER};
 use crate::PlayMode;
 use eucalyptus_core::physics::collider::shader::create_wireframe_geometry;
-use eucalyptus_core::ui::UI_CONTEXT;
 use kino_ui::widgets::{Anchor, Border, Fill};
 use kino_ui::widgets::rect::Rectangle;
 
@@ -354,6 +355,27 @@ impl Scene for PlayMode {
         }
 
         {
+            let registry = ASSET_REGISTRY.read();
+            let mut query = self
+                .world
+                .query::<(&MeshRenderer, &mut AnimationComponent)>();
+
+            for (renderer, animation) in query.iter() {
+                let handle = renderer.model();
+                if handle.is_null() {
+                    continue;
+                }
+
+                let Some(model) = registry.get_model(handle) else {
+                    continue;
+                };
+
+                animation.update(dt, model);
+                animation.prepare_gpu_resources(graphics.clone());
+            }
+        }
+
+        {
             let mut light_query = self
                 .world
                 .query::<(&mut LightComponent, Option<&Transform>, Option<&EntityTransform>, &mut Light)>();
@@ -683,32 +705,34 @@ impl Scene for PlayMode {
                 globals.write(&graphics.queue);
             }
 
-        let renderers = {
-            let mut renderers = Vec::new();
-            let mut query = self.world.query::<&MeshRenderer>();
-            for renderer in query.iter() {
-                renderers.push(renderer.clone());
-            }
-            renderers
-        };
+        let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new();
+        let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::BindGroup)> = Vec::new();
+
+        {
+            let mut query = self
+                .world
+                .query::<(&MeshRenderer, Option<&AnimationComponent>)>();
+
+            for (renderer, animation) in query.iter() {
+                let handle = renderer.model();
+                if handle.is_null() {
+                    continue;
+                }
 
-        let mut model_batches: HashMap<ModelId, Vec<InstanceRaw>> = HashMap::new();
-        for renderer in &renderers {
-            model_batches
-                .entry(renderer.model_id())
-                .or_default()
-                .push(renderer.instance.to_raw());
+                let instance = renderer.instance.to_raw();
+                if let Some(bind_group) = animation.and_then(|anim| anim.bind_group.clone()) {
+                    animated_instances.push((handle.id, instance, bind_group));
+                } else {
+                    static_batches.entry(handle.id).or_default().push(instance);
+                }
+            }
         }
 
+        let registry = ASSET_REGISTRY.read();
         let mut prepared_models = Vec::new();
-        for (model_id, instances) in model_batches {
-            let model_opt = {
-                let cache = MODEL_CACHE.lock();
-                cache.values().find(|model| model.id == model_id).cloned()
-            };
-
-            let Some(model) = model_opt else {
-                log_once::error_once!("Missing model {:?} in cache", model_id);
+        for (handle, instances) in static_batches {
+            let Some(model) = registry.get_model(Handle::new(handle)).cloned() else {
+                log_once::error_once!("Missing model handle {} in registry", handle);
                 continue;
             };
 
@@ -744,6 +768,7 @@ impl Scene for PlayMode {
             }
         }
 
+        let registry = ASSET_REGISTRY.read();
         {
             let mut render_pass = encoder
                 .begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -772,17 +797,53 @@ impl Scene for PlayMode {
                 render_pass.set_pipeline(light_pipeline.pipeline());
                 for (light, component) in &lights {
                     render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..));
-                    if component.visible {
-                        render_pass.draw_light_model(
-                            &light.cube_model,
-                            &camera.bind_group,
-                            &light.bind_group
-                        );
+                    if !component.visible {
+                        continue;
                     }
+
+                    let Some(model) = registry.get_model(light.cube_model) else {
+                        log_once::error_once!(
+                            "Missing light cube model handle {} in registry",
+                            light.cube_model.id
+                        );
+                        continue;
+                    };
+
+                    render_pass.draw_light_model(
+                        model,
+                        &camera.bind_group,
+                        &light.bind_group,
+                    );
                 }
             }
         }
 
+        if self.default_skinning_bind_group.is_none() {
+            let identity = [Mat4::IDENTITY.to_cols_array_2d()];
+            let buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                label: Some("default skinning buffer"),
+                contents: bytemuck::cast_slice(&identity),
+                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+            });
+
+            let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
+                label: Some("default skinning bind group"),
+                layout: &graphics.layouts.skinning_bind_group_layout,
+                entries: &[wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: buffer.as_entire_binding(),
+                }],
+            });
+
+            self.default_skinning_buffer = Some(buffer);
+            self.default_skinning_bind_group = Some(bind_group);
+        }
+
+        let default_skinning_bind_group = self
+            .default_skinning_bind_group
+            .as_ref()
+            .expect("Default skinning bind group not initialized");
+
         // model rendering
         if let Some(lcp) = &self.light_cube_pipeline {
             for (model, instance_buffer, instance_count) in prepared_models {
@@ -817,13 +878,82 @@ impl Scene for PlayMode {
                     });
                 render_pass.set_pipeline(pipeline.pipeline());
                 render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
-                render_pass.set_bind_group(4, globals_bind_group, &[]);
-                render_pass.draw_model_instanced(
-                    &model,
-                    0..instance_count,
-                    &camera.bind_group,
-                    lcp.bind_group(),
+                render_pass.set_bind_group(3, globals_bind_group, &[]);
+
+                for mesh in &model.meshes {
+                    let material = &model.materials[mesh.material];
+                    render_pass.draw_mesh_instanced(
+                        mesh,
+                        material,
+                        0..instance_count,
+                        &camera.bind_group,
+                        lcp.bind_group(),
+                        Some(default_skinning_bind_group),
+                    );
+                }
+            }
+        }
+
+        if let Some(lcp) = &self.light_cube_pipeline {
+            let globals_bind_group = &self
+                .shader_globals
+                .as_ref()
+                .expect("Shader globals not initialised")
+                .bind_group;
+
+            for (handle, instance, skin_bind_group) in animated_instances {
+                let Some(model) = registry.get_model(Handle::new(handle)).cloned() else {
+                    log_once::error_once!("Missing model handle {} in registry", handle);
+                    continue;
+                };
+
+                let instance_buffer = graphics.device.create_buffer_init(
+                    &wgpu::util::BufferInitDescriptor {
+                        label: Some("Runtime Animated Instance Buffer"),
+                        contents: bytemuck::cast_slice(&[instance]),
+                        usage: wgpu::BufferUsages::VERTEX,
+                    },
                 );
+
+                let mut render_pass = encoder
+                    .begin_render_pass(&wgpu::RenderPassDescriptor {
+                        label: Some("model render pass (animated)"),
+                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                            view: hdr.view(),
+                            depth_slice: None,
+                            resolve_target: None,
+                            ops: wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            },
+                        })],
+                        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                            view: &graphics.depth_texture.view,
+                            depth_ops: Some(wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            }),
+                            stencil_ops: None,
+                        }),
+                        occlusion_query_set: None,
+                        timestamp_writes: None,
+                    });
+
+                render_pass.set_pipeline(pipeline.pipeline());
+                render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
+                render_pass.set_bind_group(3, globals_bind_group, &[]);
+
+                for mesh in &model.meshes {
+                    let material = &model.materials[mesh.material];
+                    render_pass.draw_mesh_instanced(
+                        mesh,
+                        material,
+                        0..1,
+                        &camera.bind_group,
+                        lcp.bind_group(),
+                        Some(&skin_bind_group),
+                    );
+                }
             }
         }
 
diff --git a/include/dropbear.h b/include/dropbear.h
index ddd1ee3..24890be 100644
--- a/include/dropbear.h
+++ b/include/dropbear.h
@@ -179,42 +179,53 @@ typedef struct NChannelValuesFfi {
 
 typedef NChannelValuesFfi NChannelValues;
 
-typedef void* SceneLoaderPtr;
+typedef void* WorldPtr;
 
-typedef void* AssetRegistryPtr;
+typedef struct i32Array {
+    int32_t* values;
+    size_t length;
+    size_t capacity;
+} i32Array;
 
-typedef struct NVector2 {
-    double x;
-    double y;
-} NVector2;
+typedef struct f64ArrayArray {
+    double* values;
+    size_t length;
+    size_t capacity;
+} f64ArrayArray;
 
-typedef struct Progress {
-    size_t current;
-    size_t total;
-    const char* message;
-} Progress;
+typedef struct NSkin {
+    const char* name;
+    i32Array joints;
+    f64ArrayArray inverse_bind_matrices;
+    const int32_t* skeleton_root;
+} NSkin;
 
-typedef struct u64Array {
-    uint64_t* values;
+typedef struct NSkinArray {
+    NSkin* values;
     size_t length;
     size_t capacity;
-} u64Array;
+} NSkinArray;
 
-typedef struct ConnectedGamepadIds {
-    u64Array ids;
-} ConnectedGamepadIds;
+typedef struct NTransform {
+    NVector3 position;
+    NQuaternion rotation;
+    NVector3 scale;
+} NTransform;
 
-typedef struct NAttenuation {
-    float constant;
-    float linear;
-    float quadratic;
-} NAttenuation;
+typedef struct NColour {
+    uint8_t r;
+    uint8_t g;
+    uint8_t b;
+    uint8_t a;
+} NColour;
 
 typedef struct NRange {
     float start;
     float end;
 } NRange;
 
+typedef void* SceneLoaderPtr;
+
 typedef struct IndexNative {
     uint32_t index;
     uint32_t generation;
@@ -226,19 +237,96 @@ typedef struct NCollider {
     uint32_t id;
 } NCollider;
 
+typedef struct NShapeCastHit {
+    NCollider collider;
+    double distance;
+    NVector3 witness1;
+    NVector3 witness2;
+    NVector3 normal1;
+    NVector3 normal2;
+    NShapeCastStatus status;
+} NShapeCastHit;
+
+typedef struct u64Array {
+    uint64_t* values;
+    size_t length;
+    size_t capacity;
+} u64Array;
+
+typedef void* CommandBufferPtr;
+
+typedef struct Progress {
+    size_t current;
+    size_t total;
+    const char* message;
+} Progress;
+
 typedef struct NColliderArray {
     NCollider* values;
     size_t length;
     size_t capacity;
 } NColliderArray;
 
-typedef struct NTransform {
+typedef struct NVector4 {
+    double x;
+    double y;
+    double z;
+    double w;
+} NVector4;
+
+typedef struct NVector2 {
+    double x;
+    double y;
+} NVector2;
+
+typedef struct NModelVertex {
     NVector3 position;
+    NVector3 normal;
+    NVector4 tangent;
+    NVector2 tex_coords0;
+    NVector2 tex_coords1;
+    NVector4 colour0;
+    i32Array joints0;
+    NVector4 weights0;
+} NModelVertex;
+
+typedef struct NModelVertexArray {
+    NModelVertex* values;
+    size_t length;
+    size_t capacity;
+} NModelVertexArray;
+
+typedef struct NMesh {
+    const char* name;
+    int32_t num_elements;
+    int32_t material_index;
+    NModelVertexArray vertices;
+} NMesh;
+
+typedef struct NMeshArray {
+    NMesh* values;
+    size_t length;
+    size_t capacity;
+} NMeshArray;
+
+typedef struct NNodeTransform {
+    NVector3 translation;
     NQuaternion rotation;
     NVector3 scale;
-} NTransform;
+} NNodeTransform;
 
-typedef void* InputStatePtr;
+typedef struct NNode {
+    const char* name;
+    const int32_t* parent;
+    i32Array children;
+    NNodeTransform transform;
+} NNode;
+
+typedef struct NNodeArray {
+    NNode* values;
+    size_t length;
+    size_t capacity;
+} NNodeArray;
 
 typedef struct IndexNativeArray {
     IndexNative* values;
@@ -251,30 +339,15 @@ typedef struct CharacterCollisionArray {
     IndexNativeArray collisions;
 } CharacterCollisionArray;
 
-typedef struct NShapeCastHit {
-    NCollider collider;
-    double distance;
-    NVector3 witness1;
-    NVector3 witness2;
-    NVector3 normal1;
-    NVector3 normal2;
-    NShapeCastStatus status;
-} NShapeCastHit;
+typedef void* InputStatePtr;
 
-typedef struct AxisLock {
-    bool x;
-    bool y;
-    bool z;
-} AxisLock;
+typedef void* PhysicsStatePtr;
 
-typedef void* CommandBufferPtr;
+typedef struct ConnectedGamepadIds {
+    u64Array ids;
+} ConnectedGamepadIds;
 
-typedef struct NVector4 {
-    double x;
-    double y;
-    double z;
-    double w;
-} NVector4;
+typedef void* AssetRegistryPtr;
 
 typedef struct NMaterial {
     const char* name;
@@ -300,30 +373,16 @@ typedef struct NMaterialArray {
     size_t capacity;
 } NMaterialArray;
 
-typedef struct i32Array {
-    int32_t* values;
-    size_t length;
-    size_t capacity;
-} i32Array;
-
-typedef struct f64ArrayArray {
-    double* values;
-    size_t length;
-    size_t capacity;
-} f64ArrayArray;
-
-typedef struct NSkin {
-    const char* name;
-    i32Array joints;
-    f64ArrayArray inverse_bind_matrices;
-    const int32_t* skeleton_root;
-} NSkin;
+typedef struct NAttenuation {
+    float constant;
+    float linear;
+    float quadratic;
+} NAttenuation;
 
-typedef struct NSkinArray {
-    NSkin* values;
-    size_t length;
-    size_t capacity;
-} NSkinArray;
+typedef struct RayHit {
+    NCollider collider;
+    double distance;
+} RayHit;
 
 typedef struct f64Array {
     double* values;
@@ -356,78 +415,19 @@ typedef struct NAnimationArray {
     size_t capacity;
 } NAnimationArray;
 
-typedef struct NColour {
-    uint8_t r;
-    uint8_t g;
-    uint8_t b;
-    uint8_t a;
-} NColour;
-
-typedef void* PhysicsStatePtr;
-
-typedef struct NNodeTransform {
-    NVector3 translation;
-    NQuaternion rotation;
-    NVector3 scale;
-} NNodeTransform;
-
-typedef struct NNode {
-    const char* name;
-    const int32_t* parent;
-    i32Array children;
-    NNodeTransform transform;
-} NNode;
-
-typedef struct NNodeArray {
-    NNode* values;
-    size_t length;
-    size_t capacity;
-} NNodeArray;
-
-typedef void* GraphicsContextPtr;
-
-typedef struct NModelVertex {
-    NVector3 position;
-    NVector3 normal;
-    NVector4 tangent;
-    NVector2 tex_coords0;
-    NVector2 tex_coords1;
-    NVector4 colour0;
-    i32Array joints0;
-    NVector4 weights0;
-} NModelVertex;
-
-typedef struct NModelVertexArray {
-    NModelVertex* values;
-    size_t length;
-    size_t capacity;
-} NModelVertexArray;
-
-typedef struct NMesh {
-    const char* name;
-    int32_t num_elements;
-    int32_t material_index;
-    NModelVertexArray vertices;
-} NMesh;
-
-typedef struct NMeshArray {
-    NMesh* values;
-    size_t length;
-    size_t capacity;
-} NMeshArray;
-
-typedef void* WorldPtr;
-
-typedef struct RayHit {
-    NCollider collider;
-    double distance;
-} RayHit;
+typedef struct AxisLock {
+    bool x;
+    bool y;
+    bool z;
+} AxisLock;
 
 typedef struct RigidBodyContext {
     IndexNative index;
     uint64_t entity_id;
 } RigidBodyContext;
 
+typedef void* GraphicsContextPtr;
+
 int32_t dropbear_gamepad_is_button_pressed(InputStatePtr input, uint64_t gamepad_id, int32_t button_ordinal, bool* out0);
 int32_t dropbear_gamepad_get_left_stick_position(InputStatePtr input, uint64_t gamepad_id, NVector2* out0);
 int32_t dropbear_gamepad_get_right_stick_position(InputStatePtr input, uint64_t gamepad_id, NVector2* out0);
@@ -571,6 +571,12 @@ int32_t dropbear_properties_set_double_property(WorldPtr world, uint64_t entity,
 int32_t dropbear_properties_set_float_property(WorldPtr world, uint64_t entity, const char* key, double value);
 int32_t dropbear_properties_set_bool_property(WorldPtr world, uint64_t entity, const char* key, bool value);
 int32_t dropbear_properties_set_vec3_property(WorldPtr world, uint64_t entity, const char* key, const NVector3* value);
+int32_t dropbear_transform_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
+int32_t dropbear_transform_get_local_transform(WorldPtr world, uint64_t entity, NTransform* out0);
+int32_t dropbear_transform_set_local_transform(WorldPtr world, uint64_t entity, const NTransform* transform);
+int32_t dropbear_transform_get_world_transform(WorldPtr world, uint64_t entity, NTransform* out0);
+int32_t dropbear_transform_set_world_transform(WorldPtr world, uint64_t entity, const NTransform* transform);
+int32_t dropbear_transform_propogate_transform(WorldPtr world, uint64_t entity, NTransform* out0);
 int32_t dropbear_engine_get_entity(WorldPtr world, const char* label, uint64_t* out0);
 int32_t dropbear_engine_quit(CommandBufferPtr command_buffer);
 int32_t dropbear_engine_get_asset(AssetRegistryPtr asset, const char* label, const AssetKind* kind, uint64_t* out0, bool* out0_present);