Skip to main content

oxihuman_export/
mdd.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! MDD (Motion Displacement Data) binary point cache — LightWave format.
5//!
6//! Binary layout (big-endian):
7//!   - Frame count:   i32
8//!   - Point count:   i32
9//!   - Time values:   frame_count × f32   (seconds)
10//!   - Data:          frame_count × point_count × 3 × f32  (xyz)
11
12use std::path::Path;
13
14/// Complete MDD point cache.
15#[allow(dead_code)]
16pub struct MddCache {
17    pub point_count: u32,
18    /// Timestamp (seconds) for each frame.
19    pub times: Vec<f32>,
20    /// Frame data: `frames[i]` contains `point_count` XYZ positions.
21    pub frames: Vec<Vec<[f32; 3]>>,
22}
23
24impl MddCache {
25    /// Create an empty cache for the given point count.
26    #[allow(dead_code)]
27    pub fn new(point_count: u32) -> Self {
28        Self {
29            point_count,
30            times: Vec::new(),
31            frames: Vec::new(),
32        }
33    }
34
35    /// Append a frame.  Panics if `positions.len() != point_count`.
36    #[allow(dead_code)]
37    pub fn add_frame(&mut self, time: f32, positions: Vec<[f32; 3]>) {
38        assert_eq!(
39            positions.len(),
40            self.point_count as usize,
41            "add_frame: expected {} points, got {}",
42            self.point_count,
43            positions.len()
44        );
45        self.times.push(time);
46        self.frames.push(positions);
47    }
48}
49
50// ── serialisation ─────────────────────────────────────────────────────────────
51
52/// Serialise an [`MddCache`] to big-endian binary bytes.
53#[allow(dead_code)]
54pub fn write_mdd(cache: &MddCache) -> Vec<u8> {
55    let frame_count = cache.frames.len() as i32;
56    let point_count = cache.point_count as i32;
57    let time_bytes = (frame_count as usize) * 4;
58    let data_bytes = (frame_count as usize) * (point_count as usize) * 3 * 4;
59    let mut out = Vec::with_capacity(8 + time_bytes + data_bytes);
60
61    out.extend_from_slice(&frame_count.to_be_bytes());
62    out.extend_from_slice(&point_count.to_be_bytes());
63
64    for &t in &cache.times {
65        out.extend_from_slice(&t.to_be_bytes());
66    }
67
68    for frame in &cache.frames {
69        for pos in frame {
70            out.extend_from_slice(&pos[0].to_be_bytes());
71            out.extend_from_slice(&pos[1].to_be_bytes());
72            out.extend_from_slice(&pos[2].to_be_bytes());
73        }
74    }
75    out
76}
77
78/// Deserialise a big-endian MDD binary blob into an [`MddCache`].
79#[allow(dead_code)]
80pub fn read_mdd(data: &[u8]) -> anyhow::Result<MddCache> {
81    use anyhow::bail;
82
83    if data.len() < 8 {
84        bail!("MDD data too short: {} bytes", data.len());
85    }
86
87    let frame_count = i32::from_be_bytes(
88        data[0..4]
89            .try_into()
90            .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
91    ) as u32;
92    let point_count = i32::from_be_bytes(
93        data[4..8]
94            .try_into()
95            .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
96    ) as u32;
97
98    let time_end = 8 + (frame_count as usize) * 4;
99    let data_end = time_end + (frame_count as usize) * (point_count as usize) * 12;
100
101    if data.len() < data_end {
102        bail!(
103            "MDD data truncated: need {} bytes, have {}",
104            data_end,
105            data.len()
106        );
107    }
108
109    let mut times = Vec::with_capacity(frame_count as usize);
110    let mut offset = 8_usize;
111    for _ in 0..frame_count {
112        let t = f32::from_be_bytes(
113            data[offset..offset + 4]
114                .try_into()
115                .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
116        );
117        times.push(t);
118        offset += 4;
119    }
120
121    let mut frames = Vec::with_capacity(frame_count as usize);
122    for _ in 0..frame_count {
123        let mut frame = Vec::with_capacity(point_count as usize);
124        for _ in 0..point_count {
125            let x = f32::from_be_bytes(
126                data[offset..offset + 4]
127                    .try_into()
128                    .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
129            );
130            let y = f32::from_be_bytes(
131                data[offset + 4..offset + 8]
132                    .try_into()
133                    .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
134            );
135            let z = f32::from_be_bytes(
136                data[offset + 8..offset + 12]
137                    .try_into()
138                    .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
139            );
140            frame.push([x, y, z]);
141            offset += 12;
142        }
143        frames.push(frame);
144    }
145
146    Ok(MddCache {
147        point_count,
148        times,
149        frames,
150    })
151}
152
153/// Write an [`MddCache`] to a file on disk.
154#[allow(dead_code)]
155pub fn export_mdd(cache: &MddCache, path: &Path) -> anyhow::Result<()> {
156    let bytes = write_mdd(cache);
157    std::fs::write(path, &bytes)
158        .map_err(|e| anyhow::anyhow!("writing MDD to {}: {}", path.display(), e))
159}
160
161// ── helpers ───────────────────────────────────────────────────────────────────
162
163/// Build an [`MddCache`] from position frames, assigning uniform timestamps
164/// at intervals of `1.0 / fps` seconds.
165#[allow(dead_code)]
166pub fn uniform_time_mdd(frames: &[Vec<[f32; 3]>], fps: f32) -> MddCache {
167    assert!(
168        !frames.is_empty(),
169        "uniform_time_mdd: frames must not be empty"
170    );
171    let point_count = frames[0].len() as u32;
172    let mut cache = MddCache::new(point_count);
173    for (i, frame) in frames.iter().enumerate() {
174        let time = i as f32 / fps;
175        cache.add_frame(time, frame.clone());
176    }
177    cache
178}
179
180/// Return the total duration of the cache in seconds (last time value).
181/// Returns `0.0` if the cache has no frames.
182#[allow(dead_code)]
183pub fn mdd_duration(cache: &MddCache) -> f32 {
184    cache.times.last().copied().unwrap_or(0.0)
185}
186
187// ── tests ─────────────────────────────────────────────────────────────────────
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    fn two_frame_cache() -> MddCache {
194        let mut c = MddCache::new(2);
195        c.add_frame(0.0, vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
196        c.add_frame(1.0 / 24.0, vec![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]]);
197        c
198    }
199
200    // 1. round-trip preserves positions
201    #[test]
202    fn roundtrip_positions() {
203        let cache = two_frame_cache();
204        let back = read_mdd(&write_mdd(&cache)).expect("should succeed");
205        assert_eq!(back.frames[0], vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
206        assert_eq!(back.frames[1], vec![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]]);
207    }
208
209    // 2. round-trip preserves metadata
210    #[test]
211    fn roundtrip_metadata() {
212        let cache = two_frame_cache();
213        let back = read_mdd(&write_mdd(&cache)).expect("should succeed");
214        assert_eq!(back.point_count, 2);
215        assert_eq!(back.frames.len(), 2);
216    }
217
218    // 3. bytes 0..3 are big-endian frame count
219    #[test]
220    fn big_endian_frame_count() {
221        let cache = two_frame_cache();
222        let bytes = write_mdd(&cache);
223        let fc = i32::from_be_bytes(bytes[0..4].try_into().expect("should succeed"));
224        assert_eq!(fc, 2);
225    }
226
227    // 4. bytes 4..7 are big-endian point count
228    #[test]
229    fn big_endian_point_count() {
230        let cache = two_frame_cache();
231        let bytes = write_mdd(&cache);
232        let pc = i32::from_be_bytes(bytes[4..8].try_into().expect("should succeed"));
233        assert_eq!(pc, 2);
234    }
235
236    // 5. times are preserved through round-trip
237    #[test]
238    fn roundtrip_times() {
239        let cache = two_frame_cache();
240        let back = read_mdd(&write_mdd(&cache)).expect("should succeed");
241        assert!((back.times[0] - 0.0).abs() < 1e-6);
242        assert!((back.times[1] - 1.0 / 24.0).abs() < 1e-5);
243    }
244
245    // 6. uniform_time_mdd generates correct timestamps
246    #[test]
247    fn uniform_time_timestamps() {
248        let frames: Vec<Vec<[f32; 3]>> = (0..4).map(|_| vec![[0.0, 0.0, 0.0]]).collect();
249        let cache = uniform_time_mdd(&frames, 10.0);
250        assert!((cache.times[0] - 0.0).abs() < 1e-6);
251        assert!((cache.times[1] - 0.1).abs() < 1e-5);
252        assert!((cache.times[2] - 0.2).abs() < 1e-5);
253        assert!((cache.times[3] - 0.3).abs() < 1e-5);
254    }
255
256    // 7. uniform_time_mdd frame count
257    #[test]
258    fn uniform_time_frame_count() {
259        let frames: Vec<Vec<[f32; 3]>> = (0..6).map(|_| vec![[1.0, 0.0, 0.0]]).collect();
260        let cache = uniform_time_mdd(&frames, 24.0);
261        assert_eq!(cache.frames.len(), 6);
262    }
263
264    // 8. mdd_duration returns last time
265    #[test]
266    fn duration_is_last_time() {
267        let cache = two_frame_cache();
268        let d = mdd_duration(&cache);
269        assert!((d - 1.0 / 24.0).abs() < 1e-5);
270    }
271
272    // 9. mdd_duration with empty cache
273    #[test]
274    fn duration_empty_is_zero() {
275        let c = MddCache::new(5);
276        assert_eq!(mdd_duration(&c), 0.0);
277    }
278
279    // 10. add_frame wrong count panics
280    #[test]
281    #[should_panic]
282    fn add_frame_wrong_count_panics() {
283        let mut c = MddCache::new(3);
284        c.add_frame(0.0, vec![[0.0, 0.0, 0.0]]); // only 1 point
285    }
286
287    // 11. truncated data returns error
288    #[test]
289    fn read_mdd_truncated() {
290        let cache = two_frame_cache();
291        let bytes = write_mdd(&cache);
292        assert!(read_mdd(&bytes[..6]).is_err());
293    }
294
295    // 12. write then read with 1 point per frame
296    #[test]
297    fn single_point_roundtrip() {
298        let mut c = MddCache::new(1);
299        c.add_frame(0.0, vec![[1.23, 4.56, 7.89]]);
300        let back = read_mdd(&write_mdd(&c)).expect("should succeed");
301        let p = back.frames[0][0];
302        assert!((p[0] - 1.23).abs() < 1e-5);
303        assert!((p[1] - 4.56).abs() < 1e-5);
304        assert!((p[2] - 7.89).abs() < 1e-5);
305    }
306}