schematic_mesher/export/
obj.rs

1//! Wavefront OBJ export.
2//!
3//! OBJ is a simple, widely-supported text-based 3D format.
4//! This exports the mesh with UVs and vertex colors (as comments).
5
6use crate::error::Result;
7use crate::mesher::MesherOutput;
8use std::fmt::Write;
9
10/// Export a mesh to OBJ format.
11/// Returns (obj_content, mtl_content) as strings.
12/// Note: OBJ doesn't support separate transparency handling, so opaque and transparent
13/// geometry are combined.
14pub fn export_obj(output: &MesherOutput, name: &str) -> Result<(String, String)> {
15    // Combine opaque and transparent meshes for OBJ export
16    let mesh = output.mesh();
17    let mut obj = String::new();
18    let mut mtl = String::new();
19
20    // OBJ header
21    writeln!(obj, "# Schematic Mesher OBJ Export").unwrap();
22    writeln!(obj, "# Vertices: {}", mesh.vertex_count()).unwrap();
23    writeln!(obj, "# Triangles: {}", mesh.triangle_count()).unwrap();
24    writeln!(obj).unwrap();
25
26    // Reference material file
27    writeln!(obj, "mtllib {}.mtl", name).unwrap();
28    writeln!(obj).unwrap();
29
30    // Object name
31    writeln!(obj, "o {}", name).unwrap();
32    writeln!(obj).unwrap();
33
34    // Vertices (v x y z)
35    // OBJ also supports vertex colors as: v x y z r g b
36    for vertex in &mesh.vertices {
37        writeln!(
38            obj,
39            "v {} {} {} {} {} {}",
40            vertex.position[0],
41            vertex.position[1],
42            vertex.position[2],
43            vertex.color[0],
44            vertex.color[1],
45            vertex.color[2]
46        )
47        .unwrap();
48    }
49    writeln!(obj).unwrap();
50
51    // Texture coordinates (vt u v)
52    for vertex in &mesh.vertices {
53        writeln!(obj, "vt {} {}", vertex.uv[0], vertex.uv[1]).unwrap();
54    }
55    writeln!(obj).unwrap();
56
57    // Normals (vn x y z)
58    for vertex in &mesh.vertices {
59        writeln!(
60            obj,
61            "vn {} {} {}",
62            vertex.normal[0], vertex.normal[1], vertex.normal[2]
63        )
64        .unwrap();
65    }
66    writeln!(obj).unwrap();
67
68    // Use material
69    writeln!(obj, "usemtl {}_material", name).unwrap();
70    writeln!(obj).unwrap();
71
72    // Faces (f v/vt/vn v/vt/vn v/vt/vn)
73    // OBJ indices are 1-based
74    for i in (0..mesh.indices.len()).step_by(3) {
75        let i0 = mesh.indices[i] as usize + 1;
76        let i1 = mesh.indices[i + 1] as usize + 1;
77        let i2 = mesh.indices[i + 2] as usize + 1;
78        writeln!(
79            obj,
80            "f {}/{}/{} {}/{}/{} {}/{}/{}",
81            i0, i0, i0, i1, i1, i1, i2, i2, i2
82        )
83        .unwrap();
84    }
85
86    // MTL file
87    writeln!(mtl, "# Schematic Mesher Material").unwrap();
88    writeln!(mtl).unwrap();
89    writeln!(mtl, "newmtl {}_material", name).unwrap();
90    writeln!(mtl, "Ka 1.0 1.0 1.0").unwrap(); // Ambient
91    writeln!(mtl, "Kd 1.0 1.0 1.0").unwrap(); // Diffuse
92    writeln!(mtl, "Ks 0.0 0.0 0.0").unwrap(); // Specular
93    writeln!(mtl, "Ns 10.0").unwrap(); // Specular exponent
94    writeln!(mtl, "d 1.0").unwrap(); // Opacity
95    writeln!(mtl, "illum 1").unwrap(); // Illumination model (1 = diffuse only)
96    writeln!(mtl, "map_Kd {}_atlas.png", name).unwrap(); // Diffuse texture
97
98    Ok((obj, mtl))
99}
100
101/// Export mesh and atlas to OBJ format bytes for writing to files.
102pub struct ObjExport {
103    pub obj: String,
104    pub mtl: String,
105    pub texture_png: Vec<u8>,
106}
107
108impl ObjExport {
109    pub fn from_output(output: &MesherOutput, name: &str) -> Result<Self> {
110        let (obj, mtl) = export_obj(output, name)?;
111        let texture_png = output.atlas.to_png()?;
112        Ok(Self {
113            obj,
114            mtl,
115            texture_png,
116        })
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::atlas::TextureAtlas;
124    use crate::mesher::geometry::{Mesh, Vertex};
125    use crate::types::BoundingBox;
126
127    #[test]
128    fn test_export_simple_obj() {
129        let mut mesh = Mesh::new();
130
131        // Create a simple triangle
132        let v0 = mesh.add_vertex(Vertex::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0]));
133        let v1 = mesh.add_vertex(Vertex::new([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0]));
134        let v2 = mesh.add_vertex(Vertex::new([0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [0.0, 1.0]));
135        mesh.add_triangle(v0, v1, v2);
136
137        let output = MesherOutput {
138            opaque_mesh: mesh,
139            transparent_mesh: Mesh::new(),
140            atlas: TextureAtlas::empty(),
141            bounds: BoundingBox::new([0.0, 0.0, 0.0], [1.0, 0.0, 1.0]),
142        };
143
144        let (obj, mtl) = export_obj(&output, "test").unwrap();
145
146        assert!(obj.contains("v 0 0 0"));
147        assert!(obj.contains("vt 0 0"));
148        assert!(obj.contains("vn 0 1 0"));
149        assert!(obj.contains("f 1/1/1 2/2/2 3/3/3"));
150        assert!(mtl.contains("newmtl test_material"));
151    }
152}