1#![allow(dead_code)]
5
6use std::path::Path;
7
8use anyhow::Context;
9use oxihuman_mesh::MeshBuffers;
10
11pub struct UsdAnimConfig {
15 pub start_time: f32,
16 pub end_time: f32,
17 pub fps: f32,
18 pub meters_per_unit: f32,
19}
20
21impl Default for UsdAnimConfig {
22 fn default() -> Self {
23 Self {
24 start_time: 0.0,
25 end_time: 1.0,
26 fps: 24.0,
27 meters_per_unit: 1.0,
28 }
29 }
30}
31
32pub struct UsdTimeSample {
34 pub time: f32,
35 pub positions: Vec<[f32; 3]>,
36}
37
38pub fn format_usda_point_array(positions: &[[f32; 3]]) -> String {
42 let inner: Vec<String> = positions
43 .iter()
44 .map(|p| format!("({:.6}, {:.6}, {:.6})", p[0], p[1], p[2]))
45 .collect();
46 format!("[{}]", inner.join(", "))
47}
48
49pub fn build_usda_time_samples_block(attr_name: &str, samples: &[UsdTimeSample]) -> String {
51 if samples.is_empty() {
52 return format!("{}.timeSamples = {{}}", attr_name);
53 }
54
55 let mut lines: Vec<String> = Vec::new();
56 for sample in samples {
57 let time_code = sample.time;
59 let pts = format_usda_point_array(&sample.positions);
60 lines.push(format!(" {:.4}: {},", time_code, pts));
61 }
62
63 format!(
64 "{}.timeSamples = {{\n{}\n }}",
65 attr_name,
66 lines.join("\n")
67 )
68}
69
70pub fn build_usda_animated(
74 mesh: &MeshBuffers,
75 samples: &[UsdTimeSample],
76 cfg: &UsdAnimConfig,
77) -> String {
78 let start_frame = cfg.start_time * cfg.fps;
79 let end_frame = cfg.end_time * cfg.fps;
80
81 let face_count = mesh.indices.len() / 3;
82
83 let fvc: Vec<String> = vec!["3".to_string(); face_count];
85 let fvi: Vec<String> = mesh.indices.iter().map(|i| i.to_string()).collect();
86
87 let normals_str = {
89 let inner: Vec<String> = mesh
90 .normals
91 .iter()
92 .map(|n| format!("({:.6}, {:.6}, {:.6})", n[0], n[1], n[2]))
93 .collect();
94 format!("[{}]", inner.join(", "))
95 };
96
97 let uv_str = {
99 let inner: Vec<String> = mesh
100 .uvs
101 .iter()
102 .map(|uv| format!("({:.6}, {:.6})", uv[0], uv[1]))
103 .collect();
104 format!("[{}]", inner.join(", "))
105 };
106
107 let time_samples_block = build_usda_time_samples_block("points", samples);
108
109 format!(
110 r#"#usda 1.0
111(
112 defaultPrim = "Root"
113 metersPerUnit = {meters_per_unit:.4}
114 startTimeCode = {start:.4}
115 endTimeCode = {end:.4}
116 timeCodesPerSecond = {fps:.4}
117 upAxis = "Y"
118)
119
120def Xform "Root"
121{{
122 def Mesh "Body"
123 {{
124 int[] faceVertexCounts = [{fvc}]
125 int[] faceVertexIndices = [{fvi}]
126 normal3f[] normals = {normals}
127 texCoord2f[] primvars:st = {uvs} (
128 interpolation = "vertex"
129 )
130 {time_samples}
131 }}
132}}
133"#,
134 meters_per_unit = cfg.meters_per_unit,
135 start = start_frame,
136 end = end_frame,
137 fps = cfg.fps,
138 fvc = fvc.join(", "),
139 fvi = fvi.join(", "),
140 normals = normals_str,
141 uvs = uv_str,
142 time_samples = time_samples_block,
143 )
144}
145
146pub fn export_usda_animated(
148 mesh: &MeshBuffers,
149 samples: &[UsdTimeSample],
150 cfg: &UsdAnimConfig,
151 path: &Path,
152) -> anyhow::Result<()> {
153 let content = build_usda_animated(mesh, samples, cfg);
154 std::fs::write(path, content.as_bytes())
155 .with_context(|| format!("Failed to write USD anim to {}", path.display()))?;
156 Ok(())
157}
158
159pub fn uniform_time_samples(
161 base_positions: &[[f32; 3]],
162 cfg: &UsdAnimConfig,
163) -> Vec<UsdTimeSample> {
164 let frame_count = ((cfg.end_time - cfg.start_time) * cfg.fps).ceil() as usize;
165 let frame_count = frame_count.max(1);
166
167 (0..frame_count)
168 .map(|i| {
169 let t = cfg.start_time + i as f32 / cfg.fps;
170 UsdTimeSample {
171 time: t,
172 positions: base_positions.to_vec(),
173 }
174 })
175 .collect()
176}
177
178pub fn usd_anim_stats(samples: &[UsdTimeSample]) -> String {
180 if samples.is_empty() {
181 return "UsdAnim: 0 samples".to_string();
182 }
183 let frame_count = samples.len();
184 let vert_count = samples[0].positions.len();
185 let t_start = samples.first().map(|s| s.time).unwrap_or(0.0);
186 let t_end = samples.last().map(|s| s.time).unwrap_or(0.0);
187 format!(
188 "UsdAnim: frames={}, vertices_per_frame={}, time=[{:.3}..{:.3}]",
189 frame_count, vert_count, t_start, t_end
190 )
191}
192
193#[cfg(test)]
196mod tests {
197 use super::*;
198
199 fn stub_mesh() -> MeshBuffers {
200 MeshBuffers {
201 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
202 normals: vec![[0.0, 0.0, 1.0]; 3],
203 tangents: vec![[1.0, 0.0, 0.0, 1.0]; 3],
204 uvs: vec![[0.0, 0.0]; 3],
205 indices: vec![0, 1, 2],
206 colors: None,
207 has_suit: false,
208 }
209 }
210
211 fn stub_cfg() -> UsdAnimConfig {
212 UsdAnimConfig {
213 start_time: 0.0,
214 end_time: 1.0,
215 fps: 24.0,
216 meters_per_unit: 1.0,
217 }
218 }
219
220 fn stub_samples(mesh: &MeshBuffers, n: usize) -> Vec<UsdTimeSample> {
221 (0..n)
222 .map(|i| UsdTimeSample {
223 time: i as f32 / 24.0,
224 positions: mesh.positions.clone(),
225 })
226 .collect()
227 }
228
229 #[test]
230 fn build_usda_animated_contains_time_samples() {
231 let mesh = stub_mesh();
232 let cfg = stub_cfg();
233 let samples = stub_samples(&mesh, 3);
234 let usda = build_usda_animated(&mesh, &samples, &cfg);
235 assert!(
236 usda.contains("timeSamples"),
237 "USDA must contain timeSamples"
238 );
239 }
240
241 #[test]
242 fn build_usda_animated_contains_mesh_prim() {
243 let mesh = stub_mesh();
244 let cfg = stub_cfg();
245 let usda = build_usda_animated(&mesh, &[], &cfg);
246 assert!(usda.contains("def Mesh"), "USDA must contain def Mesh");
247 assert!(usda.contains("Body"), "USDA must contain Body prim");
248 }
249
250 #[test]
251 fn build_usda_animated_contains_root_xform() {
252 let mesh = stub_mesh();
253 let cfg = stub_cfg();
254 let usda = build_usda_animated(&mesh, &[], &cfg);
255 assert!(usda.contains("def Xform \"Root\""));
256 }
257
258 #[test]
259 fn format_usda_point_array_format_check() {
260 let pts = &[[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]];
261 let s = format_usda_point_array(pts);
262 assert!(s.starts_with('['));
263 assert!(s.ends_with(']'));
264 assert!(s.contains("(1.000000, 2.000000, 3.000000)"));
265 assert!(s.contains("(4.000000, 5.000000, 6.000000)"));
266 }
267
268 #[test]
269 fn format_usda_point_array_empty() {
270 let s = format_usda_point_array(&[]);
271 assert_eq!(s, "[]");
272 }
273
274 #[test]
275 fn build_usda_time_samples_block_has_both_times() {
276 let samples = vec![
277 UsdTimeSample {
278 time: 0.0,
279 positions: vec![[0.0, 0.0, 0.0]],
280 },
281 UsdTimeSample {
282 time: 1.0,
283 positions: vec![[1.0, 0.0, 0.0]],
284 },
285 ];
286 let block = build_usda_time_samples_block("points", &samples);
287 assert!(block.contains("0.0000"), "block must contain t=0");
288 assert!(block.contains("1.0000"), "block must contain t=1");
289 assert!(block.contains("timeSamples"));
290 }
291
292 #[test]
293 fn build_usda_time_samples_block_empty() {
294 let block = build_usda_time_samples_block("points", &[]);
295 assert!(block.contains("timeSamples"));
296 assert!(block.contains("{}"));
297 }
298
299 #[test]
300 fn uniform_time_samples_frame_count() {
301 let positions = vec![[0.0f32, 0.0, 0.0]];
302 let cfg = UsdAnimConfig {
303 start_time: 0.0,
304 end_time: 1.0,
305 fps: 24.0,
306 meters_per_unit: 1.0,
307 };
308 let samples = uniform_time_samples(&positions, &cfg);
309 assert_eq!(samples.len(), 24);
311 }
312
313 #[test]
314 fn uniform_time_samples_fractional_ceil() {
315 let positions = vec![[0.0f32, 0.0, 0.0]];
316 let cfg = UsdAnimConfig {
317 start_time: 0.0,
318 end_time: 0.5,
319 fps: 24.0,
320 meters_per_unit: 1.0,
321 };
322 let samples = uniform_time_samples(&positions, &cfg);
323 assert_eq!(samples.len(), 12);
325 }
326
327 #[test]
328 fn uniform_time_samples_identical_positions() {
329 let positions = vec![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]];
330 let cfg = stub_cfg();
331 let samples = uniform_time_samples(&positions, &cfg);
332 for s in &samples {
333 assert_eq!(s.positions, positions);
334 }
335 }
336
337 #[test]
338 fn usd_anim_stats_non_empty() {
339 let mesh = stub_mesh();
340 let samples = stub_samples(&mesh, 5);
341 let s = usd_anim_stats(&samples);
342 assert!(!s.is_empty());
343 assert!(s.contains("5"));
344 }
345
346 #[test]
347 fn usd_anim_stats_empty_samples() {
348 let s = usd_anim_stats(&[]);
349 assert!(!s.is_empty());
350 assert!(s.contains('0'));
351 }
352
353 #[test]
354 fn usd_anim_stats_single_sample() {
355 let sample = UsdTimeSample {
356 time: 0.5,
357 positions: vec![[0.0f32, 0.0, 0.0]; 4],
358 };
359 let s = usd_anim_stats(&[sample]);
360 assert!(s.contains("frames=1"));
361 assert!(s.contains("vertices_per_frame=4"));
362 }
363
364 #[test]
365 fn export_usda_animated_writes_file() {
366 let mesh = stub_mesh();
367 let cfg = stub_cfg();
368 let samples = stub_samples(&mesh, 2);
369 let path = std::path::Path::new("/tmp/test_usd_anim_export.usda");
370 export_usda_animated(&mesh, &samples, &cfg, path).expect("should write file");
371 assert!(path.exists());
372 let content = std::fs::read_to_string(path).expect("should succeed");
373 assert!(content.contains("timeSamples"));
374 }
375}