//! MTL file format parsing. //! //! File format documentation: use std::path::{Path, PathBuf}; use crate::color::Color; use crate::file_format::wavefront::common::{ keyword, parse_statement_line, ParsingError, Statement, }; /// Parses the content of a Wavefront `.mtl`. /// /// # Errors /// Will return `Err` if the `.mtl` content is formatted incorrectly. pub fn parse(obj_content: &str) -> Result, Error> { let lines = obj_content .lines() .enumerate() .map(|(line_index, line)| (line_index + 1, line)); let statements = lines .map(|(line_no, line)| (line_no, parse_statement_line::(line, line_no))) .filter_map(|(line_no, result)| { let opt_statement = match result { Ok(opt_statement) => opt_statement, Err(err) => { return Some(Err(err)); } }; Some(Ok((line_no, opt_statement?))) }) .collect::, _>>()?; let material_cnt = statements .iter() .filter(|(_, statement)| matches!(statement.keyword, Keyword::Newmtl)) .count(); tracing::debug!("Material count: {material_cnt}"); statements_to_materials(statements, material_cnt) } #[derive(Debug, Clone)] #[non_exhaustive] pub struct NamedMaterial { pub name: String, pub ambient: Color, pub diffuse: Color, pub specular: Color, pub ambient_map: Option, pub diffuse_map: Option, pub specular_map: Option, pub shininess: f32, } impl Default for NamedMaterial { fn default() -> Self { Self { name: String::new(), ambient: Color::WHITE_F32, diffuse: Color::WHITE_F32, specular: Color::WHITE_F32, ambient_map: None, diffuse_map: None, specular_map: None, shininess: 0.0, } } } #[derive(Debug, Clone)] #[non_exhaustive] pub struct TextureMap { pub path: PathBuf, } #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] ParsingError(#[from] ParsingError), #[error( "A material start statement (newmtl) is expected before statement at line {}", line_no )] ExpectedMaterialStartStmtBeforeStmt { line_no: usize }, #[error( "Unsupported number of arguments ({arg_count}) to {keyword} at line {line_no}" )] UnsupportedArgumentCount { keyword: String, arg_count: usize, line_no: usize, }, #[error("Invalid number of arguments ({arg_count}) to {keyword} at line {line_no}")] InvalidArgumentCount { keyword: String, arg_count: usize, line_no: usize, }, } #[tracing::instrument(skip_all)] fn statements_to_materials( statements: impl IntoIterator)>, material_cnt: usize, ) -> Result, Error> { let mut materials = Vec::::with_capacity(material_cnt); for (line_no, statement) in statements { if statement.keyword == Keyword::Newmtl { let name = statement.get_text_arg(0, line_no)?; materials.push(NamedMaterial { name: name.to_string(), ..Default::default() }); continue; } let Some(curr_material) = materials.last_mut() else { return Err(Error::ExpectedMaterialStartStmtBeforeStmt { line_no }); }; match statement.keyword { Keyword::Ka => { let color = get_color_from_statement(&statement, line_no)?; tracing::debug!( "Adding ambient color {color:?} to material {}", curr_material.name ); curr_material.ambient = color; } Keyword::Kd => { let color = get_color_from_statement(&statement, line_no)?; tracing::debug!( "Adding diffuse color {color:?} to material {}", curr_material.name ); curr_material.diffuse = color; } Keyword::Ks => { let color = get_color_from_statement(&statement, line_no)?; tracing::debug!( "Adding specular color {color:?} to material {}", curr_material.name ); curr_material.specular = color; } Keyword::MapKa => { if statement.arguments.len() > 1 { return Err(Error::UnsupportedArgumentCount { keyword: statement.keyword.to_string(), arg_count: statement.arguments.len(), line_no, }); } let texture_file_path = statement.get_text_arg(0, line_no)?; tracing::debug!( "Adding ambient map {texture_file_path} to material {}", curr_material.name ); curr_material.ambient_map = Some(TextureMap { path: Path::new(texture_file_path).to_path_buf(), }); } Keyword::MapKd => { if statement.arguments.len() > 1 { return Err(Error::UnsupportedArgumentCount { keyword: statement.keyword.to_string(), arg_count: statement.arguments.len(), line_no, }); } let texture_file_path = statement.get_text_arg(0, line_no)?; tracing::debug!( "Adding diffuse map {texture_file_path} to material {}", curr_material.name ); curr_material.diffuse_map = Some(TextureMap { path: Path::new(texture_file_path).to_path_buf(), }); } Keyword::MapKs => { if statement.arguments.len() > 1 { return Err(Error::UnsupportedArgumentCount { keyword: statement.keyword.to_string(), arg_count: statement.arguments.len(), line_no, }); } let texture_file_path = statement.get_text_arg(0, line_no)?; tracing::debug!( "Adding specular map {texture_file_path} to material {}", curr_material.name ); curr_material.specular_map = Some(TextureMap { path: Path::new(texture_file_path).to_path_buf(), }); } Keyword::Ns => { if statement.arguments.len() != 1 { return Err(Error::UnsupportedArgumentCount { keyword: statement.keyword.to_string(), arg_count: statement.arguments.len(), line_no, }); } let shininess = statement.get_float_arg(0, line_no)?; tracing::debug!( "Adding shininess {shininess} to material {}", curr_material.name ); curr_material.shininess = shininess; } Keyword::Newmtl => {} } } Ok(materials) } fn get_color_from_statement( statement: &Statement, line_no: usize, ) -> Result, Error> { if statement.arguments.len() > 3 { return Err(Error::InvalidArgumentCount { keyword: statement.keyword.to_string(), arg_count: statement.arguments.len(), line_no, }); } let red = statement.get_float_arg(0, line_no)?; let green = statement.get_float_arg(1, line_no)?; let blue = statement.get_float_arg(2, line_no)?; Ok(Color { red, green, blue }) } keyword! { #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum Keyword { #[keyword(rename = "newmtl")] Newmtl, Ka, Kd, Ks, #[keyword(rename = "map_Ka")] MapKa, #[keyword(rename = "map_Kd")] MapKd, #[keyword(rename = "map_Ks")] MapKs, Ns, } }