#[cfg(feature = "genmesh")]
pub use genmesh::{Polygon, Quad, Triangle};
use std::{
collections::HashMap,
fmt,
fs::File,
io::{self, BufRead, BufReader, Error, Read, Write},
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use crate::mtl::{Material, Mtl, MtlError};
use std::io::BufWriter;
const DEFAULT_OBJECT: &str = "default";
const DEFAULT_GROUP: &str = "default";
#[derive(Copy, Clone, Debug)]
pub struct LoadConfig {
pub strict: bool,
}
impl Default for LoadConfig {
fn default() -> Self {
LoadConfig { strict: true }
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, PartialOrd, Eq, Ord)]
pub struct IndexTuple(pub usize, pub Option<usize>, pub Option<usize>);
#[derive(Debug, Clone, Hash, PartialEq)]
pub struct SimplePolygon(pub Vec<IndexTuple>);
pub trait WriteToBuf {
type Error: std::fmt::Display;
fn write_to_buf<W: Write>(&self, out: &mut W) -> Result<(), Self::Error>;
}
impl std::fmt::Display for IndexTuple {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0 + 1)?;
if let Some(idx) = self.1 {
write!(f, "/{}", idx + 1)?;
}
if let Some(idx) = self.2 {
write!(f, "/{}", idx + 1)?;
}
Ok(())
}
}
impl WriteToBuf for SimplePolygon {
type Error = ObjError;
fn write_to_buf<W: Write>(&self, out: &mut W) -> Result<(), ObjError> {
write!(out, "f")?;
for idx in &self.0 {
write!(out, " {}", idx)?;
}
writeln!(out)?;
Ok(())
}
}
#[cfg(feature = "genmesh")]
impl SimplePolygon {
pub fn into_genmesh(self) -> Polygon<IndexTuple> {
std::convert::TryFrom::try_from(self).unwrap()
}
}
#[cfg(feature = "genmesh")]
impl std::convert::TryFrom<SimplePolygon> for Polygon<IndexTuple> {
type Error = ObjError;
fn try_from(gs: SimplePolygon) -> Result<Polygon<IndexTuple>, ObjError> {
match gs.0.len() {
3 => Ok(Polygon::PolyTri(Triangle::new(gs.0[0], gs.0[1], gs.0[2]))),
4 => Ok(Polygon::PolyQuad(Quad::new(gs.0[0], gs.0[1], gs.0[2], gs.0[3]))),
n => Err(ObjError::GenMeshWrongNumberOfVertsInPolygon { vert_count: n }),
}
}
}
#[derive(Debug)]
pub enum ObjError {
Io(io::Error),
MalformedFaceGroup {
line_number: usize,
group: String,
},
ArgumentListFailure {
line_number: usize,
list: String,
},
UnexpectedCommand {
line_number: usize,
command: String,
},
MissingMTLName {
line_number: usize,
},
ZeroVertexNumber {
line_number: usize,
},
#[cfg(feature = "genmesh")]
GenMeshWrongNumberOfVertsInPolygon {
vert_count: usize,
},
}
impl std::error::Error for ObjError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ObjError::Io(err) => Some(err),
_ => None,
}
}
}
impl fmt::Display for ObjError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ObjError::Io(err) => write!(f, "I/O error loading a .obj file: {}", err),
ObjError::MalformedFaceGroup { line_number, group } => write!(
f,
"One of the arguments to `f` is malformed (line: {}, group: {})",
line_number, group
),
ObjError::ArgumentListFailure { line_number, list } => write!(
f,
"An argument list either has unparsable arguments or is missing arguments. (line: {}, list: {})",
line_number, list
),
ObjError::UnexpectedCommand { line_number, command } => write!(
f,
"Command found that is not in the .obj spec. (line: {}, command: {})",
line_number, command
),
ObjError::MissingMTLName { line_number } => write!(
f,
"mtllib command issued, but no name was specified. (line: {})",
line_number
),
ObjError::ZeroVertexNumber { line_number } => {
write!(f, "Zero vertex numbers are invalid. (line: {})", line_number)
}
#[cfg(feature = "genmesh")]
ObjError::GenMeshWrongNumberOfVertsInPolygon { vert_count } => write!(
f,
"[`genmesh::Polygon`] only supports triangles and squares. (vertex count: {}",
vert_count
),
}
}
}
impl From<io::Error> for ObjError {
fn from(e: Error) -> Self {
Self::Io(e)
}
}
#[derive(Debug)]
pub struct MtlLibsLoadError(pub Vec<(String, MtlError)>);
impl std::error::Error for MtlLibsLoadError {}
impl fmt::Display for MtlLibsLoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "One of the material libraries failed to load: {:?}", self.0)
}
}
impl From<Vec<(String, MtlError)>> for MtlLibsLoadError {
fn from(e: Vec<(String, MtlError)>) -> Self {
MtlLibsLoadError(e)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Object {
pub name: String,
pub groups: Vec<Group>,
}
impl Object {
pub fn new(name: String) -> Self {
Object {
name,
groups: Vec::new(),
}
}
}
impl WriteToBuf for Object {
type Error = ObjError;
fn write_to_buf<W: Write>(&self, out: &mut W) -> Result<(), ObjError> {
if self.name.as_str() != DEFAULT_OBJECT {
writeln!(out, "o {}", self.name)?;
}
let mut group_iter = self.groups.iter().peekable();
while let Some(group) = group_iter.next() {
group.write_to_buf(out)?;
assert!(group_iter
.peek()
.map(|next_group| next_group.index == 0 || next_group.name == group.name)
.unwrap_or(true));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ObjMaterial {
Ref(String),
Mtl(Arc<Material>),
}
impl ObjMaterial {
fn name(&self) -> &str {
match self {
ObjMaterial::Ref(name) => name.as_str(),
ObjMaterial::Mtl(material) => material.name.as_str(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Group {
pub name: String,
pub index: usize,
pub material: Option<ObjMaterial>,
pub polys: Vec<SimplePolygon>,
}
impl Group {
pub fn new(name: String) -> Self {
Group {
name,
index: 0,
material: None,
polys: Vec::new(),
}
}
}
impl WriteToBuf for Group {
type Error = ObjError;
fn write_to_buf<W: Write>(&self, out: &mut W) -> Result<(), ObjError> {
if self.index == 0 {
writeln!(out, "g {}", self.name)?;
}
match self.material {
Some(ObjMaterial::Ref(ref name)) => writeln!(out, "usemtl {}", name)?,
Some(ObjMaterial::Mtl(ref mtl)) => writeln!(out, "usemtl {}", mtl.name)?,
None => {}
}
for poly in &self.polys {
poly.write_to_buf(out)?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ObjData {
pub position: Vec<[f32; 3]>,
pub texture: Vec<[f32; 2]>,
pub normal: Vec<[f32; 3]>,
pub objects: Vec<Object>,
pub material_libs: Vec<Mtl>,
}
impl Default for ObjData {
fn default() -> Self {
ObjData {
position: Vec::new(),
texture: Vec::new(),
normal: Vec::new(),
objects: Vec::new(),
material_libs: Vec::new(),
}
}
}
#[derive(Clone, Debug)]
pub struct Obj {
pub data: ObjData,
pub path: PathBuf,
}
fn normalize(idx: isize, len: usize) -> Option<usize> {
if idx < 0 {
Some((len as isize + idx) as usize)
} else if idx > 0 {
Some(idx as usize - 1)
} else {
None
}
}
impl Obj {
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), ObjError> {
self.data.save(path.as_ref())
}
}
impl Obj {
pub fn load(path: impl AsRef<Path>) -> Result<Obj, ObjError> {
Self::load_with_config(path, LoadConfig::default())
}
pub fn load_with_config(path: impl AsRef<Path>, config: LoadConfig) -> Result<Obj, ObjError> {
Obj::load_impl(path.as_ref(), config)
}
fn load_impl(path: &Path, config: LoadConfig) -> Result<Obj, ObjError> {
let f = File::open(path)?;
let data = ObjData::load_buf_with_config(&f, config)?;
let path = path.parent().unwrap().to_owned();
Ok(Obj { data, path })
}
pub fn load_mtls(&mut self) -> Result<(), MtlLibsLoadError> {
self.load_mtls_fn(|obj_dir, mtllib| File::open(&obj_dir.join(mtllib)).map(BufReader::new))
}
pub fn load_mtls_fn<R, F>(&mut self, mut resolve: F) -> Result<(), MtlLibsLoadError>
where
R: io::BufRead,
F: FnMut(&Path, &str) -> io::Result<R>,
{
let mut errs = Vec::new();
let mut materials = HashMap::new();
for mtl_lib in &mut self.data.material_libs {
match mtl_lib.reload_with(&self.path, &mut resolve) {
Ok(mtl_lib) => {
for m in &mtl_lib.materials {
materials.entry(m.name.clone()).or_insert_with(|| Arc::clone(m));
}
}
Err(err) => {
errs.push((mtl_lib.filename.clone(), err));
}
}
}
for object in &mut self.data.objects {
for group in &mut object.groups {
if let Some(ref mut mat) = group.material {
if let Some(newmat) = materials.get(mat.name()) {
*mat = ObjMaterial::Mtl(Arc::clone(newmat));
}
}
}
}
if errs.is_empty() {
Ok(())
} else {
Err(errs.into())
}
}
}
impl ObjData {
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), ObjError> {
self.save_impl(path.as_ref())
}
fn save_impl(&self, path: &Path) -> Result<(), ObjError> {
let f = File::create(path)?;
self.write_to_buf(&mut BufWriter::new(f))?;
let path = path.parent().unwrap();
self.save_mtls(path)
}
pub fn save_mtls(&self, base_dir: impl AsRef<Path>) -> Result<(), ObjError> {
self.save_mtls_with_fn(base_dir.as_ref(), |base_dir, mtllib| {
File::create(base_dir.join(mtllib))
})
}
pub fn save_mtls_with_fn<W: Write>(
&self,
base_dir: &Path,
mut resolve: impl FnMut(&Path, &str) -> io::Result<W>,
) -> Result<(), ObjError> {
for mtl in &self.material_libs {
mtl.write_to_buf(&mut resolve(base_dir, &mtl.filename)?)?;
}
Ok(())
}
pub fn write_to_buf(&self, out: &mut impl Write) -> Result<(), ObjError> {
writeln!(
out,
"# Generated by the obj Rust library (https://crates.io/crates/obj)."
)?;
for pos in &self.position {
writeln!(out, "v {} {} {}", pos[0], pos[1], pos[2])?;
}
for uv in &self.texture {
writeln!(out, "vt {} {}", uv[0], uv[1])?;
}
for nml in &self.normal {
writeln!(out, "vn {} {} {}", nml[0], nml[1], nml[2])?;
}
for object in &self.objects {
object.write_to_buf(out)?;
}
for mtl_lib in &self.material_libs {
writeln!(out, "mtllib {}", mtl_lib.filename)?;
}
Ok(())
}
}
impl ObjData {
fn parse_two(line_number: usize, n0: Option<&str>, n1: Option<&str>) -> Result<[f32; 2], ObjError> {
let (n0, n1) = match (n0, n1) {
(Some(n0), Some(n1)) => (n0, n1),
_ => {
return Err(ObjError::ArgumentListFailure {
line_number,
list: format!("{:?} {:?}", n0, n1),
});
}
};
let normal = match (FromStr::from_str(n0), FromStr::from_str(n1)) {
(Ok(n0), Ok(n1)) => [n0, n1],
_ => {
return Err(ObjError::ArgumentListFailure {
line_number,
list: format!("{:?} {:?}", n0, n1),
});
}
};
Ok(normal)
}
fn parse_three(
line_number: usize,
n0: Option<&str>,
n1: Option<&str>,
n2: Option<&str>,
) -> Result<[f32; 3], ObjError> {
let (n0, n1, n2) = match (n0, n1, n2) {
(Some(n0), Some(n1), Some(n2)) => (n0, n1, n2),
_ => {
return Err(ObjError::ArgumentListFailure {
line_number,
list: format!("{:?} {:?} {:?}", n0, n1, n2),
});
}
};
let normal = match (FromStr::from_str(n0), FromStr::from_str(n1), FromStr::from_str(n2)) {
(Ok(n0), Ok(n1), Ok(n2)) => [n0, n1, n2],
_ => {
return Err(ObjError::ArgumentListFailure {
line_number,
list: format!("{:?} {:?} {:?}", n0, n1, n2),
});
}
};
Ok(normal)
}
fn parse_group(&self, line_number: usize, group: &str) -> Result<IndexTuple, ObjError> {
let mut group_split = group.split('/');
let p: Option<isize> = group_split.next().and_then(|idx| FromStr::from_str(idx).ok());
let t: Option<isize> = group_split
.next()
.and_then(|idx| if idx != "" { FromStr::from_str(idx).ok() } else { None });
let n: Option<isize> = group_split.next().and_then(|idx| FromStr::from_str(idx).ok());
match (p, t, n) {
(Some(p), t, n) => Ok(IndexTuple(
normalize(p, self.position.len()).ok_or(ObjError::ZeroVertexNumber { line_number })?,
t.map(|t| normalize(t, self.texture.len())).flatten(),
n.map(|n| normalize(n, self.normal.len())).flatten(),
)),
_ => Err(ObjError::MalformedFaceGroup {
line_number,
group: String::from(group),
}),
}
}
fn parse_face<'b, I>(&self, line_number: usize, groups: &mut I) -> Result<SimplePolygon, ObjError>
where
I: Iterator<Item = &'b str>,
{
let mut ret = Vec::with_capacity(4);
for g in groups {
let ituple = self.parse_group(line_number, g)?;
ret.push(ituple);
}
Ok(SimplePolygon(ret))
}
pub fn load_buf<R: Read>(input: R) -> Result<Self, ObjError> {
Self::load_buf_with_config(input, LoadConfig::default())
}
pub fn load_buf_with_config<R: Read>(input: R, config: LoadConfig) -> Result<Self, ObjError> {
let input = BufReader::new(input);
let mut dat = ObjData::default();
let mut object = Object::new(DEFAULT_OBJECT.to_string());
let mut group: Option<Group> = None;
for (idx, line) in input.lines().enumerate() {
let (line, mut words) = match line {
Ok(ref line) => (line.clone(), line.split_whitespace().filter(|s| !s.is_empty())),
Err(err) => {
return Err(ObjError::Io(io::Error::new(
io::ErrorKind::InvalidData,
format!("failed to readline {}", err),
)));
}
};
let first = words.next();
match first {
Some("v") => {
let (v0, v1, v2) = (words.next(), words.next(), words.next());
dat.position.push(Self::parse_three(idx, v0, v1, v2)?);
}
Some("vt") => {
let (t0, t1) = (words.next(), words.next());
dat.texture.push(Self::parse_two(idx, t0, t1)?);
}
Some("vn") => {
let (n0, n1, n2) = (words.next(), words.next(), words.next());
dat.normal.push(Self::parse_three(idx, n0, n1, n2)?);
}
Some("f") => {
let poly = dat.parse_face(idx, &mut words)?;
group = Some(match group {
None => {
let mut g = Group::new(DEFAULT_GROUP.to_string());
g.polys.push(poly);
g
}
Some(mut g) => {
g.polys.push(poly);
g
}
});
}
Some("o") => {
group = match group {
Some(val) => {
object.groups.push(val);
dat.objects.push(object);
None
}
None => None,
};
object = if line.len() > 2 {
let name = line[1..].trim();
Object::new(name.to_string())
} else {
Object::new(DEFAULT_OBJECT.to_string())
};
}
Some("g") => {
object.groups.extend(group.take());
if line.len() > 2 {
let name = line[2..].trim();
group = Some(Group::new(name.to_string()));
}
}
Some("mtllib") => {
let first_word = words
.next()
.ok_or_else(|| ObjError::MissingMTLName { line_number: idx })?
.to_string();
let name = words.fold(first_word, |mut existing, next| {
existing.push(' ');
existing.push_str(next);
existing
});
dat.material_libs.push(Mtl::new(name));
}
Some("usemtl") => {
let mut g = group.unwrap_or_else(|| Group::new(DEFAULT_GROUP.to_string()));
if g.material.is_some() {
object.groups.push(g.clone());
g.index += 1;
g.polys.clear();
}
g.material = words.next().map(|w| ObjMaterial::Ref(w.to_string()));
group = Some(g);
}
Some("s") => (),
Some("l") => (),
Some(other) => {
if config.strict && !other.starts_with('#') {
return Err(ObjError::UnexpectedCommand {
line_number: idx,
command: other.to_string(),
});
}
}
None => (),
}
}
if let Some(g) = group {
object.groups.push(g);
}
dat.objects.push(object);
Ok(dat)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_error_on_zero_vertex_numbers() {
let test = b"v 0 1 2\nv 3 4 5\nf 0 1 2";
let mut reader = BufReader::new(&test[..]);
assert!(matches!(
ObjData::load_buf(&mut reader),
Err(ObjError::ZeroVertexNumber { line_number: 2 })
));
}
}