Skip to main content

oxihuman_export/
csv.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(dead_code)]
5
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use anyhow::Result;
10use oxihuman_mesh::MeshBuffers;
11
12// ---------------------------------------------------------------------------
13// Report
14// ---------------------------------------------------------------------------
15
16/// Report returned by [`export_mesh_csv`].
17pub struct CsvExportReport {
18    pub vertices_path: PathBuf,
19    pub faces_path: PathBuf,
20    pub normals_path: PathBuf,
21    pub uvs_path: PathBuf,
22    pub vertex_count: usize,
23    pub face_count: usize,
24}
25
26// ---------------------------------------------------------------------------
27// String helpers (no file I/O)
28// ---------------------------------------------------------------------------
29
30/// Export vertex positions as a CSV string: index,x,y,z
31pub fn vertices_to_csv_string(mesh: &MeshBuffers) -> String {
32    let mut out = String::from("index,x,y,z\n");
33    for (i, p) in mesh.positions.iter().enumerate() {
34        out.push_str(&format!("{},{},{},{}\n", i, p[0], p[1], p[2]));
35    }
36    out
37}
38
39/// Export face indices as a CSV string: face_index,v0,v1,v2
40pub fn faces_to_csv_string(mesh: &MeshBuffers) -> String {
41    let mut out = String::from("face_index,v0,v1,v2\n");
42    for (fi, tri) in mesh.indices.chunks(3).enumerate() {
43        if tri.len() == 3 {
44            out.push_str(&format!("{},{},{},{}\n", fi, tri[0], tri[1], tri[2]));
45        }
46    }
47    out
48}
49
50// ---------------------------------------------------------------------------
51// Individual file exporters
52// ---------------------------------------------------------------------------
53
54/// Export vertex positions as CSV: index,x,y,z
55pub fn export_vertices_csv(mesh: &MeshBuffers, path: &Path) -> Result<()> {
56    std::fs::write(path, vertices_to_csv_string(mesh))?;
57    Ok(())
58}
59
60/// Export face indices as CSV: face_index,v0,v1,v2
61pub fn export_faces_csv(mesh: &MeshBuffers, path: &Path) -> Result<()> {
62    std::fs::write(path, faces_to_csv_string(mesh))?;
63    Ok(())
64}
65
66/// Export vertex normals as CSV: index,nx,ny,nz
67pub fn export_normals_csv(mesh: &MeshBuffers, path: &Path) -> Result<()> {
68    let mut out = String::from("index,nx,ny,nz\n");
69    for (i, n) in mesh.normals.iter().enumerate() {
70        out.push_str(&format!("{},{},{},{}\n", i, n[0], n[1], n[2]));
71    }
72    std::fs::write(path, out)?;
73    Ok(())
74}
75
76/// Export UV coordinates as CSV: index,u,v
77pub fn export_uvs_csv(mesh: &MeshBuffers, path: &Path) -> Result<()> {
78    let mut out = String::from("index,u,v\n");
79    for (i, uv) in mesh.uvs.iter().enumerate() {
80        out.push_str(&format!("{},{},{}\n", i, uv[0], uv[1]));
81    }
82    std::fs::write(path, out)?;
83    Ok(())
84}
85
86/// Export mesh statistics as a single-row CSV:
87/// header: vertex_count,face_count,has_normals,has_uvs
88pub fn export_stats_csv(mesh: &MeshBuffers, path: &Path) -> Result<()> {
89    let has_normals = !mesh.normals.is_empty();
90    let has_uvs = !mesh.uvs.is_empty();
91    let out = format!(
92        "vertex_count,face_count,has_normals,has_uvs\n{},{},{},{}\n",
93        mesh.vertex_count(),
94        mesh.face_count(),
95        has_normals,
96        has_uvs,
97    );
98    std::fs::write(path, out)?;
99    Ok(())
100}
101
102/// Export a `HashMap<String, f32>` as CSV: key,value (sorted by key)
103pub fn export_map_csv(data: &HashMap<String, f32>, path: &Path) -> Result<()> {
104    let mut out = String::from("key,value\n");
105    let mut keys: Vec<&String> = data.keys().collect();
106    keys.sort();
107    for k in keys {
108        out.push_str(&format!("{},{}\n", k, data[k]));
109    }
110    std::fs::write(path, out)?;
111    Ok(())
112}
113
114/// Export all mesh data to multiple CSV files in a directory.
115pub fn export_mesh_csv(mesh: &MeshBuffers, dir: &Path) -> Result<CsvExportReport> {
116    std::fs::create_dir_all(dir)?;
117
118    let vertices_path = dir.join("vertices.csv");
119    let faces_path = dir.join("faces.csv");
120    let normals_path = dir.join("normals.csv");
121    let uvs_path = dir.join("uvs.csv");
122
123    export_vertices_csv(mesh, &vertices_path)?;
124    export_faces_csv(mesh, &faces_path)?;
125    export_normals_csv(mesh, &normals_path)?;
126    export_uvs_csv(mesh, &uvs_path)?;
127
128    Ok(CsvExportReport {
129        vertices_path,
130        faces_path,
131        normals_path,
132        uvs_path,
133        vertex_count: mesh.vertex_count(),
134        face_count: mesh.face_count(),
135    })
136}
137
138// ---------------------------------------------------------------------------
139// Tests
140// ---------------------------------------------------------------------------
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use oxihuman_mesh::MeshBuffers;
146    use oxihuman_morph::engine::MeshBuffers as MB;
147
148    /// Simple 2-triangle (4-vertex) mesh.
149    fn two_tri_mesh() -> MeshBuffers {
150        MeshBuffers::from_morph(MB {
151            positions: vec![
152                [0.0, 0.0, 0.0],
153                [1.0, 0.0, 0.0],
154                [1.0, 1.0, 0.0],
155                [0.0, 1.0, 0.0],
156            ],
157            normals: vec![[0.0, 0.0, 1.0]; 4],
158            uvs: vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
159            indices: vec![0, 1, 2, 0, 2, 3],
160            has_suit: false,
161        })
162    }
163
164    /// Empty mesh with no vertices or indices.
165    fn empty_mesh() -> MeshBuffers {
166        MeshBuffers::from_morph(MB {
167            positions: vec![],
168            normals: vec![],
169            uvs: vec![],
170            indices: vec![],
171            has_suit: false,
172        })
173    }
174
175    #[test]
176    fn test_vertices_to_csv_string() {
177        let mesh = two_tri_mesh();
178        let csv = vertices_to_csv_string(&mesh);
179        assert!(csv.starts_with("index,x,y,z\n"));
180        assert!(csv.contains("0,0,0,0"));
181        assert!(csv.contains("1,1,0,0"));
182        assert!(csv.contains("2,1,1,0"));
183        assert!(csv.contains("3,0,1,0"));
184        let lines: Vec<&str> = csv.trim_end().lines().collect();
185        // header + 4 data rows
186        assert_eq!(lines.len(), 5);
187    }
188
189    #[test]
190    fn test_faces_to_csv_string() {
191        let mesh = two_tri_mesh();
192        let csv = faces_to_csv_string(&mesh);
193        assert!(csv.starts_with("face_index,v0,v1,v2\n"));
194        assert!(csv.contains("0,0,1,2"));
195        assert!(csv.contains("1,0,2,3"));
196        let lines: Vec<&str> = csv.trim_end().lines().collect();
197        // header + 2 face rows
198        assert_eq!(lines.len(), 3);
199    }
200
201    #[test]
202    fn test_export_vertices_csv() {
203        let mesh = two_tri_mesh();
204        let path = Path::new("/tmp/test_oxihuman_vertices.csv");
205        export_vertices_csv(&mesh, path).expect("should succeed");
206        let content = std::fs::read_to_string(path).expect("should succeed");
207        assert!(content.starts_with("index,x,y,z\n"));
208        let lines: Vec<&str> = content.trim_end().lines().collect();
209        assert_eq!(lines.len(), 5);
210    }
211
212    #[test]
213    fn test_export_faces_csv() {
214        let mesh = two_tri_mesh();
215        let path = Path::new("/tmp/test_oxihuman_faces.csv");
216        export_faces_csv(&mesh, path).expect("should succeed");
217        let content = std::fs::read_to_string(path).expect("should succeed");
218        assert!(content.starts_with("face_index,v0,v1,v2\n"));
219        let lines: Vec<&str> = content.trim_end().lines().collect();
220        assert_eq!(lines.len(), 3);
221    }
222
223    #[test]
224    fn test_export_normals_csv() {
225        let mesh = two_tri_mesh();
226        let path = Path::new("/tmp/test_oxihuman_normals.csv");
227        export_normals_csv(&mesh, path).expect("should succeed");
228        let content = std::fs::read_to_string(path).expect("should succeed");
229        assert!(content.starts_with("index,nx,ny,nz\n"));
230        // 4 normals
231        let lines: Vec<&str> = content.trim_end().lines().collect();
232        assert_eq!(lines.len(), 5);
233        assert!(content.contains("0,0,0,1"));
234    }
235
236    #[test]
237    fn test_export_uvs_csv() {
238        let mesh = two_tri_mesh();
239        let path = Path::new("/tmp/test_oxihuman_uvs.csv");
240        export_uvs_csv(&mesh, path).expect("should succeed");
241        let content = std::fs::read_to_string(path).expect("should succeed");
242        assert!(content.starts_with("index,u,v\n"));
243        let lines: Vec<&str> = content.trim_end().lines().collect();
244        // header + 4 uv rows
245        assert_eq!(lines.len(), 5);
246        assert!(content.contains("0,0,0"));
247    }
248
249    #[test]
250    fn test_export_stats_csv() {
251        let mesh = two_tri_mesh();
252        let path = Path::new("/tmp/test_oxihuman_stats.csv");
253        export_stats_csv(&mesh, path).expect("should succeed");
254        let content = std::fs::read_to_string(path).expect("should succeed");
255        assert!(content.starts_with("vertex_count,face_count,has_normals,has_uvs\n"));
256        assert!(content.contains("4,2,true,true"));
257    }
258
259    #[test]
260    fn test_export_map_csv() {
261        let mut data = HashMap::new();
262        data.insert("height".to_string(), 1.75_f32);
263        data.insert("weight".to_string(), 70.0_f32);
264        let path = Path::new("/tmp/test_oxihuman_map.csv");
265        export_map_csv(&data, path).expect("should succeed");
266        let content = std::fs::read_to_string(path).expect("should succeed");
267        assert!(content.starts_with("key,value\n"));
268        assert!(content.contains("height,"));
269        assert!(content.contains("weight,"));
270    }
271
272    #[test]
273    fn test_export_mesh_csv() {
274        let mesh = two_tri_mesh();
275        let dir = Path::new("/tmp/test_oxihuman_mesh_csv");
276        let report = export_mesh_csv(&mesh, dir).expect("should succeed");
277        assert_eq!(report.vertex_count, 4);
278        assert_eq!(report.face_count, 2);
279        assert!(report.vertices_path.exists());
280        assert!(report.faces_path.exists());
281        assert!(report.normals_path.exists());
282        assert!(report.uvs_path.exists());
283    }
284
285    #[test]
286    fn test_csv_header_format() {
287        let mesh = two_tri_mesh();
288        let v_csv = vertices_to_csv_string(&mesh);
289        let f_csv = faces_to_csv_string(&mesh);
290        assert_eq!(v_csv.lines().next().expect("should succeed"), "index,x,y,z");
291        assert_eq!(f_csv.lines().next().expect("should succeed"), "face_index,v0,v1,v2");
292    }
293
294    #[test]
295    fn test_csv_empty_mesh() {
296        let mesh = empty_mesh();
297        let v_csv = vertices_to_csv_string(&mesh);
298        let f_csv = faces_to_csv_string(&mesh);
299        // Only header line when empty
300        assert_eq!(v_csv.trim_end().lines().count(), 1);
301        assert_eq!(f_csv.trim_end().lines().count(), 1);
302
303        let stats_path = Path::new("/tmp/test_oxihuman_empty_stats.csv");
304        export_stats_csv(&mesh, stats_path).expect("should succeed");
305        let content = std::fs::read_to_string(stats_path).expect("should succeed");
306        assert!(content.contains("0,0,false,false"));
307    }
308
309    #[test]
310    fn test_export_map_csv_sorted() {
311        let mut data = HashMap::new();
312        data.insert("zebra".to_string(), 3.0_f32);
313        data.insert("apple".to_string(), 1.0_f32);
314        data.insert("mango".to_string(), 2.0_f32);
315        let path = Path::new("/tmp/test_oxihuman_map_sorted.csv");
316        export_map_csv(&data, path).expect("should succeed");
317        let content = std::fs::read_to_string(path).expect("should succeed");
318        let lines: Vec<&str> = content.lines().collect();
319        // lines[0] = header, [1] = apple, [2] = mango, [3] = zebra
320        assert_eq!(lines.len(), 4);
321        assert!(lines[1].starts_with("apple,"));
322        assert!(lines[2].starts_with("mango,"));
323        assert!(lines[3].starts_with("zebra,"));
324    }
325}