Skip to main content

brepkit_io/obj/
writer.rs

1//! OBJ file writer.
2
3use std::fmt::Write;
4
5use brepkit_operations::tessellate::{self, TriangleMesh};
6use brepkit_topology::Topology;
7use brepkit_topology::explorer::solid_faces;
8use brepkit_topology::solid::SolidId;
9
10/// Write one or more solids to OBJ format as a UTF-8 string.
11///
12/// Tessellates each solid's faces and writes all triangles into a single
13/// OBJ file. The `deflection` parameter controls tessellation quality.
14///
15/// # Errors
16///
17/// Returns an error if tessellation fails.
18pub fn write_obj(
19    topo: &Topology,
20    solids: &[SolidId],
21    deflection: f64,
22) -> Result<String, crate::IoError> {
23    let mut merged = TriangleMesh::default();
24
25    for &solid_id in solids {
26        // Walk outer + inner (cavity) shells. A hollow solid's
27        // cavity surface is part of the geometry the user expects
28        // to round-trip through OBJ; outer-shell-only would emit a
29        // mesh with the void unrepresented.
30        let face_ids = solid_faces(topo, solid_id)?;
31
32        for &face_id in &face_ids {
33            let mesh = tessellate::tessellate(topo, face_id, deflection)?;
34            let offset = merged.positions.len();
35
36            merged.positions.extend_from_slice(&mesh.positions);
37            merged.normals.extend_from_slice(&mesh.normals);
38            for &idx in &mesh.indices {
39                #[allow(clippy::cast_possible_truncation)]
40                merged.indices.push((idx as usize + offset) as u32);
41            }
42        }
43    }
44
45    let mut output = String::new();
46    let _ = writeln!(output, "# brepkit OBJ export");
47    let _ = writeln!(
48        output,
49        "# {} vertices, {} faces",
50        merged.positions.len(),
51        merged.indices.len() / 3,
52    );
53
54    for pos in &merged.positions {
55        let _ = writeln!(output, "v {:.6} {:.6} {:.6}", pos.x(), pos.y(), pos.z());
56    }
57
58    for normal in &merged.normals {
59        let _ = writeln!(
60            output,
61            "vn {:.6} {:.6} {:.6}",
62            normal.x(),
63            normal.y(),
64            normal.z()
65        );
66    }
67
68    // Faces (OBJ is 1-indexed, format: f v1//n1 v2//n2 v3//n3)
69    for tri in merged.indices.chunks_exact(3) {
70        let i0 = tri[0] as usize + 1;
71        let i1 = tri[1] as usize + 1;
72        let i2 = tri[2] as usize + 1;
73        let _ = writeln!(output, "f {i0}//{i0} {i1}//{i1} {i2}//{i2}");
74    }
75
76    Ok(output)
77}
78
79#[cfg(test)]
80#[allow(clippy::unwrap_used)]
81mod tests {
82    use brepkit_topology::Topology;
83
84    use super::*;
85
86    #[test]
87    fn write_box_obj() {
88        let mut topo = Topology::new();
89        let solid = brepkit_operations::primitives::make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
90
91        let obj = write_obj(&topo, &[solid], 0.1).unwrap();
92
93        assert!(obj.starts_with("# brepkit OBJ export"));
94        assert!(obj.contains("v "));
95        assert!(obj.contains("vn "));
96        assert!(obj.contains("f "));
97
98        let v_count = obj.lines().filter(|l| l.starts_with("v ")).count();
99        assert!(v_count > 0, "should have vertices");
100
101        let f_count = obj.lines().filter(|l| l.starts_with("f ")).count();
102        assert!(f_count > 0, "should have faces");
103    }
104
105    #[test]
106    fn obj_is_valid_format() {
107        let mut topo = Topology::new();
108        let solid = brepkit_operations::primitives::make_box(&mut topo, 2.0, 3.0, 4.0).unwrap();
109
110        let obj = write_obj(&topo, &[solid], 0.1).unwrap();
111
112        let v_count = obj.lines().filter(|l| l.starts_with("v ")).count();
113        for line in obj.lines().filter(|l| l.starts_with("f ")) {
114            for token in line.split_whitespace().skip(1) {
115                let idx_str = token.split("//").next().unwrap();
116                let idx: usize = idx_str.parse().unwrap();
117                assert!(
118                    idx >= 1 && idx <= v_count,
119                    "face index {idx} out of range [1, {v_count}]"
120                );
121            }
122        }
123    }
124}