Skip to main content

oxihuman_export/
geometry_cache_v2_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Geometry cache v2 export: extended geometry cache format with normals and UVs.
6
7/// Magic bytes for format identification.
8pub const GCV2_MAGIC: &[u8; 4] = b"GCV2";
9/// Current format version.
10pub const GCV2_VERSION: u32 = 2;
11
12/// A single frame in the geometry cache v2.
13#[allow(dead_code)]
14#[derive(Debug, Clone)]
15pub struct GeoCacheV2Frame {
16    pub time: f32,
17    pub positions: Vec<[f32; 3]>,
18    pub normals: Vec<[f32; 3]>,
19    pub uvs: Vec<[f32; 2]>,
20}
21
22/// The full geometry cache v2 export.
23#[allow(dead_code)]
24#[derive(Debug, Clone)]
25pub struct GeoCacheV2Export {
26    pub frames: Vec<GeoCacheV2Frame>,
27    pub vertex_count: usize,
28}
29
30/// Create a new empty v2 cache.
31#[allow(dead_code)]
32pub fn new_geo_cache_v2(vertex_count: usize) -> GeoCacheV2Export {
33    GeoCacheV2Export {
34        frames: Vec::new(),
35        vertex_count,
36    }
37}
38
39/// Add a frame.
40#[allow(dead_code)]
41pub fn add_geo_v2_frame(
42    cache: &mut GeoCacheV2Export,
43    time: f32,
44    positions: Vec<[f32; 3]>,
45    normals: Vec<[f32; 3]>,
46    uvs: Vec<[f32; 2]>,
47) {
48    cache.frames.push(GeoCacheV2Frame {
49        time,
50        positions,
51        normals,
52        uvs,
53    });
54}
55
56/// Frame count.
57#[allow(dead_code)]
58pub fn geo_v2_frame_count(cache: &GeoCacheV2Export) -> usize {
59    cache.frames.len()
60}
61
62/// Duration of the cache.
63#[allow(dead_code)]
64pub fn geo_v2_duration(cache: &GeoCacheV2Export) -> f32 {
65    cache.frames.iter().map(|f| f.time).fold(0.0_f32, f32::max)
66}
67
68/// Validate: all frames have matching vertex count.
69#[allow(dead_code)]
70pub fn validate_geo_cache_v2(cache: &GeoCacheV2Export) -> bool {
71    cache.frames.iter().all(|f| {
72        f.positions.len() == cache.vertex_count
73            && (f.normals.is_empty() || f.normals.len() == cache.vertex_count)
74            && (f.uvs.is_empty() || f.uvs.len() == cache.vertex_count)
75    })
76}
77
78/// Estimated byte size.
79#[allow(dead_code)]
80pub fn geo_v2_size_bytes(cache: &GeoCacheV2Export) -> usize {
81    cache
82        .frames
83        .iter()
84        .map(|f| f.positions.len() * 12 + f.normals.len() * 12 + f.uvs.len() * 8)
85        .sum::<usize>()
86        + 8
87}
88
89/// Binary header bytes.
90#[allow(dead_code)]
91pub fn geo_v2_header_bytes() -> Vec<u8> {
92    let mut h = GCV2_MAGIC.to_vec();
93    h.extend_from_slice(&GCV2_VERSION.to_le_bytes());
94    h
95}
96
97/// Export to JSON.
98#[allow(dead_code)]
99pub fn geo_cache_v2_to_json(cache: &GeoCacheV2Export) -> String {
100    format!(
101        "{{\"version\":{},\"vertex_count\":{},\"frame_count\":{}}}",
102        GCV2_VERSION,
103        cache.vertex_count,
104        geo_v2_frame_count(cache)
105    )
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    fn simple_cache() -> GeoCacheV2Export {
113        let mut c = new_geo_cache_v2(3);
114        add_geo_v2_frame(
115            &mut c,
116            0.0,
117            vec![[0.0; 3]; 3],
118            vec![[0.0, 0.0, 1.0]; 3],
119            vec![[0.0; 2]; 3],
120        );
121        c
122    }
123
124    #[test]
125    fn test_new_cache() {
126        let c = new_geo_cache_v2(10);
127        assert_eq!(c.vertex_count, 10);
128        assert_eq!(geo_v2_frame_count(&c), 0);
129    }
130
131    #[test]
132    fn test_add_frame() {
133        let c = simple_cache();
134        assert_eq!(geo_v2_frame_count(&c), 1);
135    }
136
137    #[test]
138    fn test_geo_v2_duration() {
139        let mut c = new_geo_cache_v2(3);
140        add_geo_v2_frame(&mut c, 1.5, vec![[0.0; 3]; 3], vec![], vec![]);
141        assert!((geo_v2_duration(&c) - 1.5).abs() < 1e-6);
142    }
143
144    #[test]
145    fn test_validate_valid() {
146        let c = simple_cache();
147        assert!(validate_geo_cache_v2(&c));
148    }
149
150    #[test]
151    fn test_validate_wrong_vertex_count() {
152        let mut c = new_geo_cache_v2(3);
153        add_geo_v2_frame(&mut c, 0.0, vec![[0.0; 3]; 2], vec![], vec![]);
154        assert!(!validate_geo_cache_v2(&c));
155    }
156
157    #[test]
158    fn test_geo_v2_size_bytes() {
159        let c = simple_cache();
160        let sz = geo_v2_size_bytes(&c);
161        assert!(sz > 0);
162    }
163
164    #[test]
165    fn test_header_bytes() {
166        let h = geo_v2_header_bytes();
167        assert!(h.starts_with(b"GCV2"));
168    }
169
170    #[test]
171    fn test_geo_cache_v2_to_json() {
172        let c = simple_cache();
173        let j = geo_cache_v2_to_json(&c);
174        assert!(j.contains("\"version\":2"));
175    }
176
177    #[test]
178    fn test_duration_empty() {
179        let c = new_geo_cache_v2(0);
180        assert!((geo_v2_duration(&c)).abs() < 1e-9);
181    }
182
183    #[test]
184    fn test_magic_bytes() {
185        assert_eq!(GCV2_MAGIC, b"GCV2");
186    }
187}