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}