1#![allow(dead_code)]
10
11use std::io::{Read, Write};
12use std::path::Path;
13
14use anyhow::{anyhow, bail, Context};
15use oxihuman_mesh::MeshBuffers;
16
17pub const OPC_MAGIC: &[u8; 4] = b"OPC1";
19
20#[derive(Debug, Clone, PartialEq)]
22pub struct PointCacheHeader {
23 pub vertex_count: u32,
24 pub frame_count: u32,
25 pub fps: f32,
26}
27
28#[derive(Debug, Clone)]
30pub struct PointCache {
31 pub header: PointCacheHeader,
32 pub frames: Vec<Vec<[f32; 3]>>,
34}
35
36impl PointCache {
37 pub fn new(vertex_count: usize, fps: f32) -> Self {
39 Self {
40 header: PointCacheHeader {
41 vertex_count: vertex_count as u32,
42 frame_count: 0,
43 fps,
44 },
45 frames: Vec::new(),
46 }
47 }
48
49 pub fn add_frame(&mut self, positions: Vec<[f32; 3]>) -> anyhow::Result<()> {
51 if positions.len() != self.header.vertex_count as usize {
52 bail!(
53 "frame vertex count {} does not match cache vertex count {}",
54 positions.len(),
55 self.header.vertex_count
56 );
57 }
58 self.frames.push(positions);
59 self.header.frame_count = self.frames.len() as u32;
60 Ok(())
61 }
62
63 pub fn frame_count(&self) -> usize {
65 self.frames.len()
66 }
67
68 pub fn vertex_count(&self) -> usize {
70 self.header.vertex_count as usize
71 }
72
73 pub fn duration(&self) -> f32 {
75 if self.header.fps == 0.0 {
76 0.0
77 } else {
78 self.header.frame_count as f32 / self.header.fps
79 }
80 }
81
82 pub fn get_frame(&self, index: usize) -> Option<&Vec<[f32; 3]>> {
84 self.frames.get(index)
85 }
86
87 pub fn sample(&self, t: f32) -> Option<Vec<[f32; 3]>> {
91 let n = self.frames.len();
92 if n < 2 {
93 return None;
94 }
95 if t < 0.0 || t > (n - 1) as f32 {
96 return None;
97 }
98 let f = t.floor() as usize;
99 let frac = t - f as f32;
100 let f_next = (f + 1).min(n - 1);
101
102 let a = &self.frames[f];
103 let b = &self.frames[f_next];
104
105 let result = a
106 .iter()
107 .zip(b.iter())
108 .map(|(pa, pb)| {
109 [
110 pa[0] + frac * (pb[0] - pa[0]),
111 pa[1] + frac * (pb[1] - pa[1]),
112 pa[2] + frac * (pb[2] - pa[2]),
113 ]
114 })
115 .collect();
116 Some(result)
117 }
118}
119
120pub fn export_point_cache(cache: &PointCache, path: &Path) -> anyhow::Result<()> {
122 let mut file =
123 std::fs::File::create(path).with_context(|| format!("cannot create {}", path.display()))?;
124
125 file.write_all(OPC_MAGIC)?;
127 file.write_all(&cache.header.vertex_count.to_le_bytes())?;
128 file.write_all(&cache.header.frame_count.to_le_bytes())?;
129 file.write_all(&cache.header.fps.to_le_bytes())?;
130 file.write_all(&[0u8; 16])?; for frame in &cache.frames {
134 for &[x, y, z] in frame {
135 file.write_all(&x.to_le_bytes())?;
136 file.write_all(&y.to_le_bytes())?;
137 file.write_all(&z.to_le_bytes())?;
138 }
139 }
140
141 Ok(())
142}
143
144pub fn load_point_cache(path: &Path) -> anyhow::Result<PointCache> {
146 let mut file =
147 std::fs::File::open(path).with_context(|| format!("cannot open {}", path.display()))?;
148
149 let header = read_header(&mut file)?;
150
151 let vertex_count = header.vertex_count as usize;
152 let frame_count = header.frame_count as usize;
153
154 let mut frames = Vec::with_capacity(frame_count);
155 let mut buf4 = [0u8; 4];
156
157 for _ in 0..frame_count {
158 let mut verts = Vec::with_capacity(vertex_count);
159 for _ in 0..vertex_count {
160 file.read_exact(&mut buf4)?;
161 let x = f32::from_le_bytes(buf4);
162 file.read_exact(&mut buf4)?;
163 let y = f32::from_le_bytes(buf4);
164 file.read_exact(&mut buf4)?;
165 let z = f32::from_le_bytes(buf4);
166 verts.push([x, y, z]);
167 }
168 frames.push(verts);
169 }
170
171 Ok(PointCache { header, frames })
172}
173
174pub fn mesh_sequence_to_cache(frames: &[MeshBuffers], fps: f32) -> anyhow::Result<PointCache> {
176 if frames.is_empty() {
177 bail!("frame sequence is empty");
178 }
179 let vertex_count = frames[0].positions.len();
180 let mut cache = PointCache::new(vertex_count, fps);
181 for (i, mesh) in frames.iter().enumerate() {
182 if mesh.positions.len() != vertex_count {
183 bail!(
184 "frame {} has {} vertices; expected {}",
185 i,
186 mesh.positions.len(),
187 vertex_count
188 );
189 }
190 cache.add_frame(mesh.positions.clone())?;
191 }
192 Ok(cache)
193}
194
195pub fn cache_frame_to_positions(cache: &PointCache, frame: usize) -> anyhow::Result<Vec<[f32; 3]>> {
197 cache.get_frame(frame).cloned().ok_or_else(|| {
198 anyhow!(
199 "frame index {} out of range (cache has {} frames)",
200 frame,
201 cache.frame_count()
202 )
203 })
204}
205
206pub fn validate_point_cache_file(path: &Path) -> anyhow::Result<PointCacheHeader> {
208 let mut file =
209 std::fs::File::open(path).with_context(|| format!("cannot open {}", path.display()))?;
210 read_header(&mut file)
211}
212
213fn read_header<R: Read>(reader: &mut R) -> anyhow::Result<PointCacheHeader> {
218 let mut magic = [0u8; 4];
219 reader.read_exact(&mut magic)?;
220 if &magic != OPC_MAGIC {
221 bail!("invalid magic bytes: expected OPC1, got {:?}", magic);
222 }
223
224 let mut buf4 = [0u8; 4];
225
226 reader.read_exact(&mut buf4)?;
227 let vertex_count = u32::from_le_bytes(buf4);
228
229 reader.read_exact(&mut buf4)?;
230 let frame_count = u32::from_le_bytes(buf4);
231
232 reader.read_exact(&mut buf4)?;
233 let fps = f32::from_le_bytes(buf4);
234
235 let mut reserved = [0u8; 16];
237 reader.read_exact(&mut reserved)?;
238
239 Ok(PointCacheHeader {
240 vertex_count,
241 frame_count,
242 fps,
243 })
244}
245
246#[cfg(test)]
251mod tests {
252 use super::*;
253 use oxihuman_mesh::MeshBuffers;
254
255 fn make_mesh(positions: Vec<[f32; 3]>) -> MeshBuffers {
256 let n = positions.len();
257 MeshBuffers {
258 positions,
259 normals: vec![[0.0, 1.0, 0.0]; n],
260 tangents: vec![[1.0, 0.0, 0.0, 1.0]; n],
261 uvs: vec![[0.0, 0.0]; n],
262 indices: vec![],
263 colors: None,
264 has_suit: false,
265 }
266 }
267
268 #[test]
269 fn test_point_cache_new() {
270 let cache = PointCache::new(4, 24.0);
271 assert_eq!(cache.vertex_count(), 4);
272 assert_eq!(cache.frame_count(), 0);
273 assert_eq!(cache.header.fps, 24.0);
274 }
275
276 #[test]
277 fn test_add_frame() {
278 let mut cache = PointCache::new(2, 30.0);
279 let frame = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
280 cache.add_frame(frame.clone()).expect("should succeed");
281 assert_eq!(cache.frame_count(), 1);
282 assert_eq!(cache.header.frame_count, 1);
283 assert_eq!(cache.get_frame(0).expect("should succeed"), &frame);
284 }
285
286 #[test]
287 fn test_add_frame_wrong_vertex_count() {
288 let mut cache = PointCache::new(2, 30.0);
289 let bad = vec![[1.0, 2.0, 3.0]]; let result = cache.add_frame(bad);
291 assert!(result.is_err());
292 }
293
294 #[test]
295 fn test_duration() {
296 let mut cache = PointCache::new(1, 25.0);
297 cache.add_frame(vec![[0.0, 0.0, 0.0]]).expect("should succeed");
298 cache.add_frame(vec![[1.0, 1.0, 1.0]]).expect("should succeed");
299 let expected = 2.0 / 25.0;
301 assert!((cache.duration() - expected).abs() < 1e-6);
302 }
303
304 #[test]
305 fn test_get_frame() {
306 let mut cache = PointCache::new(2, 24.0);
307 let f0 = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
308 let f1 = vec![[0.0, 1.0, 0.0], [1.0, 1.0, 0.0]];
309 cache.add_frame(f0.clone()).expect("should succeed");
310 cache.add_frame(f1.clone()).expect("should succeed");
311 assert_eq!(cache.get_frame(0).expect("should succeed"), &f0);
312 assert_eq!(cache.get_frame(1).expect("should succeed"), &f1);
313 assert!(cache.get_frame(2).is_none());
314 }
315
316 #[test]
317 fn test_sample_exact_frame() {
318 let mut cache = PointCache::new(1, 24.0);
319 cache.add_frame(vec![[0.0, 0.0, 0.0]]).expect("should succeed");
320 cache.add_frame(vec![[2.0, 4.0, 6.0]]).expect("should succeed");
321 let s0 = cache.sample(0.0).expect("should succeed");
322 assert_eq!(s0[0], [0.0, 0.0, 0.0]);
323 let s1 = cache.sample(1.0).expect("should succeed");
324 assert_eq!(s1[0], [2.0, 4.0, 6.0]);
325 }
326
327 #[test]
328 fn test_sample_between_frames() {
329 let mut cache = PointCache::new(1, 24.0);
330 cache.add_frame(vec![[0.0, 0.0, 0.0]]).expect("should succeed");
331 cache.add_frame(vec![[2.0, 4.0, 6.0]]).expect("should succeed");
332 let s = cache.sample(0.5).expect("should succeed");
333 let eps = 1e-5;
334 assert!((s[0][0] - 1.0).abs() < eps);
335 assert!((s[0][1] - 2.0).abs() < eps);
336 assert!((s[0][2] - 3.0).abs() < eps);
337 }
338
339 #[test]
340 fn test_sample_out_of_range() {
341 let mut cache = PointCache::new(1, 24.0);
342 cache.add_frame(vec![[0.0, 0.0, 0.0]]).expect("should succeed");
343 cache.add_frame(vec![[1.0, 1.0, 1.0]]).expect("should succeed");
344 assert!(cache.sample(-0.1).is_none());
345 assert!(cache.sample(1.1).is_none());
346 }
347
348 #[test]
349 fn test_export_and_load() {
350 let path = std::path::Path::new("/tmp/test_export_and_load.opc");
351 let mut cache = PointCache::new(2, 30.0);
352 cache
353 .add_frame(vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
354 .expect("should succeed");
355 cache
356 .add_frame(vec![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]])
357 .expect("should succeed");
358
359 export_point_cache(&cache, path).expect("should succeed");
360 let loaded = load_point_cache(path).expect("should succeed");
361
362 assert_eq!(loaded.header.vertex_count, 2);
363 assert_eq!(loaded.header.frame_count, 2);
364 assert_eq!(loaded.header.fps, 30.0);
365 assert_eq!(loaded.frames[0], vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
366 assert_eq!(loaded.frames[1], vec![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]]);
367 }
368
369 #[test]
370 fn test_validate_header() {
371 let path = std::path::Path::new("/tmp/test_validate_header.opc");
372 let mut cache = PointCache::new(3, 60.0);
373 cache
374 .add_frame(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
375 .expect("should succeed");
376 export_point_cache(&cache, path).expect("should succeed");
377
378 let hdr = validate_point_cache_file(path).expect("should succeed");
379 assert_eq!(hdr.vertex_count, 3);
380 assert_eq!(hdr.frame_count, 1);
381 assert_eq!(hdr.fps, 60.0);
382 }
383
384 #[test]
385 fn test_validate_bad_magic() {
386 let path = std::path::Path::new("/tmp/test_validate_bad_magic.opc");
387 std::fs::write(path, b"BAAD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00").expect("should succeed");
389 let result = validate_point_cache_file(path);
390 assert!(result.is_err());
391 let msg = format!("{}", result.unwrap_err());
392 assert!(msg.contains("invalid magic bytes") || msg.contains("OPC1"));
393 }
394
395 #[test]
396 fn test_mesh_sequence_to_cache() {
397 let m0 = make_mesh(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
398 let m1 = make_mesh(vec![[0.0, 1.0, 0.0], [1.0, 1.0, 0.0]]);
399 let cache = mesh_sequence_to_cache(&[m0, m1], 24.0).expect("should succeed");
400 assert_eq!(cache.vertex_count(), 2);
401 assert_eq!(cache.frame_count(), 2);
402 assert_eq!(cache.header.fps, 24.0);
403 }
404
405 #[test]
406 fn test_mesh_sequence_mismatched_vertex_count() {
407 let m0 = make_mesh(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
408 let m1 = make_mesh(vec![[0.0, 1.0, 0.0]]); let result = mesh_sequence_to_cache(&[m0, m1], 24.0);
410 assert!(result.is_err());
411 }
412
413 #[test]
414 fn test_cache_frame_to_positions() {
415 let mut cache = PointCache::new(2, 24.0);
416 let f0 = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
417 cache.add_frame(f0.clone()).expect("should succeed");
418 let positions = cache_frame_to_positions(&cache, 0).expect("should succeed");
419 assert_eq!(positions, f0);
420 assert!(cache_frame_to_positions(&cache, 99).is_err());
421 }
422}