1use 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
10pub 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 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 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}