1use std::error::Error as StdError;
69use std::fmt;
70use std::fmt::Display;
71use std::io;
72use std::io::{BufRead, BufReader};
73use std::str::FromStr;
74
75#[derive(Debug)]
80pub enum ObjError {
81 Io(io::Error),
82 Parse { line: usize, kind: ParseErrorKind },
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum ParseErrorKind {
88 EmptyPrefix,
90 UnknownPrefix(String),
93 MissingField(&'static str),
95 InvalidNumber { field: &'static str, value: String },
97 InvalidIndex { kind: &'static str, value: String },
99 InvalidSmoothingGroup(String),
101 LineElementTooShort,
103 PointElementEmpty,
105 Custom(String),
107}
108
109impl Display for ObjError {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match self {
112 ObjError::Io(e) => write!(f, "I/O error: {e}"),
113 ObjError::Parse { line, kind } => write!(f, "line {line}: {kind}"),
114 }
115 }
116}
117
118impl Display for ParseErrorKind {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 match self {
121 ParseErrorKind::EmptyPrefix => write!(f, "empty prefix"),
122 ParseErrorKind::UnknownPrefix(p) => write!(f, "Unknown line prefix: {p}"),
123 ParseErrorKind::MissingField(field) => write!(f, "missing {field}"),
124 ParseErrorKind::InvalidNumber { field, value } => {
125 write!(f, "invalid {field}: {value}")
126 }
127 ParseErrorKind::InvalidIndex { kind, value } => {
128 write!(f, "invalid {kind} index: {value}")
129 }
130 ParseErrorKind::InvalidSmoothingGroup(v) => {
131 write!(
132 f,
133 "invalid smoothing group: {v} (expected integer or 'off')"
134 )
135 }
136 ParseErrorKind::LineElementTooShort => {
137 write!(f, "line element needs at least 2 vertices")
138 }
139 ParseErrorKind::PointElementEmpty => {
140 write!(f, "point element needs at least 1 vertex")
141 }
142 ParseErrorKind::Custom(s) => write!(f, "{s}"),
143 }
144 }
145}
146
147impl StdError for ObjError {
148 fn source(&self) -> Option<&(dyn StdError + 'static)> {
149 match self {
150 ObjError::Io(e) => Some(e),
151 ObjError::Parse { .. } => None,
152 }
153 }
154}
155
156impl From<io::Error> for ObjError {
157 fn from(e: io::Error) -> Self {
158 ObjError::Io(e)
159 }
160}
161
162impl From<ObjError> for io::Error {
163 fn from(e: ObjError) -> Self {
164 match e {
165 ObjError::Io(inner) => inner,
166 parse @ ObjError::Parse { .. } => {
167 io::Error::new(io::ErrorKind::InvalidData, parse.to_string())
168 }
169 }
170 }
171}
172
173pub trait ObjFloat: Copy + Display + FromStr + PartialEq {
176 fn fract(self) -> Self;
178
179 fn is_zero_fract(self) -> bool {
181 self.fract() == Self::zero()
182 }
183
184 fn zero() -> Self;
186}
187
188impl ObjFloat for f32 {
189 fn fract(self) -> Self {
190 self.fract()
191 }
192 fn zero() -> Self {
193 0.0
194 }
195}
196
197impl ObjFloat for f64 {
198 fn fract(self) -> Self {
199 self.fract()
200 }
201 fn zero() -> Self {
202 0.0
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub enum SmoothingGroup {
212 Off,
213 Group(u32),
214}
215
216pub trait ObjWriter<F: ObjFloat = f64> {
221 fn write_comment<S: AsRef<str>>(&mut self, comment: S) -> io::Result<()>;
222 fn write_object_name<S: AsRef<str>>(&mut self, name: S) -> io::Result<()>;
223 fn write_vertex(&mut self, x: F, y: F, z: F, w: Option<F>) -> io::Result<()>;
224 fn write_texture_coordinate(&mut self, u: F, v: Option<F>, w: Option<F>) -> io::Result<()>;
225 fn write_normal(&mut self, nx: F, ny: F, nz: F) -> io::Result<()>;
226 fn write_face(
227 &mut self,
228 vertex_indices: &[(usize, Option<usize>, Option<usize>)],
229 ) -> io::Result<()>;
230
231 fn write_material_lib<S: AsRef<str>>(&mut self, names: &[S]) -> io::Result<()>;
233
234 fn write_use_material<S: AsRef<str>>(&mut self, name: S) -> io::Result<()>;
236
237 fn write_group<S: AsRef<str>>(&mut self, names: &[S]) -> io::Result<()>;
239
240 fn write_smoothing_group(&mut self, group: SmoothingGroup) -> io::Result<()>;
242
243 fn write_line_element(&mut self, indices: &[(usize, Option<usize>)]) -> io::Result<()>;
246
247 fn write_point_element(&mut self, indices: &[usize]) -> io::Result<()>;
249}
250
251pub trait ObjReader<F: ObjFloat = f64> {
256 fn read_comment(&mut self, comment: &str) -> ();
257 fn read_object_name(&mut self, name: &str) -> ();
258 fn read_vertex(&mut self, x: F, y: F, z: F, w: Option<F>) -> ();
259 fn read_texture_coordinate(&mut self, u: F, v: Option<F>, w: Option<F>) -> ();
260 fn read_normal(&mut self, nx: F, ny: F, nz: F) -> ();
261 fn read_face(&mut self, vertex_indices: &[(usize, Option<usize>, Option<usize>)]) -> ();
262
263 fn read_material_lib(&mut self, _names: &[&str]) {}
265
266 fn read_use_material(&mut self, _name: &str) {}
268
269 fn read_group(&mut self, _names: &[&str]) {}
271
272 fn read_smoothing_group(&mut self, _group: SmoothingGroup) {}
274
275 fn read_line_element(&mut self, _indices: &[(usize, Option<usize>)]) {}
277
278 fn read_point_element(&mut self, _indices: &[usize]) {}
280
281 fn read_unknown(&mut self, prefix: &str, _rest: &str, line: usize) -> Result<(), ObjError> {
288 Err(ObjError::Parse {
289 line,
290 kind: ParseErrorKind::UnknownPrefix(prefix.to_string()),
291 })
292 }
293}
294
295pub fn read_obj_file<R: io::Read, T: ObjReader<F>, F: ObjFloat>(
296 reader: R,
297 obj_reader: &mut T,
298) -> Result<(), ObjError>
299where
300 <F as FromStr>::Err: Display,
301{
302 let mut buf_reader = BufReader::new(reader);
303 let mut line = String::new();
304 let mut lineno: usize = 0;
305
306 while buf_reader.read_line(&mut line)? != 0 {
307 lineno += 1;
308 let trimmed = line.trim();
309 if trimmed.is_empty() {
310 line.clear();
311 continue;
312 }
313 let mut parts = trimmed.split_whitespace();
314 let prefix = parts.next().ok_or(ObjError::Parse {
315 line: lineno,
316 kind: ParseErrorKind::EmptyPrefix,
317 })?;
318
319 let parse_f = |s: &str, field: &'static str| -> Result<F, ObjError> {
320 s.parse::<F>().map_err(|_| ObjError::Parse {
321 line: lineno,
322 kind: ParseErrorKind::InvalidNumber {
323 field,
324 value: s.to_string(),
325 },
326 })
327 };
328
329 let parse_index = |s: &str, kind: &'static str| -> Result<usize, ObjError> {
330 let index = s.parse::<usize>().map_err(|_| ObjError::Parse {
331 line: lineno,
332 kind: ParseErrorKind::InvalidIndex {
333 kind,
334 value: s.to_string(),
335 },
336 })?;
337 if index == 0 {
338 return Err(ObjError::Parse {
339 line: lineno,
340 kind: ParseErrorKind::InvalidIndex {
341 kind,
342 value: s.to_string(),
343 },
344 });
345 }
346 Ok(index)
347 };
348
349 let missing = |field: &'static str| -> ObjError {
350 ObjError::Parse {
351 line: lineno,
352 kind: ParseErrorKind::MissingField(field),
353 }
354 };
355
356 match prefix {
357 "#" => {
358 let comment = parts.collect::<Vec<&str>>().join(" ");
359 obj_reader.read_comment(&comment);
360 }
361 "v" => {
362 let x = parts
363 .next()
364 .ok_or_else(|| missing("vertex x"))
365 .and_then(|s| parse_f(s, "vertex x"))?;
366 let y = parts
367 .next()
368 .ok_or_else(|| missing("vertex y"))
369 .and_then(|s| parse_f(s, "vertex y"))?;
370 let z = parts
371 .next()
372 .ok_or_else(|| missing("vertex z"))
373 .and_then(|s| parse_f(s, "vertex z"))?;
374 let w = match parts.next() {
375 Some(s) => Some(parse_f(s, "vertex w")?),
376 None => None,
377 };
378 obj_reader.read_vertex(x, y, z, w);
379 }
380 "vt" => {
381 let u = parts
382 .next()
383 .ok_or_else(|| missing("texture u"))
384 .and_then(|s| parse_f(s, "texture u"))?;
385 let v = match parts.next() {
386 Some(s) => Some(parse_f(s, "texture v")?),
387 None => None,
388 };
389 let w = match parts.next() {
390 Some(s) => Some(parse_f(s, "texture w")?),
391 None => None,
392 };
393 obj_reader.read_texture_coordinate(u, v, w);
394 }
395 "vn" => {
396 let nx = parts
397 .next()
398 .ok_or_else(|| missing("normal nx"))
399 .and_then(|s| parse_f(s, "normal nx"))?;
400 let ny = parts
401 .next()
402 .ok_or_else(|| missing("normal ny"))
403 .and_then(|s| parse_f(s, "normal ny"))?;
404 let nz = parts
405 .next()
406 .ok_or_else(|| missing("normal nz"))
407 .and_then(|s| parse_f(s, "normal nz"))?;
408 obj_reader.read_normal(nx, ny, nz);
409 }
410 "f" => {
411 let mut vertex_indices = Vec::new();
412 for part in parts {
413 let first_slash = part.find('/');
415 let (v_str, rest) = match first_slash {
416 Some(i) => (&part[..i], &part[i + 1..]),
417 None => (part, ""),
418 };
419
420 let v_idx = parse_index(v_str, "vertex")?;
421
422 let (vt_idx, vn_idx) = if rest.is_empty() {
423 (None, None)
424 } else {
425 let second_slash = rest.find('/');
426 if let Some(j) = second_slash {
427 let vt_part = &rest[..j];
428 let vn_part = &rest[j + 1..];
429 let vt = if vt_part.is_empty() {
430 None
431 } else {
432 Some(parse_index(vt_part, "texcoord")?)
433 };
434 let vn = if vn_part.is_empty() {
435 None
436 } else {
437 Some(parse_index(vn_part, "normal")?)
438 };
439 (vt, vn)
440 } else {
441 let vt = if rest.is_empty() {
443 None
444 } else {
445 Some(parse_index(rest, "texcoord")?)
446 };
447 (vt, None)
448 }
449 };
450
451 vertex_indices.push((v_idx, vt_idx, vn_idx));
452 }
453 obj_reader.read_face(&vertex_indices);
454 }
455 "o" => {
456 let name = parts.collect::<Vec<&str>>().join(" ");
457 obj_reader.read_object_name(&name);
458 }
459 "mtllib" => {
460 let names: Vec<&str> = parts.collect();
461 obj_reader.read_material_lib(&names);
462 }
463 "usemtl" => {
464 let name = parts.collect::<Vec<&str>>().join(" ");
467 obj_reader.read_use_material(&name);
468 }
469 "g" => {
470 let names: Vec<&str> = parts.collect();
471 obj_reader.read_group(&names);
472 }
473 "s" => {
474 let value = parts
475 .next()
476 .ok_or_else(|| missing("smoothing group value"))?;
477 let group = if value.eq_ignore_ascii_case("off") {
478 SmoothingGroup::Off
479 } else {
480 let n = value.parse::<u32>().map_err(|_| ObjError::Parse {
481 line: lineno,
482 kind: ParseErrorKind::InvalidSmoothingGroup(value.to_string()),
483 })?;
484 if n == 0 {
485 SmoothingGroup::Off
486 } else {
487 SmoothingGroup::Group(n)
488 }
489 };
490 obj_reader.read_smoothing_group(group);
491 }
492 "l" => {
493 let mut indices: Vec<(usize, Option<usize>)> = Vec::new();
494 for part in parts {
495 let (v_str, vt_str) = match part.find('/') {
496 Some(i) => (&part[..i], Some(&part[i + 1..])),
497 None => (part, None),
498 };
499 let v_idx = parse_index(v_str, "vertex")?;
500 let vt_idx = match vt_str {
501 Some(s) if !s.is_empty() => Some(parse_index(s, "texcoord")?),
502 _ => None,
503 };
504 indices.push((v_idx, vt_idx));
505 }
506 if indices.len() < 2 {
507 return Err(ObjError::Parse {
508 line: lineno,
509 kind: ParseErrorKind::LineElementTooShort,
510 });
511 }
512 obj_reader.read_line_element(&indices);
513 }
514 "p" => {
515 let mut indices: Vec<usize> = Vec::new();
516 for part in parts {
517 indices.push(parse_index(part, "vertex")?);
518 }
519 if indices.is_empty() {
520 return Err(ObjError::Parse {
521 line: lineno,
522 kind: ParseErrorKind::PointElementEmpty,
523 });
524 }
525 obj_reader.read_point_element(&indices);
526 }
527 other => {
528 let rest = parts.collect::<Vec<&str>>().join(" ");
529 obj_reader.read_unknown(other, &rest, lineno)?;
530 }
531 }
532
533 line.clear();
534 }
535
536 Ok(())
537}
538
539pub struct IoObjWriter<W: io::Write, F: ObjFloat = f64> {
540 out: W,
541 line_buf: Vec<u8>,
542 printf_f_format: bool,
544 _phantom: std::marker::PhantomData<F>,
545}
546impl<W: io::Write, F: ObjFloat> IoObjWriter<W, F> {
547 pub fn new(writer: W) -> Self {
549 IoObjWriter {
550 out: writer,
551 line_buf: Vec::with_capacity(256),
552 printf_f_format: false,
553 _phantom: std::marker::PhantomData,
554 }
555 }
556
557 #[cfg(test)]
560 pub fn new_with_printf_f_format(writer: W) -> Self {
561 IoObjWriter {
562 out: writer,
563 line_buf: Vec::with_capacity(256),
564 printf_f_format: true,
565 _phantom: std::marker::PhantomData,
566 }
567 }
568
569 #[cfg(test)]
571 pub fn set_printf_f_format(&mut self, enabled: bool) {
572 self.printf_f_format = enabled;
573 }
574
575 pub fn into_inner(self) -> W {
577 self.out
578 }
579
580 #[inline]
581 fn push_str(&mut self, s: &str) {
582 self.line_buf.extend_from_slice(s.as_bytes());
583 }
584
585 #[inline]
586 fn push_u<T: itoa::Integer>(&mut self, v: T) {
587 let mut buf = itoa::Buffer::new();
588 self.push_str(buf.format(v));
589 }
590
591 #[inline]
592 fn push_f(&mut self, v: F) {
593 if v.is_zero_fract() {
595 self.push_str(&format!("{}", v));
596 return;
597 }
598 if self.printf_f_format {
601 self.push_str(&format!("{:.6}", v));
602 } else {
603 self.push_str(&format!("{}", v));
604 }
605 }
606
607 #[inline]
608 fn flush_line(&mut self) -> io::Result<()> {
609 self.line_buf.push(b'\n');
610 self.out.write_all(&self.line_buf)?;
611 self.line_buf.clear();
612 Ok(())
613 }
614}
615impl<W: io::Write, F: ObjFloat> ObjWriter<F> for IoObjWriter<W, F> {
616 fn write_comment<S: AsRef<str>>(&mut self, comment: S) -> io::Result<()> {
617 self.push_str("# ");
618 self.push_str(comment.as_ref());
619 self.flush_line()
620 }
621
622 fn write_object_name<S: AsRef<str>>(&mut self, name: S) -> io::Result<()> {
623 self.push_str("o ");
624 self.push_str(name.as_ref());
625 self.flush_line()
626 }
627
628 fn write_vertex(&mut self, x: F, y: F, z: F, w: Option<F>) -> io::Result<()> {
629 self.push_str("v ");
630 self.push_f(x);
631 self.push_str(" ");
632 self.push_f(y);
633 self.push_str(" ");
634 self.push_f(z);
635 if let Some(wv) = w {
636 self.push_str(" ");
637 self.push_f(wv);
638 }
639 self.flush_line()
640 }
641
642 fn write_texture_coordinate(&mut self, u: F, v: Option<F>, w: Option<F>) -> io::Result<()> {
643 self.push_str("vt ");
644 self.push_f(u);
645 if let Some(vv) = v {
646 self.push_str(" ");
647 self.push_f(vv);
648 if let Some(wv) = w {
649 self.push_str(" ");
650 self.push_f(wv);
651 }
652 }
653 self.flush_line()
654 }
655
656 fn write_normal(&mut self, nx: F, ny: F, nz: F) -> io::Result<()> {
657 self.push_str("vn ");
658 self.push_f(nx);
659 self.push_str(" ");
660 self.push_f(ny);
661 self.push_str(" ");
662 self.push_f(nz);
663 self.flush_line()
664 }
665
666 fn write_face(
667 &mut self,
668 vertex_indices: &[(usize, Option<usize>, Option<usize>)],
669 ) -> io::Result<()> {
670 self.push_str("f");
672 for (v_idx, vt_idx, vn_idx) in vertex_indices.iter() {
673 self.push_str(" ");
674 self.push_u(*v_idx);
676 match (vt_idx, vn_idx) {
677 (None, None) => {}
678 (Some(vt), None) => {
679 self.push_str("/");
680 self.push_u(*vt);
681 }
682 (None, Some(vn)) => {
683 self.push_str("//");
684 self.push_u(*vn);
685 }
686 (Some(vt), Some(vn)) => {
687 self.push_str("/");
688 self.push_u(*vt);
689 self.push_str("/");
690 self.push_u(*vn);
691 }
692 }
693 }
694 self.flush_line()
695 }
696
697 fn write_material_lib<S: AsRef<str>>(&mut self, names: &[S]) -> io::Result<()> {
698 self.push_str("mtllib");
699 for name in names {
700 self.push_str(" ");
701 self.push_str(name.as_ref());
702 }
703 self.flush_line()
704 }
705
706 fn write_use_material<S: AsRef<str>>(&mut self, name: S) -> io::Result<()> {
707 self.push_str("usemtl ");
708 self.push_str(name.as_ref());
709 self.flush_line()
710 }
711
712 fn write_group<S: AsRef<str>>(&mut self, names: &[S]) -> io::Result<()> {
713 self.push_str("g");
714 for name in names {
715 self.push_str(" ");
716 self.push_str(name.as_ref());
717 }
718 self.flush_line()
719 }
720
721 fn write_smoothing_group(&mut self, group: SmoothingGroup) -> io::Result<()> {
722 match group {
723 SmoothingGroup::Off => self.push_str("s off"),
724 SmoothingGroup::Group(n) => {
725 self.push_str("s ");
726 self.push_u(n);
727 }
728 }
729 self.flush_line()
730 }
731
732 fn write_line_element(&mut self, indices: &[(usize, Option<usize>)]) -> io::Result<()> {
733 self.push_str("l");
734 for (v_idx, vt_idx) in indices.iter() {
735 self.push_str(" ");
736 self.push_u(*v_idx);
737 if let Some(vt) = vt_idx {
738 self.push_str("/");
739 self.push_u(*vt);
740 }
741 }
742 self.flush_line()
743 }
744
745 fn write_point_element(&mut self, indices: &[usize]) -> io::Result<()> {
746 self.push_str("p");
747 for v_idx in indices.iter() {
748 self.push_str(" ");
749 self.push_u(*v_idx);
750 }
751 self.flush_line()
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758 use pretty_assertions::assert_eq;
759 use std::io::Cursor;
760
761 type Face = Vec<(usize, Option<usize>, Option<usize>)>;
762
763 #[derive(Default)]
764 struct TestObjReader64 {
765 comments: Vec<String>,
766 names: Vec<String>,
767 vertices: Vec<(f64, f64, f64, Option<f64>)>,
768 texture_coordinates: Vec<(f64, Option<f64>, Option<f64>)>,
769 normals: Vec<(f64, f64, f64)>,
770 faces: Vec<Face>,
771 }
772
773 impl ObjReader for TestObjReader64 {
774 fn read_comment(&mut self, comment: &str) {
775 self.comments.push(comment.to_string());
776 }
777
778 fn read_object_name(&mut self, name: &str) {
779 self.names.push(name.to_string());
780 }
781
782 fn read_vertex(&mut self, x: f64, y: f64, z: f64, w: Option<f64>) {
783 self.vertices.push((x, y, z, w));
784 }
785
786 fn read_texture_coordinate(&mut self, u: f64, v: Option<f64>, w: Option<f64>) {
787 self.texture_coordinates.push((u, v, w));
788 }
789
790 fn read_normal(&mut self, nx: f64, ny: f64, nz: f64) {
791 self.normals.push((nx, ny, nz));
792 }
793
794 fn read_face(&mut self, vertex_indices: &[(usize, Option<usize>, Option<usize>)]) {
795 self.faces.push(vertex_indices.to_vec());
796 }
797 }
798
799 #[test]
800 fn test_obj_reading_2() {
801 let input = "# This is a test OBJ file
802o TestObject
803v 1 2 3
804vt 0.5 0.5
805vn 0 1 1.1
806f 1/1/1 2/2/2 3/3/3
807";
808
809 let reader = Cursor::new(input);
810 let mut test_reader: TestObjReader64 = Default::default();
811 read_obj_file(reader, &mut test_reader).unwrap();
812 assert_eq!(test_reader.comments, vec!["This is a test OBJ file"]);
813 assert_eq!(test_reader.names, vec!["TestObject"]);
814 assert_eq!(test_reader.vertices, vec![(1.0, 2.0, 3.0, None)]);
815 assert_eq!(
816 test_reader.texture_coordinates,
817 vec![(0.5, Some(0.5), None)]
818 );
819 assert_eq!(test_reader.normals, vec![(0.0, 1.0, 1.1)]);
820 assert_eq!(
821 test_reader.faces,
822 vec![vec![
823 (1, Some(1), Some(1)),
824 (2, Some(2), Some(2)),
825 (3, Some(3), Some(3))
826 ]]
827 );
828 }
829
830 #[test]
831 fn test_obj_writing() {
832 let mut buffer = Vec::new();
833 let mut writer = IoObjWriter::new(&mut buffer);
834 writer.write_comment("This is a test OBJ file").unwrap();
835 writer.write_object_name("TestObject").unwrap();
836 writer.write_vertex(1.0, 2.0, 3.0, None).unwrap();
837 writer
838 .write_texture_coordinate(0.5, Some(0.5), None)
839 .unwrap();
840 writer.write_normal(0.0, 1.0, 1.1).unwrap();
841 writer
842 .write_face(&[
843 (1, Some(1), Some(1)),
844 (2, Some(2), Some(2)),
845 (3, Some(3), Some(3)),
846 ])
847 .unwrap();
848
849 let output = String::from_utf8(buffer).unwrap();
850 let expected_output = "# This is a test OBJ file
851o TestObject
852v 1 2 3
853vt 0.5 0.5
854vn 0 1 1.1
855f 1/1/1 2/2/2 3/3/3
856";
857 assert_eq!(output, expected_output);
858 }
859
860 #[derive(Default)]
863 struct TestObjReader32 {
864 vertices: Vec<(f32, f32, f32, Option<f32>)>,
865 texture_coordinates: Vec<(f32, Option<f32>, Option<f32>)>,
866 normals: Vec<(f32, f32, f32)>,
867 }
868
869 impl ObjReader<f32> for TestObjReader32 {
870 fn read_comment(&mut self, _comment: &str) {}
871 fn read_object_name(&mut self, _name: &str) {}
872
873 fn read_vertex(&mut self, x: f32, y: f32, z: f32, w: Option<f32>) {
874 self.vertices.push((x, y, z, w));
875 }
876
877 fn read_texture_coordinate(&mut self, u: f32, v: Option<f32>, w: Option<f32>) {
878 self.texture_coordinates.push((u, v, w));
879 }
880
881 fn read_normal(&mut self, nx: f32, ny: f32, nz: f32) {
882 self.normals.push((nx, ny, nz));
883 }
884
885 fn read_face(&mut self, _vertex_indices: &[(usize, Option<usize>, Option<usize>)]) {}
886 }
887
888 #[test]
889 fn test_obj_f32_reading() {
890 let input = "o TestObject
891v 1.5 2.5 3.5
892vt 0.25 0.75
893vn 0.0 1.0 0.0
894";
895
896 let reader = Cursor::new(input);
897 let mut test_reader = TestObjReader32::default();
898 read_obj_file(reader, &mut test_reader).unwrap();
899
900 assert_eq!(test_reader.vertices, vec![(1.5f32, 2.5f32, 3.5f32, None)]);
901 assert_eq!(
902 test_reader.texture_coordinates,
903 vec![(0.25f32, Some(0.75f32), None)]
904 );
905 assert_eq!(test_reader.normals, vec![(0.0f32, 1.0f32, 0.0f32)]);
906 }
907
908 #[test]
909 fn test_obj_f32_writing() {
910 let mut buffer = Vec::new();
911 let mut writer: IoObjWriter<_, f32> = IoObjWriter::new(&mut buffer);
912
913 writer.write_comment("f32 test").unwrap();
914 writer.write_object_name("F32Object").unwrap();
915 writer.write_vertex(1.5f32, 2.5f32, 3.5f32, None).unwrap();
916 writer
917 .write_texture_coordinate(0.25f32, Some(0.75f32), None)
918 .unwrap();
919 writer.write_normal(0.0f32, 1.0f32, 0.0f32).unwrap();
920
921 let output = String::from_utf8(buffer).unwrap();
922 let expected = "# f32 test
923o F32Object
924v 1.5 2.5 3.5
925vt 0.25 0.75
926vn 0 1 0
927";
928 assert_eq!(output, expected);
929 }
930
931 #[test]
932 fn test_obj_f32_round_trip() {
933 let input = "o Test
935v 0.123456789 0.987654321 1.5
936vn 0.577 0.577 0.577
937";
938
939 let reader = Cursor::new(input);
940 let mut test_reader = TestObjReader32::default();
941 read_obj_file(reader, &mut test_reader).unwrap();
942
943 let mut buffer = Vec::new();
945 let mut writer: IoObjWriter<_, f32> = IoObjWriter::new(&mut buffer);
946 writer.write_object_name("Test").unwrap();
947 for (x, y, z, w) in &test_reader.vertices {
948 writer.write_vertex(*x, *y, *z, *w).unwrap();
949 }
950 for (nx, ny, nz) in &test_reader.normals {
951 writer.write_normal(*nx, *ny, *nz).unwrap();
952 }
953
954 let output = String::from_utf8(buffer).unwrap();
955
956 assert!(output.contains("o Test"));
958 assert!(output.contains("v "));
959 assert!(output.contains("vn "));
960 }
961
962 #[test]
963 fn test_printf_f_formatting() {
964 let mut buffer = Vec::new();
965 let mut writer: IoObjWriter<_, f64> = IoObjWriter::new_with_printf_f_format(&mut buffer);
966
967 writer.write_object_name("PrintfFTest").unwrap();
968 writer
969 .write_vertex(754.4214477539063, 1753.2353515625, -91.72238159179688, None)
970 .unwrap();
971 writer
972 .write_texture_coordinate(0.123456789, Some(0.987654321), None)
973 .unwrap();
974 writer
975 .write_normal(0.5773502691896257, 0.5773502691896257, 0.5773502691896257)
976 .unwrap();
977
978 let output = String::from_utf8(buffer).unwrap();
979
980 let expected = "o PrintfFTest
982v 754.421448 1753.235352 -91.722382
983vt 0.123457 0.987654
984vn 0.577350 0.577350 0.577350
985";
986 assert_eq!(output, expected);
987 }
988
989 #[test]
990 fn test_printf_f_flag_toggle() {
991 let mut buffer = Vec::new();
993 let mut writer: IoObjWriter<_, f32> = IoObjWriter::new(&mut buffer);
994
995 writer
997 .write_vertex(1.2345678_f32, 2.345678_f32, 3.45678_f32, None)
998 .unwrap();
999
1000 writer.set_printf_f_format(true);
1002 writer
1003 .write_vertex(1.2345678_f32, 2.3456789_f32, 3.456789_f32, None)
1004 .unwrap();
1005
1006 let output = String::from_utf8(buffer).unwrap();
1007
1008 let lines: Vec<&str> = output.lines().collect();
1010 assert_eq!(lines.len(), 2);
1011
1012 assert_eq!(lines[0], "v 1.2345678 2.345678 3.45678");
1014
1015 assert_eq!(lines[1], "v 1.234568 2.345679 3.456789");
1017 }
1018
1019 #[derive(Default, Debug, PartialEq)]
1024 struct ExtendedReader32 {
1025 material_libs: Vec<Vec<String>>,
1026 use_materials: Vec<String>,
1027 groups: Vec<Vec<String>>,
1028 smoothing_groups: Vec<SmoothingGroup>,
1029 line_elements: Vec<Vec<(usize, Option<usize>)>>,
1030 point_elements: Vec<Vec<usize>>,
1031 }
1032
1033 impl ObjReader<f32> for ExtendedReader32 {
1034 fn read_comment(&mut self, _: &str) {}
1035 fn read_object_name(&mut self, _: &str) {}
1036 fn read_vertex(&mut self, _: f32, _: f32, _: f32, _: Option<f32>) {}
1037 fn read_texture_coordinate(&mut self, _: f32, _: Option<f32>, _: Option<f32>) {}
1038 fn read_normal(&mut self, _: f32, _: f32, _: f32) {}
1039 fn read_face(&mut self, _: &[(usize, Option<usize>, Option<usize>)]) {}
1040
1041 fn read_material_lib(&mut self, names: &[&str]) {
1042 self.material_libs
1043 .push(names.iter().map(|s| s.to_string()).collect());
1044 }
1045 fn read_use_material(&mut self, name: &str) {
1046 self.use_materials.push(name.to_string());
1047 }
1048 fn read_group(&mut self, names: &[&str]) {
1049 self.groups
1050 .push(names.iter().map(|s| s.to_string()).collect());
1051 }
1052 fn read_smoothing_group(&mut self, group: SmoothingGroup) {
1053 self.smoothing_groups.push(group);
1054 }
1055 fn read_line_element(&mut self, indices: &[(usize, Option<usize>)]) {
1056 self.line_elements.push(indices.to_vec());
1057 }
1058 fn read_point_element(&mut self, indices: &[usize]) {
1059 self.point_elements.push(indices.to_vec());
1060 }
1061 }
1062
1063 #[test]
1064 fn read_mtllib_and_usemtl() {
1065 let input = "mtllib first.mtl second.mtl
1066usemtl SomeMaterial
1067";
1068 let mut reader = ExtendedReader32::default();
1069 read_obj_file(Cursor::new(input), &mut reader).unwrap();
1070 assert_eq!(
1071 reader.material_libs,
1072 vec![vec!["first.mtl".to_string(), "second.mtl".to_string()]]
1073 );
1074 assert_eq!(reader.use_materials, vec!["SomeMaterial".to_string()]);
1075 }
1076
1077 #[test]
1078 fn read_group_with_multiple_names() {
1079 let input = "g cube top
1080g default
1081";
1082 let mut reader = ExtendedReader32::default();
1083 read_obj_file(Cursor::new(input), &mut reader).unwrap();
1084 assert_eq!(
1085 reader.groups,
1086 vec![
1087 vec!["cube".to_string(), "top".to_string()],
1088 vec!["default".to_string()],
1089 ]
1090 );
1091 }
1092
1093 #[test]
1094 fn read_smoothing_group_off_zero_and_named() {
1095 let input = "s off
1096s 0
1097s 1
1098s 42
1099";
1100 let mut reader = ExtendedReader32::default();
1101 read_obj_file(Cursor::new(input), &mut reader).unwrap();
1102 assert_eq!(
1103 reader.smoothing_groups,
1104 vec![
1105 SmoothingGroup::Off,
1106 SmoothingGroup::Off,
1107 SmoothingGroup::Group(1),
1108 SmoothingGroup::Group(42),
1109 ]
1110 );
1111 }
1112
1113 #[test]
1114 fn read_smoothing_group_invalid_value_errors() {
1115 let input = "s notanumber\n";
1116 let mut reader = ExtendedReader32::default();
1117 let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1118 assert!(
1119 err.to_string().contains("invalid smoothing group"),
1120 "got: {}",
1121 err
1122 );
1123 }
1124
1125 #[test]
1126 fn read_line_and_point_elements() {
1127 let input = "l 1 2 3
1128l 4/1 5/2 6/3
1129p 1 2 3 4
1130";
1131 let mut reader = ExtendedReader32::default();
1132 read_obj_file(Cursor::new(input), &mut reader).unwrap();
1133 assert_eq!(
1134 reader.line_elements,
1135 vec![
1136 vec![(1, None), (2, None), (3, None)],
1137 vec![(4, Some(1)), (5, Some(2)), (6, Some(3))],
1138 ]
1139 );
1140 assert_eq!(reader.point_elements, vec![vec![1, 2, 3, 4]]);
1141 }
1142
1143 #[test]
1144 fn read_line_element_with_one_vertex_errors() {
1145 let mut reader = ExtendedReader32::default();
1146 let err = read_obj_file(Cursor::new("l 1\n"), &mut reader).unwrap_err();
1147 assert!(
1148 err.to_string().contains("at least 2 vertices"),
1149 "got: {}",
1150 err
1151 );
1152 }
1153
1154 #[test]
1155 fn write_directive_round_trip() {
1156 let mut buffer = Vec::new();
1158 {
1159 let mut writer: IoObjWriter<_, f32> = IoObjWriter::new(&mut buffer);
1160 writer
1161 .write_material_lib(&["lib1.mtl", "lib2.mtl"])
1162 .unwrap();
1163 writer.write_use_material("Wood").unwrap();
1164 writer.write_group(&["cube", "top"]).unwrap();
1165 writer.write_smoothing_group(SmoothingGroup::Off).unwrap();
1166 writer
1167 .write_smoothing_group(SmoothingGroup::Group(7))
1168 .unwrap();
1169 writer
1170 .write_line_element(&[(1, None), (2, Some(2)), (3, None)])
1171 .unwrap();
1172 writer.write_point_element(&[1, 2, 3]).unwrap();
1173 }
1174 let text = String::from_utf8(buffer.clone()).unwrap();
1175 let expected = "mtllib lib1.mtl lib2.mtl
1176usemtl Wood
1177g cube top
1178s off
1179s 7
1180l 1 2/2 3
1181p 1 2 3
1182";
1183 assert_eq!(text, expected);
1184
1185 let mut reader = ExtendedReader32::default();
1186 read_obj_file(Cursor::new(buffer), &mut reader).unwrap();
1187 assert_eq!(
1188 reader.material_libs,
1189 vec![vec!["lib1.mtl".to_string(), "lib2.mtl".to_string()]]
1190 );
1191 assert_eq!(reader.use_materials, vec!["Wood".to_string()]);
1192 assert_eq!(
1193 reader.groups,
1194 vec![vec!["cube".to_string(), "top".to_string()]]
1195 );
1196 assert_eq!(
1197 reader.smoothing_groups,
1198 vec![SmoothingGroup::Off, SmoothingGroup::Group(7)]
1199 );
1200 assert_eq!(
1201 reader.line_elements,
1202 vec![vec![(1, None), (2, Some(2)), (3, None)]]
1203 );
1204 assert_eq!(reader.point_elements, vec![vec![1, 2, 3]]);
1205 }
1206
1207 #[test]
1208 fn unknown_prefix_still_errors_by_default() {
1209 let input = "vp 0.1 0.2 0.3\n";
1212 let mut reader = ExtendedReader32::default();
1213 let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1214 assert!(
1215 err.to_string().contains("Unknown line prefix: vp"),
1216 "got: {}",
1217 err
1218 );
1219 }
1220
1221 #[test]
1225 fn typed_error_unknown_prefix_carries_prefix_and_line() {
1226 let input = "v 0 0 0\nvp 0.1 0.2\n";
1227 let mut reader = ExtendedReader32::default();
1228 let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1229 match err {
1230 ObjError::Parse {
1231 line,
1232 kind: ParseErrorKind::UnknownPrefix(prefix),
1233 } => {
1234 assert_eq!(line, 2);
1235 assert_eq!(prefix, "vp");
1236 }
1237 other => panic!("wrong error variant: {:?}", other),
1238 }
1239 }
1240
1241 #[test]
1242 fn typed_error_invalid_number_carries_field_and_value() {
1243 let input = "v 1.0 nope 3.0\n";
1244 let mut reader = ExtendedReader32::default();
1245 let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1246 match err {
1247 ObjError::Parse {
1248 line: 1,
1249 kind: ParseErrorKind::InvalidNumber { field, value },
1250 } => {
1251 assert_eq!(field, "vertex y");
1252 assert_eq!(value, "nope");
1253 }
1254 other => panic!("wrong error variant: {:?}", other),
1255 }
1256 }
1257
1258 #[test]
1259 fn typed_error_invalid_index_for_face() {
1260 let input = "v 0 0 0\nf 0/0/0 0/0/0 0/0/0\n";
1262 let mut reader = ExtendedReader32::default();
1263 let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1264 match err {
1265 ObjError::Parse {
1266 line: 2,
1267 kind: ParseErrorKind::InvalidIndex { kind, value },
1268 } => {
1269 assert_eq!(kind, "vertex");
1270 assert_eq!(value, "0");
1271 }
1272 other => panic!("wrong error variant: {:?}", other),
1273 }
1274 }
1275
1276 #[test]
1277 fn typed_error_invalid_smoothing_group() {
1278 let input = "s notanumber\n";
1279 let mut reader = ExtendedReader32::default();
1280 let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1281 match err {
1282 ObjError::Parse {
1283 line: 1,
1284 kind: ParseErrorKind::InvalidSmoothingGroup(value),
1285 } => assert_eq!(value, "notanumber"),
1286 other => panic!("wrong error variant: {:?}", other),
1287 }
1288 }
1289
1290 #[test]
1291 fn typed_error_missing_field() {
1292 let input = "v 1.0 2.0\n";
1293 let mut reader = ExtendedReader32::default();
1294 let err = read_obj_file(Cursor::new(input), &mut reader).unwrap_err();
1295 match err {
1296 ObjError::Parse {
1297 line: 1,
1298 kind: ParseErrorKind::MissingField(field),
1299 } => assert_eq!(field, "vertex z"),
1300 other => panic!("wrong error variant: {:?}", other),
1301 }
1302 }
1303
1304 #[test]
1305 fn typed_error_converts_to_io_error() {
1306 fn legacy_caller<R: io::Read>(input: R) -> io::Result<()> {
1309 let mut reader = ExtendedReader32::default();
1310 read_obj_file(input, &mut reader)?;
1311 Ok(())
1312 }
1313
1314 let err = legacy_caller(Cursor::new("vp 0.1 0.2\n")).unwrap_err();
1315 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1316 assert!(err.to_string().contains("Unknown line prefix: vp"));
1317 }
1318
1319 #[test]
1320 fn typed_error_io_failure_propagates() {
1321 struct FailingRead;
1323 impl io::Read for FailingRead {
1324 fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
1325 Err(io::Error::other("disk on fire"))
1326 }
1327 }
1328
1329 let mut reader = ExtendedReader32::default();
1330 let err = read_obj_file(FailingRead, &mut reader).unwrap_err();
1331 match err {
1332 ObjError::Io(inner) => {
1333 assert_eq!(inner.to_string(), "disk on fire");
1334 }
1335 other => panic!("wrong error variant: {:?}", other),
1336 }
1337 }
1338
1339 #[test]
1340 fn custom_read_unknown_can_return_objerror() {
1341 struct StrictCustom;
1343 impl ObjReader<f32> for StrictCustom {
1344 fn read_comment(&mut self, _: &str) {}
1345 fn read_object_name(&mut self, _: &str) {}
1346 fn read_vertex(&mut self, _: f32, _: f32, _: f32, _: Option<f32>) {}
1347 fn read_texture_coordinate(&mut self, _: f32, _: Option<f32>, _: Option<f32>) {}
1348 fn read_normal(&mut self, _: f32, _: f32, _: f32) {}
1349 fn read_face(&mut self, _: &[(usize, Option<usize>, Option<usize>)]) {}
1350 fn read_unknown(&mut self, prefix: &str, _: &str, line: usize) -> Result<(), ObjError> {
1351 Err(ObjError::Parse {
1352 line,
1353 kind: ParseErrorKind::Custom(format!("nope: {prefix}")),
1354 })
1355 }
1356 }
1357
1358 let err = read_obj_file(Cursor::new("vp 0 0\n"), &mut StrictCustom).unwrap_err();
1359 match err {
1360 ObjError::Parse {
1361 line: 1,
1362 kind: ParseErrorKind::Custom(msg),
1363 } => assert_eq!(msg, "nope: vp"),
1364 other => panic!("wrong error variant: {:?}", other),
1365 }
1366 }
1367}