tirbofish/dropbear · diff
fix + features: got the ptython script to work, did some fixes
Signature present but could not be verified.
Unverified
@@ -1,9 +0,0 @@ -[build] -jobs = 8 - -[profile.dev] -opt-level = 0 -debug = true -codegen-units = 16 -incremental = true -lto = false @@ -8,4 +8,3 @@ debug = true codegen-units = 16 incremental = true lto = false - @@ -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" @@ -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); } @@ -3,6 +3,7 @@ mod editor; mod menu; // mod scene1; pub(crate) mod states; +pub mod utils; use std::{cell::RefCell, rc::Rc}; @@ -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( @@ -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() @@ -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 +} @@ -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); + } + } + }); +}