1#![allow(dead_code)]
31
32use std::io::{Read, Write};
33use std::path::Path;
34
35use anyhow::{bail, Context};
36use oxihuman_mesh::MeshBuffers;
37
38pub const OXGC_MAGIC: [u8; 4] = *b"OXGC";
44
45pub const OXGC_VERSION: u16 = 1;
47
48const HEADER_SIZE: usize = 96;
50
51#[derive(Debug, Clone, PartialEq)]
57pub struct GeoCacheFrame {
58 pub frame_index: u32,
60 pub time_seconds: f32,
62 pub positions: Vec<[f32; 3]>,
64 pub normals: Option<Vec<[f32; 3]>>,
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct GeoCacheHeader {
71 pub magic: [u8; 4],
72 pub version: u16,
73 pub vertex_count: u32,
74 pub frame_count: u32,
75 pub fps: f32,
76 pub has_normals: bool,
77 pub name: [u8; 64],
79}
80
81#[derive(Debug, Clone)]
83pub struct GeoCache {
84 pub name: String,
86 pub fps: f32,
88 pub vertex_count: usize,
90 pub frames: Vec<GeoCacheFrame>,
92}
93
94impl GeoCache {
99 pub fn new(name: &str, fps: f32, vertex_count: usize) -> Self {
101 Self {
102 name: name.to_owned(),
103 fps,
104 vertex_count,
105 frames: Vec::new(),
106 }
107 }
108
109 pub fn add_frame(&mut self, frame: GeoCacheFrame) -> Result<(), String> {
112 if frame.positions.len() != self.vertex_count {
113 return Err(format!(
114 "frame {} has {} positions; cache expects {}",
115 frame.frame_index,
116 frame.positions.len(),
117 self.vertex_count
118 ));
119 }
120 if let Some(ref n) = frame.normals {
121 if n.len() != self.vertex_count {
122 return Err(format!(
123 "frame {} has {} normals; cache expects {}",
124 frame.frame_index,
125 n.len(),
126 self.vertex_count
127 ));
128 }
129 }
130 if !self.frames.is_empty() {
132 let first_has = self.frames[0].normals.is_some();
133 let this_has = frame.normals.is_some();
134 if first_has != this_has {
135 return Err(format!(
136 "frame {} normals presence ({}) differs from earlier frames ({})",
137 frame.frame_index, this_has, first_has
138 ));
139 }
140 }
141 self.frames.push(frame);
142 Ok(())
143 }
144
145 pub fn frame_count(&self) -> usize {
147 self.frames.len()
148 }
149
150 pub fn duration_seconds(&self) -> f32 {
152 self.frames.last().map(|f| f.time_seconds).unwrap_or(0.0)
153 }
154
155 pub fn get_frame(&self, index: usize) -> Option<&GeoCacheFrame> {
157 self.frames.get(index)
158 }
159
160 pub fn sample(&self, time_seconds: f32) -> Option<Vec<[f32; 3]>> {
165 let n = self.frames.len();
166 if n == 0 {
167 return None;
168 }
169 if n == 1 {
170 return Some(self.frames[0].positions.clone());
171 }
172
173 let t = time_seconds.clamp(self.frames[0].time_seconds, self.frames[n - 1].time_seconds);
175
176 let idx = self
178 .frames
179 .partition_point(|f| f.time_seconds <= t)
180 .saturating_sub(1)
181 .min(n - 2);
182
183 let fa = &self.frames[idx];
184 let fb = &self.frames[idx + 1];
185
186 let dt = fb.time_seconds - fa.time_seconds;
187 let alpha = if dt.abs() < f32::EPSILON {
188 0.0
189 } else {
190 ((t - fa.time_seconds) / dt).clamp(0.0, 1.0)
191 };
192
193 let result = fa
194 .positions
195 .iter()
196 .zip(fb.positions.iter())
197 .map(|(a, b)| {
198 [
199 a[0] + alpha * (b[0] - a[0]),
200 a[1] + alpha * (b[1] - a[1]),
201 a[2] + alpha * (b[2] - a[2]),
202 ]
203 })
204 .collect();
205 Some(result)
206 }
207
208 pub fn write(&self, path: &Path) -> anyhow::Result<()> {
210 export_geo_cache(self, path)
211 }
212
213 pub fn read(path: &Path) -> anyhow::Result<Self> {
215 load_geo_cache(path)
216 }
217
218 pub fn validate(path: &Path) -> anyhow::Result<()> {
220 validate_geo_cache_file(path)
221 }
222}
223
224pub fn mesh_sequence_to_geo_cache(name: &str, fps: f32, frames: &[MeshBuffers]) -> GeoCache {
232 let vertex_count = frames.first().map(|m| m.positions.len()).unwrap_or(0);
233 let mut cache = GeoCache::new(name, fps, vertex_count);
234
235 for (i, mesh) in frames.iter().enumerate() {
236 let time_seconds = i as f32 / fps.max(f32::EPSILON);
237 let has_normals = !mesh.normals.is_empty() && mesh.normals.len() == mesh.positions.len();
238 let normals = if has_normals {
239 Some(mesh.normals.clone())
240 } else {
241 None
242 };
243 let frame = GeoCacheFrame {
244 frame_index: i as u32,
245 time_seconds,
246 positions: mesh.positions.clone(),
247 normals,
248 };
249 let _ = cache.add_frame(frame);
251 }
252
253 cache
254}
255
256pub fn export_geo_cache(cache: &GeoCache, path: &Path) -> anyhow::Result<()> {
262 let mut file =
263 std::fs::File::create(path).with_context(|| format!("cannot create {}", path.display()))?;
264
265 let has_normals = cache
266 .frames
267 .first()
268 .map(|f| f.normals.is_some())
269 .unwrap_or(false);
270
271 write_header(&mut file, cache, has_normals)?;
272
273 for frame in &cache.frames {
274 write_frame(&mut file, frame, has_normals, cache.vertex_count)?;
275 }
276
277 Ok(())
278}
279
280pub fn load_geo_cache(path: &Path) -> anyhow::Result<GeoCache> {
282 let mut file =
283 std::fs::File::open(path).with_context(|| format!("cannot open {}", path.display()))?;
284
285 let header = read_header(&mut file)?;
286
287 let name = bytes_to_name(&header.name);
288 let vertex_count = header.vertex_count as usize;
289 let frame_count = header.frame_count as usize;
290 let has_normals = header.has_normals;
291
292 let mut cache = GeoCache::new(&name, header.fps, vertex_count);
293
294 for _ in 0..frame_count {
295 let frame = read_frame(&mut file, vertex_count, has_normals)?;
296 cache.frames.push(frame);
297 }
298
299 Ok(cache)
300}
301
302pub fn validate_geo_cache_file(path: &Path) -> anyhow::Result<()> {
304 let mut file =
305 std::fs::File::open(path).with_context(|| format!("cannot open {}", path.display()))?;
306
307 let header = read_header(&mut file)?;
308
309 if header.version != OXGC_VERSION {
311 bail!(
312 "unsupported OXGC version {} (expected {})",
313 header.version,
314 OXGC_VERSION
315 );
316 }
317
318 let name = bytes_to_name(&header.name);
320 if name.len() > 64 {
321 bail!("name field exceeds 64 bytes");
322 }
323
324 if header.frame_count > 0 {
326 let mut buf4 = [0u8; 4];
327 file.read_exact(&mut buf4)
328 .with_context(|| "could not read first frame_index")?;
329 let _frame_index = u32::from_le_bytes(buf4);
330 file.read_exact(&mut buf4)
331 .with_context(|| "could not read first frame time")?;
332 let _time = f32::from_le_bytes(buf4);
333 }
334
335 Ok(())
336}
337
338fn name_to_bytes(name: &str) -> [u8; 64] {
343 let mut buf = [0u8; 64];
344 let bytes = name.as_bytes();
345 let len = bytes.len().min(63);
346 buf[..len].copy_from_slice(&bytes[..len]);
347 buf
348}
349
350fn bytes_to_name(buf: &[u8; 64]) -> String {
351 let end = buf.iter().position(|&b| b == 0).unwrap_or(64);
352 String::from_utf8_lossy(&buf[..end]).into_owned()
353}
354
355fn write_header<W: Write>(
356 writer: &mut W,
357 cache: &GeoCache,
358 has_normals: bool,
359) -> anyhow::Result<()> {
360 writer.write_all(&OXGC_MAGIC)?;
362 writer.write_all(&OXGC_VERSION.to_le_bytes())?;
364 writer.write_all(&[0u8; 2])?;
365 writer.write_all(&(cache.vertex_count as u32).to_le_bytes())?;
367 writer.write_all(&(cache.frames.len() as u32).to_le_bytes())?;
369 writer.write_all(&cache.fps.to_le_bytes())?;
371 writer.write_all(&[u8::from(has_normals)])?;
373 writer.write_all(&[0u8; 3])?;
374 writer.write_all(&name_to_bytes(&cache.name))?;
376 writer.write_all(&[0u8; 4])?;
378
379 writer.write_all(&[0u8; 4])?; Ok(())
383}
384
385fn read_header<R: Read>(reader: &mut R) -> anyhow::Result<GeoCacheHeader> {
386 let mut magic = [0u8; 4];
387 reader.read_exact(&mut magic)?;
388 if magic != OXGC_MAGIC {
389 bail!(
390 "invalid magic bytes: expected OXGC, got {:?}",
391 std::str::from_utf8(&magic).unwrap_or("???")
392 );
393 }
394
395 let mut buf2 = [0u8; 2];
396 let mut buf4 = [0u8; 4];
397
398 reader.read_exact(&mut buf2)?;
400 let version = u16::from_le_bytes(buf2);
401 reader.read_exact(&mut buf2)?;
403
404 reader.read_exact(&mut buf4)?;
406 let vertex_count = u32::from_le_bytes(buf4);
407
408 reader.read_exact(&mut buf4)?;
410 let frame_count = u32::from_le_bytes(buf4);
411
412 reader.read_exact(&mut buf4)?;
414 let fps = f32::from_le_bytes(buf4);
415
416 let mut buf1 = [0u8; 1];
418 reader.read_exact(&mut buf1)?;
419 let has_normals = buf1[0] != 0;
420 let mut pad3 = [0u8; 3];
421 reader.read_exact(&mut pad3)?;
422
423 let mut name = [0u8; 64];
425 reader.read_exact(&mut name)?;
426
427 let mut reserved = [0u8; 8];
429 reader.read_exact(&mut reserved)?;
430
431 Ok(GeoCacheHeader {
432 magic,
433 version,
434 vertex_count,
435 frame_count,
436 fps,
437 has_normals,
438 name,
439 })
440}
441
442fn write_frame<W: Write>(
443 writer: &mut W,
444 frame: &GeoCacheFrame,
445 has_normals: bool,
446 vertex_count: usize,
447) -> anyhow::Result<()> {
448 writer.write_all(&frame.frame_index.to_le_bytes())?;
449 writer.write_all(&frame.time_seconds.to_le_bytes())?;
450
451 for &[x, y, z] in &frame.positions {
452 writer.write_all(&x.to_le_bytes())?;
453 writer.write_all(&y.to_le_bytes())?;
454 writer.write_all(&z.to_le_bytes())?;
455 }
456
457 if has_normals {
458 if let Some(ref normals) = frame.normals {
459 for &[nx, ny, nz] in normals {
460 writer.write_all(&nx.to_le_bytes())?;
461 writer.write_all(&ny.to_le_bytes())?;
462 writer.write_all(&nz.to_le_bytes())?;
463 }
464 } else {
465 for _ in 0..vertex_count {
467 writer.write_all(&0f32.to_le_bytes())?;
468 writer.write_all(&1f32.to_le_bytes())?;
469 writer.write_all(&0f32.to_le_bytes())?;
470 }
471 }
472 }
473
474 Ok(())
475}
476
477fn read_frame<R: Read>(
478 reader: &mut R,
479 vertex_count: usize,
480 has_normals: bool,
481) -> anyhow::Result<GeoCacheFrame> {
482 let mut buf4 = [0u8; 4];
483
484 reader.read_exact(&mut buf4)?;
485 let frame_index = u32::from_le_bytes(buf4);
486
487 reader.read_exact(&mut buf4)?;
488 let time_seconds = f32::from_le_bytes(buf4);
489
490 let mut positions = Vec::with_capacity(vertex_count);
491 for _ in 0..vertex_count {
492 reader.read_exact(&mut buf4)?;
493 let x = f32::from_le_bytes(buf4);
494 reader.read_exact(&mut buf4)?;
495 let y = f32::from_le_bytes(buf4);
496 reader.read_exact(&mut buf4)?;
497 let z = f32::from_le_bytes(buf4);
498 positions.push([x, y, z]);
499 }
500
501 let normals = if has_normals {
502 let mut nrm = Vec::with_capacity(vertex_count);
503 for _ in 0..vertex_count {
504 reader.read_exact(&mut buf4)?;
505 let nx = f32::from_le_bytes(buf4);
506 reader.read_exact(&mut buf4)?;
507 let ny = f32::from_le_bytes(buf4);
508 reader.read_exact(&mut buf4)?;
509 let nz = f32::from_le_bytes(buf4);
510 nrm.push([nx, ny, nz]);
511 }
512 Some(nrm)
513 } else {
514 None
515 };
516
517 Ok(GeoCacheFrame {
518 frame_index,
519 time_seconds,
520 positions,
521 normals,
522 })
523}
524
525#[cfg(test)]
530mod tests {
531 use super::*;
532
533 fn make_frame(
538 index: u32,
539 time: f32,
540 positions: Vec<[f32; 3]>,
541 normals: Option<Vec<[f32; 3]>>,
542 ) -> GeoCacheFrame {
543 GeoCacheFrame {
544 frame_index: index,
545 time_seconds: time,
546 positions,
547 normals,
548 }
549 }
550
551 fn make_mesh_buffers(positions: Vec<[f32; 3]>) -> MeshBuffers {
552 let n = positions.len();
553 MeshBuffers {
554 positions,
555 normals: vec![[0.0, 1.0, 0.0]; n],
556 tangents: vec![[1.0, 0.0, 0.0, 1.0]; n],
557 uvs: vec![[0.0, 0.0]; n],
558 indices: vec![],
559 colors: None,
560 has_suit: false,
561 }
562 }
563
564 #[test]
569 fn test_constants() {
570 assert_eq!(&OXGC_MAGIC, b"OXGC");
571 assert_eq!(OXGC_VERSION, 1);
572 }
573
574 #[test]
579 fn test_new_cache() {
580 let cache = GeoCache::new("TestCache", 30.0, 8);
581 assert_eq!(cache.name, "TestCache");
582 assert_eq!(cache.fps, 30.0);
583 assert_eq!(cache.vertex_count, 8);
584 assert_eq!(cache.frame_count(), 0);
585 }
586
587 #[test]
592 fn test_add_frame_success() {
593 let mut cache = GeoCache::new("A", 24.0, 2);
594 let f = make_frame(0, 0.0, vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], None);
595 cache.add_frame(f).expect("should succeed");
596 assert_eq!(cache.frame_count(), 1);
597 }
598
599 #[test]
604 fn test_add_frame_wrong_vertex_count() {
605 let mut cache = GeoCache::new("A", 24.0, 3);
606 let f = make_frame(0, 0.0, vec![[0.0, 0.0, 0.0]], None); assert!(cache.add_frame(f).is_err());
608 }
609
610 #[test]
615 fn test_add_frame_normals_consistency() {
616 let mut cache = GeoCache::new("B", 24.0, 2);
617 let f0 = make_frame(0, 0.0, vec![[0.0; 3], [1.0; 3]], None);
618 let f1 = make_frame(
619 1,
620 1.0 / 24.0,
621 vec![[0.0; 3], [1.0; 3]],
622 Some(vec![[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]]),
623 );
624 cache.add_frame(f0).expect("should succeed");
625 assert!(cache.add_frame(f1).is_err());
627 }
628
629 #[test]
634 fn test_duration_seconds() {
635 let mut cache = GeoCache::new("D", 25.0, 1);
636 assert_eq!(cache.duration_seconds(), 0.0);
637 cache
638 .add_frame(make_frame(0, 0.0, vec![[0.0; 3]], None))
639 .expect("should succeed");
640 cache
641 .add_frame(make_frame(1, 1.0 / 25.0, vec![[1.0; 3]], None))
642 .expect("should succeed");
643 let expected = 1.0f32 / 25.0;
644 assert!((cache.duration_seconds() - expected).abs() < 1e-6);
645 }
646
647 #[test]
652 fn test_get_frame() {
653 let mut cache = GeoCache::new("G", 24.0, 1);
654 cache
655 .add_frame(make_frame(0, 0.0, vec![[1.0, 2.0, 3.0]], None))
656 .expect("should succeed");
657 let f = cache.get_frame(0).expect("should succeed");
658 assert_eq!(f.positions[0], [1.0, 2.0, 3.0]);
659 assert!(cache.get_frame(1).is_none());
660 }
661
662 #[test]
667 fn test_sample_interpolation() {
668 let mut cache = GeoCache::new("S", 24.0, 1);
669 cache
670 .add_frame(make_frame(0, 0.0, vec![[0.0, 0.0, 0.0]], None))
671 .expect("should succeed");
672 cache
673 .add_frame(make_frame(1, 1.0, vec![[10.0, 20.0, 30.0]], None))
674 .expect("should succeed");
675
676 let mid = cache.sample(0.5).expect("should succeed");
677 let eps = 1e-4;
678 assert!((mid[0][0] - 5.0).abs() < eps);
679 assert!((mid[0][1] - 10.0).abs() < eps);
680 assert!((mid[0][2] - 15.0).abs() < eps);
681 }
682
683 #[test]
688 fn test_sample_clamping() {
689 let mut cache = GeoCache::new("S", 24.0, 1);
690 cache
691 .add_frame(make_frame(0, 0.0, vec![[1.0, 2.0, 3.0]], None))
692 .expect("should succeed");
693 cache
694 .add_frame(make_frame(1, 1.0, vec![[4.0, 5.0, 6.0]], None))
695 .expect("should succeed");
696
697 let before = cache.sample(-5.0).expect("should succeed");
699 assert_eq!(before[0], [1.0, 2.0, 3.0]);
700
701 let after = cache.sample(100.0).expect("should succeed");
703 assert_eq!(after[0], [4.0, 5.0, 6.0]);
704 }
705
706 #[test]
711 fn test_sample_empty() {
712 let cache = GeoCache::new("E", 24.0, 4);
713 assert!(cache.sample(0.0).is_none());
714 }
715
716 #[test]
721 fn test_export_load_no_normals() {
722 let path = std::path::Path::new("/tmp/test_oxgc_no_normals.oxgc");
723 let mut cache = GeoCache::new("RoundTrip", 30.0, 2);
724 cache
725 .add_frame(make_frame(
726 0,
727 0.0,
728 vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
729 None,
730 ))
731 .expect("should succeed");
732 cache
733 .add_frame(make_frame(
734 1,
735 1.0 / 30.0,
736 vec![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]],
737 None,
738 ))
739 .expect("should succeed");
740
741 export_geo_cache(&cache, path).expect("should succeed");
742 let loaded = load_geo_cache(path).expect("should succeed");
743
744 assert_eq!(loaded.name, "RoundTrip");
745 assert_eq!(loaded.fps, 30.0);
746 assert_eq!(loaded.vertex_count, 2);
747 assert_eq!(loaded.frame_count(), 2);
748 assert_eq!(
749 loaded.frames[0].positions,
750 vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]
751 );
752 assert_eq!(
753 loaded.frames[1].positions,
754 vec![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]]
755 );
756 assert!(loaded.frames[0].normals.is_none());
757 }
758
759 #[test]
764 fn test_export_load_with_normals() {
765 let path = std::path::Path::new("/tmp/test_oxgc_with_normals.oxgc");
766 let mut cache = GeoCache::new("WithNormals", 24.0, 2);
767 let nrm0 = vec![[0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
768 let nrm1 = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
769 cache
770 .add_frame(make_frame(
771 0,
772 0.0,
773 vec![[0.0; 3], [1.0; 3]],
774 Some(nrm0.clone()),
775 ))
776 .expect("should succeed");
777 cache
778 .add_frame(make_frame(
779 1,
780 1.0 / 24.0,
781 vec![[2.0; 3], [3.0; 3]],
782 Some(nrm1.clone()),
783 ))
784 .expect("should succeed");
785
786 export_geo_cache(&cache, path).expect("should succeed");
787 let loaded = load_geo_cache(path).expect("should succeed");
788
789 assert_eq!(loaded.frame_count(), 2);
790 assert_eq!(loaded.frames[0].normals.as_ref().expect("should succeed"), &nrm0);
791 assert_eq!(loaded.frames[1].normals.as_ref().expect("should succeed"), &nrm1);
792 }
793
794 #[test]
799 fn test_validate_good_file() {
800 let path = std::path::Path::new("/tmp/test_oxgc_validate_ok.oxgc");
801 let mut cache = GeoCache::new("Valid", 25.0, 3);
802 cache
803 .add_frame(make_frame(0, 0.0, vec![[0.0; 3], [1.0; 3], [2.0; 3]], None))
804 .expect("should succeed");
805 export_geo_cache(&cache, path).expect("should succeed");
806 assert!(GeoCache::validate(path).is_ok());
807 }
808
809 #[test]
814 fn test_validate_bad_magic() {
815 let path = std::path::Path::new("/tmp/test_oxgc_bad_magic.oxgc");
816 let mut data = vec![0u8; HEADER_SIZE];
817 data[..4].copy_from_slice(b"BAAD");
818 std::fs::write(path, &data).expect("should succeed");
819 let result = GeoCache::validate(path);
820 assert!(result.is_err());
821 let msg = format!("{}", result.unwrap_err());
822 assert!(msg.contains("invalid magic") || msg.contains("OXGC"));
823 }
824
825 #[test]
830 fn test_mesh_sequence_to_geo_cache() {
831 let m0 = make_mesh_buffers(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
832 let m1 = make_mesh_buffers(vec![[0.0, 1.0, 0.0], [1.0, 1.0, 0.0]]);
833 let cache = mesh_sequence_to_geo_cache("Seq", 24.0, &[m0, m1]);
834 assert_eq!(cache.vertex_count, 2);
835 assert_eq!(cache.frame_count(), 2);
836 assert_eq!(cache.fps, 24.0);
837 assert_eq!(cache.name, "Seq");
838 assert!(cache.frames[0].normals.is_some());
840 }
841
842 #[test]
847 fn test_write_read_methods() {
848 let path = std::path::Path::new("/tmp/test_oxgc_methods.oxgc");
849 let mut cache = GeoCache::new("Methods", 60.0, 1);
850 cache
851 .add_frame(make_frame(0, 0.0, vec![[9.0, 8.0, 7.0]], None))
852 .expect("should succeed");
853
854 cache.write(path).expect("should succeed");
855 let loaded = GeoCache::read(path).expect("should succeed");
856 assert_eq!(loaded.name, "Methods");
857 assert_eq!(loaded.frames[0].positions[0], [9.0, 8.0, 7.0]);
858 }
859
860 #[test]
865 fn test_load_geo_cache_wrapper() {
866 let path = std::path::Path::new("/tmp/test_oxgc_load_wrapper.oxgc");
867 let mut cache = GeoCache::new("Wrap", 12.0, 1);
868 cache
869 .add_frame(make_frame(0, 0.0, vec![[3.0, 2.71, 1.41]], None))
870 .expect("should succeed");
871 export_geo_cache(&cache, path).expect("should succeed");
872
873 let loaded = load_geo_cache(path).expect("should succeed");
874 let eps = 1e-5;
875 assert!((loaded.frames[0].positions[0][0] - 3.0).abs() < eps);
876 }
877
878 #[test]
883 fn test_name_padding() {
884 let path = std::path::Path::new("/tmp/test_oxgc_name.oxgc");
885 let cache = GeoCache::new("Short", 1.0, 0);
886 export_geo_cache(&cache, path).expect("should succeed");
887 let loaded = load_geo_cache(path).expect("should succeed");
888 assert_eq!(loaded.name, "Short");
889 }
890
891 #[test]
896 fn test_name_truncation() {
897 let long_name = "A".repeat(200);
898 let name_bytes = name_to_bytes(&long_name);
899 assert_eq!(name_bytes.len(), 64);
901 assert_eq!(name_bytes[63], 0); let recovered = bytes_to_name(&name_bytes);
903 assert_eq!(recovered.len(), 63);
904 }
905
906 #[test]
911 fn test_frame_index_round_trip() {
912 let path = std::path::Path::new("/tmp/test_oxgc_frame_index.oxgc");
913 let mut cache = GeoCache::new("Idx", 24.0, 1);
914 cache
915 .add_frame(make_frame(42, 0.0, vec![[0.0; 3]], None))
916 .expect("should succeed");
917 export_geo_cache(&cache, path).expect("should succeed");
918 let loaded = load_geo_cache(path).expect("should succeed");
919 assert_eq!(loaded.frames[0].frame_index, 42);
920 }
921
922 #[test]
927 fn test_empty_cache_round_trip() {
928 let path = std::path::Path::new("/tmp/test_oxgc_empty.oxgc");
929 let cache = GeoCache::new("Empty", 24.0, 100);
930 export_geo_cache(&cache, path).expect("should succeed");
931 let loaded = load_geo_cache(path).expect("should succeed");
932 assert_eq!(loaded.frame_count(), 0);
933 assert_eq!(loaded.vertex_count, 100);
934 }
935
936 #[test]
941 fn test_sample_single_frame() {
942 let mut cache = GeoCache::new("One", 24.0, 2);
943 cache
944 .add_frame(make_frame(
945 0,
946 0.0,
947 vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
948 None,
949 ))
950 .expect("should succeed");
951 let result = cache.sample(99.0).expect("should succeed");
952 assert_eq!(result[0], [1.0, 2.0, 3.0]);
953 assert_eq!(result[1], [4.0, 5.0, 6.0]);
954 }
955
956 #[test]
961 fn test_header_binary_size() {
962 let path = std::path::Path::new("/tmp/test_oxgc_header_size.oxgc");
964 let mut cache = GeoCache::new("Sz", 1.0, 1);
965 cache
966 .add_frame(make_frame(0, 0.0, vec![[1.0, 2.0, 3.0]], None))
967 .expect("should succeed");
968 export_geo_cache(&cache, path).expect("should succeed");
969
970 let data = std::fs::read(path).expect("should succeed");
971 assert_eq!(data.len(), HEADER_SIZE + 20);
973 }
974
975 #[test]
980 fn test_header_struct_fields() {
981 let path = std::path::Path::new("/tmp/test_oxgc_hdr_fields.oxgc");
982 let cache = GeoCache::new("Hdr", 48.0, 5);
983 export_geo_cache(&cache, path).expect("should succeed");
984 let mut file = std::fs::File::open(path).expect("should succeed");
985 let hdr = read_header(&mut file).expect("should succeed");
986 assert_eq!(hdr.magic, OXGC_MAGIC);
987 assert_eq!(hdr.version, OXGC_VERSION);
988 assert_eq!(hdr.vertex_count, 5);
989 assert_eq!(hdr.frame_count, 0);
990 assert_eq!(hdr.fps, 48.0);
991 assert!(!hdr.has_normals);
992 }
993}