kitgit

tirbofish/dropbear · commit

95ef1321353fed96856b6696b502e1f3460270d1

i have no clue what i have done here

Unverified

Thribhu K <4tkbytes@pm.me> · 2026-03-22 13:47

view full diff

diff --git a/crates/dropbear-engine/src/asset.rs b/crates/dropbear-engine/src/asset.rs
index a0de69c..850d1f9 100644
--- a/crates/dropbear-engine/src/asset.rs
+++ b/crates/dropbear-engine/src/asset.rs
@@ -126,6 +126,35 @@ impl AssetRegistry {
     pub fn contains_label(&self, label: &str) -> bool {
         self.texture_labels.contains_key(label) || self.model_labels.contains_key(label)
     }
+
+    /// Flushes away all unused assets and returns the count of flushed models.
+    ///
+    /// Can be used to reduce the memory being used however, to reuse them, you will need to reimport
+    /// them into memory.
+    pub fn flush_unused(&mut self) -> usize {
+        let mut counter = 0;
+
+        // textures
+        self.models.retain(|id, arc| {
+            let is_null = *id == 0;
+            let is_protected = arc.label.eq_ignore_ascii_case("light cube");
+            if is_null || is_protected || Arc::get_mut(arc).is_none() { counter+=1; false } else { true }
+        });
+        self.model_labels.retain(|_, handle| self.models.contains_key(&handle.id));
+
+        // models
+        self.textures.retain(|_, arc| if Arc::get_mut(arc).is_none() { counter+=1; false } else { true });
+        self.texture_labels.retain(|_, handle| self.textures.contains_key(&handle.id));
+
+        counter
+    }
+
+    pub fn flush_everything(&mut self) {
+        self.models.clear();
+        self.model_labels.clear();
+        self.textures.clear();
+        self.texture_labels.clear();
+    }
 }
 
 /// Texture stuff
diff --git a/crates/dropbear-engine/src/entity.rs b/crates/dropbear-engine/src/entity.rs
index 2d8afa0..6aabe11 100644
--- a/crates/dropbear-engine/src/entity.rs
+++ b/crates/dropbear-engine/src/entity.rs
@@ -12,6 +12,212 @@ use crate::{
     utils::ResourceReference,
 };
 use egui::Ui;
+use std::hash::Hash;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RotationEditorMode {
+    EulerDegrees,
+    EulerRadians,
+    Quaternion,
+}
+
+impl Default for RotationEditorMode {
+    fn default() -> Self {
+        Self::EulerDegrees
+    }
+}
+
+impl RotationEditorMode {
+    fn label(self) -> &'static str {
+        match self {
+            Self::EulerDegrees => "Euler (degrees)",
+            Self::EulerRadians => "Euler (radians)",
+            Self::Quaternion => "Quaternion",
+        }
+    }
+}
+
+pub fn inspect_rotation_dquat(
+    ui: &mut Ui,
+    id_source: impl Hash,
+    rotation: &mut DQuat,
+) -> bool {
+    let mode_id = ui.make_persistent_id(("rotation_mode", id_source));
+    let mut mode = ui
+        .ctx()
+        .data_mut(|d| d.get_temp::<RotationEditorMode>(mode_id).unwrap_or_default());
+
+    ui.horizontal(|ui| {
+        ui.label("Mode");
+        egui::ComboBox::from_id_salt(mode_id)
+            .selected_text(mode.label())
+            .show_ui(ui, |ui| {
+                ui.selectable_value(
+                    &mut mode,
+                    RotationEditorMode::EulerDegrees,
+                    RotationEditorMode::EulerDegrees.label(),
+                );
+                ui.selectable_value(
+                    &mut mode,
+                    RotationEditorMode::EulerRadians,
+                    RotationEditorMode::EulerRadians.label(),
+                );
+                ui.selectable_value(
+                    &mut mode,
+                    RotationEditorMode::Quaternion,
+                    RotationEditorMode::Quaternion.label(),
+                );
+            });
+    });
+
+    ui.ctx().data_mut(|d| d.insert_temp(mode_id, mode));
+
+    match mode {
+        RotationEditorMode::EulerDegrees => {
+            let (mut x, mut y, mut z) = rotation.to_euler(glam::EulerRot::XYZ);
+            x = x.to_degrees();
+            y = y.to_degrees();
+            z = z.to_degrees();
+
+            let changed = ui
+                .horizontal(|ui| {
+                    ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:");
+                    let cx = ui
+                        .add(
+                            egui::DragValue::new(&mut x)
+                                .speed(1.0)
+                                .suffix("°")
+                                .fixed_decimals(1),
+                        )
+                        .changed();
+
+                    ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:");
+                    let cy = ui
+                        .add(
+                            egui::DragValue::new(&mut y)
+                                .speed(1.0)
+                                .suffix("°")
+                                .fixed_decimals(1),
+                        )
+                        .changed();
+
+                    ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:");
+                    let cz = ui
+                        .add(
+                            egui::DragValue::new(&mut z)
+                                .speed(1.0)
+                                .suffix("°")
+                                .fixed_decimals(1),
+                        )
+                        .changed();
+
+                    cx || cy || cz
+                })
+                .inner;
+
+            if changed {
+                *rotation = DQuat::from_euler(
+                    glam::EulerRot::XYZ,
+                    x.to_radians(),
+                    y.to_radians(),
+                    z.to_radians(),
+                );
+            }
+            changed
+        }
+        RotationEditorMode::EulerRadians => {
+            let (mut x, mut y, mut z) = rotation.to_euler(glam::EulerRot::XYZ);
+            let changed = ui
+                .horizontal(|ui| {
+                    ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:");
+                    let cx = ui
+                        .add(egui::DragValue::new(&mut x).speed(0.01).fixed_decimals(3))
+                        .changed();
+
+                    ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:");
+                    let cy = ui
+                        .add(egui::DragValue::new(&mut y).speed(0.01).fixed_decimals(3))
+                        .changed();
+
+                    ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:");
+                    let cz = ui
+                        .add(egui::DragValue::new(&mut z).speed(0.01).fixed_decimals(3))
+                        .changed();
+
+                    cx || cy || cz
+                })
+                .inner;
+
+            if changed {
+                *rotation = DQuat::from_euler(glam::EulerRot::XYZ, x, y, z);
+            }
+            changed
+        }
+        RotationEditorMode::Quaternion => {
+            // Store raw (unnormalized) components in temp data keyed to this editor
+            // instance so that normalization on one frame doesn't cause other components
+            // to jump on the next frame while the user is still dragging.
+            let raw_id = mode_id.with("raw_quat");
+            let stored = ui.ctx().data(|d| d.get_temp::<[f64; 4]>(raw_id));
+            let [mut x, mut y, mut z, mut w] =
+                stored.unwrap_or([rotation.x, rotation.y, rotation.z, rotation.w]);
+
+            let mut changed = false;
+            let mut any_dragging = false;
+
+            ui.horizontal(|ui| {
+                ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:");
+                let rx = ui.add(egui::DragValue::new(&mut x).speed(0.01).fixed_decimals(4));
+                changed |= rx.changed();
+                any_dragging |= rx.dragged();
+
+                ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:");
+                let ry = ui.add(egui::DragValue::new(&mut y).speed(0.01).fixed_decimals(4));
+                changed |= ry.changed();
+                any_dragging |= ry.dragged();
+
+                ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:");
+                let rz = ui.add(egui::DragValue::new(&mut z).speed(0.01).fixed_decimals(4));
+                changed |= rz.changed();
+                any_dragging |= rz.dragged();
+
+                ui.colored_label(egui::Color32::from_rgb(220, 180, 80), "W:");
+                let rw = ui.add(egui::DragValue::new(&mut w).speed(0.01).fixed_decimals(4));
+                changed |= rw.changed();
+                any_dragging |= rw.dragged();
+            });
+
+            if any_dragging || changed {
+                // Keep our raw (possibly unnormalized) values in temp storage so the
+                // next frame doesn't re-read from the normalized rotation and make
+                // unedited components jump.
+                ui.ctx().data_mut(|d| d.insert_temp(raw_id, [x, y, z, w]));
+            } else {
+                // Nothing is being edited — sync temp storage back to the real rotation
+                // so external changes (gizmo, script) are reflected.
+                ui.ctx()
+                    .data_mut(|d| d.insert_temp(raw_id, [rotation.x, rotation.y, rotation.z, rotation.w]));
+            }
+
+            if changed {
+                let q = DQuat::from_xyzw(x, y, z, w);
+                if q.length_squared() > 1e-12 {
+                    *rotation = q.normalize();
+                }
+            }
+            changed
+        }
+    }
+}
+
+pub fn inspect_rotation_quat(ui: &mut Ui, id_source: impl Hash, rotation: &mut Quat) -> bool {
+    let mut dquat = rotation.as_dquat();
+    let changed = inspect_rotation_dquat(ui, id_source, &mut dquat);
+    if changed {
+        *rotation = dquat.as_quat();
+    }
+    changed
+}
 
 /// A type of transform that is attached to all entities. It contains the local and world transforms.
 #[derive(Default, Debug, Deserialize, Serialize, Copy, PartialEq, Clone)]
@@ -204,60 +410,12 @@ impl Transform {
             ui.label("Rotation:");
         });
 
-        let (mut x, mut y, mut z) = self.rotation.to_euler(glam::EulerRot::XYZ);
-        x = x.to_degrees();
-        y = y.to_degrees();
-        z = z.to_degrees();
-
-        let changed = ui
-            .horizontal(|ui| {
-                ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:");
-                let cx = ui
-                    .add(
-                        egui::DragValue::new(&mut x)
-                            .speed(1.0)
-                            .suffix("°")
-                            .fixed_decimals(1),
-                    )
-                    .changed();
-
-                ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:");
-                let cy = ui
-                    .add(
-                        egui::DragValue::new(&mut y)
-                            .speed(1.0)
-                            .suffix("°")
-                            .fixed_decimals(1),
-                    )
-                    .changed();
-
-                ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:");
-                let cz = ui
-                    .add(
-                        egui::DragValue::new(&mut z)
-                            .speed(1.0)
-                            .suffix("°")
-                            .fixed_decimals(1),
-                    )
-                    .changed();
-
-                cx || cy || cz
-            })
-            .inner;
+        let _ = inspect_rotation_dquat(ui, "transform_rotation", &mut self.rotation);
 
         if ui.button("Reset Rotation").clicked() {
             self.rotation = DQuat::IDENTITY;
         }
 
-        if changed {
-            self.rotation = DQuat::from_euler(
-                glam::EulerRot::XYZ,
-                x.to_radians(),
-                y.to_radians(),
-                z.to_radians(),
-            );
-        }
-
         ui.add_space(4.0);
 
         // Scale
diff --git a/crates/dropbear-engine/src/lighting.rs b/crates/dropbear-engine/src/lighting.rs
index 030059a..bdaf899 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -10,6 +10,8 @@ use std::fmt::{Display, Formatter};
 use std::sync::Arc;
 use dropbear_utils::Dirty;
 
+const LIGHT_FORWARD_AXIS: DVec3 = DVec3::new(0.0, -1.0, 0.0);
+
 pub const MAX_LIGHTS: usize = 10;
 
 #[repr(C)]
