1use std::path::Path;
11
12use anyhow::Result;
13use oxihuman_mesh::MeshBuffers;
14
15use crate::{export_glb, export_gltf_sep, export_json_mesh_to_file, export_obj, export_stl_binary};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum ExportFormat {
22 Glb,
23 GltfSep,
24 Obj,
25 StlAscii,
26 StlBinary,
27 JsonMesh,
28}
29
30impl ExportFormat {
31 pub fn from_extension(ext: &str) -> Option<Self> {
33 match ext.to_ascii_lowercase().as_str() {
34 "glb" => Some(ExportFormat::Glb),
35 "gltf" => Some(ExportFormat::GltfSep),
36 "obj" => Some(ExportFormat::Obj),
37 "stl" => Some(ExportFormat::StlBinary),
38 "json" => Some(ExportFormat::JsonMesh),
39 _ => None,
40 }
41 }
42
43 pub fn extension(&self) -> &'static str {
45 match self {
46 ExportFormat::Glb => "glb",
47 ExportFormat::GltfSep => "gltf",
48 ExportFormat::Obj => "obj",
49 ExportFormat::StlAscii => "stl",
50 ExportFormat::StlBinary => "stl",
51 ExportFormat::JsonMesh => "json",
52 }
53 }
54
55 pub fn name(&self) -> &'static str {
57 match self {
58 ExportFormat::Glb => "GL Binary (GLB)",
59 ExportFormat::GltfSep => "GLTF Separated",
60 ExportFormat::Obj => "Wavefront OBJ",
61 ExportFormat::StlAscii => "STL ASCII",
62 ExportFormat::StlBinary => "STL Binary",
63 ExportFormat::JsonMesh => "JSON Mesh",
64 }
65 }
66
67 pub fn supports_normals(&self) -> bool {
69 matches!(
70 self,
71 ExportFormat::Glb
72 | ExportFormat::GltfSep
73 | ExportFormat::Obj
74 | ExportFormat::StlAscii
75 | ExportFormat::StlBinary
76 )
77 }
78
79 pub fn supports_uvs(&self) -> bool {
81 matches!(
82 self,
83 ExportFormat::Glb | ExportFormat::GltfSep | ExportFormat::Obj
84 )
85 }
86
87 pub fn all() -> Vec<ExportFormat> {
89 vec![
90 ExportFormat::Glb,
91 ExportFormat::GltfSep,
92 ExportFormat::Obj,
93 ExportFormat::StlAscii,
94 ExportFormat::StlBinary,
95 ExportFormat::JsonMesh,
96 ]
97 }
98}
99
100pub struct ExportOptions {
107 pub format: ExportFormat,
109 pub recompute_normals: bool,
111 pub flip_winding: bool,
113}
114
115impl ExportOptions {
116 pub fn new(format: ExportFormat) -> Self {
117 ExportOptions {
118 format,
119 recompute_normals: false,
120 flip_winding: false,
121 }
122 }
123}
124
125fn route_export(mesh: &MeshBuffers, format: ExportFormat, path: &Path) -> Result<()> {
135 match format {
136 ExportFormat::Glb => {
137 let mut m = mesh.clone();
139 m.has_suit = true;
140 export_glb(&m, path)
141 }
142 ExportFormat::GltfSep => {
143 let bin_path = path.with_extension("bin");
145 let mut m = mesh.clone();
146 m.has_suit = true;
147 export_gltf_sep(&m, path, &bin_path)
148 }
149 ExportFormat::Obj => export_obj(mesh, path),
150 ExportFormat::StlAscii => crate::export_stl_ascii(mesh, path, "oxihuman"),
151 ExportFormat::StlBinary => export_stl_binary(mesh, path),
152 ExportFormat::JsonMesh => export_json_mesh_to_file(mesh, path),
153 }
154}
155
156pub fn export_auto(mesh: &MeshBuffers, path: &Path) -> Result<()> {
161 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
162
163 let format = ExportFormat::from_extension(ext)
164 .ok_or_else(|| anyhow::anyhow!("Unsupported export extension: {:?}", ext))?;
165
166 route_export(mesh, format, path)
167}
168
169pub fn export_with_options(mesh: &MeshBuffers, path: &Path, options: &ExportOptions) -> Result<()> {
171 let working = if options.flip_winding {
173 let mut m = mesh.clone();
174 for chunk in m.indices.chunks_exact_mut(3) {
175 chunk.swap(1, 2);
176 }
177 std::borrow::Cow::Owned(m)
178 } else {
179 std::borrow::Cow::Borrowed(mesh)
180 };
181
182 route_export(&working, options.format, path)
183}
184
185pub fn batch_export(mesh: &MeshBuffers, paths: &[&Path]) -> Vec<(std::path::PathBuf, Result<()>)> {
188 paths
189 .iter()
190 .map(|&p| (p.to_path_buf(), export_auto(mesh, p)))
191 .collect()
192}
193
194pub fn is_format_supported(ext: &str) -> bool {
196 ExportFormat::from_extension(ext).is_some()
197}
198
199pub fn supported_extensions() -> Vec<&'static str> {
201 vec!["glb", "gltf", "obj", "stl", "json"]
202}
203
204#[cfg(test)]
207mod tests {
208 use super::*;
209 use oxihuman_mesh::MeshBuffers;
210
211 fn make_mesh() -> MeshBuffers {
213 MeshBuffers {
214 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
215 normals: vec![[0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0]],
216 tangents: vec![[1.0, 0.0, 0.0, 1.0]; 3],
217 uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
218 indices: vec![0, 1, 2],
219 colors: None,
220 has_suit: false,
221 }
222 }
223
224 #[test]
225 fn format_from_extension_glb() {
226 assert_eq!(ExportFormat::from_extension("glb"), Some(ExportFormat::Glb));
227 assert_eq!(ExportFormat::from_extension("GLB"), Some(ExportFormat::Glb));
228 }
229
230 #[test]
231 fn format_from_extension_obj() {
232 assert_eq!(ExportFormat::from_extension("obj"), Some(ExportFormat::Obj));
233 assert_eq!(ExportFormat::from_extension("OBJ"), Some(ExportFormat::Obj));
234 }
235
236 #[test]
237 fn format_from_extension_stl() {
238 assert_eq!(
239 ExportFormat::from_extension("stl"),
240 Some(ExportFormat::StlBinary)
241 );
242 assert_eq!(
243 ExportFormat::from_extension("STL"),
244 Some(ExportFormat::StlBinary)
245 );
246 }
247
248 #[test]
249 fn format_from_extension_unknown_returns_none() {
250 assert_eq!(ExportFormat::from_extension("fbx"), None);
251 assert_eq!(ExportFormat::from_extension(""), None);
252 assert_eq!(ExportFormat::from_extension("blend"), None);
253 }
254
255 #[test]
256 fn format_extension_roundtrip() {
257 for fmt in ExportFormat::all() {
258 let ext = fmt.extension();
259 let detected = ExportFormat::from_extension(ext);
262 assert!(detected.is_some(), "extension {ext} was not detected");
263 }
264 }
265
266 #[test]
267 fn format_all_has_multiple() {
268 let all = ExportFormat::all();
269 assert!(all.len() >= 5);
270 }
271
272 #[test]
273 fn is_format_supported_true_for_glb() {
274 assert!(is_format_supported("glb"));
275 assert!(is_format_supported("obj"));
276 assert!(!is_format_supported("fbx"));
277 }
278
279 #[test]
280 fn supported_extensions_not_empty() {
281 let exts = supported_extensions();
282 assert!(!exts.is_empty());
283 assert!(exts.contains(&"glb"));
284 assert!(exts.contains(&"obj"));
285 assert!(exts.contains(&"stl"));
286 }
287
288 #[test]
289 fn export_auto_glb_creates_file() {
290 let mesh = make_mesh();
291 let path = std::path::Path::new("/tmp/test_auto_export_glb.glb");
292 export_auto(&mesh, path).expect("export_auto glb failed");
293 assert!(path.exists(), "GLB file was not created");
294 }
295
296 #[test]
297 fn export_auto_obj_creates_file() {
298 let mesh = make_mesh();
299 let path = std::path::Path::new("/tmp/test_auto_export_obj.obj");
300 export_auto(&mesh, path).expect("export_auto obj failed");
301 assert!(path.exists(), "OBJ file was not created");
302 }
303
304 #[test]
305 fn export_auto_stl_creates_file() {
306 let mesh = make_mesh();
307 let path = std::path::Path::new("/tmp/test_auto_export_stl.stl");
308 export_auto(&mesh, path).expect("export_auto stl failed");
309 assert!(path.exists(), "STL file was not created");
310 }
311
312 #[test]
313 fn batch_export_multiple_formats() {
314 let mesh = make_mesh();
315 let glb_path = std::path::Path::new("/tmp/test_auto_export_batch.glb");
316 let obj_path = std::path::Path::new("/tmp/test_auto_export_batch.obj");
317 let stl_path = std::path::Path::new("/tmp/test_auto_export_batch.stl");
318 let json_path = std::path::Path::new("/tmp/test_auto_export_batch.json");
319
320 let results = batch_export(&mesh, &[glb_path, obj_path, stl_path, json_path]);
321 assert_eq!(results.len(), 4);
322 for (path, result) in &results {
323 assert!(result.is_ok(), "batch_export failed for {:?}", path);
324 assert!(path.exists(), "file not created: {:?}", path);
325 }
326 }
327
328 #[test]
329 fn export_auto_unknown_extension_errors() {
330 let mesh = make_mesh();
331 let path = std::path::Path::new("/tmp/test_auto_export_bad.fbx");
332 let result = export_auto(&mesh, path);
333 assert!(result.is_err(), "expected Err for unknown extension");
334 }
335
336 #[test]
337 fn export_with_options_flip_winding() {
338 let mesh = make_mesh();
339 let path = std::path::Path::new("/tmp/test_auto_export_opts.obj");
340 let opts = ExportOptions {
341 format: ExportFormat::Obj,
342 recompute_normals: false,
343 flip_winding: true,
344 };
345 export_with_options(&mesh, path, &opts).expect("export_with_options failed");
346 assert!(path.exists());
347 }
348
349 #[test]
350 fn format_name_not_empty() {
351 for fmt in ExportFormat::all() {
352 assert!(!fmt.name().is_empty(), "name() was empty for {:?}", fmt);
353 }
354 }
355
356 #[test]
357 fn format_supports_normals_glb() {
358 assert!(ExportFormat::Glb.supports_normals());
359 assert!(ExportFormat::Obj.supports_normals());
360 assert!(ExportFormat::StlBinary.supports_normals());
361 }
362
363 #[test]
364 fn format_supports_uvs_glb_obj_gltf() {
365 assert!(ExportFormat::Glb.supports_uvs());
366 assert!(ExportFormat::GltfSep.supports_uvs());
367 assert!(ExportFormat::Obj.supports_uvs());
368 assert!(!ExportFormat::StlBinary.supports_uvs());
369 assert!(!ExportFormat::JsonMesh.supports_uvs());
370 }
371}