use anyhow::Result;
use oxihuman_mesh::mesh::MeshBuffers;
use std::fmt::Write as FmtWrite;
use std::path::Path;
pub fn export_obj(mesh: &MeshBuffers, path: &Path) -> Result<()> {
let content = mesh_to_obj_string(mesh)?;
std::fs::write(path, content)?;
Ok(())
}
pub fn mesh_to_obj_string(mesh: &MeshBuffers) -> Result<String> {
let mut out = String::new();
writeln!(out, "# OxiHuman exported mesh")?;
writeln!(out, "# Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)")?;
writeln!(out, "# Vertices: {}", mesh.positions.len())?;
writeln!(out, "# Faces: {}", mesh.indices.len() / 3)?;
writeln!(out)?;
for p in &mesh.positions {
writeln!(out, "v {:.6} {:.6} {:.6}", p[0], p[1], p[2])?;
}
writeln!(out)?;
for uv in &mesh.uvs {
writeln!(out, "vt {:.6} {:.6}", uv[0], uv[1])?;
}
writeln!(out)?;
for n in &mesh.normals {
writeln!(out, "vn {:.6} {:.6} {:.6}", n[0], n[1], n[2])?;
}
writeln!(out)?;
let has_uvs = !mesh.uvs.is_empty();
let has_norms = !mesh.normals.is_empty();
for tri in mesh.indices.chunks_exact(3) {
let (i0, i1, i2) = (tri[0] + 1, tri[1] + 1, tri[2] + 1); if has_uvs && has_norms {
writeln!(out, "f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}", i0, i1, i2)?;
} else if has_uvs {
writeln!(out, "f {0}/{0} {1}/{1} {2}/{2}", i0, i1, i2)?;
} else {
writeln!(out, "f {} {} {}", i0, i1, i2)?;
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use oxihuman_mesh::mesh::MeshBuffers;
use oxihuman_morph::engine::MeshBuffers as MB;
fn triangle_mesh() -> MeshBuffers {
MeshBuffers::from_morph(MB {
positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
normals: vec![[0.0, 0.0, 1.0]; 3],
uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
indices: vec![0, 1, 2],
has_suit: false,
})
}
#[test]
fn obj_string_has_vertex_lines() {
let m = triangle_mesh();
let s = mesh_to_obj_string(&m).expect("should succeed");
assert!(s.contains("v 0.000000 0.000000 0.000000"));
assert!(s.contains("v 1.000000 0.000000 0.000000"));
let v_count = s.lines().filter(|l| l.starts_with("v ")).count();
assert_eq!(v_count, 3);
}
#[test]
fn obj_string_has_face_lines() {
let m = triangle_mesh();
let s = mesh_to_obj_string(&m).expect("should succeed");
let f_count = s.lines().filter(|l| l.starts_with("f ")).count();
assert_eq!(f_count, 1);
}
#[test]
fn export_obj_creates_file() {
let m = triangle_mesh();
let path = std::path::PathBuf::from("/tmp/test_oxihuman.obj");
export_obj(&m, &path).expect("should succeed");
assert!(path.exists());
let content = std::fs::read_to_string(&path).expect("should succeed");
assert!(content.contains("v "));
std::fs::remove_file(&path).ok();
}
#[test]
fn obj_indices_are_one_based() {
let m = triangle_mesh();
let s = mesh_to_obj_string(&m).expect("should succeed");
assert!(s.contains("f 1/1/1 2/2/2 3/3/3"));
}
}