@@ -52,7 +54,7 @@ impl Default for LightUniform {
     fn default() -> Self {
         Self {
             position: [0.0, 0.0, 0.0, 1.0],
-            direction: [0.0, 0.0, -1.0, 1.0],
+            direction: [0.0, -1.0, 0.0, 1.0],
             colour: [1.0, 1.0, 1.0, 1.0],
             // light_type: 0,
             constant: 0.0,
@@ -159,7 +161,7 @@ impl Default for LightComponent {
     fn default() -> Self {
         Self {
             position: DVec3::ZERO,
-            direction: DVec3::new(0.0, 0.0, -1.0),
+            direction: LIGHT_FORWARD_AXIS,
             colour: DVec3::ONE,
             light_type: LightType::Point,
             intensity: 1.0,
@@ -221,7 +223,7 @@ impl LightComponent {
             return DQuat::IDENTITY;
         }
 
-        let forward = DVec3::new(0.0, 0.0, -1.0);
+        let forward = LIGHT_FORWARD_AXIS;
         let normalized_direction = direction.normalize();
 
         let dot = forward.dot(normalized_direction);
diff --git a/crates/dropbear-engine/src/shaders/shader.wgsl b/crates/dropbear-engine/src/shaders/shader.wgsl
index b53390c..8162f0a 100644
--- a/crates/dropbear-engine/src/shaders/shader.wgsl
+++ b/crates/dropbear-engine/src/shaders/shader.wgsl
@@ -14,7 +14,7 @@ struct CameraUniform {
 struct Light {
     position: vec4<f32>,
     direction: vec4<f32>, // x, y, z, outer_cutoff_angle
-    color: vec4<f32>, // r, g, b, light_type (0, 1, 2)
+    color: vec4<f32>,     // r, g, b, light_type (0=directional, 1=point, 2=spot)
     constant: f32,
     lin: f32,
     quadratic: f32,
@@ -32,10 +32,10 @@ struct MaterialUniform {
     alpha_cutoff: f32,
     uv_tiling: vec2<f32>,
 
-    has_normal_texture: u32, // 0,1 bool
-    has_emissive_texture: u32, // 0,1 bool
-    has_metallic_texture: u32, // 0,1 bool
-    has_occlusion_texture: u32, // 0,1 bool
+    has_normal_texture: u32,
+    has_emissive_texture: u32,
+    has_metallic_texture: u32,
+    has_occlusion_texture: u32,
 }
 
 struct MorphTargetInfo {
@@ -95,8 +95,8 @@ var env_map: texture_cube<f32>;
 var env_sampler: sampler;
 
 struct InstanceInput {
-    @location(8) model_matrix_0: vec4<f32>,
-    @location(9) model_matrix_1: vec4<f32>,
+    @location(8)  model_matrix_0: vec4<f32>,
+    @location(9)  model_matrix_1: vec4<f32>,
     @location(10) model_matrix_2: vec4<f32>,
     @location(11) model_matrix_3: vec4<f32>,
 
@@ -113,7 +113,6 @@ struct VertexInput {
     @location(3) tex_coords0: vec2<f32>,
     @location(4) tex_coords1: vec2<f32>,
     @location(5) colour0: vec4<f32>,
-
     @location(6) joints: vec4<u32>,
     @location(7) weights: vec4<f32>,
 };
@@ -135,20 +134,16 @@ fn apply_morph(base_pos: vec3<f32>, vertex_id: u32) -> vec3<f32> {
         let idx = u_morph_info.base_offset + (t * u_morph_info.num_vertices + vertex_id) * 3u;
         let delta = vec3<f32>(
             s_morph_deltas[idx],
-            s_morph_deltas[idx + 1],
-            s_morph_deltas[idx + 2],
+            s_morph_deltas[idx + 1u],
+            s_morph_deltas[idx + 2u],
         );
         result += delta * weight;
     }
-
     return result;
 }
 
 @vertex
-fn vs_main(
-    model: VertexInput,
-    instance: InstanceInput,
-) -> VertexOutput {
+fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput {
     let model_matrix = mat4x4<f32>(
         instance.model_matrix_0,
         instance.model_matrix_1,
@@ -165,18 +160,16 @@ fn vs_main(
         vec4<f32>(1.0, 0.0, 0.0, 0.0),
         vec4<f32>(0.0, 1.0, 0.0, 0.0),
         vec4<f32>(0.0, 0.0, 1.0, 0.0),
-        vec4<f32>(0.0, 0.0, 0.0, 1.0)
+        vec4<f32>(0.0, 0.0, 0.0, 1.0),
     );
-
     if (dot(model.weights, vec4<f32>(1.0)) > 0.0) {
         let j = model.joints;
         let w = model.weights;
-
         skin_matrix =
-            (s_skinning[j.x] * w.x) +
-            (s_skinning[j.y] * w.y) +
-            (s_skinning[j.z] * w.z) +
-            (s_skinning[j.w] * w.w);
+            s_skinning[j.x] * w.x +
+            s_skinning[j.y] * w.y +
+            s_skinning[j.z] * w.z +
+            s_skinning[j.w] * w.w;
     }
 
     let morphed_pos = select(
@@ -186,128 +179,145 @@ fn vs_main(
     );
     let world_position = model_matrix * skin_matrix * vec4<f32>(morphed_pos, 1.0);
 
-    let skin_normal = (skin_matrix * vec4<f32>(model.normal, 0.0)).xyz;
-    let skin_tangent = (skin_matrix * vec4<f32>(model.tangent.xyz, 0.0)).xyz;
+    let skin_normal  = (skin_matrix * vec4<f32>(model.normal,       0.0)).xyz;
+    let skin_tangent = (skin_matrix * vec4<f32>(model.tangent.xyz,  0.0)).xyz;
 
-    let world_normal = normalize(normal_matrix * skin_normal);
-    let world_tangent = normalize(normal_matrix * skin_tangent);
+    let world_normal    = normalize(normal_matrix * skin_normal);
+    let world_tangent   = normalize(normal_matrix * skin_tangent);
     let world_bitangent = normalize(cross(world_normal, world_tangent) * model.tangent.w);
 
     var out: VertexOutput;
-    out.clip_position = u_camera.view_proj * world_position;
-    out.tex_coords = model.tex_coords0;
-    out.world_normal = world_normal;
-    out.world_position = world_position.xyz;
-    out.world_tangent = world_tangent;
-    out.world_bitangent = world_bitangent;
+    out.clip_position      = u_camera.view_proj * world_position;
+    out.tex_coords         = model.tex_coords0;
+    out.world_normal       = world_normal;
+    out.world_position     = world_position.xyz;
+    out.world_tangent      = world_tangent;
+    out.world_bitangent    = world_bitangent;
     out.world_view_position = u_camera.view_pos.xyz;
     return out;
 }
 
-fn directional_light(
-    light: Light,
-    world_normal: vec3<f32>,
-    view_dir: vec3<f32>,
-    tex_color: vec3<f32>,
-    world_pos: vec3<f32>
+
+fn fresnel_schlick(cos_theta: f32, f0: vec3<f32>) -> vec3<f32> {
+    return f0 + (vec3<f32>(1.0) - f0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0);
+}
+
+fn apply_normal_map(
+    world_normal_in:    vec3<f32>,
+    world_tangent_in:   vec3<f32>,
+    world_bitangent_in: vec3<f32>,
+    normal_sample_raw:  vec3<f32>,
+    normal_scale:       f32,
 ) -> vec3<f32> {
-    let light_dir = normalize(-light.direction.xyz);
+    let unpacked = normalize((normal_sample_raw * 2.0 - 1.0) * vec3<f32>(normal_scale, normal_scale, 1.0));
 
-    let diff = max(dot(world_normal, light_dir), 0.0);
-    let diffuse = light.color.xyz * diff * tex_color;
+    let n = normalize(world_normal_in);
+    var t = normalize(world_tangent_in);
+    // Gram-Schmidt re-orthogonalise
+    t = normalize(t - n * dot(n, t));
 
-    let reflect_dir = reflect(-light_dir, world_normal);
-    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
-    let specular = light.color.xyz * spec * tex_color;
+    let b_in     = normalize(world_bitangent_in);
+    let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0);
+    let b        = cross(n, t) * handedness;
 
-    return diffuse + specular;
+    let tbn = mat3x3<f32>(t, b, n);
+    return normalize(tbn * unpacked);
 }
 
-fn point_light(
-    light: Light,
-    world_pos: vec3<f32>,
+fn eval_brdf(
+    light_dir:   vec3<f32>,
     world_normal: vec3<f32>,
-    view_dir: vec3<f32>,
-    tex_color: vec3<f32>
+    view_dir:    vec3<f32>,
+    light_color: vec3<f32>,
+    tex_color:   vec3<f32>,
+    metallic:    f32,
+    roughness:   f32,
 ) -> vec3<f32> {
-    let light_dir = normalize(light.position.xyz - world_pos);
+    let r = clamp(roughness, 0.05, 1.0);
 
-    let distance = length(light.position.xyz - world_pos);
-    let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance)));
+    let n_dot_l  = max(dot(world_normal, light_dir), 0.0);
+    let diffuse  = light_color * n_dot_l * tex_color * (1.0 - metallic);
 
-    let diff = max(dot(world_normal, light_dir), 0.0);
-    let diffuse = light.color.xyz * diff * tex_color;
+    let half_dir = normalize(light_dir + view_dir);
+    let n_dot_h  = max(dot(world_normal, half_dir), 0.0);
+    let shininess = pow(2.0, (1.0 - r) * 10.0); // maps [0,1] roughness → [2, 1024]
+    let spec_val  = pow(n_dot_h, shininess);
 
-    let reflect_dir = reflect(-light_dir, world_normal);
-    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
-    let specular = light.color.xyz * spec * tex_color;
+    let f0       = mix(vec3<f32>(0.04), tex_color, metallic);
+    let n_dot_v  = max(dot(world_normal, view_dir), 0.0);
+    let fresnel  = fresnel_schlick(max(dot(half_dir, view_dir), 0.0), f0);
+    let specular = light_color * spec_val * fresnel;
 
-    return (diffuse + specular) * attenuation;
+    return diffuse + specular;
 }
 
-fn spot_light(
-    light: Light,
-    world_pos: vec3<f32>,
+fn directional_light(
+    light:        Light,
     world_normal: vec3<f32>,
-    view_dir: vec3<f32>,
-    tex_color: vec3<f32>
+    view_dir:     vec3<f32>,
+    tex_color:    vec3<f32>,
+    metallic:     f32,
+    roughness:    f32,
 ) -> vec3<f32> {
-    let light_dir = normalize(light.position.xyz - world_pos);
-    let theta = dot(light_dir, normalize(-light.direction.xyz));
-    let outer_cutoff = light.direction.w;
-
-    let epsilon = light.cutoff - outer_cutoff;
-    let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0);
-
-    let distance = length(light.position.xyz - world_pos);
-    let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance)));
+    let light_dir = normalize(-light.direction.xyz);
+    return eval_brdf(light_dir, world_normal, view_dir, light.color.xyz, tex_color, metallic, roughness);
+}
 
-    let diff = max(dot(world_normal, light_dir), 0.0);
-    let diffuse = light.color.xyz * diff * tex_color * intensity;
+fn point_light(
+    light:        Light,
+    world_pos:    vec3<f32>,
+    world_normal: vec3<f32>,
+    view_dir:     vec3<f32>,
+    tex_color:    vec3<f32>,
+    metallic:     f32,
+    roughness:    f32,
+) -> vec3<f32> {
+    let to_light  = light.position.xyz - world_pos;
+    let distance  = length(to_light);
+    let light_dir = normalize(to_light);
 
-    let reflect_dir = reflect(-light_dir, world_normal);
-    let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
-    let specular = light.color.xyz * spec * tex_color * intensity;
+    let attenuation = 1.0 / (light.constant + light.lin * distance + light.quadratic * (distance * distance));
 
-    return (diffuse + specular) * attenuation;
+    return eval_brdf(light_dir, world_normal, view_dir, light.color.xyz, tex_color, metallic, roughness)
+           * attenuation;
 }
 
-fn apply_normal_map(
-    world_normal_in: vec3<f32>,
-    world_tangent_in: vec3<f32>,
-    world_bitangent_in: vec3<f32>,
-    normal_sample_raw: vec3<f32>,
-    normal_scale: f32,
+fn spot_light(
+    light:        Light,
+    world_pos:    vec3<f32>,
+    world_normal: vec3<f32>,
+    view_dir:     vec3<f32>,
+    tex_color:    vec3<f32>,
+    metallic:     f32,
+    roughness:    f32,
 ) -> vec3<f32> {
-    let unpacked = normalize((normal_sample_raw * 2.0 - 1.0) * vec3<f32>(normal_scale, normal_scale, 1.0));
+    let to_light     = light.position.xyz - world_pos;
+    let distance     = length(to_light);
+    let light_dir    = normalize(to_light);
 
-    let n = normalize(world_normal_in);
-    var t = normalize(world_tangent_in);
-    t = normalize(t - n * dot(n, t));
+    let theta        = dot(light_dir, normalize(-light.direction.xyz));
+    let outer_cutoff = light.direction.w;
+    let epsilon      = light.cutoff - outer_cutoff;
+    let intensity    = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0);
 
-    let b_in = normalize(world_bitangent_in);
-    let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0);
-    let b = cross(n, t) * handedness;
+    let attenuation  = 1.0 / (light.constant + light.lin * distance + light.quadratic * (distance * distance));
 
-    let tbn = mat3x3<f32>(t, b, n);
-    return normalize(tbn * unpacked);
-}
-
-fn fresnel_schlick(cos_theta: f32, f0: vec3<f32>) -> vec3<f32> {
-    return f0 + (vec3<f32>(1.0) - f0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0);
+    return eval_brdf(light_dir, world_normal, view_dir, light.color.xyz, tex_color, metallic, roughness)
+           * attenuation * intensity;
 }
 
 @fragment
 fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
     let uv = in.tex_coords * u_material.uv_tiling;
 
-    let tex_color = textureSample(t_diffuse, s_diffuse, uv);
+    // base colour
+    let tex_color  = textureSample(t_diffuse, s_diffuse, uv);
     let base_colour = tex_color * u_material.base_colour;
-
     if (base_colour.a < u_material.alpha_cutoff) {
         discard;
     }
 
+    // normal
     var world_normal: vec3<f32>;
     if (u_material.has_normal_texture != 0u) {
         let normal_sample_raw = textureSample(t_normal, s_normal, uv).xyz;
@@ -322,20 +332,25 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
         world_normal = normalize(in.world_normal);
     }
 
-    var metallic = u_material.metallic;
+    // metallic and roughness
+    var metallic  = u_material.metallic;
     var roughness = u_material.roughness;
     if (u_material.has_metallic_texture != 0u) {
-        let mr = textureSample(t_metallic, s_metallic, uv);
+        let mr    = textureSample(t_metallic, s_metallic, uv);
         metallic  *= mr.b;
         roughness *= mr.g;
     }
+    metallic  = clamp(metallic,  0.0, 1.0);
+    roughness = clamp(roughness, 0.05, 1.0);
 
+    // occlusion
     var occlusion = 1.0;
     if (u_material.has_occlusion_texture != 0u) {
-        let occ = textureSample(t_occlusion, s_occlusion, uv).r;
+        let occ   = textureSample(t_occlusion, s_occlusion, uv).r;
         occlusion = 1.0 + u_material.occlusion_strength * (occ - 1.0);
     }
 
+    // emissive
     var emissive = u_material.emissive * u_material.emissive_strength;
     if (u_material.has_emissive_texture != 0u) {
         let emissive_tex = textureSample(t_emissive, s_emissive, uv).rgb;
@@ -344,32 +359,32 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
 
     let view_dir = normalize(u_camera.view_pos.xyz - in.world_position);
 
-    let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.rgb * occlusion;
-
+    // ambient
+    let ambient = u_globals.ambient_strength * base_colour.rgb * occlusion;
     var final_color = ambient;
 
     for (var i = 0u; i < u_globals.num_lights; i += 1u) {
-        let light = s_light_array[i];
+        let light      = s_light_array[i];
         let light_type = i32(light.color.w + 0.1);
+
         if (light_type == 0) {
-            final_color += directional_light(light, world_normal, view_dir, base_colour.rgb, in.world_position);
+            final_color += directional_light(light, world_normal, view_dir, base_colour.rgb, metallic, roughness);
         } else if (light_type == 1) {
-            final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.rgb);
+            final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.rgb, metallic, roughness);
         } else if (light_type == 2) {
-            final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.rgb);
+            final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.rgb, metallic, roughness);
         }
     }
 
-    let world_reflect = reflect(-view_dir, world_normal);
-    let reflection = textureSample(env_map, env_sampler, world_reflect).rgb;
-    let n_dot_v = max(dot(world_normal, view_dir), 0.0);
-    let f0 = mix(vec3<f32>(0.04), base_colour.rgb, metallic);
-    let fresnel = fresnel_schlick(n_dot_v, f0);
-    let reflection_weight = fresnel * (1.0 - roughness) * mix(vec3<f32>(1.0), base_colour.rgb, metallic);
-    final_color += reflection * reflection_weight;
+    let world_reflect      = reflect(-view_dir, world_normal);
+    let reflection         = textureSample(env_map, env_sampler, world_reflect).rgb;
+    let n_dot_v            = max(dot(world_normal, view_dir), 0.0);
+    let f0                 = mix(vec3<f32>(0.04), base_colour.rgb, metallic);
+    let fresnel            = fresnel_schlick(n_dot_v, f0);
+    let reflection_weight  = fresnel * pow(1.0 - roughness, 2.0) * mix(vec3<f32>(1.0), base_colour.rgb, metallic);
+    final_color           += reflection * reflection_weight;
 
     final_color += emissive;
 
-    // return vec4<f32>(uv.x, uv.y, 0.0, 1.0);
     return vec4<f32>(final_color, base_colour.a);
 }
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/utils.rs b/crates/dropbear-engine/src/utils.rs
index 04bce82..a66f4bf 100644
--- a/crates/dropbear-engine/src/utils.rs
+++ b/crates/dropbear-engine/src/utils.rs
@@ -101,8 +101,7 @@ pub enum ResourceReferenceType {
     /// [`include_bytes!`] macro, this type stores it.
     Bytes(Arc<[u8]>),
 
-    /// An object that can be generated at runtime with the usage of vertices and indices, as well
-    /// as a solid grey mesh.
+    /// Any object that can be generated at runtime instead of from a file. 
     ProcObj(ProcedurallyGeneratedObject),
 }
 
diff --git a/crates/eucalyptus-core/src/billboard.rs b/crates/eucalyptus-core/src/billboard.rs
index 6e18aeb..9e7eb7d 100644
--- a/crates/eucalyptus-core/src/billboard.rs
+++ b/crates/eucalyptus-core/src/billboard.rs
@@ -1,5 +1,6 @@
 use std::sync::Arc;
 use hecs::{Entity, World};
+use dropbear_engine::entity::inspect_rotation_quat;
 use dropbear_engine::graphics::SharedGraphicsContext;
 use crate::component::{Component, ComponentDescriptor, ComponentInitFuture, InspectableComponent, SerializedComponent};
 use crate::physics::PhysicsState;
@@ -114,14 +115,12 @@ impl InspectableComponent for BillboardComponent {
                 }
 
                 if let Some(rotation) = &mut self.rotation {
-                    ui.horizontal(|ui| {
-                        ui.label("Rotation");
-                        ui.add(egui::DragValue::new(&mut rotation.x).speed(0.01));
-                        ui.add(egui::DragValue::new(&mut rotation.y).speed(0.01));
-                        ui.add(egui::DragValue::new(&mut rotation.z).speed(0.01));
-                        ui.add(egui::DragValue::new(&mut rotation.w).speed(0.01));
-                    });
-                    *rotation = rotation.normalize();
+                    ui.label("Rotation");
+                    let _ = inspect_rotation_quat(
+                        ui,
+                        ("billboard_rotation", entity.to_bits()),
+                        rotation,
+                    );
                 }
 
             });
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index 89b5f28..e389fc7 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -1,7 +1,7 @@
 use crate::hierarchy::EntityTransformExt;
 use crate::physics::PhysicsState;
 use crate::ser::model::EucalyptusModel;
-use crate::states::{Label, SerializedMaterialCustomisation, SerializedMeshRenderer};
+use crate::states::{SerializedMaterialCustomisation, SerializedMeshRenderer};
 use crate::utils::{AsFile, ResolveReference};
 use downcast_rs::{Downcast, impl_downcast};
 use dropbear_engine::asset::{ASSET_REGISTRY, Handle};
@@ -594,8 +594,9 @@ impl Component for MeshRenderer {
                     .await?
                 }
                 ResourceReferenceType::ProcObj(obj) => {
-                    log::warn!("ResourceReferenceType::Bytes is unsupported and is highly NOT recommended to be used for serialization as it will explode the memory on save");
-                    log::warn!("Please serialize to a file when you get the chance to do so (could also be an editor bug...)");
+                    // log::warn!("ResourceReferenceType::Bytes is unsupported and is highly NOT recommended to be used for serialization as it will explode the memory on save");
+                    // log::warn!("Please serialize to a file when you get the chance to do so (could also be an editor bug...)");
+                    // warnings removed, only use in euca-runner on release.
                     obj.build_model(graphics.clone(), None, None, ASSET_REGISTRY.clone())
                 }
             };
