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