kitgit

tirbofish/dropbear · commit

d18cb60e38502095cea8a533bca61b0f0add9f26

wip: implementing new classes such as SceneMetadata.kt, ResourceReference.kt and AssetEntry.kt wip: attempts to do viewport clicking within the editor. would be good for stuff like interactive UI in billboarding (idk if its implemented yet). fix: changed default ambient strength to 0.1 instead of 1.0 to not make it look like its bad

Unverified

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

view full diff

diff --git a/crates/eucalyptus-core/src/input.rs b/crates/eucalyptus-core/src/input.rs
index 501bbf5..2eca9a2 100644
--- a/crates/eucalyptus-core/src/input.rs
+++ b/crates/eucalyptus-core/src/input.rs
@@ -1,6 +1,7 @@
 //! Input management and input state.
 
 pub mod gamepad;
+pub mod ndc;
 
 use crate::scripting::jni::utils::ToJObject;
 use crate::scripting::native::DropbearNativeError;
diff --git a/crates/eucalyptus-core/src/input/ndc.rs b/crates/eucalyptus-core/src/input/ndc.rs
new file mode 100644
index 0000000..fd1bc89
--- /dev/null
+++ b/crates/eucalyptus-core/src/input/ndc.rs
@@ -0,0 +1,62 @@
+use glam::{Vec3, Mat4, Vec4};
+
+/// Helpers for converting from a 2D display to a 3D object, such as billboard ui and some
+/// other applications.
+// viewport aint even work lmaoooo
+pub struct NormalisedDeviceCoordinates;
+
+impl NormalisedDeviceCoordinates {
+    pub fn screen_to_ray(
+        touch_pos: impl Into<[f32; 2]>,
+        screen_dims: impl Into<[f32; 2]>,
+        inv_proj: Mat4,
+        inv_view: Mat4,
+    ) -> (Vec3, Vec3) {
+        let [touch_x, touch_y] = touch_pos.into();
+        let [screen_width, screen_height] = screen_dims.into();
+
+        let ndc_x =  (touch_x / screen_width)  * 2.0 - 1.0;
+        let ndc_y = -(touch_y / screen_height) * 2.0 + 1.0; // flip Y, screen Y is down
+
+        // unproject to view space
+        let clip_near = Vec4::new(ndc_x, ndc_y, 0.0, 1.0); // z=0 = near plane
+        let mut view_near = inv_proj * clip_near;
+        view_near /= view_near.w;
+
+        // to world space
+        let world_near = inv_view * view_near;
+
+        // ray origin = camera position, direction = toward unprojected point
+        let ray_origin = (inv_view * Vec4::new(0.0, 0.0, 0.0, 1.0)).truncate();
+        let ray_dir    = (world_near.truncate() - ray_origin).normalize();
+
+        (ray_origin, ray_dir)
+    }
+
+    pub fn ray_aabb(
+        origin: Vec3,
+        dir: Vec3,
+        aabb_min: Vec3,
+        aabb_max: Vec3,
+    ) -> Option<f32> {
+        let inv_dir = 1.0 / dir;
+        let t1 = (aabb_min - origin) * inv_dir;
+        let t2 = (aabb_max - origin) * inv_dir;
+        let t_min = t1.min(t2).max_element();
+        let t_max = t1.max(t2).min_element();
+        if t_max >= t_min && t_max > 0.0 { Some(t_min) } else { None }
+    }
+
+    pub fn ray_plane(
+        origin: Vec3,
+        dir: Vec3,
+        plane_normal: Vec3,
+        plane_d: f32,
+    ) -> Option<Vec3> {
+        let denom = plane_normal.dot(dir);
+        if denom.abs() < 1e-10 { return None; } // ray parallel to plane
+        let t = (plane_d - plane_normal.dot(origin)) / denom;
+        if t < 0.0 { return None; } // intersection behind ray
+        Some(origin + dir * t)
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/metadata.rs b/crates/eucalyptus-core/src/metadata.rs
index b2a5252..558dac7 100644
--- a/crates/eucalyptus-core/src/metadata.rs
+++ b/crates/eucalyptus-core/src/metadata.rs
@@ -46,7 +46,6 @@ pub struct AssetEntry {
 
     /// Path to the compiled output (e.g. `compiled/meshes/f47ac10b.eucmdl`),
     /// relative to the project root.
-    /// `None` for `Embedded` and `Procedural` assets which have no compiled form.
     pub compiled_path: Option<PathBuf>,
 
     /// SHA-256 (or xxHash) of the source file at last successful import.
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index 3cfee6a..6e8c5e9 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -95,7 +95,7 @@ impl SceneSettings {
             show_hitboxes: false,
             overlay_hud: false,
             overlay_billboard: true,
-            ambient_strength: 1.0,
+            ambient_strength: 0.1,
         }
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
deleted file mode 100644
index cf422c3..0000000
--- a/crates/eucalyptus-editor/src/editor/asset_viewer.rs
+++ /dev/null
@@ -1,1484 +0,0 @@
-use dropbear_engine::asset::ASSET_REGISTRY;
-use dropbear_engine::model::Model;
-use dropbear_engine::texture::TextureBuilder;
-use dropbear_engine::{graphics::NO_TEXTURE, utils::ResourceReference};
-use egui_ltreeview::{Action, NodeBuilder, TreeViewBuilder};
-use eucalyptus_core::ser::model::EucalyptusModel;
-use eucalyptus_core::states::PROJECT;
-use eucalyptus_core::utils::ResolveReference;
-use hecs::Entity;
-use log::{info, warn};
-use std::hash::{Hash, Hasher};
-use std::{cmp::Ordering, fs, hash::DefaultHasher, io, path::{Path, PathBuf}};
-
-use crate::editor::{
-    AssetDivision, AssetNodeInfo, AssetNodeKind, ComponentNodeSelection, DraggedAsset,
-    EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, FsEntry, ResourceDivision,
-    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> {
-    pub(crate) fn show_asset_viewer(&mut self, ui: &mut egui::Ui) {
-        let mut cfg = TABS_GLOBAL.lock();
-        cfg.asset_node_assets.clear();
-        cfg.asset_node_info.clear();
-        if let Some(rename) = &cfg.asset_rename {
-            if !rename.original_path.exists() {
-                cfg.asset_rename = None;
-            }
-        }
-
-        if let Some(mut rename) = cfg.asset_rename.take() {
-            let rename_id = egui::Id::new("asset_rename_input");
-            let mut should_apply = false;
-
-            ui.horizontal(|ui| {
-                ui.label("Rename");
-                let response = ui.add(egui::TextEdit::singleline(&mut rename.buffer).id(rename_id));
-                if rename.just_started {
-                    ui.ctx().memory_mut(|m| m.request_focus(rename_id));
-                    rename.just_started = false;
-                }
-
-                let enter = ui.input(|input| input.key_pressed(egui::Key::Enter));
-                should_apply = (enter && response.has_focus()) || response.lost_focus();
-            });
-
-            if should_apply {
-                let is_dir = rename.original_path.is_dir();
-                self.apply_asset_rename(&rename, is_dir);
-            } else {
-                cfg.asset_rename = Some(rename);
-            }
-
-            ui.separator();
-        }
-
-        let project_root = {
-            let project = PROJECT.read();
-            if project.project_path.as_os_str().is_empty() {
-                ui.label("Open a project to browse assets.");
-                return;
-            }
-            project.project_path.clone()
-        };
-
-        let (_resp, action) =
-            egui_ltreeview::TreeView::new(egui::Id::new("asset_viewer")).show(ui, |builder| {
-                builder.node(Self::dir_node("euca://"));
-                self.build_resource_branch(&mut cfg, builder, &project_root);
-                self.build_scripts_branch(&mut cfg, builder, &project_root);
-                self.build_scene_branch(&mut cfg, builder, &project_root);
-                builder.close_dir();
-            });
-
-        for a in action {
-            match a {
-                Action::SetSelected(selected) => {
-                    log_once::debug_once!("Selected: {:?}", selected);
-                }
-                Action::Move(moved) => {
-                    log_once::debug_once!("Moved: {:?}", moved);
-                    self.handle_asset_move(&mut cfg, &moved);
-                }
-                Action::Drag(dragged) => {
-                    log_once::debug_once!("Dragged: {:?}", dragged);
-
-                    if let Some(&node_id) = dragged.source.first() {
-                        if let Some(asset) = cfg.asset_node_assets.get(&node_id).cloned() {
-                            cfg.dragged_asset = Some(asset.clone());
-                            ui.ctx().data_mut(|d| {
-                                d.insert_temp(
-                                    egui::Id::new(DRAGGED_ASSET_ID),
-                                    Some(asset.path.clone()),
-                                )
-                            });
-                        }
-                    }
-                }
-                Action::Activate(activated) => {
-                    log_once::debug_once!("Activated: {:?}", activated);
-                }
-                Action::DragExternal(_) => {}
-                Action::MoveExternal(_) => {}
-            }
-        }
-    }
-
-    fn build_resource_branch(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        builder: &mut TreeViewBuilder<u64>,
-        project_root: &Path,
-    ) {
-        let label = "euca://resources";
-        let resources_root = project_root.join("resources");
-        let root_info = AssetNodeInfo {
-            path: resources_root.clone(),
-            division: AssetDivision::Resources,
-            kind: AssetNodeKind::Resource(ResourceDivision::Folder),
-            is_dir: true,
-            is_division_root: true,
-            allow_add_folder: true,
-        };
-        Self::register_asset_node(cfg, label, root_info.clone());
-        let node_id = Self::asset_node_id(label);
-        let menu = Self::dir_node_kind(label, "resources", root_info.kind).context_menu(|ui| {
-            self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Folder")
-        });
-        builder.node(menu);
-        if resources_root.exists() {
-            self.walk_resource_directory(cfg, builder, &resources_root, &resources_root);
-        } else {
-            Self::add_placeholder_leaf(
-                builder,
-                "euca://resources/missing",
-                "missing",
-                AssetNodeKind::Resource(ResourceDivision::File),
-            );
-        }
-        builder.close_dir();
-    }
-
-    fn walk_resource_directory(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        builder: &mut TreeViewBuilder<u64>,
-        base_path: &Path,
-        current_path: &Path,
-    ) {
-        let entries = match Self::sorted_entries(current_path) {
-            Ok(entries) => entries,
-            Err(err) => {
-                log_once::warn_once!(
-                    "Unable to enumerate resources at '{}': {}",
-                    current_path.parent().unwrap_or(current_path).display(),
-                    err
-                );
-                return;
-            }
-        };
-
-        for entry in entries {
-            let full_label = Self::resource_label(base_path, &entry.path);
-            if entry.is_dir {
-                let dir_info = AssetNodeInfo {
-                    path: entry.path.clone(),
-                    division: AssetDivision::Resources,
-                    kind: AssetNodeKind::Resource(ResourceDivision::Folder),
-                    is_dir: true,
-                    is_division_root: false,
-                    allow_add_folder: true,
-                };
-                Self::register_asset_node(cfg, &full_label, dir_info.clone());
-                let node_id = Self::asset_node_id(&full_label);
-                let menu = Self::dir_node_kind(&full_label, &entry.name, dir_info.kind)
-                    .context_menu(|ui| {
-                        self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Folder")
-                    });
-                builder.node(menu);
-                self.walk_resource_directory(cfg, builder, base_path, &entry.path);
-                builder.close_dir();
-            } else {
-                if entry.name.ends_with(".eucmeta") {
-                    continue;
-                }
-
-                let reference = ResourceReference::from_euca_uri(&full_label)
-                    .unwrap_or_else(|_| ResourceReference::default());
-                cfg.asset_node_assets.insert(
-                    Self::asset_node_id(&full_label),
-                    DraggedAsset {
-                        name: entry.name.clone(),
-                        path: reference.clone(),
-                    },
-                );
-                let is_model = Self::is_model_file(&entry.name);
-                let is_texture = Self::is_texture_file(&entry.name);
-                let ext = entry
-                    .path
-                    .extension()
-                    .and_then(|v| v.to_str())
-                    .map(|v| v.to_ascii_lowercase());
-                let is_eucbin = matches!(ext.as_deref(), Some("eucbin"));
-                let is_eucmdl = matches!(ext.as_deref(), Some("eucmdl"));
-                let entry_name = entry.name.clone();
-                let reference_for_menu = reference.clone();
-                let file_info = AssetNodeInfo {
-                    path: entry.path.clone(),
-                    division: AssetDivision::Resources,
-                    kind: AssetNodeKind::Resource(ResourceDivision::File),
-                    is_dir: false,
-                    is_division_root: false,
-                    allow_add_folder: false,
-                };
-                Self::register_asset_node(cfg, &full_label, file_info.clone());
-                let node_id = Self::asset_node_id(&full_label);
-                let menu = Self::leaf_node_kind(&full_label, &entry.name, file_info.kind)
-                    .context_menu(|ui| {
-                        self.asset_file_context_menu(cfg, ui, node_id, &file_info);
-                        ui.separator();
-
-                        if is_model || is_eucmdl {
-                            if ui.button("Load model to memory").clicked() {
-                                ui.close();
-                                self.queue_model_load(
-                                    reference_for_menu.clone(),
-                                    entry_name.clone(),
-                                );
-                                info!("Loading model {}", entry_name);
-                            }
-                        }
-
-                        if is_texture {
-                            if ui.button("Load texture to memory").clicked() {
-                                ui.close();
-                                self.queue_texture_load(
-                                    reference_for_menu.clone(),
-                                    entry_name.clone(),
-                                    false,
-                                );
-                                info!("Loading texture {}", entry_name);
-                            }
-                        }
-
-                        if is_eucbin {
-                            if ui.button("Load as model to memory").clicked() {
-                                ui.close();
-                                self.queue_model_load(
-                                    reference_for_menu.clone(),
-                                    entry_name.clone(),
-                                );
-                                info!("Loading eucbin as model {}", entry_name);
-                            }
-
-                            if ui.button("Load as texture to memory").clicked() {
-                                ui.close();
-                                self.queue_texture_load(
-                                    reference_for_menu.clone(),
-                                    entry_name.clone(),
-                                    true,
-                                );
-                                info!("Loading eucbin as texture {}", entry_name);
-                            }
-                        }
-                    });
-                builder.node(menu);
-            }
-        }
-    }
-
-    fn resource_label(base_path: &Path, path: &Path) -> String {
-        let relative = path
-            .strip_prefix(base_path)
-            .map(|rel| rel.to_string_lossy().replace('\\', "/"))
-            .unwrap_or_else(|_| path.to_string_lossy().replace('\\', "/"));
-        if relative.is_empty() {
-            "euca://resources".to_string()
-        } else {
-            format!("euca://resources/{}", relative)
-        }
-    }
-
-    fn build_scripts_branch(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        builder: &mut TreeViewBuilder<u64>,
-        project_root: &Path,
-    ) {
-        let label = "euca://scripts";
-        let scripts_root = project_root.join("src");
-        let root_info = AssetNodeInfo {
-            path: scripts_root.clone(),
-            division: AssetDivision::Scripts,
-            kind: AssetNodeKind::Script(ScriptDivision::Package),
-            is_dir: true,
-            is_division_root: true,
-            allow_add_folder: false,
-        };
-        Self::register_asset_node(cfg, label, root_info.clone());
-        let node_id = Self::asset_node_id(label);
-        let menu = Self::dir_node_kind(label, "scripts", root_info.kind).context_menu(|ui| {
-            self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Package")
-        });
-        builder.node(menu);
-        if !scripts_root.exists() {
-            Self::add_placeholder_leaf(
-                builder,
-                "euca://scripts/missing",
-                "missing",
-                AssetNodeKind::Script(ScriptDivision::Script),
-            );
-            builder.close_dir();
-            return;
-        }
-
-        let entries = match Self::sorted_entries(&scripts_root) {
-            Ok(entries) => entries,
-            Err(err) => {
-                log::warn!(
-                    "Unable to enumerate scripts at '{}': {}",
-                    scripts_root.display(),
-                    err
-                );
-                builder.close_dir();
-                return;
-            }
-        };
-
-        let mut had_content = false;
-        for entry in entries {
-            if entry.is_dir {
-                let source_label = format!("{}/{}", label, entry.name);
-                let kotlin_root = entry.path.join("kotlin");
-                let source_info = AssetNodeInfo {
-                    path: kotlin_root,
-                    division: AssetDivision::Scripts,
-                    kind: AssetNodeKind::Script(ScriptDivision::Package),
-                    is_dir: true,
-                    is_division_root: true,
-                    allow_add_folder: true,
-                };
-                Self::register_asset_node(cfg, &source_label, source_info.clone());
-                let node_id = Self::asset_node_id(&source_label);
-                let menu = Self::dir_node_kind(&source_label, &entry.name, source_info.kind)
-                    .context_menu(|ui| {
-                        self.asset_dir_context_menu(cfg, ui, node_id, &source_info, "New Package")
-                    });
-                builder.node(menu);
-                if self.build_script_source_set(cfg, builder, &entry.path, &source_label) {
-                    had_content = true;
-                }
-                builder.close_dir();
-            } else if !entry.name.eq_ignore_ascii_case("source.eucc") {
-                let file_label = format!("{}/{}", label, entry.name);
-                let file_info = AssetNodeInfo {
-                    path: entry.path.clone(),
-                    division: AssetDivision::Scripts,
-                    kind: AssetNodeKind::Script(ScriptDivision::Script),
-                    is_dir: false,
-                    is_division_root: false,
-                    allow_add_folder: false,
-                };
-                Self::register_asset_node(cfg, &file_label, file_info.clone());
-                let node_id = Self::asset_node_id(&file_label);
-                let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind)
-                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
-                builder.node(menu);
-                had_content = true;
-            }
-        }
-
-        if !had_content {
-            Self::add_placeholder_leaf(
-                builder,
-                "euca://scripts/empty",
-                "empty",
-                AssetNodeKind::Script(ScriptDivision::Script),
-            );
-        }
-
-        builder.close_dir();
-    }
-
-    fn build_script_source_set(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        builder: &mut TreeViewBuilder<u64>,
-        source_path: &Path,
-        source_label: &str,
-    ) -> bool {
-        let entries = match Self::sorted_entries(source_path) {
-            Ok(entries) => entries,
-            Err(err) => {
-                log::warn!(
-                    "Unable to enumerate source set at '{}': {}",
-                    source_path.display(),
-                    err
-                );
-                Self::add_placeholder_leaf(
-                    builder,
-                    &format!("{source_label}/unreadable"),
-                    "unreadable",
-                    AssetNodeKind::Script(ScriptDivision::Script),
-                );
-                return true;
-            }
-        };
-
-        let mut had_content = false;
-        for entry in entries {
-            if entry.is_dir {
-                if entry.name.eq_ignore_ascii_case("kotlin") {
-                    if self.build_kotlin_tree(cfg, builder, &entry.path, source_label) {
-                        had_content = true;
-                    }
-                } else {
-                    let child_label = format!("{}/{}", source_label, entry.name);
-                    let dir_info = AssetNodeInfo {
-                        path: entry.path.clone(),
-                        division: AssetDivision::Scripts,
-                        kind: AssetNodeKind::Script(ScriptDivision::Package),
-                        is_dir: true,
-                        is_division_root: false,
-                        allow_add_folder: true,
-                    };
-                    Self::register_asset_node(cfg, &child_label, dir_info.clone());
-                    let node_id = Self::asset_node_id(&child_label);
-                    let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind)
-                        .context_menu(|ui| {
-                            self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Package")
-                        });
-                    builder.node(menu);
-                    self.build_plain_directory(
-                        cfg,
-                        builder,
-                        &entry.path,
-                        &child_label,
-                        AssetDivision::Scripts,
-                        AssetNodeKind::Script(ScriptDivision::Package),
-                        AssetNodeKind::Script(ScriptDivision::Script),
-                        "New Package",
-                    );
-                    builder.close_dir();
-                    had_content = true;
-                }
-            } else if !entry.name.eq_ignore_ascii_case("source.eucc") {
-                let file_label = format!("{}/{}", source_label, entry.name);
-                let file_info = AssetNodeInfo {
-                    path: entry.path.clone(),
-                    division: AssetDivision::Scripts,
-                    kind: AssetNodeKind::Script(ScriptDivision::Script),
-                    is_dir: false,
-                    is_division_root: false,
-                    allow_add_folder: false,
-                };
-                Self::register_asset_node(cfg, &file_label, file_info.clone());
-                let node_id = Self::asset_node_id(&file_label);
-                let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind)
-                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
-                builder.node(menu);
-                had_content = true;
-            }
-        }
-
-        if !had_content {
-            Self::add_placeholder_leaf(
-                builder,
-                &format!("{source_label}/empty"),
-                "empty",
-                AssetNodeKind::Script(ScriptDivision::Script),
-            );
-            had_content = true;
-        }
-
-        had_content
-    }
-
-    fn build_plain_directory(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        builder: &mut TreeViewBuilder<u64>,
-        dir_path: &Path,
-        parent_label: &str,
-        division: AssetDivision,
-        dir_kind: AssetNodeKind,
-        file_kind: AssetNodeKind,
-        new_folder_label: &str,
-    ) {
-        let entries = match Self::sorted_entries(dir_path) {
-            Ok(entries) => entries,
-            Err(err) => {
-                log::warn!(
-                    "Unable to enumerate directory '{}': {}",
-                    dir_path.display(),
-                    err
-                );
-                Self::add_placeholder_leaf(
-                    builder,
-                    &format!("{parent_label}/unreadable"),
-                    "unreadable",
-                    file_kind,
-                );
-                return;
-            }
-        };
-
-        if entries.is_empty() {
-            Self::add_placeholder_leaf(
-                builder,
-                &format!("{parent_label}/empty"),
-                "empty",
-                file_kind,
-            );
-            return;
-        }
-
-        for entry in entries {
-            let child_label = format!("{}/{}", parent_label, entry.name);
-            if entry.is_dir {
-                let dir_info = AssetNodeInfo {
-                    path: entry.path.clone(),
-                    division,
-                    kind: dir_kind,
-                    is_dir: true,
-                    is_division_root: false,
-                    allow_add_folder: true,
-                };
-                Self::register_asset_node(cfg, &child_label, dir_info.clone());
-                let node_id = Self::asset_node_id(&child_label);
-                let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind)
-                    .context_menu(|ui| {
-                        self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, new_folder_label)
-                    });
-                builder.node(menu);
-                self.build_plain_directory(
-                    cfg,
-                    builder,
-                    &entry.path,
-                    &child_label,
-                    division,
-                    dir_kind,
-                    file_kind,
-                    new_folder_label,
-                );
-                builder.close_dir();
-            } else {
-                let file_info = AssetNodeInfo {
-                    path: entry.path.clone(),
-                    division,
-                    kind: file_kind,
-                    is_dir: false,
-                    is_division_root: false,
-                    allow_add_folder: false,
-                };
-                Self::register_asset_node(cfg, &child_label, file_info.clone());
-                let node_id = Self::asset_node_id(&child_label);
-                let menu = Self::leaf_node_kind(&child_label, &entry.name, file_info.kind)
-                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
-                builder.node(menu);
-            }
-        }
-    }
-
-    fn build_kotlin_tree(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        builder: &mut TreeViewBuilder<u64>,
-        kotlin_path: &Path,
-        source_label: &str,
-    ) -> bool {
-        let entries = match Self::sorted_entries(kotlin_path) {
-            Ok(entries) => entries,
-            Err(err) => {
-                log::warn!(
-                    "Unable to enumerate kotlin sources at '{}': {}",
-                    kotlin_path.display(),
-                    err
-                );
-                Self::add_placeholder_leaf(
-                    builder,
-                    &format!("{source_label}/unreadable"),
-                    "unreadable",
-                    AssetNodeKind::Script(ScriptDivision::Script),
-                );
-                return true;
-            }
-        };
-
-        if entries.is_empty() {
-            Self::add_placeholder_leaf(
-                builder,
-                &format!("{source_label}/no_kotlin_files"),
-                "no kotlin files",
-                AssetNodeKind::Script(ScriptDivision::Script),
-            );
-            return true;
-        }
-
-        let mut had_entries = false;
-        for entry in entries {
-            if entry.is_dir {
-                self.build_kotlin_package_collapsed(
-                    cfg,
-                    builder,
-                    &entry.path,
-                    source_label,
-                    vec![entry.name.clone()],
-                );
-                had_entries = true;
-            } else {
-                let file_id = format!("{}/{}", source_label, entry.name);
-                let file_info = AssetNodeInfo {
-                    path: entry.path.clone(),
-                    division: AssetDivision::Scripts,
-                    kind: AssetNodeKind::Script(ScriptDivision::Script),
-                    is_dir: false,
-                    is_division_root: false,
-                    allow_add_folder: false,
-                };
-                Self::register_asset_node(cfg, &file_id, file_info.clone());
-                let node_id = Self::asset_node_id(&file_id);
-                let menu = Self::leaf_node_kind(&file_id, &entry.name, file_info.kind)
-                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
-                builder.node(menu);
-                had_entries = true;
-            }
-        }
-
-        had_entries
-    }
-
-    fn build_kotlin_package_collapsed(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        builder: &mut TreeViewBuilder<u64>,
-        dir_path: &Path,
-        parent_path_str: &str,
-        accumulated_parts: Vec<String>,
-    ) {
-        let entries = match Self::sorted_entries(dir_path) {
-            Ok(entries) => entries,
-            Err(err) => {
-                let package_suffix = accumulated_parts.join(".");
-                let full_path_str = format!("{}/{}", parent_path_str, package_suffix);
-                log::warn!(
-                    "Unable to enumerate package '{}' ({}): {}",
-                    package_suffix,
-                    dir_path.display(),
-                    err
-                );
-                Self::add_placeholder_leaf(
-                    builder,
-                    &format!("{full_path_str}/unreadable"),
-                    "unreadable",
-                    AssetNodeKind::Script(ScriptDivision::Script),
-                );
-                return;
-            }
-        };
-
-        let subdirs: Vec<&FsEntry> = entries.iter().filter(|e| e.is_dir).collect();
-        let files: Vec<&FsEntry> = entries.iter().filter(|e| !e.is_dir).collect();
-
-        if files.is_empty() && subdirs.len() == 1 {
-            let subdir = subdirs[0];
-            let mut new_parts = accumulated_parts;
-            new_parts.push(subdir.name.clone());
-            self.build_kotlin_package_collapsed(
-                cfg,
-                builder,
-                &subdir.path,
-                parent_path_str,
-                new_parts,
-            );
-        } else {
-            let package_suffix = accumulated_parts.join(".");
-            let full_path_str = format!("{}/{}", parent_path_str, package_suffix);
-
-            let dir_info = AssetNodeInfo {
-                path: dir_path.to_path_buf(),
-                division: AssetDivision::Scripts,
-                kind: AssetNodeKind::Script(ScriptDivision::Package),
-                is_dir: true,
-                is_division_root: false,
-                allow_add_folder: true,
-            };
-            Self::register_asset_node(cfg, &full_path_str, dir_info.clone());
-            let node_id = Self::asset_node_id(&full_path_str);
-            let menu = Self::dir_node_kind(&full_path_str, &package_suffix, dir_info.kind)
-                .context_menu(|ui| {
-                    self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Package")
-                });
-            builder.node(menu);
-
-            for file in files {
-                let file_id = format!("{}/{}", full_path_str, file.name);
-                let file_info = AssetNodeInfo {
-                    path: file.path.clone(),
-                    division: AssetDivision::Scripts,
-                    kind: AssetNodeKind::Script(ScriptDivision::Script),
-                    is_dir: false,
-                    is_division_root: false,
-                    allow_add_folder: false,
-                };
-                Self::register_asset_node(cfg, &file_id, file_info.clone());
-                let node_id = Self::asset_node_id(&file_id);
-                let menu = Self::leaf_node_kind(&file_id, &file.name, file_info.kind)
-                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
-                builder.node(menu);
-            }
-
-            for subdir in subdirs {
-                self.build_kotlin_package_collapsed(
-                    cfg,
-                    builder,
-                    &subdir.path,
-                    &full_path_str,
-                    vec![subdir.name.clone()],
-                );
-            }
-
-            builder.close_dir();
-        }
-    }
-
-    fn build_scene_branch(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        builder: &mut TreeViewBuilder<u64>,
-        project_root: &Path,
-    ) {
-        let label = "euca://scenes";
-        let scenes_root = project_root.join("resources").join("scenes");
-        let root_info = AssetNodeInfo {
-            path: scenes_root.clone(),
-            division: AssetDivision::Scenes,
-            kind: AssetNodeKind::Scene(SceneDivision::Folder),
-            is_dir: true,
-            is_division_root: true,
-            allow_add_folder: true,
-        };
-        Self::register_asset_node(cfg, label, root_info.clone());
-        let node_id = Self::asset_node_id(label);
-        let menu = Self::dir_node_kind(label, "scenes", root_info.kind).context_menu(|ui| {
-            self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Folder")
-        });
-        builder.node(menu);
-        if !scenes_root.exists() {
-            Self::add_placeholder_leaf(
-                builder,
-                "euca://scenes/missing",
-                "missing",
-                AssetNodeKind::Scene(SceneDivision::Scene),
-            );
-            builder.close_dir();
-            return;
-        }
-
-        let entries = match Self::sorted_entries(&scenes_root) {
-            Ok(entries) => entries,
-            Err(err) => {
-                log::warn!(
-                    "Unable to enumerate scenes at '{}': {}",
-                    scenes_root.display(),
-                    err
-                );
-                Self::add_placeholder_leaf(
-                    builder,
-                    "euca://scenes/unreadable",
-                    "unreadable",
-                    AssetNodeKind::Scene(SceneDivision::Scene),
-                );
-                builder.close_dir();
-                return;
-            }
-        };
-
-        let mut had_entries = false;
-        for entry in entries {
-            if entry.is_dir {
-                let child_label = format!("{}/{}", label, entry.name);
-                let dir_info = AssetNodeInfo {
-                    path: entry.path.clone(),
-                    division: AssetDivision::Scenes,
-                    kind: AssetNodeKind::Scene(SceneDivision::Folder),
-                    is_dir: true,
-                    is_division_root: false,
-                    allow_add_folder: true,
-                };
-                Self::register_asset_node(cfg, &child_label, dir_info.clone());
-                let node_id = Self::asset_node_id(&child_label);
-                let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind)
-                    .context_menu(|ui| {
-                        self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Folder")
-                    });
-                builder.node(menu);
-                self.build_plain_directory(
-                    cfg,
-                    builder,
-                    &entry.path,
-                    &child_label,
-                    AssetDivision::Scenes,
-                    AssetNodeKind::Scene(SceneDivision::Folder),
-                    AssetNodeKind::Scene(SceneDivision::Scene),
-                    "New Folder",
-                );
-                builder.close_dir();
-                had_entries = true;
-            } else if entry
-                .path
-                .extension()
-                .and_then(|ext| ext.to_str())
-                .map(|ext| ext.eq_ignore_ascii_case("eucs"))
-                .unwrap_or(false)
-            {
-                let file_label = format!("{}/{}", label, entry.name);
-                let file_info = AssetNodeInfo {
-                    path: entry.path.clone(),
-                    division: AssetDivision::Scenes,
-                    kind: AssetNodeKind::Scene(SceneDivision::Scene),
-                    is_dir: false,
-                    is_division_root: false,
-                    allow_add_folder: false,
-                };
-                Self::register_asset_node(cfg, &file_label, file_info.clone());
-                let node_id = Self::asset_node_id(&file_label);
-                let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind)
-                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
-                builder.node(menu);
-                had_entries = true;
-            }
-        }
-
-        if !had_entries {
-            Self::add_placeholder_leaf(
-                builder,
-                "euca://scenes/no_scenes",
-                "no scenes",
-                AssetNodeKind::Scene(SceneDivision::Scene),
-            );
-        }
-
-        builder.close_dir();
-    }
-
-    fn sorted_entries(path: &Path) -> io::Result<Vec<FsEntry>> {
-        let mut entries = Vec::new();
-        for entry in fs::read_dir(path)? {
-            let entry = entry?;
-            let file_type = entry.file_type()?;
-            let name = entry.file_name().to_string_lossy().to_string();
-            entries.push(FsEntry {
-                path: entry.path(),
-                name_lower: name.to_lowercase(),
-                name,
-                is_dir: file_type.is_dir(),
-            });
-        }
-
-        entries.sort_by(|a, b| match b.is_dir.cmp(&a.is_dir) {
-            Ordering::Equal => a.name_lower.cmp(&b.name_lower),
-            other => other,
-        });
-
-        Ok(entries)
-    }
-
-    fn asset_node_id(label: &str) -> u64 {
-        let mut hasher = DefaultHasher::new();
-        label.hash(&mut hasher);
-        let mut id = hasher.finish();
-        if id == 0 {
-            id = 1;
-        }
-        id
-    }
-
-    fn register_asset_node(cfg: &mut StaticallyKept, id_source: &str, info: AssetNodeInfo) -> u64 {
-        let node_id = Self::asset_node_id(id_source);
-        cfg.asset_node_info.insert(node_id, info);
-        node_id
-    }
-
-    fn dir_node<'ui>(label: &str) -> NodeBuilder<'ui, u64> {
-        Self::with_icon_kind(
-            NodeBuilder::dir(Self::asset_node_id(label)).label(label.to_string()),
-            AssetNodeKind::Resource(ResourceDivision::Folder),
-        )
-    }
-
-    fn dir_node_kind<'ui>(
-        id_source: &str,
-        label: &str,
-        kind: AssetNodeKind,
-    ) -> NodeBuilder<'ui, u64> {
-        Self::with_icon_kind(
-            NodeBuilder::dir(Self::asset_node_id(id_source)).label(label.to_string()),
-            kind,
-        )
-    }
-
-    fn leaf_node_kind<'ui>(
-        id_source: &str,
-        label: &str,
-        kind: AssetNodeKind,
-    ) -> NodeBuilder<'ui, u64> {
-        Self::with_icon_kind(
-            NodeBuilder::leaf(Self::asset_node_id(id_source)).label(label.to_string()),
-            kind,
-        )
-    }
-
-    fn with_icon_kind(
-        builder: NodeBuilder<u64>,
-        kind: AssetNodeKind,
-    ) -> NodeBuilder<u64> {
-        builder.icon(move |ui| {
-            egui_extras::install_image_loaders(ui.ctx());
-            Self::draw_asset_icon(ui, kind)
-        })
-    }
-
-    fn draw_asset_icon(ui: &mut egui::Ui, _kind: AssetNodeKind) {
-        let image = egui::Image::from_bytes("bytes://asset-viewer-icon", NO_TEXTURE)
-            .max_size(egui::vec2(14.0, 14.0));
-        ui.add(image);
-    }
-
-    fn add_placeholder_leaf(
-        builder: &mut TreeViewBuilder<u64>,
-        id_source: &str,
-        label: &str,
-        kind: AssetNodeKind,
-    ) {
-        builder.node(Self::leaf_node_kind(id_source, label, kind));
-    }
-
-    fn asset_dir_context_menu(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        ui: &mut egui::Ui,
-        node_id: u64,
-        info: &AssetNodeInfo,
-        new_folder_label: &str,
-    ) {
-        if info.allow_add_folder {
-            if ui.button(new_folder_label).clicked() {
-                ui.close();
-                let base_name = if info.division == AssetDivision::Scripts {
-                    "newpackage"
-                } else {
-                    "New Folder"
-                };
-                self.create_asset_folder(&info.path, base_name);
-            }
-        }
-
-        if ui.button("Paste").clicked() {
-            ui.close();
-            if !info.path.exists() {
-                if let Err(err) = fs::create_dir_all(&info.path) {
-                    warn!("Unable to create folder '{}': {}", info.path.display(), err);
-                    return;
-                }
-            }
-            self.signal.push_back(Signal::AssetPaste {
-                target_dir: info.path.clone(),
-                division: info.division,
-            });
-        }
-
-        if ui.button("Reveal Folder").clicked() {
-            ui.close();
-            if let Err(err) = open::that(&info.path) {
-                warn!("Unable to reveal folder: {}", err);
-            }
-        }
-
-        if !info.is_division_root && ui.button("Rename").clicked() {
-            let current_name = info
-                .path
-                .file_name()
-                .and_then(|name| name.to_str())
-                .unwrap_or("")
-                .to_string();
-            cfg.asset_rename = Some(crate::editor::AssetRenameState {
-                node_id,
-                original_path: info.path.clone(),
-                buffer: current_name,
-                just_started: true,
-            });
-        }
-
-        if !info.is_division_root && ui.button("Delete").clicked() {
-            ui.close();
-            self.delete_asset_entry(info);
-        }
-    }
-
-    fn asset_file_context_menu(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        ui: &mut egui::Ui,
-        node_id: u64,
-        info: &AssetNodeInfo,
-    ) {
-        if matches!(info.kind, AssetNodeKind::Script(ScriptDivision::Script))
-            && ui.button("Open Script").clicked()
-        {
-            ui.close();
-            if let Err(err) = open::that(&info.path) {
-                warn!("Unable to open script: {}", err);
-            }
-        }
-
-        if ui.button("Rename").clicked() {
-            let current_name = info
-                .path
-                .file_name()
-                .and_then(|name| name.to_str())
-                .unwrap_or("")
-                .to_string();
-            cfg.asset_rename = Some(crate::editor::AssetRenameState {
-                node_id,
-                original_path: info.path.clone(),
-                buffer: current_name,
-                just_started: true,
-            });
-        }
-
-        if ui.button("Copy").clicked() {
-            ui.close();
-            self.signal.push_back(Signal::AssetCopy {
-                source: info.path.clone(),
-                division: info.division,
-            });
-        }
-
-        if ui.button("Delete").clicked() {
-            ui.close();
-            self.delete_asset_entry(info);
-        }
-    }
-
-    fn apply_asset_rename(&self, rename: &crate::editor::AssetRenameState, is_dir: bool) {
-        let trimmed = rename.buffer.trim();
-        if trimmed.is_empty() {
-            warn!("Rename cancelled: empty name");
-            return;
-        }
-
-        let Some(parent) = rename.original_path.parent() else {
-            warn!("Unable to rename: missing parent directory");
-            return;
-        };
-
-        let mut new_name = trimmed.to_string();
-        if !is_dir {
-            if let Some(ext) = rename.original_path.extension().and_then(|e| e.to_str()) {
-                let has_ext = Path::new(&new_name)
-                    .extension()
-                    .and_then(|e| e.to_str())
-                    .is_some();
-                if !has_ext {
-                    new_name = format!("{new_name}.{ext}");
-                }
-            }
-        }
-
-        let target_path = parent.join(&new_name);
-        if target_path == rename.original_path {
-            return;
-        }
-
-        if target_path.exists() {
-            warn!("Rename target already exists: {}", target_path.display());
-            return;
-        }
-
-        if let Err(err) = fs::rename(&rename.original_path, &target_path) {
-            warn!(
-                "Failed to rename '{}': {}",
-                rename.original_path.display(),
-                err
-            );
-        } else {
-            info!("Renamed to {}", target_path.display());
-            let old_meta = PathBuf::from(format!("{}.eucmeta", rename.original_path.display()));
-            if old_meta.exists() {
-                let new_meta = PathBuf::from(format!("{}.eucmeta", target_path.display()));
-                if let Err(e) = fs::rename(&old_meta, &new_meta) {
-                    warn!("Failed to move .eucmeta sidecar '{}': {}", old_meta.display(), e);
-                }
-            }
-        }
-    }
-
-    fn delete_asset_entry(&self, info: &AssetNodeInfo) {
-        if info.is_division_root {
-            warn!("Cannot delete division root");
-            return;
-        }
-
-        let result = if info.is_dir {
-            fs::remove_dir_all(&info.path)
-        } else {
-            fs::remove_file(&info.path)
-        };
-
-        if let Err(err) = result {
-            warn!("Failed to delete '{}': {}", info.path.display(), err);
-        } else {
-            info!("Deleted {}", info.path.display());
-            if !info.is_dir {
-                let meta = PathBuf::from(format!("{}.eucmeta", info.path.display()));
-                if meta.exists() {
-                    if let Err(e) = fs::remove_file(&meta) {
-                        warn!("Failed to remove .eucmeta sidecar '{}': {}", meta.display(), e);
-                    }
-                }
-            }
-        }
-    }
-
-    fn create_asset_folder(&self, base_dir: &Path, base_name: &str) {
-        if let Err(err) = fs::create_dir_all(base_dir) {
-            warn!("Unable to create folder '{}': {}", base_dir.display(), err);
-            return;
-        }
-
-        let mut candidate = base_dir.join(base_name);
-        if candidate.exists() {
-            let mut index = 1;
-            loop {
-                let suffix = if base_name.contains(' ') {
-                    format!(" {}", index)
-                } else {
-                    format!("{}", index)
-                };
-                candidate = base_dir.join(format!("{base_name}{suffix}"));
-                if !candidate.exists() {
-                    break;
-                }
-                index += 1;
-            }
-        }
-
-        if let Err(err) = fs::create_dir_all(&candidate) {
-            warn!("Unable to create folder '{}': {}", candidate.display(), err);
-        } else {
-            info!("Created folder {}", candidate.display());
-        }
-    }
-
-    fn handle_asset_move(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        drag: &egui_ltreeview::DragAndDrop<u64>,
-    ) {
-        let Some(&source_id) = drag.source.first() else {
-            return;
-        };
-        let Some(source_info) = cfg.asset_node_info.get(&source_id).cloned() else {
-            return;
-        };
-        let Some(target_info) = cfg.asset_node_info.get(&drag.target).cloned() else {
-            return;
-        };
-
-        if source_info.is_division_root {
-            warn!("Cannot move division root");
-            return;
-        }
-
-        if source_info.division != target_info.division {
-            warn!("Cannot move assets across divisions");
-            return;
-        }
-
-        let target_dir = if target_info.is_dir {
-            target_info.path.clone()
-        } else {
-            target_info
-                .path
-                .parent()
-                .unwrap_or(&target_info.path)
-                .to_path_buf()
-        };
-
-        if !target_dir.exists() {
-            if let Err(err) = fs::create_dir_all(&target_dir) {
-                warn!("Target directory does not exist: {}", err);
-                return;
-            }
-        }
-
-        if source_info.is_dir && target_dir.starts_with(&source_info.path) {
-            warn!("Cannot move a folder into itself");
-            return;
-        }
-
-        let Some(name) = source_info.path.file_name() else {
-            warn!("Unable to move: invalid file name");
-            return;
-        };
-
-        let target_path = target_dir.join(name);
-        if target_path == source_info.path {
-            return;
-        }
-
-        if target_path.exists() {
-            warn!("Target already exists: {}", target_path.display());
-            return;
-        }
-
-        if let Err(err) = fs::rename(&source_info.path, &target_path) {
-            warn!("Failed to move '{}': {}", source_info.path.display(), err);
-        } else {
-            info!("Moved to {}", target_path.display());
-            if !source_info.is_dir {
-                let old_meta = PathBuf::from(format!("{}.eucmeta", source_info.path.display()));
-                if old_meta.exists() {
-                    let new_meta = PathBuf::from(format!("{}.eucmeta", target_path.display()));
-                    if let Err(e) = fs::rename(&old_meta, &new_meta) {
-                        warn!("Failed to move .eucmeta sidecar '{}': {}", old_meta.display(), e);
-                    }
-                }
-            }
-        }
-    }
-
-    fn is_model_file(name: &str) -> bool {
-        let name = name.to_ascii_lowercase();
-        name.ends_with(".glb") || name.ends_with(".gltf")
-    }
-
-    fn is_texture_file(name: &str) -> bool {
-        let name = name.to_ascii_lowercase();
-        name.ends_with(".png")
-            || name.ends_with(".jpg")
-            || name.ends_with(".jpeg")
-            || name.ends_with(".tga")
-            || name.ends_with(".bmp")
-            || name.ends_with(".webp")
-    }
-
-    fn queue_model_load(&self, reference: ResourceReference, label: String) {
-        if ASSET_REGISTRY
-            .read()
-            .get_model_handle_by_reference(&reference)
-            .is_some()
-        {
-            eucalyptus_core::info!("Model already loaded: {}", label);
-            return;
-        }
-
-        let graphics = self.graphics.clone();
-        let queue = graphics.future_queue.clone();
-        queue.push(async move {
-            let path = reference.resolve()?;
-            let buffer = fs::read(&path)?;
-            let extension = path
-                .extension()
-                .and_then(|ext| ext.to_str())
-                .map(|ext| ext.to_ascii_lowercase());
-
-            let handle = match extension.as_deref() {
-                Some("eucmdl") => {
-                    let model = rkyv::from_bytes::<EucalyptusModel, rkyv::rancor::Error>(&buffer)
-                        .map_err(|e| anyhow::anyhow!(
-                            "Unable to deserialize .eucmdl model '{}': {}",
-                            path.display(),
-                            e
-                        ))?;
-
-                    let runtime_model = model.load(reference.clone(), graphics.clone());
-                    let mut registry = ASSET_REGISTRY.write();
-                    registry.add_model_with_label(label.clone(), runtime_model)
-                }
-                _ => match Model::load_from_memory_raw(
-                    graphics.clone(),
-                    buffer,
-                    Some(reference.clone()),
-                    Some(label.as_str()),
-                    ASSET_REGISTRY.clone(),
-                )
-                .await
-                {
-                    Ok(v) => v,
-                    Err(e) => {
-                        eucalyptus_core::warn!("Unable to load model {}: {}", reference, e);
-                        return Err(e);
-                    }
-                },
-            };
-
-            let mut registry = ASSET_REGISTRY.write();
-            registry.label_model(label.clone(), handle);
-
-            eucalyptus_core::success!("Loaded model {}", label);
-            Ok::<(), anyhow::Error>(())
-        });
-    }
-
-    fn queue_texture_load(&self, reference: ResourceReference, label: String, strict_image_decode: bool) {
-        if ASSET_REGISTRY
-            .read()
-            .get_texture_handle_by_reference(&reference)
-            .is_some()
-        {
-            eucalyptus_core::info!("Texture already loaded: {}", label);
-            return;
-        }
-
-        let graphics = self.graphics.clone();
-        let queue = graphics.future_queue.clone();
-        queue.push(async move {
-            let path = reference.resolve()?;
-            let bytes = fs::read(&path)?;
-
-            if strict_image_decode
-                && let Err(err) = image::load_from_memory(bytes.as_slice())
-            {
-                let error = anyhow::anyhow!(
-                    "'{}' is not a texture-compatible eucbin payload: {}",
-                    path.display(),
-                    err
-                );
-                eucalyptus_core::warn!("{}", error);
-                return Err(error);
-            }
-
-            let mut texture = TextureBuilder::new(&graphics.device)
-                .with_bytes(graphics.clone(), bytes.as_slice())
-                .label(label.as_str())
-                .build();
-            texture.reference = Some(reference.clone());
-            let mut registry = ASSET_REGISTRY.write();
-            registry.add_texture_with_label(label.clone(), texture);
-            Ok::<(), anyhow::Error>(())
-        });
-    }
-
-    pub(crate) fn handle_tree_selection(&mut self, cfg: &mut StaticallyKept, items: &[u64]) {
-        for node_id in items {
-            self.resolve_tree_node(cfg, *node_id);
-        }
-    }
-
-    pub(crate) fn handle_tree_activate(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        activate: &egui_ltreeview::Activate<u64>,
-    ) {
-        self.handle_tree_selection(cfg, &activate.selected);
-    }
-
-    pub(crate) fn handle_tree_drag(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        drag: &egui_ltreeview::DragAndDrop<u64>,
-    ) {
-        if let Some(&node_id) = drag.source.first() {
-            if let Some(selection) = cfg.component_selection(node_id) {
-                cfg.pending_component_drag = Some(selection);
-                self.inspect_component_selection(cfg, selection);
-            }
-        }
-    }
-
-    pub(crate) fn handle_tree_move(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        drag: &egui_ltreeview::DragAndDrop<u64>,
-    ) {
-        let selection = cfg.pending_component_drag.take().or_else(|| {
-            drag.source
-                .first()
-                .and_then(|node_id| cfg.component_selection(*node_id))
-        });
-
-        if let Some(selection) = selection {
-            self.inspect_component_selection(cfg, selection);
-            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);
-            }
-        }
-    }
-
-    fn resolve_tree_node(&mut self, cfg: &mut StaticallyKept, node_id: u64) {
-        if node_id == u64::MAX {
-            log_once::debug_once!("Root node has been selected");
-            cfg.root_node_selected = true;
-            *self.selected_entity = None;
-        } else if let Some(selection) = cfg.component_selection(node_id) {
-            cfg.root_node_selected = false;
-            *self.selected_entity = selection.entity();
-            self.inspect_component_selection(cfg, selection);
-        } else if let Some(entity) = Self::entity_from_node_id(node_id) {
-            cfg.root_node_selected = false;
-            *self.selected_entity = Some(entity);
-        }
-    }
-
-    fn inspect_component_selection(
-        &mut self,
-        cfg: &mut StaticallyKept,
-        selection: ComponentNodeSelection,
-    ) {
-        cfg.remember_component_lookup(selection);
-        let component_id = selection.component_type_id;
-        let matches = self
-            .component_registry
-            .find_entities_by_numeric_id(self.world, component_id);
-        let descriptor = self
-            .component_registry
-            .get_descriptor_by_numeric_id(component_id);
-
-        if matches.is_empty() {
-            log::warn!("Component id #{} not found in world", component_id);
-            return;
-        }
-
-        let name = descriptor
-            .map(|desc| desc.fqtn.as_str())
-            .unwrap_or("<unknown>");
-        for entity in matches {
-            log::debug!(
-                "Serializable component '{}' (id #{}) attached to entity {:?}",
-                name,
-                component_id,
-                entity
-            );
-        }
-    }
-
-    fn entity_from_node_id(node_id: u64) -> Option<Entity> {
-        if node_id == u64::MAX {
-            None
-        } else {
-            Entity::from_bits(node_id)
-        }
-    }
-}
-
-pub struct AssetViewerDock;
-
-impl EditorTabDock for AssetViewerDock {
-    fn desc() -> EditorTabDockDescriptor {
-        EditorTabDockDescriptor {
-            id: "asset_viewer",
-            title: "Asset Viewer".to_string(),
-            visibility: EditorTabVisibility::all(),
-        }
-    }
-
-    fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) {
-        viewer.show_asset_viewer(ui);
-    }
-}
diff --git a/crates/eucalyptus-editor/src/editor/build_console.rs b/crates/eucalyptus-editor/src/editor/build_console.rs
deleted file mode 100644
index 99d4a10..0000000
--- a/crates/eucalyptus-editor/src/editor/build_console.rs
+++ /dev/null
@@ -1,162 +0,0 @@
-use std::path::PathBuf;
-
-use egui::{Margin, RichText};
-
-use crate::editor::{
-    EditorTabDock, EditorTabDockDescriptor, EditorTabViewer
-    ,
-};
-use crate::editor::console::{ConsoleItem, ErrorLevel};
-use crate::editor::page::EditorTabVisibility;
-
-impl<'a> EditorTabViewer<'a> {
-    pub fn build_console(&mut self, ui: &mut egui::Ui) {
-        fn analyse_error(log: &Vec<String>) -> Vec<ConsoleItem> {
-            fn parse_compiler_location(line: &str) -> Option<(ErrorLevel, PathBuf, String)> {
-                let trimmed = line.trim_start();
-                let (error_level, rest) = if let Some(r) = trimmed.strip_prefix("e: file:///") {
-                    (ErrorLevel::Error, r)
-                } else if let Some(r) = trimmed.strip_prefix("w: file:///") {
-                    (ErrorLevel::Warn, r)
-                } else {
-                    return None;
-                };
-
-                let location = rest.split_whitespace().next()?;
-
-                let mut segments = location.rsplitn(3, ':');
-                let column = segments.next()?;
-                let row = segments.next()?;
-                let path = segments.next()?;
-
-                Some((error_level, PathBuf::from(path), format!("{row}:{column}")))
-            }
-
-            let mut list: Vec<ConsoleItem> = Vec::new();
-            for (index, line) in log.iter().enumerate() {
-                if line.contains("The required library") {
-                    list.push(ConsoleItem {
-                        error_level: ErrorLevel::Error,
-                        msg: line.clone(),
-                        file_location: None,
-                        line_ref: None,
-                        id: index as u64,
-                    });
-                } else if let Some((error_level, path, loc)) = parse_compiler_location(line) {
-                    list.push(ConsoleItem {
-                        error_level,
-                        msg: line.clone(),
-                        file_location: Some(path),
-                        line_ref: Some(loc),
-                        id: index as u64,
-                    });
-                } else {
-                    list.push(ConsoleItem {
-                        error_level: ErrorLevel::Info,
-                        msg: line.clone(),
-                        file_location: None,
-                        line_ref: None,
-                        id: index as u64,
-                    });
-                }
-            }
-            list
-        }
-
-        let logs = analyse_error(&self.build_logs);
-
-        egui::ScrollArea::vertical()
-            .auto_shrink([false, false])
-            .stick_to_bottom(true)
-            .show(ui, |ui| {
-                if logs.is_empty() {
-                    ui.label("Build output will appear here once available.");
-                    return;
-                }
-
-                for item in &logs {
-                    let (bg_color, text_color, stroke_color) = match item.error_level {
-                        ErrorLevel::Error => (
-                            egui::Color32::from_rgb(60, 20, 20),
-                            egui::Color32::from_rgb(255, 200, 200),
-                            egui::Color32::from_rgb(255, 200, 200),
-                        ),
-                        ErrorLevel::Warn => (
-                            egui::Color32::from_rgb(40, 40, 10),
-                            egui::Color32::from_rgb(255, 255, 200),
-                            egui::Color32::from_rgb(255, 255, 200),
-                        ),
-                        ErrorLevel::Info => (
-                            egui::Color32::TRANSPARENT,
-                            ui.style().visuals.text_color(),
-                            egui::Color32::TRANSPARENT,
-                        ),
-                    };
-
-                    if matches!(item.error_level, ErrorLevel::Info) {
-                        ui.label(RichText::new(&item.msg).monospace());
-                    } else {
-                        let available_width = ui.available_width();
-                        let frame = egui::Frame::new()
-                            .inner_margin(Margin::symmetric(8, 6))
-                            .fill(bg_color)
-                            .stroke(egui::Stroke::new(1.0, stroke_color));
-
-                        let response = frame
-                            .show(ui, |ui| {
-                                ui.set_width(available_width - 10.0);
-                                ui.horizontal(|ui| {
-                                    ui.label(
-                                        RichText::new(&item.msg).color(text_color).monospace(),
-                                    );
-                                });
-                            })
-                            .response;
-
-                        if response.clicked() {
-                            log::debug!("Log item clicked: {}", &item.id);
-                            if let (Some(path), Some(loc)) = (&item.file_location, &item.line_ref) {
-                                let location_arg = format!("{}:{}", path.display(), loc);
-
-                                match std::process::Command::new("code")
-                                    .args(["-g", &location_arg])
-                                    .spawn()
-                                    .map(|_| ())
-                                {
-                                    Ok(()) => {
-                                        log::info!(
-                                            "Launched Visual Studio Code at the error: {}",
-                                            &location_arg
-                                        );
-                                    }
-                                    Err(e) => {
-                                        eucalyptus_core::warn!(
-                                            "Failed to open '{}' in VS Code: {}",
-                                            location_arg,
-                                            e
-                                        );
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-            });
-    }
-}
-
-pub struct BuildConsoleDock;
-
-impl EditorTabDock for BuildConsoleDock {
-    fn desc() -> EditorTabDockDescriptor {
-        EditorTabDockDescriptor {            
-            id: "build_console",            
-            title: "Build Output".to_string(),
-            visibility: EditorTabVisibility::all(), // idk about this one
-        }
-    }
-
-    fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) {
-        viewer.build_console(ui);
-    }
-}
diff --git a/crates/eucalyptus-editor/src/editor/console.rs b/crates/eucalyptus-editor/src/editor/console.rs
deleted file mode 100644
index 980d3c2..0000000
--- a/crates/eucalyptus-editor/src/editor/console.rs
+++ /dev/null
@@ -1,104 +0,0 @@
-use parking_lot::Mutex;
-use std::io::{BufRead, BufReader};
-use std::net::{TcpListener, TcpStream};
-use std::path::PathBuf;
-use std::sync::Arc;
-
-pub struct EucalyptusConsole {
-    pub buffer: Arc<Mutex<Vec<String>>>,
-    pub history: Vec<String>,
-
-    pub show_info: bool,
-    pub show_warning: bool,
-    pub show_error: bool,
-    pub show_debug: bool,
-    pub show_trace: bool,
-    pub auto_scroll: bool,
-}
-
-impl EucalyptusConsole {
-    /// Creates a new instance of a [EucalyptusConsole], and
-    pub fn new(port: Option<&str>) -> Self {
-        let result = Self {
-            buffer: Arc::new(Default::default()),
-            history: vec![],
-            show_info: true,
-            show_warning: true,
-            show_error: true,
-            show_debug: false,
-            show_trace: false,
-            auto_scroll: true,
-        };
-
-        let buf_clone = result.buffer.clone();
-        let addr = format!("127.0.0.1:{}", port.unwrap_or("56624"));
-        std::thread::spawn(move || {
-            let listener = TcpListener::bind(&addr).unwrap();
-
-            log::info!("eucalyptus-editor debug console started at {}", addr);
-
-            loop {
-                for stream in listener.incoming() {
-                    match stream {
-                        Ok(stream) => {
-                            let buf_clone = buf_clone.clone();
-                            std::thread::spawn(move || {
-                                EucalyptusConsole::handle_client(stream, buf_clone)
-                            });
-                        }
-                        Err(e) => {
-                            eprintln!("Connection failed: {}", e);
-                        }
-                    }
-                }
-            }
-        });
-
-        result
-    }
-
-    fn handle_client(stream: TcpStream, buf: Arc<Mutex<Vec<String>>>) {
-        let peer_addr = stream.peer_addr().unwrap();
-        println!("New connection from: {}", peer_addr);
-
-        let reader = BufReader::new(stream);
-
-        for line in reader.lines() {
-            match line {
-                Ok(text) => {
-                    buf.lock().push(text);
-                }
-                Err(e) => {
-                    buf.lock()
-                        .push(format!("Error reading from {}: {}", peer_addr, e));
-                    break;
-                }
-            }
-        }
-
-        println!("Connection closed: {}", peer_addr);
-    }
-
-    /// Drains all from the thread-safe buffer and adds to the history, while returning that value.
-    ///
-    /// It is recommended to use this function.
-    pub fn take(&mut self) -> Vec<String> {
-        let buf = self.buffer.lock().drain(..).collect::<Vec<String>>();
-        buf.iter().for_each(|v| self.history.push(v.clone()));
-        buf
-    }
-}
-
-pub enum ErrorLevel {
-    Info,
-    Warn,
-    Error,
-}
-
-pub struct ConsoleItem {
-    pub id: u64,
-    pub error_level: ErrorLevel,
-    pub msg: String,
-    pub file_location: Option<PathBuf>,
-    pub line_ref: Option<String>,
-}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/dock.rs b/crates/eucalyptus-editor/src/editor/dock.rs
index e89fbca..b8853f4 100644
--- a/crates/eucalyptus-editor/src/editor/dock.rs
+++ b/crates/eucalyptus-editor/src/editor/dock.rs
@@ -2,17 +2,34 @@ use super::*;
 use crate::editor::ViewportMode;
 use std::hash::Hasher;
 use std::{collections::HashMap, hash::Hash, path::PathBuf, sync::LazyLock};
-use crate::editor::console::EucalyptusConsole;
+use crate::editor::docks::console::EucalyptusConsole;
 use crate::plugin::PluginRegistry;
 use dropbear_engine::entity::{EntityTransform, Transform};
 use dropbear_engine::utils::ResourceReference;
 use egui::{self};
 use egui_dock::TabViewer;
+use glam::Vec3;
 use hecs::{Entity, World};
 use parking_lot::Mutex;
 use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation};
 use crate::editor::page::EditorTabVisibility;
 
+/// State for an active click-and-drag operation on a 3D entity in the viewport.
+pub struct DragState {
+    /// The entity being dragged.
+    pub entity: hecs::Entity,
+    /// World-space normal of the drag plane (camera-facing at the initial hit point).
+    pub plane_normal: Vec3,
+    /// Plane equation constant: `plane_normal · p == plane_d`.
+    pub plane_d: f32,
+    /// Offset from the entity's world origin to the initial pick point.
+    pub pick_offset: Vec3,
+    /// Snapshot of the entity's `EntityTransform` at the start of the drag, used for undo.
+    pub initial_entity_transform: Option<EntityTransform>,
+    /// Snapshot of the entity's plain `Transform` at the start of the drag (Transform-only entities).
+    pub initial_transform: Option<Transform>,
+}
+
 pub struct EditorTabViewer<'a> {
     pub view: egui::TextureId,
     pub tex_size: Extent3d,
@@ -34,6 +51,7 @@ pub struct EditorTabViewer<'a> {
     pub eucalyptus_console: &'a mut EucalyptusConsole,
     pub current_scene_name: &'a mut Option<String>,
     pub ui_editor: &'a mut UiEditor,
+    pub viewport_drag: &'a mut Option<DragState>,
 }
 
 pub type EditorTabId = u64;
diff --git a/crates/eucalyptus-editor/src/editor/docks/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/docks/asset_viewer.rs
new file mode 100644
index 0000000..b2e267f
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/docks/asset_viewer.rs
@@ -0,0 +1,1484 @@
+use dropbear_engine::asset::ASSET_REGISTRY;
+use dropbear_engine::model::Model;
+use dropbear_engine::texture::TextureBuilder;
+use dropbear_engine::{graphics::NO_TEXTURE, utils::ResourceReference};
+use egui_ltreeview::{Action, NodeBuilder, TreeViewBuilder};
+use eucalyptus_core::ser::model::EucalyptusModel;
+use eucalyptus_core::states::PROJECT;
+use eucalyptus_core::utils::ResolveReference;
+use hecs::Entity;
+use log::{info, warn};
+use std::hash::{Hash, Hasher};
+use std::{cmp::Ordering, fs, hash::DefaultHasher, io, path::{Path, PathBuf}};
+
+use crate::editor::{
+    AssetDivision, AssetNodeInfo, AssetNodeKind, ComponentNodeSelection, DraggedAsset,
+    EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, FsEntry, ResourceDivision,
+    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> {
+    pub(crate) fn show_asset_viewer(&mut self, ui: &mut egui::Ui) {
+        let mut cfg = TABS_GLOBAL.lock();
+        cfg.asset_node_assets.clear();
+        cfg.asset_node_info.clear();
+        if let Some(rename) = &cfg.asset_rename {
+            if !rename.original_path.exists() {
+                cfg.asset_rename = None;
+            }
+        }
+
+        if let Some(mut rename) = cfg.asset_rename.take() {
+            let rename_id = egui::Id::new("asset_rename_input");
+            let mut should_apply = false;
+
+            ui.horizontal(|ui| {
+                ui.label("Rename");
+                let response = ui.add(egui::TextEdit::singleline(&mut rename.buffer).id(rename_id));
+                if rename.just_started {
+                    ui.ctx().memory_mut(|m| m.request_focus(rename_id));
+                    rename.just_started = false;
+                }
+
+                let enter = ui.input(|input| input.key_pressed(egui::Key::Enter));
+                should_apply = (enter && response.has_focus()) || response.lost_focus();
+            });
+
+            if should_apply {
+                let is_dir = rename.original_path.is_dir();
+                self.apply_asset_rename(&rename, is_dir);
+            } else {
+                cfg.asset_rename = Some(rename);
+            }
+
+            ui.separator();
+        }
+
+        let project_root = {
+            let project = PROJECT.read();
+            if project.project_path.as_os_str().is_empty() {
+                ui.label("Open a project to browse assets.");
+                return;
+            }
+            project.project_path.clone()
+        };
+
+        let (_resp, action) =
+            egui_ltreeview::TreeView::new(egui::Id::new("asset_viewer")).show(ui, |builder| {
+                builder.node(Self::dir_node("euca://"));
+                self.build_resource_branch(&mut cfg, builder, &project_root);
+                self.build_scripts_branch(&mut cfg, builder, &project_root);
+                self.build_scene_branch(&mut cfg, builder, &project_root);
+                builder.close_dir();
+            });
+
+        for a in action {
+            match a {
+                Action::SetSelected(selected) => {
+                    log_once::debug_once!("Selected: {:?}", selected);
+                }
+                Action::Move(moved) => {
+                    log_once::debug_once!("Moved: {:?}", moved);
+                    self.handle_asset_move(&mut cfg, &moved);
+                }
+                Action::Drag(dragged) => {
+                    log_once::debug_once!("Dragged: {:?}", dragged);
+
+                    if let Some(&node_id) = dragged.source.first() {
+                        if let Some(asset) = cfg.asset_node_assets.get(&node_id).cloned() {
+                            cfg.dragged_asset = Some(asset.clone());
+                            ui.ctx().data_mut(|d| {
+                                d.insert_temp(
+                                    egui::Id::new(DRAGGED_ASSET_ID),
+                                    Some(asset.path.clone()),
+                                )
+                            });
+                        }
+                    }
+                }
+                Action::Activate(activated) => {
+                    log_once::debug_once!("Activated: {:?}", activated);
+                }
+                Action::DragExternal(_) => {}
+                Action::MoveExternal(_) => {}
+            }
+        }
+    }
+
+    fn build_resource_branch(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        builder: &mut TreeViewBuilder<u64>,
+        project_root: &Path,
+    ) {
+        let label = "euca://resources";
+        let resources_root = project_root.join("resources");
+        let root_info = AssetNodeInfo {
+            path: resources_root.clone(),
+            division: AssetDivision::Resources,
+            kind: AssetNodeKind::Resource(ResourceDivision::Folder),
+            is_dir: true,
+            is_division_root: true,
+            allow_add_folder: true,
+        };
+        Self::register_asset_node(cfg, label, root_info.clone());
+        let node_id = Self::asset_node_id(label);
+        let menu = Self::dir_node_kind(label, "resources", root_info.kind).context_menu(|ui| {
+            self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Folder")
+        });
+        builder.node(menu);
+        if resources_root.exists() {
+            self.walk_resource_directory(cfg, builder, &resources_root, &resources_root);
+        } else {
+            Self::add_placeholder_leaf(
+                builder,
+                "euca://resources/missing",
+                "missing",
+                AssetNodeKind::Resource(ResourceDivision::File),
+            );
+        }
+        builder.close_dir();
+    }
+
+    fn walk_resource_directory(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        builder: &mut TreeViewBuilder<u64>,
+        base_path: &Path,
+        current_path: &Path,
+    ) {
+        let entries = match Self::sorted_entries(current_path) {
+            Ok(entries) => entries,
+            Err(err) => {
+                log_once::warn_once!(
+                    "Unable to enumerate resources at '{}': {}",
+                    current_path.parent().unwrap_or(current_path).display(),
+                    err
+                );
+                return;
+            }
+        };
+
+        for entry in entries {
+            let full_label = Self::resource_label(base_path, &entry.path);
+            if entry.is_dir {
+                let dir_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Resources,
+                    kind: AssetNodeKind::Resource(ResourceDivision::Folder),
+                    is_dir: true,
+                    is_division_root: false,
+                    allow_add_folder: true,
+                };
+                Self::register_asset_node(cfg, &full_label, dir_info.clone());
+                let node_id = Self::asset_node_id(&full_label);
+                let menu = Self::dir_node_kind(&full_label, &entry.name, dir_info.kind)
+                    .context_menu(|ui| {
+                        self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Folder")
+                    });
+                builder.node(menu);
+                self.walk_resource_directory(cfg, builder, base_path, &entry.path);
+                builder.close_dir();
+            } else {
+                if entry.name.ends_with(".eucmeta") {
+                    continue;
+                }
+
+                let reference = ResourceReference::from_euca_uri(&full_label)
+                    .unwrap_or_else(|_| ResourceReference::default());
+                cfg.asset_node_assets.insert(
+                    Self::asset_node_id(&full_label),
+                    DraggedAsset {
+                        name: entry.name.clone(),
+                        path: reference.clone(),
+                    },
+                );
+                let is_model = Self::is_model_file(&entry.name);
+                let is_texture = Self::is_texture_file(&entry.name);
+                let ext = entry
+                    .path
+                    .extension()
+                    .and_then(|v| v.to_str())
+                    .map(|v| v.to_ascii_lowercase());
+                let is_eucbin = matches!(ext.as_deref(), Some("eucbin"));
+                let is_eucmdl = matches!(ext.as_deref(), Some("eucmdl"));
+                let entry_name = entry.name.clone();
+                let reference_for_menu = reference.clone();
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Resources,
+                    kind: AssetNodeKind::Resource(ResourceDivision::File),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &full_label, file_info.clone());
+                let node_id = Self::asset_node_id(&full_label);
+                let menu = Self::leaf_node_kind(&full_label, &entry.name, file_info.kind)
+                    .context_menu(|ui| {
+                        self.asset_file_context_menu(cfg, ui, node_id, &file_info);
+                        ui.separator();
+
+                        if is_model || is_eucmdl {
+                            if ui.button("Load model to memory").clicked() {
+                                ui.close();
+                                self.queue_model_load(
+                                    reference_for_menu.clone(),
+                                    entry_name.clone(),
+                                );
+                                info!("Loading model {}", entry_name);
+                            }
+                        }
+
+                        if is_texture {
+                            if ui.button("Load texture to memory").clicked() {
+                                ui.close();
+                                self.queue_texture_load(
+                                    reference_for_menu.clone(),
+                                    entry_name.clone(),
+                                    false,
+                                );
+                                info!("Loading texture {}", entry_name);
+                            }
+                        }
+
+                        if is_eucbin {
+                            if ui.button("Load as model to memory").clicked() {
+                                ui.close();
+                                self.queue_model_load(
+                                    reference_for_menu.clone(),
+                                    entry_name.clone(),
+                                );
+                                info!("Loading eucbin as model {}", entry_name);
+                            }
+
+                            if ui.button("Load as texture to memory").clicked() {
+                                ui.close();
+                                self.queue_texture_load(
+                                    reference_for_menu.clone(),
+                                    entry_name.clone(),
+                                    true,
+                                );
+                                info!("Loading eucbin as texture {}", entry_name);
+                            }
+                        }
+                    });
+                builder.node(menu);
+            }
+        }
+    }
+
+    fn resource_label(base_path: &Path, path: &Path) -> String {
+        let relative = path
+            .strip_prefix(base_path)
+            .map(|rel| rel.to_string_lossy().replace('\\', "/"))
+            .unwrap_or_else(|_| path.to_string_lossy().replace('\\', "/"));
+        if relative.is_empty() {
+            "euca://resources".to_string()
+        } else {
+            format!("euca://resources/{}", relative)
+        }
+    }
+
+    fn build_scripts_branch(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        builder: &mut TreeViewBuilder<u64>,
+        project_root: &Path,
+    ) {
+        let label = "euca://scripts";
+        let scripts_root = project_root.join("src");
+        let root_info = AssetNodeInfo {
+            path: scripts_root.clone(),
+            division: AssetDivision::Scripts,
+            kind: AssetNodeKind::Script(ScriptDivision::Package),
+            is_dir: true,
+            is_division_root: true,
+            allow_add_folder: false,
+        };
+        Self::register_asset_node(cfg, label, root_info.clone());
+        let node_id = Self::asset_node_id(label);
+        let menu = Self::dir_node_kind(label, "scripts", root_info.kind).context_menu(|ui| {
+            self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Package")
+        });
+        builder.node(menu);
+        if !scripts_root.exists() {
+            Self::add_placeholder_leaf(
+                builder,
+                "euca://scripts/missing",
+                "missing",
+                AssetNodeKind::Script(ScriptDivision::Script),
+            );
+            builder.close_dir();
+            return;
+        }
+
+        let entries = match Self::sorted_entries(&scripts_root) {
+            Ok(entries) => entries,
+            Err(err) => {
+                log::warn!(
+                    "Unable to enumerate scripts at '{}': {}",
+                    scripts_root.display(),
+                    err
+                );
+                builder.close_dir();
+                return;
+            }
+        };
+
+        let mut had_content = false;
+        for entry in entries {
+            if entry.is_dir {
+                let source_label = format!("{}/{}", label, entry.name);
+                let kotlin_root = entry.path.join("kotlin");
+                let source_info = AssetNodeInfo {
+                    path: kotlin_root,
+                    division: AssetDivision::Scripts,
+                    kind: AssetNodeKind::Script(ScriptDivision::Package),
+                    is_dir: true,
+                    is_division_root: true,
+                    allow_add_folder: true,
+                };
+                Self::register_asset_node(cfg, &source_label, source_info.clone());
+                let node_id = Self::asset_node_id(&source_label);
+                let menu = Self::dir_node_kind(&source_label, &entry.name, source_info.kind)
+                    .context_menu(|ui| {
+                        self.asset_dir_context_menu(cfg, ui, node_id, &source_info, "New Package")
+                    });
+                builder.node(menu);
+                if self.build_script_source_set(cfg, builder, &entry.path, &source_label) {
+                    had_content = true;
+                }
+                builder.close_dir();
+            } else if !entry.name.eq_ignore_ascii_case("source.eucc") {
+                let file_label = format!("{}/{}", label, entry.name);
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Scripts,
+                    kind: AssetNodeKind::Script(ScriptDivision::Script),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &file_label, file_info.clone());
+                let node_id = Self::asset_node_id(&file_label);
+                let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
+                had_content = true;
+            }
+        }
+
+        if !had_content {
+            Self::add_placeholder_leaf(
+                builder,
+                "euca://scripts/empty",
+                "empty",
+                AssetNodeKind::Script(ScriptDivision::Script),
+            );
+        }
+
+        builder.close_dir();
+    }
+
+    fn build_script_source_set(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        builder: &mut TreeViewBuilder<u64>,
+        source_path: &Path,
+        source_label: &str,
+    ) -> bool {
+        let entries = match Self::sorted_entries(source_path) {
+            Ok(entries) => entries,
+            Err(err) => {
+                log::warn!(
+                    "Unable to enumerate source set at '{}': {}",
+                    source_path.display(),
+                    err
+                );
+                Self::add_placeholder_leaf(
+                    builder,
+                    &format!("{source_label}/unreadable"),
+                    "unreadable",
+                    AssetNodeKind::Script(ScriptDivision::Script),
+                );
+                return true;
+            }
+        };
+
+        let mut had_content = false;
+        for entry in entries {
+            if entry.is_dir {
+                if entry.name.eq_ignore_ascii_case("kotlin") {
+                    if self.build_kotlin_tree(cfg, builder, &entry.path, source_label) {
+                        had_content = true;
+                    }
+                } else {
+                    let child_label = format!("{}/{}", source_label, entry.name);
+                    let dir_info = AssetNodeInfo {
+                        path: entry.path.clone(),
+                        division: AssetDivision::Scripts,
+                        kind: AssetNodeKind::Script(ScriptDivision::Package),
+                        is_dir: true,
+                        is_division_root: false,
+                        allow_add_folder: true,
+                    };
+                    Self::register_asset_node(cfg, &child_label, dir_info.clone());
+                    let node_id = Self::asset_node_id(&child_label);
+                    let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind)
+                        .context_menu(|ui| {
+                            self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Package")
+                        });
+                    builder.node(menu);
+                    self.build_plain_directory(
+                        cfg,
+                        builder,
+                        &entry.path,
+                        &child_label,
+                        AssetDivision::Scripts,
+                        AssetNodeKind::Script(ScriptDivision::Package),
+                        AssetNodeKind::Script(ScriptDivision::Script),
+                        "New Package",
+                    );
+                    builder.close_dir();
+                    had_content = true;
+                }
+            } else if !entry.name.eq_ignore_ascii_case("source.eucc") {
+                let file_label = format!("{}/{}", source_label, entry.name);
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Scripts,
+                    kind: AssetNodeKind::Script(ScriptDivision::Script),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &file_label, file_info.clone());
+                let node_id = Self::asset_node_id(&file_label);
+                let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
+                had_content = true;
+            }
+        }
+
+        if !had_content {
+            Self::add_placeholder_leaf(
+                builder,
+                &format!("{source_label}/empty"),
+                "empty",
+                AssetNodeKind::Script(ScriptDivision::Script),
+            );
+            had_content = true;
+        }
+
+        had_content
+    }
+
+    fn build_plain_directory(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        builder: &mut TreeViewBuilder<u64>,
+        dir_path: &Path,
+        parent_label: &str,
+        division: AssetDivision,
+        dir_kind: AssetNodeKind,
+        file_kind: AssetNodeKind,
+        new_folder_label: &str,
+    ) {
+        let entries = match Self::sorted_entries(dir_path) {
+            Ok(entries) => entries,
+            Err(err) => {
+                log::warn!(
+                    "Unable to enumerate directory '{}': {}",
+                    dir_path.display(),
+                    err
+                );
+                Self::add_placeholder_leaf(
+                    builder,
+                    &format!("{parent_label}/unreadable"),
+                    "unreadable",
+                    file_kind,
+                );
+                return;
+            }
+        };
+
+        if entries.is_empty() {
+            Self::add_placeholder_leaf(
+                builder,
+                &format!("{parent_label}/empty"),
+                "empty",
+                file_kind,
+            );
+            return;
+        }
+
+        for entry in entries {
+            let child_label = format!("{}/{}", parent_label, entry.name);
+            if entry.is_dir {
+                let dir_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division,
+                    kind: dir_kind,
+                    is_dir: true,
+                    is_division_root: false,
+                    allow_add_folder: true,
+                };
+                Self::register_asset_node(cfg, &child_label, dir_info.clone());
+                let node_id = Self::asset_node_id(&child_label);
+                let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind)
+                    .context_menu(|ui| {
+                        self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, new_folder_label)
+                    });
+                builder.node(menu);
+                self.build_plain_directory(
+                    cfg,
+                    builder,
+                    &entry.path,
+                    &child_label,
+                    division,
+                    dir_kind,
+                    file_kind,
+                    new_folder_label,
+                );
+                builder.close_dir();
+            } else {
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division,
+                    kind: file_kind,
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &child_label, file_info.clone());
+                let node_id = Self::asset_node_id(&child_label);
+                let menu = Self::leaf_node_kind(&child_label, &entry.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
+            }
+        }
+    }
+
+    fn build_kotlin_tree(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        builder: &mut TreeViewBuilder<u64>,
+        kotlin_path: &Path,
+        source_label: &str,
+    ) -> bool {
+        let entries = match Self::sorted_entries(kotlin_path) {
+            Ok(entries) => entries,
+            Err(err) => {
+                log::warn!(
+                    "Unable to enumerate kotlin sources at '{}': {}",
+                    kotlin_path.display(),
+                    err
+                );
+                Self::add_placeholder_leaf(
+                    builder,
+                    &format!("{source_label}/unreadable"),
+                    "unreadable",
+                    AssetNodeKind::Script(ScriptDivision::Script),
+                );
+                return true;
+            }
+        };
+
+        if entries.is_empty() {
+            Self::add_placeholder_leaf(
+                builder,
+                &format!("{source_label}/no_kotlin_files"),
+                "no kotlin files",
+                AssetNodeKind::Script(ScriptDivision::Script),
+            );
+            return true;
+        }
+
+        let mut had_entries = false;
+        for entry in entries {
+            if entry.is_dir {
+                self.build_kotlin_package_collapsed(
+                    cfg,
+                    builder,
+                    &entry.path,
+                    source_label,
+                    vec![entry.name.clone()],
+                );
+                had_entries = true;
+            } else {
+                let file_id = format!("{}/{}", source_label, entry.name);
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Scripts,
+                    kind: AssetNodeKind::Script(ScriptDivision::Script),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &file_id, file_info.clone());
+                let node_id = Self::asset_node_id(&file_id);
+                let menu = Self::leaf_node_kind(&file_id, &entry.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
+                had_entries = true;
+            }
+        }
+
+        had_entries
+    }
+
+    fn build_kotlin_package_collapsed(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        builder: &mut TreeViewBuilder<u64>,
+        dir_path: &Path,
+        parent_path_str: &str,
+        accumulated_parts: Vec<String>,
+    ) {
+        let entries = match Self::sorted_entries(dir_path) {
+            Ok(entries) => entries,
+            Err(err) => {
+                let package_suffix = accumulated_parts.join(".");
+                let full_path_str = format!("{}/{}", parent_path_str, package_suffix);
+                log::warn!(
+                    "Unable to enumerate package '{}' ({}): {}",
+                    package_suffix,
+                    dir_path.display(),
+                    err
+                );
+                Self::add_placeholder_leaf(
+                    builder,
+                    &format!("{full_path_str}/unreadable"),
+                    "unreadable",
+                    AssetNodeKind::Script(ScriptDivision::Script),
+                );
+                return;
+            }
+        };
+
+        let subdirs: Vec<&FsEntry> = entries.iter().filter(|e| e.is_dir).collect();
+        let files: Vec<&FsEntry> = entries.iter().filter(|e| !e.is_dir).collect();
+
+        if files.is_empty() && subdirs.len() == 1 {
+            let subdir = subdirs[0];
+            let mut new_parts = accumulated_parts;
+            new_parts.push(subdir.name.clone());
+            self.build_kotlin_package_collapsed(
+                cfg,
+                builder,
+                &subdir.path,
+                parent_path_str,
+                new_parts,
+            );
+        } else {
+            let package_suffix = accumulated_parts.join(".");
+            let full_path_str = format!("{}/{}", parent_path_str, package_suffix);
+
+            let dir_info = AssetNodeInfo {
+                path: dir_path.to_path_buf(),
+                division: AssetDivision::Scripts,
+                kind: AssetNodeKind::Script(ScriptDivision::Package),
+                is_dir: true,
+                is_division_root: false,
+                allow_add_folder: true,
+            };
+            Self::register_asset_node(cfg, &full_path_str, dir_info.clone());
+            let node_id = Self::asset_node_id(&full_path_str);
+            let menu = Self::dir_node_kind(&full_path_str, &package_suffix, dir_info.kind)
+                .context_menu(|ui| {
+                    self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Package")
+                });
+            builder.node(menu);
+
+            for file in files {
+                let file_id = format!("{}/{}", full_path_str, file.name);
+                let file_info = AssetNodeInfo {
+                    path: file.path.clone(),
+                    division: AssetDivision::Scripts,
+                    kind: AssetNodeKind::Script(ScriptDivision::Script),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &file_id, file_info.clone());
+                let node_id = Self::asset_node_id(&file_id);
+                let menu = Self::leaf_node_kind(&file_id, &file.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
+            }
+
+            for subdir in subdirs {
+                self.build_kotlin_package_collapsed(
+                    cfg,
+                    builder,
+                    &subdir.path,
+                    &full_path_str,
+                    vec![subdir.name.clone()],
+                );
+            }
+
+            builder.close_dir();
+        }
+    }
+
+    fn build_scene_branch(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        builder: &mut TreeViewBuilder<u64>,
+        project_root: &Path,
+    ) {
+        let label = "euca://scenes";
+        let scenes_root = project_root.join("../../../../../resources").join("scenes");
+        let root_info = AssetNodeInfo {
+            path: scenes_root.clone(),
+            division: AssetDivision::Scenes,
+            kind: AssetNodeKind::Scene(SceneDivision::Folder),
+            is_dir: true,
+            is_division_root: true,
+            allow_add_folder: true,
+        };
+        Self::register_asset_node(cfg, label, root_info.clone());
+        let node_id = Self::asset_node_id(label);
+        let menu = Self::dir_node_kind(label, "scenes", root_info.kind).context_menu(|ui| {
+            self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Folder")
+        });
+        builder.node(menu);
+        if !scenes_root.exists() {
+            Self::add_placeholder_leaf(
+                builder,
+                "euca://scenes/missing",
+                "missing",
+                AssetNodeKind::Scene(SceneDivision::Scene),
+            );
+            builder.close_dir();
+            return;
+        }
+
+        let entries = match Self::sorted_entries(&scenes_root) {
+            Ok(entries) => entries,
+            Err(err) => {
+                log::warn!(
+                    "Unable to enumerate scenes at '{}': {}",
+                    scenes_root.display(),
+                    err
+                );
+                Self::add_placeholder_leaf(
+                    builder,
+                    "euca://scenes/unreadable",
+                    "unreadable",
+                    AssetNodeKind::Scene(SceneDivision::Scene),
+                );
+                builder.close_dir();
+                return;
+            }
+        };
+
+        let mut had_entries = false;
+        for entry in entries {
+            if entry.is_dir {
+                let child_label = format!("{}/{}", label, entry.name);
+                let dir_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Scenes,
+                    kind: AssetNodeKind::Scene(SceneDivision::Folder),
+                    is_dir: true,
+                    is_division_root: false,
+                    allow_add_folder: true,
+                };
+                Self::register_asset_node(cfg, &child_label, dir_info.clone());
+                let node_id = Self::asset_node_id(&child_label);
+                let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind)
+                    .context_menu(|ui| {
+                        self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Folder")
+                    });
+                builder.node(menu);
+                self.build_plain_directory(
+                    cfg,
+                    builder,
+                    &entry.path,
+                    &child_label,
+                    AssetDivision::Scenes,
+                    AssetNodeKind::Scene(SceneDivision::Folder),
+                    AssetNodeKind::Scene(SceneDivision::Scene),
+                    "New Folder",
+                );
+                builder.close_dir();
+                had_entries = true;
+            } else if entry
+                .path
+                .extension()
+                .and_then(|ext| ext.to_str())
+                .map(|ext| ext.eq_ignore_ascii_case("eucs"))
+                .unwrap_or(false)
+            {
+                let file_label = format!("{}/{}", label, entry.name);
+                let file_info = AssetNodeInfo {
+                    path: entry.path.clone(),
+                    division: AssetDivision::Scenes,
+                    kind: AssetNodeKind::Scene(SceneDivision::Scene),
+                    is_dir: false,
+                    is_division_root: false,
+                    allow_add_folder: false,
+                };
+                Self::register_asset_node(cfg, &file_label, file_info.clone());
+                let node_id = Self::asset_node_id(&file_label);
+                let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind)
+                    .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info));
+                builder.node(menu);
+                had_entries = true;
+            }
+        }
+
+        if !had_entries {
+            Self::add_placeholder_leaf(
+                builder,
+                "euca://scenes/no_scenes",
+                "no scenes",
+                AssetNodeKind::Scene(SceneDivision::Scene),
+            );
+        }
+
+        builder.close_dir();
+    }
+
+    fn sorted_entries(path: &Path) -> io::Result<Vec<FsEntry>> {
+        let mut entries = Vec::new();
+        for entry in fs::read_dir(path)? {
+            let entry = entry?;
+            let file_type = entry.file_type()?;
+            let name = entry.file_name().to_string_lossy().to_string();
+            entries.push(FsEntry {
+                path: entry.path(),
+                name_lower: name.to_lowercase(),
+                name,
+                is_dir: file_type.is_dir(),
+            });
+        }
+
+        entries.sort_by(|a, b| match b.is_dir.cmp(&a.is_dir) {
+            Ordering::Equal => a.name_lower.cmp(&b.name_lower),
+            other => other,
+        });
+
+        Ok(entries)
+    }
+
+    fn asset_node_id(label: &str) -> u64 {
+        let mut hasher = DefaultHasher::new();
+        label.hash(&mut hasher);
+        let mut id = hasher.finish();
+        if id == 0 {
+            id = 1;
+        }
+        id
+    }
+
+    fn register_asset_node(cfg: &mut StaticallyKept, id_source: &str, info: AssetNodeInfo) -> u64 {
+        let node_id = Self::asset_node_id(id_source);
+        cfg.asset_node_info.insert(node_id, info);
+        node_id
+    }
+
+    fn dir_node<'ui>(label: &str) -> NodeBuilder<'ui, u64> {
+        Self::with_icon_kind(
+            NodeBuilder::dir(Self::asset_node_id(label)).label(label.to_string()),
+            AssetNodeKind::Resource(ResourceDivision::Folder),
+        )
+    }
+
+    fn dir_node_kind<'ui>(
+        id_source: &str,
+        label: &str,
+        kind: AssetNodeKind,
+    ) -> NodeBuilder<'ui, u64> {
+        Self::with_icon_kind(
+            NodeBuilder::dir(Self::asset_node_id(id_source)).label(label.to_string()),
+            kind,
+        )
+    }
+
+    fn leaf_node_kind<'ui>(
+        id_source: &str,
+        label: &str,
+        kind: AssetNodeKind,
+    ) -> NodeBuilder<'ui, u64> {
+        Self::with_icon_kind(
+            NodeBuilder::leaf(Self::asset_node_id(id_source)).label(label.to_string()),
+            kind,
+        )
+    }
+
+    fn with_icon_kind(
+        builder: NodeBuilder<u64>,
+        kind: AssetNodeKind,
+    ) -> NodeBuilder<u64> {
+        builder.icon(move |ui| {
+            egui_extras::install_image_loaders(ui.ctx());
+            Self::draw_asset_icon(ui, kind)
+        })
+    }
+
+    fn draw_asset_icon(ui: &mut egui::Ui, _kind: AssetNodeKind) {
+        let image = egui::Image::from_bytes("bytes://asset-viewer-icon", NO_TEXTURE)
+            .max_size(egui::vec2(14.0, 14.0));
+        ui.add(image);
+    }
+
+    fn add_placeholder_leaf(
+        builder: &mut TreeViewBuilder<u64>,
+        id_source: &str,
+        label: &str,
+        kind: AssetNodeKind,
+    ) {
+        builder.node(Self::leaf_node_kind(id_source, label, kind));
+    }
+
+    fn asset_dir_context_menu(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        ui: &mut egui::Ui,
+        node_id: u64,
+        info: &AssetNodeInfo,
+        new_folder_label: &str,
+    ) {
+        if info.allow_add_folder {
+            if ui.button(new_folder_label).clicked() {
+                ui.close();
+                let base_name = if info.division == AssetDivision::Scripts {
+                    "newpackage"
+                } else {
+                    "New Folder"
+                };
+                self.create_asset_folder(&info.path, base_name);
+            }
+        }
+
+        if ui.button("Paste").clicked() {
+            ui.close();
+            if !info.path.exists() {
+                if let Err(err) = fs::create_dir_all(&info.path) {
+                    warn!("Unable to create folder '{}': {}", info.path.display(), err);
+                    return;
+                }
+            }
+            self.signal.push_back(Signal::AssetPaste {
+                target_dir: info.path.clone(),
+                division: info.division,
+            });
+        }
+
+        if ui.button("Reveal Folder").clicked() {
+            ui.close();
+            if let Err(err) = open::that(&info.path) {
+                warn!("Unable to reveal folder: {}", err);
+            }
+        }
+
+        if !info.is_division_root && ui.button("Rename").clicked() {
+            let current_name = info
+                .path
+                .file_name()
+                .and_then(|name| name.to_str())
+                .unwrap_or("")
+                .to_string();
+            cfg.asset_rename = Some(crate::editor::AssetRenameState {
+                node_id,
+                original_path: info.path.clone(),
+                buffer: current_name,
+                just_started: true,
+            });
+        }
+
+        if !info.is_division_root && ui.button("Delete").clicked() {
+            ui.close();
+            self.delete_asset_entry(info);
+        }
+    }
+
+    fn asset_file_context_menu(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        ui: &mut egui::Ui,
+        node_id: u64,
+        info: &AssetNodeInfo,
+    ) {
+        if matches!(info.kind, AssetNodeKind::Script(ScriptDivision::Script))
+            && ui.button("Open Script").clicked()
+        {
+            ui.close();
+            if let Err(err) = open::that(&info.path) {
+                warn!("Unable to open script: {}", err);
+            }
+        }
+
+        if ui.button("Rename").clicked() {
+            let current_name = info
+                .path
+                .file_name()
+                .and_then(|name| name.to_str())
+                .unwrap_or("")
+                .to_string();
+            cfg.asset_rename = Some(crate::editor::AssetRenameState {
+                node_id,
+                original_path: info.path.clone(),
+                buffer: current_name,
+                just_started: true,
+            });
+        }
+
+        if ui.button("Copy").clicked() {
+            ui.close();
+            self.signal.push_back(Signal::AssetCopy {
+                source: info.path.clone(),
+                division: info.division,
+            });
+        }
+
+        if ui.button("Delete").clicked() {
+            ui.close();
+            self.delete_asset_entry(info);
+        }
+    }
+
+    fn apply_asset_rename(&self, rename: &crate::editor::AssetRenameState, is_dir: bool) {
+        let trimmed = rename.buffer.trim();
+        if trimmed.is_empty() {
+            warn!("Rename cancelled: empty name");
+            return;
+        }
+
+        let Some(parent) = rename.original_path.parent() else {
+            warn!("Unable to rename: missing parent directory");
+            return;
+        };
+
+        let mut new_name = trimmed.to_string();
+        if !is_dir {
+            if let Some(ext) = rename.original_path.extension().and_then(|e| e.to_str()) {
+                let has_ext = Path::new(&new_name)
+                    .extension()
+                    .and_then(|e| e.to_str())
+                    .is_some();
+                if !has_ext {
+                    new_name = format!("{new_name}.{ext}");
+                }
+            }
+        }
+
+        let target_path = parent.join(&new_name);
+        if target_path == rename.original_path {
+            return;
+        }
+
+        if target_path.exists() {
+            warn!("Rename target already exists: {}", target_path.display());
+            return;
+        }
+
+        if let Err(err) = fs::rename(&rename.original_path, &target_path) {
+            warn!(
+                "Failed to rename '{}': {}",
+                rename.original_path.display(),
+                err
+            );
+        } else {
+            info!("Renamed to {}", target_path.display());
+            let old_meta = PathBuf::from(format!("{}.eucmeta", rename.original_path.display()));
+            if old_meta.exists() {
+                let new_meta = PathBuf::from(format!("{}.eucmeta", target_path.display()));
+                if let Err(e) = fs::rename(&old_meta, &new_meta) {
+                    warn!("Failed to move .eucmeta sidecar '{}': {}", old_meta.display(), e);
+                }
+            }
+        }
+    }
+
+    fn delete_asset_entry(&self, info: &AssetNodeInfo) {
+        if info.is_division_root {
+            warn!("Cannot delete division root");
+            return;
+        }
+
+        let result = if info.is_dir {
+            fs::remove_dir_all(&info.path)
+        } else {
+            fs::remove_file(&info.path)
+        };
+
+        if let Err(err) = result {
+            warn!("Failed to delete '{}': {}", info.path.display(), err);
+        } else {
+            info!("Deleted {}", info.path.display());
+            if !info.is_dir {
+                let meta = PathBuf::from(format!("{}.eucmeta", info.path.display()));
+                if meta.exists() {
+                    if let Err(e) = fs::remove_file(&meta) {
+                        warn!("Failed to remove .eucmeta sidecar '{}': {}", meta.display(), e);
+                    }
+                }
+            }
+        }
+    }
+
+    fn create_asset_folder(&self, base_dir: &Path, base_name: &str) {
+        if let Err(err) = fs::create_dir_all(base_dir) {
+            warn!("Unable to create folder '{}': {}", base_dir.display(), err);
+            return;
+        }
+
+        let mut candidate = base_dir.join(base_name);
+        if candidate.exists() {
+            let mut index = 1;
+            loop {
+                let suffix = if base_name.contains(' ') {
+                    format!(" {}", index)
+                } else {
+                    format!("{}", index)
+                };
+                candidate = base_dir.join(format!("{base_name}{suffix}"));
+                if !candidate.exists() {
+                    break;
+                }
+                index += 1;
+            }
+        }
+
+        if let Err(err) = fs::create_dir_all(&candidate) {
+            warn!("Unable to create folder '{}': {}", candidate.display(), err);
+        } else {
+            info!("Created folder {}", candidate.display());
+        }
+    }
+
+    fn handle_asset_move(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        drag: &egui_ltreeview::DragAndDrop<u64>,
+    ) {
+        let Some(&source_id) = drag.source.first() else {
+            return;
+        };
+        let Some(source_info) = cfg.asset_node_info.get(&source_id).cloned() else {
+            return;
+        };
+        let Some(target_info) = cfg.asset_node_info.get(&drag.target).cloned() else {
+            return;
+        };
+
+        if source_info.is_division_root {
+            warn!("Cannot move division root");
+            return;
+        }
+
+        if source_info.division != target_info.division {
+            warn!("Cannot move assets across divisions");
+            return;
+        }
+
+        let target_dir = if target_info.is_dir {
+            target_info.path.clone()
+        } else {
+            target_info
+                .path
+                .parent()
+                .unwrap_or(&target_info.path)
+                .to_path_buf()
+        };
+
+        if !target_dir.exists() {
+            if let Err(err) = fs::create_dir_all(&target_dir) {
+                warn!("Target directory does not exist: {}", err);
+                return;
+            }
+        }
+
+        if source_info.is_dir && target_dir.starts_with(&source_info.path) {
+            warn!("Cannot move a folder into itself");
+            return;
+        }
+
+        let Some(name) = source_info.path.file_name() else {
+            warn!("Unable to move: invalid file name");
+            return;
+        };
+
+        let target_path = target_dir.join(name);
+        if target_path == source_info.path {
+            return;
+        }
+
+        if target_path.exists() {
+            warn!("Target already exists: {}", target_path.display());
+            return;
+        }
+
+        if let Err(err) = fs::rename(&source_info.path, &target_path) {
+            warn!("Failed to move '{}': {}", source_info.path.display(), err);
+        } else {
+            info!("Moved to {}", target_path.display());
+            if !source_info.is_dir {
+                let old_meta = PathBuf::from(format!("{}.eucmeta", source_info.path.display()));
+                if old_meta.exists() {
+                    let new_meta = PathBuf::from(format!("{}.eucmeta", target_path.display()));
+                    if let Err(e) = fs::rename(&old_meta, &new_meta) {
+                        warn!("Failed to move .eucmeta sidecar '{}': {}", old_meta.display(), e);
+                    }
+                }
+            }
+        }
+    }
+
+    fn is_model_file(name: &str) -> bool {
+        let name = name.to_ascii_lowercase();
+        name.ends_with(".glb") || name.ends_with(".gltf")
+    }
+
+    fn is_texture_file(name: &str) -> bool {
+        let name = name.to_ascii_lowercase();
+        name.ends_with(".png")
+            || name.ends_with(".jpg")
+            || name.ends_with(".jpeg")
+            || name.ends_with(".tga")
+            || name.ends_with(".bmp")
+            || name.ends_with(".webp")
+    }
+
+    fn queue_model_load(&self, reference: ResourceReference, label: String) {
+        if ASSET_REGISTRY
+            .read()
+            .get_model_handle_by_reference(&reference)
+            .is_some()
+        {
+            eucalyptus_core::info!("Model already loaded: {}", label);
+            return;
+        }
+
+        let graphics = self.graphics.clone();
+        let queue = graphics.future_queue.clone();
+        queue.push(async move {
+            let path = reference.resolve()?;
+            let buffer = fs::read(&path)?;
+            let extension = path
+                .extension()
+                .and_then(|ext| ext.to_str())
+                .map(|ext| ext.to_ascii_lowercase());
+
+            let handle = match extension.as_deref() {
+                Some("eucmdl") => {
+                    let model = rkyv::from_bytes::<EucalyptusModel, rkyv::rancor::Error>(&buffer)
+                        .map_err(|e| anyhow::anyhow!(
+                            "Unable to deserialize .eucmdl model '{}': {}",
+                            path.display(),
+                            e
+                        ))?;
+
+                    let runtime_model = model.load(reference.clone(), graphics.clone());
+                    let mut registry = ASSET_REGISTRY.write();
+                    registry.add_model_with_label(label.clone(), runtime_model)
+                }
+                _ => match Model::load_from_memory_raw(
+                    graphics.clone(),
+                    buffer,
+                    Some(reference.clone()),
+                    Some(label.as_str()),
+                    ASSET_REGISTRY.clone(),
+                )
+                .await
+                {
+                    Ok(v) => v,
+                    Err(e) => {
+                        eucalyptus_core::warn!("Unable to load model {}: {}", reference, e);
+                        return Err(e);
+                    }
+                },
+            };
+
+            let mut registry = ASSET_REGISTRY.write();
+            registry.label_model(label.clone(), handle);
+
+            eucalyptus_core::success!("Loaded model {}", label);
+            Ok::<(), anyhow::Error>(())
+        });
+    }
+
+    fn queue_texture_load(&self, reference: ResourceReference, label: String, strict_image_decode: bool) {
+        if ASSET_REGISTRY
+            .read()
+            .get_texture_handle_by_reference(&reference)
+            .is_some()
+        {
+            eucalyptus_core::info!("Texture already loaded: {}", label);
+            return;
+        }
+
+        let graphics = self.graphics.clone();
+        let queue = graphics.future_queue.clone();
+        queue.push(async move {
+            let path = reference.resolve()?;
+            let bytes = fs::read(&path)?;
+
+            if strict_image_decode
+                && let Err(err) = image::load_from_memory(bytes.as_slice())
+            {
+                let error = anyhow::anyhow!(
+                    "'{}' is not a texture-compatible eucbin payload: {}",
+                    path.display(),
+                    err
+                );
+                eucalyptus_core::warn!("{}", error);
+                return Err(error);
+            }
+
+            let mut texture = TextureBuilder::new(&graphics.device)
+                .with_bytes(graphics.clone(), bytes.as_slice())
+                .label(label.as_str())
+                .build();
+            texture.reference = Some(reference.clone());
+            let mut registry = ASSET_REGISTRY.write();
+            registry.add_texture_with_label(label.clone(), texture);
+            Ok::<(), anyhow::Error>(())
+        });
+    }
+
+    pub(crate) fn handle_tree_selection(&mut self, cfg: &mut StaticallyKept, items: &[u64]) {
+        for node_id in items {
+            self.resolve_tree_node(cfg, *node_id);
+        }
+    }
+
+    pub(crate) fn handle_tree_activate(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        activate: &egui_ltreeview::Activate<u64>,
+    ) {
+        self.handle_tree_selection(cfg, &activate.selected);
+    }
+
+    pub(crate) fn handle_tree_drag(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        drag: &egui_ltreeview::DragAndDrop<u64>,
+    ) {
+        if let Some(&node_id) = drag.source.first() {
+            if let Some(selection) = cfg.component_selection(node_id) {
+                cfg.pending_component_drag = Some(selection);
+                self.inspect_component_selection(cfg, selection);
+            }
+        }
+    }
+
+    pub(crate) fn handle_tree_move(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        drag: &egui_ltreeview::DragAndDrop<u64>,
+    ) {
+        let selection = cfg.pending_component_drag.take().or_else(|| {
+            drag.source
+                .first()
+                .and_then(|node_id| cfg.component_selection(*node_id))
+        });
+
+        if let Some(selection) = selection {
+            self.inspect_component_selection(cfg, selection);
+            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);
+            }
+        }
+    }
+
+    fn resolve_tree_node(&mut self, cfg: &mut StaticallyKept, node_id: u64) {
+        if node_id == u64::MAX {
+            log_once::debug_once!("Root node has been selected");
+            cfg.root_node_selected = true;
+            *self.selected_entity = None;
+        } else if let Some(selection) = cfg.component_selection(node_id) {
+            cfg.root_node_selected = false;
+            *self.selected_entity = selection.entity();
+            self.inspect_component_selection(cfg, selection);
+        } else if let Some(entity) = Self::entity_from_node_id(node_id) {
+            cfg.root_node_selected = false;
+            *self.selected_entity = Some(entity);
+        }
+    }
+
+    fn inspect_component_selection(
+        &mut self,
+        cfg: &mut StaticallyKept,
+        selection: ComponentNodeSelection,
+    ) {
+        cfg.remember_component_lookup(selection);
+        let component_id = selection.component_type_id;
+        let matches = self
+            .component_registry
+            .find_entities_by_numeric_id(self.world, component_id);
+        let descriptor = self
+            .component_registry
+            .get_descriptor_by_numeric_id(component_id);
+
+        if matches.is_empty() {
+            log::warn!("Component id #{} not found in world", component_id);
+            return;
+        }
+
+        let name = descriptor
+            .map(|desc| desc.fqtn.as_str())
+            .unwrap_or("<unknown>");
+        for entity in matches {
+            log::debug!(
+                "Serializable component '{}' (id #{}) attached to entity {:?}",
+                name,
+                component_id,
+                entity
+            );
+        }
+    }
+
+    fn entity_from_node_id(node_id: u64) -> Option<Entity> {
+        if node_id == u64::MAX {
+            None
+        } else {
+            Entity::from_bits(node_id)
+        }
+    }
+}
+
+pub struct AssetViewerDock;
+
+impl EditorTabDock for AssetViewerDock {
+    fn desc() -> EditorTabDockDescriptor {
+        EditorTabDockDescriptor {
+            id: "asset_viewer",
+            title: "Asset Viewer".to_string(),
+            visibility: EditorTabVisibility::all(),
+        }
+    }
+
+    fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) {
+        viewer.show_asset_viewer(ui);
+    }
+}
diff --git a/crates/eucalyptus-editor/src/editor/docks/build_console.rs b/crates/eucalyptus-editor/src/editor/docks/build_console.rs
new file mode 100644
index 0000000..bddd268
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/docks/build_console.rs
@@ -0,0 +1,162 @@
+use std::path::PathBuf;
+
+use egui::{Margin, RichText};
+
+use crate::editor::{
+    EditorTabDock, EditorTabDockDescriptor, EditorTabViewer
+    ,
+};
+use crate::editor::docks::console::{ConsoleItem, ErrorLevel};
+use crate::editor::page::EditorTabVisibility;
+
+impl<'a> EditorTabViewer<'a> {
+    pub fn build_console(&mut self, ui: &mut egui::Ui) {
+        fn analyse_error(log: &Vec<String>) -> Vec<ConsoleItem> {
+            fn parse_compiler_location(line: &str) -> Option<(ErrorLevel, PathBuf, String)> {
+                let trimmed = line.trim_start();
+                let (error_level, rest) = if let Some(r) = trimmed.strip_prefix("e: file:///") {
+                    (ErrorLevel::Error, r)
+                } else if let Some(r) = trimmed.strip_prefix("w: file:///") {
+                    (ErrorLevel::Warn, r)
+                } else {
+                    return None;
+                };
+
+                let location = rest.split_whitespace().next()?;
+
+                let mut segments = location.rsplitn(3, ':');
+                let column = segments.next()?;
+                let row = segments.next()?;
+                let path = segments.next()?;
+
+                Some((error_level, PathBuf::from(path), format!("{row}:{column}")))
+            }
+
+            let mut list: Vec<ConsoleItem> = Vec::new();
+            for (index, line) in log.iter().enumerate() {
+                if line.contains("The required library") {
+                    list.push(ConsoleItem {
+                        error_level: ErrorLevel::Error,
+                        msg: line.clone(),
+                        file_location: None,
+                        line_ref: None,
+                        id: index as u64,
+                    });
+                } else if let Some((error_level, path, loc)) = parse_compiler_location(line) {
+                    list.push(ConsoleItem {
+                        error_level,
+                        msg: line.clone(),
+                        file_location: Some(path),
+                        line_ref: Some(loc),
+                        id: index as u64,
+                    });
+                } else {
+                    list.push(ConsoleItem {
+                        error_level: ErrorLevel::Info,
+                        msg: line.clone(),
+                        file_location: None,
+                        line_ref: None,
+                        id: index as u64,
+                    });
+                }
+            }
+            list
+        }
+
+        let logs = analyse_error(&self.build_logs);
+
+        egui::ScrollArea::vertical()
+            .auto_shrink([false, false])
+            .stick_to_bottom(true)
+            .show(ui, |ui| {
+                if logs.is_empty() {
+                    ui.label("Build output will appear here once available.");
+                    return;
+                }
+
+                for item in &logs {
+                    let (bg_color, text_color, stroke_color) = match item.error_level {
+                        ErrorLevel::Error => (
+                            egui::Color32::from_rgb(60, 20, 20),
+                            egui::Color32::from_rgb(255, 200, 200),
+                            egui::Color32::from_rgb(255, 200, 200),
+                        ),
+                        ErrorLevel::Warn => (
+                            egui::Color32::from_rgb(40, 40, 10),
+                            egui::Color32::from_rgb(255, 255, 200),
+                            egui::Color32::from_rgb(255, 255, 200),
+                        ),
+                        ErrorLevel::Info => (
+                            egui::Color32::TRANSPARENT,
+                            ui.style().visuals.text_color(),
+                            egui::Color32::TRANSPARENT,
+                        ),
+                    };
+
+                    if matches!(item.error_level, ErrorLevel::Info) {
+                        ui.label(RichText::new(&item.msg).monospace());
+                    } else {
+                        let available_width = ui.available_width();
+                        let frame = egui::Frame::new()
+                            .inner_margin(Margin::symmetric(8, 6))
+                            .fill(bg_color)
+                            .stroke(egui::Stroke::new(1.0, stroke_color));
+
+                        let response = frame
+                            .show(ui, |ui| {
+                                ui.set_width(available_width - 10.0);
+                                ui.horizontal(|ui| {
+                                    ui.label(
+                                        RichText::new(&item.msg).color(text_color).monospace(),
+                                    );
+                                });
+                            })
+                            .response;
+
+                        if response.clicked() {
+                            log::debug!("Log item clicked: {}", &item.id);
+                            if let (Some(path), Some(loc)) = (&item.file_location, &item.line_ref) {
+                                let location_arg = format!("{}:{}", path.display(), loc);
+
+                                match std::process::Command::new("code")
+                                    .args(["-g", &location_arg])
+                                    .spawn()
+                                    .map(|_| ())
+                                {
+                                    Ok(()) => {
+                                        log::info!(
+                                            "Launched Visual Studio Code at the error: {}",
+                                            &location_arg
+                                        );
+                                    }
+                                    Err(e) => {
+                                        eucalyptus_core::warn!(
+                                            "Failed to open '{}' in VS Code: {}",
+                                            location_arg,
+                                            e
+                                        );
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            });
+    }
+}
+
+pub struct BuildConsoleDock;
+
+impl EditorTabDock for BuildConsoleDock {
+    fn desc() -> EditorTabDockDescriptor {
+        EditorTabDockDescriptor {            
+            id: "build_console",            
+            title: "Build Output".to_string(),
+            visibility: EditorTabVisibility::all(), // idk about this one
+        }
+    }
+
+    fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) {
+        viewer.build_console(ui);
+    }
+}
diff --git a/crates/eucalyptus-editor/src/editor/docks/console.rs b/crates/eucalyptus-editor/src/editor/docks/console.rs
new file mode 100644
index 0000000..980d3c2
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/docks/console.rs
@@ -0,0 +1,104 @@
+use parking_lot::Mutex;
+use std::io::{BufRead, BufReader};
+use std::net::{TcpListener, TcpStream};
+use std::path::PathBuf;
+use std::sync::Arc;
+
+pub struct EucalyptusConsole {
+    pub buffer: Arc<Mutex<Vec<String>>>,
+    pub history: Vec<String>,
+
+    pub show_info: bool,
+    pub show_warning: bool,
+    pub show_error: bool,
+    pub show_debug: bool,
+    pub show_trace: bool,
+    pub auto_scroll: bool,
+}
+
+impl EucalyptusConsole {
+    /// Creates a new instance of a [EucalyptusConsole], and
+    pub fn new(port: Option<&str>) -> Self {
+        let result = Self {
+            buffer: Arc::new(Default::default()),
+            history: vec![],
+            show_info: true,
+            show_warning: true,
+            show_error: true,
+            show_debug: false,
+            show_trace: false,
+            auto_scroll: true,
+        };
+
+        let buf_clone = result.buffer.clone();
+        let addr = format!("127.0.0.1:{}", port.unwrap_or("56624"));
+        std::thread::spawn(move || {
+            let listener = TcpListener::bind(&addr).unwrap();
+
+            log::info!("eucalyptus-editor debug console started at {}", addr);
+
+            loop {
+                for stream in listener.incoming() {
+                    match stream {
+                        Ok(stream) => {
+                            let buf_clone = buf_clone.clone();
+                            std::thread::spawn(move || {
+                                EucalyptusConsole::handle_client(stream, buf_clone)
+                            });
+                        }
+                        Err(e) => {
+                            eprintln!("Connection failed: {}", e);
+                        }
+                    }
+                }
+            }
+        });
+
+        result
+    }
+
+    fn handle_client(stream: TcpStream, buf: Arc<Mutex<Vec<String>>>) {
+        let peer_addr = stream.peer_addr().unwrap();
+        println!("New connection from: {}", peer_addr);
+
+        let reader = BufReader::new(stream);
+
+        for line in reader.lines() {
+            match line {
+                Ok(text) => {
+                    buf.lock().push(text);
+                }
+                Err(e) => {
+                    buf.lock()
+                        .push(format!("Error reading from {}: {}", peer_addr, e));
+                    break;
+                }
+            }
+        }
+
+        println!("Connection closed: {}", peer_addr);
+    }
+
+    /// Drains all from the thread-safe buffer and adds to the history, while returning that value.
+    ///
+    /// It is recommended to use this function.
+    pub fn take(&mut self) -> Vec<String> {
+        let buf = self.buffer.lock().drain(..).collect::<Vec<String>>();
+        buf.iter().for_each(|v| self.history.push(v.clone()));
+        buf
+    }
+}
+
+pub enum ErrorLevel {
+    Info,
+    Warn,
+    Error,
+}
+
+pub struct ConsoleItem {
+    pub id: u64,
+    pub error_level: ErrorLevel,
+    pub msg: String,
+    pub file_location: Option<PathBuf>,
+    pub line_ref: Option<String>,
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/docks/entity_list.rs b/crates/eucalyptus-editor/src/editor/docks/entity_list.rs
new file mode 100644
index 0000000..bf2a525
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/docks/entity_list.rs
@@ -0,0 +1,308 @@
+use egui_ltreeview::{NodeBuilder, TreeViewBuilder};
+use eucalyptus_core::{
+    component::ComponentRegistry,
+    hierarchy::{Children, Hierarchy, Parent},
+    physics::{collider::ColliderGroup, rigidbody::RigidBody},
+    states::{Label, PROJECT},
+};
+use hecs::{Entity, World};
+use std::collections::{BTreeMap, HashMap, VecDeque};
+
+use crate::editor::{Editor, EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, StaticallyKept, TABS_GLOBAL};
+use crate::editor::page::EditorTabVisibility;
+
+impl<'a> EditorTabViewer<'a> {
+    pub(crate) fn entity_list(&mut self, ui: &mut egui::Ui) {
+        puffin::profile_function!();
+        let mut cfg = TABS_GLOBAL.lock();
+
+        let (_response, action) = {
+            puffin::profile_scope!("entity_list.tree_build");
+            egui_ltreeview::TreeView::new(egui::Id::new(
+            "model_entity_list",
+        ))
+            .show(ui, |builder| {
+                let current_scene_name = {
+                    PROJECT
+                        .read()
+                        .last_opened_scene
+                        .clone()
+                        .unwrap_or("Scene".to_string())
+                };
+                builder.node(
+                    // root node/scene display
+                    NodeBuilder::dir(u64::MAX)
+                        .label(format!("Scene: {}", current_scene_name))
+                        .context_menu(|ui| {
+                            if ui.button("New Empty Entity").clicked() {
+                                let label = Editor::unique_label_for_world(self.world, "Blank Entity");
+                                self.world.spawn((label,));
+                                ui.close();
+                            }
+                            ui.menu_button("Import Template", |_| {
+
+                            });
+                        }),
+                );
+                // the root scene must be the biggest number possible to remove any ambiguity
+
+                fn add_entity_to_tree(
+                    builder: &mut TreeViewBuilder<u64>,
+                    entity: Entity,
+                    world: &mut World,
+                    registry: &ComponentRegistry,
+                    component_ids_by_entity: &HashMap<Entity, Vec<u64>>,
+                    rigidbody_component_id: Option<u64>,
+                    cfg: &mut StaticallyKept,
+                    signal: &mut VecDeque<Signal>,
+                ) -> anyhow::Result<()> {
+                    puffin::profile_scope!("entity_list.add_entity_to_tree");
+                    let entity_id = entity.to_bits().get();
+                    let label = if let Ok(label) = world.query_one::<&Label>(entity).get()
+                    {
+                        label.clone()
+                    } else {
+                        anyhow::bail!(
+                                "This entity [{}] is expected to contain Label",
+                                entity_id
+                            );
+                    };
+
+                    builder.node(
+                        NodeBuilder::dir(entity_id)
+                            .label(label.as_str())
+                            .context_menu(|ui| {
+                                ui.menu_button("New", |ui| {
+                                    if ui.button("Child").clicked() {
+                                        let label = Editor::unique_label_for_world(world, "New Entity");
+                                        let child = world.spawn((label,));
+                                        Hierarchy::set_parent(world, child, entity);
+                                        ui.close();
+                                    }
+                                });
+                                ui.menu_button("Add", |ui| {
+                                    let mut grouped_components: BTreeMap<String, Vec<(u64, &eucalyptus_core::component::ComponentDescriptor)>> =
+                                        BTreeMap::new();
+
+                                    for (id, desc) in registry.iter_available_components() {
+                                        let category = desc
+                                            .category
+                                            .clone()
+                                            .unwrap_or_else(|| "Uncategorised".to_string());
+                                        grouped_components
+                                            .entry(category)
+                                            .or_default()
+                                            .push((id, desc));
+                                    }
+
+                                    for (category, components) in grouped_components.iter_mut() {
+                                        components.sort_by(|a, b| a.1.type_name.cmp(&b.1.type_name));
+
+                                        ui.menu_button(category, |ui| {
+                                            for (id, desc) in components.iter() {
+                                                let response = ui.button(desc.type_name.as_str());
+
+                                                if let Some(description) = desc.description.as_ref() {
+                                                    response.clone().on_hover_text(description);
+                                                }
+
+                                                if response.clicked() {
+                                                    if let Some(component) =
+                                                        registry.create_default_component(*id)
+                                                    {
+                                                        signal.push_back(Signal::AddComponent(entity, component));
+                                                    }
+                                                    ui.close();
+                                                }
+                                            }
+                                        });
+                                    }
+                                });
+                                if ui.button("Create Template").clicked() {
+                                    eucalyptus_core::fatal!("Not implemented yet");
+                                }
+                            }),
+                    );
+
+                    if let Some(component_ids) = component_ids_by_entity.get(&entity) {
+                        let display_id = crate::features::is_enabled(crate::features::ShowComponentTypeIDInEditor);
+                        let has_rigidbody = world.get::<&RigidBody>(entity).is_ok();
+                        let has_collider = world.get::<&ColliderGroup>(entity).is_ok();
+
+                        for component_type_id in component_ids {
+                            let component_node_id =
+                                cfg.component_node_id(entity, *component_type_id);
+                            let display = registry
+                                .get_descriptor_by_numeric_id(*component_type_id)
+                                .map(|desc| {
+                                    if display_id {
+                                        format!("{} (id #{component_type_id})", desc.type_name)
+                                    } else {
+                                        desc.type_name.clone()
+                                    }
+                                })
+                                .unwrap_or_else(|| {
+                                    if display_id {
+                                        format!("Unknown (id #{component_type_id})")
+                                    } else {
+                                        String::from("Unknown")
+                                    }
+                                });
+
+                            let node = NodeBuilder::leaf(component_node_id)
+                                .label_ui(|ui| {
+                                    ui.label(display.clone());
+
+                                    if has_rigidbody
+                                        && !has_collider
+                                        && Some(*component_type_id) == rigidbody_component_id
+                                    {
+                                        ui.add_space(4.0);
+                                        ui.small_button("⚠")
+                                            .on_hover_text("RigidBody has no colliders! Add the ColliderGroup component");
+                                    }
+                                })
+                                .context_menu(|ui| {
+                                    if ui.button("Remove Component").clicked() {
+                                        registry.remove_component_by_id(
+                                            world,
+                                            entity,
+                                            *component_type_id,
+                                        );
+                                        ui.close();
+                                    }
+                                });
+
+                            builder.node(node);
+                        }
+                    }
+
+                    let children_entities = if let Ok(children) = world.get::<&Children>(entity) {
+                        children.children().to_vec()
+                    } else {
+                        Vec::new()
+                    };
+
+                    for child in children_entities {
+                        if let Err(e) =
+                            add_entity_to_tree(
+                                builder,
+                                child,
+                                world,
+                                registry,
+                                component_ids_by_entity,
+                                rigidbody_component_id,
+                                cfg,
+                                signal,
+                            )
+                        {
+                            log_once::error_once!(
+                                    "Failed to add child entity to tree, skipping: {}",
+                                    e
+                                );
+                            continue;
+                        }
+                    }
+
+                    builder.close_dir();
+                    Ok(())
+                }
+
+                let mut component_ids_by_entity: HashMap<Entity, Vec<u64>> = HashMap::new();
+                let rigidbody_component_id = self
+                    .component_registry
+                    .iter_available_components()
+                    .find_map(|(id, desc)| {
+                        if desc.fqtn == "eucalyptus_core::physics::rigidbody::RigidBody" {
+                            Some(id)
+                        } else {
+                            None
+                        }
+                    });
+                {
+                    puffin::profile_scope!("entity_list.index_components");
+                    for (component_id, _) in self.component_registry.iter_available_components() {
+                        for entity in self
+                            .component_registry
+                            .find_entities_by_numeric_id(self.world, component_id)
+                        {
+                            component_ids_by_entity
+                                .entry(entity)
+                                .or_default()
+                                .push(component_id);
+                        }
+                    }
+                }
+
+                let root_entities: Vec<Entity> = self
+                    .world
+                    .query::<Entity>()
+                    .without::<&Parent>()
+                    .iter()
+                    .map(|e| e)
+                    .collect();
+
+                for entity in root_entities {
+                    puffin::profile_scope!("entity_list.root_entity");
+                    if let Err(e) = add_entity_to_tree(
+                        builder,
+                        entity,
+                        &mut self.world,
+                        &self.component_registry,
+                        &component_ids_by_entity,
+                        rigidbody_component_id,
+                        &mut cfg,
+                        self.signal,
+                    ) {
+                        log_once::error_once!(
+                                "Failed to add child entity to tree, skipping: {}",
+                                e
+                            );
+                    }
+                }
+
+                builder.close_dir();
+            })
+        };
+
+        puffin::profile_scope!("entity_list.actions");
+        for i in action {
+            match i {
+                egui_ltreeview::Action::SetSelected(items) => {
+                    log_once::debug_once!("Selected: {:?}", items);
+                    self.handle_tree_selection(&mut cfg, &items);
+                }
+                egui_ltreeview::Action::Move(drag_and_drop) => {
+                    log_once::debug_once!("Moved: {:?}", drag_and_drop);
+                    self.handle_tree_move(&mut cfg, &drag_and_drop);
+                }
+                egui_ltreeview::Action::Drag(drag_and_drop) => {
+                    log_once::debug_once!("Dragged: {:?}", drag_and_drop);
+                    self.handle_tree_drag(&mut cfg, &drag_and_drop);
+                }
+                egui_ltreeview::Action::Activate(activate) => {
+                    log_once::debug_once!("Activated: {:?}", activate);
+                    self.handle_tree_activate(&mut cfg, &activate);
+                }
+                egui_ltreeview::Action::DragExternal(_drag_and_drop_external) => {}
+                egui_ltreeview::Action::MoveExternal(_drag_and_drop_external) => {}
+            }
+        }
+    }
+}
+
+pub struct EntityListDock;
+
+impl EditorTabDock for EntityListDock {
+    fn desc() -> EditorTabDockDescriptor {
+        EditorTabDockDescriptor {            
+            id: "entity_list",            
+            title: "Model/Entity List".to_string(),
+            visibility: EditorTabVisibility::GameEditor,
+        }
+    }
+
+    fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) {
+        viewer.entity_list(ui);
+    }
+}
diff --git a/crates/eucalyptus-editor/src/editor/docks/mod.rs b/crates/eucalyptus-editor/src/editor/docks/mod.rs
new file mode 100644
index 0000000..c059535
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/docks/mod.rs
@@ -0,0 +1,6 @@
+pub mod asset_viewer;
+pub mod build_console;
+pub mod console;
+pub mod entity_list;
+pub mod resource;
+pub mod viewport;
\ No newline at end of file
diff --git a/crates/eucalyptus-editor/src/editor/docks/resource.rs b/crates/eucalyptus-editor/src/editor/docks/resource.rs
new file mode 100644
index 0000000..0eaec04
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/docks/resource.rs
@@ -0,0 +1,139 @@
+use hecs::Entity;
+use eucalyptus_core::entity_status::EntityStatus;
+use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, TABS_GLOBAL};
+use dropbear_engine::camera::Camera;
+use eucalyptus_core::camera::{CameraComponent, CameraType};
+use crate::editor::page::EditorTabVisibility;
+
+impl<'a> EditorTabViewer<'a> {
+    pub(crate) fn resource_inspector(&mut self, ui: &mut egui::Ui) {
+        let mut cfg = TABS_GLOBAL.lock();
+
+        let local_scene_settings = cfg.root_node_selected;
+
+        if let Some(entity) = self.selected_entity {
+            let inspect_entity = *entity;
+
+            if !cfg.root_node_selected {
+                ui.label(format!("Entity ID: {}", inspect_entity.id()));
+                ui.separator();
+
+                // entity status
+                {
+                    let (mut hidden, mut disabled) = self
+                        .world
+                        .get::<&EntityStatus>(inspect_entity)
+                        .map(|s| (s.hidden, s.disabled))
+                        .unwrap_or((false, false));
+
+                    let mut status_changed = false;
+                    ui.horizontal(|ui| {
+                        if ui.checkbox(&mut hidden, "Hidden").changed() {
+                            status_changed = true;
+                        }
+                        if ui.checkbox(&mut disabled, "Disabled").changed() {
+                            status_changed = true;
+                        }
+                    });
+
+                    if status_changed {
+                        let has_status = self.world.get::<&EntityStatus>(inspect_entity).is_ok();
+                        if has_status {
+                            if let Ok(mut s) = self.world.get::<&mut EntityStatus>(inspect_entity) {
+                                s.hidden = hidden;
+                                s.disabled = disabled;
+                            }
+                        } else {
+                            let _ = self.world.insert_one(
+                                inspect_entity,
+                                EntityStatus { hidden, disabled },
+                            );
+                        }
+                    }
+                }
+                ui.separator();
+
+                let mut local_unset_comp = false;
+                if let Ok((_, comp)) = self.world.query_one::<(&Camera, &CameraComponent)>(inspect_entity).get() {
+                    let is_active = self
+                        .active_camera
+                        .lock()
+                        .map_or(false, |active| active == inspect_entity);
+                    ui.horizontal(|ui| {
+                        let label = if is_active {
+                            "Viewing Through This Camera"
+                        } else {
+                            "View Through This Camera"
+                        };
+                        if ui
+                            .add_enabled(!is_active, egui::Button::new(label))
+                            .clicked()
+                        {
+                            let mut active_camera = self.active_camera.lock();
+                            *active_camera = Some(inspect_entity);
+                        }
+
+                        let mut is_starting = comp.starting_camera;
+                        let is_starting_label = if comp.camera_type == CameraType::Debug {
+                            is_starting = true;
+                            "Cannot set a Debug camera as starting"
+                        } else if is_starting {
+                            "Already set as starting"
+                        } else {
+                            "Set as starting"
+                        };
+
+                        if ui
+                            .add_enabled(!is_starting, egui::Button::new(is_starting_label))
+                            .clicked()
+                        {
+                            local_unset_comp = true;
+                        }
+                    });
+                    ui.separator();
+                }
+
+                if local_unset_comp {
+                    for (e, comp) in self.world.query::<(Entity, &mut CameraComponent)>().iter() {
+                        if e == inspect_entity {
+                            comp.starting_camera = true;
+                            continue;
+                        }
+                        log::debug!("Unset starting camera for entity {:?}", e);
+                        comp.starting_camera = false;
+                    }
+                }
+
+                self.component_registry.inspect_components(
+                    self.world,
+                    inspect_entity,
+                    ui,
+                    self.graphics.clone(),
+                );
+            }
+        } else if !local_scene_settings {
+            ui.label("No entity selected, therefore no info to provide. Go on, what are you waiting for? Click an entity!");
+        }
+
+        if local_scene_settings {
+            log_once::debug_once!("Rendering scene settings");
+            self.scene_settings(&mut cfg, ui);
+        }
+    }
+}
+
+pub struct ResourceInspectorDock;
+
+impl EditorTabDock for ResourceInspectorDock {
+    fn desc() -> EditorTabDockDescriptor {
+        EditorTabDockDescriptor {            
+            id: "inspector",            
+            title: "Resource Inspector".to_string(),
+            visibility: EditorTabVisibility::GameEditor,
+        }
+    }
+
+    fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) {
+        viewer.resource_inspector(ui);
+    }
+}
diff --git a/crates/eucalyptus-editor/src/editor/docks/viewport.rs b/crates/eucalyptus-editor/src/editor/docks/viewport.rs
new file mode 100644
index 0000000..9ed054c
--- /dev/null
+++ b/crates/eucalyptus-editor/src/editor/docks/viewport.rs
@@ -0,0 +1,485 @@
+use crate::editor::{DragState, EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, TABS_GLOBAL, UndoableAction};
+use dropbear_engine::asset::ASSET_REGISTRY;
+use dropbear_engine::camera::Camera;
+use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
+use dropbear_engine::lighting::Light;
+use eucalyptus_core::camera::CameraComponent;
+use eucalyptus_core::hierarchy::EntityTransformExt;
+use eucalyptus_core::utils::ViewportMode;
+use glam::DVec3;
+use hecs::Entity;
+use transform_gizmo_egui::{GizmoConfig, GizmoExt, GizmoOrientation};
+use eucalyptus_core::input::ndc::NormalisedDeviceCoordinates;
+use crate::editor::page::EditorTabVisibility;
+
+impl<'a> EditorTabViewer<'a> {
+    fn on_pointer_down(&mut self, touch_pos: [f32; 2], screen_size: [f32; 2], camera: &Camera) {
+        if self.gizmo.is_focused() {
+            return;
+        }
+
+        let inv_proj = camera.proj_mat.inverse().as_mat4();
+        let inv_view = camera.view_mat.inverse().as_mat4();
+        let (ray_origin, ray_dir) =
+            NormalisedDeviceCoordinates::screen_to_ray(touch_pos, screen_size, inv_proj, inv_view);
+
+        let mut closest: Option<(Entity, f32)> = None;
+
+        for (entity, transform, mesh) in
+            self.world.query::<(Entity, &EntityTransform, &MeshRenderer)>().iter()
+        {
+            let (aabb_min, aabb_max) = compute_world_aabb(transform, mesh);
+            if let Some(t) =
+                NormalisedDeviceCoordinates::ray_aabb(ray_origin, ray_dir, aabb_min, aabb_max)
+            {
+                if closest.map_or(true, |(_, best)| t < best) {
+                    closest = Some((entity, t));
+                }
+            }
+        }
+
+        for (entity, transform, mesh) in
+            self.world.query::<(Entity, &Transform, &MeshRenderer)>().iter()
+        {
+            let (aabb_min, aabb_max) = compute_transform_aabb(transform, mesh);
+            if let Some(t) =
+                NormalisedDeviceCoordinates::ray_aabb(ray_origin, ray_dir, aabb_min, aabb_max)
+            {
+                if closest.map_or(true, |(_, best)| t < best) {
+                    closest = Some((entity, t));
+                }
+            }
+        }
+
+        if let Some((entity, t)) = closest {
+            let hit_point = ray_origin + ray_dir * t;
+
+            let entity_origin: glam::Vec3 =
+                if let Ok(et) = self.world.get::<&EntityTransform>(entity) {
+                    let p = et.world().position;
+                    glam::Vec3::new(p.x as f32, p.y as f32, p.z as f32)
+                } else if let Ok(tr) = self.world.get::<&Transform>(entity) {
+                    let p = tr.position;
+                    glam::Vec3::new(p.x as f32, p.y as f32, p.z as f32)
+                } else {
+                    hit_point
+                };
+
+            let plane_normal = -ray_dir;
+            let plane_d = plane_normal.dot(hit_point);
+            let pick_offset = hit_point - entity_origin;
+
+            let initial_entity_transform =
+                self.world.get::<&EntityTransform>(entity).ok().map(|et| *et);
+            let initial_transform = if initial_entity_transform.is_none() {
+                self.world.get::<&Transform>(entity).ok().map(|tr| *tr)
+            } else {
+                None
+            };
+
+            *self.selected_entity = Some(entity);
+            *self.viewport_drag = Some(DragState {
+                entity,
+                plane_normal,
+                plane_d,
+                pick_offset,
+                initial_entity_transform,
+                initial_transform,
+            });
+            log::debug!("Pointer down - {:?}", entity);
+        } else {
+            *self.selected_entity = None;
+            *self.viewport_drag = None;
+        }
+    }
+
+    fn on_pointer_move(&mut self, touch_pos: [f32; 2], screen_size: [f32; 2], camera: &Camera) {
+        let drag = match self.viewport_drag.as_ref() {
+            Some(d) => d,
+            None => return,
+        };
+
+        let inv_proj = camera.proj_mat.inverse().as_mat4();
+        let inv_view = camera.view_mat.inverse().as_mat4();
+        let (ray_origin, ray_dir) =
+            NormalisedDeviceCoordinates::screen_to_ray(touch_pos, screen_size, inv_proj, inv_view);
+
+        let new_world_pos = match NormalisedDeviceCoordinates::ray_plane(
+            ray_origin,
+            ray_dir,
+            drag.plane_normal,
+            drag.plane_d,
+        ) {
+            Some(p) => p - drag.pick_offset,
+            None => return,
+        };
+
+        let entity = drag.entity;
+        let new_pos = DVec3::new(
+            new_world_pos.x as f64,
+            new_world_pos.y as f64,
+            new_world_pos.z as f64,
+        );
+
+        if let Ok(mut et) = self.world.get::<&mut EntityTransform>(entity) {
+            et.world_mut().position = new_pos;
+        } else if let Ok(mut tr) = self.world.get::<&mut Transform>(entity) {
+            tr.position = new_pos;
+        }
+
+        log::debug!("Pointer moved - {:?}", entity);
+    }
+
+    fn on_pointer_up(&mut self) {
+        let drag = match self.viewport_drag.take() {
+            Some(d) => d,
+            None => return,
+        };
+
+        if let Some(initial_et) = drag.initial_entity_transform {
+            if let Ok(current_et) = self.world.get::<&EntityTransform>(drag.entity) {
+                if *current_et != initial_et {
+                    self.undo_stack
+                        .push(UndoableAction::EntityTransform(drag.entity, initial_et));
+                    log::debug!("Pushed viewport drag entity-transform to undo stack");
+                }
+            }
+        } else if let Some(initial_tr) = drag.initial_transform {
+            if let Ok(current_tr) = self.world.get::<&Transform>(drag.entity) {
+                if *current_tr != initial_tr {
+                    self.undo_stack
+                        .push(UndoableAction::Transform(drag.entity, initial_tr));
+                    log::debug!("Pushed viewport drag transform to undo stack");
+                }
+            }
+        }
+    }
+
+    pub(crate) fn viewport_tab(&mut self, ui: &mut egui::Ui) {
+        let mut cfg = TABS_GLOBAL.lock();
+
+        log_once::debug_once!("Viewport focused");
+
+        let available_rect = ui.available_rect_before_wrap();
+        let available_size = available_rect.size();
+        let pixels_per_point = ui.ctx().pixels_per_point();
+
+        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 self.signal.is_empty() {
+                self.signal.push_back(Signal::UpdateViewportSize((desired_width as f32, desired_height as f32)));
+            }
+        }
+
+        let tex_aspect = self.tex_size.width as f32 / self.tex_size.height as f32;
+        let available_aspect = available_size.x / available_size.y;
+
+        let (display_width, display_height) = if available_aspect > tex_aspect {
+            let height = available_size.y;
+            let width = height * tex_aspect;
+            (width, height)
+        } else {
+            let width = available_size.x;
+            let height = width / tex_aspect;
+            (width, height)
+        };
+
+        let center_x = available_rect.center().x;
+        let center_y = available_rect.center().y;
+
+        let image_rect = egui::Rect::from_center_size(
+            egui::pos2(center_x, center_y),
+            egui::vec2(display_width, display_height),
+        );
+
+        // let (_rect, _response) =
+        //     ui.allocate_exact_size(available_size, egui::Sense::click_and_drag());
+
+
+        let (_full, _) = ui.allocate_exact_size(available_size, egui::Sense::hover());
+
+        ui.painter().image(
+            self.view,
+            image_rect,
+            egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
+            egui::Color32::WHITE,
+        );
+
+        let image_response = ui.interact(
+            image_rect,
+            egui::Id::new("viewport_image_interaction"),
+            egui::Sense::click_and_drag(),
+        );
+
+        let snapping = ui.input(|input| input.modifiers.shift);
+        let active_camera_entity: Option<Entity> = *self.active_camera.lock();
+        if let Some(active_camera) = active_camera_entity {
+            let camera_data = {
+                if let Ok((cam, _comp)) = self
+                    .world
+                    .query_one::<(&Camera, &CameraComponent)>(active_camera)
+                    .get()
+                {
+                    Some(cam.clone())
+                } else {
+                    log::warn!("Queried camera but found no value");
+                    None
+                }
+            };
+
+            if let Some(camera) = camera_data {
+                self.gizmo.update_config(GizmoConfig {
+                    view_matrix: camera.view_mat.into(),
+                    projection_matrix: camera.proj_mat.into(),
+                    viewport: image_rect,
+                    modes: *self.gizmo_mode,
+                    orientation: *self.gizmo_orientation,
+                    snapping,
+                    snap_distance: 1.0,
+                    ..Default::default()
+                });
+
+                // viewport 3d clicking / dragging
+                if image_response.clicked() || image_response.drag_started() {
+                    if let Some(pos) = image_response.interact_pointer_pos() {
+                        let local_pos = pos - image_rect.min;
+                        let touch_pos = [local_pos.x, local_pos.y];
+                        let screen_size = [image_rect.width(), image_rect.height()];
+                        self.on_pointer_down(touch_pos, screen_size, &camera);
+                    }
+                }
+
+                if image_response.dragged() {
+                    if let Some(pos) = image_response.interact_pointer_pos() {
+                        let local_pos = pos - image_rect.min;
+                        let touch_pos = [local_pos.x, local_pos.y];
+                        let screen_size = [image_rect.width(), image_rect.height()];
+                        self.on_pointer_move(touch_pos, screen_size, &camera);
+                    }
+                }
+
+                if image_response.drag_stopped() {
+                    self.on_pointer_up();
+                }
+            }
+        }
+
+        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(mut entity_transform) = self
+                .world
+                .get::<&mut EntityTransform>(*entity_id)
+            {
+                let was_focused = cfg.is_focused;
+                cfg.is_focused = self.gizmo.is_focused();
+
+                if cfg.is_focused && !was_focused {
+                    cfg.entity_transform_original = Some(*entity_transform);
+                }
+
+                let synced = entity_transform.propagate(&self.world, *entity_id);
+                let gizmo_transform =
+                    transform_gizmo_egui::math::Transform::from_scale_rotation_translation(
+                        synced.scale,
+                        synced.rotation,
+                        synced.position,
+                    );
+
+                if let Some((_result, new_transforms)) = self.gizmo.interact(ui, &[gizmo_transform])
+                    && let Some(new_transform) = new_transforms.first()
+                {
+                    let new_synced_pos: glam::DVec3 = new_transform.translation.into();
+                    let new_synced_rot: glam::DQuat = new_transform.rotation.into();
+                    let new_synced_scale: glam::DVec3 = new_transform.scale.into();
+
+                    let prev_world_pos = synced.position;
+                    let prev_world_rot = synced.rotation;
+                    let prev_world_scale = synced.scale;
+
+                    let safe = |v: f64| if v.abs() < 1e-9 { 1.0 } else { v };
+
+                    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),
+                    );
+
+                    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_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 = entity_transform.local_mut();
+
+                            local.position += world_rot.inverse() * delta_pos / safe_ws;
+
+                            local.rotation  = world_rot.inverse() * delta_rot * world_rot * local.rotation;
+
+                            local.scale    *= delta_scale;
+                        }
+                    }
+
+                    updated_light_transform = Some(entity_transform.sync());
+                }
+
+                if was_focused && !cfg.is_focused {
+                    if let Some(original) = cfg.entity_transform_original {
+                        if original != *entity_transform {
+                            self.undo_stack.push(UndoableAction::EntityTransform(*entity_id, original));
+                            log::debug!("Pushed entity transform action to stack");
+                        }
+                    }
+                }
+
+                handled = true;
+            }
+
+            if !handled {
+                if let Ok(transform) = self.world.query_one::<&mut Transform>(*entity_id).get() {
+                    let was_focused = cfg.is_focused;
+                    cfg.is_focused = self.gizmo.is_focused();
+
+                    if cfg.is_focused && !was_focused {
+                        cfg.old_pos = *transform;
+                    }
+
+                    let gizmo_transform =
+                        transform_gizmo_egui::math::Transform::from_scale_rotation_translation(
+                            transform.scale,
+                            transform.rotation,
+                            transform.position,
+                        );
+
+                    if let Some((_result, new_transforms)) =
+                        self.gizmo.interact(ui, &[gizmo_transform])
+                        && let Some(new_transform) = new_transforms.first()
+                    {
+                        transform.position = new_transform.translation.into();
+                        transform.rotation = new_transform.rotation.into();
+                        transform.scale    = new_transform.scale.into();
+                        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;
+
+                        if transform_changed {
+                            self.undo_stack.push(
+                                UndoableAction::Transform(*entity_id, cfg.old_pos)
+                            );
+                            log::debug!("Pushed transform action to stack");
+                        }
+                    }
+                }
+            }
+
+            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, -1.0, 0.0);
+                    light.component.position  = updated_transform.position;
+                    light.component.direction =
+                        (updated_transform.rotation * forward).normalize_or_zero();
+                }
+            }
+        }
+    }
+}
+
+/// Compute a world-space AABB for an entity that has an [`EntityTransform`].
+fn compute_world_aabb(transform: &EntityTransform, mesh: &MeshRenderer) -> (glam::Vec3, glam::Vec3) {
+    aabb_for_world_matrix(transform.world().matrix().as_mat4(), mesh)
+}
+
+/// Compute a world-space AABB for an entity that only has a plain [`Transform`].
+fn compute_transform_aabb(transform: &Transform, mesh: &MeshRenderer) -> (glam::Vec3, glam::Vec3) {
+    aabb_for_world_matrix(transform.matrix().as_mat4(), mesh)
+}
+
+/// Transform the model's local AABB by `world_mat` and return the resulting world AABB.
+///
+/// If the model hasn't loaded yet a unit box centred on the origin is used as a fallback.
+fn aabb_for_world_matrix(
+    world_mat: glam::Mat4,
+    mesh: &MeshRenderer,
+) -> (glam::Vec3, glam::Vec3) {
+    use glam::Vec3;
+
+    let (local_min, local_max) = {
+        let registry = ASSET_REGISTRY.read();
+        let bounds = registry.get_model(mesh.model()).and_then(|model| {
+            let mut lo = Vec3::splat(f32::INFINITY);
+            let mut hi = Vec3::splat(f32::NEG_INFINITY);
+            for m in &model.meshes {
+                for v in &m.vertices {
+                    let p = Vec3::from(v.position);
+                    lo = lo.min(p);
+                    hi = hi.max(p);
+                }
+            }
+            if lo.x <= hi.x { Some((lo, hi)) } else { None }
+        });
+        bounds.unwrap_or((Vec3::splat(-0.5), Vec3::splat(0.5)))
+    };
+
+    let s = mesh.import_scale();
+    let local_min = local_min * s;
+    let local_max = local_max * s;
+
+    let corners = [
+        Vec3::new(local_min.x, local_min.y, local_min.z),
+        Vec3::new(local_max.x, local_min.y, local_min.z),
+        Vec3::new(local_min.x, local_max.y, local_min.z),
+        Vec3::new(local_max.x, local_max.y, local_min.z),
+        Vec3::new(local_min.x, local_min.y, local_max.z),
+        Vec3::new(local_max.x, local_min.y, local_max.z),
+        Vec3::new(local_min.x, local_max.y, local_max.z),
+        Vec3::new(local_max.x, local_max.y, local_max.z),
+    ];
+
+    let mut world_min = Vec3::splat(f32::INFINITY);
+    let mut world_max = Vec3::splat(f32::NEG_INFINITY);
+    for &corner in &corners {
+        let wc = world_mat.transform_point3(corner);
+        world_min = world_min.min(wc);
+        world_max = world_max.max(wc);
+    }
+    (world_min, world_max)
+}
+
+
+pub struct ViewportDock;
+
+impl EditorTabDock for ViewportDock {
+    fn desc() -> EditorTabDockDescriptor {
+        EditorTabDockDescriptor {
+            id: "viewport",
+            title: "Viewport".to_string(),
+            visibility: EditorTabVisibility::GameEditor,
+        }
+    }
+
+    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/editor/entity_list.rs b/crates/eucalyptus-editor/src/editor/entity_list.rs
deleted file mode 100644
index bf2a525..0000000
--- a/crates/eucalyptus-editor/src/editor/entity_list.rs
+++ /dev/null
@@ -1,308 +0,0 @@
-use egui_ltreeview::{NodeBuilder, TreeViewBuilder};
-use eucalyptus_core::{
-    component::ComponentRegistry,
-    hierarchy::{Children, Hierarchy, Parent},
-    physics::{collider::ColliderGroup, rigidbody::RigidBody},
-    states::{Label, PROJECT},
-};
-use hecs::{Entity, World};
-use std::collections::{BTreeMap, HashMap, VecDeque};
-
-use crate::editor::{Editor, EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, StaticallyKept, TABS_GLOBAL};
-use crate::editor::page::EditorTabVisibility;
-
-impl<'a> EditorTabViewer<'a> {
-    pub(crate) fn entity_list(&mut self, ui: &mut egui::Ui) {
-        puffin::profile_function!();
-        let mut cfg = TABS_GLOBAL.lock();
-
-        let (_response, action) = {
-            puffin::profile_scope!("entity_list.tree_build");
-            egui_ltreeview::TreeView::new(egui::Id::new(
-            "model_entity_list",
-        ))
-            .show(ui, |builder| {
-                let current_scene_name = {
-                    PROJECT
-                        .read()
-                        .last_opened_scene
-                        .clone()
-                        .unwrap_or("Scene".to_string())
-                };
-                builder.node(
-                    // root node/scene display
-                    NodeBuilder::dir(u64::MAX)
-                        .label(format!("Scene: {}", current_scene_name))
-                        .context_menu(|ui| {
-                            if ui.button("New Empty Entity").clicked() {
-                                let label = Editor::unique_label_for_world(self.world, "Blank Entity");
-                                self.world.spawn((label,));
-                                ui.close();
-                            }
-                            ui.menu_button("Import Template", |_| {
-
-                            });
-                        }),
-                );
-                // the root scene must be the biggest number possible to remove any ambiguity
-
-                fn add_entity_to_tree(
-                    builder: &mut TreeViewBuilder<u64>,
-                    entity: Entity,
-                    world: &mut World,
-                    registry: &ComponentRegistry,
-                    component_ids_by_entity: &HashMap<Entity, Vec<u64>>,
-                    rigidbody_component_id: Option<u64>,
-                    cfg: &mut StaticallyKept,
-                    signal: &mut VecDeque<Signal>,
-                ) -> anyhow::Result<()> {
-                    puffin::profile_scope!("entity_list.add_entity_to_tree");
-                    let entity_id = entity.to_bits().get();
-                    let label = if let Ok(label) = world.query_one::<&Label>(entity).get()
-                    {
-                        label.clone()
-                    } else {
-                        anyhow::bail!(
-                                "This entity [{}] is expected to contain Label",
-                                entity_id
-                            );
-                    };
-
-                    builder.node(
-                        NodeBuilder::dir(entity_id)
-                            .label(label.as_str())
-                            .context_menu(|ui| {
-                                ui.menu_button("New", |ui| {
-                                    if ui.button("Child").clicked() {
-                                        let label = Editor::unique_label_for_world(world, "New Entity");
-                                        let child = world.spawn((label,));
-                                        Hierarchy::set_parent(world, child, entity);
-                                        ui.close();
-                                    }
-                                });
-                                ui.menu_button("Add", |ui| {
-                                    let mut grouped_components: BTreeMap<String, Vec<(u64, &eucalyptus_core::component::ComponentDescriptor)>> =
-                                        BTreeMap::new();
-
-                                    for (id, desc) in registry.iter_available_components() {
-                                        let category = desc
-                                            .category
-                                            .clone()
-                                            .unwrap_or_else(|| "Uncategorised".to_string());
-                                        grouped_components
-                                            .entry(category)
-                                            .or_default()
-                                            .push((id, desc));
-                                    }
-
-                                    for (category, components) in grouped_components.iter_mut() {
-                                        components.sort_by(|a, b| a.1.type_name.cmp(&b.1.type_name));
-
-                                        ui.menu_button(category, |ui| {
-                                            for (id, desc) in components.iter() {
-                                                let response = ui.button(desc.type_name.as_str());
-
-                                                if let Some(description) = desc.description.as_ref() {
-                                                    response.clone().on_hover_text(description);
-                                                }
-
-                                                if response.clicked() {
-                                                    if let Some(component) =
-                                                        registry.create_default_component(*id)
-                                                    {
-                                                        signal.push_back(Signal::AddComponent(entity, component));
-                                                    }
-                                                    ui.close();
-                                                }
-                                            }
-                                        });
-                                    }
-                                });
-                                if ui.button("Create Template").clicked() {
-                                    eucalyptus_core::fatal!("Not implemented yet");
-                                }
-                            }),
-                    );
-
-                    if let Some(component_ids) = component_ids_by_entity.get(&entity) {
-                        let display_id = crate::features::is_enabled(crate::features::ShowComponentTypeIDInEditor);
-                        let has_rigidbody = world.get::<&RigidBody>(entity).is_ok();
-                        let has_collider = world.get::<&ColliderGroup>(entity).is_ok();
-
-                        for component_type_id in component_ids {
-                            let component_node_id =
-                                cfg.component_node_id(entity, *component_type_id);
-                            let display = registry
-                                .get_descriptor_by_numeric_id(*component_type_id)
-                                .map(|desc| {
-                                    if display_id {
-                                        format!("{} (id #{component_type_id})", desc.type_name)
-                                    } else {
-                                        desc.type_name.clone()
-                                    }
-                                })
-                                .unwrap_or_else(|| {
-                                    if display_id {
-                                        format!("Unknown (id #{component_type_id})")
-                                    } else {
-                                        String::from("Unknown")
-                                    }
-                                });
-
-                            let node = NodeBuilder::leaf(component_node_id)
-                                .label_ui(|ui| {
-                                    ui.label(display.clone());
-
-                                    if has_rigidbody
-                                        && !has_collider
-                                        && Some(*component_type_id) == rigidbody_component_id
-                                    {
-                                        ui.add_space(4.0);
-                                        ui.small_button("⚠")
-                                            .on_hover_text("RigidBody has no colliders! Add the ColliderGroup component");
-                                    }
-                                })
-                                .context_menu(|ui| {
-                                    if ui.button("Remove Component").clicked() {
-                                        registry.remove_component_by_id(
-                                            world,
-                                            entity,
-                                            *component_type_id,
-                                        );
-                                        ui.close();
-                                    }
-                                });
-
-                            builder.node(node);
-                        }
-                    }
-
-                    let children_entities = if let Ok(children) = world.get::<&Children>(entity) {
-                        children.children().to_vec()
-                    } else {
-                        Vec::new()
-                    };
-
-                    for child in children_entities {
-                        if let Err(e) =
-                            add_entity_to_tree(
-                                builder,
-                                child,
-                                world,
-                                registry,
-                                component_ids_by_entity,
-                                rigidbody_component_id,
-                                cfg,
-                                signal,
-                            )
-                        {
-                            log_once::error_once!(
-                                    "Failed to add child entity to tree, skipping: {}",
-                                    e
-                                );
-                            continue;
-                        }
-                    }
-
-                    builder.close_dir();
-                    Ok(())
-                }
-
-                let mut component_ids_by_entity: HashMap<Entity, Vec<u64>> = HashMap::new();
-                let rigidbody_component_id = self
-                    .component_registry
-                    .iter_available_components()
-                    .find_map(|(id, desc)| {
-                        if desc.fqtn == "eucalyptus_core::physics::rigidbody::RigidBody" {
-                            Some(id)
-                        } else {
-                            None
-                        }
-                    });
-                {
-                    puffin::profile_scope!("entity_list.index_components");
-                    for (component_id, _) in self.component_registry.iter_available_components() {
-                        for entity in self
-                            .component_registry
-                            .find_entities_by_numeric_id(self.world, component_id)
-                        {
-                            component_ids_by_entity
-                                .entry(entity)
-                                .or_default()
-                                .push(component_id);
-                        }
-                    }
-                }
-
-                let root_entities: Vec<Entity> = self
-                    .world
-                    .query::<Entity>()
-                    .without::<&Parent>()
-                    .iter()
-                    .map(|e| e)
-                    .collect();
-
-                for entity in root_entities {
-                    puffin::profile_scope!("entity_list.root_entity");
-                    if let Err(e) = add_entity_to_tree(
-                        builder,
-                        entity,
-                        &mut self.world,
-                        &self.component_registry,
-                        &component_ids_by_entity,
-                        rigidbody_component_id,
-                        &mut cfg,
-                        self.signal,
-                    ) {
-                        log_once::error_once!(
-                                "Failed to add child entity to tree, skipping: {}",
-                                e
-                            );
-                    }
-                }
-
-                builder.close_dir();
-            })
-        };
-
-        puffin::profile_scope!("entity_list.actions");
-        for i in action {
-            match i {
-                egui_ltreeview::Action::SetSelected(items) => {
-                    log_once::debug_once!("Selected: {:?}", items);
-                    self.handle_tree_selection(&mut cfg, &items);
-                }
-                egui_ltreeview::Action::Move(drag_and_drop) => {
-                    log_once::debug_once!("Moved: {:?}", drag_and_drop);
-                    self.handle_tree_move(&mut cfg, &drag_and_drop);
-                }
-                egui_ltreeview::Action::Drag(drag_and_drop) => {
-                    log_once::debug_once!("Dragged: {:?}", drag_and_drop);
-                    self.handle_tree_drag(&mut cfg, &drag_and_drop);
-                }
-                egui_ltreeview::Action::Activate(activate) => {
-                    log_once::debug_once!("Activated: {:?}", activate);
-                    self.handle_tree_activate(&mut cfg, &activate);
-                }
-                egui_ltreeview::Action::DragExternal(_drag_and_drop_external) => {}
-                egui_ltreeview::Action::MoveExternal(_drag_and_drop_external) => {}
-            }
-        }
-    }
-}
-
-pub struct EntityListDock;
-
-impl EditorTabDock for EntityListDock {
-    fn desc() -> EditorTabDockDescriptor {
-        EditorTabDockDescriptor {            
-            id: "entity_list",            
-            title: "Model/Entity List".to_string(),
-            visibility: EditorTabVisibility::GameEditor,
-        }
-    }
-
-    fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) {
-        viewer.entity_list(ui);
-    }
-}
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 57a9c52..4af9744 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -1,22 +1,17 @@
-pub mod asset_viewer;
-pub mod build_console;
-pub mod console;
 pub mod dock;
