From 0e45405d799822b3324cd2fc5c820c70951ab121 Mon Sep 17 00:00:00 2001 From: IllusionMan1212 Date: Fri, 20 Oct 2023 19:29:36 +0200 Subject: [PATCH] importer/stl: binary stl loader is implemented --- TODO.md | 1 + src/importer/mod.rs | 20 ++++-- src/importer/stl.rs | 171 ++++++++++++++++++++++++++++++++++++++------ src/utils.rs | 27 +++++-- 4 files changed, 188 insertions(+), 31 deletions(-) diff --git a/TODO.md b/TODO.md index 6ddd4b6..7166d59 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,7 @@ - [x] Help menu - [x] About window - [ ] File logger + - [ ] Logger should log to file, console and stdout - [x] Move console to be under objects - [x] Don't have the viewport window be scrollable - [x] Object rotation with left click + mouse movement (very rudimentary implementation, but works) diff --git a/src/importer/mod.rs b/src/importer/mod.rs index d17d863..61a18db 100644 --- a/src/importer/mod.rs +++ b/src/importer/mod.rs @@ -1,8 +1,9 @@ mod obj; +mod stl; use std::{path::PathBuf, collections::HashMap}; -use crate::{mesh::Vertex, aabb::AABB}; +use crate::{mesh::Vertex, aabb::AABB, utils::SupportedFileExtensions}; #[derive(Debug, Clone)] pub enum TextureType { @@ -86,13 +87,20 @@ pub struct Object { } pub fn load_from_file(path: &PathBuf) -> Result> { - // FIXME: don't explode if to_str() fails - let path_str = path.to_str().unwrap(); + let path_str = match path.to_str() { + Some(s) => s, + None => return Err("Failed to convert path to string".into()) + }; + let file = std::fs::File::open(path_str)?; - // TODO: support both stl and obj based on file extension - // if no extension, then test for binary STL magic bytes + // TODO: if no extension, then test for binary STL magic bytes // if no magic bytes, then try to guess based on the first line of text in the file - let obj = obj::load_obj(path, file)?; + let obj = match SupportedFileExtensions::from_str(path.extension().unwrap().to_str().unwrap()) { + Some(SupportedFileExtensions::STL) => stl::load_stl(file)?, + Some(SupportedFileExtensions::OBJ) => obj::load_obj(path, file)?, + _ => panic!("Unsupported file extension: {}", path_str), + }; + Ok(obj) } diff --git a/src/importer/stl.rs b/src/importer/stl.rs index 9362eb8..a2e73ed 100644 --- a/src/importer/stl.rs +++ b/src/importer/stl.rs @@ -1,27 +1,156 @@ -struct STLHeader { - header: [u8; 80], - num_triangles: u32, +use std::io::{Read, Seek, BufReader}; + +use crate::{importer::ObjMesh, importer::Object, importer::Material, aabb::AABB, mesh::Vertex}; + +const STL_HEADER_SIZE: u64 = 80; +// might not be enough since it's not guaranteed. we'll see +const ASCII_STL_MAGIC: [u8; 11] = [0x73, 0x6F, 0x6C, 0x69, 0x64, 0x20, 0x61, 0x73, 0x63, 0x69, 0x69]; // "solid ascii" + +#[repr(packed(2))] +struct STLTriangle { + normal: glm::Vec3, + verts: [glm::Vec3; 3], + attribute_byte_count: u16 } -struct Vertex { - x: f32, - y: f32, - z: f32, +#[derive(Debug)] +struct TrianglesIter { + reader: R, + buf: Vec, + triangles_to_read: usize, + triangles_read: usize, } -struct Triangle { - normal: [f32; 3], - vertices: [Vertex; 3], - attribute_byte_count: u16, -} - -struct STL { - header: STLHeader, - triangles: Vec, -} - -impl STL { - pub fn new(stl: Vec) -> () { - // TODO: +impl TrianglesIter { + fn new(reader: R, triangles_to_read: usize) -> Self { + TrianglesIter { + reader, + buf: vec![0u8; 50], + triangles_to_read, + triangles_read: 0, + } } } + +impl Iterator for TrianglesIter { + type Item = STLTriangle; + + fn next(&mut self) -> Option { + if self.triangles_read >= self.triangles_to_read { + None + } else { + let _ = self.reader.read_exact(&mut self.buf); + + self.triangles_read += 1; + + unsafe { + let normal = (self.buf[0..12].as_ptr() as *const glm::Vec3).read(); + let verts = [ + (self.buf[12..24].as_ptr() as *const glm::Vec3).read(), + (self.buf[24..36].as_ptr() as *const glm::Vec3).read(), + (self.buf[36..48].as_ptr() as *const glm::Vec3).read(), + ]; + let abc = u16::from_le_bytes([self.buf[48], self.buf[49]]); + + Some(STLTriangle{ + verts, + normal, + attribute_byte_count: abc + }) + } + } + } +} + +fn parse_ascii_stl(file: std::fs::File) -> Result> { + // TODO: +} + +fn parse_binary_stl(mut file: std::fs::File) -> Result> { + // skip header for now + file.seek(std::io::SeekFrom::Start(STL_HEADER_SIZE))?; + + let mut buf: [u8; 4] = [0; 4]; + file.read(&mut buf)?; + let tri_count: u32 = u32::from_le_bytes(buf); + + let mut min_aabb = glm::vec3(f32::MAX, f32::MAX, f32::MAX); + let mut max_aabb = glm::vec3(f32::MIN, f32::MIN, f32::MIN); + let mut vertices = Vec::new(); + let mut indices = Vec::new(); + + let reader = BufReader::new(file); + + let triangles_reader = TrianglesIter::new(reader, tri_count as usize); + + let tex_coords = glm::vec2(0.0, 0.0); + for (i, triangle) in triangles_reader.enumerate() { + for vert in triangle.verts { + vertices.push(Vertex{ + position: glm::vec3(vert.x, vert.y, vert.z), + normal: glm::vec3(triangle.normal.x, triangle.normal.y, triangle.normal.z), + tex_coords, + }); + + if vert.x < min_aabb.x { + min_aabb.x = vert.x; + } + + if vert.y < min_aabb.y { + min_aabb.y = vert.y; + } + + if vert.z < min_aabb.z { + min_aabb.z = vert.z; + } + + if vert.x > max_aabb.x { + max_aabb.x = vert.x; + } + + if vert.y > max_aabb.y { + max_aabb.y = vert.y; + } + + if vert.z > max_aabb.z { + max_aabb.z = vert.z; + } + } + + indices.push(i as u32*3); + indices.push(i as u32*3 + 1); + indices.push(i as u32*3 + 2); + } + + let aabb = AABB::new(min_aabb, max_aabb); + + Ok(Object{ + name: "default_object".to_string(), + meshes: vec![ObjMesh{ + name: "default_mesh".to_string(), + vertices, + indices, + material: Some(Material::default()) + }], + aabb, + }) +} + +pub fn load_stl(mut file: std::fs::File) -> Result> { + let mut buf: [u8; 11] = [0; 11]; + let _ = file.read_exact(&mut buf); + _ = file.seek(std::io::SeekFrom::Start(0)); + let is_ascii = buf == ASCII_STL_MAGIC; + + let obj = if is_ascii { + parse_ascii_stl(file)? + } else { + parse_binary_stl(file)? + }; + let now = std::time::Instant::now(); + let obj = parse_binary_stl(file)?; + let elapsed = now.elapsed(); + println!("STL file loaded in {} ms", elapsed.as_millis()); + + Ok(obj) +} diff --git a/src/utils.rs b/src/utils.rs index f2b5213..1ea4152 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,7 +6,20 @@ use anyhow::{Result, Context}; use crate::{log, model, ui, importer}; -const SUPPORTED_FILE_EXTENSIONS: [&str; 2] = ["obj", "stl"]; +pub enum SupportedFileExtensions { + OBJ, + STL +} + +impl SupportedFileExtensions { + pub fn from_str(s: &str) -> Option { + match s.to_ascii_lowercase().as_str() { + "obj" => Some(Self::OBJ), + "stl" => Some(Self::STL), + _ => None + } + } +} pub fn load_texture(path: &str) -> Result { let tex = image::io::Reader::open(path) @@ -56,9 +69,15 @@ pub fn import_models_from_paths(paths: &Vec, state: &mut ui::ui::State) state.logger.log(&format!("Skipping directory \"{}\"", filename.unwrap().to_str().unwrap()), log::LogLevel::Info); continue; } - if let Some(ext) = model_path.extension() { - if !SUPPORTED_FILE_EXTENSIONS.contains(&ext.to_ascii_lowercase().to_str().unwrap()) { - state.logger.log(&format!("Skipping file \"{}\" because it is not an OBJ or STL file", filename.unwrap().to_str().unwrap()), log::LogLevel::Info); + match model_path.extension() { + Some(ext) => { + if SupportedFileExtensions::from_str(ext.to_str().unwrap()).is_none() { + state.logger.log(&format!("Skipping file \"{}\" because it is not an OBJ or STL file", filename.unwrap().to_str().unwrap()), log::LogLevel::Info); + continue; + } + }, + None => { + state.logger.log(&format!("Skipping file \"{}\" because it has no extension", filename.unwrap().to_str().unwrap()), log::LogLevel::Info); continue; } }