From 9530d22cf5369ceba369487fff1b85376da64657 Mon Sep 17 00:00:00 2001 From: HampusM Date: Thu, 25 Apr 2024 20:48:39 +0200 Subject: feat(engine): add basic Wavefront obj file parsing --- engine/src/file_format/wavefront/common.rs | 391 +++++++++++++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 engine/src/file_format/wavefront/common.rs (limited to 'engine/src/file_format/wavefront/common.rs') diff --git a/engine/src/file_format/wavefront/common.rs b/engine/src/file_format/wavefront/common.rs new file mode 100644 index 0000000..0c5ac33 --- /dev/null +++ b/engine/src/file_format/wavefront/common.rs @@ -0,0 +1,391 @@ +use std::num::{ParseFloatError, ParseIntError}; + +macro_rules! keyword { + ( + $(#[$attr: meta])* + $visibility: vis enum $name: ident + { + $( + $(#[keyword(rename = $variant_rename: literal)])? + $variant: ident, + )* + } + ) => { + $(#[$attr])* + $visibility enum $name + { + $($variant,)* + } + + impl $crate::file_format::wavefront::common::Keyword for $name + { + fn from_str( + keyword: &str, + line_no: usize + ) -> Result + { + use $crate::file_format::wavefront::common::ParsingError; + + match keyword { + $( + $crate::util::or!( + ($($variant_rename)?) + else (stringify!($variant)) + ) => Ok(Keyword::$variant),)* + _ => Err(ParsingError::UnknownKeyword { + keyword: keyword.to_string(), + line_no, + }), + } + } + } + + impl std::fmt::Display for $name + { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + match self { + $(Self::$variant => write!(formatter, stringify!($variant)),)* + } + } + } + }; +} + +pub(crate) use keyword; + +pub fn parse_statement_line( + line: &str, + line_no: usize, +) -> Result>, ParsingError> +where + KeywordT: Keyword, +{ + if line.is_empty() || line.starts_with('#') { + return Ok(None); + } + + let mut parts = line.split(' '); + let keyword = KeywordT::from_str(parts.next().unwrap(), line_no)?; + + let arguments = parts + .map(|part| Value::parse::(part, line_no)) + .collect::, _>>()?; + + Ok(Some(Statement { keyword, arguments })) +} + +#[derive(Debug, thiserror::Error)] +pub enum ParsingError +{ + #[error("Unknown keyword '{keyword}' at line {line_no}")] + UnknownKeyword + { + keyword: String, line_no: usize + }, + + #[error("Invalid float value '{value}'")] + InvalidFloatValue + { + err: ParseFloatError, value: String + }, + + #[error("Invalid integer value '{value}' at line {line_no}")] + InvalidIntegerValue + { + err: ParseIntError, + value: String, + line_no: usize, + }, + + #[error("Invalid type of value '{0}'")] + UnknownValueType(String), + + #[error("Missing triplet value at line {line_no}")] + MissingTripletValue + { + triplet_index: usize, + line_no: usize, + }, + + #[error("Missing argument to {keyword} at {line_no}")] + MissingArgument + { + keyword: String, line_no: usize + }, + + #[error("Unexpected type of argument {argument_index} at line {line_no}")] + UnexpectedArgumentType + { + argument_index: usize, + line_no: usize, + }, +} + +pub trait Keyword: ToString + Sized +{ + fn from_str(keyword: &str, line_no: usize) -> Result; +} + +type TripletNumber = u32; + +#[derive(Debug, Clone)] +pub struct Triplet( + pub TripletNumber, + pub Option, + pub Option, +); + +impl Triplet +{ + fn parse( + triplet: &str, + line_no: usize, + ) -> Result + { + let mut parts = triplet.splitn(3, '/'); + + let first = parts + .next() + .ok_or(ParsingError::MissingTripletValue { triplet_index: 0, line_no }) + .and_then(|value| { + value.parse::().map_err(|err| { + ParsingError::InvalidIntegerValue { + err, + value: value.to_string(), + line_no, + } + }) + })?; + + let second = parts + .next() + .ok_or(ParsingError::MissingTripletValue { triplet_index: 0, line_no }) + .and_then(|value| { + if value.is_empty() { + return Ok(None); + } + + Ok(Some(value.parse::().map_err(|err| { + ParsingError::InvalidIntegerValue { + err, + value: value.to_string(), + line_no, + } + })?)) + })?; + + let third = parts + .next() + .ok_or(ParsingError::MissingTripletValue { triplet_index: 2, line_no }) + .and_then(|value| { + if value.is_empty() { + return Ok(None); + } + + Ok(Some(value.parse::().map_err(|err| { + ParsingError::InvalidIntegerValue { + err, + value: value.to_string(), + line_no, + } + })?)) + })?; + + Ok(Self(first, second, third)) + } +} + +#[derive(Debug)] +pub enum Value +{ + Integer(i32), + Float(f32), + Triplet(Triplet), + Text(String), +} + +impl Value +{ + fn parse(value: &str, line_no: usize) + -> Result + { + if value.contains(|character: char| character.is_ascii_digit()) + && value.contains('.') + { + return Ok(Self::Float(value.parse().map_err(|err| { + ParsingError::InvalidFloatValue { err, value: value.to_string() } + })?)); + } + + if value.contains('/') { + return Ok(Self::Triplet(Triplet::parse::(value, line_no)?)); + } + + if value.contains(|character: char| character.is_ascii_digit()) { + return Ok(Self::Integer(value.parse().map_err(|err| { + ParsingError::InvalidIntegerValue { + err, + value: value.to_string(), + line_no, + } + })?)); + } + + if value.chars().all(|character| character.is_ascii()) { + return Ok(Self::Text(value.to_string())); + } + + Err(ParsingError::UnknownValueType(value.to_string())) + } +} + +#[derive(Debug)] +pub struct Statement +{ + pub keyword: KeywordT, + pub arguments: Vec, +} + +impl Statement +{ + pub fn get_float_arg(&self, index: usize, line_no: usize) + -> Result + { + match self.arguments.get(index) { + Some(Value::Float(value)) => Ok(*value), + Some(_) => Err(ParsingError::UnexpectedArgumentType { + argument_index: index, + line_no, + }), + None => Err(ParsingError::MissingArgument { + keyword: self.keyword.to_string(), + line_no, + }), + } + } + + pub fn get_triplet_arg( + &self, + index: usize, + line_no: usize, + ) -> Result + { + match self.arguments.get(index) { + Some(Value::Triplet(value)) => Ok(value.clone()), + Some(_) => Err(ParsingError::UnexpectedArgumentType { + argument_index: index, + line_no, + }), + None => Err(ParsingError::MissingArgument { + keyword: self.keyword.to_string(), + line_no, + }), + } + } +} + +#[cfg(test)] +mod tests +{ + use std::fmt::Display; + + use super::{parse_statement_line, Value}; + use crate::file_format::wavefront::common::ParsingError; + + #[derive(Debug, PartialEq, Eq)] + enum Keyword + { + V, + Vn, + F, + } + + impl super::Keyword for Keyword + { + fn from_str(keyword: &str, _line_no: usize) -> Result + { + Ok(match keyword { + "v" => Self::V, + "vn" => Self::Vn, + "f" => Self::F, + _ => { + panic!("Unknown keyword '{keyword}'"); + } + }) + } + } + + impl Display for Keyword + { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + match self { + Self::V => write!(formatter, "v"), + Self::Vn => write!(formatter, "vn"), + Self::F => write!(formatter, "f"), + } + } + } + + #[test] + fn parse_statement_line_works() + { + assert!(parse_statement_line::("", 1) + .is_ok_and(|statement| statement.is_none())); + + let res = parse_statement_line::("v", 3); + + assert!(matches!(res, Ok(Some(statement)) if statement.keyword == Keyword::V)); + } + + #[test] + fn parse_statement_line_with_args_works() + { + let statement = parse_statement_line::("vn 45 86 11", 3) + .expect("Expected Ok") + .expect("Expected Some"); + + assert_eq!(statement.keyword, Keyword::Vn); + + assert!(matches!(statement.arguments[0], Value::Integer(45))); + assert!(matches!(statement.arguments[1], Value::Integer(86))); + assert!(matches!(statement.arguments[2], Value::Integer(11))); + } + + #[test] + fn parse_statement_line_with_triplet_arg() + { + let statement = parse_statement_line::("f 67/925/23", 1) + .expect("Expected Ok") + .expect("Expected Some"); + + assert_eq!(statement.keyword, Keyword::F); + + let Value::Triplet(triplet) = &statement.arguments[0] else { + panic!("Argument is not a triplet"); + }; + + assert_eq!(triplet.0, 67); + assert_eq!(triplet.1, Some(925)); + assert_eq!(triplet.2, Some(23)); + } + + #[test] + fn parse_statement_line_with_two_value_triplet_arg() + { + let statement = parse_statement_line::("v 102//336", 1) + .expect("Expected Ok") + .expect("Expected Some"); + + assert_eq!(statement.keyword, Keyword::V); + + let Value::Triplet(triplet) = &statement.arguments[0] else { + panic!("Argument is not a triplet"); + }; + + assert_eq!(triplet.0, 102); + assert!(triplet.1.is_none()); + assert_eq!(triplet.2, Some(336)); + } +} -- cgit v1.2.3-18-g5258