-pub mod entity_list;
 pub mod input;
-pub mod resource;
 pub mod scene;
 pub mod settings;
-pub mod viewport;
 pub mod page;
 pub mod ui;
+pub mod docks;
 
 pub(crate) use crate::editor::dock::*;
 
 use crate::about::AboutWindow;
 use crate::build::build;
 use crate::debug;
-use crate::editor::console::EucalyptusConsole;
+use docks::console::EucalyptusConsole;
 use crate::editor::settings::editor::{EditorSettingsWindow, EDITOR_SETTINGS};
 use crate::editor::settings::project::ProjectSettingsWindow;
 use crate::plugin::PluginRegistry;
@@ -136,6 +131,7 @@ pub struct Editor {
     pub previously_selected_entity: Option<hecs::Entity>,
     pub selected_entity: Option<hecs::Entity>,
     pub viewport_mode: ViewportMode,
+    pub viewport_drag: Option<crate::editor::dock::DragState>,
 
     pub(crate) signal: VecDeque<Signal>,
     pub(crate) undo_stack: Vec<UndoableAction>,
@@ -281,6 +277,7 @@ impl Editor {
             previously_selected_entity: None,
             selected_entity: None,
             viewport_mode: ViewportMode::None,
+            viewport_drag: None,
             signal: VecDeque::new(),
             undo_stack: Vec::new(),
             // script_manager: ScriptManager::new()?,
@@ -1411,6 +1408,7 @@ impl Editor {
                         eucalyptus_console: &mut self.console,
                         current_scene_name: &mut self.current_scene_name,
                         ui_editor: &mut self.ui_editor,
+                        viewport_drag: &mut self.viewport_drag,
                     },
                 );
 
diff --git a/crates/eucalyptus-editor/src/editor/resource.rs b/crates/eucalyptus-editor/src/editor/resource.rs
deleted file mode 100644
index 0eaec04..0000000
--- a/crates/eucalyptus-editor/src/editor/resource.rs
+++ /dev/null
@@ -1,139 +0,0 @@
-use hecs::Entity;
-use eucalyptus_core::entity_status::EntityStatus;
-use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, TABS_GLOBAL};
-use dropbear_engine::camera::Camera;
-use eucalyptus_core::camera::{CameraComponent, CameraType};
-use crate::editor::page::EditorTabVisibility;
-
-impl<'a> EditorTabViewer<'a> {
-    pub(crate) fn resource_inspector(&mut self, ui: &mut egui::Ui) {
-        let mut cfg = TABS_GLOBAL.lock();
-
-        let local_scene_settings = cfg.root_node_selected;
-
-        if let Some(entity) = self.selected_entity {
-            let inspect_entity = *entity;
-
-            if !cfg.root_node_selected {
-                ui.label(format!("Entity ID: {}", inspect_entity.id()));
-                ui.separator();
-
-                // entity status
-                {
-                    let (mut hidden, mut disabled) = self
-                        .world
-                        .get::<&EntityStatus>(inspect_entity)
-                        .map(|s| (s.hidden, s.disabled))
-                        .unwrap_or((false, false));
-
-                    let mut status_changed = false;
-                    ui.horizontal(|ui| {
-                        if ui.checkbox(&mut hidden, "Hidden").changed() {
-                            status_changed = true;
-                        }
-                        if ui.checkbox(&mut disabled, "Disabled").changed() {
-                            status_changed = true;
-                        }
-                    });
-
-                    if status_changed {
-                        let has_status = self.world.get::<&EntityStatus>(inspect_entity).is_ok();
-                        if has_status {
-                            if let Ok(mut s) = self.world.get::<&mut EntityStatus>(inspect_entity) {
-                                s.hidden = hidden;
-                                s.disabled = disabled;
-                            }
-                        } else {
-                            let _ = self.world.insert_one(
-                                inspect_entity,
-                                EntityStatus { hidden, disabled },
-                            );
-                        }
-                    }
-                }
-                ui.separator();
-
-                let mut local_unset_comp = false;
-                if let Ok((_, comp)) = self.world.query_one::<(&Camera, &CameraComponent)>(inspect_entity).get() {
-                    let is_active = self
-                        .active_camera
-                        .lock()
-                        .map_or(false, |active| active == inspect_entity);
-                    ui.horizontal(|ui| {
-                        let label = if is_active {
-                            "Viewing Through This Camera"
-                        } else {
-                            "View Through This Camera"
-                        };
-                        if ui
-                            .add_enabled(!is_active, egui::Button::new(label))
-                            .clicked()
-                        {
-                            let mut active_camera = self.active_camera.lock();
-                            *active_camera = Some(inspect_entity);
-                        }
-
-                        let mut is_starting = comp.starting_camera;
-                        let is_starting_label = if comp.camera_type == CameraType::Debug {
-                            is_starting = true;
-                            "Cannot set a Debug camera as starting"
-                        } else if is_starting {
-                            "Already set as starting"
-                        } else {
-                            "Set as starting"
-                        };
-
-                        if ui
-                            .add_enabled(!is_starting, egui::Button::new(is_starting_label))
-                            .clicked()
-                        {
-                            local_unset_comp = true;
-                        }
-                    });
-                    ui.separator();
-                }
-
-                if local_unset_comp {
-                    for (e, comp) in self.world.query::<(Entity, &mut CameraComponent)>().iter() {
-                        if e == inspect_entity {
-                            comp.starting_camera = true;
-                            continue;
-                        }
-                        log::debug!("Unset starting camera for entity {:?}", e);
-                        comp.starting_camera = false;
-                    }
-                }
-
-                self.component_registry.inspect_components(
-                    self.world,
-                    inspect_entity,
-                    ui,
-                    self.graphics.clone(),
-                );
-            }
-        } else if !local_scene_settings {
-            ui.label("No entity selected, therefore no info to provide. Go on, what are you waiting for? Click an entity!");
-        }
-
-        if local_scene_settings {
-            log_once::debug_once!("Rendering scene settings");
-            self.scene_settings(&mut cfg, ui);
-        }
-    }
-}
-
-pub struct ResourceInspectorDock;
-
-impl EditorTabDock for ResourceInspectorDock {
-    fn desc() -> EditorTabDockDescriptor {
-        EditorTabDockDescriptor {            
-            id: "inspector",            
-            title: "Resource Inspector".to_string(),
-            visibility: EditorTabVisibility::GameEditor,
-        }
-    }
-
-    fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) {
-        viewer.resource_inspector(ui);
-    }
-}
diff --git a/crates/eucalyptus-editor/src/editor/viewport.rs b/crates/eucalyptus-editor/src/editor/viewport.rs
deleted file mode 100644
index 01ed176..0000000
--- a/crates/eucalyptus-editor/src/editor/viewport.rs
+++ /dev/null
@@ -1,250 +0,0 @@
-use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, TABS_GLOBAL, UndoableAction};
-use dropbear_engine::camera::Camera;
-use dropbear_engine::entity::{EntityTransform, Transform};
-use dropbear_engine::lighting::Light;
-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> {
-    pub(crate) fn viewport_tab(&mut self, ui: &mut egui::Ui) {
-        let mut cfg = TABS_GLOBAL.lock();
-
-        log_once::debug_once!("Viewport focused");
-
-        let available_rect = ui.available_rect_before_wrap();
-        let available_size = available_rect.size();
-        let pixels_per_point = ui.ctx().pixels_per_point();
-
-        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 self.signal.is_empty() {
-                self.signal.push_back(Signal::UpdateViewportSize((desired_width as f32, desired_height as f32)));
-            }
-        }
-
-        let tex_aspect = self.tex_size.width as f32 / self.tex_size.height as f32;
-        let available_aspect = available_size.x / available_size.y;
-
-        let (display_width, display_height) = if available_aspect > tex_aspect {
-            let height = available_size.y;
-            let width = height * tex_aspect;
-            (width, height)
-        } else {
-            let width = available_size.x;
-            let height = width / tex_aspect;
-            (width, height)
-        };
-
-        let center_x = available_rect.center().x;
-        let center_y = available_rect.center().y;
-
-        let image_rect = egui::Rect::from_center_size(
-            egui::pos2(center_x, center_y),
-            egui::vec2(display_width, display_height),
-        );
-
-        let (_rect, _response) =
-            ui.allocate_exact_size(available_size, egui::Sense::click_and_drag());
-
-        let _image_response = ui.allocate_rect(image_rect, egui::Sense::click_and_drag());
-
-        ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| {
-            ui.add_sized(
-                [display_width, display_height],
-                egui::Image::new((self.view, [display_width, display_height].into()))
-                    .fit_to_exact_size([display_width, display_height].into()),
-            )
-        });
-
-        let snapping = ui.input(|input| input.modifiers.shift);
-
-        let active_cam = self.active_camera.lock();
-        if let Some(active_camera) = *active_cam {
-            let camera_data = {
-                if let Ok((cam, _comp)) = self
-                    .world
-                    .query_one::<(&Camera, &CameraComponent)>(active_camera)
-                    .get()
-                {
-                    Some(cam.clone())
-                } else {
-                    log::warn!("Queried camera but found no value");
-                    None
-                }
-            };
-
-            if let Some(camera) = camera_data {
-                self.gizmo.update_config(GizmoConfig {
-                    view_matrix: camera.view_mat.into(),
-                    projection_matrix: camera.proj_mat.into(),
-                    viewport: image_rect,
-                    modes: *self.gizmo_mode,
-                    orientation: *self.gizmo_orientation,
-                    snapping,
-                    snap_distance: 1.0,
-                    ..Default::default()
-                });
-            }
-        }
-
-        if !matches!(self.viewport_mode, ViewportMode::None)
-            && let Some(entity_id) = self.selected_entity
-        {
-            let mut handled = false;
-            let mut updated_light_transform: Option<Transform> = None;
-
-            if let Ok(mut entity_transform) = self
-                .world
-                .get::<&mut EntityTransform>(*entity_id)
-            {
-                let was_focused = cfg.is_focused;
-                cfg.is_focused = self.gizmo.is_focused();
-
-                if cfg.is_focused && !was_focused {
-                    cfg.entity_transform_original = Some(*entity_transform);
-                }
-
-                let synced = entity_transform.propagate(&self.world, *entity_id);
-                let gizmo_transform =
-                    transform_gizmo_egui::math::Transform::from_scale_rotation_translation(
-                        synced.scale,
-                        synced.rotation,
-                        synced.position,
-                    );
-
-                if let Some((_result, new_transforms)) = self.gizmo.interact(ui, &[gizmo_transform])
-                    && let Some(new_transform) = new_transforms.first()
-                {
-                    let new_synced_pos: glam::DVec3 = new_transform.translation.into();
-                    let new_synced_rot: glam::DQuat = new_transform.rotation.into();
-                    let new_synced_scale: glam::DVec3 = new_transform.scale.into();
-
-                    let prev_world_pos = synced.position;
-                    let prev_world_rot = synced.rotation;
-                    let prev_world_scale = synced.scale;
-
-                    let safe = |v: f64| if v.abs() < 1e-9 { 1.0 } else { v };
-
-                    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),
-                    );
-
-                    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_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 = entity_transform.local_mut();
-
-                            local.position += world_rot.inverse() * delta_pos / safe_ws;
-
-                            local.rotation  = world_rot.inverse() * delta_rot * world_rot * local.rotation;
-
-                            local.scale    *= delta_scale;
-                        }
-                    }
-
-                    updated_light_transform = Some(entity_transform.sync());
-                }
-
-                if was_focused && !cfg.is_focused {
-                    if let Some(original) = cfg.entity_transform_original {
-                        if original != *entity_transform {
-                            self.undo_stack.push(UndoableAction::EntityTransform(*entity_id, original));
-                            log::debug!("Pushed entity transform action to stack");
-                        }
-                    }
-                }
-
-                handled = true;
-            }
-
-            if !handled {
-                if let Ok(transform) = self.world.query_one::<&mut Transform>(*entity_id).get() {
-                    let was_focused = cfg.is_focused;
-                    cfg.is_focused = self.gizmo.is_focused();
-
-                    if cfg.is_focused && !was_focused {
-                        cfg.old_pos = *transform;
-                    }
-
-                    let gizmo_transform =
-                        transform_gizmo_egui::math::Transform::from_scale_rotation_translation(
-                            transform.scale,
-                            transform.rotation,
-                            transform.position,
-                        );
-
-                    if let Some((_result, new_transforms)) =
-                        self.gizmo.interact(ui, &[gizmo_transform])
-                        && let Some(new_transform) = new_transforms.first()
-                    {
-                        transform.position = new_transform.translation.into();
-                        transform.rotation = new_transform.rotation.into();
-                        transform.scale    = new_transform.scale.into();
-                        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;
-
-                        if transform_changed {
-                            self.undo_stack.push(
-                                UndoableAction::Transform(*entity_id, cfg.old_pos)
-                            );
-                            log::debug!("Pushed transform action to stack");
-                        }
-                    }
-                }
-            }
-
-            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, -1.0, 0.0);
-                    light.component.position  = updated_transform.position;
-                    light.component.direction =
-                        (updated_transform.rotation * forward).normalize_or_zero();
-                }
-            }
-        }
-    }
-}
-
-pub struct ViewportDock;
-
-impl EditorTabDock for ViewportDock {
-    fn desc() -> EditorTabDockDescriptor {
-        EditorTabDockDescriptor {
-            id: "viewport",
-            title: "Viewport".to_string(),
-            visibility: EditorTabVisibility::GameEditor,
-        }
-    }
-
-    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 6dc8ba6..b49a7a2 100644
--- a/crates/eucalyptus-editor/src/lib.rs
+++ b/crates/eucalyptus-editor/src/lib.rs
@@ -10,10 +10,16 @@ pub mod signal;
 pub mod spawn;
 pub mod stats;
 pub mod utils;
+
+use editor::docks::asset_viewer::AssetViewerDock;
+use editor::docks::build_console::BuildConsoleDock;
+use editor::docks::entity_list::EntityListDock;
+use editor::docks::resource::ResourceInspectorDock;
+use editor::docks::viewport::ViewportDock;
 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::UICanvas, viewport::ViewportDock
+    dock::ConsoleDock, ui::viewport::UICanvas, EditorTabRegistry
 };
 use crate::editor::ui::inspector::UIInspector;
 use crate::editor::ui::widget_tree::UIWidgetTree;
