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