1use std::path::Path;
16
17pub const PC2_MAGIC: &[u8; 12] = b"POINTCACHE2\0";
19
20#[allow(dead_code)]
22pub struct Pc2Header {
23 pub point_count: u32,
24 pub start_time: f32,
25 pub sample_rate: f32,
26 pub sample_count: u32,
27}
28
29#[allow(dead_code)]
31pub struct Pc2Cache {
32 pub header: Pc2Header,
33 pub frames: Vec<Vec<[f32; 3]>>,
35}
36
37impl Pc2Cache {
38 #[allow(dead_code)]
40 pub fn new(point_count: u32, start_time: f32, sample_rate: f32) -> Self {
41 Self {
42 header: Pc2Header {
43 point_count,
44 start_time,
45 sample_rate,
46 sample_count: 0,
47 },
48 frames: Vec::new(),
49 }
50 }
51
52 #[allow(dead_code)]
54 pub fn add_frame(&mut self, positions: Vec<[f32; 3]>) {
55 assert_eq!(
56 positions.len(),
57 self.header.point_count as usize,
58 "add_frame: expected {} points, got {}",
59 self.header.point_count,
60 positions.len()
61 );
62 self.frames.push(positions);
63 self.header.sample_count += 1;
64 }
65}
66
67#[allow(dead_code)]
71pub fn write_pc2(cache: &Pc2Cache) -> Vec<u8> {
72 let h = &cache.header;
73 let data_bytes = (h.point_count as usize) * 3 * 4 * cache.frames.len();
74 let mut out = Vec::with_capacity(12 + 4 + 4 + 4 + 4 + 4 + data_bytes);
75
76 out.extend_from_slice(PC2_MAGIC);
78 out.extend_from_slice(&1_i32.to_le_bytes());
80 out.extend_from_slice(&(h.point_count as i32).to_le_bytes());
82 out.extend_from_slice(&h.start_time.to_le_bytes());
83 out.extend_from_slice(&h.sample_rate.to_le_bytes());
84 out.extend_from_slice(&(h.sample_count as i32).to_le_bytes());
85
86 for frame in &cache.frames {
88 for pos in frame {
89 out.extend_from_slice(&pos[0].to_le_bytes());
90 out.extend_from_slice(&pos[1].to_le_bytes());
91 out.extend_from_slice(&pos[2].to_le_bytes());
92 }
93 }
94 out
95}
96
97#[allow(dead_code)]
99pub fn read_pc2(data: &[u8]) -> anyhow::Result<Pc2Cache> {
100 use anyhow::bail;
101
102 if data.len() < 28 {
103 bail!("PC2 data too short: {} bytes", data.len());
104 }
105
106 if &data[..12] != PC2_MAGIC {
108 bail!("PC2 magic mismatch");
109 }
110
111 let version = i32::from_le_bytes(
112 data[12..16]
113 .try_into()
114 .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
115 );
116 if version != 1 {
117 bail!("unsupported PC2 version: {}", version);
118 }
119
120 let point_count = i32::from_le_bytes(
121 data[16..20]
122 .try_into()
123 .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
124 ) as u32;
125 let start_time = f32::from_le_bytes(
126 data[20..24]
127 .try_into()
128 .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
129 );
130 let sample_rate = f32::from_le_bytes(
131 data[24..28]
132 .try_into()
133 .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
134 );
135 let sample_count = i32::from_le_bytes(
136 data[28..32]
137 .try_into()
138 .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
139 ) as u32;
140
141 let frame_stride = (point_count as usize) * 3 * 4;
142 let expected_total = 32 + frame_stride * (sample_count as usize);
143 if data.len() < expected_total {
144 bail!(
145 "PC2 data truncated: need {} bytes, have {}",
146 expected_total,
147 data.len()
148 );
149 }
150
151 let mut frames = Vec::with_capacity(sample_count as usize);
152 let mut offset = 32_usize;
153 for _ in 0..sample_count {
154 let mut frame = Vec::with_capacity(point_count as usize);
155 for _ in 0..point_count {
156 let x = f32::from_le_bytes(
157 data[offset..offset + 4]
158 .try_into()
159 .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
160 );
161 let y = f32::from_le_bytes(
162 data[offset + 4..offset + 8]
163 .try_into()
164 .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
165 );
166 let z = f32::from_le_bytes(
167 data[offset + 8..offset + 12]
168 .try_into()
169 .map_err(|_| anyhow::anyhow!("byte conversion failed"))?,
170 );
171 frame.push([x, y, z]);
172 offset += 12;
173 }
174 frames.push(frame);
175 }
176
177 Ok(Pc2Cache {
178 header: Pc2Header {
179 point_count,
180 start_time,
181 sample_rate,
182 sample_count,
183 },
184 frames,
185 })
186}
187
188#[allow(dead_code)]
190pub fn export_pc2(cache: &Pc2Cache, path: &Path) -> anyhow::Result<()> {
191 let bytes = write_pc2(cache);
192 std::fs::write(path, &bytes)
193 .map_err(|e| anyhow::anyhow!("writing PC2 to {}: {}", path.display(), e))
194}
195
196#[allow(dead_code)]
203pub fn mesh_sequence_to_pc2(
204 frames: &[Vec<[f32; 3]>],
205 start_time: f32,
206 sample_rate: f32,
207) -> Pc2Cache {
208 assert!(
209 !frames.is_empty(),
210 "mesh_sequence_to_pc2: frames must not be empty"
211 );
212 let point_count = frames[0].len() as u32;
213 let mut cache = Pc2Cache::new(point_count, start_time, sample_rate);
214 for frame in frames {
215 cache.add_frame(frame.clone());
216 }
217 cache
218}
219
220#[allow(dead_code)]
222pub fn pc2_stats(cache: &Pc2Cache) -> String {
223 let h = &cache.header;
224 format!(
225 "PC2 | points={} | frames={} | start={:.3} | rate={:.2} fps",
226 h.point_count, h.sample_count, h.start_time, h.sample_rate
227 )
228}
229
230#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn two_point_cache() -> Pc2Cache {
237 let mut c = Pc2Cache::new(2, 0.0, 24.0);
238 c.add_frame(vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
239 c.add_frame(vec![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]]);
240 c
241 }
242
243 #[test]
245 fn roundtrip_basic() {
246 let cache = two_point_cache();
247 let bytes = write_pc2(&cache);
248 let back = read_pc2(&bytes).expect("should succeed");
249 assert_eq!(back.header.point_count, 2);
250 assert_eq!(back.header.sample_count, 2);
251 assert_eq!(back.frames[0], vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
252 assert_eq!(back.frames[1], vec![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]]);
253 }
254
255 #[test]
257 fn roundtrip_metadata() {
258 let mut c = Pc2Cache::new(1, 1.5, 30.0);
259 c.add_frame(vec![[0.0, 0.0, 0.0]]);
260 let back = read_pc2(&write_pc2(&c)).expect("should succeed");
261 assert!((back.header.start_time - 1.5).abs() < 1e-6);
262 assert!((back.header.sample_rate - 30.0).abs() < 1e-6);
263 }
264
265 #[test]
267 fn magic_bytes() {
268 let cache = two_point_cache();
269 let bytes = write_pc2(&cache);
270 assert_eq!(&bytes[..12], PC2_MAGIC);
271 }
272
273 #[test]
275 fn version_field_is_one() {
276 let cache = two_point_cache();
277 let bytes = write_pc2(&cache);
278 let ver = i32::from_le_bytes(bytes[12..16].try_into().expect("should succeed"));
279 assert_eq!(ver, 1);
280 }
281
282 #[test]
284 #[should_panic]
285 fn add_frame_wrong_count_panics() {
286 let mut c = Pc2Cache::new(3, 0.0, 24.0);
287 c.add_frame(vec![[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]); }
289
290 #[test]
292 fn empty_frames() {
293 let c = Pc2Cache::new(5, 0.0, 24.0);
294 let bytes = write_pc2(&c);
295 let back = read_pc2(&bytes).expect("should succeed");
296 assert_eq!(back.header.sample_count, 0);
297 assert!(back.frames.is_empty());
298 }
299
300 #[test]
302 fn pc2_stats_contains_points_and_frames() {
303 let cache = two_point_cache();
304 let s = pc2_stats(&cache);
305 assert!(s.contains("points=2"));
306 assert!(s.contains("frames=2"));
307 }
308
309 #[test]
311 fn pc2_stats_contains_rate() {
312 let cache = two_point_cache();
313 let s = pc2_stats(&cache);
314 assert!(s.contains("24"));
315 }
316
317 #[test]
319 fn mesh_sequence_frame_count() {
320 let frames: Vec<Vec<[f32; 3]>> = (0..5)
321 .map(|i| vec![[i as f32, 0.0, 0.0], [0.0, i as f32, 0.0]])
322 .collect();
323 let cache = mesh_sequence_to_pc2(&frames, 0.0, 24.0);
324 assert_eq!(cache.header.sample_count, 5);
325 assert_eq!(cache.header.point_count, 2);
326 }
327
328 #[test]
330 fn mesh_sequence_positions_preserved() {
331 let frames = vec![vec![[1.0_f32, 2.0, 3.0]], vec![[4.0_f32, 5.0, 6.0]]];
332 let cache = mesh_sequence_to_pc2(&frames, 0.0, 24.0);
333 let back = read_pc2(&write_pc2(&cache)).expect("should succeed");
334 assert_eq!(back.frames[0][0], [1.0, 2.0, 3.0]);
335 assert_eq!(back.frames[1][0], [4.0, 5.0, 6.0]);
336 }
337
338 #[test]
340 fn read_pc2_truncated_error() {
341 let cache = two_point_cache();
342 let bytes = write_pc2(&cache);
343 let result = read_pc2(&bytes[..20]);
344 assert!(result.is_err());
345 }
346
347 #[test]
349 fn read_pc2_bad_magic() {
350 let cache = two_point_cache();
351 let mut bytes = write_pc2(&cache);
352 bytes[0] = 0xFF;
353 assert!(read_pc2(&bytes).is_err());
354 }
355}