1use crate::{MeshReader, MeshWriter};
12use threecrate_core::{TriangleMesh, Result, Point3f, Vector3f, Error};
13use std::path::{Path, PathBuf};
14use std::fs::File;
15use std::io::{BufRead, BufReader, BufWriter, Write};
16use std::collections::HashMap;
17
18#[derive(Debug, Clone)]
20pub struct Material {
21 pub name: String,
23 pub ambient: Option<[f32; 3]>,
25 pub diffuse: Option<[f32; 3]>,
27 pub specular: Option<[f32; 3]>,
29 pub shininess: Option<f32>,
31 pub transparency: Option<f32>,
33 pub illumination: Option<u32>,
35 pub diffuse_map: Option<String>,
37 pub normal_map: Option<String>,
39 pub specular_map: Option<String>,
41}
42
43#[derive(Debug, Clone, Copy)]
45pub struct FaceVertex {
46 pub vertex: usize,
48 pub texture: Option<usize>,
50 pub normal: Option<usize>,
52}
53
54#[derive(Debug, Clone)]
56pub struct Face {
57 pub vertices: Vec<FaceVertex>,
59 pub material: Option<String>,
61}
62
63#[derive(Debug, Clone)]
65pub struct Group {
66 pub name: String,
68 pub faces: Vec<Face>,
70}
71
72#[derive(Debug)]
74pub struct ObjData {
75 pub vertices: Vec<Point3f>,
77 pub texture_coords: Vec<[f32; 2]>,
79 pub normals: Vec<Vector3f>,
81 pub groups: Vec<Group>,
83 pub materials: HashMap<String, Material>,
85 pub mtl_files: Vec<String>,
87}
88
89pub struct RobustObjReader;
91
92pub struct ObjReader;
94
95#[derive(Debug, Clone)]
97pub struct ObjWriteOptions {
98 pub write_normals: bool,
100 pub write_texcoords: bool,
102 pub write_materials: bool,
104 pub comments: Vec<String>,
106 pub object_name: Option<String>,
108 pub group_name: Option<String>,
110 pub material_name: Option<String>,
112 pub mtl_filename: Option<String>,
114}
115
116impl Default for ObjWriteOptions {
117 fn default() -> Self {
118 Self {
119 write_normals: true,
120 write_texcoords: false,
121 write_materials: false,
122 comments: vec!["Generated by ThreeCrate".to_string()],
123 object_name: None,
124 group_name: None,
125 material_name: None,
126 mtl_filename: None,
127 }
128 }
129}
130
131impl ObjWriteOptions {
132 pub fn new() -> Self {
134 Self::default()
135 }
136
137 pub fn with_normals(mut self, write_normals: bool) -> Self {
139 self.write_normals = write_normals;
140 self
141 }
142
143 pub fn with_texcoords(mut self, write_texcoords: bool) -> Self {
145 self.write_texcoords = write_texcoords;
146 self
147 }
148
149 pub fn with_materials(mut self, write_materials: bool) -> Self {
151 self.write_materials = write_materials;
152 self
153 }
154
155 pub fn with_comment<S: Into<String>>(mut self, comment: S) -> Self {
157 self.comments.push(comment.into());
158 self
159 }
160
161 pub fn with_object_name<S: Into<String>>(mut self, name: S) -> Self {
163 self.object_name = Some(name.into());
164 self
165 }
166
167 pub fn with_group_name<S: Into<String>>(mut self, name: S) -> Self {
169 self.group_name = Some(name.into());
170 self
171 }
172
173 pub fn with_material_name<S: Into<String>>(mut self, name: S) -> Self {
175 self.material_name = Some(name.into());
176 self
177 }
178
179 pub fn with_mtl_filename<S: Into<String>>(mut self, filename: S) -> Self {
181 self.mtl_filename = Some(filename.into());
182 self
183 }
184}
185
186pub struct RobustObjWriter;
188
189pub struct ObjWriter;
191
192impl Material {
193 pub fn new(name: String) -> Self {
195 Self {
196 name,
197 ambient: None,
198 diffuse: None,
199 specular: None,
200 shininess: None,
201 transparency: None,
202 illumination: None,
203 diffuse_map: None,
204 normal_map: None,
205 specular_map: None,
206 }
207 }
208}
209
210impl Default for Material {
211 fn default() -> Self {
212 Self::new("default".to_string())
213 }
214}
215
216impl FaceVertex {
217 fn parse(s: &str) -> Result<Self> {
219 let parts: Vec<&str> = s.split('/').collect();
220
221 if parts.is_empty() {
222 return Err(Error::InvalidData("Empty face vertex".to_string()));
223 }
224
225 let vertex = parts[0].parse::<usize>()
226 .map_err(|_| Error::InvalidData(format!("Invalid vertex index: {}", parts[0])))?;
227
228 let vertex = vertex.checked_sub(1)
230 .ok_or_else(|| Error::InvalidData("Vertex index cannot be 0".to_string()))?;
231
232 let texture = if parts.len() > 1 && !parts[1].is_empty() {
233 let tex_idx = parts[1].parse::<usize>()
234 .map_err(|_| Error::InvalidData(format!("Invalid texture index: {}", parts[1])))?;
235 Some(tex_idx.checked_sub(1)
236 .ok_or_else(|| Error::InvalidData("Texture index cannot be 0".to_string()))?)
237 } else {
238 None
239 };
240
241 let normal = if parts.len() > 2 && !parts[2].is_empty() {
242 let norm_idx = parts[2].parse::<usize>()
243 .map_err(|_| Error::InvalidData(format!("Invalid normal index: {}", parts[2])))?;
244 Some(norm_idx.checked_sub(1)
245 .ok_or_else(|| Error::InvalidData("Normal index cannot be 0".to_string()))?)
246 } else {
247 None
248 };
249
250 Ok(FaceVertex { vertex, texture, normal })
251 }
252}
253
254impl crate::registry::MeshReader for ObjReader {
255 fn read_mesh(&self, path: &Path) -> Result<TriangleMesh> {
256 let obj_data = RobustObjReader::read_obj_file(path)?;
257 RobustObjReader::obj_data_to_mesh(&obj_data)
258 }
259
260 fn can_read(&self, path: &Path) -> bool {
261 if let Ok(file) = File::open(path) {
263 let mut reader = BufReader::new(file);
264 let mut line = String::new();
265 if let Ok(_) = reader.read_line(&mut line) {
266 let trimmed = line.trim();
267 return trimmed.starts_with("#") || trimmed.starts_with("v ");
268 }
269 }
270 false
271 }
272
273 fn format_name(&self) -> &'static str {
274 "obj"
275 }
276}
277
278impl crate::registry::MeshWriter for ObjWriter {
279 fn write_mesh(&self, mesh: &TriangleMesh, path: &Path) -> Result<()> {
280 let file = File::create(path)?;
281 let mut writer = BufWriter::new(file);
282
283 writeln!(writer, "# OBJ file generated by ThreeCrate")?;
285 writeln!(writer, "# Vertices: {}", mesh.vertices.len())?;
286 writeln!(writer, "# Faces: {}", mesh.faces.len())?;
287 writeln!(writer)?;
288
289 for vertex in &mesh.vertices {
291 writeln!(writer, "v {} {} {}", vertex.x, vertex.y, vertex.z)?;
292 }
293 writeln!(writer)?;
294
295 if let Some(normals) = &mesh.normals {
297 for normal in normals {
298 writeln!(writer, "vn {} {} {}", normal.x, normal.y, normal.z)?;
299 }
300 writeln!(writer)?;
301 }
302
303 if mesh.normals.is_some() {
305 for face in &mesh.faces {
307 writeln!(
308 writer,
309 "f {}//{} {}//{} {}//{}",
310 face[0] + 1, face[0] + 1,
311 face[1] + 1, face[1] + 1,
312 face[2] + 1, face[2] + 1
313 )?;
314 }
315 } else {
316 for face in &mesh.faces {
318 writeln!(
319 writer,
320 "f {} {} {}",
321 face[0] + 1,
322 face[1] + 1,
323 face[2] + 1
324 )?;
325 }
326 }
327
328 Ok(())
329 }
330
331 fn format_name(&self) -> &'static str {
332 "obj"
333 }
334}
335
336impl MeshReader for ObjReader {
338 fn read_mesh<P: AsRef<Path>>(path: P) -> Result<TriangleMesh> {
339 let reader = ObjReader;
340 crate::registry::MeshReader::read_mesh(&reader, path.as_ref())
341 }
342}
343
344impl MeshWriter for ObjWriter {
345 fn write_mesh<P: AsRef<Path>>(mesh: &TriangleMesh, path: P) -> Result<()> {
346 let writer = ObjWriter;
347 crate::registry::MeshWriter::write_mesh(&writer, mesh, path.as_ref())
348 }
349}
350
351pub fn read_obj_vertices<P: AsRef<Path>>(path: P) -> Result<Vec<Point3f>> {
353 let obj_data = RobustObjReader::read_obj_file(path)?;
354 Ok(obj_data.vertices)
355}
356
357pub fn write_obj_vertices<P: AsRef<Path>>(vertices: &[Point3f], path: P) -> Result<()> {
359 let file = File::create(path)?;
360 let mut writer = BufWriter::new(file);
361
362 writeln!(writer, "# OBJ vertices file generated by ThreeCrate")?;
363 writeln!(writer, "# Vertices: {}", vertices.len())?;
364 writeln!(writer)?;
365
366 for vertex in vertices {
367 writeln!(writer, "v {} {} {}", vertex.x, vertex.y, vertex.z)?;
368 }
369
370 Ok(())
371}
372
373impl RobustObjReader {
374 pub fn read_obj_file<P: AsRef<Path>>(path: P) -> Result<ObjData> {
376 let path = path.as_ref();
377 let file = File::open(path)
378 .map_err(|e| Error::Io(e))?;
379 let reader = BufReader::new(file);
380
381 let mut obj_data = ObjData {
382 vertices: Vec::new(),
383 texture_coords: Vec::new(),
384 normals: Vec::new(),
385 groups: Vec::new(),
386 materials: HashMap::new(),
387 mtl_files: Vec::new(),
388 };
389
390 let mut current_group = Group {
391 name: "default".to_string(),
392 faces: Vec::new(),
393 };
394 let mut current_material: Option<String> = None;
395
396 for (line_num, line) in reader.lines().enumerate() {
397 let line = line.map_err(|e| Error::Io(e))?;
398 let line = line.trim();
399
400 if line.is_empty() || line.starts_with('#') {
402 continue;
403 }
404
405 let parts: Vec<&str> = line.split_whitespace().collect();
406 if parts.is_empty() {
407 continue;
408 }
409
410 match parts[0] {
411 "v" => {
412 if parts.len() < 4 {
414 return Err(Error::InvalidData(
415 format!("Invalid vertex at line {}: expected 3 coordinates", line_num + 1)
416 ));
417 }
418
419 let x = parts[1].parse::<f32>()
420 .map_err(|_| Error::InvalidData(format!("Invalid x coordinate at line {}", line_num + 1)))?;
421 let y = parts[2].parse::<f32>()
422 .map_err(|_| Error::InvalidData(format!("Invalid y coordinate at line {}", line_num + 1)))?;
423 let z = parts[3].parse::<f32>()
424 .map_err(|_| Error::InvalidData(format!("Invalid z coordinate at line {}", line_num + 1)))?;
425
426 obj_data.vertices.push(Point3f::new(x, y, z));
427 }
428 "vt" => {
429 if parts.len() < 3 {
431 return Err(Error::InvalidData(
432 format!("Invalid texture coordinate at line {}: expected 2 coordinates", line_num + 1)
433 ));
434 }
435
436 let u = parts[1].parse::<f32>()
437 .map_err(|_| Error::InvalidData(format!("Invalid u coordinate at line {}", line_num + 1)))?;
438 let v = parts[2].parse::<f32>()
439 .map_err(|_| Error::InvalidData(format!("Invalid v coordinate at line {}", line_num + 1)))?;
440
441 obj_data.texture_coords.push([u, v]);
442 }
443 "vn" => {
444 if parts.len() < 4 {
446 return Err(Error::InvalidData(
447 format!("Invalid normal at line {}: expected 3 components", line_num + 1)
448 ));
449 }
450
451 let x = parts[1].parse::<f32>()
452 .map_err(|_| Error::InvalidData(format!("Invalid normal x at line {}", line_num + 1)))?;
453 let y = parts[2].parse::<f32>()
454 .map_err(|_| Error::InvalidData(format!("Invalid normal y at line {}", line_num + 1)))?;
455 let z = parts[3].parse::<f32>()
456 .map_err(|_| Error::InvalidData(format!("Invalid normal z at line {}", line_num + 1)))?;
457
458 obj_data.normals.push(Vector3f::new(x, y, z));
459 }
460 "f" => {
461 if parts.len() < 4 {
463 return Err(Error::InvalidData(
464 format!("Invalid face at line {}: expected at least 3 vertices", line_num + 1)
465 ));
466 }
467
468 let mut face_vertices = Vec::new();
469 for vertex_str in &parts[1..] {
470 let face_vertex = FaceVertex::parse(vertex_str)
471 .map_err(|e| Error::InvalidData(format!("Invalid face vertex '{}' at line {}: {}", vertex_str, line_num + 1, e)))?;
472 face_vertices.push(face_vertex);
473 }
474
475 let triangulated_faces = Self::triangulate_face(&face_vertices);
477 for triangle in triangulated_faces {
478 current_group.faces.push(Face {
479 vertices: triangle,
480 material: current_material.clone(),
481 });
482 }
483 }
484 "g" => {
485 if !current_group.faces.is_empty() || current_group.name != "default" {
487 obj_data.groups.push(current_group);
488 }
489
490 let group_name = if parts.len() > 1 {
491 parts[1..].join(" ")
492 } else {
493 format!("group_{}", obj_data.groups.len())
494 };
495
496 current_group = Group {
497 name: group_name,
498 faces: Vec::new(),
499 };
500 }
501 "usemtl" => {
502 if parts.len() > 1 {
504 current_material = Some(parts[1].to_string());
505 }
506 }
507 "mtllib" => {
508 if parts.len() > 1 {
510 let mtl_file = parts[1].to_string();
511 obj_data.mtl_files.push(mtl_file.clone());
512
513 let mtl_path = if let Some(parent) = path.parent() {
515 parent.join(&mtl_file)
516 } else {
517 PathBuf::from(&mtl_file)
518 };
519
520 if let Ok(materials) = Self::read_mtl_file(&mtl_path) {
521 obj_data.materials.extend(materials);
522 }
523 }
524 }
525 _ => {
526 }
528 }
529 }
530
531 if !current_group.faces.is_empty() {
533 obj_data.groups.push(current_group);
534 }
535
536 if obj_data.groups.is_empty() {
538 obj_data.groups.push(Group {
539 name: "default".to_string(),
540 faces: Vec::new(),
541 });
542 }
543
544 Ok(obj_data)
545 }
546
547 pub fn read_mtl_file<P: AsRef<Path>>(path: P) -> Result<HashMap<String, Material>> {
549 let file = File::open(path)
550 .map_err(|e| Error::Io(e))?;
551 let reader = BufReader::new(file);
552
553 let mut materials = HashMap::new();
554 let mut current_material: Option<Material> = None;
555
556 for (_line_num, line) in reader.lines().enumerate() {
557 let line = line.map_err(|e| Error::Io(e))?;
558 let line = line.trim();
559
560 if line.is_empty() || line.starts_with('#') {
562 continue;
563 }
564
565 let parts: Vec<&str> = line.split_whitespace().collect();
566 if parts.is_empty() {
567 continue;
568 }
569
570 match parts[0] {
571 "newmtl" => {
572 if let Some(material) = current_material.take() {
574 materials.insert(material.name.clone(), material);
575 }
576
577 if parts.len() > 1 {
579 current_material = Some(Material::new(parts[1].to_string()));
580 }
581 }
582 "Ka" => {
583 if let Some(ref mut material) = current_material {
585 if parts.len() >= 4 {
586 if let (Ok(r), Ok(g), Ok(b)) = (
587 parts[1].parse::<f32>(),
588 parts[2].parse::<f32>(),
589 parts[3].parse::<f32>()
590 ) {
591 material.ambient = Some([r, g, b]);
592 }
593 }
594 }
595 }
596 "Kd" => {
597 if let Some(ref mut material) = current_material {
599 if parts.len() >= 4 {
600 if let (Ok(r), Ok(g), Ok(b)) = (
601 parts[1].parse::<f32>(),
602 parts[2].parse::<f32>(),
603 parts[3].parse::<f32>()
604 ) {
605 material.diffuse = Some([r, g, b]);
606 }
607 }
608 }
609 }
610 "Ks" => {
611 if let Some(ref mut material) = current_material {
613 if parts.len() >= 4 {
614 if let (Ok(r), Ok(g), Ok(b)) = (
615 parts[1].parse::<f32>(),
616 parts[2].parse::<f32>(),
617 parts[3].parse::<f32>()
618 ) {
619 material.specular = Some([r, g, b]);
620 }
621 }
622 }
623 }
624 "Ns" => {
625 if let Some(ref mut material) = current_material {
627 if parts.len() > 1 {
628 if let Ok(ns) = parts[1].parse::<f32>() {
629 material.shininess = Some(ns);
630 }
631 }
632 }
633 }
634 "d" => {
635 if let Some(ref mut material) = current_material {
637 if parts.len() > 1 {
638 if let Ok(d) = parts[1].parse::<f32>() {
639 material.transparency = Some(d);
640 }
641 }
642 }
643 }
644 "Tr" => {
645 if let Some(ref mut material) = current_material {
647 if parts.len() > 1 {
648 if let Ok(tr) = parts[1].parse::<f32>() {
649 material.transparency = Some(1.0 - tr); }
651 }
652 }
653 }
654 "illum" => {
655 if let Some(ref mut material) = current_material {
657 if parts.len() > 1 {
658 if let Ok(illum) = parts[1].parse::<u32>() {
659 material.illumination = Some(illum);
660 }
661 }
662 }
663 }
664 "map_Kd" => {
665 if let Some(ref mut material) = current_material {
667 if parts.len() > 1 {
668 material.diffuse_map = Some(parts[1..].join(" "));
669 }
670 }
671 }
672 "map_Bump" | "bump" => {
673 if let Some(ref mut material) = current_material {
675 if parts.len() > 1 {
676 material.normal_map = Some(parts[1..].join(" "));
677 }
678 }
679 }
680 "map_Ks" => {
681 if let Some(ref mut material) = current_material {
683 if parts.len() > 1 {
684 material.specular_map = Some(parts[1..].join(" "));
685 }
686 }
687 }
688 _ => {
689 }
691 }
692 }
693
694 if let Some(material) = current_material {
696 materials.insert(material.name.clone(), material);
697 }
698
699 Ok(materials)
700 }
701
702 fn triangulate_face(vertices: &[FaceVertex]) -> Vec<Vec<FaceVertex>> {
704 match vertices.len() {
705 3 => {
706 vec![vertices.to_vec()]
708 }
709 4 => {
710 vec![
712 vec![vertices[0], vertices[1], vertices[2]],
713 vec![vertices[0], vertices[2], vertices[3]],
714 ]
715 }
716 n if n > 4 => {
717 let mut triangles = Vec::new();
719 for i in 1..(n - 1) {
720 triangles.push(vec![vertices[0], vertices[i], vertices[i + 1]]);
721 }
722 triangles
723 }
724 _ => {
725 vec![]
727 }
728 }
729 }
730
731 pub fn obj_data_to_mesh(obj_data: &ObjData) -> Result<TriangleMesh> {
733 let mut mesh_faces = Vec::new();
734 let mut mesh_normals = Vec::new();
735 let mut has_normals = false;
736
737 for group in &obj_data.groups {
739 for face in &group.faces {
740 if face.vertices.len() != 3 {
741 continue; }
743
744 let face_indices = [
746 face.vertices[0].vertex,
747 face.vertices[1].vertex,
748 face.vertices[2].vertex,
749 ];
750
751 for &idx in &face_indices {
753 if idx >= obj_data.vertices.len() {
754 return Err(Error::InvalidData(
755 format!("Vertex index {} out of range (max: {})", idx, obj_data.vertices.len() - 1)
756 ));
757 }
758 }
759
760 mesh_faces.push(face_indices);
761
762 if !obj_data.normals.is_empty() {
764 for vertex in &face.vertices {
765 if let Some(normal_idx) = vertex.normal {
766 if normal_idx >= obj_data.normals.len() {
767 return Err(Error::InvalidData(
768 format!("Normal index {} out of range (max: {})", normal_idx, obj_data.normals.len() - 1)
769 ));
770 }
771 mesh_normals.push(obj_data.normals[normal_idx]);
772 has_normals = true;
773 } else if has_normals {
774 mesh_normals.push(Vector3f::new(0.0, 0.0, 1.0));
777 }
778 }
779 }
780 }
781 }
782
783 let mut mesh = TriangleMesh::from_vertices_and_faces(obj_data.vertices.clone(), mesh_faces);
784
785 if has_normals && mesh_normals.len() == mesh.vertices.len() {
787 mesh.set_normals(mesh_normals);
788 }
789
790 Ok(mesh)
791 }
792}
793
794impl RobustObjWriter {
795 pub fn write_obj_file<P: AsRef<Path>>(
797 obj_data: &ObjData,
798 path: P,
799 options: &ObjWriteOptions,
800 ) -> Result<()> {
801 let path = path.as_ref();
802 let file = File::create(path)?;
803 let mut writer = BufWriter::new(file);
804
805 for comment in &options.comments {
807 writeln!(writer, "# {}", comment)?;
808 }
809 writeln!(writer, "# Vertices: {}", obj_data.vertices.len())?;
810 writeln!(writer, "# Texture coordinates: {}", obj_data.texture_coords.len())?;
811 writeln!(writer, "# Normals: {}", obj_data.normals.len())?;
812 writeln!(writer, "# Groups: {}", obj_data.groups.len())?;
813 writeln!(writer)?;
814
815 if options.write_materials && !obj_data.materials.is_empty() {
817 let default_mtl_filename = Self::get_mtl_filename(path);
818 let mtl_filename = options.mtl_filename.as_ref()
819 .unwrap_or(&default_mtl_filename);
820 writeln!(writer, "mtllib {}", mtl_filename)?;
821 writeln!(writer)?;
822 }
823
824 if let Some(ref object_name) = options.object_name {
826 writeln!(writer, "o {}", object_name)?;
827 writeln!(writer)?;
828 }
829
830 for vertex in &obj_data.vertices {
832 writeln!(writer, "v {} {} {}", vertex.x, vertex.y, vertex.z)?;
833 }
834 if !obj_data.vertices.is_empty() {
835 writeln!(writer)?;
836 }
837
838 if options.write_texcoords && !obj_data.texture_coords.is_empty() {
840 for tex_coord in &obj_data.texture_coords {
841 writeln!(writer, "vt {} {}", tex_coord[0], tex_coord[1])?;
842 }
843 writeln!(writer)?;
844 }
845
846 if options.write_normals && !obj_data.normals.is_empty() {
848 for normal in &obj_data.normals {
849 writeln!(writer, "vn {} {} {}", normal.x, normal.y, normal.z)?;
850 }
851 writeln!(writer)?;
852 }
853
854 for group in &obj_data.groups {
856 let group_name = if group.name == "default" && options.group_name.is_some() {
858 options.group_name.as_ref().unwrap()
859 } else if group.name != "default" {
860 &group.name
861 } else {
862 "default"
863 };
864
865 if group_name != "default" || options.group_name.is_some() {
866 writeln!(writer, "g {}", group_name)?;
867 }
868
869 let mut current_material: Option<&String> = None;
871
872 for face in &group.faces {
873 if let Some(ref material) = face.material {
875 if current_material != Some(material) {
876 writeln!(writer, "usemtl {}", material)?;
877 current_material = Some(material);
878 }
879 } else if let Some(ref material_name) = options.material_name {
880 if current_material.is_none() {
881 writeln!(writer, "usemtl {}", material_name)?;
882 current_material = Some(material_name);
883 }
884 }
885
886 write!(writer, "f")?;
888 for vertex in &face.vertices {
889 write!(writer, " {}", vertex.vertex + 1)?; if options.write_texcoords && !obj_data.texture_coords.is_empty() {
893 if let Some(tex_idx) = vertex.texture {
894 write!(writer, "/{}", tex_idx + 1)?;
895 } else {
896 write!(writer, "/")?;
897 }
898 }
899
900 if options.write_normals && !obj_data.normals.is_empty() {
902 if !options.write_texcoords || obj_data.texture_coords.is_empty() {
903 write!(writer, "//")?;
904 } else {
905 write!(writer, "/")?;
906 }
907
908 if let Some(norm_idx) = vertex.normal {
909 write!(writer, "{}", norm_idx + 1)?;
910 }
911 }
912 }
913 writeln!(writer)?;
914 }
915
916 if !group.faces.is_empty() {
917 writeln!(writer)?;
918 }
919 }
920
921 if options.write_materials && !obj_data.materials.is_empty() {
923 let default_mtl_filename = Self::get_mtl_filename(path);
924 let mtl_filename = options.mtl_filename.as_ref()
925 .unwrap_or(&default_mtl_filename);
926 let mtl_path = path.parent().unwrap_or(Path::new(".")).join(mtl_filename);
927 Self::write_mtl_file(&obj_data.materials, &mtl_path)?;
928 }
929
930 Ok(())
931 }
932
933 pub fn write_mesh<P: AsRef<Path>>(
935 mesh: &TriangleMesh,
936 path: P,
937 options: &ObjWriteOptions,
938 ) -> Result<()> {
939 let obj_data = Self::mesh_to_obj_data(mesh, options)?;
941 Self::write_obj_file(&obj_data, path, options)
942 }
943
944 pub fn write_mtl_file<P: AsRef<Path>>(
946 materials: &HashMap<String, Material>,
947 path: P,
948 ) -> Result<()> {
949 let file = File::create(path)?;
950 let mut writer = BufWriter::new(file);
951
952 writeln!(writer, "# MTL file generated by ThreeCrate")?;
953 writeln!(writer, "# Materials: {}", materials.len())?;
954 writeln!(writer)?;
955
956 for (name, material) in materials {
957 writeln!(writer, "newmtl {}", name)?;
958
959 if let Some(ambient) = material.ambient {
961 writeln!(writer, "Ka {} {} {}", ambient[0], ambient[1], ambient[2])?;
962 }
963
964 if let Some(diffuse) = material.diffuse {
966 writeln!(writer, "Kd {} {} {}", diffuse[0], diffuse[1], diffuse[2])?;
967 }
968
969 if let Some(specular) = material.specular {
971 writeln!(writer, "Ks {} {} {}", specular[0], specular[1], specular[2])?;
972 }
973
974 if let Some(shininess) = material.shininess {
976 writeln!(writer, "Ns {}", shininess)?;
977 }
978
979 if let Some(transparency) = material.transparency {
981 writeln!(writer, "d {}", transparency)?;
982 }
983
984 if let Some(illumination) = material.illumination {
986 writeln!(writer, "illum {}", illumination)?;
987 }
988
989 if let Some(ref diffuse_map) = material.diffuse_map {
991 writeln!(writer, "map_Kd {}", diffuse_map)?;
992 }
993
994 if let Some(ref normal_map) = material.normal_map {
995 writeln!(writer, "map_Bump {}", normal_map)?;
996 }
997
998 if let Some(ref specular_map) = material.specular_map {
999 writeln!(writer, "map_Ks {}", specular_map)?;
1000 }
1001
1002 writeln!(writer)?;
1003 }
1004
1005 Ok(())
1006 }
1007
1008 fn mesh_to_obj_data(mesh: &TriangleMesh, options: &ObjWriteOptions) -> Result<ObjData> {
1010 let mut obj_data = ObjData {
1011 vertices: mesh.vertices.clone(),
1012 texture_coords: Vec::new(),
1013 normals: Vec::new(),
1014 groups: Vec::new(),
1015 materials: HashMap::new(),
1016 mtl_files: Vec::new(),
1017 };
1018
1019 if options.write_normals {
1021 if let Some(ref normals) = mesh.normals {
1022 obj_data.normals = normals.clone();
1023 }
1024 }
1025
1026 let mut faces = Vec::new();
1028 for face_indices in &mesh.faces {
1029 let mut face_vertices = Vec::new();
1030 for &vertex_idx in face_indices {
1031 let face_vertex = FaceVertex {
1032 vertex: vertex_idx,
1033 texture: None, normal: if options.write_normals && mesh.normals.is_some() {
1035 Some(vertex_idx) } else {
1037 None
1038 },
1039 };
1040 face_vertices.push(face_vertex);
1041 }
1042
1043 faces.push(Face {
1044 vertices: face_vertices,
1045 material: options.material_name.clone(),
1046 });
1047 }
1048
1049 let group_name = options.group_name.clone().unwrap_or_else(|| "default".to_string());
1051 obj_data.groups.push(Group {
1052 name: group_name,
1053 faces,
1054 });
1055
1056 if let Some(ref material_name) = options.material_name {
1058 let material = Material::new(material_name.clone());
1059 obj_data.materials.insert(material_name.clone(), material);
1060 }
1061
1062 Ok(obj_data)
1063 }
1064
1065 fn get_mtl_filename(obj_path: &Path) -> String {
1067 obj_path
1068 .file_stem()
1069 .and_then(|s| s.to_str())
1070 .map(|s| format!("{}.mtl", s))
1071 .unwrap_or_else(|| "materials.mtl".to_string())
1072 }
1073}
1074
1075pub struct ObjStreamingReader {
1077 reader: BufReader<File>,
1078 current_line: usize,
1079 total_vertices: usize,
1080 vertices_read: usize,
1081 chunk_size: usize,
1082 buffer: Vec<String>,
1083}
1084
1085impl ObjStreamingReader {
1086 pub fn new<P: AsRef<Path>>(path: P, chunk_size: usize) -> Result<Self> {
1088 let path = path.as_ref();
1089 let file = File::open(path)?;
1090 let mut reader = BufReader::new(file);
1091
1092 let mut total_vertices = 0;
1094 let mut line = String::new();
1095 while reader.read_line(&mut line)? > 0 {
1096 let trimmed = line.trim();
1097 if trimmed.starts_with("v ") && !trimmed.starts_with("vt ") && !trimmed.starts_with("vn ") {
1098 total_vertices += 1;
1099 }
1100 line.clear();
1101 }
1102
1103 let file = File::open(path)?;
1105 let reader = BufReader::new(file);
1106
1107 Ok(Self {
1108 reader,
1109 current_line: 0,
1110 total_vertices,
1111 vertices_read: 0,
1112 chunk_size,
1113 buffer: Vec::with_capacity(chunk_size),
1114 })
1115 }
1116}
1117
1118impl Iterator for ObjStreamingReader {
1119 type Item = Result<Point3f>;
1120
1121 fn next(&mut self) -> Option<Self::Item> {
1122 if self.vertices_read >= self.total_vertices {
1123 return None;
1124 }
1125
1126 if self.buffer.is_empty() {
1128 let remaining = self.total_vertices - self.vertices_read;
1129 let to_read = std::cmp::min(remaining, self.chunk_size);
1130
1131 for _ in 0..to_read {
1132 let mut line = String::new();
1133 loop {
1134 match self.reader.read_line(&mut line) {
1135 Ok(0) => return None, Ok(_) => {
1137 self.current_line += 1;
1138 let trimmed = line.trim();
1139
1140 if trimmed.starts_with("v ") && !trimmed.starts_with("vt ") && !trimmed.starts_with("vn ") {
1142 self.buffer.push(line.clone());
1143 line.clear();
1144 break;
1145 }
1146 line.clear();
1147 }
1148 Err(e) => return Some(Err(Error::Io(e))),
1149 }
1150 }
1151 }
1152 }
1153
1154 if !self.buffer.is_empty() {
1156 let vertex_line = self.buffer.remove(0);
1157 let parts: Vec<&str> = vertex_line.trim().split_whitespace().collect();
1158 if parts.len() < 4 {
1159 return Some(Err(Error::InvalidData(
1160 format!("Invalid vertex at line {}: expected 3 coordinates", self.current_line)
1161 )));
1162 }
1163
1164 let x = match parts[1].parse::<f32>() {
1165 Ok(v) => v,
1166 Err(_) => return Some(Err(Error::InvalidData(
1167 format!("Invalid x coordinate at line {}", self.current_line)
1168 ))),
1169 };
1170 let y = match parts[2].parse::<f32>() {
1171 Ok(v) => v,
1172 Err(_) => return Some(Err(Error::InvalidData(
1173 format!("Invalid y coordinate at line {}", self.current_line)
1174 ))),
1175 };
1176 let z = match parts[3].parse::<f32>() {
1177 Ok(v) => v,
1178 Err(_) => return Some(Err(Error::InvalidData(
1179 format!("Invalid z coordinate at line {}", self.current_line)
1180 ))),
1181 };
1182
1183 self.vertices_read += 1;
1184 Some(Ok(Point3f::new(x, y, z)))
1185 } else {
1186 None
1187 }
1188 }
1189}
1190
1191pub struct ObjMeshStreamingReader {
1193 reader: BufReader<File>,
1194 current_line: usize,
1195 total_faces: usize,
1196 faces_read: usize,
1197 chunk_size: usize,
1198 buffer: Vec<String>,
1199 vertices: Vec<Point3f>, }
1201
1202impl ObjMeshStreamingReader {
1203 pub fn new<P: AsRef<Path>>(path: P, chunk_size: usize) -> Result<Self> {
1205 let path = path.as_ref();
1206 let file = File::open(path)?;
1207 let mut reader = BufReader::new(file);
1208
1209 let mut vertices = Vec::new();
1211 let mut total_faces = 0;
1212 let mut line = String::new();
1213
1214 while reader.read_line(&mut line)? > 0 {
1215 let trimmed = line.trim();
1216 if trimmed.starts_with("v ") && !trimmed.starts_with("vt ") && !trimmed.starts_with("vn ") {
1217 let parts: Vec<&str> = trimmed.split_whitespace().collect();
1218 if parts.len() >= 4 {
1219 if let (Ok(x), Ok(y), Ok(z)) = (
1220 parts[1].parse::<f32>(),
1221 parts[2].parse::<f32>(),
1222 parts[3].parse::<f32>()
1223 ) {
1224 vertices.push(Point3f::new(x, y, z));
1225 }
1226 }
1227 } else if trimmed.starts_with("f ") {
1228 total_faces += 1;
1229 }
1230 line.clear();
1231 }
1232
1233 let file = File::open(path)?;
1235 let reader = BufReader::new(file);
1236
1237 Ok(Self {
1238 reader,
1239 current_line: 0,
1240 total_faces,
1241 faces_read: 0,
1242 chunk_size,
1243 buffer: Vec::with_capacity(chunk_size),
1244 vertices,
1245 })
1246 }
1247}
1248
1249impl Iterator for ObjMeshStreamingReader {
1250 type Item = Result<[usize; 3]>;
1251
1252 fn next(&mut self) -> Option<Self::Item> {
1253 if self.faces_read >= self.total_faces {
1254 return None;
1255 }
1256
1257 if self.buffer.is_empty() {
1259 let remaining = self.total_faces - self.faces_read;
1260 let to_read = std::cmp::min(remaining, self.chunk_size);
1261
1262 for _ in 0..to_read {
1263 let mut line = String::new();
1264 loop {
1265 match self.reader.read_line(&mut line) {
1266 Ok(0) => return None, Ok(_) => {
1268 self.current_line += 1;
1269 let trimmed = line.trim();
1270
1271 if trimmed.starts_with("f ") {
1273 self.buffer.push(line.clone());
1274 line.clear();
1275 break;
1276 }
1277 line.clear();
1278 }
1279 Err(e) => return Some(Err(Error::Io(e))),
1280 }
1281 }
1282 }
1283 }
1284
1285 if !self.buffer.is_empty() {
1287 let face_line = self.buffer.remove(0);
1288 let parts: Vec<&str> = face_line.trim().split_whitespace().collect();
1289 if parts.len() < 4 {
1290 return Some(Err(Error::InvalidData(
1291 format!("Invalid face at line {}: expected at least 3 vertices", self.current_line)
1292 )));
1293 }
1294
1295 let mut face_vertices = Vec::new();
1297 for vertex_str in &parts[1..] {
1298 let face_vertex = match FaceVertex::parse(vertex_str) {
1299 Ok(fv) => fv.vertex,
1300 Err(e) => return Some(Err(Error::InvalidData(
1301 format!("Invalid face vertex '{}' at line {}: {}", vertex_str, self.current_line, e)
1302 ))),
1303 };
1304 face_vertices.push(face_vertex);
1305 }
1306
1307 let triangulated_faces = RobustObjReader::triangulate_face(
1309 &face_vertices.iter().map(|&v| FaceVertex { vertex: v, texture: None, normal: None }).collect::<Vec<_>>()
1310 );
1311
1312 if triangulated_faces.is_empty() {
1313 return Some(Err(Error::InvalidData("Degenerate face".to_string())));
1314 }
1315
1316 let first_triangle = &triangulated_faces[0];
1318 let face_indices = [first_triangle[0].vertex, first_triangle[1].vertex, first_triangle[2].vertex];
1319
1320 for &idx in &face_indices {
1322 if idx >= self.vertices.len() {
1323 return Some(Err(Error::InvalidData(
1324 format!("Face index {} out of range (max: {})", idx, self.vertices.len() - 1)
1325 )));
1326 }
1327 }
1328
1329 for triangle in triangulated_faces.into_iter().skip(1) {
1331 let face_str = format!("f {} {} {}",
1332 triangle[0].vertex + 1,
1333 triangle[1].vertex + 1,
1334 triangle[2].vertex + 1
1335 );
1336 self.buffer.push(face_str);
1337 }
1338
1339 self.faces_read += 1;
1340 Some(Ok(face_indices))
1341 } else {
1342 None
1343 }
1344 }
1345}