//! MTL file format parsing. //! //! File format documentation: use std::path::Path; use crate::color::Color; use crate::file_format::wavefront::common::{ keyword, parse_statement_line, ParsingError, Statement, }; use crate::material::{Builder as MaterialBuilder, Material}; use crate::texture::{Error as TextureError, Texture}; /// 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)] pub struct NamedMaterial { pub name: String, pub material: Material, } #[derive(Debug, Clone)] pub struct UnfinishedNamedMaterial { name: String, material_builder: MaterialBuilder, ready: bool, } #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] ParsingError(#[from] ParsingError), #[error("Failed to open texture")] TextureError(#[from] TextureError), #[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); let mut curr_material = UnfinishedNamedMaterial { name: String::new(), material_builder: MaterialBuilder::new(), ready: false, }; for (line_no, statement) in statements { if statement.keyword == Keyword::Newmtl { if curr_material.ready { tracing::debug!("Building material"); let material = curr_material.material_builder.clone().build(); materials.push(NamedMaterial { name: curr_material.name, material }); } let name = statement.get_text_arg(0, line_no)?; curr_material.name = name.to_string(); curr_material.ready = true; continue; } if !curr_material.ready { // Discard statements not belonging to a material continue; }; match statement.keyword { Keyword::Ka => { let color = get_color_from_statement(&statement, line_no)?; tracing::debug!("Adding ambient color"); curr_material.material_builder = curr_material.material_builder.ambient(color); } Keyword::Kd => { let color = get_color_from_statement(&statement, line_no)?; tracing::debug!("Adding diffuse color"); curr_material.material_builder = curr_material.material_builder.diffuse(color); } Keyword::Ks => { let color = get_color_from_statement(&statement, line_no)?; tracing::debug!("Adding specular color"); curr_material.material_builder = curr_material.material_builder.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)?; let texture = Texture::open(Path::new(texture_file_path))?; tracing::debug!("Adding ambient map"); let texture_id = texture.id(); curr_material.material_builder = curr_material .material_builder .texture(texture) .ambient_map(texture_id); } Keyword::MapKd => { let texture = get_map_from_texture(&statement, line_no)?; tracing::debug!("Adding diffuse map"); let texture_id = texture.id(); curr_material.material_builder = curr_material .material_builder .texture(texture) .diffuse_map(texture_id); } Keyword::MapKs => { let texture = get_map_from_texture(&statement, line_no)?; tracing::debug!("Adding specular map"); let texture_id = texture.id(); curr_material.material_builder = curr_material .material_builder .texture(texture) .specular_map(texture_id); } Keyword::Newmtl => {} } } if curr_material.ready { tracing::debug!("Building last material"); let material = curr_material.material_builder.build(); materials.push(NamedMaterial { name: curr_material.name, material }); } 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 }) } fn get_map_from_texture( statement: &Statement, line_no: usize, ) -> Result { 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)?; Ok(Texture::open(Path::new(texture_file_path))?) } 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, } }