kitgit

tirbofish/dropbear · commit

a684e042ebac9579b408939349c48a866336cd36

wip: deriving collider from parent

Unverified

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

view full diff

diff --git a/crates/eucalyptus-core/src/config.rs b/crates/eucalyptus-core/src/config.rs
index 8345b38..733e074 100644
--- a/crates/eucalyptus-core/src/config.rs
+++ b/crates/eucalyptus-core/src/config.rs
@@ -220,15 +220,23 @@ impl ProjectConfig {
             }
         }
 
-        for scene_entry in fs::read_dir(scene_folder)? {
-            let scene_entry = scene_entry?;
-            let path = scene_entry.path();
+        fn collect_eucs_files(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
+            let Ok(entries) = fs::read_dir(dir) else { return; };
+            for entry in entries.flatten() {
+                let path = entry.path();
+                if path.is_dir() {
+                    collect_eucs_files(&path, out);
+                } else if path.extension().and_then(|s| s.to_str()) == Some("eucs") {
+                    out.push(path);
+                }
+            }
+        }
 
-            if path.is_file()
-                && path.extension().and_then(|s| s.to_str()) == Some("eucs")
-                && path.extension().and_then(|s| s.to_str()) != Some("bak")
-            {
-                match SceneConfig::read_from(&path) {
+        let mut eucs_files = Vec::new();
+        collect_eucs_files(scene_folder, &mut eucs_files);
+
+        for path in eucs_files {
+            match SceneConfig::read_from(&path) {
                     Ok(scene) => {
                         log::debug!("Loaded scene config from file, added to SCENES entry: {}", scene.scene_name);
                         scene_configs.push(scene);
@@ -269,7 +277,6 @@ impl ProjectConfig {
                         }
                     }
                 }
-            }
         }
 
         if scene_configs.is_empty() {
diff --git a/crates/eucalyptus-core/src/physics/collider.rs b/crates/eucalyptus-core/src/physics/collider.rs
index 32c226d..5ad01c5 100644
--- a/crates/eucalyptus-core/src/physics/collider.rs
+++ b/crates/eucalyptus-core/src/physics/collider.rs
@@ -27,7 +27,8 @@ use crate::states::Label;
 use crate::types::{NCollider, NVector3};
 use ::jni::JNIEnv;
 use ::jni::objects::{JObject, JValue};
-use dropbear_engine::entity::inspect_rotation_dquat;
+use dropbear_engine::asset::ASSET_REGISTRY;
+use dropbear_engine::entity::{inspect_rotation_dquat, MeshRenderer};
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::wgpu::util::{BufferInitDescriptor, DeviceExt};
 use dropbear_engine::wgpu::{Buffer, BufferUsages};
@@ -107,7 +108,7 @@ impl Component for ColliderGroup {
 impl InspectableComponent for ColliderGroup {
     fn inspect(
         &mut self,
-        _world: &World,
+        world: &World,
         entity: Entity,
         ui: &mut Ui,
         _graphics: Arc<SharedGraphicsContext>,
@@ -143,6 +144,47 @@ impl InspectableComponent for ColliderGroup {
                 if ui.button("Add Collider").clicked() {
                     self.colliders.push(Collider::new());
                 }
+
+                if ui.button("Derive from Mesh").clicked() {
+                    let model_handle = world
+                        .get::<&MeshRenderer>(entity)
+                        .ok()
+                        .map(|r| r.model());
+
+                    if let Some(handle) = model_handle {
+                        let registry = ASSET_REGISTRY.read();
+                        if let Some(model) = registry.get_model(handle) {
+                            let mut min = [f32::INFINITY; 3];
+                            let mut max = [f32::NEG_INFINITY; 3];
+
+                            for mesh in &model.meshes {
+                                for vertex in &mesh.vertices {
+                                    let p = vertex.position;
+                                    for i in 0..3 {
+                                        min[i] = min[i].min(p[i]);
+                                        max[i] = max[i].max(p[i]);
+                                    }
+                                }
+                            }
+
+                            if min.iter().all(|v| v.is_finite()) && max.iter().all(|v| v.is_finite()) {
+                                let half_extents = [
+                                    (max[0] - min[0]) * 0.5,
+                                    (max[1] - min[1]) * 0.5,
+                                    (max[2] - min[2]) * 0.5,
+                                ];
+                                let center = [
+                                    (max[0] + min[0]) * 0.5,
+                                    (max[1] + min[1]) * 0.5,
+                                    (max[2] + min[2]) * 0.5,
+                                ];
+                                let mut collider = Collider::box_collider(half_extents);
+                                collider.translation = center;
+                                self.colliders.push(collider);
+                            }
+                        }
+                    }
+                }
             });
     }
 }
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index e170a18..3cfee6a 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -165,6 +165,18 @@ impl SceneConfig {
         Ok(())
     }
 
+    /// Write the scene config directly to `self.path`, creating parent directories as needed.
+    pub fn write_to_path(&self) -> anyhow::Result<()> {
+        log::debug!("Writing scene config to {}", self.path.display());
+        let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default())
+            .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?;
+        if let Some(parent) = self.path.parent() {
+            fs::create_dir_all(parent)?;
+        }
+        fs::write(&self.path, ron_str)?;
+        Ok(())
+    }
+
     /// Read a scene config from a .eucs file
     pub fn read_from(scene_path: impl AsRef<Path>) -> anyhow::Result<Self> {
         let ron_str = fs::read_to_string(scene_path.as_ref())?;
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index c070f39..3ff929c 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -68,7 +68,7 @@ use parking_lot::{Mutex, RwLock};
 use rfd::FileDialog;
 use std::collections::{HashSet, VecDeque};
 use std::rc::Rc;
-use std::{collections::HashMap, fs, path::PathBuf, sync::Arc, time::Instant};
+use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Instant};
 use std::cmp::PartialEq;
 use tokio::sync::oneshot;
 use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation};
@@ -187,9 +187,10 @@ pub struct Editor {
     // scene creation
     open_new_scene_window: bool,
     new_scene_name: String,
+    new_scene_target_dir: Option<PathBuf>,
     current_scene_name: Option<String>,
     pending_scene_load: Option<PendingSceneLoad>,
-    pending_scene_creation: Option<String>,
+    pending_scene_creation: Option<(String, PathBuf)>,
 
     // about
     nerd_stats: Rc<RwLock<NerdStats>>,
@@ -311,6 +312,7 @@ impl Editor {
             ui_dock_state_shared: None,
             open_new_scene_window: false,
             new_scene_name: String::new(),
+            new_scene_target_dir: None,
             current_scene_name: None,
             pending_scene_load: None,
             pending_scene_creation: None,
@@ -857,7 +859,7 @@ impl Editor {
         self.world_load_handle = Some(handle);
     }
 
-    fn create_new_scene(&mut self, name: &str) -> anyhow::Result<()> {
+    fn create_new_scene(&mut self, name: &str, target_dir: PathBuf) -> anyhow::Result<()> {
         let trimmed_name = name.trim();
         if trimmed_name.is_empty() {
             return Err(anyhow::anyhow!("Scene name cannot be empty"));
@@ -869,8 +871,6 @@ impl Editor {
             ));
         }
 
-        let scene_name_owned = trimmed_name.to_string();
-
         let project_root = {
             let cfg = PROJECT.read();
             cfg.project_path.clone()
@@ -880,24 +880,59 @@ impl Editor {
             return Err(anyhow::anyhow!("Project path is not set"));
         }
 
-        let scenes_dir = project_root.join("scenes");
-        if !scenes_dir.exists() {
-            fs::create_dir_all(&scenes_dir)?;
+        if !target_dir.starts_with(&project_root) {
+            return Err(anyhow::anyhow!("Target directory is outside the project"));
         }
 
-        let target_path = scenes_dir.join(format!("{}.eucs", scene_name_owned));
+        let target_path = target_dir.join(format!("{}.eucs", trimmed_name));
         if target_path.exists() {
-            return Err(anyhow::anyhow!(
-                "Scene '{}' already exists",
-                scene_name_owned
-            ));
+            return Err(anyhow::anyhow!("Scene '{}' already exists", trimmed_name));
         }
 
-        let scene_config = SceneConfig::new(scene_name_owned.clone(), &target_path);
-        scene_config.write_to(&project_root)?;
+        let scene_config = SceneConfig::new(trimmed_name.to_string(), &target_path);
+        scene_config.write_to_path()?;
+
+        self.queue_scene_load_from_path(&target_path)?;
+        success!("Created scene '{}'", trimmed_name);
+        Ok(())
+    }
+
+    fn queue_scene_load_from_path(&mut self, path: &std::path::Path) -> anyhow::Result<()> {
+        let should_persist_current = self.current_scene_name.is_some()
+            && self.is_world_loaded.is_fully_loaded()
+            && self.world.len() > 0
+            && {
+                let scenes = SCENES.read();
+                !scenes.is_empty()
+            };
+
+        if should_persist_current {
+            self.save_current_scene()?;
+        }
+
+        if let Some(current) = self.current_scene_name.as_deref() {
+            states::unload_scene(current);
+        }
+
+        let scene = SceneConfig::read_from(path)?;
+
+        {
+            let mut scenes = SCENES.write();
+            scenes.retain(|existing| existing.scene_name != scene.scene_name);
+            scenes.insert(0, scene.clone());
+        }
+
+        {
+            let mut project = PROJECT.write();
+            project.last_opened_scene = Some(scene.scene_name.clone());
+            project.write_to_all()?;
+        }
+
+        log::info!("Scene '{}' staged for loading", scene.scene_name);
+
+        self.current_scene_name = Some(scene.scene_name.clone());
+        self.pending_scene_load = Some(PendingSceneLoad { scene });
 
-        self.queue_scene_load_by_name(&scene_name_owned)?;
-        success!("Created scene '{}'", scene_name_owned);
         Ok(())
     }
 
@@ -920,22 +955,15 @@ impl Editor {
             return Err(anyhow::anyhow!("Project path is not set"));
         }
 
-        let scenes_dir = project_root.join("scenes");
-        if !path.starts_with(&scenes_dir) {
+        if !path.starts_with(&project_root) {
             return Err(anyhow::anyhow!(
                 "Scene '{}' is outside of the current project",
                 path.display()
             ));
         }
 
-        let scene_name = path
-            .file_stem()
-            .and_then(|stem| stem.to_str())
-            .ok_or_else(|| anyhow::anyhow!("Scene file name is invalid"))?;
-
-        self.queue_scene_load_by_name(scene_name)?;
-        info!("Queued scene '{}' for loading", scene_name);
-        Ok(())
+        info!("Loading scene from path: {}", path.display());
+        self.queue_scene_load_from_path(&path)
     }
 
     pub fn show_ui(&mut self, ctx: &Context, graphics: Arc<SharedGraphicsContext>) {
@@ -943,8 +971,8 @@ impl Editor {
 
         {
             puffin::profile_scope!("show_ui.pending_scene_creation");
-            if let Some(scene_name) = self.pending_scene_creation.take() {
-                let result = self.create_new_scene(scene_name.as_str());
+            if let Some((scene_name, target_dir)) = self.pending_scene_creation.take() {
+                let result = self.create_new_scene(scene_name.as_str(), target_dir);
                 self.new_scene_name.clear();
                 if let Err(e) = result {
                     fatal!("Failed to create scene '{}': {}", scene_name, e);
@@ -1461,8 +1489,53 @@ impl Editor {
                     ui.vertical(|ui| {
                         ui.label("Name: ");
                         ui.text_edit_singleline(&mut self.new_scene_name);
+
+                        ui.add_space(4.0);
+
+                        let scenes_root = {
+                            let project = PROJECT.read();
+                            project.project_path.join("resources").join("scenes")
+                        };
+                        let target_dir = self
+                            .new_scene_target_dir
+                            .as_deref()
+                            .unwrap_or(&scenes_root);
+                        let display_path = target_dir
+                            .strip_prefix(
+                                PROJECT.read().project_path.as_path(),
+                            )
+                            .unwrap_or(target_dir)
+                            .display()
+                            .to_string();
+
+                        ui.label("Folder:");
+                        ui.horizontal(|ui| {
+                            ui.label(&display_path);
+                            if ui.button("Browse…").clicked() {
+                                let mut dialog = FileDialog::new();
+                                if scenes_root.exists() {
+                                    dialog = dialog.set_directory(&scenes_root);
+                                }
+                                if let Some(dir) = dialog.pick_folder() {
+                                    self.new_scene_target_dir = Some(dir);
+                                }
+                            }
+                            if self.new_scene_target_dir.is_some()
+                                && ui.button("Reset").clicked()
+                            {
+                                self.new_scene_target_dir = None;
+                            }
+                        });
+
+                        ui.add_space(4.0);
+
                         if ui.button("Create").clicked() {
-                            self.pending_scene_creation = Some(self.new_scene_name.clone());
+                            let dir = self
+                                .new_scene_target_dir
+                                .clone()
+                                .unwrap_or(scenes_root);
+                            self.pending_scene_creation =
+                                Some((self.new_scene_name.clone(), dir));
                             close_requested = true;
                         }
                     });