kitgit

tirbofish/dropbear · commit

25d0fc37cbc1847c6fbb50a4e55b5995d99b5924

fix + features: got the ptython script to work, did some fixes

Unverified

tk <4tkbytes@pm.me> · 2025-07-25 08:58

view full diff

diff --git a/.cargo/config.toml b/.cargo/config.toml
deleted file mode 100644
index f1863bb..0000000
--- a/.cargo/config.toml
+++ /dev/null
@@ -1,9 +0,0 @@
-[build]
-jobs = 8
-
-[profile.dev]
-opt-level = 0
-debug = true
-codegen-units = 16
-incremental = true
-lto = false
diff --git a/Cargo.toml b/Cargo.toml
index ef5c329..81a2ce6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,4 +8,3 @@ debug = true
 codegen-units = 16
 incremental = true
 lto = false
-
diff --git a/eucalyptus/Cargo.toml b/eucalyptus/Cargo.toml
index 27fc788..82f9052 100644
--- a/eucalyptus/Cargo.toml
+++ b/eucalyptus/Cargo.toml
@@ -6,6 +6,7 @@ license = "GPL-3.0-or-later"
 description = "A visual game editor for the dropbear game engine"
 repository = "https://github.com/4tkbytes/dropbear-engine"
 readme = "README.md"
+default-run = "eucalyptus"
 
 [dependencies]
 serde = { version = "1.0.219", features = ["derive"] }
@@ -19,7 +20,6 @@ chrono = "0.4.41"
 egui-toast-fork = "0.18.0"
 egui_dock-fork = { version = "0.17", features = ["serde"] }
 once_cell = "1.21"
-pyo3 = { version = "0.25", features = ["auto-initialize"] }
 
 [dependencies.rhai]
 version = "1.22"
diff --git a/eucalyptus/src/editor.rs b/eucalyptus/src/editor.rs
index 4fa2809..0989557 100644
--- a/eucalyptus/src/editor.rs
+++ b/eucalyptus/src/editor.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashSet, path::PathBuf, sync::Arc};
+use std::{collections::HashSet, sync::Arc};
 
 use dropbear_engine::{
     camera::Camera,
@@ -76,8 +76,9 @@ impl Editor {
     pub fn save_project_config(&self) -> anyhow::Result<()> {
         let mut config = PROJECT.write().unwrap();
         config.dock_layout = Some(self.dock_state.clone());
-        let project_path = config.project_path.clone();
-        config.write_to(&PathBuf::from(project_path))
+        // let project_path = config.project_path.clone();
+        // config.write_to(&PathBuf::from(project_path))
+        config.write_to_all()
     }
 
     pub fn load_project_config(&mut self) -> anyhow::Result<()> {
@@ -175,6 +176,8 @@ impl Editor {
                     },
                 );
         });
+
+        self.toasts.show(ctx);
     }
 }
 
@@ -204,7 +207,7 @@ impl TabViewer for EditorTabViewer {
                 ui.label("Model/Entity List");
             }
             EditorTab::AssetViewer => {
-                ui.label("Asset Viewer");
+                
             }
             EditorTab::ResourceInspector => {
                 ui.label("Resource Inspector");
@@ -302,10 +305,10 @@ impl Keyboard for Editor {
     fn key_down(
         &mut self,
         key: dropbear_engine::winit::keyboard::KeyCode,
-        event_loop: &dropbear_engine::winit::event_loop::ActiveEventLoop,
+        _event_loop: &dropbear_engine::winit::event_loop::ActiveEventLoop,
     ) {
         match key {
-            KeyCode::Escape => event_loop.exit(),
+            // KeyCode::Escape => event_loop.exit(),
             KeyCode::F1 => {
                 self.is_cursor_locked = !self.is_cursor_locked;
                 if !self.is_cursor_locked {
@@ -350,6 +353,7 @@ impl Keyboard for Editor {
                             });
                         }
                     }
+                    
                 } else {
                     self.pressed_keys.insert(key);
                 }
diff --git a/eucalyptus/src/main.rs b/eucalyptus/src/main.rs
index c2b3bc6..1bddee1 100644
--- a/eucalyptus/src/main.rs
+++ b/eucalyptus/src/main.rs
@@ -3,6 +3,7 @@ mod editor;
 mod menu;
 // mod scene1;
 pub(crate) mod states;
+pub mod utils;
 
 use std::{cell::RefCell, rc::Rc};
 
diff --git a/eucalyptus/src/menu.rs b/eucalyptus/src/menu.rs
index d764002..e81b888 100644
--- a/eucalyptus/src/menu.rs
+++ b/eucalyptus/src/menu.rs
@@ -95,19 +95,6 @@ impl MainMenu {
             fs::remove_dir_all(&tests_path)?;
         }
 
-        log::debug!("Running poetry install in {:?}", project_path);
-        let status = Command::new("poetry")
-            .arg("install")
-            .current_dir(project_path)
-            .status()
-            .expect("Failed to run poetry install");
-        if !status.success() {
-            return Err(anyhow!(io::Error::new(
-                io::ErrorKind::Other,
-                "Poetry install failed",
-            )));
-        }
-
         Ok(())
     }
 
@@ -135,6 +122,7 @@ impl MainMenu {
                 ),
                 ("resources/shaders", 0.6, "Creating shader folder..."),
                 ("resources/textures", 0.8, "Creating textures folder..."),
+                ("scripts2", 0.85, "Copying python scripts to folder"),
                 ("src2", 0.9, "Creating project config file..."),
             ];
 
