Skip to main content

oxihuman_export/
dotobj_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Extended Wavefront OBJ export with material library (.mtl) support.
6
7/// Material definition for .mtl export.
8#[derive(Clone, Debug)]
9pub struct DotObjMaterial {
10    pub name: String,
11    pub diffuse: [f32; 3],
12    pub specular: [f32; 3],
13    pub shininess: f32,
14    pub opacity: f32,
15    pub diffuse_map: Option<String>,
16}
17
18impl Default for DotObjMaterial {
19    fn default() -> Self {
20        Self {
21            name: "default".to_string(),
22            diffuse: [0.8, 0.8, 0.8],
23            specular: [0.5, 0.5, 0.5],
24            shininess: 32.0,
25            opacity: 1.0,
26            diffuse_map: None,
27        }
28    }
29}
30
31/// Extended OBJ export container.
32#[derive(Clone, Debug, Default)]
33pub struct DotObjExport {
34    pub positions: Vec<[f32; 3]>,
35    pub normals: Vec<[f32; 3]>,
36    pub uvs: Vec<[f32; 2]>,
37    pub indices: Vec<u32>,
38    pub materials: Vec<DotObjMaterial>,
39    pub object_name: String,
40    pub mtl_lib_name: String,
41}
42
43/// Create a new DotObj export.
44pub fn new_dotobj_export(name: &str) -> DotObjExport {
45    DotObjExport {
46        object_name: name.to_string(),
47        mtl_lib_name: format!("{}.mtl", name),
48        ..Default::default()
49    }
50}
51
52/// Set mesh geometry.
53pub fn dotobj_set_mesh(
54    doc: &mut DotObjExport,
55    positions: Vec<[f32; 3]>,
56    normals: Vec<[f32; 3]>,
57    uvs: Vec<[f32; 2]>,
58    indices: Vec<u32>,
59) {
60    doc.positions = positions;
61    doc.normals = normals;
62    doc.uvs = uvs;
63    doc.indices = indices;
64}
65
66/// Add a material.
67pub fn dotobj_add_material(doc: &mut DotObjExport, mat: DotObjMaterial) {
68    doc.materials.push(mat);
69}
70
71/// Return the material count.
72pub fn dotobj_material_count(doc: &DotObjExport) -> usize {
73    doc.materials.len()
74}
75
76/// Return the face count.
77pub fn dotobj_face_count(doc: &DotObjExport) -> usize {
78    doc.indices.len() / 3
79}
80
81/// Render the .obj text content.
82pub fn render_dotobj(doc: &DotObjExport) -> String {
83    let mut out = String::from("# oxihuman dotobj export\n");
84    if !doc.mtl_lib_name.is_empty() {
85        out.push_str(&format!("mtllib {}\n", doc.mtl_lib_name));
86    }
87    if !doc.object_name.is_empty() {
88        out.push_str(&format!("o {}\n", doc.object_name));
89    }
90    for p in &doc.positions {
91        out.push_str(&format!("v {:.6} {:.6} {:.6}\n", p[0], p[1], p[2]));
92    }
93    for uv in &doc.uvs {
94        out.push_str(&format!("vt {:.6} {:.6}\n", uv[0], uv[1]));
95    }
96    for n in &doc.normals {
97        out.push_str(&format!("vn {:.6} {:.6} {:.6}\n", n[0], n[1], n[2]));
98    }
99    if !doc.materials.is_empty() {
100        out.push_str(&format!("usemtl {}\n", doc.materials[0].name));
101    }
102    let tri_count = doc.indices.len() / 3;
103    let has_uv = !doc.uvs.is_empty();
104    let has_n = !doc.normals.is_empty();
105    for t in 0..tri_count {
106        let mut face = String::from("f");
107        for k in 0..3 {
108            let vi = doc.indices[t * 3 + k] + 1; // OBJ is 1-indexed
109            face.push(' ');
110            if has_uv && has_n {
111                face.push_str(&format!("{}/{}/{}", vi, vi, vi));
112            } else if has_uv {
113                face.push_str(&format!("{}/{}", vi, vi));
114            } else if has_n {
115                face.push_str(&format!("{}//{}", vi, vi));
116            } else {
117                face.push_str(&vi.to_string());
118            }
119        }
120        out.push_str(&face);
121        out.push('\n');
122    }
123    out
124}
125
126/// Render the .mtl text content.
127pub fn render_dotmtl(doc: &DotObjExport) -> String {
128    let mut out = String::from("# oxihuman mtl export\n");
129    for mat in &doc.materials {
130        out.push_str(&format!("newmtl {}\n", mat.name));
131        out.push_str(&format!(
132            "Kd {:.4} {:.4} {:.4}\n",
133            mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]
134        ));
135        out.push_str(&format!(
136            "Ks {:.4} {:.4} {:.4}\n",
137            mat.specular[0], mat.specular[1], mat.specular[2]
138        ));
139        out.push_str(&format!("Ns {:.4}\n", mat.shininess));
140        out.push_str(&format!("d {:.4}\n", mat.opacity));
141        if let Some(ref map) = mat.diffuse_map {
142            out.push_str(&format!("map_Kd {}\n", map));
143        }
144    }
145    out
146}
147
148/// Validate the export (positions non-empty, indices valid).
149pub fn validate_dotobj(doc: &DotObjExport) -> bool {
150    if doc.positions.is_empty() {
151        return false;
152    }
153    let n = doc.positions.len() as u32;
154    doc.indices.iter().all(|&i| i < n)
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn simple_doc() -> DotObjExport {
162        let mut d = new_dotobj_export("test");
163        dotobj_set_mesh(
164            &mut d,
165            vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
166            vec![],
167            vec![],
168            vec![0, 1, 2],
169        );
170        d
171    }
172
173    #[test]
174    fn new_export_empty() {
175        let d = new_dotobj_export("test");
176        assert_eq!(dotobj_face_count(&d), 0);
177    }
178
179    #[test]
180    fn face_count_after_set() {
181        let d = simple_doc();
182        assert_eq!(dotobj_face_count(&d), 1);
183    }
184
185    #[test]
186    fn add_material_increments() {
187        let mut d = simple_doc();
188        dotobj_add_material(&mut d, DotObjMaterial::default());
189        assert_eq!(dotobj_material_count(&d), 1);
190    }
191
192    #[test]
193    fn render_obj_contains_v() {
194        let d = simple_doc();
195        let s = render_dotobj(&d);
196        assert!(s.contains("\nv "));
197    }
198
199    #[test]
200    fn render_obj_contains_face() {
201        let d = simple_doc();
202        let s = render_dotobj(&d);
203        assert!(s.contains("\nf "));
204    }
205
206    #[test]
207    fn render_mtl_contains_newmtl() {
208        let mut d = simple_doc();
209        dotobj_add_material(
210            &mut d,
211            DotObjMaterial {
212                name: "mat1".to_string(),
213                ..Default::default()
214            },
215        );
216        let s = render_dotmtl(&d);
217        assert!(s.contains("newmtl mat1"));
218    }
219
220    #[test]
221    fn validate_valid_doc() {
222        let d = simple_doc();
223        assert!(validate_dotobj(&d));
224    }
225
226    #[test]
227    fn validate_empty_fails() {
228        let d = new_dotobj_export("test");
229        assert!(!validate_dotobj(&d));
230    }
231}