nobject_rs/
lib.rs

1#![allow(clippy::needless_doctest_main)]
2//! # Overview
3//!
4//! `nobject-rs` is a library for parsing wavefront .obj and .mtl content.
5//! To this end, the crate exposes two methos:  
6//! * `load_obj`
7//! * `load_mtl`
8//!
9//! Both methods take the content of the respective files (.obj and .mtl),
10//! parse and then return a result with either some kind of parse error, or
11//! a struct containing the data.  
12//!
13//! Note that this crate leaves the responsibility of file I/O to the consuming
14//! application. For example, it's possible to specify file names as attributes
15//! in the material, or file names as material libraries in the obj file. This
16//! library will NOT attempt to open and parse those files. It is left to the
17//! consuming application/library to take the file information from the results
18//! of the parse methods, find and open the appropriate files, and then pass on
19//! the contents to be parsed.
20//!
21//! # Reference
22//!
23//! Parsing is done based on the specification for Obj's and Mtl's found at:
24//! * [Obj]( http://paulbourke.net/dataformats/obj/)
25//! * [Mtl](http://paulbourke.net/dataformats/mtl/)
26//!
27//! # Examples
28//!
29//! ## Obj parsing
30//! ```rust
31//! fn main() {
32//!     let input =
33//!     "
34//!     o 1
35//!     v -0.5 -0.5 0.5
36//!     v -0.5 -0.5 -0.5
37//!     v -0.5 0.5 -0.5
38//!     v -0.5 0.5 0.5
39//!     v 0.5 -0.5 0.5
40//!     v 0.5 -0.5 -0.5
41//!     v 0.5 0.5 -0.5
42//!     v 0.5 0.5 0.5
43//!     
44//!     usemtl Default
45//!     f 4 3 2 1
46//!     f 2 6 5 1
47//!     f 3 7 6 2
48//!     f 8 7 3 4
49//!     f 5 8 4 1
50//!     f 6 7 8 5
51//!     ";
52//!
53//!     let res = nobject_rs::load_obj(&input).unwrap();
54//!     let group = &res.groups["default"];
55//!     let face_group = &res.faces["default"];
56//!     assert_eq!(res.vertices.len(), 8);
57//!     assert_eq!(group.material_name, "Default".to_string());
58//!     assert_eq!(res.normals.len(), 0);
59//!     assert_eq!(res.faces.len(), 1);
60//!     assert_eq!(face_group.len(), 6);;
61//! }
62//! ```
63//!
64//! ## Mtl parsing
65//! ```rust
66//! fn main() {
67//!     let input =
68//!     "newmtl frost_wind
69//!     Ka 0.2 0.2 0.2
70//!     Kd 0.6 0.6 0.6
71//!     Ks 0.1 0.1 0.1
72//!     d 1
73//!     Ns 200
74//!     illum 2
75//!     map_d -mm 0.200 0.800 window.mps";
76//!
77//!     let res = nobject_rs::load_mtl(&input).unwrap();
78//!     assert_eq!(res.len(), 1);
79//! }
80//! ```
81#[macro_use]
82mod macros;
83
84#[cfg(test)]
85mod test;
86mod tokenizer;
87
88mod material;
89mod model;
90
91use std::borrow::Cow;
92use std::result::Result;
93
94pub use model::{
95    Face, FaceElement, Group, Line, LineElement, Model, ModelError, Normal, Point, Texture, Vertex,
96};
97
98pub use material::{
99    BumpMap, ColorCorrectedMap, ColorType, DisolveType, Material, MaterialError,
100    NonColorCorrectedMap, ReflectionMap,
101};
102
103use thiserror::Error;
104use tokenizer::{Token, TokenizeError};
105
106/// The set of errors which might be generated.
107#[derive(Error, Debug)]
108pub enum ObjError {
109    /// A tokenization error, typically something
110    /// in the file is not as the parser expects it.
111    #[error("Tokenize Error: `{0}`")]
112    Tokenize(#[from] TokenizeError),
113
114    /// The result of an error constructing a `Model`
115    /// from the token stream.
116    #[error("Model Error: `{0}`")]
117    ModelParse(#[from] ModelError),
118
119    /// The result of an error constructing a `Material`
120    /// collection from the token stream.
121    #[error("Material Error: `{0}`")]
122    MaterialParse(#[from] MaterialError),
123
124    /// An unexpected token was encountered in the token stream.
125    #[error("Unexpected token encountered: `{0}`")]
126    UnexpectedToken(String),
127
128    /// The specification for obj/mtl files has some settings
129    /// either being "on" or "off". If there is an issue
130    /// parsing those values, this error will occur.
131    #[error("Unexpected on/off value encountered: `{0}`")]
132    InvalidOnOffValue(String),
133}
134
135/// Takes the content of an obj file and parses it.
136///
137/// # Arguments  
138/// * input - The content of the obj file as a string
139///
140/// # Returns  
141/// Returns a `Result` of either ObjError on parse errors
142/// or a constructed `Model`.
143pub fn load_obj(input: &str) -> Result<Model, ObjError> {
144    match tokenizer::parse_obj(input) {
145        Ok(tokens) => Ok(model::parse(tokens)?),
146        Err(e) => Err(e.into()),
147    }
148}
149
150/// Takes the content of an mtl file and parses it.
151///
152/// # Arguments  
153/// * input - The content of the mtl file as a string
154///
155/// # Returns  
156/// Returns a `Result` of either ObjError on parse errors
157/// or a collection of `Material`.
158pub fn load_mtl(input: &str) -> Result<Vec<Material>, ObjError> {
159    match tokenizer::parse_mtl(input) {
160        Ok(tokens) => Ok(material::parse(tokens)?),
161        Err(e) => Err(e.into()),
162    }
163}
164
165fn get_token_float(token: &Token) -> Result<f32, ObjError> {
166    if let Token::Float(f) = token {
167        Ok(*f)
168    } else if let Token::Int(i) = token {
169        Ok(*i as f32)
170    } else {
171        Err(ObjError::UnexpectedToken(format!("{:#?}", token)))
172    }
173}
174
175fn get_opt_token_float_opt(token: &Option<Token>) -> Result<Option<f32>, ObjError> {
176    if let Some(t) = token {
177        if let Token::Float(f) = t {
178            Ok(Some(*f))
179        } else if let Token::Int(i) = t {
180            Ok(Some(*i as f32))
181        } else {
182            Err(ObjError::UnexpectedToken(format!("{:#?}", token)))
183        }
184    } else {
185        Ok(None)
186    }
187}
188
189fn get_token_int(token: &Token) -> Result<i32, ObjError> {
190    if let Token::Int(i) = token {
191        Ok(*i)
192    } else {
193        Err(ObjError::UnexpectedToken(format!("{:#?}", token)))
194    }
195}
196
197fn get_token_string<'a>(token: &'a Token) -> Result<Cow<'a, str>, ObjError> {
198    if let Token::String(s) = token {
199        Ok(s.clone())
200    } else if let Token::Int(i) = token {
201        Ok(Cow::Owned(i.to_string()))
202    } else if let Token::Float(f) = token {
203        Ok(Cow::Owned(f.to_string()))
204    } else {
205        Err(ObjError::UnexpectedToken(format!("{:#?}", token)))
206    }
207}
208
209fn get_on_off_from_str(token: &Token) -> Result<bool, ObjError> {
210    let s = get_token_string(token)?;
211    if s.eq_ignore_ascii_case("on") {
212        Ok(true)
213    } else if s.eq_ignore_ascii_case("off") {
214        Ok(false)
215    } else {
216        Err(ObjError::UnexpectedToken(format!("{:#?}", token)))
217    }
218}