diff --git a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
index ced3eed..8e3be3f 100644
--- a/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/DropbearEngine.kt
@@ -1,5 +1,6 @@
 package com.dropbear
 
+import com.dropbear.asset.AssetEntry
 import com.dropbear.asset.AssetType
 import com.dropbear.asset.Handle
 import com.dropbear.ffi.NativeEngine
@@ -43,6 +44,10 @@ class DropbearEngine(val native: NativeEngine) {
         return entityRef
     }
 
+    fun getAssetEntry(displayName: String): AssetEntry {
+
+    }
+
     /**
      * Fetches the asset information from the internal AssetRegistry (located in
      * `dropbear_engine::asset::AssetRegistry`).
@@ -50,6 +55,7 @@ class DropbearEngine(val native: NativeEngine) {
      * ## Warning
      * The eucalyptus asset URI (or `euca://`) is case-sensitive.
      */
+    // idk if this still works...
     fun <T : AssetType> getAsset(eucaURI: String): Handle<T>? {
         val id = com.dropbear.getAsset(eucaURI)
         if (id == null || id <= 0L) return null
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/AssetEntry.kt b/scripting/commonMain/kotlin/com/dropbear/asset/AssetEntry.kt
new file mode 100644
index 0000000..9002692
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/AssetEntry.kt
@@ -0,0 +1,13 @@
+package com.dropbear.asset
+
+import kotlin.uuid.ExperimentalUuidApi
+import kotlin.uuid.Uuid
+
+class AssetEntry @OptIn(ExperimentalUuidApi::class) constructor(
+    val uuid: Uuid,
+    val name: String,
+    val assetType: AssetKind,
+    val location: ResourceReference,
+    val dependencies: List<Uuid>
+) {
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/AssetKind.kt b/scripting/commonMain/kotlin/com/dropbear/asset/AssetKind.kt
index 099bad7..26cf723 100644
--- a/scripting/commonMain/kotlin/com/dropbear/asset/AssetKind.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/AssetKind.kt
@@ -1,6 +1,10 @@
 package com.dropbear.asset
 
 enum class AssetKind {
+    Mesh,
     Texture,
-    Model
+    Audio,
+    Material,
+    Scene,
+    Script,
 }
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/ResourceReference.kt b/scripting/commonMain/kotlin/com/dropbear/asset/ResourceReference.kt
new file mode 100644
index 0000000..b6bc13a
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/ResourceReference.kt
@@ -0,0 +1,36 @@
+package com.dropbear.asset
+
+/**
+ * A resolved pointer to asset data, handed to the loader.
+ *
+ * You should never construct this by hand with a raw string —
+ * always derive it from an `AssetEntry` via `AssetEntry::to_reference()`, or from a `PackedAssetEntry`
+ * via the pak reader.
+ *
+ * This is the *loading strategy*, not the identity.
+ *
+ * Identity is always the [kotlin.uuid.Uuid] on [AssetEntry].
+*/
+sealed class ResourceReference {
+    /**
+     * A file within the project's `resources/` directory.
+     */
+    data class File(val path: String) : ResourceReference()
+
+    /**
+     * Bytes compiled into the eucalyptus-editor binary via `include_bytes!` (rust macro).
+     */
+    data class Embedded(val bytes: ByteArray): ResourceReference()
+
+    data class Packed(
+        val offset: ULong,
+        val length: ULong
+    ): ResourceReference()
+
+    /**
+     * No backing data; generated entirely at runtime.
+     *
+     * @param foo Does nothing. It's `null` by default to make the kotlin compiler happy.
+     */
+    data class Procedural(val foo: Any? = null): ResourceReference()
+}
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/scene/SceneManager.kt b/scripting/commonMain/kotlin/com/dropbear/scene/SceneManager.kt
index bcba826..a49d1d2 100644
--- a/scripting/commonMain/kotlin/com/dropbear/scene/SceneManager.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/scene/SceneManager.kt
@@ -3,7 +3,14 @@ package com.dropbear.scene
 /**
  * A manager for dealing with scene loading and scene querying. 
  */
-class SceneManager() {
+class SceneManager {
+    /**
+     * Returns the metadata of a scene.
+     */
+    fun getSceneMetadata(sceneName: String): SceneMetadata? {
+        return getSceneMetadataNative(sceneName)
+    }
+
     /**
      * Loads the resources of a scene asynchronously.
      *
@@ -42,4 +49,5 @@ class SceneManager() {
 
 internal expect fun SceneManager.loadSceneAsyncNative(sceneName: String): SceneLoadHandle?
 internal expect fun SceneManager.loadSceneAsyncNative(sceneName: String, loadingScene: String): SceneLoadHandle?
-internal expect fun SceneManager.switchToSceneImmediateNative(sceneName: String)
\ No newline at end of file
+internal expect fun SceneManager.switchToSceneImmediateNative(sceneName: String)
+internal expect fun SceneManager.getSceneMetadataNative(sceneName: String): SceneMetadata?
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/scene/SceneMetadata.kt b/scripting/commonMain/kotlin/com/dropbear/scene/SceneMetadata.kt
index 79423a5..fc653fb 100644
--- a/scripting/commonMain/kotlin/com/dropbear/scene/SceneMetadata.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/scene/SceneMetadata.kt
@@ -1,10 +1,18 @@
 package com.dropbear.scene
 
+import com.dropbear.EntityRef
+
 /**
  * A class that describes a `eucalyptus_core::scene::SceneConfig` and other information
  * related to that specific scene.
- *
- * Currently not used at this moment, will probably be implemented.
  */
-@Deprecated(message = "It is not implemented yet", level = DeprecationLevel.WARNING)
-class SceneMetadata(val id: Long)
\ No newline at end of file
+class SceneMetadata(
+    val id: Long,
+    val name: String,
+    val settings: SceneSettings
+) {
+    val entities: List<EntityRef>
+        get() = getEntities()
+}
+
+expect fun SceneMetadata.getEntities(): List<EntityRef>
\ No newline at end of file
diff --git a/scripting/commonMain/kotlin/com/dropbear/scene/SceneSettings.kt b/scripting/commonMain/kotlin/com/dropbear/scene/SceneSettings.kt
new file mode 100644
index 0000000..ba2f417
--- /dev/null
+++ b/scripting/commonMain/kotlin/com/dropbear/scene/SceneSettings.kt
@@ -0,0 +1,48 @@
+package com.dropbear.scene
+
+class SceneSettings {
+    /**
+     * Returns whether the scene's assets has been preloaded.
+     */
+    // must be val, cannot be changed (how could it possibly do anything if changed)
+    val preloaded: Boolean
+        get() = getPreload()
+
+    /**
+     * Shows hitboxes/wireframe for colliders for that specific scene.
+     */
+    var showHitboxes: Boolean
+        get() = getHitboxState()
+        set(value) = setHitboxState(value)
+
+    /**
+     * Overlays the HUD on top of the viewport if it exists as an entity.
+     */
+    var overlayHUD: Boolean
+        get() = getOverlayHUDState()
+        set(value) = setOverlayHUDState(value)
+
+    /**
+     * Overlays all billboard ui for all entities if it exists as a component.
+     */
+    var overlayBillboard: Boolean
+        get() = getOverlayBillboardState()
+        set(value) = setOverlayBillboardState(value)
+
+    /**
+     * Controls the strength of ambient/IBL lighting for this scene.
+     */
+    var ambientStrength: Double
+        get() = getAmbientStrength()
+        set(value) = setAmbientStrength(value)
+}
+
+expect fun SceneSettings.getPreload(): Boolean
+expect fun SceneSettings.getHitboxState(): Boolean
+expect fun SceneSettings.setHitboxState(value: Boolean)
+expect fun SceneSettings.getOverlayHUDState(): Boolean
+expect fun SceneSettings.setOverlayHUDState(value: Boolean)
+expect fun SceneSettings.getOverlayBillboardState(): Boolean
+expect fun SceneSettings.setOverlayBillboardState(value: Boolean)
+expect fun SceneSettings.getAmbientStrength(): Double
+expect fun SceneSettings.setAmbientStrength(value: Double)
\ No newline at end of file