kitgit

tirbofish/dropbear · diff

2643d1f · Thribhu K

fix: magna-carta parser fix: reset animation state on animation change so its legs don't look like its snapped fix: collider wireframe finally renders now (fixes #90) fix: kcc contains a CharacterMovementResult now refactor: included a world + entity in all InspectableComponents jarvis, run github actions

Unverified

diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
index a267fe1..b812f40 100644
--- a/crates/dropbear-engine/src/animation.rs
+++ b/crates/dropbear-engine/src/animation.rs
@@ -33,6 +33,9 @@ pub struct AnimationComponent {
 
     #[serde(skip)]
     pub available_animations: Vec<String>,
+
+    #[serde(skip)]
+    pub last_animation_index: Option<usize>,
 }
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -72,6 +75,7 @@ impl Default for AnimationComponent {
             bone_buffer: None,
             bind_group: None,
             available_animations: vec![],
+            last_animation_index: None,
         }
     }
 }
@@ -89,6 +93,11 @@ impl AnimationComponent {
             .map(|v| v.name.clone())
             .collect::<Vec<_>>();
 
+        if self.active_animation_index != self.last_animation_index {
+            self.local_pose.clear();
+            self.last_animation_index = self.active_animation_index;
+        }
+
         let Some(anim_idx) = self.active_animation_index else {
             self.reset_to_bind_pose(model);
             return;
diff --git a/crates/eucalyptus-core/src/animation.rs b/crates/eucalyptus-core/src/animation.rs
index b06a9c7..7b7b136 100644
--- a/crates/eucalyptus-core/src/animation.rs
+++ b/crates/eucalyptus-core/src/animation.rs
@@ -66,7 +66,13 @@ impl Component for AnimationComponent {
 }
 
 impl InspectableComponent for AnimationComponent {
-    fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
+    fn inspect(
+        &mut self,
+        _world: &World,
+        _entity: Entity,
+        ui: &mut Ui,
+        _graphics: Arc<SharedGraphicsContext>,
+    ) {
         CollapsingHeader::new("Animation")
             .default_open(true)
             .show(ui, |ui| {
diff --git a/crates/eucalyptus-core/src/camera.rs b/crates/eucalyptus-core/src/camera.rs
index 8217e4b..ada38d6 100644
--- a/crates/eucalyptus-core/src/camera.rs
+++ b/crates/eucalyptus-core/src/camera.rs
@@ -75,7 +75,13 @@ impl Component for Camera {
 }
 
 impl InspectableComponent for Camera {
-    fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
+    fn inspect(
+        &mut self,
+        _world: &World,
+        _entity: Entity,
+        ui: &mut Ui,
+        _graphics: Arc<SharedGraphicsContext>,
+    ) {
         CollapsingHeader::new("Camera3D")
             .default_open(true)
             .show(ui, |ui| {
@@ -302,7 +308,7 @@ pub mod shared {
 )]
 fn exists_for_entity(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<bool> {
     Ok(shared::camera_exists_for_entity(world, entity))
 }
@@ -313,7 +319,7 @@ fn exists_for_entity(
 )]
 fn get_eye(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<NVector3> {
     match world.get::<&Camera>(entity) {
         Ok(camera) => Ok(camera.eye.into()),
@@ -327,7 +333,7 @@ fn get_eye(
 )]
 fn set_eye(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
     eye: &NVector3,
 ) -> DropbearNativeResult<()> {
     match world.get::<&mut Camera>(entity) {
@@ -348,7 +354,7 @@ fn set_eye(
 )]
 fn get_target(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<NVector3> {
     match world.get::<&Camera>(entity) {
         Ok(camera) => Ok(camera.target.into()),
@@ -365,7 +371,7 @@ fn get_target(
 )]
 fn set_target(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
     target: &NVector3,
 ) -> DropbearNativeResult<()> {
     match world.get::<&mut Camera>(entity) {
@@ -383,7 +389,7 @@ fn set_target(
 )]
 fn get_up(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<NVector3> {
     match world.get::<&Camera>(entity) {
         Ok(camera) => Ok(camera.up.into()),
@@ -397,7 +403,7 @@ fn get_up(
 )]
 fn set_up(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
     up: &NVector3,
 ) -> DropbearNativeResult<()> {
     match world.get::<&mut Camera>(entity) {
@@ -418,7 +424,7 @@ fn set_up(
 )]
 fn get_aspect(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<f64> {
     match world.get::<&Camera>(entity) {
         Ok(camera) => Ok(camera.aspect.into()),
@@ -432,7 +438,7 @@ fn get_aspect(
 )]
 fn get_fovy(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<f64> {
     match world.get::<&Camera>(entity) {
         Ok(camera) => Ok(camera.settings.fov_y.into()),
@@ -446,7 +452,7 @@ fn get_fovy(
 )]
 fn set_fovy(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
     fovy: f64,
 ) -> DropbearNativeResult<()> {
     match world.get::<&mut Camera>(entity) {
@@ -467,7 +473,7 @@ fn set_fovy(
 )]
 fn get_znear(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<f64> {
     match world.get::<&Camera>(entity) {
         Ok(camera) => Ok(camera.znear.into()),
@@ -484,7 +490,7 @@ fn get_znear(
 )]
 fn set_znear(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
     znear: f64,
 ) -> DropbearNativeResult<()> {
     match world.get::<&mut Camera>(entity) {
@@ -502,7 +508,7 @@ fn set_znear(
 )]
 fn get_zfar(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<f64> {
     match world.get::<&Camera>(entity) {
         Ok(camera) => Ok(camera.zfar.into()),
@@ -516,7 +522,7 @@ fn get_zfar(
 )]
 fn set_zfar(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
     zfar: f64,
 ) -> DropbearNativeResult<()> {
     match world.get::<&mut Camera>(entity) {
@@ -534,7 +540,7 @@ fn set_zfar(
 )]
 fn get_yaw(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<f64> {
     match world.get::<&Camera>(entity) {
         Ok(camera) => Ok(camera.yaw.into()),
@@ -548,7 +554,7 @@ fn get_yaw(
 )]
 fn set_yaw(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
     yaw: f64,
 ) -> DropbearNativeResult<()> {
     match world.get::<&mut Camera>(entity) {
@@ -569,7 +575,7 @@ fn set_yaw(
 )]
 fn get_pitch(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<f64> {
     match world.get::<&Camera>(entity) {
         Ok(camera) => Ok(camera.pitch.into()),
@@ -586,7 +592,7 @@ fn get_pitch(
 )]
 fn set_pitch(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
     pitch: f64,
 ) -> DropbearNativeResult<()> {
     match world.get::<&mut Camera>(entity) {
@@ -607,7 +613,7 @@ fn set_pitch(
 )]
 fn get_speed(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<f64> {
     match world.get::<&Camera>(entity) {
         Ok(camera) => Ok(camera.settings.speed.into()),
@@ -624,7 +630,7 @@ fn get_speed(
 )]
 fn set_speed(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
     speed: f64,
 ) -> DropbearNativeResult<()> {
     match world.get::<&mut Camera>(entity) {
@@ -645,7 +651,7 @@ fn set_speed(
 )]
 fn get_sensitivity(
     #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
-    #[dropebear_macro::entity] entity: hecs::Entity,
+    #[dropbear_macro::entity] entity: hecs::Entity,
 ) -> DropbearNativeResult<f64> {
     match world.get::<&Camera>(entity) {
         Ok(camera) => Ok(camera.settings.sensitivity.into()),
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index b061d3c..74fe2b4 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -90,7 +90,7 @@ type InspectFn = Box<
     dyn Fn(&mut hecs::World, hecs::Entity, &mut egui::Ui, Arc<SharedGraphicsContext>) + Send + Sync,
 >;
 
-// fn inspect(&mut self, ui: &mut egui::Ui);
+// fn inspect(&mut self, world: &hecs::World, entity: hecs::Entity, ui: &mut egui::Ui, graphics: Arc<SharedGraphicsContext>);
 
 impl ComponentRegistry {
     pub fn new() -> Self {
@@ -196,8 +196,10 @@ impl ComponentRegistry {
         self.inspectors.insert(
             type_id,
             Box::new(|world, entity, ui, graphics| {
+                let world_ptr = world as *const hecs::World;
                 if let Ok(mut comp) = world.get::<&mut T>(entity) {
-                    comp.inspect(ui, graphics);
+                    let world_ref = unsafe { &*world_ptr };
+                    comp.inspect(world_ref, entity, ui, graphics);
                 }
             }),
         );
@@ -492,7 +494,13 @@ pub trait Component: Sync + Send {
 
 pub trait InspectableComponent: Send + Sync {
     /// In the editor, how the component will be represented in the `Resource Viewer` dock.
-    fn inspect(&mut self, ui: &mut egui::Ui, graphics: Arc<SharedGraphicsContext>);
+    fn inspect(
+        &mut self,
+        world: &hecs::World,
+        entity: hecs::Entity,
+        ui: &mut egui::Ui,
+        graphics: Arc<SharedGraphicsContext>,
+    );
 }
 
 #[typetag::serde]
@@ -701,7 +709,13 @@ impl Component for MeshRenderer {
 }
 
 impl InspectableComponent for MeshRenderer {
-    fn inspect(&mut self, ui: &mut egui::Ui, graphics: Arc<SharedGraphicsContext>) {
+    fn inspect(
+        &mut self,
+        _world: &hecs::World,
+        _entity: hecs::Entity,
+        ui: &mut egui::Ui,
+        graphics: Arc<SharedGraphicsContext>,
+    ) {
         fn is_probably_model_uri(uri: &str) -> bool {
             let uri = uri.to_ascii_lowercase();
             uri.ends_with(".glb")
diff --git a/crates/eucalyptus-core/src/lighting.rs b/crates/eucalyptus-core/src/lighting.rs
index 799998f..7483458 100644
--- a/crates/eucalyptus-core/src/lighting.rs
+++ b/crates/eucalyptus-core/src/lighting.rs
@@ -85,7 +85,13 @@ impl Component for Light {
 }
 
 impl InspectableComponent for Light {
-    fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
+    fn inspect(
+        &mut self,
+        _world: &World,
+        _entity: Entity,
+        ui: &mut Ui,
+        _graphics: Arc<SharedGraphicsContext>,
+    ) {
         CollapsingHeader::new("Light")
             .default_open(true)
             .show(ui, |ui| {
diff --git a/crates/eucalyptus-core/src/physics/collider.rs b/crates/eucalyptus-core/src/physics/collider.rs
index 5b27ec8..88d764f 100644
--- a/crates/eucalyptus-core/src/physics/collider.rs
+++ b/crates/eucalyptus-core/src/physics/collider.rs
@@ -102,7 +102,13 @@ impl Component for ColliderGroup {
 }
 
 impl InspectableComponent for ColliderGroup {
-    fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
+    fn inspect(
+        &mut self,
+        _world: &World,
+        _entity: Entity,
+        ui: &mut Ui,
+        _graphics: Arc<SharedGraphicsContext>,
+    ) {
         CollapsingHeader::new("Colliders")
             .default_open(true)
             .show(ui, |ui| {
@@ -398,9 +404,9 @@ impl From<&ColliderShape> for ColliderShapeKey {
         match *shape {
             ColliderShape::Box { half_extents } => Self::Box {
                 half_extents_bits: [
-                    half_extents.x.to_bits() as u32,
-                    half_extents.y.to_bits() as u32,
-                    half_extents.z.to_bits() as u32,
+                    (half_extents.x as f32).to_bits(),
+                    (half_extents.y as f32).to_bits(),
+                    (half_extents.z as f32).to_bits(),
                 ],
             },
             ColliderShape::Sphere { radius } => Self::Sphere {
diff --git a/crates/eucalyptus-core/src/physics/collider/shaders/collider.wgsl b/crates/eucalyptus-core/src/physics/collider/shaders/collider.wgsl
index b899ac9..c60a251 100644
--- a/crates/eucalyptus-core/src/physics/collider/shaders/collider.wgsl
+++ b/crates/eucalyptus-core/src/physics/collider/shaders/collider.wgsl
@@ -1,6 +1,9 @@
 struct CameraUniform {
     view_pos: vec4<f32>,
+    view: mat4x4<f32>,
     view_proj: mat4x4<f32>,
+    inv_proj: mat4x4<f32>,
+    inv_view: mat4x4<f32>,
 };
 
 @group(0) @binding(0)
diff --git a/crates/eucalyptus-core/src/physics/kcc.rs b/crates/eucalyptus-core/src/physics/kcc.rs
index fe21794..ce78155 100644
--- a/crates/eucalyptus-core/src/physics/kcc.rs
+++ b/crates/eucalyptus-core/src/physics/kcc.rs
@@ -5,7 +5,7 @@ pub mod character_collision;
 
 use crate::component::{Component, ComponentDescriptor, InspectableComponent, SerializedComponent};
 use crate::physics::PhysicsState;
-use crate::ptr::WorldPtr;
+use crate::ptr::{WorldPtr};
 use crate::scripting::jni::utils::ToJObject;
 use crate::scripting::native::DropbearNativeError;
 use crate::scripting::result::DropbearNativeResult;
@@ -16,9 +16,7 @@ use egui::{ComboBox, DragValue, Ui};
 use hecs::{Entity, World};
 use jni::JNIEnv;
 use jni::objects::{JObject, JValue};
-use rapier3d::control::{
-    CharacterAutostep, CharacterCollision, CharacterLength, KinematicCharacterController,
-};
+use rapier3d::control::{CharacterAutostep, CharacterCollision, CharacterLength, KinematicCharacterController};
 use rapier3d::dynamics::RigidBodyType;
 use rapier3d::math::Rotation;
 use rapier3d::na::{UnitVector3, Vector3};
@@ -33,6 +31,42 @@ pub struct KCC {
     pub controller: KinematicCharacterController,
     #[serde(skip)]
     pub collisions: Vec<CharacterCollision>,
+    #[serde(skip)]
+    pub movement: Option<CharacterMovementResult>,
+}
+
+#[repr(C)]
+#[derive(Debug, Clone)]
+pub struct CharacterMovementResult {
+    pub translation: NVector3,
+    pub grounded: bool,
+    pub is_sliding_down_slope: bool,
+}
+
+impl ToJObject for CharacterMovementResult {
+    fn to_jobject<'a>(&self, env: &mut JNIEnv<'a>) -> DropbearNativeResult<JObject<'a>> {
+        let class = env
+            .find_class("com/dropbear/physics/CharacterMovementResult")
+            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
+
+        let translation_obj = self.translation.to_jobject(env)?;
+
+        let args = [
+            JValue::Object(&translation_obj),
+            JValue::Bool(self.grounded as u8),
+            JValue::Bool(self.is_sliding_down_slope as u8),
+        ];
+
+        let obj = env
+            .new_object(
+                &class,
+                "(Lcom/dropbear/math/Vector3d;ZZ)V",
+                &args,
+            )
+            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
+
+        Ok(obj)
+    }
 }
 
 #[typetag::serde]
@@ -51,10 +85,10 @@ impl Component for KCC {
         }
     }
 
-    fn init<'a>(
-        ser: &'a Self::SerializedForm,
+    fn init(
+        ser: &'_ Self::SerializedForm,
         _: Arc<SharedGraphicsContext>,
-    ) -> crate::component::ComponentInitFuture<'a, Self> {
+    ) -> crate::component::ComponentInitFuture<'_, Self> {
         Box::pin(async move { Ok((ser.clone(),)) })
     }
 
@@ -74,7 +108,13 @@ impl Component for KCC {
 }
 
 impl InspectableComponent for KCC {
-    fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
+    fn inspect(
+        &mut self,
+        _world: &World,
+        _entity: Entity,
+        ui: &mut Ui,
+        _graphics: Arc<SharedGraphicsContext>,
+    ) {
         egui::CollapsingHeader::new("Kinematic Character Controller")
             .default_open(true)
             .show(ui, |ui| {
@@ -223,6 +263,7 @@ impl KCC {
             entity: label.clone(),
             controller: KinematicCharacterController::default(),
             collisions: vec![],
+            movement: None,
         }
     }
 }
@@ -301,7 +342,7 @@ fn move_character(
     translation: &NVector3,
     delta_time: f64,
 ) -> DropbearNativeResult<()> {
-    if let Ok((label, kcc)) = world.query_one::<(&Label, &KCC)>(entity).get() {
+    if let Ok((label, kcc)) = world.query_one::<(&Label, &mut KCC)>(entity).get() {
         let rigid_body_handle = physics_state
             .bodies_entity_map
             .get(label)
@@ -337,7 +378,7 @@ fn move_character(
             *collider.position()
         };
 
-        let filter = QuprintlneryFilter::default().exclude_rigid_body(*rigid_body_handle);
+        let filter = QueryFilter::default().exclude_rigid_body(*rigid_body_handle);
         let query_pipeline = physics_state.broad_phase.as_query_pipeline(
             physics_state.narrow_phase.query_dispatcher(),
             &physics_state.bodies,
@@ -374,6 +415,12 @@ fn move_character(
             body.set_next_kinematic_translation(new_pos);
         }
 
+        kcc.movement = Some(CharacterMovementResult {
+            translation: movement.translation.into(),
+            grounded: movement.grounded,
+            is_sliding_down_slope: movement.is_sliding_down_slope,
+        });
+
         Ok(())
     } else {
         Err(DropbearNativeError::MissingComponent)
@@ -467,3 +514,17 @@ fn get_hit(
         collisions,
     })
 }
+
+#[dropbear_macro::export(
+    kotlin(
+        class = "com.dropbear.physics.KinematicCharacterControllerNative",
+        func = "getMovementResult"
+    ),
+    c
+)]
+fn get_movement_result(
+    #[dropbear_macro::define(WorldPtr)] world: &hecs::World,
+    #[dropbear_macro::entity] entity: hecs::Entity,
+) -> DropbearNativeResult<Option<CharacterMovementResult>> {
+    world.get::<&KCC>(entity).map(|kcc| kcc.movement.clone()).map(Ok).unwrap_or(Ok(None))
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/physics/rigidbody.rs b/crates/eucalyptus-core/src/physics/rigidbody.rs
index f3e3171..3d92997 100644
--- a/crates/eucalyptus-core/src/physics/rigidbody.rs
+++ b/crates/eucalyptus-core/src/physics/rigidbody.rs
@@ -233,7 +233,13 @@ impl Component for RigidBody {
 }
 
 impl InspectableComponent for RigidBody {
-    fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
+    fn inspect(
+        &mut self,
+        _world: &World,
+        _entity: Entity,
+        ui: &mut Ui,
+        _graphics: Arc<SharedGraphicsContext>,
+    ) {
         CollapsingHeader::new("RigidBody")
             .default_open(true)
             .show(ui, |ui| {
diff --git a/crates/eucalyptus-core/src/properties.rs b/crates/eucalyptus-core/src/properties.rs
index fe2c7f7..98f77db 100644
--- a/crates/eucalyptus-core/src/properties.rs
+++ b/crates/eucalyptus-core/src/properties.rs
@@ -61,7 +61,13 @@ impl Component for CustomProperties {
 }
 
 impl InspectableComponent for CustomProperties {
-    fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
+    fn inspect(
+        &mut self,
+        _world: &World,
+        _entity: Entity,
+        ui: &mut Ui,
+        _graphics: Arc<SharedGraphicsContext>,
+    ) {
         CollapsingHeader::new("Custom Properties")
             .default_open(true)
             .show(ui, |ui| {
diff --git a/crates/eucalyptus-core/src/states.rs b/crates/eucalyptus-core/src/states.rs
index 56daa97..c199292 100644
--- a/crates/eucalyptus-core/src/states.rs
+++ b/crates/eucalyptus-core/src/states.rs
@@ -196,7 +196,13 @@ impl Component for Script {
 }
 
 impl InspectableComponent for Script {
-    fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
+    fn inspect(
+        &mut self,
+        _world: &World,
+        _entity: Entity,
+        ui: &mut Ui,
+        _graphics: Arc<SharedGraphicsContext>,
+    ) {
         CollapsingHeader::new("Scripting")
             .default_open(true)
             .show(ui, |ui| {
diff --git a/crates/eucalyptus-core/src/transform.rs b/crates/eucalyptus-core/src/transform.rs
index fa37c50..bfa0573 100644
--- a/crates/eucalyptus-core/src/transform.rs
+++ b/crates/eucalyptus-core/src/transform.rs
@@ -53,7 +53,13 @@ impl Component for EntityTransform {
 }
 
 impl InspectableComponent for EntityTransform {
-    fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) {
+    fn inspect(
+        &mut self,
+        _world: &World,
+        _entity: Entity,
+        ui: &mut Ui,
+        _graphics: Arc<SharedGraphicsContext>,
+    ) {
         CollapsingHeader::new("Entity Transform")
             .default_open(true)
             .show(ui, |ui| {
diff --git a/crates/eucalyptus-editor/src/editor/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
index d68535d..1ed454b 100644
--- a/crates/eucalyptus-editor/src/editor/asset_viewer.rs
+++ b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
@@ -18,15 +18,6 @@ use crate::editor::{
 };
 use eucalyptus_core::component::DRAGGED_ASSET_ID;
 
-#[derive(Clone, Copy, Debug)]
-enum TextureSlot {
-    Diffuse,
-    Normal,
-    Emissive,
-    MetallicRoughness,
-    Occlusion,
-}
-
 impl<'a> EditorTabViewer<'a> {
     pub(crate) fn show_asset_viewer(&mut self, ui: &mut egui::Ui) {
         let mut cfg = TABS_GLOBAL.lock();
@@ -229,6 +220,7 @@ impl<'a> EditorTabViewer<'a> {
                                     reference_for_menu.clone(),
                                     entry_name.clone(),
                                 );
+                                info!("Loading model {}", entry_name);
                             }
                         }
 
@@ -239,46 +231,8 @@ impl<'a> EditorTabViewer<'a> {
                                     reference_for_menu.clone(),
                                     entry_name.clone(),
                                 );
+                                info!("Loading texture {}", entry_name);
                             }
-
-                            ui.separator();
-                            ui.menu_button("Choose", |ui| {
-                                if ui.button("Diffuse").clicked() {
-                                    ui.close();
-                                    self.apply_texture_slot(
-                                        reference_for_menu.clone(),
-                                        TextureSlot::Diffuse,
-                                    );
-                                }
-                                if ui.button("Normal").clicked() {
-                                    ui.close();
-                                    self.apply_texture_slot(
-                                        reference_for_menu.clone(),
-                                        TextureSlot::Normal,
-                                    );
-                                }
-                                if ui.button("Emissive").clicked() {
-                                    ui.close();
-                                    self.apply_texture_slot(
-                                        reference_for_menu.clone(),
-                                        TextureSlot::Emissive,
-                                    );
-                                }
-                                if ui.button("Metal/Rough").clicked() {
-                                    ui.close();
-                                    self.apply_texture_slot(
-                                        reference_for_menu.clone(),
-                                        TextureSlot::MetallicRoughness,
-                                    );
-                                }
-                                if ui.button("Occlusion").clicked() {
-                                    ui.close();
-                                    self.apply_texture_slot(
-                                        reference_for_menu.clone(),
-                                        TextureSlot::Occlusion,
-                                    );
-                                }
-                            });
                         }
                     });
                 builder.node(menu);
@@ -1265,7 +1219,7 @@ impl<'a> EditorTabViewer<'a> {
             {
                 Ok(v) => v,
                 Err(e) => {
-                    warn!("Unable to load model {}: {}", reference, e);
+                    eucalyptus_core::warn!("Unable to load model {}: {}", reference, e);
                     return Err(e);
                 }
             };
@@ -1308,47 +1262,6 @@ impl<'a> EditorTabViewer<'a> {
         });
     }
 
-    fn apply_texture_slot(&mut self, reference: ResourceReference, slot: TextureSlot) {
-        let Some(entity) = *self.selected_entity else {
-            warn!("Unable to apply texture: no entity selected");
-            return;
-        };
-
-        let Some(handle) = ASSET_REGISTRY
-            .read()
-            .get_texture_handle_by_reference(&reference)
-        else {
-            warn!("Texture not loaded in memory, load it first");
-            return;
-        };
-
-        let texture = {
-            let registry = ASSET_REGISTRY.read();
-            registry.get_texture(handle).cloned()
-        };
-
-        let Some(texture) = texture else {
-            warn!("Texture handle missing from registry");
-            return;
-        };
-
-        if let Ok(renderer) = self.world.query_one::<&mut MeshRenderer>(entity).get() {
-            for material in renderer.material_snapshot.values_mut() {
-                match slot {
-                    TextureSlot::Diffuse => material.diffuse_texture = texture.clone(),
-                    TextureSlot::Normal => material.normal_texture = texture.clone(),
-                    TextureSlot::Emissive => material.emissive_texture = Some(texture.clone()),
-                    TextureSlot::MetallicRoughness => {
-                        material.metallic_roughness_texture = Some(texture.clone())
-                    }
-                    TextureSlot::Occlusion => material.occlusion_texture = Some(texture.clone()),
-                }
-            }
-        } else {
-            warn!("Selected entity has no MeshRenderer");
-        }
-    }
-
     pub(crate) fn handle_tree_selection(&mut self, cfg: &mut StaticallyKept, items: &[u64]) {
         for node_id in items {
             self.resolve_tree_node(cfg, *node_id);
diff --git a/crates/eucalyptus-editor/src/editor/component.rs b/crates/eucalyptus-editor/src/editor/component.rs
deleted file mode 100644
index 42a2764..0000000
--- a/crates/eucalyptus-editor/src/editor/component.rs
+++ /dev/null
@@ -1,1377 +0,0 @@
-//! This module should describe the different components that are editable in the resource inspector.
-
-use crate::editor::{Signal, StaticallyKept, UndoableAction};
-use dropbear_engine::asset::{ASSET_REGISTRY};
-use dropbear_engine::attenuation::ATTENUATION_PRESETS;
-use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
-use dropbear_engine::lighting::LightType;
-use dropbear_engine::texture::TextureWrapMode;
-use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
-use egui::{CollapsingHeader, ComboBox, DragValue, Grid, RichText, TextEdit, Ui, UiBuilder};
-use eucalyptus_core::camera::CameraType;
-use eucalyptus_core::states::{Camera3D, Light, Property, Script};
-use eucalyptus_core::{fatal, warn};
-use glam::{DVec3, Vec3};
-use hecs::Entity;
-use std::time::Instant;
-use dropbear_engine::procedural::ProcObj;
-use eucalyptus_core::properties::{CustomProperties, Value};
-
-/// A trait that can added to any component that allows you to inspect the value in the editor.
-pub trait InspectableComponent {
-    fn inspect(
-        &mut self,
-        entity: &mut Entity,
-        cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        undo_stack: &mut Vec<UndoableAction>,
-        signal: &mut Signal,
-        _label: &mut String,
-    );
-}
-
-
-fn wrap_angle_degrees(angle: f64) -> f64 {
-    (angle + 180.0).rem_euclid(360.0) - 180.0
-}
-
-fn reconcile_angle(angle: f64, reference: f64) -> f64 {
-    let delta = wrap_angle_degrees(angle - reference);
-    wrap_angle_degrees(reference + delta)
-}
-
-impl InspectableComponent for CustomProperties {
-    fn inspect(
-        &mut self,
-        _entity: &mut Entity,
-        _cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        _undo_stack: &mut Vec<UndoableAction>,
-        _signal: &mut Signal,
-        _label: &mut String,
-    ) {
-
-    }
-}
-
-impl InspectableComponent for EntityTransform {
-    fn inspect(
-        &mut self,
-        entity: &mut Entity,
-        cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        undo_stack: &mut Vec<UndoableAction>,
-        signal: &mut Signal,
-        _label: &mut String,
-    ) {
-        self.local_mut().inspect(
-            entity,
-            cfg,
-            ui,
-            undo_stack,
-            signal,
-            &mut "Local Transform".to_string(),
-        );
-        self.world_mut().inspect(
-            entity,
-            cfg,
-            ui,
-            undo_stack,
-            signal,
-            &mut "World Transform".to_string(),
-        );
-    }
-}
-
-fn inspect_light_transform(
-    transform: &mut Transform,
-    entity: &mut Entity,
-    cfg: &mut StaticallyKept,
-    ui: &mut Ui,
-    undo_stack: &mut Vec<UndoableAction>,
-    light_type: &LightType,
-) {
-    let show_position = matches!(light_type, LightType::Point | LightType::Spot);
-    let show_rotation = !matches!(light_type, LightType::Point);
-
-    if show_position {
-        ui.label(RichText::new("Position").strong());
-
-        ui.horizontal_wrapped(|ui| {
-            ui.horizontal(|ui| {
-                ui.label("X:");
-                let response = ui.add(
-                    egui::DragValue::new(&mut transform.position.x)
-                        .speed(0.1)
-                        .fixed_decimals(3),
-                );
-
-                if response.drag_started() {
-                    cfg.transform_old_entity = Some(*entity);
-                    cfg.transform_original_transform = Some(*transform);
-                    cfg.transform_in_progress = true;
-                }
-
-                if response.drag_stopped() && cfg.transform_in_progress {
-                    if let Some(ent) = cfg.transform_old_entity.take()
-                        && let Some(orig) = cfg.transform_original_transform.take()
-                    {
-                        UndoableAction::push_to_undo(
-                            undo_stack,
-                            UndoableAction::Transform(ent, orig),
-                        );
-                    }
-                    cfg.transform_in_progress = false;
-                }
-            });
-
-            ui.horizontal(|ui| {
-                ui.label("Y:");
-                let response = ui.add(
-                    egui::DragValue::new(&mut transform.position.y)
-                        .speed(0.1)
-                        .fixed_decimals(3),
-                );
-
-                if response.drag_started() {
-                    cfg.transform_old_entity = Some(*entity);
-                    cfg.transform_original_transform = Some(*transform);
-                    cfg.transform_in_progress = true;
-                }
-
-                if response.drag_stopped() && cfg.transform_in_progress {
-                    if let Some(ent) = cfg.transform_old_entity.take()
-                        && let Some(orig) = cfg.transform_original_transform.take()
-                    {
-                        UndoableAction::push_to_undo(
-                            undo_stack,
-                            UndoableAction::Transform(ent, orig),
-                        );
-                    }
-                    cfg.transform_in_progress = false;
-                }
-            });
-
-            ui.horizontal(|ui| {
-                ui.label("Z:");
-                let response = ui.add(
-                    egui::DragValue::new(&mut transform.position.z)
-                        .speed(0.1)
-                        .fixed_decimals(3),
-                );
-
-                if response.drag_started() {
-                    cfg.transform_old_entity = Some(*entity);
-                    cfg.transform_original_transform = Some(*transform);
-                    cfg.transform_in_progress = true;
-                }
-
-                if response.drag_stopped() && cfg.transform_in_progress {
-                    if let Some(ent) = cfg.transform_old_entity.take()
-                        && let Some(orig) = cfg.transform_original_transform.take()
-                    {
-                        UndoableAction::push_to_undo(
-                            undo_stack,
-                            UndoableAction::Transform(ent, orig),
-                        );
-                    }
-                    cfg.transform_in_progress = false;
-                }
-            });
-        });
-
-        if ui.button("Reset Position").clicked() {
-            transform.position = DVec3::ZERO;
-        }
-        ui.add_space(5.0);
-    }
-
-    if show_rotation {
-        ui.label(RichText::new("Rotation").strong());
-
-        let mut rotation_deg = *cfg
-            .transform_rotation_cache
-            .entry(*entity)
-            .or_insert_with(|| {
-                let (x, y, z) = transform.rotation.to_euler(glam::EulerRot::XYZ);
-                DVec3::new(x.to_degrees(), y.to_degrees(), z.to_degrees())
-            });
-
-        if let Some(prev) = cfg.transform_rotation_cache.get(entity) {
-            let mut degrees = rotation_deg;
-            let (x, y, z) = transform.rotation.to_euler(glam::EulerRot::XYZ);
-            degrees.x = x.to_degrees();
-            degrees.y = y.to_degrees();
-            degrees.z = z.to_degrees();
-
-            degrees.x = reconcile_angle(degrees.x, prev.x);
-            degrees.y = reconcile_angle(degrees.y, prev.y);
-            degrees.z = reconcile_angle(degrees.z, prev.z);
-
-            degrees.x = wrap_angle_degrees(degrees.x);
-            degrees.y = wrap_angle_degrees(degrees.y);
-            degrees.z = wrap_angle_degrees(degrees.z);
-
-            cfg.transform_rotation_cache.insert(*entity, degrees);
-            rotation_deg = degrees;
-        };
-
-        let mut rotation_changed = false;
-
-        ui.horizontal(|ui| {
-            ui.label("Pitch (X):");
-            let response = ui.add(
-                egui::DragValue::new(&mut rotation_deg.x)
-                    .speed(0.5)
-                    .suffix("°")
-                    .range(-180.0..=180.0)
-                    .fixed_decimals(2),
-            );
-
-            if response.drag_started() {
-                cfg.transform_old_entity = Some(*entity);
-                cfg.transform_original_transform = Some(*transform);
-                cfg.transform_in_progress = true;
-            }
-
-            if response.changed() {
-                rotation_changed = true;
-            }
-
-            if response.drag_stopped() && cfg.transform_in_progress {
-                if let Some(ent) = cfg.transform_old_entity.take()
-                    && let Some(orig) = cfg.transform_original_transform.take()
-                {
-                    UndoableAction::push_to_undo(
-                        undo_stack,
-                        UndoableAction::Transform(ent, orig),
-                    );
-                }
-                cfg.transform_in_progress = false;
-            }
-        });
-
-        ui.horizontal(|ui| {
-            ui.label("Yaw (Y):");
-            let response = ui.add(
-                egui::DragValue::new(&mut rotation_deg.y)
-                    .speed(0.5)
-                    .suffix("°")
-                    .range(-180.0..=180.0)
-                    .fixed_decimals(2),
-            );
-
-            if response.drag_started() {
-                cfg.transform_old_entity = Some(*entity);
-                cfg.transform_original_transform = Some(*transform);
-                cfg.transform_in_progress = true;
-            }
-
-            if response.changed() {
-                rotation_changed = true;
-            }
-
-            if response.drag_stopped() && cfg.transform_in_progress {
-                if let Some(ent) = cfg.transform_old_entity.take()
-                    && let Some(orig) = cfg.transform_original_transform.take()
-                {
-                    UndoableAction::push_to_undo(
-                        undo_stack,
-                        UndoableAction::Transform(ent, orig),
-                    );
-                }
-                cfg.transform_in_progress = false;
-            }
-        });
-
-        ui.horizontal(|ui| {
-            ui.label("Roll (Z):");
-            let response = ui.add(
-                egui::DragValue::new(&mut rotation_deg.z)
-                    .speed(0.5)
-                    .suffix("°")
-                    .range(-180.0..=180.0)
-                    .fixed_decimals(2),
-            );
-
-            if response.drag_started() {
-                cfg.transform_old_entity = Some(*entity);
-                cfg.transform_original_transform = Some(*transform);
-                cfg.transform_in_progress = true;
-            }
-
-            if response.changed() {
-                rotation_changed = true;
-            }
-
-            if response.drag_stopped() && cfg.transform_in_progress {
-                if let Some(ent) = cfg.transform_old_entity.take()
-                    && let Some(orig) = cfg.transform_original_transform.take()
-                {
-                    UndoableAction::push_to_undo(
-                        undo_stack,
-                        UndoableAction::Transform(ent, orig),
-                    );
-                }
-                cfg.transform_in_progress = false;
-            }
-        });
-
-        if rotation_changed {
-            let rot_x = glam::DQuat::from_rotation_x(rotation_deg.x.to_radians());
-            let rot_y = glam::DQuat::from_rotation_y(rotation_deg.y.to_radians());
-            let rot_z = glam::DQuat::from_rotation_z(rotation_deg.z.to_radians());
-            transform.rotation = rot_y * rot_x * rot_z;
-            cfg.transform_rotation_cache.insert(*entity, rotation_deg);
-        }
-
-        if ui.button("Reset Rotation").clicked() {
-            transform.rotation = glam::DQuat::IDENTITY;
-            cfg.transform_rotation_cache.insert(*entity, DVec3::NEG_Y);
-        }
-        ui.add_space(5.0);
-    }
-}
-
-fn inspect_transform(
-    transform: &mut Transform,
-    entity: &mut Entity,
-    cfg: &mut StaticallyKept,
-    ui: &mut Ui,
-    undo_stack: &mut Vec<UndoableAction>,
-    label: &str,
-    show_position: bool,
-    show_rotation: bool,
-    show_scale: bool,
-) {
-    ui.vertical(|ui| {
-        CollapsingHeader::new(label)
-            .default_open(true)
-            .show(ui, |ui| {
-                if show_position {
-                    ui.horizontal(|ui| {
-                        ui.label("Position:");
-                    });
-
-                    ui.horizontal_wrapped(|ui| {
-                        ui.horizontal(|ui| {
-                            ui.label("X:");
-                            let response = ui.add(
-                                egui::DragValue::new(&mut transform.position.x)
-                                    .speed(0.1)
-                                    .fixed_decimals(3),
-                            );
-
-                            if response.drag_started() {
-                                cfg.transform_old_entity = Some(*entity);
-                                cfg.transform_original_transform = Some(*transform);
-                                cfg.transform_in_progress = true;
-                            }
-
-                            if response.drag_stopped() && cfg.transform_in_progress {
-                                if let Some(ent) = cfg.transform_old_entity.take()
-                                    && let Some(orig) = cfg.transform_original_transform.take()
-                                {
-                                    UndoableAction::push_to_undo(
-                                        undo_stack,
-                                        UndoableAction::Transform(ent, orig),
-                                    );
-                                    log::debug!("Pushed X transform change to undo stack");
-                                }
-                                cfg.transform_in_progress = false;
-                            }
-                        });
-                        ui.horizontal(|ui| {
-                            ui.label("Y:");
-                            let response = ui.add(
-                                egui::DragValue::new(&mut transform.position.y)
-                                    .speed(0.1)
-                                    .fixed_decimals(3),
-                            );
-
-                            if response.drag_started() {
-                                cfg.transform_old_entity = Some(*entity);
-                                cfg.transform_original_transform = Some(*transform);
-                                cfg.transform_in_progress = true;
-                            }
-
-                            if response.drag_stopped() && cfg.transform_in_progress {
-                                if let Some(ent) = cfg.transform_old_entity.take()
-                                    && let Some(orig) = cfg.transform_original_transform.take()
-                                {
-                                    UndoableAction::push_to_undo(
-                                        undo_stack,
-                                        UndoableAction::Transform(ent, orig),
-                                    );
-                                    log::debug!("Pushed Y transform change to undo stack");
-                                }
-                                cfg.transform_in_progress = false;
-                            }
-                        });
-
-                        ui.horizontal(|ui| {
-                            ui.label("Z:");
-                            let response = ui.add(
-                                egui::DragValue::new(&mut transform.position.z)
-                                    .speed(0.1)
-                                    .fixed_decimals(3),
-                            );
-
-                            if response.drag_started() {
-                                cfg.transform_old_entity = Some(*entity);
-                                cfg.transform_original_transform = Some(*transform);
-                                cfg.transform_in_progress = true;
-                            }
-
-                            if response.drag_stopped() && cfg.transform_in_progress {
-                                if let Some(ent) = cfg.transform_old_entity.take()
-                                    && let Some(orig) = cfg.transform_original_transform.take()
-                                {
-                                    UndoableAction::push_to_undo(
-                                        undo_stack,
-                                        UndoableAction::Transform(ent, orig),
-                                    );
-                                    log::debug!("Pushed Z transform change to undo stack");
-                                }
-                                cfg.transform_in_progress = false;
-                            }
-                        });
-                    });
-
-                    if ui.button("Reset Position").clicked() {
-                        transform.position = DVec3::ZERO;
-                    }
-
-                    ui.add_space(5.0);
-                }
-
-                if show_rotation {
-                    ui.label("Rotation:");
-
-                    ui.horizontal_wrapped(|ui| {
-                        let cached_rotation = cfg.transform_rotation_cache.get(entity).copied();
-
-                        let mut rotation_deg: DVec3 = if cfg.transform_in_progress {
-                            cached_rotation.unwrap_or_else(|| {
-                                let (x, y, z) = transform.rotation.to_euler(glam::EulerRot::YXZ);
-                                DVec3::new(x.to_degrees(), y.to_degrees(), z.to_degrees())
-                            })
-                        } else {
-                            let (x, y, z) = transform.rotation.to_euler(glam::EulerRot::YXZ);
-                            let mut degrees =
-                                DVec3::new(x.to_degrees(), y.to_degrees(), z.to_degrees());
-
-                            if let Some(prev) = cached_rotation {
-                                degrees.x = reconcile_angle(degrees.x, prev.x);
-                                degrees.y = reconcile_angle(degrees.y, prev.y);
-                                degrees.z = reconcile_angle(degrees.z, prev.z);
-                            }
-
-                            degrees.x = wrap_angle_degrees(degrees.x);
-                            degrees.y = wrap_angle_degrees(degrees.y);
-                            degrees.z = wrap_angle_degrees(degrees.z);
-
-                            cfg.transform_rotation_cache.insert(*entity, degrees);
-                            degrees
-                        };
-
-                        let mut rotation_changed = false;
-
-                        ui.horizontal(|ui| {
-                            ui.label("Pitch (X):");
-                            let response = ui.add(
-                                egui::DragValue::new(&mut rotation_deg.x)
-                                    .speed(0.5)
-                                    .suffix("°")
-                                    .range(-180.0..=180.0)
-                                    .fixed_decimals(2),
-                            );
-
-                            if response.drag_started() {
-                                cfg.transform_old_entity = Some(*entity);
-                                cfg.transform_original_transform = Some(*transform);
-                                cfg.transform_in_progress = true;
-                            }
-
-                            if response.changed() {
-                                rotation_changed = true;
-                            }
-
-                            if response.drag_stopped() && cfg.transform_in_progress {
-                                if let Some(ent) = cfg.transform_old_entity.take()
-                                    && let Some(orig) = cfg.transform_original_transform.take()
-                                {
-                                    UndoableAction::push_to_undo(
-                                        undo_stack,
-                                        UndoableAction::Transform(ent, orig),
-                                    );
-                                    log::debug!("Pushed X rotation change to undo stack");
-                                }
-                                cfg.transform_in_progress = false;
-                            }
-                        });
-
-                        ui.horizontal(|ui| {
-                            ui.label("Yaw (Y):");
-                            let response = ui.add(
-                                egui::DragValue::new(&mut rotation_deg.y)
-                                    .speed(0.5)
-                                    .suffix("°")
-                                    .range(-180.0..=180.0)
-                                    .fixed_decimals(2),
-                            );
-
-                            if response.drag_started() {
-                                cfg.transform_old_entity = Some(*entity);
-                                cfg.transform_original_transform = Some(*transform);
-                                cfg.transform_in_progress = true;
-                            }
-
-                            if response.changed() {
-                                rotation_changed = true;
-                            }
-
-                            if response.drag_stopped() && cfg.transform_in_progress {
-                                if let Some(ent) = cfg.transform_old_entity.take()
-                                    && let Some(orig) = cfg.transform_original_transform.take()
-                                {
-                                    UndoableAction::push_to_undo(
-                                        undo_stack,
-                                        UndoableAction::Transform(ent, orig),
-                                    );
-                                    log::debug!("Pushed Y rotation change to undo stack");
-                                }
-                                cfg.transform_in_progress = false;
-                            }
-                        });
-
-                        ui.horizontal(|ui| {
-                            ui.label("Roll (Z):");
-                            let response = ui.add(
-                                egui::DragValue::new(&mut rotation_deg.z)
-                                    .speed(0.5)
-                                    .suffix("°")
-                                    .range(-180.0..=180.0)
-                                    .fixed_decimals(2),
-                            );
-
-                            if response.drag_started() {
-                                cfg.transform_old_entity = Some(*entity);
-                                cfg.transform_original_transform = Some(*transform);
-                                cfg.transform_in_progress = true;
-                            }
-
-                            if response.changed() {
-                                rotation_changed = true;
-                            }
-
-                            if response.drag_stopped() && cfg.transform_in_progress {
-                                if let Some(ent) = cfg.transform_old_entity.take()
-                                    && let Some(orig) = cfg.transform_original_transform.take()
-                                {
-                                    UndoableAction::push_to_undo(
-                                        undo_stack,
-                                        UndoableAction::Transform(ent, orig),
-                                    );
-                                    log::debug!("Pushed Z rotation change to undo stack");
-                                }
-                                cfg.transform_in_progress = false;
-                            }
-                        });
-
-                        if rotation_changed {
-                            rotation_deg.x = wrap_angle_degrees(rotation_deg.x);
-                            rotation_deg.y = wrap_angle_degrees(rotation_deg.y);
-                            rotation_deg.z = wrap_angle_degrees(rotation_deg.z);
-
-                            cfg.transform_rotation_cache.insert(*entity, rotation_deg);
-                            transform.rotation = glam::DQuat::from_euler(
-                                glam::EulerRot::YXZ,
-                                rotation_deg.x.to_radians(),
-                                rotation_deg.y.to_radians(),
-                                rotation_deg.z.to_radians(),
-                            );
-                        }
-                    });
-
-                    if ui.button("Reset Rotation").clicked() {
-                        transform.rotation = glam::DQuat::IDENTITY;
-                        cfg.transform_rotation_cache.insert(*entity, DVec3::ZERO);
-                    }
-                    ui.add_space(5.0);
-                }
-
-                if show_scale {
-                    ui.horizontal(|ui| {
-                        ui.label("Scale:");
-                        let lock_icon = if cfg.scale_locked { "🔒" } else { "🔓" };
-                        if ui
-                            .button(lock_icon)
-                            .on_hover_text("Lock uniform scaling")
-                            .clicked()
-                        {
-                            cfg.scale_locked = !cfg.scale_locked;
-                        }
-                    });
-
-                    let mut scale_changed = false;
-                    let mut new_scale = transform.scale;
-
-                    ui.horizontal(|ui| {
-                        ui.label("X:");
-                        let response = ui.add(
-                            DragValue::new(&mut new_scale.x)
-                                .speed(0.01)
-                                .fixed_decimals(3),
-                        );
-
-                        if response.drag_started() {
-                            cfg.transform_old_entity = Some(*entity);
-                            cfg.transform_original_transform = Some(*transform);
-                            cfg.transform_in_progress = true;
-                        }
-
-                        if response.changed() {
-                            scale_changed = true;
-                            if cfg.scale_locked {
-                                let scale_factor = new_scale.x / transform.scale.x;
-                                new_scale.y = transform.scale.y * scale_factor;
-                                new_scale.z = transform.scale.z * scale_factor;
-                            }
-                        }
-
-                        if response.drag_stopped() && cfg.transform_in_progress {
-                            if let Some(ent) = cfg.transform_old_entity.take()
-                                && let Some(orig) = cfg.transform_original_transform.take()
-                            {
-                                UndoableAction::push_to_undo(
-                                    undo_stack,
-                                    UndoableAction::Transform(ent, orig),
-                                );
-                                log::debug!("Pushed X scale change to undo stack");
-                            }
-                            cfg.transform_in_progress = false;
-                        }
-                    });
-
-                    ui.horizontal(|ui| {
-                        ui.label("Y:");
-                        let y_slider = egui::DragValue::new(&mut new_scale.y)
-                            .speed(0.01)
-                            .fixed_decimals(3);
-
-                        let response = ui.add_enabled(!cfg.scale_locked, y_slider);
-
-                        if response.drag_started() && !cfg.scale_locked {
-                            cfg.transform_old_entity = Some(*entity);
-                            cfg.transform_original_transform = Some(*transform);
-                            cfg.transform_in_progress = true;
-                        }
-
-                        if response.changed() {
-                            scale_changed = true;
-                        }
-
-                        if response.drag_stopped() && cfg.transform_in_progress {
-                            if let Some(ent) = cfg.transform_old_entity.take()
-                                && let Some(orig) = cfg.transform_original_transform.take()
-                            {
-                                UndoableAction::push_to_undo(
-                                    undo_stack,
-                                    UndoableAction::Transform(ent, orig),
-                                );
-                                log::debug!("Pushed Y scale change to undo stack");
-                            }
-                            cfg.transform_in_progress = false;
-                        }
-                    });
-
-                    ui.horizontal(|ui| {
-                        ui.label("Z:");
-                        let z_slider = egui::DragValue::new(&mut new_scale.z)
-                            .speed(0.01)
-                            .fixed_decimals(3);
-
-                        let response = ui.add_enabled(!cfg.scale_locked, z_slider);
-
-                        if response.drag_started() && !cfg.scale_locked {
-                            cfg.transform_old_entity = Some(*entity);
-                            cfg.transform_original_transform = Some(*transform);
-                            cfg.transform_in_progress = true;
-                        }
-
-                        if response.changed() {
-                            scale_changed = true;
-                        }
-
-                        if response.drag_stopped() && cfg.transform_in_progress {
-                            if let Some(ent) = cfg.transform_old_entity.take()
-                                && let Some(orig) = cfg.transform_original_transform.take()
-                            {
-                                UndoableAction::push_to_undo(
-                                    undo_stack,
-                                    UndoableAction::Transform(ent, orig),
-                                );
-                                log::debug!("Pushed Z scale change to undo stack");
-                            }
-                            cfg.transform_in_progress = false;
-                        }
-                    });
-
-                    if scale_changed {
-                        transform.scale = new_scale;
-                    }
-
-                    if ui.button("Reset Scale").clicked() {
-                        transform.scale = DVec3::ONE;
-                    }
-                    ui.add_space(5.0);
-                }
-            });
-    });
-    ui.separator();
-}
-
-impl InspectableComponent for Transform {
-    fn inspect(
-        &mut self,
-        entity: &mut Entity,
-        cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        undo_stack: &mut Vec<UndoableAction>,
-        _signal: &mut Signal,
-        label: &mut String,
-    ) {
-        inspect_transform(self, entity, cfg, ui, undo_stack, label, true, true, true);
-    }
-}
-
-impl InspectableComponent for Script {
-    fn inspect(
-        &mut self,
-        _entity: &mut Entity,
-        _cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        _undo_stack: &mut Vec<UndoableAction>,
-        _signal: &mut Signal,
-        _label: &mut String,
-    ) {
-
-    }
-}
-
-impl InspectableComponent for eucalyptus_core::states::Label {
-    fn inspect(
-        &mut self,
-        entity: &mut Entity,
-        cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        undo_stack: &mut Vec<UndoableAction>,
-        _signal: &mut Signal,
-        _label: &mut String,
-    ) {
-        ui.horizontal(|ui| {
-            ui.label("Name: ");
-
-            let resp = ui.text_edit_singleline(self.as_mut_string());
-
-            if resp.changed() {
-                if cfg.old_label_entity.is_none() {
-                    cfg.old_label_entity = Some(*entity);
-                    cfg.label_original = Some(self.to_string());
-                }
-                cfg.label_last_edit = Some(Instant::now());
-            }
-
-            if resp.lost_focus() {
-                if let Some(ent) = cfg.old_label_entity.take() {
-                    if ent == *entity {
-                        if let Some(orig) = cfg.label_original.take() {
-                            UndoableAction::push_to_undo(
-                                undo_stack,
-                                UndoableAction::Label(ent, orig),
-                            );
-                            log::debug!("Pushed label change to undo stack (immediate)");
-                        }
-                    } else {
-                        cfg.label_original = None;
-                    }
-                }
-                cfg.label_last_edit = None;
-            }
-        });
-    }
-}
-
-impl InspectableComponent for MeshRenderer {
-    fn inspect(
-        &mut self,
-        entity: &mut Entity,
-        cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        _undo_stack: &mut Vec<UndoableAction>,
-        signal: &mut Signal,
-        _label: &mut String,
-    ) {
-        fn is_probably_model_uri(uri: &str) -> bool {
-            let uri = uri.to_ascii_lowercase();
-            uri.ends_with(".glb")
-                || uri.ends_with(".gltf")
-                || uri.ends_with(".obj")
-                || uri.ends_with(".fbx")
-        }
-
-        fn is_probably_texture_uri(uri: &str) -> bool {
-            let uri = uri.to_ascii_lowercase();
-            uri.ends_with(".png")
-                || uri.ends_with(".jpg")
-                || uri.ends_with(".jpeg")
-                || uri.ends_with(".tga")
-                || uri.ends_with(".bmp")
-        }
-
-        let model_reference = self.model().path.clone();
-        let model_title = match &model_reference.ref_type {
-            ResourceReferenceType::Unassigned { .. } => "None".to_string(),
-            ResourceReferenceType::ProcObj(obj) => {
-                match obj {
-                    ProcObj::Cuboid { .. } => "Cuboid".to_string(),
-                }
-            }
-            _ => self.handle().label.clone(),
-        };
-
-        ui.vertical(|ui| {
-            let expand_id = ui.make_persistent_id(format!("mesh_renderer_expand_{:?}", entity));
-            let mut expanded = ui.data_mut(|d| d.get_temp::<bool>(expand_id).unwrap_or(false));
-
-            let mut selected_model: Option<AssetHandle> = None;
-            let mut choose_proc_cuboid = false;
-            let mut choose_none = false;
-
-            let (rect, response) = ui.allocate_exact_size(
-                egui::vec2(ui.available_width(), 72.0),
-                egui::Sense::click(),
-            );
-
-            let fill = if response.hovered() {
-                ui.visuals().widgets.hovered.bg_fill
-            } else {
-                ui.visuals().widgets.inactive.bg_fill
-            };
-
-            ui.painter()
-                .rect_filled(rect, 4.0, fill);
-            ui.painter()
-                .rect_stroke(
-                    rect,
-                    4.0,
-                    ui.visuals().widgets.inactive.bg_stroke,
-                    egui::StrokeKind::Inside,
-                );
-
-            let mut card_ui = ui.new_child(
-                UiBuilder::new()
-                    .layout(egui::Layout::top_down(egui::Align::Min))
-                    .max_rect(rect),
-            );
-
-            card_ui.horizontal(|ui| {
-                let arrow = if expanded { "🔽" } else { "▶️" };
-                if ui.button(arrow).clicked() {
-                    expanded = !expanded;
-                }
-
-                ui.vertical(|ui| {
-                    ui.label(RichText::new(&model_title).strong());
-                    ui.label(
-                        RichText::new("Drop a model from the Asset Viewer")
-                            .small()
-                            .color(ui.visuals().weak_text_color()),
-                    );
-                });
-
-                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
-                    ComboBox::from_id_salt("mesh_renderer_model_picker")
-                        .selected_text(&model_title)
-                        .show_ui(ui, |ui| {
-                            if ui
-                                .selectable_label(
-                                    matches!(
-                                        model_reference.ref_type,
-                                        ResourceReferenceType::Unassigned { .. }
-                                    ),
-                                    "None",
-                                )
-                                .clicked()
-                            {
-                                choose_none = true;
-                            }
-
-                            ui.separator();
-
-                            if ui
-                                .selectable_label(
-                                    matches!(model_reference.ref_type, ResourceReferenceType::ProcObj(ProcObj::Cuboid { .. })),
-                                    "Cuboid",
-                                )
-                                .clicked()
-                            {
-                                choose_proc_cuboid = true;
-                            }
-
-                            ui.separator();
-
-                            for i in ASSET_REGISTRY.iter_model() {
-                                if i.path.as_uri().is_none() {
-                                    continue;
-                                }
-                                if i.label.eq_ignore_ascii_case("light cube") {
-                                    continue;
-                                }
-
-                                let is_selected = self.asset_handle() == *i.key();
-                                if ui
-                                    .selectable_label(is_selected, i.label.clone())
-                                    .clicked()
-                                {
-                                    selected_model = Some(*i.key());
-                                }
-                            }
-                        });
-                });
-            });
-
-            let pointer_released = ui.input(|i| i.pointer.any_released());
-            if pointer_released && response.hovered() {
-                if let Some(asset) = cfg.dragged_asset.clone() {
-                    if let Some(uri) = asset.path.as_uri() {
-                        if is_probably_model_uri(uri) {
-                            *signal = Signal::ReplaceModel(*entity, uri.to_string());
-                            cfg.dragged_asset = None;
-                        }
-                    }
-                }
-            }
-
-            ui.data_mut(|d| d.insert_temp(expand_id, expanded));
-
-            if choose_proc_cuboid {
-                let default_size = match &model_reference.ref_type {
-                    ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) => [
-                        f32::from_bits(size_bits[0]),
-                        f32::from_bits(size_bits[1]),
-                        f32::from_bits(size_bits[2]),
-                    ],
-                    _ => [1.0, 1.0, 1.0],
-                };
-                *signal = Signal::SetProceduralCuboid(*entity, default_size);
-            } else if choose_none {
-                *signal = Signal::ClearModel(*entity);
-            } else if let Some(model) = selected_model {
-                if let Err(e) = self.set_asset_handle(model) {
-                    fatal!("Unable to swap model: {}", e);
-                }
-            }
-
-            if expanded {
-                ui.add_space(6.0);
-
-                if let ResourceReferenceType::File(uri) = &model_reference.ref_type {
-                    if is_probably_model_uri(uri) {
-                        let mut import_scale = self.import_scale();
-                        ui.horizontal(|ui| {
-                            ui.label("Import Scale");
-                            let resp = ui.add(
-                                egui::DragValue::new(&mut import_scale)
-                                    .speed(0.01)
-                                    .range(0.0001..=10_000.0),
-                            );
-
-                            if resp.changed() {
-                                self.set_import_scale(import_scale);
-                            }
-
-                            if ui.button("Reset").clicked() {
-                                self.set_import_scale(1.0);
-                            }
-                        });
-                        ui.add_space(6.0);
-                    }
-                }
-
-                if let ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) = &self.model().path.ref_type {
-                    let mut size = [
-                        f32::from_bits(size_bits[0]),
-                        f32::from_bits(size_bits[1]),
-                        f32::from_bits(size_bits[2]),
-                    ];
-
-                    ui.label(RichText::new("Cuboid").strong());
-                    ui.horizontal(|ui| {
-                        ui.label("Extents:");
-                        let mut changed = false;
-                        ui.label("X");
-                        changed |= ui
-                            .add(DragValue::new(&mut size[0]).speed(0.05).range(0.01..=10_000.0))
-                            .changed();
-                        ui.label("Y");
-                        changed |= ui
-                            .add(DragValue::new(&mut size[1]).speed(0.05).range(0.01..=10_000.0))
-                            .changed();
-                        ui.label("Z");
-                        changed |= ui
-                            .add(DragValue::new(&mut size[2]).speed(0.05).range(0.01..=10_000.0))
-                            .changed();
-
-                        if changed {
-                            *signal = Signal::UpdateProceduralCuboid(*entity, size);
-                        }
-                    });
-
-                    ui.separator();
-                }
-
-                ui.label(RichText::new("Textures").strong());
-
-                for material in self.model().materials.iter() {
-                    let material_name = material.name.clone();
-                    let mut tint_rgb = [material.tint[0], material.tint[1], material.tint[2]];
-                    let mut wrap_mode = material.wrap_mode;
-                    let mut uv_tiling = material.uv_tiling;
-                    let texture_label = material
-                        .texture_tag
-                        .clone()
-                        .and_then(|tag| if is_probably_texture_uri(&tag) { Some(tag) } else { None })
-                        .unwrap_or_else(|| "(embedded)".to_string());
-
-                    ui.horizontal(|ui| {
-                        ui.label(RichText::new(&material_name).strong());
-
-                        let (slot_rect, slot_resp) = ui.allocate_exact_size(
-                            egui::vec2(160.0, 22.0),
-                            egui::Sense::click(),
-                        );
-
-                        let slot_fill = if slot_resp.hovered() {
-                            ui.visuals().widgets.hovered.bg_fill
-                        } else {
-                            ui.visuals().widgets.inactive.bg_fill
-                        };
-                        ui.painter().rect_filled(slot_rect, 3.0, slot_fill);
-                        ui.painter().rect_stroke(
-                            slot_rect,
-                            3.0,
-                            ui.visuals().widgets.inactive.bg_stroke,
-                            egui::StrokeKind::Inside,
-                        );
-
-                        let mut slot_ui = ui.new_child(
-                            UiBuilder::new()
-                                .layout(egui::Layout::left_to_right(egui::Align::Center))
-                                .max_rect(slot_rect),
-                        );
-                        slot_ui.add_space(4.0);
-                        slot_ui.label(
-                            RichText::new(texture_label)
-                                .small()
-                                .color(slot_ui.visuals().weak_text_color()),
-                        );
-
-                        if ui.button("Remove").clicked() {
-                            *signal = Signal::ClearMaterialTexture(*entity, material_name.clone());
-                        }
-
-                        let mut wrap_changed = false;
-                        egui::ComboBox::from_id_salt(format!(
-                            "mesh_renderer_wrap_{:?}_{}",
-                            entity,
-                            material_name
-                        ))
-                        .selected_text(match wrap_mode {
-                            TextureWrapMode::Repeat => "Repeat",
-                            TextureWrapMode::Clamp => "Clamp",
-                        })
-                        .show_ui(ui, |ui| {
-                            wrap_changed |= ui
-                                .selectable_value(
-                                &mut wrap_mode,
-                                TextureWrapMode::Repeat,
-                                "Repeat",
-                                )
-                                .changed();
-                            wrap_changed |= ui
-                                .selectable_value(
-                                &mut wrap_mode,
-                                TextureWrapMode::Clamp,
-                                "Clamp",
-                                )
-                                .changed();
-                        });
-
-                        if wrap_changed {
-                            *signal = Signal::SetMaterialWrapMode(
-                                *entity,
-                                material_name.clone(),
-                                wrap_mode,
-                            );
-                        }
-
-                        if matches!(wrap_mode, TextureWrapMode::Repeat) {
-                            ui.label("Repeat");
-                            let mut tiling_changed = ui
-                                .add(DragValue::new(&mut uv_tiling[0]).speed(0.05).range(0.01..=10_000.0))
-                                .changed();
-                            ui.label("x");
-                            tiling_changed |= ui
-                                .add(DragValue::new(&mut uv_tiling[1]).speed(0.05).range(0.01..=10_000.0))
-                                .changed();
-
-                            if tiling_changed {
-                                *signal = Signal::SetMaterialUvTiling(
-                                    *entity,
-                                    material_name.clone(),
-                                    uv_tiling,
-                                );
-                            }
-                        }
-
-                        let colour_changed =
-                            egui::color_picker::color_edit_button_rgb(ui, &mut tint_rgb)
-                                .changed();
-                        if colour_changed {
-                            *signal = Signal::SetMaterialTint(
-                                *entity,
-                                material_name.clone(),
-                                [tint_rgb[0], tint_rgb[1], tint_rgb[2], 1.0],
-                            );
-                        }
-
-                        let pointer_released = ui.input(|i| i.pointer.any_released());
-                        if pointer_released && slot_resp.hovered() {
-                            if let Some(asset) = cfg.dragged_asset.clone() {
-                                if let Some(uri) = asset.path.as_uri() {
-                                    if is_probably_texture_uri(uri) {
-                                        *signal = Signal::SetMaterialTexture(
-                                            *entity,
-                                            material_name.clone(),
-                                            uri.to_string(),
-                                            wrap_mode,
-                                        );
-                                        cfg.dragged_asset = None;
-                                    }
-                                }
-                            }
-                        }
-                    });
-                }
-            }
-        });
-
-        ui.separator();
-    }
-}
-
-impl InspectableComponent for Light {
-    fn inspect(
-        &mut self,
-        entity: &mut Entity,
-        cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        undo_stack: &mut Vec<UndoableAction>,
-        _signal: &mut Signal,
-        _label: &mut String,
-    ) {
-        CollapsingHeader::new("Light").default_open(true).show(ui, |ui| {
-            ui.horizontal(|ui| {
-                ComboBox::new("light_type", "Light Type")
-                    .selected_text(self.light_component.light_type.to_string())
-                    .show_ui(ui, |ui| {
-                        ui.selectable_value(
-                            &mut self.light_component.light_type,
-                            LightType::Directional,
-                            "Directional",
-                        );
-                        ui.selectable_value(&mut self.light_component.light_type, LightType::Point, "Point");
-                        ui.selectable_value(&mut self.light_component.light_type, LightType::Spot, "Spot");
-                    });
-            });
-            
-            ui.separator();
-            
-            inspect_light_transform(
-                &mut self.transform,
-                entity,
-                cfg,
-                ui,
-                undo_stack,
-                &self.light_component.light_type,
-            );
-            
-            let is_point = matches!(self.light_component.light_type, LightType::Point);
-            let is_spot = matches!(self.light_component.light_type, LightType::Spot);
-
-            ui.separator();
-            let mut colour = self.light_component.colour.clone().as_vec3().to_array();
-            ui.horizontal(|ui| {
-                ui.label("Colour");
-                egui::color_picker::color_edit_button_rgb(ui, &mut colour)
-            });
-            self.light_component.colour = Vec3::from_array(colour).as_dvec3();
-
-            ui.separator();
-            ui.horizontal(|ui| {
-                ui.label("Intensity");
-                ui.add(egui::Slider::new(&mut self.light_component.intensity, 0.0..=10.0));
-            });
-
-            ui.separator();
-            ui.horizontal(|ui| {
-                ui.checkbox(&mut self.light_component.enabled, "Enabled");
-                ui.checkbox(&mut self.light_component.visible, "Visible");
-            });
-
-            if is_spot || is_point {
-                ui.separator();
-                ui.horizontal(|ui| {
-                    ComboBox::new("attenuation_range", "Range")
-                        .selected_text(format!("Range {}", self.light_component.attenuation.range))
-                        .show_ui(ui, |ui| {
-                            for (preset, label) in ATTENUATION_PRESETS {
-                                ui.selectable_value(&mut self.light_component.attenuation, *preset, *label);
-                            }
-                        });
-                });
-            }
-
-            if is_spot {
-                ui.separator();
-                ui.horizontal(|ui| {
-                    ui.add(
-                        egui::Slider::new(&mut self.light_component.cutoff_angle, 1.0..=89.0)
-                            .text("Inner")
-                            .suffix("°")
-                            .step_by(0.1),
-                    );
-                });
-
-                ui.horizontal(|ui| {
-                    ui.add(
-                        egui::Slider::new(&mut self.light_component.outer_cutoff_angle, 1.0..=90.0)
-                            .text("Outer")
-                            .suffix("°")
-                            .step_by(0.1),
-                    );
-                });
-
-                if self.light_component.outer_cutoff_angle <= self.light_component.cutoff_angle {
-                    self.light_component.outer_cutoff_angle = self.light_component.cutoff_angle + 1.0;
-                }
-
-                let cone_softness = self.light_component.outer_cutoff_angle - self.light_component.cutoff_angle;
-                ui.label(format!("Soft edge: {:.1}°", cone_softness));
-            }
-
-            ui.separator();
-
-            ui.label("Shadows");
-            ui.checkbox(&mut self.light_component.cast_shadows, "Cast Shadows");
-            ui.horizontal(|ui| {
-                ui.label("Depth");
-                ui.add(egui::DragValue::new(&mut self.light_component.depth.start).speed(0.1));
-                ui.label("..");
-                ui.add(egui::DragValue::new(&mut self.light_component.depth.end).speed(0.1));
-            });
-
-            if self.light_component.depth.end < self.light_component.depth.start {
-                self.light_component.depth.end = self.light_component.depth.start;
-            }
-
-            ui.separator();
-        });
-    }
-}
-
-impl InspectableComponent for Camera3D {
-    fn inspect(
-        &mut self,
-        entity: &mut Entity,
-        cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        undo_stack: &mut Vec<UndoableAction>,
-        signal: &mut Signal,
-        label: &mut String,
-    ) {
-        self.transform
-            .inspect(entity, cfg, ui, undo_stack, signal, label);
-
-        ui.vertical(|ui| {
-            CollapsingHeader::new("Camera Settings")
-                .default_open(true)
-                .show(ui, |ui| {
-                    ui.horizontal(|ui| {
-                        ui.label("Type");
-                        ComboBox::from_id_salt("camera_type")
-                            .selected_text(format!("{:?}", self.camera_type))
-                            .show_ui(ui, |ui| {
-                                ui.selectable_value(
-                                    &mut self.camera_type,
-                                    CameraType::Normal,
-                                    "Normal",
-                                );
-                                ui.selectable_value(
-                                    &mut self.camera_type,
-                                    CameraType::Debug,
-                                    "Debug",
-                                );
-                                ui.selectable_value(
-                                    &mut self.camera_type,
-                                    CameraType::Player,
-                                    "Player",
-                                );
-                            });
-                    });
-
-                    ui.horizontal(|ui| {
-                        ui.label("FOV");
-                        ui.add(egui::Slider::new(&mut self.fov, 1.0..=179.0).suffix("°"));
-                    });
-
-                    ui.horizontal(|ui| {
-                        ui.label("Near Plane");
-                        ui.add(
-                            egui::DragValue::new(&mut self.near)
-                                .speed(0.1)
-                                .range(0.01..=1000.0),
-                        );
-                    });
-
-                    ui.horizontal(|ui| {
-                        ui.label("Far Plane");
-                        ui.add(
-                            egui::DragValue::new(&mut self.far)
-                                .speed(1.0)
-                                .range(0.1..=10000.0),
-                        );
-                    });
-
-                    ui.separator();
-
-                    ui.horizontal(|ui| {
-                        ui.label("Speed");
-                        ui.add(egui::DragValue::new(&mut self.speed).speed(0.1));
-                    });
-
-                    ui.horizontal(|ui| {
-                        ui.label("Sensitivity");
-                        ui.add(egui::DragValue::new(&mut self.sensitivity).speed(0.01));
-                    });
-
-                    ui.separator();
-
-                    ui.checkbox(&mut self.starting_camera, "Starting Camera");
-                });
-        });
-        ui.separator();
-    }
-}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/resource.rs b/crates/eucalyptus-editor/src/editor/resource.rs
index c6435d1..9649336 100644
--- a/crates/eucalyptus-editor/src/editor/resource.rs
+++ b/crates/eucalyptus-editor/src/editor/resource.rs
@@ -38,7 +38,7 @@ impl<'a> EditorTabViewer<'a> {
 
                         let mut is_starting = comp.starting_camera;
                         let is_starting_label = if comp.camera_type == CameraType::Debug {
-                            is_starting = false;
+                            is_starting = true;
                             "Cannot set a Debug camera as starting"
                         } else if is_starting {
                             "Already set as starting"
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index e170cab..74c928f 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -17,7 +17,7 @@ use eucalyptus_core::physics::collider::ColliderShapeKey;
 use eucalyptus_core::physics::collider::shader::{ColliderInstanceRaw, create_wireframe_geometry};
 use eucalyptus_core::properties::CustomProperties;
 use eucalyptus_core::states::{Label, WorldLoadingStatus};
-use glam::{DMat4, Mat4};
+use glam::{Mat4};
 use log;
 use parking_lot::Mutex;
 use std::collections::HashMap;
@@ -506,7 +506,9 @@ impl Scene for Editor {
         let environment_bind_group = &sky.bind_group;
 
         let globals = self.shader_globals.as_ref().expect("Shader globals not initialised");
-        if self.scene_globals_bind_group.is_none() {
+        if let Some(scene_globals) = &mut self.scene_globals_bind_group {
+            scene_globals.update(&graphics, &globals.buffer, camera.buffer());
+        } else {
             self.scene_globals_bind_group = Some(dropbear_engine::bind_groups::SceneGlobalsBindGroup::new(
                 &graphics,
                 &globals.buffer,
@@ -724,19 +726,11 @@ impl Scene for Editor {
                     > = HashMap::new();
 
                     let mut q = self.world.query::<(&EntityTransform, &ColliderGroup)>();
-                    let mut entity_count = 0;
-                    let mut collider_count = 0;
                     for (entity_transform, group) in q.iter() {
-                        entity_count += 1;
                         for collider in &group.colliders {
-                            collider_count += 1;
                             let world_tf = entity_transform.sync();
 
-                            let entity_matrix = DMat4::from_rotation_translation(
-                                world_tf.rotation,
-                                world_tf.position,
-                            )
-                            .as_mat4();
+                            let entity_matrix = world_tf.matrix().as_mat4();
 
                             let offset_transform = Transform::new()
                                 .with_offset(collider.translation, collider.rotation);
diff --git a/crates/eucalyptus-editor/src/lib.rs b/crates/eucalyptus-editor/src/lib.rs
index 709e5ff..fd6333e 100644
--- a/crates/eucalyptus-editor/src/lib.rs
+++ b/crates/eucalyptus-editor/src/lib.rs
@@ -4,7 +4,6 @@ pub mod camera;
 pub mod debug;
 pub mod editor;
 pub mod menu;
-pub mod physics;
 pub mod plugin;
 pub mod process;
 pub mod signal;
diff --git a/crates/eucalyptus-editor/src/physics.rs b/crates/eucalyptus-editor/src/physics.rs
deleted file mode 100644
index 099bf85..0000000
--- a/crates/eucalyptus-editor/src/physics.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-// pub mod rigidbody;
-// pub mod collider;
-// pub mod kcc;
diff --git a/crates/eucalyptus-editor/src/physics/collider.rs b/crates/eucalyptus-editor/src/physics/collider.rs
deleted file mode 100644
index 215d4ee..0000000
--- a/crates/eucalyptus-editor/src/physics/collider.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-use egui::{ComboBox, Ui};
-use hecs::Entity;
-use eucalyptus_core::physics::collider::{Collider, ColliderShape};
-use eucalyptus_core::states::Label;
-use crate::editor::{Signal, StaticallyKept, UndoableAction};
-
-impl InspectableComponent for Collider {
-    fn inspect(
-        &mut self,
-        _entity: &mut Entity,
-        _cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        _undo_stack: &mut Vec<UndoableAction>,
-        _signal: &mut Signal,
-        label: &mut String
-    ) {
-        
-    }
-}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/physics/kcc.rs b/crates/eucalyptus-editor/src/physics/kcc.rs
deleted file mode 100644
index 0601e77..0000000
--- a/crates/eucalyptus-editor/src/physics/kcc.rs
+++ /dev/null
@@ -1,140 +0,0 @@
-use egui::{ComboBox, DragValue, Ui};
-use hecs::Entity;
-use eucalyptus_core::physics::kcc::KCC;
-use eucalyptus_core::rapier3d::control::{CharacterAutostep, CharacterLength};
-use eucalyptus_core::rapier3d::na::{UnitVector3, Vector3};
-use eucalyptus_core::states::Label;
-use crate::editor::component::InspectableComponent;
-use crate::editor::{Signal, StaticallyKept, UndoableAction};
-
-impl InspectableComponent for KCC {
-    fn inspect(
-        &mut self,
-        _entity: &mut Entity,
-        _cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        _undo_stack: &mut Vec<UndoableAction>,
-        _signal: &mut Signal,
-        label: &mut String
-    ) {
-        self.entity = Label::new(label.clone());
-
-        fn edit_character_length(ui: &mut Ui, id_salt: impl std::hash::Hash, value: &mut CharacterLength, text: &str) {
-            ui.horizontal(|ui| {
-                ui.label(text);
-
-                let (mut kind, mut v) = match *value {
-                    CharacterLength::Absolute(x) => (0, x),
-                    CharacterLength::Relative(x) => (1, x),
-                };
-
-                ComboBox::from_id_salt(id_salt)
-                    .selected_text(match kind {
-                        0 => "Absolute",
-                        _ => "Relative",
-                    })
-                    .show_ui(ui, |ui| {
-                        ui.selectable_value(&mut kind, 0, "Absolute");
-                        ui.selectable_value(&mut kind, 1, "Relative");
-                    });
-
-                ui.add(DragValue::new(&mut v).speed(0.01));
-
-                *value = if kind == 0 {
-                    CharacterLength::Absolute(v)
-                } else {
-                    CharacterLength::Relative(v)
-                };
-            });
-        }
-
-        ui.vertical(|ui| {
-            ui.label("Up Vector:");
-            let up = self.controller.up;
-            let mut up_v = Vector3::new(up.x, up.y, up.z);
-
-            ui.horizontal(|ui| {
-                ui.label("X:");
-                ui.add(DragValue::new(&mut up_v.x).speed(0.01));
-                ui.label("Y:");
-                ui.add(DragValue::new(&mut up_v.y).speed(0.01));
-                ui.label("Z:");
-                ui.add(DragValue::new(&mut up_v.z).speed(0.01));
-            });
-
-            if up_v.norm_squared() > 0.0 {
-                self.controller.up = UnitVector3::new_normalize(up_v).into();
-            }
-
-            ui.add_space(8.0);
-
-            edit_character_length(ui, "kcc_offset", &mut self.controller.offset, "Offset:");
-
-            ui.add_space(8.0);
-            ui.separator();
-
-            ui.checkbox(&mut self.controller.slide, "Slide against floor?");
-
-            ui.label("Slope Angles (degrees):");
-            ui.horizontal(|ui| {
-                ui.label("Max climb:");
-                let mut deg = self.controller.max_slope_climb_angle.to_degrees();
-                if ui.add(DragValue::new(&mut deg).speed(1.0)).changed() {
-                    self.controller.max_slope_climb_angle = deg.to_radians();
-                }
-            });
-            ui.horizontal(|ui| {
-                ui.label("Min slide:");
-                let mut deg = self.controller.min_slope_slide_angle.to_degrees();
-                if ui.add(DragValue::new(&mut deg).speed(1.0)).changed() {
-                    self.controller.min_slope_slide_angle = deg.to_radians();
-                }
-            });
-
-            ui.add_space(8.0);
-
-            ui.horizontal(|ui| {
-                ui.label("Normal nudge:");
-                ui.add(
-                    egui::Slider::new(&mut self.controller.normal_nudge_factor, 0.001..=0.5)
-                        .logarithmic(true)
-                        .smallest_positive(0.001)
-                        .largest_finite(0.5)
-                        .suffix(" m")
-                );
-            });
-
-            ui.add_space(8.0);
-            ui.separator();
-
-            let mut enable_snap = self.controller.snap_to_ground.is_some();
-            ui.checkbox(&mut enable_snap, "Snap to ground");
-            if enable_snap && self.controller.snap_to_ground.is_none() {
-                self.controller.snap_to_ground = Some(CharacterLength::Absolute(0.2));
-            } else if !enable_snap {
-                self.controller.snap_to_ground = None;
-            }
-
-            if let Some(ref mut snap) = self.controller.snap_to_ground {
-                edit_character_length(ui, "kcc_snap_to_ground", snap, "Snap distance:");
-            }
-
-            ui.add_space(8.0);
-            ui.separator();
-
-            let mut enable_autostep = self.controller.autostep.is_some();
-            ui.checkbox(&mut enable_autostep, "Enable autostep");
-            if enable_autostep && self.controller.autostep.is_none() {
-                self.controller.autostep = Some(CharacterAutostep::default());
-            } else if !enable_autostep {
-                self.controller.autostep = None;
-            }
-
-            if let Some(step) = &mut self.controller.autostep {
-                edit_character_length(ui, "kcc_autostep_max_height", &mut step.max_height, "Max height:");
-                edit_character_length(ui, "kcc_autostep_min_width", &mut step.min_width, "Min width:");
-                ui.checkbox(&mut step.include_dynamic_bodies, "Include dynamic bodies");
-            }
-        });
-    }
-}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/physics/rigidbody.rs b/crates/eucalyptus-editor/src/physics/rigidbody.rs
deleted file mode 100644
index 6204385..0000000
--- a/crates/eucalyptus-editor/src/physics/rigidbody.rs
+++ /dev/null
@@ -1,106 +0,0 @@
-use egui::{ComboBox, Ui};
-use hecs::Entity;
-use eucalyptus_core::physics::rigidbody::{RigidBody, RigidBodyMode};
-use eucalyptus_core::states::Label;
-use crate::editor::component::InspectableComponent;
-use crate::editor::{Signal, StaticallyKept, UndoableAction};
-
-impl InspectableComponent for RigidBody {
-    fn inspect(
-        &mut self,
-        _entity: &mut Entity,
-        _cfg: &mut StaticallyKept,
-        ui: &mut Ui,
-        _undo_stack: &mut Vec<UndoableAction>,
-        _signal: &mut Signal,
-        label: &mut String
-    ) {
-        ui.vertical(|ui| {
-            self.entity = Label::new(label.clone());
-
-            let mut selected = self.mode.clone();
-            ComboBox::from_id_salt("rigidbody")
-                .selected_text(format!("{:?}", self.mode))
-                .show_ui(ui, |ui| {
-                    ui.selectable_value(&mut selected, RigidBodyMode::Dynamic, "Dynamic");
-                    ui.selectable_value(&mut selected, RigidBodyMode::Fixed, "Fixed");
-                    ui.selectable_value(&mut selected, RigidBodyMode::KinematicPosition, "Kinematic Position");
-                    ui.selectable_value(&mut selected, RigidBodyMode::KinematicVelocity, "Kinematic Velocity");
-                });
-
-            if selected != self.mode {
-                self.mode = selected;
-            }
-
-            ui.add_space(8.0);
-
-            ui.horizontal(|ui| {
-                ui.label("Gravity Scale:");
-                ui.add(egui::DragValue::new(&mut self.gravity_scale)
-                    .speed(0.1)
-                    .range(0.0..=10.0));
-            });
-
-
-            ui.checkbox(&mut self.can_sleep, "Can Sleep");
-
-            ui.checkbox(&mut self.sleeping, "Initially sleeping?");
-
-            ui.checkbox(&mut self.ccd_enabled, "CCD Enabled");
-
-            ui.add_space(8.0);
-
-            ui.label("Linear Velocity:");
-            ui.horizontal(|ui| {
-                ui.label("X:");
-                ui.add(egui::DragValue::new(&mut self.linvel[0]).speed(0.1));
-                ui.label("Y:");
-                ui.add(egui::DragValue::new(&mut self.linvel[1]).speed(0.1));
-                ui.label("Z:");
-                ui.add(egui::DragValue::new(&mut self.linvel[2]).speed(0.1));
-            });
-
-            ui.label("Angular Velocity:");
-            ui.horizontal(|ui| {
-                ui.label("X:");
-                ui.add(egui::DragValue::new(&mut self.angvel[0]).speed(0.1));
-                ui.label("Y:");
-                ui.add(egui::DragValue::new(&mut self.angvel[1]).speed(0.1));
-                ui.label("Z:");
-                ui.add(egui::DragValue::new(&mut self.angvel[2]).speed(0.1));
-            });
-
-            ui.add_space(8.0);
-
-            ui.horizontal(|ui| {
-                ui.label("Linear Damping:");
-                ui.add(egui::DragValue::new(&mut self.linear_damping)
-                    .speed(0.01)
-                    .range(0.0..=10.0));
-            });
-
-            ui.horizontal(|ui| {
-                ui.label("Angular Damping:");
-                ui.add(egui::DragValue::new(&mut self.angular_damping)
-                    .speed(0.01)
-                    .range(0.0..=10.0))
-            });
-
-            ui.add_space(8.0);
-
-            ui.label("Lock Translation:");
-            ui.horizontal(|ui| {
-                ui.checkbox(&mut self.lock_translation.x, "X");
-                ui.checkbox(&mut self.lock_translation.y, "Y");
-                ui.checkbox(&mut self.lock_translation.z, "Z");
-            });
-
-            ui.label("Lock Rotation:");
-            ui.horizontal(|ui| {
-                ui.checkbox(&mut self.lock_rotation.x, "X");
-                ui.checkbox(&mut self.lock_rotation.y, "Y");
-                ui.checkbox(&mut self.lock_rotation.z, "Z");
-            });
-        });
-    }
-}
\ No newline at end of file
diff --git a/crates/magna-carta/src/lib.rs b/crates/magna-carta/src/lib.rs
index 0a30ca3..5ad17dd 100644
--- a/crates/magna-carta/src/lib.rs
+++ b/crates/magna-carta/src/lib.rs
@@ -8,6 +8,12 @@ use std::fs;
 use std::path::{Path, PathBuf};
 use tree_sitter::{Parser, Query, QueryCursor};
 
+struct AnnotationInfo<'a> {
+    node: tree_sitter::Node<'a>,
+    name: Option<String>,
+    value_args: Option<tree_sitter::Node<'a>>,
+}
+
 /// A group of manifests.
 #[derive(Debug, Clone)]
 pub struct ScriptManifest {
@@ -127,6 +133,16 @@ impl KotlinProcessor {
             return Ok(Some(ManifestItem::new(fqcn, class_name, tags, file_path)));
         }
 
+        if std::env::var("MAGNA_CARTA_DEBUG").is_ok() && source_code.contains("@Runnable") {
+            let class_names = self.collect_class_names(root_node, source_code)?;
+            eprintln!(
+                "magna-carta: @Runnable found but no manifest item for {}. Classes seen: {:?}",
+                file_path.display(),
+                class_names
+            );
+            self.debug_dump_annotations(root_node, source_code, &file_path)?;
+        }
+
         Ok(None)
     }
 
@@ -181,10 +197,10 @@ impl KotlinProcessor {
             (annotation
               (constructor_invocation
                 (user_type
-                  (type_identifier) @annotation_name2)
-                (value_arguments)? @value_args)
-              (#eq? @annotation_name2 "Runnable")))
-          (type_identifier) @class_name2)
+                (type_identifier) @annotation_name2)
+                    (value_arguments)? @value_args)
+                (#eq? @annotation_name2 "Runnable")))
+            (type_identifier) @class_name2)
         "#,
         )?;
 
@@ -253,9 +269,333 @@ impl KotlinProcessor {
             }
         }
 
+        if let Some(result) = self.extract_class_info_fallback(root_node, source)? {
+            return Ok(Some(result));
+        }
+
+        self.extract_class_info_loose(root_node, source)
+    }
+
+    fn extract_class_info_fallback(
+        &self,
+        root_node: tree_sitter::Node,
+        source: &str,
+    ) -> anyhow::Result<Option<(String, Vec<String>)>> {
+        let mut stack = vec![root_node];
+
+        while let Some(node) = stack.pop() {
+            if node.kind() == "class_declaration" {
+                if let Some(class_name) = self.class_name_from_node(node, source)? {
+                    if let Some(modifiers) = self.first_child_by_kind(node, "modifiers") {
+                        let mut mod_cursor = modifiers.walk();
+                        for modifier in modifiers.children(&mut mod_cursor) {
+                            if modifier.kind() != "annotation" {
+                                continue;
+                            }
+
+                            let (annotation_name, value_args) =
+                                self.annotation_name_and_args(modifier, source)?;
+                            if annotation_name.as_deref() == Some("Runnable") {
+                                let tags = if let Some(args) = value_args {
+                                    self.extract_tags_from_value_args(args, source)?
+                                } else {
+                                    let raw = modifier.utf8_text(source.as_bytes())?;
+                                    self.extract_tags_from_raw_annotation(raw)
+                                };
+
+                                return Ok(Some((class_name, tags)));
+                            }
+                        }
+                    }
+                }
+            }
+
+            let mut cursor = node.walk();
+            for child in node.children(&mut cursor) {
+                stack.push(child);
+            }
+        }
+
+        Ok(None)
+    }
+
+    fn class_name_from_node(
+        &self,
+        class_node: tree_sitter::Node,
+        source: &str,
+    ) -> anyhow::Result<Option<String>> {
+        let mut cursor = class_node.walk();
+        for child in class_node.children(&mut cursor) {
+            if child.kind() == "type_identifier" {
+                let text = child.utf8_text(source.as_bytes())?;
+                return Ok(Some(text.to_string()));
+            }
+        }
+
+        Ok(None)
+    }
+
+    fn annotation_name_and_args<'a>(
+        &self,
+        annotation_node: tree_sitter::Node<'a>,
+        source: &str,
+    ) -> anyhow::Result<(Option<String>, Option<tree_sitter::Node<'a>>)> {
+        let mut name: Option<String> = None;
+        let mut value_args: Option<tree_sitter::Node> = None;
+        let mut stack = vec![annotation_node];
+
+        while let Some(node) = stack.pop() {
+            if node.kind() == "value_arguments" && value_args.is_none() {
+                value_args = Some(node);
+            }
+
+            if node.kind() == "type_identifier" {
+                let text = node.utf8_text(source.as_bytes())?;
+                name = Some(text.to_string());
+            }
+
+            let mut cursor = node.walk();
+            for child in node.children(&mut cursor) {
+                stack.push(child);
+            }
+        }
+
+        Ok((name, value_args))
+    }
+
+    fn first_child_by_kind<'a>(
+        &self,
+        node: tree_sitter::Node<'a>,
+        kind: &str,
+    ) -> Option<tree_sitter::Node<'a>> {
+        let mut cursor = node.walk();
+        for child in node.children(&mut cursor) {
+            if child.kind() == kind {
+                return Some(child);
+            }
+        }
+
+        None
+    }
+
+    fn extract_class_info_loose<'a>(
+        &self,
+        root_node: tree_sitter::Node<'a>,
+        source: &str,
+    ) -> anyhow::Result<Option<(String, Vec<String>)>> {
+        let annotations = self.collect_annotation_nodes(root_node, source)?;
+        let classes = self.collect_class_nodes(root_node);
+
+        for annotation in annotations {
+            if annotation.name.as_deref() != Some("Runnable") {
+                continue;
+            }
+
+            let Some((class_node, _)) = classes
+                .iter()
+                .filter(|(node, _)| node.start_byte() > annotation.node.end_byte())
+                .min_by_key(|(node, _)| node.start_byte().saturating_sub(annotation.node.end_byte()))
+            else {
+                continue;
+            };
+
+            if let Some(class_name) = self.class_name_from_node(*class_node, source)? {
+                let tags = if let Some(args) = annotation.value_args {
+                    self.extract_tags_from_value_args(args, source)?
+                } else {
+                    let raw = annotation.node.utf8_text(source.as_bytes())?;
+                    let mut tags = self.extract_tags_from_raw_annotation(raw);
+                    if tags.is_empty() {
+                        tags = self.extract_tags_from_source_slice(
+                            source,
+                            annotation.node.start_byte(),
+                            class_node.start_byte(),
+                        );
+                    }
+                    tags
+                };
+
+                return Ok(Some((class_name, tags)));
+            }
+        }
+
         Ok(None)
     }
 
+    fn collect_class_nodes<'a>(
+        &self,
+        root_node: tree_sitter::Node<'a>,
+    ) -> Vec<(tree_sitter::Node<'a>, usize)> {
+        let mut nodes = Vec::new();
+        let mut stack = vec![root_node];
+
+        while let Some(node) = stack.pop() {
+            if node.kind() == "class_declaration" {
+                nodes.push((node, node.start_byte()));
+            }
+
+            let mut cursor = node.walk();
+            for child in node.children(&mut cursor) {
+                stack.push(child);
+            }
+        }
+
+        nodes
+    }
+
+    fn collect_annotation_nodes<'a>(
+        &self,
+        root_node: tree_sitter::Node<'a>,
+        source: &str,
+    ) -> anyhow::Result<Vec<AnnotationInfo<'a>>> {
+        let mut nodes = Vec::new();
+        let mut stack = vec![root_node];
+
+        while let Some(node) = stack.pop() {
+            if node.kind() == "annotation" {
+                let (name, value_args) = self.annotation_name_and_args(node, source)?;
+                nodes.push(AnnotationInfo {
+                    node,
+                    name,
+                    value_args,
+                });
+            }
+
+            let mut cursor = node.walk();
+            for child in node.children(&mut cursor) {
+                stack.push(child);
+            }
+        }
+
+        Ok(nodes)
+    }
+
+    fn extract_tags_from_raw_annotation(&self, raw: &str) -> Vec<String> {
+        let mut tags = Vec::new();
+        let mut current = String::new();
+        let mut in_string = false;
+        let mut quote_char = '\0';
+
+        for ch in raw.chars() {
+            if !in_string {
+                if ch == '"' || ch == '\'' {
+                    in_string = true;
+                    quote_char = ch;
+                    current.clear();
+                }
+                continue;
+            }
+
+            if ch == quote_char {
+                if !current.is_empty() {
+                    tags.push(current.clone());
+                }
+                in_string = false;
+                quote_char = '\0';
+                current.clear();
+                continue;
+            }
+
+            current.push(ch);
+        }
+
+        tags
+    }
+
+    fn extract_tags_from_source_slice(
+        &self,
+        source: &str,
+        start: usize,
+        end: usize,
+    ) -> Vec<String> {
+        let Some(slice) = source.get(start..end) else {
+            return Vec::new();
+        };
+
+        self.extract_tags_from_raw_annotation(slice)
+    }
+
+    fn collect_class_names(
+        &self,
+        root_node: tree_sitter::Node,
+        source: &str,
+    ) -> anyhow::Result<Vec<String>> {
+        let mut names = Vec::new();
+        let mut stack = vec![root_node];
+
+        while let Some(node) = stack.pop() {
+            if node.kind() == "class_declaration" {
+                if let Some(name) = self.class_name_from_node(node, source)? {
+                    names.push(name);
+                }
+            }
+
+            let mut cursor = node.walk();
+            for child in node.children(&mut cursor) {
+                stack.push(child);
+            }
+        }
+
+        Ok(names)
+    }
+
+    fn debug_dump_annotations(
+        &self,
+        root_node: tree_sitter::Node,
+        source: &str,
+        file_path: &Path,
+    ) -> anyhow::Result<()> {
+        let mut stack = vec![root_node];
+
+        while let Some(node) = stack.pop() {
+            if node.kind() == "class_declaration" {
+                let class_name = self
+                    .class_name_from_node(node, source)?
+                    .unwrap_or_else(|| "<unknown>".to_string());
+                let annotations = self.collect_annotation_debug(node, source)?;
+                eprintln!(
+                    "magna-carta: {} class {} annotations: {:?}",
+                    file_path.display(),
+                    class_name,
+                    annotations
+                );
+            }
+
+            let mut cursor = node.walk();
+            for child in node.children(&mut cursor) {
+                stack.push(child);
+            }
+        }
+
+        Ok(())
+    }
+
+    fn collect_annotation_debug(
+        &self,
+        class_node: tree_sitter::Node,
+        source: &str,
+    ) -> anyhow::Result<Vec<String>> {
+        let mut results = Vec::new();
+
+        let Some(modifiers) = self.first_child_by_kind(class_node, "modifiers") else {
+            return Ok(results);
+        };
+
+        let mut mod_cursor = modifiers.walk();
+        for modifier in modifiers.children(&mut mod_cursor) {
+            if modifier.kind() != "annotation" {
+                continue;
+            }
+
+            let raw = modifier.utf8_text(source.as_bytes())?;
+            let (annotation_name, _) = self.annotation_name_and_args(modifier, source)?;
+            let name = annotation_name.unwrap_or_else(|| "<unknown>".to_string());
+            results.push(format!("{} => {}", name, raw.trim()));
+        }
+
+        Ok(results)
+    }
+
     fn extract_tags_from_value_args(
         &self,
         value_args_node: tree_sitter::Node,
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index a4a0635..57d2b0c 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -25,10 +25,10 @@ use eucalyptus_core::rapier3d::prelude::QueryFilter;
 use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER, SceneLoadResult};
 use eucalyptus_core::states::SCENES;
 use eucalyptus_core::states::{Label, PROJECT};
-use glam::{DMat4, DQuat, DVec3, Mat4, Quat, Vec2, vec2};
+use glam::{DQuat, DVec3, Mat4, Quat, Vec2};
 use hecs::Entity;
-use kino_ui::widgets::rect::Rectangle;
-use kino_ui::widgets::{Border, Fill};
+// use kino_ui::widgets::rect::Rectangle;
+// use kino_ui::widgets::{Border, Fill};
 use std::collections::HashMap;
 use wgpu::util::DeviceExt;
 use winit::event::WindowEvent;
@@ -55,7 +55,7 @@ impl Scene for PlayMode {
         }
     }
 
-    fn physics_update(&muprintlnt self, dt: f32, _graphics: Arc<SharedGraphicsContext>) {
+    fn physics_update(&mut self, dt: f32, _graphics: Arc<SharedGraphicsContext>) {
         if self.scripts_ready {
             let _ = self
                 .script_manager
@@ -816,7 +816,9 @@ impl Scene for PlayMode {
         let environment_bind_group = &sky.bind_group;
         
         let globals = self.shader_globals.as_ref().expect("Shader globals not initialised");
-        if self.scene_globals_bind_group.is_none() {
+        if let Some(scene_globals) = &mut self.scene_globals_bind_group {
+            scene_globals.update(&graphics, &globals.buffer, camera.buffer());
+        } else {
             self.scene_globals_bind_group = Some(dropbear_engine::bind_groups::SceneGlobalsBindGroup::new(
                 &graphics,
                 &globals.buffer,
@@ -955,37 +957,6 @@ impl Scene for PlayMode {
             }
         }
 
-        // collider pipeline
-        {
-            let show_hitboxes = self
-                .current_scene
-                .as_ref()
-                .and_then(|scene_name| {
-                    let scenes = SCENES.read();
-                    scenes
-                        .iter()
-                        .find(|scene| &scene.scene_name == scene_name)
-                        .map(|scene| scene.settings.show_hitboxes)
-                })
-                .unwrap_or(false);
-
-            // UI_CONTEXT.with(|v| {
-            //     let commands = graphics.yakui_renderer.lock().paint(
-            //         &mut v.borrow().yakui_state.lock(),
-            //         &graphics.device,
-            //         &graphics.queue,
-            //         SurfaceInfo {
-            //             format: Texture::TEXTURE_FORMAT,
-            //             sample_count: 1,
-            //             color_attachment: &graphics.viewport_texture.view,
-            //             resolve_target: None,
-            //         }
-            //     );
-            //
-            //     graphics.queue.submit([commands]);
-            // });
-        }
-
         if let Some(sky) = &self.sky_pipeline {
             let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                 label: Some("sky render pass"),
@@ -1067,11 +1038,7 @@ impl Scene for PlayMode {
                         for collider in &group.colliders {
                             let world_tf = entity_transform.sync();
 
-                            let entity_matrix = DMat4::from_rotation_translation(
-                                world_tf.rotation,
-                                world_tf.position,
-                            )
-                            .as_mat4();
+                            let entity_matrix = world_tf.matrix().as_mat4();
 
                             let offset_transform = Transform::new()
                                 .with_offset(collider.translation, collider.rotation);
@@ -1161,6 +1128,8 @@ impl Scene for PlayMode {
         }
     }
 
+    fn exit(&mut self, _event_loop: &ActiveEventLoop) {}
+
     fn handle_event(&mut self, event: &WindowEvent) {
         // UI_CONTEXT.with(|yakui_cell| {
         //     let yak = yakui_cell.borrow();
@@ -1175,8 +1144,6 @@ impl Scene for PlayMode {
         }
     }
 
-    fn exit(&mut self, _event_loop: &ActiveEventLoop) {}
-
     fn run_command(&mut self) -> SceneCommand {
         std::mem::replace(&mut self.scene_command, SceneCommand::None)
     }
diff --git a/include/dropbear.h b/include/dropbear.h
index 8dbc3f7..b6d4173 100644
--- a/include/dropbear.h
+++ b/include/dropbear.h
@@ -203,6 +203,12 @@ typedef struct CharacterCollisionArray {
     IndexNativeArray collisions;
 } CharacterCollisionArray;
 
+typedef struct CharacterMovementResult {
+    NVector3 translation;
+    bool grounded;
+    bool is_sliding_down_slope;
+} CharacterMovementResult;
+
 typedef void* CommandBufferPtr;
 
 typedef struct u64Array {
@@ -531,6 +537,7 @@ int32_t dropbear_input_print_input_state(InputStatePtr input);
 int32_t dropbear_input_set_cursor_hidden(CommandBufferPtr command_buffer, InputStatePtr input, bool hidden);
 int32_t dropbear_input_set_cursor_locked(CommandBufferPtr command_buffer, InputStatePtr input, bool locked);
 int32_t dropbear_kcc_get_hit(WorldPtr world, uint64_t entity, CharacterCollisionArray* out0);
+int32_t dropbear_kcc_get_movement_result(WorldPtr world, uint64_t entity, CharacterMovementResult* out0, bool* out0_present);
 int32_t dropbear_kcc_kcc_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0);
 int32_t dropbear_kcc_move_character(WorldPtr world, PhysicsStatePtr physics_state, uint64_t entity, const NVector3* translation, double delta_time);
 int32_t dropbear_kcc_set_rotation(WorldPtr world, PhysicsStatePtr physics_state, uint64_t entity, const NQuaternion* rotation);
diff --git a/scripting/commonMain/kotlin/com/dropbear/Runnable.kt b/scripting/commonMain/kotlin/com/dropbear/Runnable.kt
index cdad1b1..f95f101 100644
--- a/scripting/commonMain/kotlin/com/dropbear/Runnable.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/Runnable.kt
@@ -14,4 +14,4 @@ package com.dropbear
  */
 @Target(AnnotationTarget.CLASS)
 @Retention(AnnotationRetention.SOURCE)
-annotation class Runnable(val tags: Array<String> = [])
+annotation class Runnable(vararg val tags: String)
diff --git a/scripting/commonMain/kotlin/com/dropbear/physics/CharacterMovementResult.kt b/scripting/commonMain/kotlin/com/dropbear/physics/CharacterMovementResult.kt
new file mode 100644
index 0000000..3c3f40b
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/physics/CharacterMovementResult.kt
@@ -0,0 +1,9 @@
+package com.dropbear.physics
+
+import com.dropbear.math.Vector3d
+
+data class CharacterMovementResult(
+    val translation: Vector3d,
+    val grounded: Boolean,
+    val isSlidingDownSlope: Boolean,
+)
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt b/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt
index bfc14f1..612f408 100644
--- a/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/physics/KinematicCharacterController.kt
@@ -11,6 +11,9 @@ import kotlin.math.cos
 class KinematicCharacterController(
     val entity: EntityId,
 ) : Component(entity, "KCC") {
+    val movementResult: CharacterMovementResult?
+        get() = getMovementResult()
+
     /**
      * Moves the character by a translation (displacement) for this tick.
      *
@@ -53,14 +56,9 @@ class KinematicCharacterController(
      * Returns true if the character is currently in contact with a "floor-like" surface.
      *
      * This uses the cached character-collision hits and checks if any collision normal points upward.
-     *
-     * @param minUpwardNormalY Minimum Y component of the contact normal to be considered floor.
-     *        Default is ~45° slope limit (cos(45°) ~= 0.707).
      */
-    fun isOnFloor(minUpwardNormalY: Double = cos(degreesToRadians(45.0))): Boolean {
-        return getHits().any { hit ->
-            hit.status != ShapeCastStatus.Failed && hit.normal1.y >= minUpwardNormalY
-        }
+    fun isGrounded(): Boolean {
+        return movementResult?.grounded == true
     }
 
     companion object : ComponentType<KinematicCharacterController> {
@@ -74,4 +72,5 @@ internal expect fun kccExistsForEntity(entityId: EntityId): Boolean
 
 internal expect fun KinematicCharacterController.moveCharacter(dt: Double, translation: Vector3d)
 internal expect fun KinematicCharacterController.setRotationNative(rotation: Quaterniond)
-internal expect fun KinematicCharacterController.getHitsNative(): List<CharacterCollision>
\ No newline at end of file
+internal expect fun KinematicCharacterController.getHitsNative(): List<CharacterCollision>
+internal expect fun KinematicCharacterController.getMovementResult(): CharacterMovementResult?
\ No newline at end of file
diff --git a/scripting/jvmMain/java/com/dropbear/physics/KinematicCharacterControllerNative.java b/scripting/jvmMain/java/com/dropbear/physics/KinematicCharacterControllerNative.java
index 9d2ea7f..daa6c54 100644
--- a/scripting/jvmMain/java/com/dropbear/physics/KinematicCharacterControllerNative.java
+++ b/scripting/jvmMain/java/com/dropbear/physics/KinematicCharacterControllerNative.java
@@ -15,4 +15,5 @@ public class KinematicCharacterControllerNative {
     public static native void moveCharacter(long worldHandle, long physicsHandle, long entityHandle, Vector3d translation, double deltaTime);
     public static native void setRotation(long worldHandle, long physicsHandle, long entityHandle, Quaterniond rotation);
     public static native CharacterCollision[] getHit(long worldHandle, long entity);
+    public static native CharacterMovementResult getMovementResult(long worldHandle, long entity);
 }
diff --git a/scripting/jvmMain/java/com/dropbear/ui/widgets/ButtonNative.java b/scripting/jvmMain/java/com/dropbear/ui/widgets/ButtonNative.java
deleted file mode 100644
index f61d102..0000000
--- a/scripting/jvmMain/java/com/dropbear/ui/widgets/ButtonNative.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.dropbear.ui.widgets;
-
-import com.dropbear.EucalyptusCoreLoader;
-
-public class ButtonNative {
-    static {
-        new EucalyptusCoreLoader().ensureLoaded();
-    }
-
-    public static native boolean getClicked(long uiBufferHandle, long id);
-    public static native boolean getHovering(long uiBufferHandle, long id);
-}
diff --git a/scripting/jvmMain/java/com/dropbear/ui/widgets/CheckboxNative.java b/scripting/jvmMain/java/com/dropbear/ui/widgets/CheckboxNative.java
deleted file mode 100644
index e32abf2..0000000
--- a/scripting/jvmMain/java/com/dropbear/ui/widgets/CheckboxNative.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.dropbear.ui.widgets;
-
-import com.dropbear.EucalyptusCoreLoader;
-
-public class CheckboxNative {
-    static {
-        new EucalyptusCoreLoader().ensureLoaded();
-    }
-
-    public static native boolean getChecked(long uiBufferHandle, long id);
-    public static native boolean hasCheckedState(long uiBufferHandle, long id);
-}
diff --git a/scripting/jvmMain/kotlin/com/dropbear/physics/KinematicCharacterController.jvm.kt b/scripting/jvmMain/kotlin/com/dropbear/physics/KinematicCharacterController.jvm.kt
index d88b4d9..5cbf822 100644
--- a/scripting/jvmMain/kotlin/com/dropbear/physics/KinematicCharacterController.jvm.kt
+++ b/scripting/jvmMain/kotlin/com/dropbear/physics/KinematicCharacterController.jvm.kt
@@ -32,4 +32,11 @@ internal actual fun KinematicCharacterController.getHitsNative(): List<Character
         DropbearEngine.native.worldHandle,
         entity.raw,
     ).toList()
+}
+
+internal actual fun KinematicCharacterController.getMovementResult(): CharacterMovementResult? {
+    return KinematicCharacterControllerNative.getMovementResult(
+        DropbearEngine.native.worldHandle,
+        entity.raw
+    )
 }
\ No newline at end of file
diff --git a/scripting/nativeMain/kotlin/com/dropbear/physics/KinematicCharacterController.native.kt b/scripting/nativeMain/kotlin/com/dropbear/physics/KinematicCharacterController.native.kt
index ed0e696..8578d5c 100644
--- a/scripting/nativeMain/kotlin/com/dropbear/physics/KinematicCharacterController.native.kt
+++ b/scripting/nativeMain/kotlin/com/dropbear/physics/KinematicCharacterController.native.kt
@@ -17,4 +17,8 @@ internal actual fun kccExistsForEntity(entityId: EntityId): Boolean {
 
 internal actual fun KinematicCharacterController.getHitsNative(): List<CharacterCollision> {
     TODO("Not yet implemented")
+}
+
+internal actual fun KinematicCharacterController.getMovementResult(): CharacterMovementResult? {
+    TODO("Not yet implemented")
 }
\ No newline at end of file