use std::path::Path;
use anyhow::Result;
use oxihuman_mesh::MeshBuffers;
use crate::{export_glb, export_gltf_sep, export_json_mesh_to_file, export_obj, export_stl_binary};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExportFormat {
Glb,
GltfSep,
Obj,
StlAscii,
StlBinary,
JsonMesh,
}
impl ExportFormat {
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_ascii_lowercase().as_str() {
"glb" => Some(ExportFormat::Glb),
"gltf" => Some(ExportFormat::GltfSep),
"obj" => Some(ExportFormat::Obj),
"stl" => Some(ExportFormat::StlBinary),
"json" => Some(ExportFormat::JsonMesh),
_ => None,
}
}
pub fn extension(&self) -> &'static str {
match self {
ExportFormat::Glb => "glb",
ExportFormat::GltfSep => "gltf",
ExportFormat::Obj => "obj",
ExportFormat::StlAscii => "stl",
ExportFormat::StlBinary => "stl",
ExportFormat::JsonMesh => "json",
}
}
pub fn name(&self) -> &'static str {
match self {
ExportFormat::Glb => "GL Binary (GLB)",
ExportFormat::GltfSep => "GLTF Separated",
ExportFormat::Obj => "Wavefront OBJ",
ExportFormat::StlAscii => "STL ASCII",
ExportFormat::StlBinary => "STL Binary",
ExportFormat::JsonMesh => "JSON Mesh",
}
}
pub fn supports_normals(&self) -> bool {
matches!(
self,
ExportFormat::Glb
| ExportFormat::GltfSep
| ExportFormat::Obj
| ExportFormat::StlAscii
| ExportFormat::StlBinary
)
}
pub fn supports_uvs(&self) -> bool {
matches!(
self,
ExportFormat::Glb | ExportFormat::GltfSep | ExportFormat::Obj
)
}
pub fn all() -> Vec<ExportFormat> {
vec![
ExportFormat::Glb,
ExportFormat::GltfSep,
ExportFormat::Obj,
ExportFormat::StlAscii,
ExportFormat::StlBinary,
ExportFormat::JsonMesh,
]
}
}
pub struct ExportOptions {
pub format: ExportFormat,
pub recompute_normals: bool,
pub flip_winding: bool,
}
impl ExportOptions {
pub fn new(format: ExportFormat) -> Self {
ExportOptions {
format,
recompute_normals: false,
flip_winding: false,
}
}
}
fn route_export(mesh: &MeshBuffers, format: ExportFormat, path: &Path) -> Result<()> {
match format {
ExportFormat::Glb => {
let mut m = mesh.clone();
m.has_suit = true;
export_glb(&m, path)
}
ExportFormat::GltfSep => {
let bin_path = path.with_extension("bin");
let mut m = mesh.clone();
m.has_suit = true;
export_gltf_sep(&m, path, &bin_path)
}
ExportFormat::Obj => export_obj(mesh, path),
ExportFormat::StlAscii => crate::export_stl_ascii(mesh, path, "oxihuman"),
ExportFormat::StlBinary => export_stl_binary(mesh, path),
ExportFormat::JsonMesh => export_json_mesh_to_file(mesh, path),
}
}
pub fn export_auto(mesh: &MeshBuffers, path: &Path) -> Result<()> {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let format = ExportFormat::from_extension(ext)
.ok_or_else(|| anyhow::anyhow!("Unsupported export extension: {:?}", ext))?;
route_export(mesh, format, path)
}
pub fn export_with_options(mesh: &MeshBuffers, path: &Path, options: &ExportOptions) -> Result<()> {
let working = if options.flip_winding {
let mut m = mesh.clone();
for chunk in m.indices.chunks_exact_mut(3) {
chunk.swap(1, 2);
}
std::borrow::Cow::Owned(m)
} else {
std::borrow::Cow::Borrowed(mesh)
};
route_export(&working, options.format, path)
}
pub fn batch_export(mesh: &MeshBuffers, paths: &[&Path]) -> Vec<(std::path::PathBuf, Result<()>)> {
paths
.iter()
.map(|&p| (p.to_path_buf(), export_auto(mesh, p)))
.collect()
}
pub fn is_format_supported(ext: &str) -> bool {
ExportFormat::from_extension(ext).is_some()
}
pub fn supported_extensions() -> Vec<&'static str> {
vec!["glb", "gltf", "obj", "stl", "json"]
}
#[cfg(test)]
mod tests {
use super::*;
use oxihuman_mesh::MeshBuffers;
fn make_mesh() -> MeshBuffers {
MeshBuffers {
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], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0]],
tangents: vec![[1.0, 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],
colors: None,
has_suit: false,
}
}
#[test]
fn format_from_extension_glb() {
assert_eq!(ExportFormat::from_extension("glb"), Some(ExportFormat::Glb));
assert_eq!(ExportFormat::from_extension("GLB"), Some(ExportFormat::Glb));
}
#[test]
fn format_from_extension_obj() {
assert_eq!(ExportFormat::from_extension("obj"), Some(ExportFormat::Obj));
assert_eq!(ExportFormat::from_extension("OBJ"), Some(ExportFormat::Obj));
}
#[test]
fn format_from_extension_stl() {
assert_eq!(
ExportFormat::from_extension("stl"),
Some(ExportFormat::StlBinary)
);
assert_eq!(
ExportFormat::from_extension("STL"),
Some(ExportFormat::StlBinary)
);
}
#[test]
fn format_from_extension_unknown_returns_none() {
assert_eq!(ExportFormat::from_extension("fbx"), None);
assert_eq!(ExportFormat::from_extension(""), None);
assert_eq!(ExportFormat::from_extension("blend"), None);
}
#[test]
fn format_extension_roundtrip() {
for fmt in ExportFormat::all() {
let ext = fmt.extension();
let detected = ExportFormat::from_extension(ext);
assert!(detected.is_some(), "extension {ext} was not detected");
}
}
#[test]
fn format_all_has_multiple() {
let all = ExportFormat::all();
assert!(all.len() >= 5);
}
#[test]
fn is_format_supported_true_for_glb() {
assert!(is_format_supported("glb"));
assert!(is_format_supported("obj"));
assert!(!is_format_supported("fbx"));
}
#[test]
fn supported_extensions_not_empty() {
let exts = supported_extensions();
assert!(!exts.is_empty());
assert!(exts.contains(&"glb"));
assert!(exts.contains(&"obj"));
assert!(exts.contains(&"stl"));
}
#[test]
fn export_auto_glb_creates_file() {
let mesh = make_mesh();
let path = std::path::Path::new("/tmp/test_auto_export_glb.glb");
export_auto(&mesh, path).expect("export_auto glb failed");
assert!(path.exists(), "GLB file was not created");
}
#[test]
fn export_auto_obj_creates_file() {
let mesh = make_mesh();
let path = std::path::Path::new("/tmp/test_auto_export_obj.obj");
export_auto(&mesh, path).expect("export_auto obj failed");
assert!(path.exists(), "OBJ file was not created");
}
#[test]
fn export_auto_stl_creates_file() {
let mesh = make_mesh();
let path = std::path::Path::new("/tmp/test_auto_export_stl.stl");
export_auto(&mesh, path).expect("export_auto stl failed");
assert!(path.exists(), "STL file was not created");
}
#[test]
fn batch_export_multiple_formats() {
let mesh = make_mesh();
let glb_path = std::path::Path::new("/tmp/test_auto_export_batch.glb");
let obj_path = std::path::Path::new("/tmp/test_auto_export_batch.obj");
let stl_path = std::path::Path::new("/tmp/test_auto_export_batch.stl");
let json_path = std::path::Path::new("/tmp/test_auto_export_batch.json");
let results = batch_export(&mesh, &[glb_path, obj_path, stl_path, json_path]);
assert_eq!(results.len(), 4);
for (path, result) in &results {
assert!(result.is_ok(), "batch_export failed for {:?}", path);
assert!(path.exists(), "file not created: {:?}", path);
}
}
#[test]
fn export_auto_unknown_extension_errors() {
let mesh = make_mesh();
let path = std::path::Path::new("/tmp/test_auto_export_bad.fbx");
let result = export_auto(&mesh, path);
assert!(result.is_err(), "expected Err for unknown extension");
}
#[test]
fn export_with_options_flip_winding() {
let mesh = make_mesh();
let path = std::path::Path::new("/tmp/test_auto_export_opts.obj");
let opts = ExportOptions {
format: ExportFormat::Obj,
recompute_normals: false,
flip_winding: true,
};
export_with_options(&mesh, path, &opts).expect("export_with_options failed");
assert!(path.exists());
}
#[test]
fn format_name_not_empty() {
for fmt in ExportFormat::all() {
assert!(!fmt.name().is_empty(), "name() was empty for {:?}", fmt);
}
}
#[test]
fn format_supports_normals_glb() {
assert!(ExportFormat::Glb.supports_normals());
assert!(ExportFormat::Obj.supports_normals());
assert!(ExportFormat::StlBinary.supports_normals());
}
#[test]
fn format_supports_uvs_glb_obj_gltf() {
assert!(ExportFormat::Glb.supports_uvs());
assert!(ExportFormat::GltfSep.supports_uvs());
assert!(ExportFormat::Obj.supports_uvs());
assert!(!ExportFormat::StlBinary.supports_uvs());
assert!(!ExportFormat::JsonMesh.supports_uvs());
}
}