1use std::sync::{Arc, Mutex};
36
37use crate::types::{ChunkRequest, LaserPoint};
38
39#[derive(Clone, Debug)]
41pub struct Frame {
42 pub points: Vec<LaserPoint>,
43}
44
45impl Frame {
46 pub fn new(points: Vec<LaserPoint>) -> Self {
48 Self { points }
49 }
50
51 pub fn empty() -> Self {
53 Self { points: Vec::new() }
54 }
55}
56
57impl From<Vec<LaserPoint>> for Frame {
58 fn from(points: Vec<LaserPoint>) -> Self {
59 Self::new(points)
60 }
61}
62
63pub struct FrameAdapter {
87 current: Frame,
88 pending: Option<Frame>,
89 point_index: usize,
90 last_position: (f32, f32),
91}
92
93impl FrameAdapter {
94 pub fn new() -> Self {
98 Self {
99 current: Frame::empty(),
100 pending: None,
101 point_index: 0,
102 last_position: (0.0, 0.0),
103 }
104 }
105
106 pub fn update(&mut self, frame: Frame) {
112 self.pending = Some(frame);
113 }
114
115 pub fn next_chunk(&mut self, req: &ChunkRequest) -> Vec<LaserPoint> {
120 self.generate_points(req.n_points)
121 }
122
123 fn generate_points(&mut self, n_points: usize) -> Vec<LaserPoint> {
124 let mut output = Vec::with_capacity(n_points);
125
126 for _ in 0..n_points {
127 if self.current.points.is_empty() {
129 if let Some(pending) = self.pending.take() {
130 self.current = pending;
131 self.point_index = 0;
132 }
133
134 if self.current.points.is_empty() {
135 let (x, y) = self.last_position;
136 output.push(LaserPoint::blanked(x, y));
137 continue;
138 }
139 }
140
141 let point = self.current.points[self.point_index];
142 output.push(point);
143 self.last_position = (point.x, point.y);
144
145 self.point_index += 1;
146 if self.point_index >= self.current.points.len() {
147 self.point_index = 0;
148 if let Some(pending) = self.pending.take() {
150 self.current = pending;
151 self.point_index = 0;
152 }
153 }
154 }
155
156 output
157 }
158
159 pub fn shared(self) -> SharedFrameAdapter {
161 SharedFrameAdapter {
162 inner: Arc::new(Mutex::new(self)),
163 }
164 }
165}
166
167impl Default for FrameAdapter {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173#[derive(Clone)]
175pub struct SharedFrameAdapter {
176 inner: Arc<Mutex<FrameAdapter>>,
177}
178
179impl SharedFrameAdapter {
180 pub fn update(&self, frame: Frame) {
182 let mut adapter = self.inner.lock().unwrap();
183 adapter.update(frame);
184 }
185
186 pub fn next_chunk(&self, req: &ChunkRequest) -> Vec<LaserPoint> {
188 let mut adapter = self.inner.lock().unwrap();
189 adapter.next_chunk(req)
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::types::StreamInstant;
197
198 #[test]
199 fn test_empty_frame() {
200 let mut adapter = FrameAdapter::new();
201 let req = ChunkRequest {
202 start: StreamInstant(0),
203 pps: 30000,
204 n_points: 100,
205 scheduled_ahead_points: 0,
206 device_queued_points: None,
207 };
208
209 let points = adapter.next_chunk(&req);
210 assert_eq!(points.len(), 100);
211 assert!(points.iter().all(|p| p.intensity == 0));
212 }
213
214 #[test]
215 fn test_frame_cycles() {
216 let mut adapter = FrameAdapter::new();
217 let frame_points: Vec<LaserPoint> = (0..10)
218 .map(|i| LaserPoint::new(i as f32 / 10.0, 0.0, 65535, 0, 0, 65535))
219 .collect();
220 adapter.update(Frame::new(frame_points));
221
222 let req = ChunkRequest {
223 start: StreamInstant(0),
224 pps: 30000,
225 n_points: 25,
226 scheduled_ahead_points: 0,
227 device_queued_points: None,
228 };
229
230 let points = adapter.next_chunk(&req);
231 assert_eq!(points.len(), 25);
232 }
233
234 #[test]
235 fn test_single_point_swaps_immediately() {
236 let mut adapter = FrameAdapter::new();
237 adapter.update(Frame::new(vec![LaserPoint::new(
238 0.0, 0.0, 65535, 0, 0, 65535,
239 )]));
240
241 let req = ChunkRequest {
242 start: StreamInstant(0),
243 pps: 30000,
244 n_points: 10,
245 scheduled_ahead_points: 0,
246 device_queued_points: None,
247 };
248
249 let points1 = adapter.next_chunk(&req);
250 assert_eq!(points1[0].x, 0.0);
251
252 adapter.update(Frame::new(vec![LaserPoint::new(
253 1.0, 1.0, 0, 65535, 0, 65535,
254 )]));
255
256 let points2 = adapter.next_chunk(&req);
259 assert_eq!(points2[0].x, 0.0, "First point finishes old frame");
260 assert_eq!(points2[1].x, 1.0, "Second point starts new frame");
261 assert!(points2[2..].iter().all(|p| p.x == 1.0), "Rest is new frame");
262 }
263
264 #[test]
265 fn test_swap_waits_for_frame_end() {
266 let mut adapter = FrameAdapter::new();
267 let frame1: Vec<LaserPoint> = (0..100)
268 .map(|i| LaserPoint::new(i as f32 / 100.0, 0.0, 65535, 0, 0, 65535))
269 .collect();
270 adapter.update(Frame::new(frame1));
271
272 let req = ChunkRequest {
273 start: StreamInstant(0),
274 pps: 30000,
275 n_points: 10,
276 scheduled_ahead_points: 0,
277 device_queued_points: None,
278 };
279
280 let points1 = adapter.next_chunk(&req);
281 assert_eq!(points1[0].x, 0.0);
282
283 let frame2: Vec<LaserPoint> = (0..100)
285 .map(|_| LaserPoint::new(9.0, 9.0, 0, 65535, 0, 65535))
286 .collect();
287 adapter.update(Frame::new(frame2));
288
289 let points2 = adapter.next_chunk(&req);
291 assert!(
292 (points2[0].x - 0.1).abs() < 1e-4,
293 "Expected ~0.1, got {}",
294 points2[0].x
295 );
296
297 for _ in 0..8 {
299 adapter.next_chunk(&req);
300 }
301
302 let points_after_wrap = adapter.next_chunk(&req);
304 assert_eq!(points_after_wrap[0].x, 9.0);
305 }
306
307 #[test]
308 fn test_mid_chunk_stitching() {
309 let mut adapter = FrameAdapter::new();
311 let frame1: Vec<LaserPoint> = (0..95)
312 .map(|i| LaserPoint::new(i as f32, 0.0, 65535, 0, 0, 65535))
313 .collect();
314 adapter.update(Frame::new(frame1));
315
316 let req = ChunkRequest {
317 start: StreamInstant(0),
318 pps: 30000,
319 n_points: 10,
320 scheduled_ahead_points: 0,
321 device_queued_points: None,
322 };
323
324 for _ in 0..9 {
326 adapter.next_chunk(&req);
327 }
328
329 let frame2: Vec<LaserPoint> = (0..95)
331 .map(|_| LaserPoint::new(999.0, 999.0, 0, 65535, 0, 65535))
332 .collect();
333 adapter.update(Frame::new(frame2));
334
335 let stitched = adapter.next_chunk(&req);
337 assert_eq!(stitched.len(), 10);
338
339 for (i, p) in stitched[0..5].iter().enumerate() {
341 assert_eq!(p.x, (90 + i) as f32, "Point {} should be from frame1", i);
342 }
343
344 for (i, p) in stitched[5..10].iter().enumerate() {
346 assert_eq!(p.x, 999.0, "Point {} should be from frame2", i + 5);
347 }
348 }
349
350 #[test]
351 fn test_empty_holds_last_position() {
352 let mut adapter = FrameAdapter::new();
353 adapter.update(Frame::new(vec![LaserPoint::new(
354 0.5, -0.3, 65535, 0, 0, 65535,
355 )]));
356
357 let req = ChunkRequest {
358 start: StreamInstant(0),
359 pps: 30000,
360 n_points: 5,
361 scheduled_ahead_points: 0,
362 device_queued_points: None,
363 };
364
365 adapter.next_chunk(&req);
366 adapter.update(Frame::empty());
367
368 let points = adapter.next_chunk(&req);
370 assert_eq!(points[0].intensity, 65535, "First point finishes old frame");
371 assert!(
372 points[1..].iter().all(|p| p.intensity == 0),
373 "Rest is blanked"
374 );
375 assert_eq!(points[1].x, 0.5);
377 assert_eq!(points[1].y, -0.3);
378 }
379
380 #[test]
381 fn test_integer_index_deterministic() {
382 let mut adapter = FrameAdapter::new();
383 let frame: Vec<LaserPoint> = (0..7)
384 .map(|i| LaserPoint::new(i as f32, 0.0, 65535, 0, 0, 65535))
385 .collect();
386 adapter.update(Frame::new(frame));
387
388 let req = ChunkRequest {
389 start: StreamInstant(0),
390 pps: 30000,
391 n_points: 7,
392 scheduled_ahead_points: 0,
393 device_queued_points: None,
394 };
395
396 for cycle in 0..1000 {
398 let points = adapter.next_chunk(&req);
399 for (i, p) in points.iter().enumerate() {
400 assert_eq!(p.x, i as f32, "Cycle {}: drift detected", cycle);
401 }
402 }
403 }
404
405 #[test]
406 fn test_from_vec() {
407 let points: Vec<LaserPoint> = vec![LaserPoint::new(0.0, 0.0, 65535, 0, 0, 65535)];
408 let frame: Frame = points.into();
409 assert_eq!(frame.points.len(), 1);
410 }
411}