@@ -170,7 +158,8 @@ impl MainMenu {
                     } else if folder == "src2" {
                         if let Some(path) = &project_path {
                             let mut config = ProjectConfig::new(project_name.clone(), &path);
-                            let _ = config.write_to(&path);
+                            // let _ = config.write_to(&path);
+                            let _ = config.write_to_all();
                             let mut global = PROJECT.write().unwrap();
                             *global = config;
                             Ok(())
@@ -190,6 +179,16 @@ impl MainMenu {
                                 Err(e) => Err(anyhow!(e)),
                             }
                         }
+                    } else if folder == "scripts2" {
+                        if path.join("src/scripts").exists() {
+                            fs::write(
+                                &path.join("src/scripts/convert_model_to_image.py"),
+                                include_str!("scripts/convert_model_to_image.py"),
+                            )
+                            .map_err(|e| anyhow!(e))
+                        } else {
+                            Err(anyhow!("The src/scripts folder does not exist"))
+                        }
                     } else {
                         if !full_path.exists() {
                             fs::create_dir_all(&full_path)
@@ -401,12 +400,12 @@ impl Scene for MainMenu {
 impl Keyboard for MainMenu {
     fn key_down(
         &mut self,
-        key: dropbear_engine::winit::keyboard::KeyCode,
-        event_loop: &dropbear_engine::winit::event_loop::ActiveEventLoop,
+        _key: dropbear_engine::winit::keyboard::KeyCode,
+        _event_loop: &dropbear_engine::winit::event_loop::ActiveEventLoop,
     ) {
-        if key == dropbear_engine::winit::keyboard::KeyCode::Escape {
-            event_loop.exit();
-        }
+        // if key == dropbear_engine::winit::keyboard::KeyCode::Escape {
+        //     event_loop.exit();
+        // }
     }
 
     fn key_up(
diff --git a/eucalyptus/src/scripts/convert_model_to_image.py b/eucalyptus/src/scripts/convert_model_to_image.py
new file mode 100644
index 0000000..ba50d4d
--- /dev/null
+++ b/eucalyptus/src/scripts/convert_model_to_image.py
@@ -0,0 +1,85 @@
+"""
+A script used to generate images from .obj, .glb and .stl 3D models to
+use with thumbnails.
+
+Package Requirements: 3d-to-image
+Author: tk
+"""
+import os, io, sys
+from model_to_image import process_obj, glb_to_image, process_stl
+
+SUPPORTED_EXTS = {".obj", ".glb", ".stl"}
+
+class ProcessModel:
+    def __init__(self, model_path) -> None:
+        self.model_path = model_path
+        self.model_dir = os.path.dirname(model_path)
+        self.model_name = os.path.splitext(os.path.basename(model_path))[0]
+        self.thumbnails_dir = os.path.join(self.model_dir, "thumbnails")
+        os.makedirs(self.thumbnails_dir, exist_ok=True)
+        self.output_dir = os.path.join(self.thumbnails_dir, f"{self.model_name}.png")
+
+    def render(self) -> bool:
+        """
+        Render the selected model (from the class) and generate an image stored in
+        the model_dir/thumbnails/model_name.png
+        """
+        ext = os.path.splitext(self.model_path)[1].lower()
+        image_bytes = None
+        if ext == ".obj":
+            image_bytes = process_obj(self.model_path)
+        elif ext == ".glb":
+            with open(self.model_path, "rb") as f:
+                glb_bytes = f.read()
+            image_bytes = glb_to_image(glb_bytes)
+        elif ext == ".stl":
+            stl_images = process_stl(self.model_path)
+            if "error" in stl_images:
+                print(stl_images["error"])
+                return False
+            if "front" in stl_images and stl_images["front"]:
+                image_bytes = stl_images["front"]
+            else:
+                for img_bytes in stl_images.values():
+                    if img_bytes:
+                        image_bytes = img_bytes
+                        break
+        else:
+            print(f"Unsupported model format: {ext}")
+            return False
+
+        if isinstance(image_bytes, io.BytesIO):
+            with open(self.output_dir, "wb") as f:
+                f.write(image_bytes.getvalue())
+            print(f"Thumbnail saved to {self.output_dir}")
+            return True
+        else:
+            print(f"Model conversion failed or returned error: {image_bytes}")
+            return False
+
+def process_path(path):
+    if os.path.isdir(path):
+        print(f"Processing folder: {path}")
+        for fname in os.listdir(path):
+            fpath = os.path.join(path, fname)
+            if os.path.isfile(fpath) and os.path.splitext(fname)[1].lower() in SUPPORTED_EXTS:
+                print(f"Processing file: {fpath}")
+                ProcessModel(fpath).render()
+    elif os.path.isfile(path):
+        ext = os.path.splitext(path)[1].lower()
+        if ext in SUPPORTED_EXTS:
+            print(f"Processing file: {path}")
+            ProcessModel(path).render()
+        else:
+            print(f"Unsupported file format: {ext}")
+    else:
+        print(f"Path does not exist: {path}")
+
+def main():
+    if len(sys.argv) < 2:
+        print("Usage: python convert_model_to_image.py <file_or_folder_path>")
+        return
+    process_path(sys.argv[1])
+
+if __name__ == "__main__":
+    main()
diff --git a/eucalyptus/src/states.rs b/eucalyptus/src/states.rs
index 630660a..a2af6d5 100644
--- a/eucalyptus/src/states.rs
+++ b/eucalyptus/src/states.rs
@@ -5,9 +5,7 @@
 //! as well as public structs related to that config and docs (hopefully).
 
 use std::{
-    fs,
-    path::{Path, PathBuf},
-    sync::RwLock,
+    fmt::{self, Display, Formatter}, fs, path::PathBuf, sync::RwLock
 };
 
 use chrono::Utc;
@@ -35,7 +33,9 @@ pub struct ProjectConfig {
     #[serde(default)]
     pub dock_layout: Option<DockState<EditorTab>>,
     #[serde(default)]
-    pub assets: Assets,
+    pub resources_config: Option<ResourceConfig>,
+    #[serde(default)]
+    pub source_config: Option<SourceConfig>,
 }
 
 impl ProjectConfig {
@@ -45,14 +45,17 @@ impl ProjectConfig {
         let date_created = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S"));
         let project_path_str = project_path.to_str().unwrap().to_string();
         let date_last_accessed = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S"));
-        Self {
+        let mut result = Self {
             project_name,
             project_path: project_path_str,
             date_created,
             date_last_accessed,
             dock_layout: None,
-            assets: Assets::walk(&project_path),
-        }
+            resources_config: None,
+            source_config: None,
+        };
+        let _ = result.load_config_to_memory(); // TODO: Deal with later...
+        result
     }
 
     /// This function writes the [`ProjectConfig`] struct (and other PathBufs) to a file of the choice
@@ -60,12 +63,16 @@ impl ProjectConfig {
     ///
     /// # Parameters
     /// * path - The root **folder** of the project.
+    
     pub fn write_to(&mut self, path: &PathBuf) -> anyhow::Result<()> {
+        self.load_config_to_memory()?;
         self.date_last_accessed = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S"));
-        self.assets = Assets::walk(path);
+        // self.assets = Assets::walk(path);
         let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default())
             .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?;
         let config_path = path.join(format!("{}.eucp", self.project_name.clone().to_lowercase()));
+        self.project_path = path.clone().to_str().unwrap().to_string();
+
         fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?;
         Ok(())
     }
@@ -81,121 +88,268 @@ impl ProjectConfig {
         log::info!("Loaded project!");
         log::debug!("Loaded config info: {:#?}", config);
         log::debug!("Updating with new content");
-        config.write_to(&path.parent().unwrap().to_path_buf())?;
-        config.assets = Assets::walk(&path.parent().unwrap().to_path_buf());
+        // config.write_to(&path.parent().unwrap().to_path_buf())?;
+        // config.assets = Assets::walk(&path.parent().unwrap().to_path_buf());
+        config.load_config_to_memory()?;
+        config.write_to_all()?;
         log::debug!("Successfully updated!");
         Ok(config)
     }
-}
 
-#[derive(Default, Debug, Serialize, Deserialize)]
-pub struct Assets {
-    nodes: Vec<Node>,
-}
-
-pub(crate) fn path_contains_folder(path: &PathBuf, folder: &str) -> bool {
-    path.components().any(|comp| comp.as_os_str() == folder)
-}
+    /// This function loads a `source.eucc` or a `resources.eucc` config file into memory, allowing
+    /// you to reference and load the nodes located inside them. 
+    pub fn load_config_to_memory(&mut self) -> anyhow::Result<()> {
+        let project_root = PathBuf::from(&self.project_path);
 
-impl Assets {
-    /// This function goes into your project directory and "walks" (recursively) / fetches other configuration files
-    /// and other assets/scripts for the .eucp project file to look for.
-    ///
-    /// If there are config files missing, it will generate the config file and populate it, then
-    /// create a reference to that folder config file (.eucc) to the .eucp project config file.
-    pub fn walk(project_path: &PathBuf) -> Self {
-        fn locate_config_in_dir(path: &Path) -> Vec<Node> {
-            let mut nodes = Vec::new();
-
-            if let Ok(entries) = fs::read_dir(path) {
-                for entry in entries.flatten() {
-                    let entry_path = entry.path();
-                    let name = entry_path.file_name().unwrap().to_str().unwrap();
-                    if path_contains_folder(&entry_path, ".git") {
-                        continue;
+        match ResourceConfig::read_from(&project_root) {
+            Ok(resources) => self.resources_config = Some(resources),
+            Err(e) => {
+                if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
+                    if io_err.kind() == std::io::ErrorKind::NotFound {
+                        log::warn!("resources.eucc not found, creating default.");
+                        let default = ResourceConfig {
+                            path: project_root.join("resources"),
+                            nodes: vec![],
+                        };
+                        default.write_to(&project_root)?;
+                        self.resources_config = Some(default);
+                    } else {
+                        log::warn!("Failed to load resources.eucc: {}", e);
+                        self.resources_config = None;
                     }
+                } else {
+                    log::warn!("Failed to load resources.eucc: {}", e);
+                    self.resources_config = None;
+                }
+            }
+        }
 
-                    if entry_path.is_dir() {
-                        let config_path = entry_path.join(format!("{}.eucc", name));
-                        let mut folder = Folder {
-                            name: String::from(name),
-                            path: entry_path.clone(),
-                            nodes: locate_config_in_dir(&entry_path),
+        match SourceConfig::read_from(&project_root) {
+            Ok(source) => self.source_config = Some(source),
+            Err(e) => {
+                if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
+                    if io_err.kind() == std::io::ErrorKind::NotFound {
+                        log::warn!("source.eucc not found, creating default.");
+                        let default = SourceConfig {
+                            path: project_root.join("src"),
+                            nodes: vec![],
                         };
-
-                        if config_path.exists() {
-                            folder.nodes.push(Node::File(File {
-                                name: format!("{}.eucc", name),
-                                path: config_path.clone(),
-                            }));
-                        }
-
-                        nodes.push(Node::Folder(folder));
-                    } else if entry_path.extension().map_or(false, |ext| ext == "eucc") {
-                        let parent = entry_path
-                            .parent()
-                            .and_then(|p| p.file_name())
-                            .map(|n| n.to_str().unwrap());
-                        let expected_name = parent.map(|n| format!("{}.eucc", n));
-                        if Some(name) != expected_name.as_deref() {
-                            nodes.push(Node::File(File {
-                                name: String::from(name),
-                                path: entry_path.clone(),
-                            }));
-                        }
+                        default.write_to(&project_root)?;
+                        self.source_config = Some(default);
+                    } else {
+                        log::warn!("Failed to load source.eucc: {}", e);
+                        self.source_config = None;
                     }
+                } else {
+                    log::warn!("Failed to load source.eucc: {}", e);
+                    self.source_config = None;
                 }
             }
+        }
+
+        Ok(())
+}
+
 
-            nodes
+    /// # Parameters
+    /// * path - The root folder of the project
+    pub fn write_to_all(&mut self) -> anyhow::Result<()> {
+        let path = PathBuf::from(self.project_path.clone());
+        if let Some(res) = &self.resources_config {
+            res.write_to(&path)?;
         }
 
-        Assets {
-            nodes: locate_config_in_dir(project_path),
+        if let Some(src) = &self.source_config {
+            src.write_to(&path)?;
         }
+
+        self.write_to(&path)?;
+        Ok(())
     }
 }
 
+#[allow(dead_code)]
+pub(crate) fn path_contains_folder(path: &PathBuf, folder: &str) -> bool {
+    path.components().any(|comp| comp.as_os_str() == folder)
+}
+
 #[derive(Debug, Serialize, Deserialize)]
 pub enum Node {
     File(File),
     Folder(Folder),
 }
 
-#[derive(Debug, Serialize, Deserialize)]
-pub enum ConfigType {
-    None,
-    Project(ProjectConfig),
-    Resource(ResourceConfig),
-    Source(SourceCodeConfig),
+#[derive(Default, Debug, Serialize, Deserialize)]
+pub struct File {
+    pub name: String,
+    pub path: PathBuf,
+    pub resource_type: Option<ResourceType>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
-pub struct ResourceConfig {
+#[derive(Default, Debug, Serialize, Deserialize)]
+pub struct Folder {
+    pub name: String,
+    pub path: PathBuf,
     pub nodes: Vec<Node>,
 }
 
+/// The type of resource
 #[derive(Debug, Serialize, Deserialize)]
-pub struct SourceCodeConfig {
-    pub nodes: Vec<Node>,
+pub enum ResourceType {
+    Unknown,
+    Model(Model),
+    Thumbnail,
+    Texture,
+    Shader,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Model {
+    pub thumbnail_location: PathBuf
 }
 
-impl Default for ConfigType {
-    fn default() -> Self {
-        ConfigType::None
+impl Model {
+    pub fn gen_thumbnail(project_path: &PathBuf, model_path: &PathBuf) -> Self {
+        let thumbnail_path = model_path.parent().unwrap().join("thumbnails").join(format!("{}.png", model_path.file_stem().unwrap().to_string_lossy()));
+        if !thumbnail_path.exists() {
+            crate::utils::convert_model_to_image(project_path, model_path);
+        }
+        Self {
+            thumbnail_location: thumbnail_path,
+        }
+    }
+}
+
+impl Display for ResourceType {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        let str = match self {
+            ResourceType::Unknown => "unknown",
+            ResourceType::Model(_) => "model",
+            ResourceType::Texture => "texture",
+            ResourceType::Shader => "shader",
+            ResourceType::Thumbnail => "thumbnail",
+        };
+        write!(f, "{}", str)
     }
 }
 
+/// This is the resource config.
 #[derive(Default, Debug, Serialize, Deserialize)]
-pub struct File {
-    pub name: String,
-    /// A reference from the root node
+pub struct ResourceConfig {
+    /// The path to the resource folder. 
     pub path: PathBuf,
+    /// The files and folders of the assets
+    pub nodes: Vec<Node>,
+}
+
+impl ResourceConfig {
+    /// # Parameters
+    /// - path: The root **folder** of the project
+    pub fn write_to(&self, path: &PathBuf) -> anyhow::Result<()> {
+        let resource_dir = path.join("resources");
+        let updated_config = ResourceConfig {
+            path: resource_dir.clone(),
+            nodes: collect_nodes(&resource_dir, path, vec!["thumbnails"].as_slice()),
+        };
+        
+        let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default())
+            .map_err(|e| anyhow::anyhow!("RON serialisation error: {}", e))?;
+        let config_path = path.join("resources").join("resources.eucc");
+        fs::create_dir_all(config_path.parent().unwrap())?;
+        fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?;
+        Ok(())
+    }
+
+    /// # Parameters
+    /// - path: The location to the **resources.eucc** file
+    pub fn read_from(path: &PathBuf) -> anyhow::Result<Self> {
+        let config_path = path.join("resources").join("resources.eucc");
+        let ron_str = fs::read_to_string(&config_path)?;
+        let config: ResourceConfig = ron::de::from_str(&ron_str)
+            .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?;
+        Ok(config)
+    }
 }
 
 #[derive(Default, Debug, Serialize, Deserialize)]
-pub struct Folder {
-    pub name: String,
+pub struct SourceConfig {
+    /// The path to the resource folder. 
     pub path: PathBuf,
+    /// The files and folders of the assets
     pub nodes: Vec<Node>,
 }
+
+impl SourceConfig {
+    /// # Parameters
+    /// - path: The root **folder** of the project
+    pub fn write_to(&self, path: &PathBuf) -> anyhow::Result<()> {
+        let resource_dir = path.join("source");
+        let updated_config = SourceConfig {
+            path: resource_dir.clone(),
+            nodes: collect_nodes(&resource_dir, path, vec!["scripts"].as_slice()),
+        };
+        
+        let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default())
+            .map_err(|e| anyhow::anyhow!("RON serialisation error: {}", e))?;
+        let config_path = path.join("source").join("source.eucc");
+        fs::create_dir_all(config_path.parent().unwrap())?;
+        fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?;
+        Ok(())
+    }
+
+    /// # Parameters
+    /// - path: The location to the **source.eucc** file
+    pub fn read_from(path: &PathBuf) -> anyhow::Result<Self> {
+        let config_path = path.join("src").join("source.eucc");
+        let ron_str = fs::read_to_string(&config_path)?;
+        let config: SourceConfig = ron::de::from_str(&ron_str)
+            .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?;
+        Ok(config)
+    }
+}
+
+fn collect_nodes(dir: &PathBuf, project_path: &PathBuf, exclude_list: &[&str]) -> Vec<Node> {
+    let mut nodes = Vec::new();
+    if let Ok(entries) = fs::read_dir(dir) {
+        for entry in entries.flatten() {
+            let entry_path = entry.path();
+            let name = entry_path.file_name().unwrap_or_default().to_string_lossy().to_string();
+
+            if entry_path.is_dir() && exclude_list.iter().any(|ex| &ex.to_string() == &name) {
+                log::debug!("Skipped past folder {:?}", name);
+                continue;
+            }
+
+            if entry_path.is_dir() {
+                let folder_nodes = collect_nodes(&entry_path, project_path, exclude_list);
+                nodes.push(Node::Folder(Folder {
+                    name,
+                    path: entry_path.clone(),
+                    nodes: folder_nodes,
+                }));
+            } else {
+                let parent_folder = entry_path.parent()
+                    .and_then(|p| p.file_name())
+                    .map(|n| n.to_string_lossy().to_lowercase())
+                    .unwrap_or_default();
+
+                let resource_type = if parent_folder.contains("model") {
+                    Some(ResourceType::Model(Model::gen_thumbnail(project_path, &entry_path)))
+                } else if parent_folder.contains("texture") {
+                    Some(ResourceType::Texture)
+                } else if parent_folder.contains("shader") {
+                    Some(ResourceType::Shader)
+                } else {
+                    Some(ResourceType::Unknown)
+                };
+
+                nodes.push(Node::File(File {
+                    name,
+                    path: entry_path.clone(),
+                    resource_type,
+                }));
+            }
+        }
+    }
+    nodes
+}
\ No newline at end of file
diff --git a/eucalyptus/src/utils.rs b/eucalyptus/src/utils.rs
new file mode 100644
index 0000000..5eb20d2
--- /dev/null
+++ b/eucalyptus/src/utils.rs
@@ -0,0 +1,37 @@
+use std::{path::PathBuf, process::Command};
+
+use dropbear_engine::log;
+
+pub fn convert_model_to_image(project_path: &PathBuf, path: &PathBuf) {
+    let path = path.clone();
+    let project_path = project_path.clone();
+    std::thread::spawn(move || {
+        let script_path = "src/scripts/convert_model_to_image.py";
+        let poetry_cmd = Command::new("poetry")
+            .current_dir(project_path)
+            .arg("run")
+            .arg("python")
+            .arg(script_path)
+            .arg(path.to_str().unwrap())
+            .output();
+
+        match poetry_cmd {
+            Ok(output) => {
+                if !output.status.success() {
+                    log::error!(
+                        "Thumbnail generation failed: {}",
+                        String::from_utf8_lossy(&output.stderr)
+                    );
+                } else {
+                    log::info!(
+                        "Thumbnail generation succeeded: {}",
+                        String::from_utf8_lossy(&output.stdout)
+                    );
+                }
+            }
+            Err(e) => {
+                log::error!("Failed to run thumbnail generator: {}", e);
+            }
+        }
+    });
+}
\ No newline at end of file