obj/
obj.rs

1//   Copyright 2017 GFX Developers
2//
3//   Licensed under the Apache License, Version 2.0 (the "License");
4//   you may not use this file except in compliance with the License.
5//   You may obtain a copy of the License at
6//
7//       http://www.apache.org/licenses/LICENSE-2.0
8//
9//   Unless required by applicable law or agreed to in writing, software
10//   distributed under the License is distributed on an "AS IS" BASIS,
11//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//   See the License for the specific language governing permissions and
13//   limitations under the License.
14
15//! Parsing and writing of a .obj file as defined in the
16//! [full spec](http://paulbourke.net/dataformats/obj/).
17
18#[cfg(feature = "genmesh")]
19pub use genmesh::{Polygon, Quad, Triangle};
20
21use std::{
22    collections::HashMap,
23    fmt,
24    fs::File,
25    io::{self, BufRead, BufReader, Error, Read, Write},
26    path::{Path, PathBuf},
27    str::FromStr,
28    sync::Arc,
29};
30
31use crate::mtl::{Material, Mtl, MtlError};
32use std::io::BufWriter;
33
34const DEFAULT_OBJECT: &str = "default";
35const DEFAULT_GROUP: &str = "default";
36
37/// Load configuration options.
38#[derive(Copy, Clone, Debug)]
39pub struct LoadConfig {
40    /// Expect a strict spec-compliant `.obj` format.
41    ///
42    /// If this option is set to `true` (default), the parser will return an error when an
43    /// unrecognized `obj` command is found. Otherwise the parser will simply ignore lines starting
44    /// with unrecognized commands.
45    ///
46    /// This is useful for loading `obj` files that have been extended with third-party commands.
47    pub strict: bool,
48}
49
50impl Default for LoadConfig {
51    fn default() -> Self {
52        LoadConfig { strict: true }
53    }
54}
55
56/// A tuple of position, texture and normal indices assigned to each polygon vertex.
57///
58/// These appear as `/` separated indices in `.obj` files.
59#[derive(Debug, Clone, Copy, Hash, PartialEq, PartialOrd, Eq, Ord)]
60pub struct IndexTuple(pub usize, pub Option<usize>, pub Option<usize>);
61
62/// A a simple polygon with arbitrary many vertices.
63///
64/// Each vertex has an associated tuple of `(position, texture, normal)` indices.
65#[derive(Debug, Clone, Hash, PartialEq)]
66pub struct SimplePolygon(pub Vec<IndexTuple>);
67
68pub trait WriteToBuf {
69    type Error: std::fmt::Display;
70    fn write_to_buf<W: Write>(&self, out: &mut W) -> Result<(), Self::Error>;
71}
72
73impl std::fmt::Display for IndexTuple {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(f, "{}", self.0 + 1)?;
76        if let Some(idx) = self.1 {
77            write!(f, "/{}", idx + 1)?;
78        }
79        if let Some(idx) = self.2 {
80            write!(f, "/{}", idx + 1)?;
81        }
82        Ok(())
83    }
84}
85
86impl WriteToBuf for SimplePolygon {
87    type Error = ObjError;
88    fn write_to_buf<W: Write>(&self, out: &mut W) -> Result<(), ObjError> {
89        write!(out, "f")?;
90        for idx in &self.0 {
91            write!(out, " {}", idx)?;
92        }
93        writeln!(out)?;
94        Ok(())
95    }
96}
97
98#[cfg(feature = "genmesh")]
99impl SimplePolygon {
100    /// Convert a `SimplePolygon` into a `genmesh` `Polygon` of `IndexTuple`s.
101    ///
102    /// # Panics
103    ///
104    /// This function will panic if the polygon has more than 4 or less than 3 vertices.
105    pub fn into_genmesh(self) -> Polygon<IndexTuple> {
106        std::convert::TryFrom::try_from(self).unwrap()
107    }
108}
109
110#[cfg(feature = "genmesh")]
111impl std::convert::TryFrom<SimplePolygon> for Polygon<IndexTuple> {
112    type Error = ObjError;
113    fn try_from(gs: SimplePolygon) -> Result<Polygon<IndexTuple>, ObjError> {
114        match gs.0.len() {
115            3 => Ok(Polygon::PolyTri(Triangle::new(gs.0[0], gs.0[1], gs.0[2]))),
116            4 => Ok(Polygon::PolyQuad(Quad::new(gs.0[0], gs.0[1], gs.0[2], gs.0[3]))),
117            n => Err(ObjError::GenMeshWrongNumberOfVertsInPolygon { vert_count: n }),
118        }
119    }
120}
121
122/// Errors parsing or loading a .obj file.
123#[derive(Debug)]
124pub enum ObjError {
125    Io(io::Error),
126    /// One of the arguments to `f` is malformed.
127    MalformedFaceGroup {
128        line_number: usize,
129        group: String,
130    },
131    /// An argument list either has unparsable arguments or is
132    /// missing one or more arguments.
133    ArgumentListFailure {
134        line_number: usize,
135        list: String,
136    },
137    /// Command found that is not in the .obj spec.
138    UnexpectedCommand {
139        line_number: usize,
140        command: String,
141    },
142    /// `mtllib` command issued, but no name was specified.
143    MissingMTLName {
144        line_number: usize,
145    },
146    /// Vertices are referenced using positive 1-based indices or negative relative indices.
147    ///
148    /// Zero indices are invalid.
149    ZeroVertexNumber {
150        line_number: usize,
151    },
152    /// [`genmesh::Polygon`] only supports triangles and squares.
153    #[cfg(feature = "genmesh")]
154    GenMeshWrongNumberOfVertsInPolygon {
155        vert_count: usize,
156    },
157}
158
159impl std::error::Error for ObjError {
160    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
161        match self {
162            ObjError::Io(err) => Some(err),
163            _ => None,
164        }
165    }
166}
167
168impl fmt::Display for ObjError {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        match self {
171            ObjError::Io(err) => write!(f, "I/O error loading a .obj file: {}", err),
172            ObjError::MalformedFaceGroup { line_number, group } => write!(
173                f,
174                "One of the arguments to `f` is malformed (line: {}, group: {})",
175                line_number, group
176            ),
177            ObjError::ArgumentListFailure { line_number, list } => write!(
178                f,
179                "An argument list either has unparsable arguments or is missing arguments. (line: {}, list: {})",
180                line_number, list
181            ),
182            ObjError::UnexpectedCommand { line_number, command } => write!(
183                f,
184                "Command found that is not in the .obj spec. (line: {}, command: {})",
185                line_number, command
186            ),
187            ObjError::MissingMTLName { line_number } => write!(
188                f,
189                "mtllib command issued, but no name was specified. (line: {})",
190                line_number
191            ),
192            ObjError::ZeroVertexNumber { line_number } => {
193                write!(f, "Zero vertex numbers are invalid. (line: {})", line_number)
194            }
195            #[cfg(feature = "genmesh")]
196            ObjError::GenMeshWrongNumberOfVertsInPolygon { vert_count } => write!(
197                f,
198                "[`genmesh::Polygon`] only supports triangles and squares. (vertex count: {}",
199                vert_count
200            ),
201        }
202    }
203}
204
205impl From<io::Error> for ObjError {
206    fn from(e: Error) -> Self {
207        Self::Io(e)
208    }
209}
210
211/// Error loading individual material libraries.
212///
213/// The `Vec` items are tuples with first component being the the .mtl file, and the second its
214/// corresponding error.
215#[derive(Debug)]
216pub struct MtlLibsLoadError(pub Vec<(String, MtlError)>);
217
218impl std::error::Error for MtlLibsLoadError {}
219
220impl fmt::Display for MtlLibsLoadError {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        write!(f, "One of the material libraries failed to load: {:?}", self.0)
223    }
224}
225
226impl From<Vec<(String, MtlError)>> for MtlLibsLoadError {
227    fn from(e: Vec<(String, MtlError)>) -> Self {
228        MtlLibsLoadError(e)
229    }
230}
231
232#[derive(Debug, Clone, PartialEq)]
233pub struct Object {
234    /// Name of the object assigned by the `o ...` command in the `.obj` file.
235    pub name: String,
236    /// Groups belonging to this object.
237    pub groups: Vec<Group>,
238}
239
240impl Object {
241    pub fn new(name: String) -> Self {
242        Object {
243            name,
244            groups: Vec::new(),
245        }
246    }
247}
248
249impl WriteToBuf for Object {
250    type Error = ObjError;
251    /// Serialize this `Object` into the given writer.
252    fn write_to_buf<W: Write>(&self, out: &mut W) -> Result<(), ObjError> {
253        if self.name.as_str() != DEFAULT_OBJECT {
254            writeln!(out, "o {}", self.name)?;
255        }
256
257        let mut group_iter = self.groups.iter().peekable();
258        while let Some(group) = group_iter.next() {
259            group.write_to_buf(out)?;
260
261            // Below we check that groups with `index > 0` have the same name as their predecessors
262            // which enables us to merge the two by omitting the additional `g ...` command.
263            assert!(group_iter
264                .peek()
265                .map(|next_group| next_group.index == 0 || next_group.name == group.name)
266                .unwrap_or(true));
267        }
268
269        Ok(())
270    }
271}
272
273/// The data represented by the `usemtl` command.
274///
275/// The material name is replaced by the actual material data when the material libraries are
276/// laoded if a match is found.
277#[derive(Debug, Clone, PartialEq)]
278pub enum ObjMaterial {
279    /// A reference to a material as a material name.
280    Ref(String),
281    /// A complete `Material` object loaded from a .mtl file in place of the material reference.
282    Mtl(Arc<Material>),
283}
284
285impl ObjMaterial {
286    fn name(&self) -> &str {
287        match self {
288            ObjMaterial::Ref(name) => name.as_str(),
289            ObjMaterial::Mtl(material) => material.name.as_str(),
290        }
291    }
292}
293
294#[derive(Debug, Clone, PartialEq)]
295pub struct Group {
296    /// Name of the group assigned by the `g ...` command in the `.obj` file.
297    pub name: String,
298    /// An index is used to tell groups apart that share the same name.
299    ///
300    /// This doesn't appear explicitly in the `.obj` file, but is used here to simplify groups by
301    /// limiting them to single materials.
302    pub index: usize,
303    /// Material assigned to this group via the `usemtl ...` command in the `.obj` file.
304    ///
305    /// After material libs are loaded, this will point to the loaded `Material` struct.
306    pub material: Option<ObjMaterial>,
307    /// A list of polygons appearing as `f ...` in the `.obj` file.
308    pub polys: Vec<SimplePolygon>,
309}
310
311impl Group {
312    pub fn new(name: String) -> Self {
313        Group {
314            name,
315            index: 0,
316            material: None,
317            polys: Vec::new(),
318        }
319    }
320}
321
322impl WriteToBuf for Group {
323    type Error = ObjError;
324    /// Serialize this `Group` into the given writer.
325    fn write_to_buf<W: Write>(&self, out: &mut W) -> Result<(), ObjError> {
326        // When index is greater than 0, we know that this group is the same as the previous group,
327        // so don't bother declaring a new one.
328        if self.index == 0 {
329            writeln!(out, "g {}", self.name)?;
330        }
331
332        match self.material {
333            Some(ObjMaterial::Ref(ref name)) => writeln!(out, "usemtl {}", name)?,
334            Some(ObjMaterial::Mtl(ref mtl)) => writeln!(out, "usemtl {}", mtl.name)?,
335            None => {}
336        }
337
338        for poly in &self.polys {
339            poly.write_to_buf(out)?;
340        }
341
342        Ok(())
343    }
344}
345
346/// The data model associated with each `Obj` file.
347#[derive(Clone, Debug, PartialEq)]
348pub struct ObjData {
349    /// Vertex positions.
350    pub position: Vec<[f32; 3]>,
351    /// 2D texture coordinates.
352    pub texture: Vec<[f32; 2]>,
353    /// A set of normals.
354    pub normal: Vec<[f32; 3]>,
355    /// A collection of associated objects indicated by `o`, as well as the default object at the
356    /// top level.
357    pub objects: Vec<Object>,
358    /// The set of all `mtllib` references to .mtl files.
359    pub material_libs: Vec<Mtl>,
360}
361
362impl Default for ObjData {
363    fn default() -> Self {
364        ObjData {
365            position: Vec::new(),
366            texture: Vec::new(),
367            normal: Vec::new(),
368            objects: Vec::new(),
369            material_libs: Vec::new(),
370        }
371    }
372}
373
374/// A struct used to store `Obj` data as well as its source directory used to load the referenced
375/// .mtl files.
376#[derive(Clone, Debug)]
377pub struct Obj {
378    /// The data associated with this `Obj` file.
379    pub data: ObjData,
380    /// The path of the parent directory from which this file was read.
381    ///
382    /// It is not always set since the file may have been read from a `String`.
383    pub path: PathBuf,
384}
385
386/// Convert absolute 1-based vertex numbers or relative negative vertex numbers into 0-based index.
387///
388/// If the given index is 0, then None is returned.
389fn normalize(idx: isize, len: usize) -> Option<usize> {
390    if idx < 0 {
391        Some((len as isize + idx) as usize)
392    } else if idx > 0 {
393        Some(idx as usize - 1)
394    } else {
395        None
396    }
397}
398
399impl Obj {
400    /// Save the current `Obj` at the given file path as well as any associated .mtl files.
401    ///
402    /// If a file already exists, it will be overwritten.
403    pub fn save(&self, path: impl AsRef<Path>) -> Result<(), ObjError> {
404        self.data.save(path.as_ref())
405    }
406}
407
408impl Obj {
409    /// Load an `Obj` file from the given path with the default load configuration.
410    pub fn load(path: impl AsRef<Path>) -> Result<Obj, ObjError> {
411        Self::load_with_config(path, LoadConfig::default())
412    }
413
414    /// Load an `Obj` file from the given path using a custom load configuration.
415    pub fn load_with_config(path: impl AsRef<Path>, config: LoadConfig) -> Result<Obj, ObjError> {
416        Obj::load_impl(path.as_ref(), config)
417    }
418
419    fn load_impl(path: &Path, config: LoadConfig) -> Result<Obj, ObjError> {
420        let f = File::open(path)?;
421        let data = ObjData::load_buf_with_config(&f, config)?;
422
423        // unwrap is safe since we've read this file before.
424        let path = path.parent().unwrap().to_owned();
425
426        Ok(Obj { data, path })
427    }
428
429    /// Loads the .mtl files referenced in the .obj file.
430    ///
431    /// If it encounters an error for an .mtl, it appends its error to the
432    /// returning Vec, and tries the rest.
433    pub fn load_mtls(&mut self) -> Result<(), MtlLibsLoadError> {
434        self.load_mtls_fn(|obj_dir, mtllib| File::open(&obj_dir.join(mtllib)).map(BufReader::new))
435    }
436
437    /// Loads the .mtl files referenced in the .obj file with user provided loading logic.
438    ///
439    /// See also [`load_mtls`].
440    ///
441    /// The provided function must take two arguments:
442    ///  - `&Path` - The parent directory of the .obj file
443    ///  - `&str`  - The name of the mtllib as listed in the file.
444    ///
445    /// This function allows loading .mtl files in directories different from the default .obj
446    /// directory.
447    ///
448    /// It must return:
449    ///  - Anything that implements [`io::BufRead`] that yields the contents of the intended .mtl file.
450    ///
451    /// [`load_mtls`]: #method.load_mtls
452    /// [`io::BufRead`]: https://doc.rust-lang.org/std/io/trait.BufRead.html
453    pub fn load_mtls_fn<R, F>(&mut self, mut resolve: F) -> Result<(), MtlLibsLoadError>
454    where
455        R: io::BufRead,
456        F: FnMut(&Path, &str) -> io::Result<R>,
457    {
458        let mut errs = Vec::new();
459        let mut materials = HashMap::new();
460
461        for mtl_lib in &mut self.data.material_libs {
462            match mtl_lib.reload_with(&self.path, &mut resolve) {
463                Ok(mtl_lib) => {
464                    for m in &mtl_lib.materials {
465                        // We don't want to overwrite existing entries because of how the materials
466                        // are looked up. From the spec:
467                        // "If multiple filenames are specified, the first file
468                        //  listed is searched first for the material definition, the second
469                        //  file is searched next, and so on."
470                        materials.entry(m.name.clone()).or_insert_with(|| Arc::clone(m));
471                    }
472                }
473                Err(err) => {
474                    errs.push((mtl_lib.filename.clone(), err));
475                }
476            }
477        }
478
479        // Assign loaded materials to the corresponding objects.
480        for object in &mut self.data.objects {
481            for group in &mut object.groups {
482                if let Some(ref mut mat) = group.material {
483                    if let Some(newmat) = materials.get(mat.name()) {
484                        *mat = ObjMaterial::Mtl(Arc::clone(newmat));
485                    }
486                }
487            }
488        }
489
490        if errs.is_empty() {
491            Ok(())
492        } else {
493            Err(errs.into())
494        }
495    }
496}
497
498impl ObjData {
499    /// Save the current `ObjData` at the given file path as well as any associated .mtl files.
500    ///
501    /// If a file already exists, it will be overwritten.
502    pub fn save(&self, path: impl AsRef<Path>) -> Result<(), ObjError> {
503        self.save_impl(path.as_ref())
504    }
505
506    fn save_impl(&self, path: &Path) -> Result<(), ObjError> {
507        let f = File::create(path)?;
508        self.write_to_buf(&mut BufWriter::new(f))?;
509
510        // unwrap is safe because we created the file above.
511        let path = path.parent().unwrap();
512        self.save_mtls(path)
513    }
514
515    /// Save all material libraries referenced in this `Obj` to the given base directory.
516    pub fn save_mtls(&self, base_dir: impl AsRef<Path>) -> Result<(), ObjError> {
517        self.save_mtls_with_fn(base_dir.as_ref(), |base_dir, mtllib| {
518            File::create(base_dir.join(mtllib))
519        })
520    }
521
522    /// Save all material libraries referenced in this `Obj` struct according to `resolve`.
523    pub fn save_mtls_with_fn<W: Write>(
524        &self,
525        base_dir: &Path,
526        mut resolve: impl FnMut(&Path, &str) -> io::Result<W>,
527    ) -> Result<(), ObjError> {
528        for mtl in &self.material_libs {
529            mtl.write_to_buf(&mut resolve(base_dir, &mtl.filename)?)?;
530        }
531        Ok(())
532    }
533
534    /// Serialize this `Obj` into the given writer.
535    pub fn write_to_buf(&self, out: &mut impl Write) -> Result<(), ObjError> {
536        writeln!(
537            out,
538            "# Generated by the obj Rust library (https://crates.io/crates/obj)."
539        )?;
540
541        for pos in &self.position {
542            writeln!(out, "v {} {} {}", pos[0], pos[1], pos[2])?;
543        }
544        for uv in &self.texture {
545            writeln!(out, "vt {} {}", uv[0], uv[1])?;
546        }
547        for nml in &self.normal {
548            writeln!(out, "vn {} {} {}", nml[0], nml[1], nml[2])?;
549        }
550        for object in &self.objects {
551            object.write_to_buf(out)?;
552        }
553        for mtl_lib in &self.material_libs {
554            writeln!(out, "mtllib {}", mtl_lib.filename)?;
555        }
556
557        Ok(())
558    }
559}
560
561impl ObjData {
562    fn parse_two(line_number: usize, n0: Option<&str>, n1: Option<&str>) -> Result<[f32; 2], ObjError> {
563        let (n0, n1) = match (n0, n1) {
564            (Some(n0), Some(n1)) => (n0, n1),
565            _ => {
566                return Err(ObjError::ArgumentListFailure {
567                    line_number,
568                    list: format!("{:?} {:?}", n0, n1),
569                });
570            }
571        };
572        let normal = match (FromStr::from_str(n0), FromStr::from_str(n1)) {
573            (Ok(n0), Ok(n1)) => [n0, n1],
574            _ => {
575                return Err(ObjError::ArgumentListFailure {
576                    line_number,
577                    list: format!("{:?} {:?}", n0, n1),
578                });
579            }
580        };
581        Ok(normal)
582    }
583
584    fn parse_three(
585        line_number: usize,
586        n0: Option<&str>,
587        n1: Option<&str>,
588        n2: Option<&str>,
589    ) -> Result<[f32; 3], ObjError> {
590        let (n0, n1, n2) = match (n0, n1, n2) {
591            (Some(n0), Some(n1), Some(n2)) => (n0, n1, n2),
592            _ => {
593                return Err(ObjError::ArgumentListFailure {
594                    line_number,
595                    list: format!("{:?} {:?} {:?}", n0, n1, n2),
596                });
597            }
598        };
599        let normal = match (FromStr::from_str(n0), FromStr::from_str(n1), FromStr::from_str(n2)) {
600            (Ok(n0), Ok(n1), Ok(n2)) => [n0, n1, n2],
601            _ => {
602                return Err(ObjError::ArgumentListFailure {
603                    line_number,
604                    list: format!("{:?} {:?} {:?}", n0, n1, n2),
605                });
606            }
607        };
608        Ok(normal)
609    }
610
611    fn parse_group(&self, line_number: usize, group: &str) -> Result<IndexTuple, ObjError> {
612        let mut group_split = group.split('/');
613        let p: Option<isize> = group_split.next().and_then(|idx| FromStr::from_str(idx).ok());
614        let t: Option<isize> = group_split
615            .next()
616            .and_then(|idx| if idx != "" { FromStr::from_str(idx).ok() } else { None });
617        let n: Option<isize> = group_split.next().and_then(|idx| FromStr::from_str(idx).ok());
618
619        match (p, t, n) {
620            (Some(p), t, n) => Ok(IndexTuple(
621                normalize(p, self.position.len()).ok_or(ObjError::ZeroVertexNumber { line_number })?,
622                // Zero indices are silently ignored for tangent and normal indices.
623                t.map(|t| normalize(t, self.texture.len())).flatten(),
624                n.map(|n| normalize(n, self.normal.len())).flatten(),
625            )),
626            _ => Err(ObjError::MalformedFaceGroup {
627                line_number,
628                group: String::from(group),
629            }),
630        }
631    }
632
633    fn parse_face<'b, I>(&self, line_number: usize, groups: &mut I) -> Result<SimplePolygon, ObjError>
634    where
635        I: Iterator<Item = &'b str>,
636    {
637        let mut ret = Vec::with_capacity(4);
638        for g in groups {
639            let ituple = self.parse_group(line_number, g)?;
640            ret.push(ituple);
641        }
642        Ok(SimplePolygon(ret))
643    }
644
645    pub fn load_buf<R: Read>(input: R) -> Result<Self, ObjError> {
646        Self::load_buf_with_config(input, LoadConfig::default())
647    }
648
649    pub fn load_buf_with_config<R: Read>(input: R, config: LoadConfig) -> Result<Self, ObjError> {
650        let input = BufReader::new(input);
651        let mut dat = ObjData::default();
652        let mut object = Object::new(DEFAULT_OBJECT.to_string());
653        let mut group: Option<Group> = None;
654
655        for (idx, line) in input.lines().enumerate() {
656            let (line, mut words) = match line {
657                Ok(ref line) => (line.clone(), line.split_whitespace().filter(|s| !s.is_empty())),
658                Err(err) => {
659                    return Err(ObjError::Io(io::Error::new(
660                        io::ErrorKind::InvalidData,
661                        format!("failed to readline {}", err),
662                    )));
663                }
664            };
665            let first = words.next();
666
667            match first {
668                Some("v") => {
669                    let (v0, v1, v2) = (words.next(), words.next(), words.next());
670                    dat.position.push(Self::parse_three(idx, v0, v1, v2)?);
671                }
672                Some("vt") => {
673                    let (t0, t1) = (words.next(), words.next());
674                    dat.texture.push(Self::parse_two(idx, t0, t1)?);
675                }
676                Some("vn") => {
677                    let (n0, n1, n2) = (words.next(), words.next(), words.next());
678                    dat.normal.push(Self::parse_three(idx, n0, n1, n2)?);
679                }
680                Some("f") => {
681                    let poly = dat.parse_face(idx, &mut words)?;
682                    group = Some(match group {
683                        None => {
684                            let mut g = Group::new(DEFAULT_GROUP.to_string());
685                            g.polys.push(poly);
686                            g
687                        }
688                        Some(mut g) => {
689                            g.polys.push(poly);
690                            g
691                        }
692                    });
693                }
694                Some("o") => {
695                    group = match group {
696                        Some(val) => {
697                            object.groups.push(val);
698                            dat.objects.push(object);
699                            None
700                        }
701                        None => None,
702                    };
703                    object = if line.len() > 2 {
704                        let name = line[1..].trim();
705                        Object::new(name.to_string())
706                    } else {
707                        Object::new(DEFAULT_OBJECT.to_string())
708                    };
709                }
710                Some("g") => {
711                    object.groups.extend(group.take());
712
713                    if line.len() > 2 {
714                        let name = line[2..].trim();
715                        group = Some(Group::new(name.to_string()));
716                    }
717                }
718                Some("mtllib") => {
719                    // Obj strictly does not allow spaces in filenames.
720                    // "mtllib Some File.mtl" is forbidden.
721                    // However, everyone does it anyway and if we want to ingest blender-outputted files, we need to support it.
722                    // This works by walking word by word and combining them with a space in between. This may not be a totally
723                    // accurate way to do it, but until the parser can be re-worked, this is good-enough, better-than-before solution.
724                    let first_word = words
725                        .next()
726                        .ok_or_else(|| ObjError::MissingMTLName { line_number: idx })?
727                        .to_string();
728                    let name = words.fold(first_word, |mut existing, next| {
729                        existing.push(' ');
730                        existing.push_str(next);
731                        existing
732                    });
733                    dat.material_libs.push(Mtl::new(name));
734                }
735                Some("usemtl") => {
736                    let mut g = group.unwrap_or_else(|| Group::new(DEFAULT_GROUP.to_string()));
737                    // we found a new material that was applied to an existing
738                    // object. It is treated as a new group.
739                    if g.material.is_some() {
740                        object.groups.push(g.clone());
741                        g.index += 1;
742                        g.polys.clear();
743                    }
744                    g.material = words.next().map(|w| ObjMaterial::Ref(w.to_string()));
745                    group = Some(g);
746                }
747                Some("s") => (),
748                Some("l") => (),
749                Some(other) => {
750                    if config.strict && !other.starts_with('#') {
751                        return Err(ObjError::UnexpectedCommand {
752                            line_number: idx,
753                            command: other.to_string(),
754                        });
755                    }
756                }
757                None => (),
758            }
759        }
760
761        if let Some(g) = group {
762            object.groups.push(g);
763        }
764
765        dat.objects.push(object);
766        Ok(dat)
767    }
768}
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773
774    // Test that given zero vertex numbers, the loader throws an error instead of crashing.
775    #[test]
776    fn load_error_on_zero_vertex_numbers() {
777        let test = b"v 0 1 2\nv 3 4 5\nf 0 1 2";
778        let mut reader = BufReader::new(&test[..]);
779        assert!(matches!(
780            ObjData::load_buf(&mut reader),
781            Err(ObjError::ZeroVertexNumber { line_number: 2 })
782        ));
783    }
784}