Skip to main content

laser_dac/
frame_adapter.rs

1//! Frame adapter for converting point buffers to continuous streams.
2//!
3//! This module provides [`FrameAdapter`] which converts a point buffer (frame)
4//! into a continuous stream of points for the DAC. The adapter cycles through
5//! the frame's points, producing chunks on demand.
6//!
7//! # Update Semantics
8//!
9//! - **Latest-wins**: `update()` sets the pending frame. Multiple calls before
10//!   a swap keep only the most recent.
11//! - **Immediate swap on frame end**: When the current frame completes (all
12//!   points output), any pending frame becomes current immediately—even
13//!   mid-chunk. This ensures clean frame-to-frame transitions.
14//!
15//! The adapter does not insert blanking on frame swaps. If the new frame's first
16//! point is far from the previous frame's last point, content should include
17//! lead-in blanking to avoid visible travel lines.
18//!
19//! # Example
20//!
21//! ```ignore
22//! let mut adapter = FrameAdapter::new();
23//! adapter.update(Frame::new(points));
24//!
25//! loop {
26//!     let req = stream.next_request()?;
27//!     let points = adapter.next_chunk(&req);
28//!     stream.write(&req, &points)?;
29//! }
30//! ```
31//!
32//! For time-varying animation, use the streaming API directly with a point
33//! generator (see the `manual` or `callback` examples with `orbiting-circle`).
34
35use std::sync::{Arc, Mutex};
36
37use crate::types::{ChunkRequest, LaserPoint};
38
39/// A point buffer to be cycled by the adapter.
40#[derive(Clone, Debug)]
41pub struct Frame {
42    pub points: Vec<LaserPoint>,
43}
44
45impl Frame {
46    /// Creates a new frame from a vector of points.
47    pub fn new(points: Vec<LaserPoint>) -> Self {
48        Self { points }
49    }
50
51    /// Creates an empty frame (outputs blanked points at last position).
52    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
63/// Converts a point buffer (frame) into a continuous stream.
64///
65/// The adapter cycles through the frame's points, producing exactly
66/// `req.n_points` on each `next_chunk()` call.
67///
68/// # Update semantics
69///
70/// - `update()` sets the pending frame (latest-wins if called multiple times)
71/// - When the current frame ends, any pending frame becomes current immediately
72///   (even mid-chunk), ensuring clean frame-to-frame transitions
73///
74/// # Example
75///
76/// ```ignore
77/// let mut adapter = FrameAdapter::new();
78/// adapter.update(Frame::new(circle_points));
79///
80/// loop {
81///     let req = stream.next_request()?;
82///     let points = adapter.next_chunk(&req);
83///     stream.write(&req, &points)?;
84/// }
85/// ```
86pub struct FrameAdapter {
87    current: Frame,
88    pending: Option<Frame>,
89    point_index: usize,
90    last_position: (f32, f32),
91}
92
93impl FrameAdapter {
94    /// Creates a new adapter with an empty frame.
95    ///
96    /// Call `update()` to set the initial frame before streaming.
97    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    /// Sets the pending frame.
107    ///
108    /// The frame becomes current when the current frame ends (all points
109    /// output). If called multiple times before a swap, only the most recent
110    /// frame is kept (latest-wins).
111    pub fn update(&mut self, frame: Frame) {
112        self.pending = Some(frame);
113    }
114
115    /// Produces exactly `req.n_points` points.
116    ///
117    /// Cycles through the current frame. When the frame ends and a pending
118    /// frame is available, switches immediately (even mid-chunk).
119    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            // Handle empty frame: try to swap, else output blanked
128            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                // Immediately swap to pending frame if available
149                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    /// Returns a thread-safe handle for updating frames from another thread.
160    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/// Thread-safe handle for updating frames from another thread.
174#[derive(Clone)]
175pub struct SharedFrameAdapter {
176    inner: Arc<Mutex<FrameAdapter>>,
177}
178
179impl SharedFrameAdapter {
180    /// Sets the pending frame. Takes effect when the current frame ends.
181    pub fn update(&self, frame: Frame) {
182        let mut adapter = self.inner.lock().unwrap();
183        adapter.update(frame);
184    }
185
186    /// Produces exactly `req.n_points` points.
187    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        // Single-point frame wraps every point; swap happens immediately after
257        // each point, so first point is old frame, rest is new frame
258        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        // Update mid-cycle with different frame
284        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        // Should still use frame1 (not finished yet)
290        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        // Output remaining 80 points to complete frame1
298        for _ in 0..8 {
299            adapter.next_chunk(&req);
300        }
301
302        // Now frame1 finished, uses frame2
303        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        // Frame with 95 points, chunk size 10: wrap happens mid-chunk
310        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        // Output 90 points (9 chunks)
325        for _ in 0..9 {
326            adapter.next_chunk(&req);
327        }
328
329        // Now at index 90, update with new frame
330        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        // Next chunk: points 90-94 from frame1, then points 0-4 from frame2
336        let stitched = adapter.next_chunk(&req);
337        assert_eq!(stitched.len(), 10);
338
339        // First 5 points are frame1 (90, 91, 92, 93, 94)
340        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        // Last 5 points are frame2 (all 999.0)
345        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        // First point finishes the single-point frame, then swap to empty
369        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        // Empty frame holds the last known position
376        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        // No drift over 1000 cycles
397        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}