@@ -622,16 +623,16 @@ impl Component for MeshRenderer {
                         match resource {
                             Some(TextureReference::Resource(dif)) => match &dif.ref_type {
                                 ResourceReferenceType::None => None,
-                                ResourceReferenceType::File(_) => {
+                                ResourceReferenceType::File(file_path) => {
                                     let path = dif.resolve().ok()?;
                                     let bytes = std::fs::read(&path).ok()?;
                                     let mut texture = TextureBuilder::new(&graphics.device)
                                         .with_bytes(graphics.clone(), bytes.as_slice())
-                                        .label(label.as_str())
+                                        .label(file_path.as_str())
                                         .build();
                                     texture.reference = Some(dif.clone());
                                     let mut registry = ASSET_REGISTRY.write();
-                                    Some(Ok(registry.add_texture_with_label(label.clone(), texture)))
+                                    Some(Ok(registry.add_texture_with_label(file_path.clone(), texture)))
                                 }
                                 ResourceReferenceType::Bytes(bytes) => {
                                     let texture = TextureBuilder::new(&graphics.device)
@@ -710,65 +711,56 @@ impl Component for MeshRenderer {
         }
     }
 
-    fn save(&self, world: &World, entity: Entity) -> Box<dyn SerializedComponent> {
-        let entity_label_raw = world
-            .query_one::<&Label>(entity)
-            .get()
-            .map(|label| label.as_str().to_string())
-            .unwrap_or_else(|_| "unnamed_entity".to_string());
-
-        let sanitize_segment = |segment: &str| -> String {
-            let mut result = String::with_capacity(segment.len());
-            for ch in segment.chars() {
-                if ch.is_ascii_alphanumeric() {
-                    result.push(ch.to_ascii_lowercase());
-                } else if ch == '_' || ch == '-' {
-                    result.push(ch);
-                } else {
-                    result.push('_');
-                }
-            }
-
-            let trimmed = result.trim_matches('_').to_string();
-            if trimmed.is_empty() {
-                "unnamed_entity".to_string()
-            } else {
-                trimmed
-            }
-        };
-
-        let proc_obj_type_name = |ty: &ProcObjType| -> &'static str {
-            match ty {
-                ProcObjType::Cuboid => "cuboid",
-            }
-        };
-
-        let entity_label = sanitize_segment(entity_label_raw.as_str());
+    fn save(&self, _world: &World, _entity: Entity) -> Box<dyn SerializedComponent> {
+        // let entity_label_raw = world
+        //     .query_one::<&Label>(entity)
+        //     .get()
+        //     .map(|label| label.as_str().to_string())
+        //     .unwrap_or_else(|_| "unnamed_entity".to_string());
+        //
+        // let sanitize_segment = |segment: &str| -> String {
+        //     let mut result = String::with_capacity(segment.len());
+        //     for ch in segment.chars() {
+        //         if ch.is_ascii_alphanumeric() {
+        //             result.push(ch.to_ascii_lowercase());
+        //         } else if ch == '_' || ch == '-' {
+        //             result.push(ch);
+        //         } else {
+        //             result.push('_');
+        //         }
+        //     }
+        //
+        //     let trimmed = result.trim_matches('_').to_string();
+        //     if trimmed.is_empty() {
+        //         "unnamed_entity".to_string()
+        //     } else {
+        //         trimmed
+        //     }
+        // };
+
+        // let proc_obj_type_name = |ty: &ProcObjType| -> &'static str {
+        //     match ty {
+        //         ProcObjType::Cuboid => "cuboid",
+        //     }
+        // };
+        //
+        // let entity_label = sanitize_segment(entity_label_raw.as_str());
 
         let save_reference = |reference: ResourceReference, context: &str| -> ResourceReference {
             match &reference.ref_type {
-                ResourceReferenceType::None | ResourceReferenceType::File(_) => reference,
-                ResourceReferenceType::Bytes(_) | ResourceReferenceType::ProcObj(_) => {
-                    let desired_ref = match &reference.ref_type {
-                        ResourceReferenceType::ProcObj(obj) => Some(format!(
-                            "gen/{}.{}.eucmdl",
-                            entity_label,
-                            proc_obj_type_name(&obj.ty)
-                        )),
-                        _ => None,
-                    };
-
+                ResourceReferenceType::None | ResourceReferenceType::File(_) | ResourceReferenceType::ProcObj(_) => reference,
+                ResourceReferenceType::Bytes(_) => {
                     match reference
-                        .as_file(desired_ref)
+                        .as_file(None)
                         .and_then(|path| ResourceReference::from_path(path.as_path()))
                     {
                         Ok(file_ref) => file_ref,
                         Err(err) => {
                             log::warn!(
-                                "Failed to save {} as file-backed reference: {}",
-                                context,
-                                err
-                            );
+                        "Failed to save {} as file-backed reference: {}",
+                        context,
+                        err
+                    );
                             ResourceReference::default()
                         }
                     }
@@ -921,16 +913,6 @@ impl InspectableComponent for MeshRenderer {
                 || 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")
-        //         || uri.ends_with(".webp")
-        // }
-
         fn proc_obj_size(obj: &ProcedurallyGeneratedObject) -> Option<[f32; 3]> {
             if obj.ty != ProcObjType::Cuboid {
                 return None;
@@ -1262,10 +1244,8 @@ impl InspectableComponent for MeshRenderer {
                                     .changed();
 
                                 if changed {
-                                    // Preserve material customizations across cuboid size change
                                     let saved_materials = self.material_snapshot.clone();
                                     apply_cuboid(self, size, false);
-                                    // Re-apply material customizations to the new model
                                     for (name, saved_mat) in saved_materials {
                                         if let Some(mat) = self.material_snapshot.get_mut(&name) {
                                             mat.tint = saved_mat.tint;
diff --git a/crates/eucalyptus-core/src/lighting.rs b/crates/eucalyptus-core/src/lighting.rs
index 5126d90..d5d336e 100644
--- a/crates/eucalyptus-core/src/lighting.rs
+++ b/crates/eucalyptus-core/src/lighting.rs
@@ -8,7 +8,7 @@ use crate::scripting::result::DropbearNativeResult;
 use crate::states::SerializedLight;
 use crate::types::NVector3;
 use dropbear_engine::attenuation::ATTENUATION_PRESETS;
-use dropbear_engine::entity::{EntityTransform, Transform};
+use dropbear_engine::entity::{EntityTransform, Transform, inspect_rotation_dquat};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::lighting::{Light, LightType};
 use egui::{CollapsingHeader, ComboBox, DragValue, Ui};
@@ -18,6 +18,8 @@ use jni::JNIEnv;
 use jni::objects::{JObject, JValue};
 use std::sync::Arc;
 
+const LIGHT_FORWARD_AXIS: DVec3 = DVec3::new(0.0, -1.0, 0.0);
+
 #[typetag::serde]
 impl SerializedComponent for SerializedLight {}
 
@@ -64,12 +66,10 @@ impl Component for Light {
         if let Ok(entity_transform) = world.query_one::<&EntityTransform>(entity).get() {
             let transform = entity_transform.sync();
             synced.position = transform.position;
-            synced.direction =
-                (transform.rotation * DVec3::new(0.0, 0.0, -1.0)).normalize_or_zero();
+            synced.direction = (transform.rotation * LIGHT_FORWARD_AXIS).normalize_or_zero();
         } else if let Ok(transform) = world.query_one::<&Transform>(entity).get() {
             synced.position = transform.position;
-            synced.direction =
-                (transform.rotation * DVec3::new(0.0, 0.0, -1.0)).normalize_or_zero();
+            synced.direction = (transform.rotation * LIGHT_FORWARD_AXIS).normalize_or_zero();
         }
 
         self.update(&graphics);
@@ -87,7 +87,7 @@ impl Component for Light {
 impl InspectableComponent for Light {
     fn inspect(
         &mut self,
-        _world: &World,
+        world: &World,
         entity: Entity,
         ui: &mut Ui,
         _graphics: Arc<SharedGraphicsContext>,
@@ -121,33 +121,119 @@ impl InspectableComponent for Light {
                     });
 
                 let mut display_pos = |yueye: &mut Ui| {
+                    let pos_id = yueye.make_persistent_id(("light_pos", entity.to_bits()));
+                    let stored = yueye
+                        .ctx()
+                        .data(|d| d.get_temp::<[f64; 3]>(pos_id));
+                    let [mut px, mut py, mut pz] = stored.unwrap_or([
+                        self.component.position.x,
+                        self.component.position.y,
+                        self.component.position.z,
+                    ]);
+
+                    let mut changed = false;
+                    let mut any_dragging = false;
+                    let mut reset = false;
+
                     yueye.horizontal(|yueye| {
                         yueye.label("Position");
-                        yueye.add(DragValue::new(&mut self.component.position.x).speed(0.01));
-                        yueye.add(DragValue::new(&mut self.component.position.y).speed(0.01));
-                        yueye.add(DragValue::new(&mut self.component.position.z).speed(0.01));
+                        let rx = yueye.add(DragValue::new(&mut px).speed(0.01));
+                        changed |= rx.changed();
+                        any_dragging |= rx.dragged();
+
+                        let ry = yueye.add(DragValue::new(&mut py).speed(0.01));
+                        changed |= ry.changed();
+                        any_dragging |= ry.dragged();
+
+                        let rz = yueye.add(DragValue::new(&mut pz).speed(0.01));
+                        changed |= rz.changed();
+                        any_dragging |= rz.dragged();
+
+                        if yueye.button("Reset").clicked() {
+                            px = 0.0;
+                            py = 0.0;
+                            pz = 0.0;
+                            changed = true;
+                            reset = true;
+                        }
                     });
+
+                    if any_dragging || changed || reset {
+                        yueye.ctx().data_mut(|d| d.insert_temp(pos_id, [px, py, pz]));
+                        self.component.position = DVec3::new(px, py, pz);
+                    } else {
+                        yueye.ctx().data_mut(|d| d.insert_temp(pos_id, [
+                            self.component.position.x,
+                            self.component.position.y,
+                            self.component.position.z,
+                        ]));
+                    }
+
+                    changed
                 };
 
-                let mut display_dir = |yueye: &mut Ui| {
-                    yueye.horizontal(|yueye| {
-                        yueye.label("Direction");
-                        yueye.add(DragValue::new(&mut self.component.direction.x).speed(0.01));
-                        yueye.add(DragValue::new(&mut self.component.direction.y).speed(0.01));
-                        yueye.add(DragValue::new(&mut self.component.direction.z).speed(0.01));
-                    });
+                let mut display_rot = |yueye: &mut Ui| {
+                    yueye.label("Rotation");
+                    let mut direction = self.component.direction.normalize_or_zero();
+                    if direction.length_squared() < 1e-12 {
+                        direction = LIGHT_FORWARD_AXIS;
+                    }
+
+                    let mut rotation = DQuat::from_rotation_arc(LIGHT_FORWARD_AXIS, direction);
+                    let mut changed = inspect_rotation_dquat(
+                        yueye,
+                        ("light_rotation", entity.to_bits()),
+                        &mut rotation,
+                    );
+
+                    if changed {
+                        self.component.direction = (rotation * LIGHT_FORWARD_AXIS).normalize_or_zero();
+                    }
+
+                    if yueye.button("Reset Rotation").clicked() {
+                        self.component.direction = LIGHT_FORWARD_AXIS;
+                        changed = true;
+                    }
+
+                    changed
                 };
 
+                let mut position_changed = false;
+                let mut direction_changed = false;
+
                 match self.component.light_type {
                     LightType::Directional => {
-                        display_dir(ui);
+                        direction_changed |= display_rot(ui);
                     }
                     LightType::Point => {
-                        display_pos(ui);
+                        position_changed |= display_pos(ui);
                     }
                     LightType::Spot => {
-                        display_pos(ui);
-                        display_dir(ui);
+                        position_changed |= display_pos(ui);
+                        direction_changed |= display_rot(ui);
+                    }
+                }
+
+                if position_changed {
+                    if let Ok(entity_transform) = world.query_one::<&mut EntityTransform>(entity).get() {
+                        entity_transform.local_mut().position = self.component.position;
+                    } else if let Ok(transform) = world.query_one::<&mut Transform>(entity).get() {
+                        transform.position = self.component.position;
+                    }
+                }
+
+                if direction_changed {
+                    let desired = self.component.direction.normalize_or_zero();
+                    if desired.length_squared() >= 1e-12 {
+                        self.component.direction = desired;
+                        let rotation = DQuat::from_rotation_arc(LIGHT_FORWARD_AXIS, desired);
+
+                        if let Ok(entity_transform) = world.query_one::<&mut EntityTransform>(entity).get() {
+                            entity_transform.local_mut().rotation = rotation;
+                        } else if let Ok(transform) = world.query_one::<&mut Transform>(entity).get()
+                        {
+                            transform.rotation = rotation;
+                        }
                     }
                 }
 
@@ -517,7 +603,7 @@ fn get_direction(
     #[dropbear_macro::entity] entity: Entity,
 ) -> DropbearNativeResult<NVector3> {
     let transform = get_transform(world, entity)?;
-    let forward = DVec3::new(0.0, 0.0, -1.0);
+    let forward = LIGHT_FORWARD_AXIS;
     let dir = (transform.rotation * forward).normalize_or_zero();
     Ok(NVector3::from(dir))
 }
@@ -537,7 +623,7 @@ fn set_direction(
         return Err(DropbearNativeError::InvalidArgument);
     }
 
-    let forward = DVec3::new(0.0, 0.0, -1.0);
+    let forward = LIGHT_FORWARD_AXIS;
     let rotation = DQuat::from_rotation_arc(forward, desired);
     set_transform_rotation(world, entity, rotation)
 }
diff --git a/crates/eucalyptus-core/src/physics/collider.rs b/crates/eucalyptus-core/src/physics/collider.rs
index 44b9b78..26edf95 100644
--- a/crates/eucalyptus-core/src/physics/collider.rs
+++ b/crates/eucalyptus-core/src/physics/collider.rs
@@ -27,6 +27,7 @@ use crate::states::Label;
 use crate::types::{NCollider, NVector3};
 use ::jni::JNIEnv;
 use ::jni::objects::{JObject, JValue};
+use dropbear_engine::entity::inspect_rotation_dquat;
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::wgpu::util::{BufferInitDescriptor, DeviceExt};
 use dropbear_engine::wgpu::{Buffer, BufferUsages};
@@ -339,33 +340,17 @@ impl Collider {
                 ui.add(egui::DragValue::new(&mut self.translation[2]).speed(0.01));
             });
 
-            ui.label("Rotation (degrees):");
-            ui.horizontal(|ui| {
-                ui.label("X:");
-                let mut deg_x = self.rotation[0].to_degrees();
-                if ui
-                    .add(egui::DragValue::new(&mut deg_x).speed(1.0))
-                    .changed()
-                {
-                    self.rotation[0] = deg_x.to_radians();
-                }
-                ui.label("Y:");
-                let mut deg_y = self.rotation[1].to_degrees();
-                if ui
-                    .add(egui::DragValue::new(&mut deg_y).speed(1.0))
-                    .changed()
-                {
-                    self.rotation[1] = deg_y.to_radians();
-                }
-                ui.label("Z:");
-                let mut deg_z = self.rotation[2].to_degrees();
-                if ui
-                    .add(egui::DragValue::new(&mut deg_z).speed(1.0))
-                    .changed()
-                {
-                    self.rotation[2] = deg_z.to_radians();
-                }
-            });
+            ui.label("Rotation:");
+            let mut rotation = DQuat::from_euler(
+                glam::EulerRot::XYZ,
+                self.rotation[0] as f64,
+                self.rotation[1] as f64,
+                self.rotation[2] as f64,
+            );
+            if inspect_rotation_dquat(ui, "collider_local_rotation", &mut rotation) {
+                let (x, y, z) = rotation.to_euler(glam::EulerRot::XYZ);
+                self.rotation = [x as f32, y as f32, z as f32];
+            }
         });
     }
 }
diff --git a/crates/eucalyptus-editor/src/debug.rs b/crates/eucalyptus-editor/src/debug.rs
index f699126..d52e288 100644
--- a/crates/eucalyptus-editor/src/debug.rs
+++ b/crates/eucalyptus-editor/src/debug.rs
@@ -2,6 +2,7 @@
 
 mod window;
 
+use std::collections::VecDeque;
 use crate::debug::window::DebugWindow;
 use crate::editor::Signal;
 use dropbear_engine::DropbearWindowBuilder;
@@ -10,7 +11,7 @@ use parking_lot::RwLock;
 use std::rc::Rc;
 use winit::window::WindowAttributes;
 
-pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut Signal) {
+pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut VecDeque<Signal>) {
     ui.menu_button("Debug", |ui_debug| {
         if ui_debug.button("Panic").clicked() {
             log::warn!("Panic caused on purpose from Menu Button Click");
@@ -35,7 +36,7 @@ pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut Signal) {
                 .set_initial_scene("debug_window")
                 .build();
 
-            *signal = Signal::RequestNewWindow(window_data);
+            signal.push_back(Signal::RequestNewWindow(window_data));
         }
 
         if ui_debug.button("Show TypeID of components").clicked() {
diff --git a/crates/eucalyptus-editor/src/editor/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
index b39062c..c6a691f 100644
--- a/crates/eucalyptus-editor/src/editor/asset_viewer.rs
+++ b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
@@ -17,6 +17,7 @@ use crate::editor::{
     SceneDivision, ScriptDivision, Signal, StaticallyKept, TABS_GLOBAL,
 };
 use eucalyptus_core::component::DRAGGED_ASSET_ID;
+use eucalyptus_core::hierarchy::Hierarchy;
 use crate::editor::page::EditorTabVisibility;
 
 impl<'a> EditorTabViewer<'a> {
@@ -965,10 +966,10 @@ impl<'a> EditorTabViewer<'a> {
                     return;
                 }
             }
-            *self.signal = Signal::AssetPaste {
+            self.signal.push_back(Signal::AssetPaste {
                 target_dir: info.path.clone(),
                 division: info.division,
-            };
+            });
         }
 
         if ui.button("Reveal Folder").clicked() {
@@ -1032,10 +1033,10 @@ impl<'a> EditorTabViewer<'a> {
 
         if ui.button("Copy").clicked() {
             ui.close();
-            *self.signal = Signal::AssetCopy {
+            self.signal.push_back(Signal::AssetCopy {
                 source: info.path.clone(),
                 division: info.division,
-            };
+            });
         }
 
         if ui.button("Delete").clicked() {
@@ -1229,7 +1230,7 @@ impl<'a> EditorTabViewer<'a> {
             .get_model_handle_by_reference(&reference)
             .is_some()
         {
-            info!("Model already loaded: {}", label);
+            eucalyptus_core::info!("Model already loaded: {}", label);
             return;
         }
 
@@ -1276,6 +1277,7 @@ impl<'a> EditorTabViewer<'a> {
             let mut registry = ASSET_REGISTRY.write();
             registry.label_model(label.clone(), handle);
 
+            eucalyptus_core::success!("Loaded model {}", label);
             Ok::<(), anyhow::Error>(())
         });
     }
@@ -1356,16 +1358,31 @@ impl<'a> EditorTabViewer<'a> {
                 .first()
                 .and_then(|node_id| cfg.component_selection(*node_id))
         });
-        
+
         if let Some(selection) = selection {
             self.inspect_component_selection(cfg, selection);
-            if let Some(target_entity) = Self::entity_from_node_id(drag.target) {
-                let v = if crate::features::is_enabled(crate::features::ShowComponentTypeIDInEditor) { format!(" id #{}", selection.component_type_id) } else { "".to_string() };
-                eucalyptus_core::info!(
-                    "Component{} ready to drop onto entity {:?}",
-                    v,
-                    target_entity
-                );
+            return;
+        }
+
+        let target_entity = Self::entity_from_node_id(drag.target);
+
+        for &source_id in &drag.source {
+            let Some(source_entity) = Self::entity_from_node_id(source_id) else { continue };
+
+            if cfg.component_selection(source_id).is_some() {
+                continue;
+            }
+
+            if drag.target == u64::MAX {
+                Hierarchy::remove_parent(self.world, source_entity);
+            } else if let Some(target_entity) = target_entity {
+                if source_entity == target_entity {
+                    continue;
+                }
+                if Hierarchy::is_descendant_of(self.world, target_entity, source_entity) {
+                    continue;
+                }
+                Hierarchy::set_parent(self.world, source_entity, target_entity);
             }
         }
     }
diff --git a/crates/eucalyptus-editor/src/editor/dock.rs b/crates/eucalyptus-editor/src/editor/dock.rs
index d9cc38e..e89fbca 100644
--- a/crates/eucalyptus-editor/src/editor/dock.rs
+++ b/crates/eucalyptus-editor/src/editor/dock.rs
@@ -22,7 +22,7 @@ pub struct EditorTabViewer<'a> {
     pub selected_entity: &'a mut Option<Entity>,
     pub viewport_mode: &'a mut ViewportMode,
     pub undo_stack: &'a mut Vec<UndoableAction>,
-    pub signal: &'a mut Signal,
+    pub signal: &'a mut VecDeque<Signal>,
     pub gizmo_mode: &'a mut EnumSet<GizmoMode>,
     pub gizmo_orientation: &'a mut GizmoOrientation,
     pub editor_mode: &'a mut EditorState,
diff --git a/crates/eucalyptus-editor/src/editor/entity_list.rs b/crates/eucalyptus-editor/src/editor/entity_list.rs
index 0a2106d..8cd133c 100644
--- a/crates/eucalyptus-editor/src/editor/entity_list.rs
+++ b/crates/eucalyptus-editor/src/editor/entity_list.rs
@@ -6,7 +6,7 @@ use eucalyptus_core::{
     states::{Label, PROJECT},
 };
 use hecs::{Entity, World};
-use std::collections::{BTreeMap, HashMap};
+use std::collections::{BTreeMap, HashMap, VecDeque};
 
 use crate::editor::{Editor, EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, StaticallyKept, TABS_GLOBAL};
 use crate::editor::page::EditorTabVisibility;
@@ -50,7 +50,7 @@ impl<'a> EditorTabViewer<'a> {
                     component_ids_by_entity: &HashMap<Entity, Vec<u64>>,
                     rigidbody_component_id: Option<u64>,
                     cfg: &mut StaticallyKept,
-                    signal: &mut Signal,
+                    signal: &mut VecDeque<Signal>,
                 ) -> anyhow::Result<()> {
                     puffin::profile_scope!("entity_list.add_entity_to_tree");
                     let entity_id = entity.to_bits().get();
@@ -106,8 +106,7 @@ impl<'a> EditorTabViewer<'a> {
                                                     if let Some(component) =
                                                         registry.create_default_component(*id)
                                                     {
-                                                        *signal =
-                                                            Signal::AddComponent(entity, component);
+                                                        signal.push_back(Signal::AddComponent(entity, component));
                                                     }
                                                     ui.close();
                                                 }
diff --git a/crates/eucalyptus-editor/src/editor/input.rs b/crates/eucalyptus-editor/src/editor/input.rs
index b9a232b..baee7f6 100644
--- a/crates/eucalyptus-editor/src/editor/input.rs
+++ b/crates/eucalyptus-editor/src/editor/input.rs
@@ -1,6 +1,5 @@
 use super::*;
 use dropbear_engine::input::{Controller, Keyboard, Mouse};
-use eucalyptus_core::states::Label;
 use eucalyptus_core::success_without_console;
 use gilrs::{Button, GamepadId};
 use log;
@@ -84,7 +83,7 @@ impl Keyboard for Editor {
                             .map_or(false, |id| *tab == id)
                     {
                         if self.selected_entity.is_some() {
-                            self.signal = Signal::Delete;
+                            self.signal.push_back(Signal::Delete);
                         } else {
                             warn!("Failed to delete: No entity selected");
                         }
@@ -95,7 +94,7 @@ impl Keyboard for Editor {
             }
             KeyCode::Escape => {
                 if is_playing {
-                    self.signal = Signal::StopPlaying;
+                    self.signal.push_back(Signal::StopPlaying);
                 } else if is_double_press {
                     if self.selected_entity.is_some() {
                         self.selected_entity = None;
@@ -165,7 +164,6 @@ impl Keyboard for Editor {
                 }
             }
             KeyCode::KeyC => {
-                // todo: fix this
                 if ctrl_pressed && !is_playing {
                     if let Some((_, tab)) = self.game_editor_dock_state.find_active_focused()
                         && self
@@ -174,24 +172,20 @@ impl Keyboard for Editor {
                             .map_or(false, |id| *tab == id)
                     {
                         if let Some(entity) = &self.selected_entity {
-                            let Ok(label) = self.world.get::<&Label>(*entity) else {
+                            let entity = *entity;
+                            let (entities, parent_map) = Editor::collect_entity_subtree(
+                                self.world.as_ref(),
+                                entity,
+                                &self.component_registry,
+                            );
+                            if entities.is_empty() {
                                 warn!("Unable to copy entity: Unable to obtain label");
-                                return;
-                            };
-
-                            let components = self
-                                .component_registry
-                                .extract_all_components(self.world.as_ref(), *entity);
-                            let s_entity = SceneEntity {
-                                label: Label::new(label.as_str()),
-                                components,
-                                entity_id: None,
-                            };
-                            self.signal = Signal::Copy(s_entity);
-
-                            info!("Copied!");
-
-                            log::debug!("Copied selected entity");
+                            } else {
+                                self.signal.retain(|s| !matches!(s, Signal::Copy(_, _)));
+                                self.signal.push_back(Signal::Copy(entities, parent_map));
+                                info!("Copied!");
+                                log::debug!("Copied selected entity");
+                            }
                         } else {
                             warn!("Unable to copy entity: None selected");
                         }
@@ -205,8 +199,10 @@ impl Keyboard for Editor {
             }
             KeyCode::KeyV => {
                 if ctrl_pressed && !is_playing {
-                    if let Signal::Copy(entity) = &self.signal {
-                        self.signal = Signal::Paste(entity.clone());
+                    if let Some(Signal::Copy(entities, parent_map)) = self.signal.iter().find(|s| matches!(s, Signal::Copy(_, _))) {
+                        let entities = entities.clone();
+                        let parent_map = parent_map.clone();
+                        self.signal.push_back(Signal::Paste(entities, parent_map));
                     }
                 } else {
                     self.input_state.pressed_keys.insert(key);
@@ -240,7 +236,7 @@ impl Keyboard for Editor {
                     } else {
                         // undo
                         log::debug!("Undo signal sent");
-                        self.signal = Signal::Undo;
+                        self.signal.push_back(Signal::Undo);
                     }
                 } else if matches!(self.viewport_mode, ViewportMode::Gizmo) && !is_playing {
                     info!("GizmoMode set to translate");
@@ -270,14 +266,14 @@ impl Keyboard for Editor {
             }
             KeyCode::KeyP => {
                 if !is_playing && ctrl_pressed {
-                    self.signal = Signal::Play
+                    self.signal.push_back(Signal::Play);
                 } else {
                     self.input_state.pressed_keys.insert(key);
                 }
             }
             KeyCode::F12 => {
                 if is_playing {
-                    self.signal = Signal::StopPlaying;
+                    self.signal.push_back(Signal::StopPlaying);
                     info!("Stopping play mode");
                 } else {
                     self.input_state.pressed_keys.insert(key);
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 1db224c..f8b1629 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -66,7 +66,7 @@ use kino_ui::windowing::KinoWinitWindowing;
 use log::{debug, error};
 use parking_lot::{Mutex, RwLock};
 use rfd::FileDialog;
-use std::collections::HashSet;
+use std::collections::{HashSet, VecDeque};
 use std::rc::Rc;
 use std::{collections::HashMap, fs, path::PathBuf, sync::Arc, time::Instant};
 use std::cmp::PartialEq;
@@ -79,6 +79,7 @@ use dropbear_engine::multisampling::AntiAliasingMode;
 use winit::window::{CursorGrabMode, WindowAttributes};
 use winit::{keyboard::KeyCode, window::Window};
 use dropbear_engine::animation::{MorphTargetInfo, MAX_MORPH_WEIGHTS};
+use dropbear_engine::asset::ASSET_REGISTRY;
 use crate::editor::page::EditorTabVisibility;
 use crate::editor::ui::UiEditor;
 
@@ -137,7 +138,7 @@ pub struct Editor {
     pub selected_entity: Option<hecs::Entity>,
     pub viewport_mode: ViewportMode,
 
-    pub(crate) signal: Signal,
+    pub(crate) signal: VecDeque<Signal>,
     pub(crate) undo_stack: Vec<UndoableAction>,
     // todo: add redo (later)
     // redo_stack: Vec<UndoableAction>,
@@ -280,7 +281,7 @@ impl Editor {
             previously_selected_entity: None,
             selected_entity: None,
             viewport_mode: ViewportMode::None,
-            signal: Signal::None,
+            signal: VecDeque::new(),
             undo_stack: Vec::new(),
             // script_manager: ScriptManager::new()?,
             editor_state: EditorState::Editing,
@@ -396,6 +397,49 @@ impl Editor {
         }
     }
 
+    /// Collects `entity` and all of its descendants into a flat list of [`SceneEntity`]s
+    /// (root-first, DFS), plus a map of `child_label -> parent_label` for the subtree.
+    pub(crate) fn collect_entity_subtree(
+        world: &World,
+        entity: hecs::Entity,
+        registry: &ComponentRegistry,
+    ) -> (Vec<SceneEntity>, HashMap<Label, Label>) {
+        let mut entities: Vec<SceneEntity> = Vec::new();
+        let mut parent_map: HashMap<Label, Label> = HashMap::new();
+
+        fn visit(
+            world: &World,
+            entity: hecs::Entity,
+            registry: &ComponentRegistry,
+            parent_label: Option<&Label>,
+            entities: &mut Vec<SceneEntity>,
+            parent_map: &mut HashMap<Label, Label>,
+        ) {
+            let Some(scene_entity) = SceneEntity::from_world(world, entity, registry) else {
+                return;
+            };
+
+            if let Some(pl) = parent_label {
+                parent_map.insert(scene_entity.label.clone(), pl.clone());
+            }
+
+            let own_label = scene_entity.label.clone();
+            entities.push(scene_entity);
+
+            let children: Vec<hecs::Entity> = world
+                .get::<&Children>(entity)
+                .map(|c| c.children().to_vec())
+                .unwrap_or_default();
+
+            for child in children {
+                visit(world, child, registry, Some(&own_label), entities, parent_map);
+            }
+        }
+
+        visit(world, entity, registry, None, &mut entities, &mut parent_map);
+        (entities, parent_map)
+    }
+
     fn double_key_pressed(&mut self, key: KeyCode) -> bool {
         let now = Instant::now();
 
@@ -890,6 +934,9 @@ impl Editor {
             .and_then(|stem| stem.to_str())
             .ok_or_else(|| anyhow::anyhow!("Scene file name is invalid"))?;
 
+        {
+            ASSET_REGISTRY.write().flush_everything();
+        }
         self.queue_scene_load_by_name(scene_name)?;
         info!("Queued scene '{}' for loading", scene_name);
         Ok(())
@@ -997,7 +1044,7 @@ impl Editor {
                     ui.separator();
                     if matches!(self.editor_state, EditorState::Playing) {
                         if ui.button("Stop").clicked() {
-                            self.signal = Signal::StopPlaying;
+                            self.signal.push_back(Signal::StopPlaying);
                         }
                     } else if ui.button("Play").clicked() {
                         // run prechecks to ensure a starting camera exists and stuff
@@ -1011,7 +1058,7 @@ impl Editor {
                         if !found_starting {
                             fatal!("Unable to start play mode: No initial camera set for this scene");
                         } else {
-                            self.signal = Signal::Play;
+                            self.signal.push_back(Signal::Play);
                         }
                     }
                     ui.menu_button("Export", |ui| {
@@ -1056,51 +1103,45 @@ impl Editor {
                     ui.menu_button("Edit", |ui| {
                     if ui.button("Copy").clicked() {
                         if let Some(entity) = &self.selected_entity {
-                            let Ok(label) = self.world.get::<&Label>(*entity) else {
+                            let entity = *entity;
+                            let (entities, parent_map) = Editor::collect_entity_subtree(
+                                self.world.as_ref(),
+                                entity,
+                                &self.component_registry,
+                            );
+                            if entities.is_empty() {
                                 warn!("Unable to copy entity: Unable to obtain label");
-                                return;
-                            };
-
-                            let mut components = self
-                                .component_registry
-                                .extract_all_components(self.world.as_ref(), *entity);
-                            components.retain(|component| {
-                                self.component_registry
-                                    .id_for_component(component.as_ref())
-                                    .and_then(|id| {
-                                        self.component_registry.get_descriptor_by_numeric_id(id)
-                                    })
-                                    .map(|desc| {
-                                        desc.fqtn != "dropbear_engine::entity::EntityTransform"
-                                    })
-                                    .unwrap_or(true)
-                            });
-                            let s_entity = SceneEntity {
-                                label: Label::new(label.as_str()),
-                                components,
-                                entity_id: None,
-                            };
-                            self.signal = Signal::Copy(s_entity);
-
-                            info!("Copied selected entity!");
+                            } else {
+                                self.signal.retain(|s| !matches!(s, Signal::Copy(_, _)));
+                                self.signal.push_back(Signal::Copy(entities, parent_map));
+                                info!("Copied selected entity!");
+                            }
                         } else {
                             warn!("Unable to copy entity: None selected");
                         }
                     }
 
                     if ui.button("Paste").clicked() {
-                        match &self.signal {
-                            Signal::Copy(entity) => {
-                                self.signal = Signal::Paste(entity.clone());
+                        let clipboard = self.signal.iter().find_map(|s| {
+                            if let Signal::Copy(entities, parent_map) = s {
+                                Some((entities.clone(), parent_map.clone()))
+                            } else {
+                                None
+                            }
+                        });
+
+                        match clipboard {
+                            Some((entities, parent_map)) => {
+                                self.signal.push_back(Signal::Paste(entities, parent_map));
                             }
-                            _ => {
+                            None => {
                                 warn!("Unable to paste: You haven't selected anything!");
                             }
                         }
                     }
 
                     if ui.button("Undo").clicked() {
-                        self.signal = Signal::Undo;
+                        self.signal.push_back(Signal::Undo);
                     }
                     ui.label("Redo");
                     });
@@ -1151,6 +1192,13 @@ impl Editor {
                     }
                     });
 
+                    ui.menu_button("Assets", |ui| {
+                        if ui.button("Flush unused assets").clicked() {
+                            let mut asset = ASSET_REGISTRY.write();
+                            let count = asset.flush_unused();
+                            success!("Flushed {} unused assets", count);
+                        }
+                    });
                     ui.menu_button("Help", |ui| {
                     if ui.button("Show AppData folder").clicked() {
                         match app_dirs2::app_root(app_dirs2::AppDataType::UserData, &APP_INFO) {
@@ -1168,18 +1216,6 @@ impl Editor {
                         log::debug!("Requested nerd stats window");
 
                         self.nerd_stats.write().show_window = true;
-
-                        // let window_data = DropbearWindowBuilder::new()
-                        //     .with_attributes(
-                        //         WindowAttributes::default()
-                        //             .with_title("Nerd Stats")
-                        //             .with_inner_size(PhysicalSize::new(500, 600))
-                        //     )
-                        //     .add_scene_with_input(self.nerd_stats.clone(), "nerd_stats")
-                        //     .set_initial_scene("nerd_stats")
-                        //     .build();
-
-                        // self.scene_command = SceneCommand::RequestWindow(window_data);
                     }
 
                     if ui.button("About").clicked() {
@@ -1251,7 +1287,7 @@ impl Editor {
                         ui.add_enabled_ui(can_play, |ui| {
                             if ui.button("⏹").clicked() {
                                 log::debug!("Menu button Stop button pressed");
-                                self.signal = Signal::StopPlaying;
+                                self.signal.push_back(Signal::StopPlaying);
                             }
                         });
 
@@ -1272,7 +1308,7 @@ impl Editor {
                                 if !found_starting {
                                     fatal!("Unable to start play mode: No initial camera set for this scene");
                                 } else {
-                                    self.signal = Signal::Play;
+                                    self.signal.push_back(Signal::Play);
                                 }
                             }
                         });
@@ -1768,26 +1804,17 @@ impl Editor {
 }
 
 /// Describes an action that is undoable
-#[derive(Debug)]
 pub enum UndoableAction {
     /// A change in transform. The entity + the old transform. Undoing will revert the transform
     Transform(hecs::Entity, Transform),
     /// A change in EntityTransform. The entity + the old transform. Undoing will revert the transform
     EntityTransform(hecs::Entity, EntityTransform),
-    #[allow(dead_code)] // don't know why its considered dead code, todo: check the cause
-    /// A spawn of the entity. Undoing will delete the entity
-    Spawn(hecs::Entity),
     /// A change of label of the entity. Undoing will revert its label
     Label(hecs::Entity, String),
     RemoveStartingCamera(Entity),
 }
 
 impl UndoableAction {
-    pub fn push_to_undo(undo_stack: &mut Vec<UndoableAction>, action: Self) {
-        undo_stack.push(action);
-        // log::debug!("Undo Stack contents: {:#?}", undo_stack);
-    }
-
     pub fn undo(&self, world: &mut World) -> anyhow::Result<()> {
         match self {
             UndoableAction::Transform(entity, transform) => {
@@ -1808,14 +1835,6 @@ impl UndoableAction {
                     Err(anyhow::anyhow!("Could not find an entity to query"))
                 }
             }
-            UndoableAction::Spawn(entity) => {
-                if world.despawn(*entity).is_ok() {
-                    log::debug!("Undid spawn by despawning entity {:?}", entity);
-                    Ok(())
-                } else {
-                    Err(anyhow::anyhow!("Failed to despawn entity {:?}", entity))
-                }
-            }
             UndoableAction::Label(entity, original_label) => {
                 if let Ok(label) = world.query_one_mut::<&mut Label>(*entity) {
                     label.set(original_label.clone());
@@ -1846,8 +1865,10 @@ impl UndoableAction {
 pub enum Signal {
     #[default]
     None,
-    Copy(SceneEntity),
-    Paste(SceneEntity),
+    /// Carries the entity subtree to be pasted: a flat list of entities (root first, DFS order)
+    /// and a map of child_label -> parent_label within the subtree.
+    Copy(Vec<SceneEntity>, HashMap<Label, Label>),
+    Paste(Vec<SceneEntity>, HashMap<Label, Label>),
     AssetCopy {
         source: PathBuf,
         division: AssetDivision,
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 030d79a..782d7bd 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -178,11 +178,11 @@ impl Scene for Editor {
                     self.scene_command = SceneCommand::SetAntialiasing(desired);
                     self.pending_aa_reload = Some(desired);
                 }
-            } else if self.pending_aa_reload.is_some() && matches!(self.signal, Signal::None) {
+            } else if self.pending_aa_reload.is_some() && self.signal.is_empty() {
                 log::debug!("Anti aliasing mode applied, reloading WGPU data");
-                self.signal = Signal::ReloadWGPUData {
+                self.signal.push_back(Signal::ReloadWGPUData {
                     skybox_texture: None,
-                };
+                });
                 self.pending_aa_reload = None;
             }
         }
diff --git a/crates/eucalyptus-editor/src/editor/ui/inspector.rs b/crates/eucalyptus-editor/src/editor/ui/inspector.rs
new file mode 100644
index 0000000..067596c
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/ui/inspector.rs
@@ -0,0 +1,21 @@
+use egui::Ui;
+use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer};
+use crate::editor::page::EditorTabVisibility;
+
+pub struct UIInspector {
+
+}
+
+impl EditorTabDock for UIInspector {
+    fn desc() -> EditorTabDockDescriptor {
+        EditorTabDockDescriptor {
+            id: "Widget Inspector",
+            title: "Widget Inspector".to_string(),
+            visibility: EditorTabVisibility::UIEditor,
+        }
+    }
+
+    fn display(_viewer: &mut EditorTabViewer<'_>, ui: &mut Ui) {
+        ui.label("Not implemented yet.");
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/ui/mod.rs b/crates/eucalyptus-editor/src/editor/ui/mod.rs
index 35266c3..231da78 100644
--- a/crates/eucalyptus-editor/src/editor/ui/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/ui/mod.rs
@@ -7,6 +7,8 @@ use kino_ui::camera::Camera2D;
 use wgpu::TextureFormat;
 
 pub mod viewport;
+pub mod inspector;
+pub mod widget_tree;
 
 pub struct UiEditor {
     pub grid_pipeline: Option<UIGridPipeline>,
diff --git a/crates/eucalyptus-editor/src/editor/ui/viewport.rs b/crates/eucalyptus-editor/src/editor/ui/viewport.rs
index 6bbc39b..b9bf4f1 100644
--- a/crates/eucalyptus-editor/src/editor/ui/viewport.rs
+++ b/crates/eucalyptus-editor/src/editor/ui/viewport.rs
@@ -3,13 +3,13 @@ use glam::Vec2;
 use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer};
 use crate::editor::page::EditorTabVisibility;
 
-pub struct UIViewport;
+pub struct UICanvas;
 
-impl EditorTabDock for UIViewport {
+impl EditorTabDock for UICanvas {
     fn desc() -> EditorTabDockDescriptor {
         EditorTabDockDescriptor {
-            id: "UI Viewport",
-            title: "UI Viewport".to_string(),
+            id: "UI Viewport", // kept for some reason, change on release
+            title: "UI Canvas".to_string(),
             visibility: EditorTabVisibility::UIEditor,
         }
     }
@@ -36,17 +36,34 @@ impl EditorTabDock for UIViewport {
             let viewport_pixels = Vec2::new(available_size.x * ppp, available_size.y * ppp);
 
             if response.hovered() {
-                let scroll_y = ui.ctx().input(|i| i.raw_scroll_delta.y);
-                if scroll_y.abs() > 0.0 {
-                    let zoom_delta = scroll_y * 0.0025;
-                    viewer.ui_editor.zoom_by(zoom_delta);
-                }
+                ui.ctx().input(|i| {
+                    // pinch
+                    let zoom_delta = i.zoom_delta();
+                    if (zoom_delta - 1.0).abs() > f32::EPSILON {
+                        viewer.ui_editor.zoom_by(zoom_delta - 1.0);
+                    }
+
+                    // scroll wheel
+                    let scroll_y = i.raw_scroll_delta.y;
+                    let is_trackpad_panning = i.smooth_scroll_delta != egui::Vec2::ZERO;
+                    if scroll_y.abs() > 0.0 && !is_trackpad_panning {
+                        viewer.ui_editor.zoom_by(scroll_y * 0.0025);
+                    }
+
+                    // trackpad pan
+                    let smooth_scroll = i.smooth_scroll_delta;
+                    if smooth_scroll != egui::Vec2::ZERO {
+                        let delta_pixels = Vec2::new(smooth_scroll.x * ppp, smooth_scroll.y * ppp);
+                        viewer.ui_editor.pan_by_pixels(delta_pixels);
+                    }
+                });
             }
 
             let is_middle_down = ui
                 .ctx()
                 .input(|i| i.pointer.button_down(egui::PointerButton::Middle));
 
+            // middle mouse button
             if response.hovered() && is_middle_down {
                 let pointer_delta_points = ui.ctx().input(|i| i.pointer.delta());
                 if pointer_delta_points != egui::Vec2::ZERO {
diff --git a/crates/eucalyptus-editor/src/editor/ui/widget_tree.rs b/crates/eucalyptus-editor/src/editor/ui/widget_tree.rs
new file mode 100644
index 0000000..bdc0a65
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/ui/widget_tree.rs
@@ -0,0 +1,21 @@
+use egui::Ui;
+use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer};
+use crate::editor::page::EditorTabVisibility;
+
+pub struct UIWidgetTree {
+
+}
+
+impl EditorTabDock for UIWidgetTree {
+    fn desc() -> EditorTabDockDescriptor {
+        EditorTabDockDescriptor {
+            id: "Widget Tree",
+            title: "Widget Tree".to_string(),
+            visibility: EditorTabVisibility::UIEditor,
+        }
+    }
+
+    fn display(_viewer: &mut EditorTabViewer<'_>, ui: &mut Ui) {
+        ui.label("Not implemented yet.");
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/viewport.rs b/crates/eucalyptus-editor/src/editor/viewport.rs
index 2d22d10..01ed176 100644
--- a/crates/eucalyptus-editor/src/editor/viewport.rs
+++ b/crates/eucalyptus-editor/src/editor/viewport.rs
@@ -6,6 +6,7 @@ use eucalyptus_core::camera::CameraComponent;
 use eucalyptus_core::utils::ViewportMode;
 use glam::DVec3;
 use transform_gizmo_egui::{GizmoConfig, GizmoExt, GizmoOrientation};
+use eucalyptus_core::hierarchy::EntityTransformExt;
 use crate::editor::page::EditorTabVisibility;
 
 impl<'a> EditorTabViewer<'a> {
@@ -21,8 +22,8 @@ impl<'a> EditorTabViewer<'a> {
         let desired_width = (available_size.x * pixels_per_point).max(1.0).round() as u32;
         let desired_height = (available_size.y * pixels_per_point).max(1.0).round() as u32;
         if self.tex_size.width != desired_width || self.tex_size.height != desired_height {
-            if matches!(*self.signal, Signal::None) {
-                *self.signal = Signal::UpdateViewportSize((desired_width as f32, desired_height as f32));
+            if self.signal.is_empty() {
+                self.signal.push_back(Signal::UpdateViewportSize((desired_width as f32, desired_height as f32)));
             }
         }
 
@@ -62,9 +63,6 @@ impl<'a> EditorTabViewer<'a> {
 
         let snapping = ui.input(|input| input.modifiers.shift);
 
-        // Note to self: fuck you >:(
-        // Note to self: ok wow thats pretty rude im trying my best >﹏<
-        // Note to self: finally holy shit i got it working
         let active_cam = self.active_camera.lock();
         if let Some(active_camera) = *active_cam {
             let camera_data = {
@@ -93,16 +91,16 @@ impl<'a> EditorTabViewer<'a> {
                 });
             }
         }
+
         if !matches!(self.viewport_mode, ViewportMode::None)
             && let Some(entity_id) = self.selected_entity
         {
             let mut handled = false;
             let mut updated_light_transform: Option<Transform> = None;
 
-            if let Ok(entity_transform) = self
+            if let Ok(mut entity_transform) = self
                 .world
-                .query_one::<&mut EntityTransform>(*entity_id)
-                .get()
+                .get::<&mut EntityTransform>(*entity_id)
             {
                 let was_focused = cfg.is_focused;
                 cfg.is_focused = self.gizmo.is_focused();
@@ -111,7 +109,7 @@ impl<'a> EditorTabViewer<'a> {
                     cfg.entity_transform_original = Some(*entity_transform);
                 }
 
-                let synced = entity_transform.sync();
+                let synced = entity_transform.propagate(&self.world, *entity_id);
                 let gizmo_transform =
                     transform_gizmo_egui::math::Transform::from_scale_rotation_translation(
                         synced.scale,
@@ -126,71 +124,44 @@ impl<'a> EditorTabViewer<'a> {
                     let new_synced_rot: glam::DQuat = new_transform.rotation.into();
                     let new_synced_scale: glam::DVec3 = new_transform.scale.into();
 
-                    match *self.gizmo_orientation {
-                        GizmoOrientation::Global => {
-                            let local = *entity_transform.local();
-
-                            let safe_local_scale = glam::DVec3::new(
-                                if local.scale.x.abs() < 1e-6 {
-                                    1.0
-                                } else {
-                                    local.scale.x
-                                },
-                                if local.scale.y.abs() < 1e-6 {
-                                    1.0
-                                } else {
-                                    local.scale.y
-                                },
-                                if local.scale.z.abs() < 1e-6 {
-                                    1.0
-                                } else {
-                                    local.scale.z
-                                },
-                            );
+                    let prev_world_pos = synced.position;
+                    let prev_world_rot = synced.rotation;
+                    let prev_world_scale = synced.scale;
 
-                            let new_world_scale = new_synced_scale / safe_local_scale;
-                            let new_world_rot = new_synced_rot * local.rotation.inverse();
+                    let safe = |v: f64| if v.abs() < 1e-9 { 1.0 } else { v };
 
-                            let scaled_local_pos = local.position * new_world_scale;
-                            let rotated_local_pos = new_world_rot * scaled_local_pos;
-                            let new_world_pos = new_synced_pos - rotated_local_pos;
+                    let delta_pos   = new_synced_pos - prev_world_pos;
+                    let delta_rot   = new_synced_rot * prev_world_rot.inverse();
+                    let delta_scale = glam::DVec3::new(
+                        new_synced_scale.x / safe(prev_world_scale.x),
+                        new_synced_scale.y / safe(prev_world_scale.y),
+                        new_synced_scale.z / safe(prev_world_scale.z),
+                    );
 
-                            let world_transform = entity_transform.world_mut();
-                            world_transform.position = new_world_pos;
-                            world_transform.rotation = new_world_rot;
-                            world_transform.scale = new_world_scale;
+                    match *self.gizmo_orientation {
+                        GizmoOrientation::Global => {
+                            let world = entity_transform.world_mut();
+                            world.position += delta_pos;
+                            world.rotation  = delta_rot * world.rotation;
+                            world.scale    *= delta_scale;
                         }
                         GizmoOrientation::Local => {
-                            let world_transform = entity_transform.world();
-                            let world_scale = world_transform.scale;
-                            let world_rot = world_transform.rotation;
-                            let world_pos = world_transform.position;
-
-                            let safe_world_scale = glam::DVec3::new(
-                                if world_scale.x.abs() < 1e-6 {
-                                    1.0
-                                } else {
-                                    world_scale.x
-                                },
-                                if world_scale.y.abs() < 1e-6 {
-                                    1.0
-                                } else {
-                                    world_scale.y
-                                },
-                                if world_scale.z.abs() < 1e-6 {
-                                    1.0
-                                } else {
-                                    world_scale.z
-                                },
+                            let world_rot   = entity_transform.world().rotation;
+                            let world_scale = entity_transform.world().scale;
+
+                            let safe_ws = glam::DVec3::new(
+                                safe(world_scale.x),
+                                safe(world_scale.y),
+                                safe(world_scale.z),
                             );
 
-                            let local_transform = entity_transform.local_mut();
-                            local_transform.scale = new_synced_scale / safe_world_scale;
-                            local_transform.rotation = world_rot.inverse() * new_synced_rot;
+                            let local = entity_transform.local_mut();
+
+                            local.position += world_rot.inverse() * delta_pos / safe_ws;
+
+                            local.rotation  = world_rot.inverse() * delta_rot * world_rot * local.rotation;
 
-                            let delta_pos = new_synced_pos - world_pos;
-                            let unrotated_delta = world_rot.inverse() * delta_pos;
-                            local_transform.position = unrotated_delta / safe_world_scale;
+                            local.scale    *= delta_scale;
                         }
                     }
 
@@ -200,14 +171,12 @@ impl<'a> EditorTabViewer<'a> {
                 if was_focused && !cfg.is_focused {
                     if let Some(original) = cfg.entity_transform_original {
                         if original != *entity_transform {
-                            UndoableAction::push_to_undo(
-                                self.undo_stack,
-                                UndoableAction::EntityTransform(*entity_id, original),
-                            );
+                            self.undo_stack.push(UndoableAction::EntityTransform(*entity_id, original));
                             log::debug!("Pushed entity transform action to stack");
                         }
                     }
                 }
+
                 handled = true;
             }
 
@@ -233,19 +202,18 @@ impl<'a> EditorTabViewer<'a> {
                     {
                         transform.position = new_transform.translation.into();
                         transform.rotation = new_transform.rotation.into();
-                        transform.scale = new_transform.scale.into();
+                        transform.scale    = new_transform.scale.into();
                         updated_light_transform = Some(*transform);
                     }
 
                     if was_focused && !cfg.is_focused {
                         let transform_changed = cfg.old_pos.position != transform.position
                             || cfg.old_pos.rotation != transform.rotation
-                            || cfg.old_pos.scale != transform.scale;
+                            || cfg.old_pos.scale    != transform.scale;
 
                         if transform_changed {
-                            UndoableAction::push_to_undo(
-                                self.undo_stack,
-                                UndoableAction::Transform(*entity_id, cfg.old_pos),
+                            self.undo_stack.push(
+                                UndoableAction::Transform(*entity_id, cfg.old_pos)
                             );
                             log::debug!("Pushed transform action to stack");
                         }
@@ -255,8 +223,8 @@ impl<'a> EditorTabViewer<'a> {
 
             if let Some(updated_transform) = updated_light_transform {
                 if let Ok(mut light) = self.world.get::<&mut Light>(*entity_id) {
-                    let forward = DVec3::new(0.0, 0.0, -1.0);
-                    light.component.position = updated_transform.position;
+                    let forward = DVec3::new(0.0, -1.0, 0.0);
+                    light.component.position  = updated_transform.position;
                     light.component.direction =
                         (updated_transform.rotation * forward).normalize_or_zero();
                 }
@@ -279,4 +247,4 @@ impl EditorTabDock for ViewportDock {
     fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) {
         viewer.viewport_tab(ui);
     }
-}
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/lib.rs b/crates/eucalyptus-editor/src/lib.rs
index c304208..6dc8ba6 100644
--- a/crates/eucalyptus-editor/src/lib.rs
+++ b/crates/eucalyptus-editor/src/lib.rs
@@ -13,8 +13,10 @@ pub mod utils;
 pub use redback_runtime as runtime;
 
 use crate::editor::{
-    EditorTabRegistry, asset_viewer::AssetViewerDock, build_console::BuildConsoleDock, dock::ConsoleDock, entity_list::EntityListDock, resource::ResourceInspectorDock, ui::viewport::UIViewport, viewport::ViewportDock
+    EditorTabRegistry, asset_viewer::AssetViewerDock, build_console::BuildConsoleDock, dock::ConsoleDock, entity_list::EntityListDock, resource::ResourceInspectorDock, ui::viewport::UICanvas, viewport::ViewportDock
 };
+use crate::editor::ui::inspector::UIInspector;
+use crate::editor::ui::widget_tree::UIWidgetTree;
 
 dropbear_engine::features! {
     pub mod features {
@@ -29,5 +31,7 @@ pub fn register_docks(registry: &mut EditorTabRegistry) {
     registry.register::<ResourceInspectorDock>();
     registry.register::<BuildConsoleDock>();
     registry.register::<ConsoleDock>();
-    registry.register::<UIViewport>();
+    registry.register::<UICanvas>();
+    registry.register::<UIInspector>();
+    registry.register::<UIWidgetTree>();
 }
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index d82f4f0..6c60a4b 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -3,9 +3,11 @@ use crate::spawn::{PendingSpawn, push_pending_spawn};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use egui::Align2;
 use eucalyptus_core::camera::{CameraComponent, CameraType};
+use eucalyptus_core::scene::SceneEntity;
 use eucalyptus_core::scripting::{BuildStatus, build_jvm};
-use eucalyptus_core::states::{PROJECT};
+use eucalyptus_core::states::{Label, PROJECT};
 use eucalyptus_core::{fatal, info, success, success_without_console, warn, warn_without_console};
+use std::collections::HashMap;
 use std::fs;
 use std::path::PathBuf;
 use std::sync::Arc;
@@ -17,588 +19,623 @@ pub trait SignalController {
 
 impl SignalController for Editor {
     fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> {
-        let local_signal: Option<Signal> = None;
-        let show = true;
-
-        match std::mem::replace(&mut self.signal, Signal::None) {
-            Signal::None => {
-                // returns absolutely nothing because no signal is set.
-                Ok::<(), anyhow::Error>(())
-            }
-            Signal::Copy(_) => Ok(()),
-            Signal::AssetCopy { source, division } => {
-                self.asset_clipboard = Some(AssetClipboard {
-                    source: source.clone(),
-                    division: division,
-                });
-                self.signal = Signal::None;
-                Ok(())
-            }
-            Signal::AssetPaste {
-                target_dir,
-                division,
-            } => {
-                let clipboard = self.asset_clipboard.clone();
-                if clipboard.is_none() {
-                    warn!("Nothing copied to paste");
-                    self.signal = Signal::None;
-                    return Ok(());
+        let mut requeue = vec![];
+        while let Some(signal) = self.signal.pop_front() {
+            let local_signal: Option<Signal> = None;
+            let show = true;
+
+            match signal {
+                Signal::None => {
+                    // returns absolutely nothing because no signal is set.
+                    Ok::<(), anyhow::Error>(())
                 }
-
-                let clipboard = clipboard.unwrap();
-                if clipboard.division != division {
-                    warn!("Cannot paste across different asset divisions");
-                    self.signal = Signal::None;
-                    return Ok(());
+                Signal::Copy(entities, parent_map) => {
+                    requeue.push(Signal::Copy(entities, parent_map));
+                    Ok(())
+                },
+                Signal::AssetCopy { source, division } => {
+                    self.asset_clipboard = Some(AssetClipboard {
+                        source: source.clone(),
+                        division: division,
+                    });
+                    
+                    Ok(())
                 }
+                Signal::AssetPaste {
+                    target_dir,
+                    division,
+                } => {
+                    let clipboard = self.asset_clipboard.clone();
+                    if clipboard.is_none() {
+                        warn!("Nothing copied to paste");
+                        
+                        return Ok(());
+                    }
 
-                if !clipboard.source.is_file() {
-                    warn!("Copied asset is not a file");
-                    self.signal = Signal::None;
-                    return Ok(());
-                }
+                    let clipboard = clipboard.unwrap();
+                    if clipboard.division != division {
+                        warn!("Cannot paste across different asset divisions");
+                        
+                        return Ok(());
+                    }
 
-                if !target_dir.exists() {
-                    warn!("Target directory does not exist");
-                    self.signal = Signal::None;
-                    return Ok(());
-                }
+                    if !clipboard.source.is_file() {
+                        warn!("Copied asset is not a file");
+                        
+                        return Ok(());
+                    }
 
-                let Some(file_name) = clipboard.source.file_name() else {
-                    warn!("Unable to paste: invalid file name");
-                    self.signal = Signal::None;
-                    return Ok(());
-                };
-
-                let target_path = target_dir.join(file_name);
-                if target_path.exists() {
-                    warn!("Target already exists: {}", target_path.display());
-                    self.signal = Signal::None;
-                    return Ok(());
-                }
+                    if !target_dir.exists() {
+                        warn!("Target directory does not exist");
+                        
+                        return Ok(());
+                    }
+
+                    let Some(file_name) = clipboard.source.file_name() else {
+                        warn!("Unable to paste: invalid file name");
+                        
+                        return Ok(());
+                    };
+
+                    let target_path = target_dir.join(file_name);
+                    if target_path.exists() {
+                        warn!("Target already exists: {}", target_path.display());
+                        
+                        return Ok(());
+                    }
+
+                    if let Err(err) = fs::copy(&clipboard.source, &target_path) {
+                        warn!("Unable to paste file: {}", err);
+                        
+                        return Ok(());
+                    }
 
-                if let Err(err) = fs::copy(&clipboard.source, &target_path) {
-                    warn!("Unable to paste file: {}", err);
-                    self.signal = Signal::None;
-                    return Ok(());
+                    info!("Pasted asset to {}", target_path.display());
+                    
+                    Ok(())
                 }
+                Signal::Paste(entities, parent_map) => {
+                    log::debug!("Paste requested for {} entity(ies)", entities.len());
+
+                    // Rename all entities to ensure uniqueness, tracking old→new label mapping.
+                    let mut label_remap: HashMap<Label, Label> = HashMap::new();
+                    let mut renamed: Vec<SceneEntity> = Vec::new();
+                    for mut scene_entity in entities {
+                        let new_label = Editor::unique_label_for_world(
+                            self.world.as_ref(),
+                            scene_entity.label.as_str(),
+                        );
+                        label_remap.insert(scene_entity.label.clone(), new_label.clone());
+                        scene_entity.label = new_label;
+                        renamed.push(scene_entity);
+                    }
 
-                info!("Pasted asset to {}", target_path.display());
-                self.signal = Signal::None;
-                Ok(())
-            }
-            Signal::Paste(scene_entity) => {
-                let mut scene_entity = scene_entity.clone();
-                scene_entity.label = Editor::unique_label_for_world(
-                    self.world.as_ref(),
-                    scene_entity.label.as_str(),
-                );
-                let spawn = PendingSpawn {
-                    scene_entity: scene_entity.clone(),
-                    handle: None,
-                };
-                push_pending_spawn(spawn);
-                self.signal = Signal::Copy(scene_entity.clone());
-                Ok(())
-            }
-            Signal::Delete => {
-                if let Some(sel_e) = &self.selected_entity {
-                    let is_viewport_cam =
-                        if let Ok(c) = self.world.query_one::<&CameraComponent>(*sel_e).get() {
-                            matches!(c.camera_type, CameraType::Debug)
+                    // Rebuild parent map with renamed labels.
+                    let renamed_parent_map: HashMap<Label, Label> = parent_map
+                        .into_iter()
+                        .filter_map(|(child, parent)| {
+                            let new_child = label_remap.get(&child)?.clone();
+                            let new_parent = label_remap.get(&parent)?.clone();
+                            Some((new_child, new_parent))
+                        })
+                        .collect();
+
+                    // Keep clipboard alive so the user can paste again.
+                    self.signal.push_back(Signal::Copy(renamed.clone(), renamed_parent_map.clone()));
+
+                    // Queue a PendingSpawn for each entity, with parent_label set as needed.
+                    for scene_entity in renamed {
+                        let parent_label = renamed_parent_map.get(&scene_entity.label).cloned();
+                        push_pending_spawn(PendingSpawn {
+                            scene_entity,
+                            handle: None,
+                            parent_label,
+                        });
+                    }
+                    Ok(())
+                }
+                Signal::Delete => {
+                    if let Some(sel_e) = &self.selected_entity {
+                        let is_viewport_cam =
+                            if let Ok(c) = self.world.query_one::<&CameraComponent>(*sel_e).get() {
+                                matches!(c.camera_type, CameraType::Debug)
+                            } else {
+                                false
+                            };
+                        if is_viewport_cam {
+                            warn!("You can't delete the viewport camera");
+                            
+                            Ok(())
                         } else {
-                            false
-                        };
-                    if is_viewport_cam {
-                        warn!("You can't delete the viewport camera");
-                        self.signal = Signal::None;
-                        Ok(())
+                            match self.world.despawn(*sel_e) {
+                                Ok(_) => {
+                                    info!("Decimated entity");
+                                    
+                                    Ok(())
+                                }
+                                Err(e) => {
+                                    
+                                    fatal!("Failed to delete entity: {}", e);
+                                    Err(anyhow::anyhow!(e))
+                                }
+                            }
+                        }
                     } else {
-                        match self.world.despawn(*sel_e) {
+                        // no entity has been selected, so all good
+                        Ok(())
+                    }
+                }
+                Signal::Undo => {
+                    if let Some(action) = self.undo_stack.pop() {
+                        match action.undo(&mut self.world) {
                             Ok(_) => {
-                                info!("Decimated entity");
-                                self.signal = Signal::None;
-                                Ok(())
+                                info!("Undid action");
                             }
                             Err(e) => {
-                                self.signal = Signal::None;
-                                fatal!("Failed to delete entity: {}", e);
-                                Err(anyhow::anyhow!(e))
+                                warn!("Failed to undo action: {}", e);
                             }
                         }
+                    } else {
+                        warn_without_console!("Nothing to undo");
+                        log::debug!("No undoable actions in stack");
                     }
-                } else {
-                    // no entity has been selected, so all good
+                    
                     Ok(())
                 }
-            }
-            Signal::Undo => {
-                if let Some(action) = self.undo_stack.pop() {
-                    match action.undo(&mut self.world) {
-                        Ok(_) => {
-                            info!("Undid action");
-                        }
-                        Err(e) => {
-                            warn!("Failed to undo action: {}", e);
-                        }
+                Signal::Play => {
+                    if matches!(self.editor_state, EditorState::Playing) {
+                        log::warn!("Unable to play: already in playing mode");
+                        
+                        return Err(anyhow::anyhow!("Unable to play: already in playing mode"));
                     }
-                } else {
-                    warn_without_console!("Nothing to undo");
-                    log::debug!("No undoable actions in stack");
-                }
-                self.signal = Signal::None;
-                Ok(())
-            }
-            Signal::Play => {
-                if matches!(self.editor_state, EditorState::Playing) {
-                    log::warn!("Unable to play: already in playing mode");
-                    self.signal = Signal::None;
-                    return Err(anyhow::anyhow!("Unable to play: already in playing mode"));
-                }
 
-                if matches!(self.editor_state, EditorState::Editing) {
-                    log::debug!("Project save");
-                    match self.save_project_config() {
-                        Ok(_) => {}
-                        Err(e) => {
-                            fatal!("Error saving project: {}", e);
+                    if matches!(self.editor_state, EditorState::Editing) {
+                        log::debug!("Project save");
+                        match self.save_project_config() {
+                            Ok(_) => {}
+                            Err(e) => {
+                                fatal!("Error saving project: {}", e);
+                            }
                         }
-                    }
 
-                    log::debug!("Starting build process");
-                    let (tx, rx) = crossbeam_channel::unbounded();
-                    self.progress_rx = Some(rx);
+                        log::debug!("Starting build process");
+                        let (tx, rx) = crossbeam_channel::unbounded();
+                        self.progress_rx = Some(rx);
 
-                    self.build_logs.clear();
-                    self.build_progress = 0.0;
-                    self.show_build_window = true;
-                    self.last_build_error = None;
+                        self.build_logs.clear();
+                        self.build_progress = 0.0;
+                        self.show_build_window = true;
+                        self.last_build_error = None;
 
-                    let project_root = {
-                        let cfg = PROJECT.read();
-                        cfg.project_path.clone()
-                    };
+                        let project_root = {
+                            let cfg = PROJECT.read();
+                            cfg.project_path.clone()
+                        };
 
-                    let project_root = project_root.to_path_buf();
-                    let status_tx = tx.clone();
+                        let project_root = project_root.to_path_buf();
+                        let status_tx = tx.clone();
 
-                    let handle = graphics
-                        .future_queue
-                        .push(async move { build_jvm(project_root, status_tx).await });
+                        let handle = graphics
+                            .future_queue
+                            .push(async move { build_jvm(project_root, status_tx).await });
 
-                    log::debug!(
-                        "Pushed future to future_queue, received handle: {:?}",
-                        handle
-                    );
+                        log::debug!(
+                            "Pushed future to future_queue, received handle: {:?}",
+                            handle
+                        );
 
-                    self.handle_created = Some(handle);
+                        self.handle_created = Some(handle);
 
-                    self.editor_state = EditorState::Building;
-                    log::debug!("Set editor state to EditorState::Building");
-                }
+                        self.editor_state = EditorState::Building;
+                        log::debug!("Set editor state to EditorState::Building");
+                    }
 
-                if matches!(self.editor_state, EditorState::Building) {
-                    #[cfg(not(target_os = "macos"))]
-                    let ctrl_pressed = self
-                        .input_state
-                        .pressed_keys
-                        .contains(&KeyCode::ControlLeft)
-                        || self
+                    if matches!(self.editor_state, EditorState::Building) {
+                        #[cfg(not(target_os = "macos"))]
+                        let ctrl_pressed = self
+                            .input_state
+                            .pressed_keys
+                            .contains(&KeyCode::ControlLeft)
+                            || self
                             .input_state
                             .pressed_keys
                             .contains(&KeyCode::ControlRight);
-                    #[cfg(target_os = "macos")]
-                    let ctrl_pressed = self.input_state.pressed_keys.contains(&KeyCode::SuperLeft)
-                        || self.input_state.pressed_keys.contains(&KeyCode::SuperRight);
-
-                    let alt_pressed = self.input_state.pressed_keys.contains(&KeyCode::AltLeft)
-                        || self.input_state.pressed_keys.contains(&KeyCode::AltRight);
-
-                    // Ctrl+Alt+P skips build process and starts running, such as if using cached jar
-                    if ctrl_pressed
-                        && alt_pressed
-                        && self.input_state.pressed_keys.contains(&KeyCode::KeyP)
-                    {
-                        if let Some(handle) = self.handle_created {
-                            log::debug!("Cancelling build task due to manual intervention");
-                            graphics.future_queue.cancel(&handle);
-                        } else {
-                            log::warn!("No handle was created during this time. Weird...")
-                        }
+                        #[cfg(target_os = "macos")]
+                        let ctrl_pressed = self.input_state.pressed_keys.contains(&KeyCode::SuperLeft)
+                            || self.input_state.pressed_keys.contains(&KeyCode::SuperRight);
+
+                        let alt_pressed = self.input_state.pressed_keys.contains(&KeyCode::AltLeft)
+                            || self.input_state.pressed_keys.contains(&KeyCode::AltRight);
+
+                        // Ctrl+Alt+P skips build process and starts running, such as if using cached jar
+                        if ctrl_pressed
+                            && alt_pressed
+                            && self.input_state.pressed_keys.contains(&KeyCode::KeyP)
+                        {
+                            if let Some(handle) = self.handle_created {
+                                log::debug!("Cancelling build task due to manual intervention");
+                                graphics.future_queue.cancel(&handle);
+                            } else {
+                                log::warn!("No handle was created during this time. Weird...")
+                            }
 
-                        let project_root = {
-                            let cfg = PROJECT.read();
-                            cfg.project_path.clone()
-                        };
-                        let libs_dir = project_root.join("build").join("libs");
-                        if !libs_dir.exists() {
-                            let err =
-                                "Build succeeded but 'build/libs' directory is missing".to_string();
-                            return Err(anyhow::anyhow!(err));
-                        }
+                            let project_root = {
+                                let cfg = PROJECT.read();
+                                cfg.project_path.clone()
+                            };
+                            let libs_dir = project_root.join("build").join("libs");
+                            if !libs_dir.exists() {
+                                let err =
+                                    "Build succeeded but 'build/libs' directory is missing".to_string();
+                                return Err(anyhow::anyhow!(err));
+                            }
 
-                        let jar_files: Vec<PathBuf> = std::fs::read_dir(&libs_dir)?
-                            .filter_map(|entry| entry.ok().map(|e| e.path()))
-                            .filter(|path| {
-                                path.extension()
-                                    .map_or(false, |ext| ext.eq_ignore_ascii_case("jar"))
-                                    && !path
+                            let jar_files: Vec<PathBuf> = std::fs::read_dir(&libs_dir)?
+                                .filter_map(|entry| entry.ok().map(|e| e.path()))
+                                .filter(|path| {
+                                    path.extension()
+                                        .map_or(false, |ext| ext.eq_ignore_ascii_case("jar"))
+                                        && !path
                                         .file_name()
                                         .unwrap_or_default()
                                         .to_string_lossy()
                                         .contains("-sources")
-                                    && !path
+                                        && !path
                                         .file_name()
                                         .unwrap_or_default()
                                         .to_string_lossy()
                                         .contains("-javadoc")
-                            })
-                            .collect();
+                                })
+                                .collect();
 
-                        if jar_files.is_empty() {
-                            let err = "No JAR artifact found in 'build/libs'".to_string();
-                            return Err(anyhow::anyhow!(err));
-                        }
+                            if jar_files.is_empty() {
+                                let err = "No JAR artifact found in 'build/libs'".to_string();
+                                return Err(anyhow::anyhow!(err));
+                            }
 
-                        let fat_jar = jar_files.iter().find(|path| {
-                            path.file_name()
-                                .and_then(|n| n.to_str())
-                                .map_or(false, |name| name.contains("-all"))
-                        });
+                            let fat_jar = jar_files.iter().find(|path| {
+                                path.file_name()
+                                    .and_then(|n| n.to_str())
+                                    .map_or(false, |name| name.contains("-all"))
+                            });
 
-                        let jar_path = if let Some(fat) = fat_jar {
-                            fat.clone()
-                        } else {
-                            jar_files
-                                .into_iter()
-                                .max_by_key(|path| {
-                                    std::fs::metadata(path).map(|m| m.len()).unwrap_or(0)
-                                })
-                                .unwrap()
-                        };
+                            let jar_path = if let Some(fat) = fat_jar {
+                                fat.clone()
+                            } else {
+                                jar_files
+                                    .into_iter()
+                                    .max_by_key(|path| {
+                                        std::fs::metadata(path).map(|m| m.len()).unwrap_or(0)
+                                    })
+                                    .unwrap()
+                            };
 
-                        info!("Using cached JAR: {}", jar_path.display());
+                            info!("Using cached JAR: {}", jar_path.display());
 
-                        self.show_build_window = false;
-                        self.signal = Signal::None;
+                            self.show_build_window = false;
+                            
 
-                        self.load_play_mode()?;
-                        return Ok(());
-                    }
+                            self.load_play_mode()?;
+                            return Ok(());
+                        }
 
-                    let mut local_handle_exchanged: Option<anyhow::Result<PathBuf>> = None;
-                    if let Some(rx) = &self.progress_rx {
-                        while let Ok(status) = rx.try_recv() {
-                            match status {
-                                BuildStatus::Started => {
-                                    self.build_logs.push("Build started...".to_string());
-                                    self.build_progress = 0.1;
-                                    log::info!("Build started");
-                                }
-                                BuildStatus::Building(msg) => {
-                                    log::info!("[BUILD] {}", msg);
-                                    self.build_logs.push(msg.clone());
-                                    self.build_progress = (self.build_progress + 0.01).min(0.9);
-                                }
-                                BuildStatus::Completed => {
-                                    self.build_logs
-                                        .push("Build completed successfully!".to_string());
-                                    self.build_progress = 1.0;
-                                    success_without_console!("Build completed");
-                                    log::info!("Build completed successfully!");
-
-                                    if let Some(handle) = self.handle_created {
-                                        if let Some(result) = graphics
-                                            .future_queue
-                                            .exchange_owned_as::<anyhow::Result<PathBuf>>(&handle)
-                                        {
-                                            local_handle_exchanged = Some(result);
+                        let mut local_handle_exchanged: Option<anyhow::Result<PathBuf>> = None;
+                        if let Some(rx) = &self.progress_rx {
+                            while let Ok(status) = rx.try_recv() {
+                                match status {
+                                    BuildStatus::Started => {
+                                        self.build_logs.push("Build started...".to_string());
+                                        self.build_progress = 0.1;
+                                        log::info!("Build started");
+                                    }
+                                    BuildStatus::Building(msg) => {
+                                        log::info!("[BUILD] {}", msg);
+                                        self.build_logs.push(msg.clone());
+                                        self.build_progress = (self.build_progress + 0.01).min(0.9);
+                                    }
+                                    BuildStatus::Completed => {
+                                        self.build_logs
+                                            .push("Build completed successfully!".to_string());
+                                        self.build_progress = 1.0;
+                                        success_without_console!("Build completed");
+                                        log::info!("Build completed successfully!");
+
+                                        if let Some(handle) = self.handle_created {
+                                            if let Some(result) = graphics
+                                                .future_queue
+                                                .exchange_owned_as::<anyhow::Result<PathBuf>>(&handle)
+                                            {
+                                                local_handle_exchanged = Some(result);
+                                            }
+                                        } else {
+                                            
+                                            self.show_build_window = false;
+                                            self.editor_state = EditorState::Editing;
                                         }
-                                    } else {
-                                        self.signal = Signal::None;
-                                        self.show_build_window = false;
-                                        self.editor_state = EditorState::Editing;
                                     }
-                                }
-                                BuildStatus::Failed(_e) => {
-                                    let error_msg = format!("Build failed, check logs");
-                                    self.build_logs.push(error_msg.clone());
+                                    BuildStatus::Failed(_e) => {
+                                        let error_msg = format!("Build failed, check logs");
+                                        self.build_logs.push(error_msg.clone());
 
-                                    self.build_progress = 0.0;
-                                    fatal!("Failed to build gradle, check logs");
+                                        self.build_progress = 0.0;
+                                        fatal!("Failed to build gradle, check logs");
 
-                                    self.signal = Signal::None;
-                                    self.show_build_window = false;
-                                    self.editor_state = EditorState::Editing;
-                                    // self.dock_state
-                                    //     .push_to_focused_leaf(EditorTab::ErrorConsole); // getting too problematic
+                                        
+                                        self.show_build_window = false;
+                                        self.editor_state = EditorState::Editing;
+                                        // self.dock_state
+                                        //     .push_to_focused_leaf(EditorTab::ErrorConsole); // getting too problematic
+                                    }
                                 }
                             }
                         }
-                    }
-
-                    if self.show_build_window {
-                        let mut window_open = true;
-                        egui::Window::new("Building Project")
-                            .collapsible(false)
-                            .resizable(false)
-                            .fixed_size([500.0, 400.0])
-                            .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
-                            .open(&mut window_open)
-                            .show(&graphics.get_egui_context(), |ui| {
-                                ui.vertical_centered(|ui| {
-                                    ui.heading("Gradle Build Progress");
-                                    ui.add_space(10.0);
-
-                                    let progress_bar = egui::ProgressBar::new(self.build_progress)
-                                        .show_percentage()
-                                        .animate(true);
-                                    ui.add(progress_bar);
-
-                                    ui.add_space(15.0);
-                                    ui.separator();
-                                    ui.add_space(5.0);
-
-                                    ui.heading("Build Log");
-                                    ui.add_space(5.0);
-
-                                    egui::ScrollArea::vertical()
-                                        .stick_to_bottom(true)
-                                        .max_height(200.0)
-                                        .auto_shrink([false, false])
-                                        .show(ui, |ui| {
-                                            for log_line in &self.build_logs {
-                                                ui.label(
-                                                    egui::RichText::new(log_line)
-                                                        .family(egui::FontFamily::Monospace)
-                                                        .size(12.0),
-                                                );
-                                            }
-
-                                            if !self.build_logs.is_empty() {
-                                                ui.add_space(10.0);
-                                                ui.label(
-                                                    egui::RichText::new(format!(
-                                                        "Total log entries: {}",
-                                                        self.build_logs.len()
-                                                    ))
-                                                    .italics()
-                                                    .color(egui::Color32::GRAY),
-                                                );
-                                                ui.label(
-                                                    "Tip: Press Ctrl+Alt+P to skip build and start running",
-                                                );
-                                            }
-                                        });
 
-                                    ui.add_space(10.0);
+                        if self.show_build_window {
+                            let mut window_open = true;
+                            egui::Window::new("Building Project")
+                                .collapsible(false)
+                                .resizable(false)
+                                .fixed_size([500.0, 400.0])
+                                .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
+                                .open(&mut window_open)
+                                .show(&graphics.get_egui_context(), |ui| {
+                                    ui.vertical_centered(|ui| {
+                                        ui.heading("Gradle Build Progress");
+                                        ui.add_space(10.0);
+
+                                        let progress_bar = egui::ProgressBar::new(self.build_progress)
+                                            .show_percentage()
+                                            .animate(true);
+                                        ui.add(progress_bar);
+
+                                        ui.add_space(15.0);
+                                        ui.separator();
+                                        ui.add_space(5.0);
+
+                                        ui.heading("Build Log");
+                                        ui.add_space(5.0);
+
+                                        egui::ScrollArea::vertical()
+                                            .stick_to_bottom(true)
+                                            .max_height(200.0)
+                                            .auto_shrink([false, false])
+                                            .show(ui, |ui| {
+                                                for log_line in &self.build_logs {
+                                                    ui.label(
+                                                        egui::RichText::new(log_line)
+                                                            .family(egui::FontFamily::Monospace)
+                                                            .size(12.0),
+                                                    );
+                                                }
+
+                                                if !self.build_logs.is_empty() {
+                                                    ui.add_space(10.0);
+                                                    ui.label(
+                                                        egui::RichText::new(format!(
+                                                            "Total log entries: {}",
+                                                            self.build_logs.len()
+                                                        ))
+                                                            .italics()
+                                                            .color(egui::Color32::GRAY),
+                                                    );
+                                                    ui.label(
+                                                        "Tip: Press Ctrl+Alt+P to skip build and start running",
+                                                    );
+                                                }
+                                            });
+
+                                        ui.add_space(10.0);
+                                    });
                                 });
-                            });
 
-                        if !window_open {
-                            if let Some(handle) = self.handle_created {
-                                log::warn!("Cancelling build task due to window close");
-                                graphics.future_queue.cancel(&handle);
+                            if !window_open {
+                                if let Some(handle) = self.handle_created {
+                                    log::warn!("Cancelling build task due to window close");
+                                    graphics.future_queue.cancel(&handle);
+                                }
+
+                                self.show_build_window = false;
+                                self.handle_created = None;
+                                self.progress_rx = None;
+                                self.editor_state = EditorState::Editing;
+                                
                             }
+                        }
 
-                            self.show_build_window = false;
+                        if let Some(result) = local_handle_exchanged {
+                            log::debug!("Build future completed, processing result");
                             self.handle_created = None;
                             self.progress_rx = None;
-                            self.editor_state = EditorState::Editing;
-                            self.signal = Signal::None;
-                        }
-                    }
-
-                    if let Some(result) = local_handle_exchanged {
-                        log::debug!("Build future completed, processing result");
-                        self.handle_created = None;
-                        self.progress_rx = None;
 
-                        match result {
-                            Ok(path) => {
-                                log::debug!("Path is valid, JAR location as {}", path.display());
-                                success!("Build completed successfully!");
-                                self.show_build_window = false;
-                                self.signal = Signal::None;
+                            match result {
+                                Ok(path) => {
+                                    log::debug!("Path is valid, JAR location as {}", path.display());
+                                    success!("Build completed successfully!");
+                                    self.show_build_window = false;
+                                    
 
-                                self.load_play_mode()?;
-                            }
-                            Err(e) => {
-                                let error_msg = format!("Build process error: {}", e);
-                                self.build_logs.push(error_msg.clone());
-                                self.last_build_error = Some(self.build_logs.join("\n"));
+                                    self.load_play_mode()?;
+                                }
+                                Err(e) => {
+                                    let error_msg = format!("Build process error: {}", e);
+                                    self.build_logs.push(error_msg.clone());
+                                    self.last_build_error = Some(self.build_logs.join("\n"));
 
-                                fatal!("Failed to ready script manager interface because {}", e);
-                                self.show_build_window = false;
-                                self.show_build_error_window = true;
-                                self.editor_state = EditorState::Editing;
+                                    fatal!("Failed to ready script manager interface because {}", e);
+                                    self.show_build_window = false;
+                                    self.show_build_error_window = true;
+                                    self.editor_state = EditorState::Editing;
+                                }
                             }
                         }
                     }
-                }
 
-                if self.show_build_error_window {
-                    if let Some(error_log) = &self.last_build_error {
-                        let mut window_open = true;
-                        let mut close_clicked = false;
-
-                        egui::Window::new("Build Error")
-                            .collapsible(true)
-                            .resizable(false)
-                            .fixed_size([700.0, 500.0])
-                            .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
-                            .open(&mut window_open)
-                            .show(&graphics.get_egui_context(), |ui| {
-                                ui.vertical(|ui| {
-                                    ui.heading("Build Failed");
-                                    ui.add_space(5.0);
-                                    ui.label("The Gradle build failed. See the error log below:");
-                                    ui.add_space(10.0);
-                                    ui.separator();
-                                    ui.add_space(10.0);
-
-                                    egui::ScrollArea::both()
-                                        .auto_shrink([false, false])
-                                        .max_height(300.0)
-                                        .show(ui, |ui| {
-                                            ui.add(
-                                                egui::TextEdit::multiline(&mut error_log.as_str())
-                                                    .font(egui::TextStyle::Monospace)
-                                                    .desired_width(f32::INFINITY)
-                                                    .desired_rows(20),
-                                            );
-                                        });
-
-                                    ui.add_space(10.0);
-
-                                    if ui.button("Close").clicked() {
-                                        close_clicked = true;
-                                    }
+                    if self.show_build_error_window {
+                        if let Some(error_log) = &self.last_build_error {
+                            let mut window_open = true;
+                            let mut close_clicked = false;
+
+                            egui::Window::new("Build Error")
+                                .collapsible(true)
+                                .resizable(false)
+                                .fixed_size([700.0, 500.0])
+                                .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
+                                .open(&mut window_open)
+                                .show(&graphics.get_egui_context(), |ui| {
+                                    ui.vertical(|ui| {
+                                        ui.heading("Build Failed");
+                                        ui.add_space(5.0);
+                                        ui.label("The Gradle build failed. See the error log below:");
+                                        ui.add_space(10.0);
+                                        ui.separator();
+                                        ui.add_space(10.0);
+
+                                        egui::ScrollArea::both()
+                                            .auto_shrink([false, false])
+                                            .max_height(300.0)
+                                            .show(ui, |ui| {
+                                                ui.add(
+                                                    egui::TextEdit::multiline(&mut error_log.as_str())
+                                                        .font(egui::TextStyle::Monospace)
+                                                        .desired_width(f32::INFINITY)
+                                                        .desired_rows(20),
+                                                );
+                                            });
+
+                                        ui.add_space(10.0);
+
+                                        if ui.button("Close").clicked() {
+                                            close_clicked = true;
+                                        }
+                                    });
                                 });
-                            });
 
-                        if !window_open || close_clicked {
+                            if !window_open || close_clicked {
+                                self.show_build_error_window = false;
+                            }
+                        } else {
                             self.show_build_error_window = false;
                         }
-                    } else {
-                        self.show_build_error_window = false;
                     }
-                }
 
-                if matches!(self.editor_state, EditorState::Building) || self.show_build_error_window {
-                    self.signal = Signal::Play;
-                }
-                Ok(())
-            }
-            Signal::StopPlaying => {
-                self.editor_state = EditorState::Editing;
-
-                if let Some(pid) = self.play_mode_pid {
-                    log::debug!(
-                        "Play mode requested to be exited, killing processes [{}]",
-                        pid
-                    );
-                    let _ = crate::process::kill_process(pid);
+                    if matches!(self.editor_state, EditorState::Building) || self.show_build_error_window {
+                        self.signal.push_back(Signal::Play);
+                    }
+                    Ok(())
                 }
+                Signal::StopPlaying => {
+                    self.editor_state = EditorState::Editing;
+
+                    if let Some(pid) = self.play_mode_pid {
+                        log::debug!(
+                            "Play mode requested to be exited, killing processes [{}]",
+                            pid
+                        );
+                        let _ = crate::process::kill_process(pid);
+                    }
 
-                self.play_mode_pid = None;
-                self.play_mode_exit_rx = None;
+                    self.play_mode_pid = None;
+                    self.play_mode_exit_rx = None;
 
-                success!("Exited play mode");
-                log::info!("Back to the editor you go...");
+                    success!("Exited play mode");
+                    log::info!("Back to the editor you go...");
 
-                self.signal = Signal::None;
-                Ok(())
-            }
-            Signal::AddComponent(entity, component) => {
-                let registry = self.component_registry.clone();
-
-                let Some(component_id) = registry.id_for_component(component.as_ref()) else {
-                    warn!(
-                        "Failed to resolve component type for add request on entity {:?}",
-                        entity
-                    );
-                    self.signal = Signal::None;
-                    return Ok(());
-                };
-
-                if registry
-                    .find_entities_by_numeric_id(&self.world, component_id)
-                    .contains(&entity)
-                {
-                    let component_name = registry
-                        .get_descriptor_by_numeric_id(component_id)
-                        .map(|desc| desc.type_name.as_str())
-                        .unwrap_or("Unknown");
-
-                    warn!(
-                        "Entity {:?} already has component '{}'",
-                        entity,
-                        component_name
-                    );
-                    self.signal = Signal::None;
-                    return Ok(());
+                    
+                    Ok(())
                 }
+                Signal::AddComponent(entity, component) => {
+                    let registry = self.component_registry.clone();
+
+                    let Some(component_id) = registry.id_for_component(component.as_ref()) else {
+                        warn!(
+                            "Failed to resolve component type for add request on entity {:?}",
+                            entity
+                        );
+                        
+                        return Ok(());
+                    };
 
-                let graphics_clone = graphics.clone();
-                let init_future = async move {
-                    let Some(loader_future) =
-                        registry.load_component(component.as_ref(), graphics_clone.clone())
-                    else {
-                        return Err(anyhow::anyhow!(
-                            "Component type is not registered in ComponentRegistry"
-                        ));
+                    if registry
+                        .find_entities_by_numeric_id(&self.world, component_id)
+                        .contains(&entity)
+                    {
+                        let component_name = registry
+                            .get_descriptor_by_numeric_id(component_id)
+                            .map(|desc| desc.type_name.as_str())
+                            .unwrap_or("Unknown");
+
+                        warn!(
+                            "Entity {:?} already has component '{}'",
+                            entity,
+                            component_name
+                        );
+                        
+                        return Ok(());
+                    }
+
+                    let graphics_clone = graphics.clone();
+                    let init_future = async move {
+                        let Some(loader_future) =
+                            registry.load_component(component.as_ref(), graphics_clone.clone())
+                        else {
+                            return Err(anyhow::anyhow!(
+                                "Component type is not registered in ComponentRegistry"
+                            ));
+                        };
+
+                        loader_future.await
                     };
+                    let handle = graphics.future_queue.push(init_future);
+                    self.pending_components.push((entity, handle));
 
-                    loader_future.await
-                };
-                let handle = graphics.future_queue.push(init_future);
-                self.pending_components.push((entity, handle));
+                    success!("Queued component addition for entity {:?}", entity);
+                    
+                    Ok(())
+                }
+                Signal::RequestNewWindow(window_data) => {
+                    use dropbear_engine::scene::SceneCommand;
+                    self.scene_command = SceneCommand::RequestWindow(window_data.clone());
+                    
+                    Ok(())
+                }
+                Signal::UpdateViewportSize((x, y)) => {
+                    let width = x.max(1.0).round() as u32;
+                    let height = y.max(1.0).round() as u32;
+                    let current_size = graphics.viewport_texture.size;
+
+                    if current_size.width != width || current_size.height != height {
+                        self.scene_command =
+                            dropbear_engine::scene::SceneCommand::ResizeViewport((width, height));
+                    }
+                    
+                    Ok(())
+                }
+                Signal::ReloadWGPUData { skybox_texture } => {
+                    self.main_render_pipeline = None;
+                    self.light_cube_pipeline = None;
+                    self.shader_globals = None;
+                    self.collider_wireframe_pipeline = None;
+                    self.mipmapper = None;
+                    self.texture_id = None;
+                    self.window = None;
+                    self.sky_pipeline = None;
+                    self.load_wgpu_nerdy_stuff(graphics.clone(), skybox_texture.as_ref());
 
-                success!("Queued component addition for entity {:?}", entity);
-                self.signal = Signal::None;
-                Ok(())
-            }
-            Signal::RequestNewWindow(window_data) => {
-                use dropbear_engine::scene::SceneCommand;
-                self.scene_command = SceneCommand::RequestWindow(window_data.clone());
-                self.signal = Signal::None;
-                Ok(())
-            }
-            Signal::UpdateViewportSize((x, y)) => {
-                let width = x.max(1.0).round() as u32;
-                let height = y.max(1.0).round() as u32;
-                let current_size = graphics.viewport_texture.size;
-
-                if current_size.width != width || current_size.height != height {
-                    self.scene_command =
-                        dropbear_engine::scene::SceneCommand::ResizeViewport((width, height));
+                    Ok(())
                 }
-                self.signal = Signal::None;
-                Ok(())
-            }
-            Signal::ReloadWGPUData { skybox_texture } => {
-                self.main_render_pipeline = None;
-                self.light_cube_pipeline = None;
-                self.shader_globals = None;
-                self.collider_wireframe_pipeline = None;
-                self.mipmapper = None;
-                self.texture_id = None;
-                self.window = None;
-                self.sky_pipeline = None;
-                self.load_wgpu_nerdy_stuff(graphics.clone(), skybox_texture.as_ref());
-                
-                self.signal = Signal::None;
+            }?;
+            if !show {
                 
-                Ok(())
             }
-        }?;
-        if !show {
-            self.signal = Signal::None;
+            if let Some(signal) = local_signal {
+                self.signal.push_back(signal);
+            }
         }
-        if let Some(signal) = local_signal {
-            self.signal = signal;
+
+        for s in requeue {
+            self.signal.push_front(s);
         }
+
         Ok(())
     }
 }
diff --git a/crates/eucalyptus-editor/src/spawn.rs b/crates/eucalyptus-editor/src/spawn.rs
index 6ae5ad4..da26abd 100644
--- a/crates/eucalyptus-editor/src/spawn.rs
+++ b/crates/eucalyptus-editor/src/spawn.rs
@@ -4,11 +4,11 @@ use dropbear_engine::entity::{EntityTransform, MeshRenderer};
 use dropbear_engine::future::{FutureHandle, FutureQueue};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::model::Model;
-use eucalyptus_core::hierarchy::Parent;
+use eucalyptus_core::hierarchy::{Hierarchy, Parent};
 use eucalyptus_core::scene::SceneEntity;
 use eucalyptus_core::states::Label;
 use eucalyptus_core::{fatal, success, warn};
-use hecs::EntityBuilder;
+use hecs::{Entity, EntityBuilder};
 use parking_lot::Mutex;
 use std::sync::{Arc, LazyLock};
 
@@ -16,6 +16,8 @@ use std::sync::{Arc, LazyLock};
 pub struct PendingSpawn {
     pub scene_entity: SceneEntity,
     pub handle: Option<FutureHandle>,
+    /// If set, the spawned entity will be reparented to the entity whose label matches this.
+    pub parent_label: Option<Label>,
 }
 
 pub static PENDING_SPAWNS: LazyLock<Mutex<Vec<PendingSpawn>>> =
@@ -108,6 +110,22 @@ impl PendingSpawnController for Editor {
                                         self.world.insert_one(entity, EntityTransform::default());
                                 }
 
+                                // Attach to parent if requested.
+                                if let Some(ref parent_label) = spawn.parent_label {
+                                    let parent_entity = self
+                                        .world
+                                        .query::<(&Label, Entity)>()
+                                        .iter()
+                                        .find_map(|(l, e)| {
+                                            if l == parent_label { Some(e) } else { None }
+                                        });
+                                    if let Some(parent_entity) = parent_entity {
+                                        Hierarchy::set_parent(&mut self.world, entity, parent_entity);
+                                    } else {
+                                        log::warn!("Parent '{}' not found when spawning '{}'", parent_label.as_str(), label.as_str());
+                                    }
+                                }
+
                                 success!("Spawned '{}' from pending queue", label);
                                 completed.push(index);
                             }
@@ -187,6 +205,7 @@ impl PendingSpawnController for Editor {
                             }
 
                             success!("Swapped MeshRenderer model for entity {:?}", entity);
+                            self.selected_entity = Some(*entity);
                             completed_swaps.push(index);
                         }
                         Ok(Err(err)) => {
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 8dd7e17..b598df5 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -27,7 +27,7 @@ use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER, SceneLoadResu
 use eucalyptus_core::states::SCENES;
 use eucalyptus_core::states::{Label, PROJECT};
 use eucalyptus_core::ui::HUDComponent;
-use glam::{DVec3, Mat4, Quat, Vec2, Vec3};
+use glam::{DVec3, Mat4, Quat, Vec2};
 use hecs::Entity;
 use kino_ui::WidgetTree;
 use kino_ui::rendering::KinoRenderTargetId;