Skip to main content

draco_io/
obj_writer.rs

1//! OBJ format writer for meshes and point clouds.
2//!
3//! Supports writing:
4//! - Vertex positions
5//! - Triangle faces (for meshes)
6//! - Vertex normals (if present)
7//! - Vertex texture coordinates (if present)
8//!
9//! # Example
10//!
11//! ```ignore
12//! use draco_io::ObjWriter;
13//! use draco_core::mesh::Mesh;
14//!
15//! let mesh: Mesh = /* ... */;
16//! let mut writer = ObjWriter::new();
17//! writer.add_mesh(&mesh, Some("MyMesh"));
18//! writer.write("output.obj")?;
19//!
20//! // Or write point cloud
21//! let mut writer = ObjWriter::new();
22//! writer.add_points(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
23//! writer.write("points.obj")?;
24//! ```
25
26use std::fs::File;
27use std::io::{self, BufWriter, Write};
28use std::path::Path;
29
30use draco_core::draco_types::DataType;
31use draco_core::geometry_attribute::GeometryAttributeType;
32use draco_core::geometry_indices::FaceIndex;
33use draco_core::mesh::Mesh;
34
35use crate::traits::{PointCloudWriter, WriteToBytes, Writer};
36
37/// OBJ format writer.
38///
39/// This struct provides a builder-style API for writing OBJ files.
40/// Meshes or points are added, then written with `write()`.
41///
42/// # Example
43///
44/// ```ignore
45/// use draco_io::ObjWriter;
46///
47/// let mut writer = ObjWriter::new();
48/// writer.add_mesh(&mesh, Some("Cube"));
49/// writer.write("cube.obj")?;
50/// ```
51#[derive(Debug, Clone, Default)]
52pub struct ObjWriter {
53    /// Collected vertex positions
54    positions: Vec<[f32; 3]>,
55    /// Collected vertex normals
56    normals: Vec<[f32; 3]>,
57    /// Collected vertex texture coordinates
58    texcoords: Vec<[f32; 2]>,
59    /// Collected faces (1-based indices)
60    faces: Vec<ObjFace>,
61    /// Object groups with (name, start_face_index)
62    groups: Vec<(String, usize)>,
63}
64
65#[derive(Debug, Clone)]
66struct ObjFace {
67    positions: [u32; 3],
68    texcoords: Option<[u32; 3]>,
69    normals: Option<[u32; 3]>,
70}
71
72impl ObjWriter {
73    /// Create a new OBJ writer.
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Add raw point positions (for point cloud output).
79    pub fn add_points(&mut self, points: &[[f32; 3]]) {
80        self.positions.extend_from_slice(points);
81    }
82
83    /// Add a single point.
84    pub fn add_point(&mut self, point: [f32; 3]) {
85        self.positions.push(point);
86    }
87
88    /// Get the number of vertices added.
89    pub fn vertex_count(&self) -> usize {
90        self.positions.len()
91    }
92
93    /// Get the number of faces added.
94    pub fn face_count(&self) -> usize {
95        self.faces.len()
96    }
97
98    /// Write the OBJ file to the given path.
99    pub fn write<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
100        let file = File::create(path)?;
101        let mut writer = BufWriter::new(file);
102        self.write_to(&mut writer)
103    }
104
105    /// Write the OBJ data to a writer.
106    pub fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
107        // Header comment
108        writeln!(writer, "# OBJ file generated by draco-io")?;
109        writeln!(writer, "# Vertices: {}", self.positions.len())?;
110        writeln!(writer, "# Faces: {}", self.faces.len())?;
111        writeln!(writer)?;
112
113        // Write all positions
114        for [x, y, z] in &self.positions {
115            writeln!(writer, "v {:.6} {:.6} {:.6}", x, y, z)?;
116        }
117
118        // Write texture coordinates if present
119        if !self.texcoords.is_empty() {
120            writeln!(writer)?;
121            for [u, v] in &self.texcoords {
122                writeln!(writer, "vt {:.6} {:.6}", u, v)?;
123            }
124        }
125
126        // Write normals if present
127        if !self.normals.is_empty() {
128            writeln!(writer)?;
129            for [x, y, z] in &self.normals {
130                writeln!(writer, "vn {:.6} {:.6} {:.6}", x, y, z)?;
131            }
132        }
133
134        // Write faces with groups
135        if !self.faces.is_empty() {
136            writeln!(writer)?;
137
138            let mut group_iter = self.groups.iter().peekable();
139            let has_texcoords = !self.texcoords.is_empty();
140            let has_normals = !self.normals.is_empty();
141
142            for (i, face) in self.faces.iter().enumerate() {
143                // Check if we need to start a new group
144                if let Some((name, start_idx)) = group_iter.peek() {
145                    if i == *start_idx {
146                        writeln!(writer, "o {}", name)?;
147                        group_iter.next();
148                    }
149                }
150
151                match (
152                    has_texcoords.then_some(face.texcoords).flatten(),
153                    has_normals.then_some(face.normals).flatten(),
154                ) {
155                    (Some(texcoords), Some(normals)) => {
156                        writeln!(
157                            writer,
158                            "f {}/{}/{} {}/{}/{} {}/{}/{}",
159                            face.positions[0],
160                            texcoords[0],
161                            normals[0],
162                            face.positions[1],
163                            texcoords[1],
164                            normals[1],
165                            face.positions[2],
166                            texcoords[2],
167                            normals[2]
168                        )?;
169                    }
170                    (Some(texcoords), None) => {
171                        writeln!(
172                            writer,
173                            "f {}/{} {}/{} {}/{}",
174                            face.positions[0],
175                            texcoords[0],
176                            face.positions[1],
177                            texcoords[1],
178                            face.positions[2],
179                            texcoords[2]
180                        )?;
181                    }
182                    (None, Some(normals)) => {
183                        writeln!(
184                            writer,
185                            "f {}//{} {}//{} {}//{}",
186                            face.positions[0],
187                            normals[0],
188                            face.positions[1],
189                            normals[1],
190                            face.positions[2],
191                            normals[2]
192                        )?;
193                    }
194                    (None, None) => {
195                        writeln!(
196                            writer,
197                            "f {} {} {}",
198                            face.positions[0], face.positions[1], face.positions[2]
199                        )?;
200                    }
201                }
202            }
203        }
204
205        Ok(())
206    }
207
208    /// Write the OBJ data into a byte vector.
209    pub fn write_to_vec(&self) -> io::Result<Vec<u8>> {
210        let mut out = Vec::new();
211        self.write_to(&mut out)?;
212        Ok(out)
213    }
214}
215
216/// Read a float3 from an attribute at a given point index.
217fn read_float3(mesh: &Mesh, att_id: i32, point_idx: usize) -> [f32; 3] {
218    let att = mesh.attribute(att_id);
219    let byte_stride = att.byte_stride() as usize;
220    let buffer = att.buffer();
221    let mut bytes = [0u8; 12];
222    buffer.read(point_idx * byte_stride, &mut bytes);
223    [
224        f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
225        f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
226        f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
227    ]
228}
229
230/// Read a float2 from an attribute at a given point index.
231fn read_float2(mesh: &Mesh, att_id: i32, point_idx: usize) -> [f32; 2] {
232    let att = mesh.attribute(att_id);
233    let byte_stride = att.byte_stride() as usize;
234    let buffer = att.buffer();
235    let mut bytes = [0u8; 8];
236    buffer.read(point_idx * byte_stride, &mut bytes);
237    [
238        f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
239        f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
240    ]
241}
242
243fn require_f32_components(mesh: &Mesh, att_id: i32, components: u8, label: &str) -> io::Result<()> {
244    let att = mesh.attribute(att_id);
245    if att.data_type() != DataType::Float32 || att.num_components() != components {
246        return Err(io::Error::new(
247            io::ErrorKind::InvalidInput,
248            format!("OBJ writer requires {label} attributes to be Float32x{components}"),
249        ));
250    }
251    Ok(())
252}
253
254// ============================================================================
255// Trait Implementations
256// ============================================================================
257
258impl Writer for ObjWriter {
259    fn new() -> Self {
260        Self::default()
261    }
262
263    fn add_mesh(&mut self, mesh: &Mesh, name: Option<&str>) -> io::Result<()> {
264        let vertex_offset = self.positions.len() as u32;
265        let normal_offset = self.normals.len() as u32;
266        let texcoord_offset = self.texcoords.len() as u32;
267        let face_start = self.faces.len();
268
269        // Add group if name provided
270        if let Some(n) = name {
271            self.groups.push((n.to_string(), face_start));
272        }
273
274        // Extract positions
275        let pos_att_id = mesh.named_attribute_id(GeometryAttributeType::Position);
276        if pos_att_id >= 0 {
277            require_f32_components(mesh, pos_att_id, 3, "position")?;
278            for i in 0..mesh.num_points() {
279                self.positions.push(read_float3(mesh, pos_att_id, i));
280            }
281        }
282
283        // Extract normals if present
284        let normal_att_id = mesh.named_attribute_id(GeometryAttributeType::Normal);
285        let has_normals = normal_att_id >= 0;
286        if has_normals {
287            require_f32_components(mesh, normal_att_id, 3, "normal")?;
288            for i in 0..mesh.num_points() {
289                self.normals.push(read_float3(mesh, normal_att_id, i));
290            }
291        }
292
293        // Extract texture coordinates if present
294        let texcoord_att_id = mesh.named_attribute_id(GeometryAttributeType::TexCoord);
295        let has_texcoords = texcoord_att_id >= 0;
296        if has_texcoords {
297            require_f32_components(mesh, texcoord_att_id, 2, "texcoord")?;
298            for i in 0..mesh.num_points() {
299                self.texcoords.push(read_float2(mesh, texcoord_att_id, i));
300            }
301        }
302
303        // Extract faces (convert to 1-based indices with offsets)
304        for i in 0..mesh.num_faces() as u32 {
305            let face = mesh.face(FaceIndex(i));
306            let positions = [
307                face[0].0 + vertex_offset + 1,
308                face[1].0 + vertex_offset + 1,
309                face[2].0 + vertex_offset + 1,
310            ];
311            let normals = has_normals.then(|| {
312                [
313                    face[0].0 + normal_offset + 1,
314                    face[1].0 + normal_offset + 1,
315                    face[2].0 + normal_offset + 1,
316                ]
317            });
318            let texcoords = has_texcoords.then(|| {
319                [
320                    face[0].0 + texcoord_offset + 1,
321                    face[1].0 + texcoord_offset + 1,
322                    face[2].0 + texcoord_offset + 1,
323                ]
324            });
325            self.faces.push(ObjFace {
326                positions,
327                texcoords,
328                normals,
329            });
330        }
331        Ok(())
332    }
333
334    fn write<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
335        self.write(path)
336    }
337
338    fn vertex_count(&self) -> usize {
339        self.vertex_count()
340    }
341
342    fn face_count(&self) -> usize {
343        self.face_count()
344    }
345}
346
347impl PointCloudWriter for ObjWriter {
348    fn add_points(&mut self, points: &[[f32; 3]]) {
349        self.positions.extend_from_slice(points);
350    }
351
352    fn add_point(&mut self, point: [f32; 3]) {
353        self.positions.push(point);
354    }
355}
356
357impl WriteToBytes for ObjWriter {
358    fn write_to_vec(&self) -> io::Result<Vec<u8>> {
359        ObjWriter::write_to_vec(self)
360    }
361
362    fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
363        ObjWriter::write_to(self, writer)
364    }
365}
366
367// ============================================================================
368// Convenience Functions (for backward compatibility)
369// ============================================================================
370
371/// Write a mesh to an OBJ file with positions and faces.
372///
373/// This is a convenience function. For more control, use `ObjWriter` directly.
374pub fn write_obj_mesh<P: AsRef<Path>>(path: P, mesh: &Mesh) -> io::Result<()> {
375    let mut writer = ObjWriter::new();
376    Writer::add_mesh(&mut writer, mesh, None)?;
377    writer.write(path)
378}
379
380/// Write point positions to an OBJ file (point cloud, no faces).
381///
382/// This is a convenience function. For more control, use `ObjWriter` directly.
383pub fn write_obj_positions<P: AsRef<Path>>(path: P, points: &[[f32; 3]]) -> io::Result<()> {
384    let mut writer = ObjWriter::new();
385    writer.add_points(points);
386    writer.write(path)
387}
388
389// ============================================================================
390// Tests
391// ============================================================================
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use draco_core::draco_types::DataType;
397    use draco_core::geometry_attribute::PointAttribute;
398    use draco_core::geometry_indices::PointIndex;
399    use std::fs;
400    use std::io::{BufRead, BufReader};
401    use tempfile::NamedTempFile;
402
403    fn create_triangle_mesh() -> Mesh {
404        let mut mesh = Mesh::new();
405        let mut pos_att = PointAttribute::new();
406
407        pos_att.init(
408            GeometryAttributeType::Position,
409            3,
410            DataType::Float32,
411            false,
412            3,
413        );
414        let buffer = pos_att.buffer_mut();
415        let positions: [[f32; 3]; 3] = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
416        for (i, pos) in positions.iter().enumerate() {
417            let bytes: Vec<u8> = pos.iter().flat_map(|v| v.to_le_bytes()).collect();
418            buffer.write(i * 12, &bytes);
419        }
420        mesh.add_attribute(pos_att);
421
422        mesh.set_num_faces(1);
423        mesh.set_face(FaceIndex(0), [PointIndex(0), PointIndex(1), PointIndex(2)]);
424
425        mesh
426    }
427
428    fn add_f32_attribute(
429        mesh: &mut Mesh,
430        attribute_type: GeometryAttributeType,
431        components: u8,
432        values: &[f32],
433    ) {
434        let mut att = PointAttribute::new();
435        att.init(
436            attribute_type,
437            components,
438            DataType::Float32,
439            false,
440            values.len() / components as usize,
441        );
442        let bytes: Vec<u8> = values
443            .iter()
444            .flat_map(|component| component.to_le_bytes())
445            .collect();
446        att.buffer_mut().write(0, &bytes);
447        mesh.add_attribute(att);
448    }
449
450    #[test]
451    fn test_obj_writer_new() {
452        let writer = ObjWriter::new();
453        assert_eq!(writer.vertex_count(), 0);
454        assert_eq!(writer.face_count(), 0);
455    }
456
457    #[test]
458    fn test_obj_writer_add_mesh() {
459        let mesh = create_triangle_mesh();
460        let mut writer = ObjWriter::new();
461        Writer::add_mesh(&mut writer, &mesh, Some("Triangle")).unwrap();
462        assert_eq!(writer.vertex_count(), 3);
463        assert_eq!(writer.face_count(), 1);
464    }
465
466    #[test]
467    fn test_obj_writer_add_points() {
468        let mut writer = ObjWriter::new();
469        writer.add_points(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
470        assert_eq!(writer.vertex_count(), 2);
471        assert_eq!(writer.face_count(), 0);
472    }
473
474    #[test]
475    fn test_write_obj_positions() {
476        let points = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
477
478        let file = NamedTempFile::new().unwrap();
479        write_obj_positions(file.path(), &points).unwrap();
480
481        let content = fs::read_to_string(file.path()).unwrap();
482        assert!(content.contains("v 0.000000 0.000000 0.000000"));
483        assert!(content.contains("v 1.000000 0.000000 0.000000"));
484        assert!(content.contains("v 0.000000 1.000000 0.000000"));
485    }
486
487    #[test]
488    fn test_write_obj_mesh() {
489        let mesh = create_triangle_mesh();
490        let file = NamedTempFile::new().unwrap();
491        write_obj_mesh(file.path(), &mesh).unwrap();
492
493        let reader = BufReader::new(fs::File::open(file.path()).unwrap());
494        let lines: Vec<String> = reader.lines().map_while(Result::ok).collect();
495
496        // Check vertices and face
497        assert!(lines.iter().any(|l| l.starts_with("v ")));
498        assert!(lines.iter().any(|l| l == "f 1 2 3"));
499    }
500
501    #[test]
502    fn test_multiple_meshes() {
503        let mesh1 = create_triangle_mesh();
504        let mesh2 = create_triangle_mesh();
505
506        let mut writer = ObjWriter::new();
507        Writer::add_mesh(&mut writer, &mesh1, Some("Mesh1")).unwrap();
508        Writer::add_mesh(&mut writer, &mesh2, Some("Mesh2")).unwrap();
509
510        assert_eq!(writer.vertex_count(), 6);
511        assert_eq!(writer.face_count(), 2);
512
513        let file = NamedTempFile::new().unwrap();
514        writer.write(file.path()).unwrap();
515
516        let content = fs::read_to_string(file.path()).unwrap();
517        assert!(content.contains("o Mesh1"));
518        assert!(content.contains("o Mesh2"));
519        // Second mesh should have offset indices
520        assert!(content.contains("f 4 5 6"));
521    }
522
523    #[test]
524    fn test_write_obj_mesh_with_normals_and_texcoords() {
525        let mut mesh = create_triangle_mesh();
526        add_f32_attribute(
527            &mut mesh,
528            GeometryAttributeType::Normal,
529            3,
530            &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
531        );
532        add_f32_attribute(
533            &mut mesh,
534            GeometryAttributeType::TexCoord,
535            2,
536            &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
537        );
538
539        let mut writer = ObjWriter::new();
540        Writer::add_mesh(&mut writer, &mesh, Some("Triangle")).unwrap();
541
542        let content = String::from_utf8(writer.write_to_vec().unwrap()).unwrap();
543        assert!(content.contains("vt 1.000000 0.000000"));
544        assert!(content.contains("vn 0.000000 0.000000 1.000000"));
545        assert!(content.contains("f 1/1/1 2/2/2 3/3/3"));
546    }
547}