Skip to main content

wavefront_obj_io/
lib.rs

1//! Streaming, callback-based Wavefront OBJ and MTL reader and writer with
2//! matched read/write traits.
3//!
4//! # Why
5//!
6//! Most OBJ crates on crates.io eagerly load a file into a `Mesh` struct.
7//! That works great when the input fits in memory and you don't care about
8//! preserving exact byte layout. This crate fills the opposite niche:
9//!
10//! - **Streaming.** [`read_obj_file`] walks the file once and dispatches to
11//!   trait callbacks - no intermediate allocation per element. You can
12//!   process arbitrarily large meshes by writing your own [`ObjReader`]
13//!   that pushes data straight into the buffer of your choice.
14//! - **Round-trip fidelity.** [`ObjReader`] and [`ObjWriter`] are matched
15//!   trait pairs: every directive a reader can produce, a writer can emit.
16//!   That makes byte-equal round trips of OBJ files trivial.
17//! - **Configurable float precision.** Read and write `f32` or `f64` via
18//!   the [`ObjFloat`] generic parameter.
19//! - **Strict-by-default with explicit opt-in for lenient parsing.**
20//!   [`ObjReader::read_unknown`] returns an error by default; override it
21//!   to silently skip directives you don't care about (e.g. NURBS).
22//!
23//! If what you want is `let mesh = obj::load(path)?`, use
24//! [`tobj`](https://crates.io/crates/tobj) instead - that is the right tool
25//! for that job.
26//!
27//! # Supported directives
28//!
29//! Core geometry: `v`, `vt`, `vn`, `f`, `o`, `#` comments.
30//!
31//! Standard auxiliary directives have first-class trait methods on both
32//! [`ObjReader`] and [`ObjWriter`]: `mtllib`, `usemtl`, `g`, `s` (with
33//! [`SmoothingGroup`]), `l`, `p`.
34//!
35//! Anything else (NURBS / free-form geometry, display attributes, vendor
36//! extensions) routes to [`ObjReader::read_unknown`] - reject or ignore as
37//! you see fit.
38//!
39//! MTL (material library) files are handled by the matched
40//! [`MtlReader`] / [`MtlWriter`] pair via [`read_mtl_file`]. Supported
41//! directives: `newmtl`, `Ka`, `Kd`, `Ks`, `Ke`, `Ns`, `Ni`, `d`, `Tr`,
42//! `illum`, and the texture-map family (`map_Ka`, `map_Kd`, `map_Ks`,
43//! `map_Ke`, `map_Ns`, `map_d`, `bump` / `map_bump`, `disp`, `decal`,
44//! `refl`). Map options (`-bm`, `-clamp`, `-o`, `-s`, ...) are passed
45//! through verbatim to keep round trips byte-equal.
46//!
47//! # Quick start
48//!
49//! ```
50//! use wavefront_obj_io::{ObjReader, read_obj_file};
51//! use std::io::Cursor;
52//!
53//! #[derive(Default)]
54//! struct CountVertices(usize);
55//!
56//! impl ObjReader<f32> for CountVertices {
57//!     fn read_comment(&mut self, _: &str) {}
58//!     fn read_object_name(&mut self, _: &str) {}
59//!     fn read_vertex(&mut self, _: f32, _: f32, _: f32, _: Option<f32>) {
60//!         self.0 += 1;
61//!     }
62//!     fn read_texture_coordinate(&mut self, _: f32, _: Option<f32>, _: Option<f32>) {}
63//!     fn read_normal(&mut self, _: f32, _: f32, _: f32) {}
64//!     fn read_face(&mut self, _: &[(usize, Option<usize>, Option<usize>)]) {}
65//! }
66//!
67//! let obj = "v 0 0 0\nv 1 0 0\nv 0 1 0\n";
68//! let mut counter = CountVertices::default();
69//! read_obj_file(Cursor::new(obj), &mut counter).unwrap();
70//! assert_eq!(counter.0, 3);
71//! ```
72//!
73//! Indices follow the Wavefront convention and are kept 1-based throughout
74//! the API.
75
76use std::error::Error as StdError;
77use std::fmt;
78use std::fmt::Display;
79use std::io;
80use std::io::{BufRead, BufReader};
81use std::str::FromStr;
82
83/// Error type returned by [`read_obj_file`].
84///
85/// `Io` wraps an underlying [`io::Error`] from the source. `Parse` carries a
86/// structured description of an OBJ syntax problem at a specific line.
87#[derive(Debug)]
88pub enum ObjError {
89    Io(io::Error),
90    Parse { line: usize, kind: ParseErrorKind },
91}
92
93/// Structured description of an OBJ syntax problem.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum ParseErrorKind {
96    /// A line had no directive prefix after trimming whitespace.
97    EmptyPrefix,
98    /// The line started with a directive the parser does not recognize and
99    /// the [`ObjReader::read_unknown`] callback rejected it.
100    UnknownPrefix(String),
101    /// A directive was missing a required field.
102    MissingField(&'static str),
103    /// A numeric value could not be parsed as a float.
104    InvalidNumber { field: &'static str, value: String },
105    /// A face / line / point index was zero, negative, or non-numeric.
106    InvalidIndex { kind: &'static str, value: String },
107    /// `s <value>` where the value was neither `off` nor a non-negative integer.
108    InvalidSmoothingGroup(String),
109    /// `l` element with fewer than 2 vertices.
110    LineElementTooShort,
111    /// `p` element with no vertices.
112    PointElementEmpty,
113    /// Free-form message, e.g. from a custom [`ObjReader::read_unknown`].
114    Custom(String),
115}
116
117impl Display for ObjError {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            ObjError::Io(e) => write!(f, "I/O error: {e}"),
121            ObjError::Parse { line, kind } => write!(f, "line {line}: {kind}"),
122        }
123    }
124}
125
126impl Display for ParseErrorKind {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self {
129            ParseErrorKind::EmptyPrefix => write!(f, "empty prefix"),
130            ParseErrorKind::UnknownPrefix(p) => write!(f, "Unknown line prefix: {p}"),
131            ParseErrorKind::MissingField(field) => write!(f, "missing {field}"),
132            ParseErrorKind::InvalidNumber { field, value } => {
133                write!(f, "invalid {field}: {value}")
134            }
135            ParseErrorKind::InvalidIndex { kind, value } => {
136                write!(f, "invalid {kind} index: {value}")
137            }
138            ParseErrorKind::InvalidSmoothingGroup(v) => {
139                write!(
140                    f,
141                    "invalid smoothing group: {v} (expected integer or 'off')"
142                )
143            }
144            ParseErrorKind::LineElementTooShort => {
145                write!(f, "line element needs at least 2 vertices")
146            }
147            ParseErrorKind::PointElementEmpty => {
148                write!(f, "point element needs at least 1 vertex")
149            }
150            ParseErrorKind::Custom(s) => write!(f, "{s}"),
151        }
152    }
153}
154
155impl StdError for ObjError {
156    fn source(&self) -> Option<&(dyn StdError + 'static)> {
157        match self {
158            ObjError::Io(e) => Some(e),
159            ObjError::Parse { .. } => None,
160        }
161    }
162}
163
164impl From<io::Error> for ObjError {
165    fn from(e: io::Error) -> Self {
166        ObjError::Io(e)
167    }
168}
169
170impl From<ObjError> for io::Error {
171    fn from(e: ObjError) -> Self {
172        match e {
173            ObjError::Io(inner) => inner,
174            parse @ ObjError::Parse { .. } => {
175                io::Error::new(io::ErrorKind::InvalidData, parse.to_string())
176            }
177        }
178    }
179}
180
181/// Trait for floating point types that can be used in OBJ files.
182/// This allows the library to work with both f32 and f64 precision.
183pub trait ObjFloat: Copy + Display + FromStr + PartialEq {
184    /// Returns the fractional part of the number
185    fn fract(self) -> Self;
186
187    /// Returns true if the fractional part is zero
188    fn is_zero_fract(self) -> bool {
189        self.fract() == Self::zero()
190    }
191
192    /// Returns the zero value for this type
193    fn zero() -> Self;
194}
195
196impl ObjFloat for f32 {
197    fn fract(self) -> Self {
198        self.fract()
199    }
200    fn zero() -> Self {
201        0.0
202    }
203}
204
205impl ObjFloat for f64 {
206    fn fract(self) -> Self {
207        self.fract()
208    }
209    fn zero() -> Self {
210        0.0
211    }
212}
213
214/// Smoothing group selector for the `s` directive.
215///
216/// `s 0` and `s off` both disable smoothing; any positive integer names a
217/// smoothing group.
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum SmoothingGroup {
220    Off,
221    Group(u32),
222}
223
224/// Trait for writing OBJ file data with configurable float precision.
225///
226/// The generic parameter `F` defaults to `f64` for backward compatibility,
227/// but can be set to `f32` for applications that work with single-precision data.
228pub trait ObjWriter<F: ObjFloat = f64> {
229    fn write_comment<S: AsRef<str>>(&mut self, comment: S) -> io::Result<()>;
230    fn write_object_name<S: AsRef<str>>(&mut self, name: S) -> io::Result<()>;
231    fn write_vertex(&mut self, x: F, y: F, z: F, w: Option<F>) -> io::Result<()>;
232    fn write_texture_coordinate(&mut self, u: F, v: Option<F>, w: Option<F>) -> io::Result<()>;
233    fn write_normal(&mut self, nx: F, ny: F, nz: F) -> io::Result<()>;
234    fn write_face(
235        &mut self,
236        vertex_indices: &[(usize, Option<usize>, Option<usize>)],
237    ) -> io::Result<()>;
238
239    /// `mtllib lib1.mtl lib2.mtl ...` - reference one or more material libraries.
240    fn write_material_lib<S: AsRef<str>>(&mut self, names: &[S]) -> io::Result<()>;
241
242    /// `usemtl name` - select a material from a previously declared library.
243    fn write_use_material<S: AsRef<str>>(&mut self, name: S) -> io::Result<()>;
244
245    /// `g name1 name2 ...` - assign subsequent elements to one or more groups.
246    fn write_group<S: AsRef<str>>(&mut self, names: &[S]) -> io::Result<()>;
247
248    /// `s 0|off|<n>` - select a smoothing group for subsequent faces.
249    fn write_smoothing_group(&mut self, group: SmoothingGroup) -> io::Result<()>;
250
251    /// `l v1[/vt1] v2[/vt2] ...` - polyline element. Each index is a vertex,
252    /// optionally with a texture coordinate.
253    fn write_line_element(&mut self, indices: &[(usize, Option<usize>)]) -> io::Result<()>;
254
255    /// `p v1 v2 ...` - point element.
256    fn write_point_element(&mut self, indices: &[usize]) -> io::Result<()>;
257}
258
259/// Trait for reading OBJ file data with configurable float precision.
260///
261/// The generic parameter `F` defaults to `f64` for backward compatibility,
262/// but can be set to `f32` for applications that work with single-precision data.
263pub trait ObjReader<F: ObjFloat = f64> {
264    fn read_comment(&mut self, comment: &str) -> ();
265    fn read_object_name(&mut self, name: &str) -> ();
266    fn read_vertex(&mut self, x: F, y: F, z: F, w: Option<F>) -> ();
267    fn read_texture_coordinate(&mut self, u: F, v: Option<F>, w: Option<F>) -> ();
268    fn read_normal(&mut self, nx: F, ny: F, nz: F) -> ();
269    fn read_face(&mut self, vertex_indices: &[(usize, Option<usize>, Option<usize>)]) -> ();
270
271    /// `mtllib lib1.mtl lib2.mtl ...` - default no-op.
272    fn read_material_lib(&mut self, _names: &[&str]) {}
273
274    /// `usemtl name` - default no-op.
275    fn read_use_material(&mut self, _name: &str) {}
276
277    /// `g name1 name2 ...` - default no-op.
278    fn read_group(&mut self, _names: &[&str]) {}
279
280    /// `s 0|off|<n>` - default no-op.
281    fn read_smoothing_group(&mut self, _group: SmoothingGroup) {}
282
283    /// `l v1[/vt1] v2[/vt2] ...` - default no-op.
284    fn read_line_element(&mut self, _indices: &[(usize, Option<usize>)]) {}
285
286    /// `p v1 v2 ...` - default no-op.
287    fn read_point_element(&mut self, _indices: &[usize]) {}
288
289    /// Called when a line with an unknown prefix is encountered.
290    ///
291    /// The default implementation returns `ParseErrorKind::UnknownPrefix`,
292    /// treating any prefix outside the supported core (NURBS / free-form
293    /// geometry, display attributes, vendor extensions) as a hard error.
294    /// Override to skip or otherwise handle these lines.
295    fn read_unknown(&mut self, prefix: &str, _rest: &str, line: usize) -> Result<(), ObjError> {
296        Err(ObjError::Parse {
297            line,
298            kind: ParseErrorKind::UnknownPrefix(prefix.to_string()),
299        })
300    }
301}
302
303pub fn read_obj_file<R: io::Read, T: ObjReader<F>, F: ObjFloat>(
304    reader: R,
305    obj_reader: &mut T,
306) -> Result<(), ObjError>
307where
308    <F as FromStr>::Err: Display,
309{
310    let mut buf_reader = BufReader::new(reader);
311    let mut line = String::new();
312    let mut lineno: usize = 0;
313
314    while buf_reader.read_line(&mut line)? != 0 {
315        lineno += 1;
316        let trimmed = line.trim();
317        if trimmed.is_empty() {
318            line.clear();
319            continue;
320        }
321        let mut parts = trimmed.split_whitespace();
322        let prefix = parts.next().ok_or(ObjError::Parse {
323            line: lineno,
324            kind: ParseErrorKind::EmptyPrefix,
325        })?;
326
327        let parse_f = |s: &str, field: &'static str| -> Result<F, ObjError> {
328            s.parse::<F>().map_err(|_| ObjError::Parse {
329                line: lineno,
330                kind: ParseErrorKind::InvalidNumber {
331                    field,
332                    value: s.to_string(),
333                },
334            })
335        };
336
337        let parse_index = |s: &str, kind: &'static str| -> Result<usize, ObjError> {
338            let index = s.parse::<usize>().map_err(|_| ObjError::Parse {
339                line: lineno,
340                kind: ParseErrorKind::InvalidIndex {
341                    kind,
342                    value: s.to_string(),
343                },
344            })?;
345            if index == 0 {
346                return Err(ObjError::Parse {
347                    line: lineno,
348                    kind: ParseErrorKind::InvalidIndex {
349                        kind,
350                        value: s.to_string(),
351                    },
352                });
353            }
354            Ok(index)
355        };
356
357        let missing = |field: &'static str| -> ObjError {
358            ObjError::Parse {
359                line: lineno,
360                kind: ParseErrorKind::MissingField(field),
361            }
362        };
363
364        match prefix {
365            "#" => {
366                let comment = parts.collect::<Vec<&str>>().join(" ");
367                obj_reader.read_comment(&comment);
368            }
369            "v" => {
370                let x = parts
371                    .next()
372                    .ok_or_else(|| missing("vertex x"))
373                    .and_then(|s| parse_f(s, "vertex x"))?;
374                let y = parts
375                    .next()
376                    .ok_or_else(|| missing("vertex y"))
377                    .and_then(|s| parse_f(s, "vertex y"))?;
378                let z = parts
379                    .next()
380                    .ok_or_else(|| missing("vertex z"))
381                    .and_then(|s| parse_f(s, "vertex z"))?;
382                let w = match parts.next() {
383                    Some(s) => Some(parse_f(s, "vertex w")?),
384                    None => None,
385                };
386                obj_reader.read_vertex(x, y, z, w);
387            }
388            "vt" => {
389                let u = parts
390                    .next()
391                    .ok_or_else(|| missing("texture u"))
392                    .and_then(|s| parse_f(s, "texture u"))?;
393                let v = match parts.next() {
394                    Some(s) => Some(parse_f(s, "texture v")?),
395                    None => None,
396                };
397                let w = match parts.next() {
398                    Some(s) => Some(parse_f(s, "texture w")?),
399                    None => None,
400                };
401                obj_reader.read_texture_coordinate(u, v, w);
402            }
403            "vn" => {
404                let nx = parts
405                    .next()
406                    .ok_or_else(|| missing("normal nx"))
407                    .and_then(|s| parse_f(s, "normal nx"))?;
408                let ny = parts
409                    .next()
410                    .ok_or_else(|| missing("normal ny"))
411                    .and_then(|s| parse_f(s, "normal ny"))?;
412                let nz = parts
413                    .next()
414                    .ok_or_else(|| missing("normal nz"))
415                    .and_then(|s| parse_f(s, "normal nz"))?;
416                obj_reader.read_normal(nx, ny, nz);
417            }
418            "f" => {
419                let mut vertex_indices = Vec::new();
420                for part in parts {
421                    // parse "v[/vt[/vn]]" by slicing without allocating
422                    let first_slash = part.find('/');
423                    let (v_str, rest) = match first_slash {
424                        Some(i) => (&part[..i], &part[i + 1..]),
425                        None => (part, ""),
426                    };
427
428                    let v_idx = parse_index(v_str, "vertex")?;
429
430                    let (vt_idx, vn_idx) = if rest.is_empty() {
431                        (None, None)
432                    } else {
433                        let second_slash = rest.find('/');
434                        if let Some(j) = second_slash {
435                            let vt_part = &rest[..j];
436                            let vn_part = &rest[j + 1..];
437                            let vt = if vt_part.is_empty() {
438                                None
439                            } else {
440                                Some(parse_index(vt_part, "texcoord")?)
441                            };
442                            let vn = if vn_part.is_empty() {
443                                None
444                            } else {
445                                Some(parse_index(vn_part, "normal")?)
446                            };
447                            (vt, vn)
448                        } else {
449                            // only vt present
450                            let vt = if rest.is_empty() {
451                                None
452                            } else {
453                                Some(parse_index(rest, "texcoord")?)
454                            };
455                            (vt, None)
456                        }
457                    };
458
459                    vertex_indices.push((v_idx, vt_idx, vn_idx));
460                }
461                obj_reader.read_face(&vertex_indices);
462            }
463            "o" => {
464                let name = parts.collect::<Vec<&str>>().join(" ");
465                obj_reader.read_object_name(&name);
466            }
467            "mtllib" => {
468                let names: Vec<&str> = parts.collect();
469                obj_reader.read_material_lib(&names);
470            }
471            "usemtl" => {
472                // Material names should not contain whitespace per spec; join
473                // anything we get just to be tolerant.
474                let name = parts.collect::<Vec<&str>>().join(" ");
475                obj_reader.read_use_material(&name);
476            }
477            "g" => {
478                let names: Vec<&str> = parts.collect();
479                obj_reader.read_group(&names);
480            }
481            "s" => {
482                let value = parts
483                    .next()
484                    .ok_or_else(|| missing("smoothing group value"))?;
485                let group = if value.eq_ignore_ascii_case("off") {
486                    SmoothingGroup::Off
487                } else {
488                    let n = value.parse::<u32>().map_err(|_| ObjError::Parse {
489                        line: lineno,
490                        kind: ParseErrorKind::InvalidSmoothingGroup(value.to_string()),
491                    })?;
492                    if n == 0 {
493                        SmoothingGroup::Off
494                    } else {
495                        SmoothingGroup::Group(n)
496                    }
497                };
498                obj_reader.read_smoothing_group(group);
499            }
500            "l" => {
501                let mut indices: Vec<(usize, Option<usize>)> = Vec::new();
502                for part in parts {
503                    let (v_str, vt_str) = match part.find('/') {
504                        Some(i) => (&part[..i], Some(&part[i + 1..])),
505                        None => (part, None),
506                    };
507                    let v_idx = parse_index(v_str, "vertex")?;
508                    let vt_idx = match vt_str {
509                        Some(s) if !s.is_empty() => Some(parse_index(s, "texcoord")?),
510                        _ => None,
511                    };
512                    indices.push((v_idx, vt_idx));
513                }
514                if indices.len() < 2 {
515                    return Err(ObjError::Parse {
516                        line: lineno,
517                        kind: ParseErrorKind::LineElementTooShort,
518                    });
519                }
520                obj_reader.read_line_element(&indices);
521            }
522            "p" => {
523                let mut indices: Vec<usize> = Vec::new();
524                for part in parts {
525                    indices.push(parse_index(part, "vertex")?);
526                }
527                if indices.is_empty() {
528                    return Err(ObjError::Parse {
529                        line: lineno,
530                        kind: ParseErrorKind::PointElementEmpty,
531                    });
532                }
533                obj_reader.read_point_element(&indices);
534            }
535            other => {
536                let rest = parts.collect::<Vec<&str>>().join(" ");
537                obj_reader.read_unknown(other, &rest, lineno)?;
538            }
539        }
540
541        line.clear();
542    }
543
544    Ok(())
545}
546
547pub struct IoObjWriter<W: io::Write, F: ObjFloat = f64> {
548    out: W,
549    line_buf: Vec<u8>,
550    /// When true, format floats with 6 decimal places (matching C `printf %f`).
551    printf_f_format: bool,
552    _phantom: std::marker::PhantomData<F>,
553}
554impl<W: io::Write, F: ObjFloat> IoObjWriter<W, F> {
555    /// Creates a new OBJ writer with default formatting (full precision).
556    pub fn new(writer: W) -> Self {
557        IoObjWriter {
558            out: writer,
559            line_buf: Vec::with_capacity(256),
560            printf_f_format: false,
561            _phantom: std::marker::PhantomData,
562        }
563    }
564
565    /// Creates a new OBJ writer that formats floats with 6 decimal places,
566    /// matching the output produced by C's `fprintf("%f", ...)`.
567    #[cfg(test)]
568    pub fn new_with_printf_f_format(writer: W) -> Self {
569        IoObjWriter {
570            out: writer,
571            line_buf: Vec::with_capacity(256),
572            printf_f_format: true,
573            _phantom: std::marker::PhantomData,
574        }
575    }
576
577    /// Toggle 6-decimal-place float formatting (`printf %f` style).
578    #[cfg(test)]
579    pub fn set_printf_f_format(&mut self, enabled: bool) {
580        self.printf_f_format = enabled;
581    }
582
583    /// Consume the writer and return the underlying sink.
584    pub fn into_inner(self) -> W {
585        self.out
586    }
587
588    #[inline]
589    fn push_str(&mut self, s: &str) {
590        self.line_buf.extend_from_slice(s.as_bytes());
591    }
592
593    #[inline]
594    fn push_u<T: itoa::Integer>(&mut self, v: T) {
595        let mut buf = itoa::Buffer::new();
596        self.push_str(buf.format(v));
597    }
598
599    #[inline]
600    fn push_f(&mut self, v: F) {
601        // we want 0 as "0" not "0.0"
602        if v.is_zero_fract() {
603            self.push_str(&format!("{}", v));
604            return;
605        }
606        // 6 decimal places when matching C `printf %f`, otherwise full
607        // round-trippable precision via the type's Display impl.
608        if self.printf_f_format {
609            self.push_str(&format!("{:.6}", v));
610        } else {
611            self.push_str(&format!("{}", v));
612        }
613    }
614
615    #[inline]
616    fn flush_line(&mut self) -> io::Result<()> {
617        self.line_buf.push(b'\n');
618        self.out.write_all(&self.line_buf)?;
619        self.line_buf.clear();
620        Ok(())
621    }
622}
623impl<W: io::Write, F: ObjFloat> ObjWriter<F> for IoObjWriter<W, F> {
624    fn write_comment<S: AsRef<str>>(&mut self, comment: S) -> io::Result<()> {
625        self.push_str("# ");
626        self.push_str(comment.as_ref());
627        self.flush_line()
628    }
629
630    fn write_object_name<S: AsRef<str>>(&mut self, name: S) -> io::Result<()> {
631        self.push_str("o ");
632        self.push_str(name.as_ref());
633        self.flush_line()
634    }
635
636    fn write_vertex(&mut self, x: F, y: F, z: F, w: Option<F>) -> io::Result<()> {
637        self.push_str("v ");
638        self.push_f(x);
639        self.push_str(" ");
640        self.push_f(y);
641        self.push_str(" ");
642        self.push_f(z);
643        if let Some(wv) = w {
644            self.push_str(" ");
645            self.push_f(wv);
646        }
647        self.flush_line()
648    }
649
650    fn write_texture_coordinate(&mut self, u: F, v: Option<F>, w: Option<F>) -> io::Result<()> {
651        self.push_str("vt ");
652        self.push_f(u);
653        if let Some(vv) = v {
654            self.push_str(" ");
655            self.push_f(vv);
656            if let Some(wv) = w {
657                self.push_str(" ");
658                self.push_f(wv);
659            }
660        }
661        self.flush_line()
662    }
663
664    fn write_normal(&mut self, nx: F, ny: F, nz: F) -> io::Result<()> {
665        self.push_str("vn ");
666        self.push_f(nx);
667        self.push_str(" ");
668        self.push_f(ny);
669        self.push_str(" ");
670        self.push_f(nz);
671        self.flush_line()
672    }
673
674    fn write_face(
675        &mut self,
676        vertex_indices: &[(usize, Option<usize>, Option<usize>)],
677    ) -> io::Result<()> {
678        // Build the whole face line and write once.
679        self.push_str("f");
680        for (v_idx, vt_idx, vn_idx) in vertex_indices.iter() {
681            self.push_str(" ");
682            // If your internal indices are zero-based, emit +1 here:
683            self.push_u(*v_idx);
684            match (vt_idx, vn_idx) {
685                (None, None) => {}
686                (Some(vt), None) => {
687                    self.push_str("/");
688                    self.push_u(*vt);
689                }
690                (None, Some(vn)) => {
691                    self.push_str("//");
692                    self.push_u(*vn);
693                }
694                (Some(vt), Some(vn)) => {
695                    self.push_str("/");
696                    self.push_u(*vt);
697                    self.push_str("/");
698                    self.push_u(*vn);
699                }
700            }
701        }
702        self.flush_line()
703    }
704
705    fn write_material_lib<S: AsRef<str>>(&mut self, names: &[S]) -> io::Result<()> {
706        self.push_str("mtllib");
707        for name in names {
708            self.push_str(" ");
709            self.push_str(name.as_ref());
710        }
711        self.flush_line()
712    }
713
714    fn write_use_material<S: AsRef<str>>(&mut self, name: S) -> io::Result<()> {
715        self.push_str("usemtl ");
716        self.push_str(name.as_ref());
717        self.flush_line()
718    }
719
720    fn write_group<S: AsRef<str>>(&mut self, names: &[S]) -> io::Result<()> {
721        self.push_str("g");
722        for name in names {
723            self.push_str(" ");
724            self.push_str(name.as_ref());
725        }
726        self.flush_line()
727    }
728
729    fn write_smoothing_group(&mut self, group: SmoothingGroup) -> io::Result<()> {
730        match group {
731            SmoothingGroup::Off => self.push_str("s off"),
732            SmoothingGroup::Group(n) => {
733                self.push_str("s ");
734                self.push_u(n);
735            }
736        }
737        self.flush_line()
738    }
739
740    fn write_line_element(&mut self, indices: &[(usize, Option<usize>)]) -> io::Result<()> {
741        self.push_str("l");
742        for (v_idx, vt_idx) in indices.iter() {
743            self.push_str(" ");
744            self.push_u(*v_idx);
745            if let Some(vt) = vt_idx {
746                self.push_str("/");
747                self.push_u(*vt);
748            }
749        }
750        self.flush_line()
751    }
752
753    fn write_point_element(&mut self, indices: &[usize]) -> io::Result<()> {
754        self.push_str("p");
755        for v_idx in indices.iter() {
756            self.push_str(" ");
757            self.push_u(*v_idx);
758        }
759        self.flush_line()
760    }
761}
762
763// ---------------------------------------------------------------------------
764// MTL (material library) support
765// ---------------------------------------------------------------------------
766
767/// Error type returned by [`read_mtl_file`].
768#[derive(Debug)]
769pub enum MtlError {
770    Io(io::Error),
771    Parse {
772        line: usize,
773        kind: MtlParseErrorKind,
774    },
775}
776
777/// Structured description of an MTL syntax problem.
778#[derive(Debug, Clone, PartialEq, Eq)]
779pub enum MtlParseErrorKind {
780    EmptyPrefix,
781    UnknownPrefix(String),
782    MissingField(&'static str),
783    InvalidNumber { field: &'static str, value: String },
784    InvalidIlluminationModel(String),
785    Custom(String),
786}
787
788impl Display for MtlError {
789    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
790        match self {
791            MtlError::Io(e) => write!(f, "I/O error: {e}"),
792            MtlError::Parse { line, kind } => write!(f, "line {line}: {kind}"),
793        }
794    }
795}
796
797impl Display for MtlParseErrorKind {
798    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
799        match self {
800            MtlParseErrorKind::EmptyPrefix => write!(f, "empty prefix"),
801            MtlParseErrorKind::UnknownPrefix(p) => write!(f, "Unknown line prefix: {p}"),
802            MtlParseErrorKind::MissingField(field) => write!(f, "missing {field}"),
803            MtlParseErrorKind::InvalidNumber { field, value } => {
804                write!(f, "invalid {field}: {value}")
805            }
806            MtlParseErrorKind::InvalidIlluminationModel(v) => {
807                write!(f, "invalid illumination model: {v}")
808            }
809            MtlParseErrorKind::Custom(s) => write!(f, "{s}"),
810        }
811    }
812}
813
814impl StdError for MtlError {
815    fn source(&self) -> Option<&(dyn StdError + 'static)> {
816        match self {
817            MtlError::Io(e) => Some(e),
818            MtlError::Parse { .. } => None,
819        }
820    }
821}
822
823impl From<io::Error> for MtlError {
824    fn from(e: io::Error) -> Self {
825        MtlError::Io(e)
826    }
827}
828
829impl From<MtlError> for io::Error {
830    fn from(e: MtlError) -> Self {
831        match e {
832            MtlError::Io(inner) => inner,
833            parse @ MtlError::Parse { .. } => {
834                io::Error::new(io::ErrorKind::InvalidData, parse.to_string())
835            }
836        }
837    }
838}
839
840/// Texture-map directive kind.
841///
842/// Variants `Bump` and `MapBump` represent the two equivalent spellings
843/// (`bump` and `map_bump`) and are kept distinct so writers can preserve
844/// the original form for byte-equal round trips.
845#[derive(Debug, Clone, Copy, PartialEq, Eq)]
846pub enum MapKind {
847    /// `map_Ka`
848    Ambient,
849    /// `map_Kd`
850    Diffuse,
851    /// `map_Ks`
852    Specular,
853    /// `map_Ke`
854    Emissive,
855    /// `map_Ns`
856    SpecularExponent,
857    /// `map_d`
858    Dissolve,
859    /// `bump`
860    Bump,
861    /// `map_bump`
862    MapBump,
863    /// `disp`
864    Displacement,
865    /// `decal`
866    Decal,
867    /// `refl`
868    Reflection,
869}
870
871impl MapKind {
872    fn from_prefix(s: &str) -> Option<Self> {
873        Some(match s {
874            "map_Ka" => MapKind::Ambient,
875            "map_Kd" => MapKind::Diffuse,
876            "map_Ks" => MapKind::Specular,
877            "map_Ke" => MapKind::Emissive,
878            "map_Ns" => MapKind::SpecularExponent,
879            "map_d" => MapKind::Dissolve,
880            "bump" => MapKind::Bump,
881            "map_bump" => MapKind::MapBump,
882            "disp" => MapKind::Displacement,
883            "decal" => MapKind::Decal,
884            "refl" => MapKind::Reflection,
885            _ => return None,
886        })
887    }
888
889    fn as_prefix(self) -> &'static str {
890        match self {
891            MapKind::Ambient => "map_Ka",
892            MapKind::Diffuse => "map_Kd",
893            MapKind::Specular => "map_Ks",
894            MapKind::Emissive => "map_Ke",
895            MapKind::SpecularExponent => "map_Ns",
896            MapKind::Dissolve => "map_d",
897            MapKind::Bump => "bump",
898            MapKind::MapBump => "map_bump",
899            MapKind::Displacement => "disp",
900            MapKind::Decal => "decal",
901            MapKind::Reflection => "refl",
902        }
903    }
904}
905
906/// Trait for reading MTL (material library) data.
907///
908/// MTL is implicitly hierarchical: every directive after `newmtl <name>`
909/// belongs to that material until the next `newmtl`. Implementations should
910/// flush any in-progress material on each [`MtlReader::read_new_material`]
911/// call and again at end-of-input.
912pub trait MtlReader<F: ObjFloat = f64> {
913    fn read_comment(&mut self, comment: &str);
914    fn read_new_material(&mut self, name: &str);
915    fn read_ambient(&mut self, r: F, g: Option<F>, b: Option<F>);
916    fn read_diffuse(&mut self, r: F, g: Option<F>, b: Option<F>);
917    fn read_specular(&mut self, r: F, g: Option<F>, b: Option<F>);
918    fn read_specular_exponent(&mut self, value: F);
919    fn read_dissolve(&mut self, value: F);
920    fn read_illumination_model(&mut self, value: u32);
921
922    /// `map_Ka|map_Kd|map_Ks|map_Ke|map_Ns|map_d|bump|map_bump|disp|decal|refl`
923    /// with the entire rest-of-line passed as `args`. Map options
924    /// (`-bm 0.5`, `-clamp on`, `-o u v w`, ...) are not parsed by this
925    /// crate; the trailing token of `args` is conventionally the filename.
926    fn read_map(&mut self, kind: MapKind, args: &str);
927
928    /// `Ke r [g b]` - default no-op.
929    fn read_emissive(&mut self, _r: F, _g: Option<F>, _b: Option<F>) {}
930    /// `Ni <value>` - default no-op.
931    fn read_optical_density(&mut self, _value: F) {}
932    /// `Tr <value>` - default no-op. Note that `Tr` and `d` express
933    /// opacity inversely (`d = 1 - Tr`); both are surfaced raw.
934    fn read_transparency(&mut self, _value: F) {}
935
936    /// Called for any line whose prefix is not recognized. Default
937    /// implementation rejects with [`MtlParseErrorKind::UnknownPrefix`].
938    fn read_unknown(&mut self, prefix: &str, _rest: &str, line: usize) -> Result<(), MtlError> {
939        Err(MtlError::Parse {
940            line,
941            kind: MtlParseErrorKind::UnknownPrefix(prefix.to_string()),
942        })
943    }
944}
945
946/// Trait for writing MTL data. Mirror of [`MtlReader`].
947pub trait MtlWriter<F: ObjFloat = f64> {
948    fn write_comment<S: AsRef<str>>(&mut self, comment: S) -> io::Result<()>;
949    fn write_new_material<S: AsRef<str>>(&mut self, name: S) -> io::Result<()>;
950    fn write_ambient(&mut self, r: F, g: Option<F>, b: Option<F>) -> io::Result<()>;
951    fn write_diffuse(&mut self, r: F, g: Option<F>, b: Option<F>) -> io::Result<()>;
952    fn write_specular(&mut self, r: F, g: Option<F>, b: Option<F>) -> io::Result<()>;
953    fn write_emissive(&mut self, r: F, g: Option<F>, b: Option<F>) -> io::Result<()>;
954    fn write_specular_exponent(&mut self, value: F) -> io::Result<()>;
955    fn write_optical_density(&mut self, value: F) -> io::Result<()>;
956    fn write_dissolve(&mut self, value: F) -> io::Result<()>;
957    fn write_transparency(&mut self, value: F) -> io::Result<()>;
958    fn write_illumination_model(&mut self, value: u32) -> io::Result<()>;
959    fn write_map<S: AsRef<str>>(&mut self, kind: MapKind, args: S) -> io::Result<()>;
960}
961
962pub fn read_mtl_file<R: io::Read, T: MtlReader<F>, F: ObjFloat>(
963    reader: R,
964    mtl_reader: &mut T,
965) -> Result<(), MtlError>
966where
967    <F as FromStr>::Err: Display,
968{
969    let mut buf_reader = BufReader::new(reader);
970    let mut line = String::new();
971    let mut lineno: usize = 0;
972
973    while buf_reader.read_line(&mut line)? != 0 {
974        lineno += 1;
975        let trimmed = line.trim();
976        if trimmed.is_empty() {
977            line.clear();
978            continue;
979        }
980        let mut parts = trimmed.split_whitespace();
981        let prefix = parts.next().ok_or(MtlError::Parse {
982            line: lineno,
983            kind: MtlParseErrorKind::EmptyPrefix,
984        })?;
985
986        let parse_f = |s: &str, field: &'static str| -> Result<F, MtlError> {
987            s.parse::<F>().map_err(|_| MtlError::Parse {
988                line: lineno,
989                kind: MtlParseErrorKind::InvalidNumber {
990                    field,
991                    value: s.to_string(),
992                },
993            })
994        };
995
996        let missing = |field: &'static str| -> MtlError {
997            MtlError::Parse {
998                line: lineno,
999                kind: MtlParseErrorKind::MissingField(field),
1000            }
1001        };
1002
1003        let parse_color = |parts: &mut std::str::SplitWhitespace<'_>,
1004                           field_r: &'static str,
1005                           field_g: &'static str,
1006                           field_b: &'static str|
1007         -> Result<(F, Option<F>, Option<F>), MtlError> {
1008            let r = parts
1009                .next()
1010                .ok_or_else(|| missing(field_r))
1011                .and_then(|s| parse_f(s, field_r))?;
1012            let g = match parts.next() {
1013                Some(s) => Some(parse_f(s, field_g)?),
1014                None => None,
1015            };
1016            let b = match parts.next() {
1017                Some(s) => Some(parse_f(s, field_b)?),
1018                None => None,
1019            };
1020            Ok((r, g, b))
1021        };
1022
1023        match prefix {
1024            "#" => {
1025                let comment = parts.collect::<Vec<&str>>().join(" ");
1026                mtl_reader.read_comment(&comment);
1027            }
1028            "newmtl" => {
1029                let name = parts.collect::<Vec<&str>>().join(" ");
1030                if name.is_empty() {
1031                    return Err(missing("material name"));
1032                }
1033                mtl_reader.read_new_material(&name);
1034            }
1035            "Ka" => {
1036                let (r, g, b) = parse_color(&mut parts, "Ka r", "Ka g", "Ka b")?;
1037                mtl_reader.read_ambient(r, g, b);
1038            }
1039            "Kd" => {
1040                let (r, g, b) = parse_color(&mut parts, "Kd r", "Kd g", "Kd b")?;
1041                mtl_reader.read_diffuse(r, g, b);
1042            }
1043            "Ks" => {
1044                let (r, g, b) = parse_color(&mut parts, "Ks r", "Ks g", "Ks b")?;
1045                mtl_reader.read_specular(r, g, b);
1046            }
1047            "Ke" => {
1048                let (r, g, b) = parse_color(&mut parts, "Ke r", "Ke g", "Ke b")?;
1049                mtl_reader.read_emissive(r, g, b);
1050            }
1051            "Ns" => {
1052                let v = parts
1053                    .next()
1054                    .ok_or_else(|| missing("Ns"))
1055                    .and_then(|s| parse_f(s, "Ns"))?;
1056                mtl_reader.read_specular_exponent(v);
1057            }
1058            "Ni" => {
1059                let v = parts
1060                    .next()
1061                    .ok_or_else(|| missing("Ni"))
1062                    .and_then(|s| parse_f(s, "Ni"))?;
1063                mtl_reader.read_optical_density(v);
1064            }
1065            "d" => {
1066                let v = parts
1067                    .next()
1068                    .ok_or_else(|| missing("d"))
1069                    .and_then(|s| parse_f(s, "d"))?;
1070                mtl_reader.read_dissolve(v);
1071            }
1072            "Tr" => {
1073                let v = parts
1074                    .next()
1075                    .ok_or_else(|| missing("Tr"))
1076                    .and_then(|s| parse_f(s, "Tr"))?;
1077                mtl_reader.read_transparency(v);
1078            }
1079            "illum" => {
1080                let s = parts.next().ok_or_else(|| missing("illum"))?;
1081                let v = s.parse::<u32>().map_err(|_| MtlError::Parse {
1082                    line: lineno,
1083                    kind: MtlParseErrorKind::InvalidIlluminationModel(s.to_string()),
1084                })?;
1085                mtl_reader.read_illumination_model(v);
1086            }
1087            other => {
1088                if let Some(kind) = MapKind::from_prefix(other) {
1089                    let args = parts.collect::<Vec<&str>>().join(" ");
1090                    if args.is_empty() {
1091                        return Err(missing("map filename"));
1092                    }
1093                    mtl_reader.read_map(kind, &args);
1094                } else {
1095                    let rest = parts.collect::<Vec<&str>>().join(" ");
1096                    mtl_reader.read_unknown(other, &rest, lineno)?;
1097                }
1098            }
1099        }
1100
1101        line.clear();
1102    }
1103
1104    Ok(())
1105}
1106
1107/// File-backed [`MtlWriter`]. Mirrors [`IoObjWriter`].
1108pub struct IoMtlWriter<W: io::Write, F: ObjFloat = f64> {
1109    out: W,
1110    line_buf: Vec<u8>,
1111    _phantom: std::marker::PhantomData<F>,
1112}
1113
1114impl<W: io::Write, F: ObjFloat> IoMtlWriter<W, F> {
1115    pub fn new(writer: W) -> Self {
1116        IoMtlWriter {
1117            out: writer,
1118            line_buf: Vec::with_capacity(256),
1119            _phantom: std::marker::PhantomData,
1120        }
1121    }
1122
1123    /// Consume the writer and return the underlying sink.
1124    pub fn into_inner(self) -> W {
1125        self.out
1126    }
1127
1128    #[inline]
1129    fn push_str(&mut self, s: &str) {
1130        self.line_buf.extend_from_slice(s.as_bytes());
1131    }
1132
1133    #[inline]
1134    fn push_u<T: itoa::Integer>(&mut self, v: T) {
1135        let mut buf = itoa::Buffer::new();
1136        self.push_str(buf.format(v));
1137    }
1138
1139    #[inline]
1140    fn push_f(&mut self, v: F) {
1141        self.push_str(&format!("{}", v));
1142    }
1143
1144    #[inline]
1145    fn flush_line(&mut self) -> io::Result<()> {
1146        self.line_buf.push(b'\n');
1147        self.out.write_all(&self.line_buf)?;
1148        self.line_buf.clear();
1149        Ok(())
1150    }
1151
1152    fn write_color(&mut self, prefix: &str, r: F, g: Option<F>, b: Option<F>) -> io::Result<()> {
1153        self.push_str(prefix);
1154        self.push_str(" ");
1155        self.push_f(r);
1156        if let Some(gv) = g {
1157            self.push_str(" ");
1158            self.push_f(gv);
1159        }
1160        if let Some(bv) = b {
1161            self.push_str(" ");
1162            self.push_f(bv);
1163        }
1164        self.flush_line()
1165    }
1166}
1167
1168impl<W: io::Write, F: ObjFloat> MtlWriter<F> for IoMtlWriter<W, F> {
1169    fn write_comment<S: AsRef<str>>(&mut self, comment: S) -> io::Result<()> {
1170        self.push_str("# ");
1171        self.push_str(comment.as_ref());
1172        self.flush_line()
1173    }
1174
1175    fn write_new_material<S: AsRef<str>>(&mut self, name: S) -> io::Result<()> {
1176        self.push_str("newmtl ");
1177        self.push_str(name.as_ref());
1178        self.flush_line()
1179    }
1180
1181    fn write_ambient(&mut self, r: F, g: Option<F>, b: Option<F>) -> io::Result<()> {
1182        self.write_color("Ka", r, g, b)
1183    }
1184
1185    fn write_diffuse(&mut self, r: F, g: Option<F>, b: Option<F>) -> io::Result<()> {
1186        self.write_color("Kd", r, g, b)
1187    }
1188
1189    fn write_specular(&mut self, r: F, g: Option<F>, b: Option<F>) -> io::Result<()> {
1190        self.write_color("Ks", r, g, b)
1191    }
1192
1193    fn write_emissive(&mut self, r: F, g: Option<F>, b: Option<F>) -> io::Result<()> {
1194        self.write_color("Ke", r, g, b)
1195    }
1196
1197    fn write_specular_exponent(&mut self, value: F) -> io::Result<()> {
1198        self.push_str("Ns ");
1199        self.push_f(value);
1200        self.flush_line()
1201    }
1202
1203    fn write_optical_density(&mut self, value: F) -> io::Result<()> {
1204        self.push_str("Ni ");
1205        self.push_f(value);
1206        self.flush_line()
1207    }
1208
1209    fn write_dissolve(&mut self, value: F) -> io::Result<()> {
1210        self.push_str("d ");
1211        self.push_f(value);
1212        self.flush_line()
1213    }
1214
1215    fn write_transparency(&mut self, value: F) -> io::Result<()> {
1216        self.push_str("Tr ");
1217        self.push_f(value);
1218        self.flush_line()
1219    }
1220
1221    fn write_illumination_model(&mut self, value: u32) -> io::Result<()> {
1222        self.push_str("illum ");
1223        self.push_u(value);
1224        self.flush_line()
1225    }
1226
1227    fn write_map<S: AsRef<str>>(&mut self, kind: MapKind, args: S) -> io::Result<()> {
1228        self.push_str(kind.as_prefix());
1229        self.push_str(" ");
1230        self.push_str(args.as_ref());
1231        self.flush_line()
1232    }
1233}
1234
1235#[cfg(test)]
1236mod tests {
1237    use super::*;
1238    use pretty_assertions::assert_eq;
1239    use std::io::Cursor;
1240
1241    type Face = Vec<(usize, Option<usize>, Option<usize>)>;
1242
1243    #[derive(Default)]
1244    struct TestObjReader64 {
1245        comments: Vec<String>,
1246        names: Vec<String>,
1247        vertices: Vec<(f64, f64, f64, Option<f64>)>,
1248        texture_coordinates: Vec<(f64, Option<f64>, Option<f64>)>,
1249        normals: Vec<(f64, f64, f64)>,
1250        faces: Vec<Face>,
1251    }
1252
1253    impl ObjReader for TestObjReader64 {
1254        fn read_comment(&mut self, comment: &str) {
1255            self.comments.push(comment.to_string());
1256        }
1257
1258        fn read_object_name(&mut self, name: &str) {
1259            self.names.push(name.to_string());
1260        }
1261
1262        fn read_vertex(&mut self, x: f64, y: f64, z: f64, w: Option<f64>) {
1263            self.vertices.push((x, y, z, w));
1264        }
1265
1266        fn read_texture_coordinate(&mut self, u: f64, v: Option<f64>, w: Option<f64>) {
1267            self.texture_coordinates.push((u, v, w));
1268        }
1269
1270        fn read_normal(&mut self, nx: f64, ny: f64, nz: f64) {
1271            self.normals.push((nx, ny, nz));
1272        }
1273
1274        fn read_face(&mut self, vertex_indices: &[(usize, Option<usize>, Option<usize>)]) {
1275            self.faces.push(vertex_indices.to_vec());
1276        }
1277    }
1278
1279    #[test]
1280    fn test_obj_reading_2() {
1281        let input = "# This is a test OBJ file
1282o TestObject
1283v 1 2 3
1284vt 0.5 0.5
1285vn 0 1 1.1
1286f 1/1/1 2/2/2 3/3/3
1287";
1288
1289        let reader = Cursor::new(input);
1290        let mut test_reader: TestObjReader64 = Default::default();
1291        read_obj_file(reader, &mut test_reader).unwrap();
1292        assert_eq!(test_reader.comments, vec!["This is a test OBJ file"]);
1293        assert_eq!(test_reader.names, vec!["TestObject"]);
1294        assert_eq!(test_reader.vertices, vec![(1.0, 2.0, 3.0, None)]);
1295        assert_eq!(
1296            test_reader.texture_coordinates,
1297            vec![(0.5, Some(0.5), None)]
1298        );
1299        assert_eq!(test_reader.normals, vec![(0.0, 1.0, 1.1)]);
1300        assert_eq!(
1301            test_reader.faces,
1302            vec![vec![
1303                (1, Some(1), Some(1)),
1304                (2, Some(2), Some(2)),
1305                (3, Some(3), Some(3))
1306            ]]
1307        );
1308    }
1309
1310    #[test]
1311    fn test_obj_writing() {
1312        let mut buffer = Vec::new();
1313        let mut writer = IoObjWriter::new(&mut buffer);
1314        writer.write_comment("This is a test OBJ file").unwrap();
1315        writer.write_object_name("TestObject").unwrap();
1316        writer.write_vertex(1.0, 2.0, 3.0, None).unwrap();
1317        writer
1318            .write_texture_coordinate(0.5, Some(0.5), None)
1319            .unwrap();
1320        writer.write_normal(0.0, 1.0, 1.1).unwrap();
1321        writer
1322            .write_face(&[
1323                (1, Some(1), Some(1)),
1324                (2, Some(2), Some(2)),
1325                (3, Some(3), Some(3)),
1326            ])
1327            .unwrap();
1328
1329        let output = String::from_utf8(buffer).unwrap();
1330        let expected_output = "# This is a test OBJ file
1331o TestObject
1332v 1 2 3
1333vt 0.5 0.5
1334vn 0 1 1.1
1335f 1/1/1 2/2/2 3/3/3
1336";
1337        assert_eq!(output, expected_output);
1338    }
1339
1340    // Tests for f32 support
1341
1342    #[derive(Default)]
1343    struct TestObjReader32 {
1344        vertices: Vec<(f32, f32, f32, Option<f32>)>,
1345        texture_coordinates: Vec<(f32, Option<f32>, Option<f32>)>,
1346        normals: Vec<(f32, f32, f32)>,
1347    }
1348
1349    impl ObjReader<f32> for TestObjReader32 {
1350        fn read_comment(&mut self, _comment: &str) {}
1351        fn read_object_name(&mut self, _name: &str) {}
1352
1353        fn read_vertex(&mut self, x: f32, y: f32, z: f32, w: Option<f32>) {
1354            self.vertices.push((x, y, z, w));
1355        }
1356
1357        fn read_texture_coordinate(&mut self, u: f32, v: Option<f32>, w: Option<f32>) {
1358            self.texture_coordinates.push((u, v, w));
1359        }
1360
1361        fn read_normal(&mut self, nx: f32, ny: f32, nz: f32) {
1362            self.normals.push((nx, ny, nz));
1363        }
1364
1365        fn read_face(&mut self, _vertex_indices: &[(usize, Option<usize>, Option<usize>)]) {}
1366    }
1367
1368    #[test]
1369    fn test_obj_f32_reading() {
1370        let input = "o TestObject
1371v 1.5 2.5 3.5
1372vt 0.25 0.75
1373vn 0.0 1.0 0.0
1374";
1375
1376        let reader = Cursor::new(input);
1377        let mut test_reader = TestObjReader32::default();
1378        read_obj_file(reader, &mut test_reader).unwrap();
1379
1380        assert_eq!(test_reader.vertices, vec![(1.5f32, 2.5f32, 3.5f32, None)]);
1381        assert_eq!(
1382            test_reader.texture_coordinates,
1383            vec![(0.25f32, Some(0.75f32), None)]
1384        );
1385        assert_eq!(test_reader.normals, vec![(0.0f32, 1.0f32, 0.0f32)]);
1386    }
1387
1388    #[test]
1389    fn test_obj_f32_writing() {
1390        let mut buffer = Vec::new();
1391        let mut writer: IoObjWriter<_, f32> = IoObjWriter::new(&mut buffer);
1392
1393        writer.write_comment("f32 test").unwrap();
1394        writer.write_object_name("F32Object").unwrap();
1395        writer.write_vertex(1.5f32, 2.5f32, 3.5f32, None).unwrap();
1396        writer
1397            .write_texture_coordinate(0.25f32, Some(0.75f32), None)
1398            .unwrap();
1399        writer.write_normal(0.0f32, 1.0f32, 0.0f32).unwrap();
1400
1401        let output = String::from_utf8(buffer).unwrap();
1402        let expected = "# f32 test
1403o F32Object
1404v 1.5 2.5 3.5
1405vt 0.25 0.75
1406vn 0 1 0
1407";
1408        assert_eq!(output, expected);
1409    }
1410
1411    #[test]
1412    fn test_obj_f32_round_trip() {
1413        // Test that f32 values round-trip correctly
1414        let input = "o Test
1415v 0.123456789 0.987654321 1.5
1416vn 0.577 0.577 0.577
1417";
1418
1419        let reader = Cursor::new(input);
1420        let mut test_reader = TestObjReader32::default();
1421        read_obj_file(reader, &mut test_reader).unwrap();
1422
1423        // Write back using f32 writer
1424        let mut buffer = Vec::new();
1425        let mut writer: IoObjWriter<_, f32> = IoObjWriter::new(&mut buffer);
1426        writer.write_object_name("Test").unwrap();
1427        for (x, y, z, w) in &test_reader.vertices {
1428            writer.write_vertex(*x, *y, *z, *w).unwrap();
1429        }
1430        for (nx, ny, nz) in &test_reader.normals {
1431            writer.write_normal(*nx, *ny, *nz).unwrap();
1432        }
1433
1434        let output = String::from_utf8(buffer).unwrap();
1435
1436        // The values should be f32-precision
1437        assert!(output.contains("o Test"));
1438        assert!(output.contains("v "));
1439        assert!(output.contains("vn "));
1440    }
1441
1442    #[test]
1443    fn test_printf_f_formatting() {
1444        let mut buffer = Vec::new();
1445        let mut writer: IoObjWriter<_, f64> = IoObjWriter::new_with_printf_f_format(&mut buffer);
1446
1447        writer.write_object_name("PrintfFTest").unwrap();
1448        writer
1449            .write_vertex(754.4214477539063, 1753.2353515625, -91.72238159179688, None)
1450            .unwrap();
1451        writer
1452            .write_texture_coordinate(0.123456789, Some(0.987654321), None)
1453            .unwrap();
1454        writer
1455            .write_normal(0.5773502691896257, 0.5773502691896257, 0.5773502691896257)
1456            .unwrap();
1457
1458        let output = String::from_utf8(buffer).unwrap();
1459
1460        // C `printf %f` defaults to 6 decimal places.
1461        let expected = "o PrintfFTest
1462v 754.421448 1753.235352 -91.722382
1463vt 0.123457 0.987654
1464vn 0.577350 0.577350 0.577350
1465";
1466        assert_eq!(output, expected);
1467    }
1468
1469    #[test]
1470    fn test_printf_f_flag_toggle() {
1471        // Test that we can toggle the flag
1472        let mut buffer = Vec::new();
1473        let mut writer: IoObjWriter<_, f32> = IoObjWriter::new(&mut buffer);
1474
1475        // Start with default (full precision)
1476        writer
1477            .write_vertex(1.2345678_f32, 2.345678_f32, 3.45678_f32, None)
1478            .unwrap();
1479
1480        // Enable printf %f formatting
1481        writer.set_printf_f_format(true);
1482        writer
1483            .write_vertex(1.2345678_f32, 2.3456789_f32, 3.456789_f32, None)
1484            .unwrap();
1485
1486        let output = String::from_utf8(buffer).unwrap();
1487
1488        // First line should have full f32 precision, second should have 6 decimals
1489        let lines: Vec<&str> = output.lines().collect();
1490        assert_eq!(lines.len(), 2);
1491
1492        // Full precision output (f32 Display)
1493        assert_eq!(lines[0], "v 1.2345678 2.345678 3.45678");
1494
1495        // printf %f output (6 decimal places)
1496        assert_eq!(lines[1], "v 1.234568 2.345679 3.456789");
1497    }
1498
1499    // Tests for the standard auxiliary directives:
1500    //   mtllib, usemtl, g, s, l, p
1501
1502    /// Captures every directive seen, for round-trip and equality testing.
1503    #[derive(Default, Debug, PartialEq)]
1504    struct ExtendedReader32 {
1505        material_libs: Vec<Vec<String>>,
1506        use_materials: Vec<String>,
1507        groups: Vec<Vec<String>>,
1508        smoothing_groups: Vec<SmoothingGroup>,
1509        line_elements: Vec<Vec<(usize, Option<usize>)>>,
1510        point_elements: Vec<Vec<usize>>,
1511    }
1512
1513    impl ObjReader<f32> for ExtendedReader32 {
1514        fn read_comment(&mut self, _: &str) {}
1515        fn read_object_name(&mut self, _: &str) {}
1516        fn read_vertex(&mut self, _: f32, _: f32, _: f32, _: Option<f32>) {}
1517        fn read_texture_coordinate(&mut self, _: f32, _: Option<f32>, _: Option<f32>) {}
1518        fn read_normal(&mut self, _: f32, _: f32, _: f32) {}
1519        fn read_face(&mut self, _: &[(usize, Option<usize>, Option<usize>)]) {}
1520
1521        fn read_material_lib(&mut self, names: &[&str]) {
1522            self.material_libs
1523                .push(names.iter().map(|s| s.to_string()).collect());
1524        }
1525        fn read_use_material(&mut self, name: &str) {
1526            self.use_materials.push(name.to_string());
1527        }
1528        fn read_group(&mut self, names: &[&str]) {
1529            self.groups
1530                .push(names.iter().map(|s| s.to_string()).collect());
1531        }
1532        fn read_smoothing_group(&mut self, group: SmoothingGroup) {
1533            self.smoothing_groups.push(group);
1534        }
1535        fn read_line_element(&mut self, indices: &[(usize, Option<usize>)]) {
1536            self.line_elements.push(indices.to_vec());
1537        }
1538        fn read_point_element(&mut self, indices: &[usize]) {
1539            self.point_elements.push(indices.to_vec());
1540        }
1541    }
1542
1543    #[test]
1544    fn read_mtllib_and_usemtl() {
1545        let input = "mtllib first.mtl second.mtl
1546usemtl SomeMaterial
1547";
1548        let mut reader = ExtendedReader32::default();
1549        read_obj_file(Cursor::new(input), &mut reader).unwrap();
1550        assert_eq!(
1551            reader.material_libs,
1552            vec![vec!["first.mtl".to_string(), "second.mtl".to_string()]]
1553        );
1554        assert_eq!(reader.use_materials, vec!["SomeMaterial".to_string()]);
1555    }
1556
1557    #[test]
1558    fn read_group_with_multiple_names() {
1559        let input = "g cube top
1560g default
1561";
1562        let mut reader = ExtendedReader32::default();
1563        read_obj_file(Cursor::new(input), &mut reader).unwrap();
1564        assert_eq!(
1565            reader.groups,
1566            vec![
1567                vec!["cube".to_string(), "top".to_string()],
1568                vec!["default".to_string()],
1569            ]
1570        );
1571    }
1572
1573    #[test]
1574    fn read_smoothing_group_off_zero_and_named() {
1575        let input = "s off
1576s 0
1577s 1
1578s 42
1579";
1580        let mut reader = ExtendedReader32::default();
1581        read_obj_file(Cursor::new(input), &mut reader).unwrap();
1582        assert_eq!(
1583            reader.smoothing_groups,
1584            vec![
1585                SmoothingGroup::Off,
1586                SmoothingGroup::Off,
1587                SmoothingGroup::Group(1),
1588                SmoothingGroup::Group(42),
1589            ]
1590        );
1591    }
1592
1593    #[test]
1594    fn read_smoothing_group_invalid_value_errors() {
1595        let input = "s notanumber\n";
1596        let mut reader = ExtendedReader32::default();
1597        let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1598        assert!(
1599            err.to_string().contains("invalid smoothing group"),
1600            "got: {}",
1601            err
1602        );
1603    }
1604
1605    #[test]
1606    fn read_line_and_point_elements() {
1607        let input = "l 1 2 3
1608l 4/1 5/2 6/3
1609p 1 2 3 4
1610";
1611        let mut reader = ExtendedReader32::default();
1612        read_obj_file(Cursor::new(input), &mut reader).unwrap();
1613        assert_eq!(
1614            reader.line_elements,
1615            vec![
1616                vec![(1, None), (2, None), (3, None)],
1617                vec![(4, Some(1)), (5, Some(2)), (6, Some(3))],
1618            ]
1619        );
1620        assert_eq!(reader.point_elements, vec![vec![1, 2, 3, 4]]);
1621    }
1622
1623    #[test]
1624    fn read_line_element_with_one_vertex_errors() {
1625        let mut reader = ExtendedReader32::default();
1626        let err = read_obj_file(Cursor::new("l 1\n"), &mut reader).unwrap_err();
1627        assert!(
1628            err.to_string().contains("at least 2 vertices"),
1629            "got: {}",
1630            err
1631        );
1632    }
1633
1634    #[test]
1635    fn write_directive_round_trip() {
1636        // Write every new directive, parse the output back, compare.
1637        let mut buffer = Vec::new();
1638        {
1639            let mut writer: IoObjWriter<_, f32> = IoObjWriter::new(&mut buffer);
1640            writer
1641                .write_material_lib(&["lib1.mtl", "lib2.mtl"])
1642                .unwrap();
1643            writer.write_use_material("Wood").unwrap();
1644            writer.write_group(&["cube", "top"]).unwrap();
1645            writer.write_smoothing_group(SmoothingGroup::Off).unwrap();
1646            writer
1647                .write_smoothing_group(SmoothingGroup::Group(7))
1648                .unwrap();
1649            writer
1650                .write_line_element(&[(1, None), (2, Some(2)), (3, None)])
1651                .unwrap();
1652            writer.write_point_element(&[1, 2, 3]).unwrap();
1653        }
1654        let text = String::from_utf8(buffer.clone()).unwrap();
1655        let expected = "mtllib lib1.mtl lib2.mtl
1656usemtl Wood
1657g cube top
1658s off
1659s 7
1660l 1 2/2 3
1661p 1 2 3
1662";
1663        assert_eq!(text, expected);
1664
1665        let mut reader = ExtendedReader32::default();
1666        read_obj_file(Cursor::new(buffer), &mut reader).unwrap();
1667        assert_eq!(
1668            reader.material_libs,
1669            vec![vec!["lib1.mtl".to_string(), "lib2.mtl".to_string()]]
1670        );
1671        assert_eq!(reader.use_materials, vec!["Wood".to_string()]);
1672        assert_eq!(
1673            reader.groups,
1674            vec![vec!["cube".to_string(), "top".to_string()]]
1675        );
1676        assert_eq!(
1677            reader.smoothing_groups,
1678            vec![SmoothingGroup::Off, SmoothingGroup::Group(7)]
1679        );
1680        assert_eq!(
1681            reader.line_elements,
1682            vec![vec![(1, None), (2, Some(2)), (3, None)]]
1683        );
1684        assert_eq!(reader.point_elements, vec![vec![1, 2, 3]]);
1685    }
1686
1687    #[test]
1688    fn unknown_prefix_still_errors_by_default() {
1689        // Sanity-check that adding the typed directives didn't accidentally
1690        // make the strict reader lenient for prefixes outside the new set.
1691        let input = "vp 0.1 0.2 0.3\n";
1692        let mut reader = ExtendedReader32::default();
1693        let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1694        assert!(
1695            err.to_string().contains("Unknown line prefix: vp"),
1696            "got: {}",
1697            err
1698        );
1699    }
1700
1701    // Typed-error tests: callers can pattern-match on `ObjError` and
1702    // `ParseErrorKind` instead of string-matching on the Display.
1703
1704    #[test]
1705    fn typed_error_unknown_prefix_carries_prefix_and_line() {
1706        let input = "v 0 0 0\nvp 0.1 0.2\n";
1707        let mut reader = ExtendedReader32::default();
1708        let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1709        match err {
1710            ObjError::Parse {
1711                line,
1712                kind: ParseErrorKind::UnknownPrefix(prefix),
1713            } => {
1714                assert_eq!(line, 2);
1715                assert_eq!(prefix, "vp");
1716            }
1717            other => panic!("wrong error variant: {:?}", other),
1718        }
1719    }
1720
1721    #[test]
1722    fn typed_error_invalid_number_carries_field_and_value() {
1723        let input = "v 1.0 nope 3.0\n";
1724        let mut reader = ExtendedReader32::default();
1725        let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1726        match err {
1727            ObjError::Parse {
1728                line: 1,
1729                kind: ParseErrorKind::InvalidNumber { field, value },
1730            } => {
1731                assert_eq!(field, "vertex y");
1732                assert_eq!(value, "nope");
1733            }
1734            other => panic!("wrong error variant: {:?}", other),
1735        }
1736    }
1737
1738    #[test]
1739    fn typed_error_invalid_index_for_face() {
1740        // Index 0 is illegal in OBJ.
1741        let input = "v 0 0 0\nf 0/0/0 0/0/0 0/0/0\n";
1742        let mut reader = ExtendedReader32::default();
1743        let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1744        match err {
1745            ObjError::Parse {
1746                line: 2,
1747                kind: ParseErrorKind::InvalidIndex { kind, value },
1748            } => {
1749                assert_eq!(kind, "vertex");
1750                assert_eq!(value, "0");
1751            }
1752            other => panic!("wrong error variant: {:?}", other),
1753        }
1754    }
1755
1756    #[test]
1757    fn typed_error_invalid_smoothing_group() {
1758        let input = "s notanumber\n";
1759        let mut reader = ExtendedReader32::default();
1760        let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1761        match err {
1762            ObjError::Parse {
1763                line: 1,
1764                kind: ParseErrorKind::InvalidSmoothingGroup(value),
1765            } => assert_eq!(value, "notanumber"),
1766            other => panic!("wrong error variant: {:?}", other),
1767        }
1768    }
1769
1770    #[test]
1771    fn typed_error_missing_field() {
1772        let input = "v 1.0 2.0\n";
1773        let mut reader = ExtendedReader32::default();
1774        let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1775        match err {
1776            ObjError::Parse {
1777                line: 1,
1778                kind: ParseErrorKind::MissingField(field),
1779            } => assert_eq!(field, "vertex z"),
1780            other => panic!("wrong error variant: {:?}", other),
1781        }
1782    }
1783
1784    #[test]
1785    fn typed_error_converts_to_io_error() {
1786        // `read_obj_file` returns `ObjError`; callers that work in
1787        // `io::Result` get a free conversion via `From<ObjError>`.
1788        fn legacy_caller<R: io::Read>(input: R) -> io::Result<()> {
1789            let mut reader = ExtendedReader32::default();
1790            read_obj_file(input, &mut reader)?;
1791            Ok(())
1792        }
1793
1794        let err = legacy_caller(Cursor::new("vp 0.1 0.2\n")).unwrap_err();
1795        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1796        assert!(err.to_string().contains("Unknown line prefix: vp"));
1797    }
1798
1799    #[test]
1800    fn typed_error_io_failure_propagates() {
1801        // Force a Read error mid-stream.
1802        struct FailingRead;
1803        impl io::Read for FailingRead {
1804            fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
1805                Err(io::Error::other("disk on fire"))
1806            }
1807        }
1808
1809        let mut reader = ExtendedReader32::default();
1810        let err = read_obj_file(FailingRead, &mut reader).unwrap_err();
1811        match err {
1812            ObjError::Io(inner) => {
1813                assert_eq!(inner.to_string(), "disk on fire");
1814            }
1815            other => panic!("wrong error variant: {:?}", other),
1816        }
1817    }
1818
1819    #[test]
1820    fn custom_read_unknown_can_return_objerror() {
1821        // A reader that surfaces unknown prefixes as Custom errors.
1822        struct StrictCustom;
1823        impl ObjReader<f32> for StrictCustom {
1824            fn read_comment(&mut self, _: &str) {}
1825            fn read_object_name(&mut self, _: &str) {}
1826            fn read_vertex(&mut self, _: f32, _: f32, _: f32, _: Option<f32>) {}
1827            fn read_texture_coordinate(&mut self, _: f32, _: Option<f32>, _: Option<f32>) {}
1828            fn read_normal(&mut self, _: f32, _: f32, _: f32) {}
1829            fn read_face(&mut self, _: &[(usize, Option<usize>, Option<usize>)]) {}
1830            fn read_unknown(&mut self, prefix: &str, _: &str, line: usize) -> Result<(), ObjError> {
1831                Err(ObjError::Parse {
1832                    line,
1833                    kind: ParseErrorKind::Custom(format!("nope: {prefix}")),
1834                })
1835            }
1836        }
1837
1838        let err = read_obj_file(Cursor::new("vp 0 0\n"), &mut StrictCustom).unwrap_err();
1839        match err {
1840            ObjError::Parse {
1841                line: 1,
1842                kind: ParseErrorKind::Custom(msg),
1843            } => assert_eq!(msg, "nope: vp"),
1844            other => panic!("wrong error variant: {:?}", other),
1845        }
1846    }
1847
1848    // -----------------------------------------------------------------
1849    // MTL tests
1850    // -----------------------------------------------------------------
1851
1852    #[derive(Default, Debug, PartialEq)]
1853    struct CapturingMtlReader {
1854        comments: Vec<String>,
1855        materials: Vec<String>,
1856        ambients: Vec<(f64, Option<f64>, Option<f64>)>,
1857        diffuses: Vec<(f64, Option<f64>, Option<f64>)>,
1858        speculars: Vec<(f64, Option<f64>, Option<f64>)>,
1859        emissives: Vec<(f64, Option<f64>, Option<f64>)>,
1860        specular_exponents: Vec<f64>,
1861        optical_densities: Vec<f64>,
1862        dissolves: Vec<f64>,
1863        transparencies: Vec<f64>,
1864        illumination_models: Vec<u32>,
1865        maps: Vec<(MapKind, String)>,
1866    }
1867
1868    impl MtlReader for CapturingMtlReader {
1869        fn read_comment(&mut self, c: &str) {
1870            self.comments.push(c.to_string());
1871        }
1872        fn read_new_material(&mut self, name: &str) {
1873            self.materials.push(name.to_string());
1874        }
1875        fn read_ambient(&mut self, r: f64, g: Option<f64>, b: Option<f64>) {
1876            self.ambients.push((r, g, b));
1877        }
1878        fn read_diffuse(&mut self, r: f64, g: Option<f64>, b: Option<f64>) {
1879            self.diffuses.push((r, g, b));
1880        }
1881        fn read_specular(&mut self, r: f64, g: Option<f64>, b: Option<f64>) {
1882            self.speculars.push((r, g, b));
1883        }
1884        fn read_emissive(&mut self, r: f64, g: Option<f64>, b: Option<f64>) {
1885            self.emissives.push((r, g, b));
1886        }
1887        fn read_specular_exponent(&mut self, v: f64) {
1888            self.specular_exponents.push(v);
1889        }
1890        fn read_optical_density(&mut self, v: f64) {
1891            self.optical_densities.push(v);
1892        }
1893        fn read_dissolve(&mut self, v: f64) {
1894            self.dissolves.push(v);
1895        }
1896        fn read_transparency(&mut self, v: f64) {
1897            self.transparencies.push(v);
1898        }
1899        fn read_illumination_model(&mut self, v: u32) {
1900            self.illumination_models.push(v);
1901        }
1902        fn read_map(&mut self, kind: MapKind, args: &str) {
1903            self.maps.push((kind, args.to_string()));
1904        }
1905    }
1906
1907    #[test]
1908    fn read_mtl_simple_material() {
1909        let input = "# A simple material\n\
1910                     newmtl Wood\n\
1911                     Ka 0.2 0.2 0.2\n\
1912                     Kd 0.8 0.6 0.4\n\
1913                     Ks 1.0 1.0 1.0\n\
1914                     Ns 100.0\n\
1915                     d 1.0\n\
1916                     illum 2\n\
1917                     map_Kd wood.png\n";
1918        let mut reader = CapturingMtlReader::default();
1919        read_mtl_file(Cursor::new(input), &mut reader).unwrap();
1920        assert_eq!(reader.comments, vec!["A simple material"]);
1921        assert_eq!(reader.materials, vec!["Wood"]);
1922        assert_eq!(reader.ambients, vec![(0.2, Some(0.2), Some(0.2))]);
1923        assert_eq!(reader.diffuses, vec![(0.8, Some(0.6), Some(0.4))]);
1924        assert_eq!(reader.speculars, vec![(1.0, Some(1.0), Some(1.0))]);
1925        assert_eq!(reader.specular_exponents, vec![100.0]);
1926        assert_eq!(reader.dissolves, vec![1.0]);
1927        assert_eq!(reader.illumination_models, vec![2]);
1928        assert_eq!(
1929            reader.maps,
1930            vec![(MapKind::Diffuse, "wood.png".to_string())]
1931        );
1932    }
1933
1934    #[test]
1935    fn read_mtl_multiple_materials() {
1936        let input = "newmtl First\nKd 1 0 0\nnewmtl Second\nKd 0 1 0\n";
1937        let mut reader = CapturingMtlReader::default();
1938        read_mtl_file(Cursor::new(input), &mut reader).unwrap();
1939        assert_eq!(reader.materials, vec!["First", "Second"]);
1940        assert_eq!(
1941            reader.diffuses,
1942            vec![(1.0, Some(0.0), Some(0.0)), (0.0, Some(1.0), Some(0.0))]
1943        );
1944    }
1945
1946    #[test]
1947    fn read_mtl_ka_grey_single_value() {
1948        let input = "newmtl M\nKa 0.5\n";
1949        let mut reader = CapturingMtlReader::default();
1950        read_mtl_file(Cursor::new(input), &mut reader).unwrap();
1951        assert_eq!(reader.ambients, vec![(0.5, None, None)]);
1952    }
1953
1954    #[test]
1955    fn read_mtl_all_map_kinds() {
1956        let input = "newmtl M\n\
1957                     map_Ka a.png\n\
1958                     map_Kd d.png\n\
1959                     map_Ks s.png\n\
1960                     map_Ke e.png\n\
1961                     map_Ns ns.png\n\
1962                     map_d alpha.png\n\
1963                     bump bump.png\n\
1964                     map_bump bump2.png\n\
1965                     disp disp.png\n\
1966                     decal decal.png\n\
1967                     refl -type sphere refl.png\n";
1968        let mut reader = CapturingMtlReader::default();
1969        read_mtl_file(Cursor::new(input), &mut reader).unwrap();
1970        assert_eq!(
1971            reader.maps,
1972            vec![
1973                (MapKind::Ambient, "a.png".to_string()),
1974                (MapKind::Diffuse, "d.png".to_string()),
1975                (MapKind::Specular, "s.png".to_string()),
1976                (MapKind::Emissive, "e.png".to_string()),
1977                (MapKind::SpecularExponent, "ns.png".to_string()),
1978                (MapKind::Dissolve, "alpha.png".to_string()),
1979                (MapKind::Bump, "bump.png".to_string()),
1980                (MapKind::MapBump, "bump2.png".to_string()),
1981                (MapKind::Displacement, "disp.png".to_string()),
1982                (MapKind::Decal, "decal.png".to_string()),
1983                (MapKind::Reflection, "-type sphere refl.png".to_string()),
1984            ]
1985        );
1986    }
1987
1988    #[test]
1989    fn read_mtl_unknown_prefix_errors() {
1990        let input = "newmtl M\nweirdo 1 2 3\n";
1991        let mut reader = CapturingMtlReader::default();
1992        let err = read_mtl_file(Cursor::new(input), &mut reader).unwrap_err();
1993        match err {
1994            MtlError::Parse {
1995                line: 2,
1996                kind: MtlParseErrorKind::UnknownPrefix(p),
1997            } => assert_eq!(p, "weirdo"),
1998            other => panic!("wrong error: {:?}", other),
1999        }
2000    }
2001
2002    #[test]
2003    fn read_mtl_invalid_illum_errors() {
2004        let input = "newmtl M\nillum nope\n";
2005        let mut reader = CapturingMtlReader::default();
2006        let err = read_mtl_file(Cursor::new(input), &mut reader).unwrap_err();
2007        match err {
2008            MtlError::Parse {
2009                line: 2,
2010                kind: MtlParseErrorKind::InvalidIlluminationModel(v),
2011            } => assert_eq!(v, "nope"),
2012            other => panic!("wrong error: {:?}", other),
2013        }
2014    }
2015
2016    #[test]
2017    fn write_mtl_simple_material() {
2018        let mut buffer = Vec::new();
2019        let mut writer: IoMtlWriter<_, f64> = IoMtlWriter::new(&mut buffer);
2020        writer.write_comment("A simple material").unwrap();
2021        writer.write_new_material("Wood").unwrap();
2022        writer.write_ambient(0.2, Some(0.2), Some(0.2)).unwrap();
2023        writer.write_diffuse(0.8, Some(0.6), Some(0.4)).unwrap();
2024        writer.write_specular(1.0, Some(1.0), Some(1.0)).unwrap();
2025        writer.write_specular_exponent(100.0).unwrap();
2026        writer.write_dissolve(1.0).unwrap();
2027        writer.write_illumination_model(2).unwrap();
2028        writer.write_map(MapKind::Diffuse, "wood.png").unwrap();
2029        let output = String::from_utf8(buffer).unwrap();
2030        let expected = "# A simple material\n\
2031                        newmtl Wood\n\
2032                        Ka 0.2 0.2 0.2\n\
2033                        Kd 0.8 0.6 0.4\n\
2034                        Ks 1 1 1\n\
2035                        Ns 100\n\
2036                        d 1\n\
2037                        illum 2\n\
2038                        map_Kd wood.png\n";
2039        assert_eq!(output, expected);
2040    }
2041
2042    /// Pass-through reader/writer pair for byte-equal round-trip.
2043    struct WritingMtlReader {
2044        writer: IoMtlWriter<Vec<u8>>,
2045    }
2046    impl MtlReader for WritingMtlReader {
2047        fn read_comment(&mut self, c: &str) {
2048            self.writer.write_comment(c).unwrap();
2049        }
2050        fn read_new_material(&mut self, name: &str) {
2051            self.writer.write_new_material(name).unwrap();
2052        }
2053        fn read_ambient(&mut self, r: f64, g: Option<f64>, b: Option<f64>) {
2054            self.writer.write_ambient(r, g, b).unwrap();
2055        }
2056        fn read_diffuse(&mut self, r: f64, g: Option<f64>, b: Option<f64>) {
2057            self.writer.write_diffuse(r, g, b).unwrap();
2058        }
2059        fn read_specular(&mut self, r: f64, g: Option<f64>, b: Option<f64>) {
2060            self.writer.write_specular(r, g, b).unwrap();
2061        }
2062        fn read_emissive(&mut self, r: f64, g: Option<f64>, b: Option<f64>) {
2063            self.writer.write_emissive(r, g, b).unwrap();
2064        }
2065        fn read_specular_exponent(&mut self, v: f64) {
2066            self.writer.write_specular_exponent(v).unwrap();
2067        }
2068        fn read_optical_density(&mut self, v: f64) {
2069            self.writer.write_optical_density(v).unwrap();
2070        }
2071        fn read_dissolve(&mut self, v: f64) {
2072            self.writer.write_dissolve(v).unwrap();
2073        }
2074        fn read_transparency(&mut self, v: f64) {
2075            self.writer.write_transparency(v).unwrap();
2076        }
2077        fn read_illumination_model(&mut self, v: u32) {
2078            self.writer.write_illumination_model(v).unwrap();
2079        }
2080        fn read_map(&mut self, kind: MapKind, args: &str) {
2081            self.writer.write_map(kind, args).unwrap();
2082        }
2083    }
2084
2085    #[test]
2086    fn mtl_round_trip_byte_equal() {
2087        let input = "# library header\n\
2088                     newmtl First\n\
2089                     Ka 0.1 0.2 0.3\n\
2090                     Kd 0.4 0.5 0.6\n\
2091                     Ks 0.7 0.8 0.9\n\
2092                     Ke 0 0 0\n\
2093                     Ns 50\n\
2094                     Ni 1.45\n\
2095                     d 0.5\n\
2096                     Tr 0.5\n\
2097                     illum 2\n\
2098                     map_Ka ambient.png\n\
2099                     map_Kd diffuse.png\n\
2100                     bump bump.png\n\
2101                     map_bump bump2.png\n\
2102                     refl -type sphere refl.png\n\
2103                     newmtl Second\n\
2104                     Kd 1 0 0\n\
2105                     illum 1\n";
2106        let writer: IoMtlWriter<_, f64> = IoMtlWriter::new(Vec::new());
2107        let mut reader = WritingMtlReader { writer };
2108        read_mtl_file(Cursor::new(input), &mut reader).unwrap();
2109        let output = String::from_utf8(reader.writer.into_inner()).unwrap();
2110        assert_eq!(output, input);
2111    }
2112}