Skip to main content

laser_dac/
point.rs

1//! The DAC-agnostic laser point type and coordinate/colour conversion helpers.
2//!
3//! [`LaserPoint`] is the neutral point representation used throughout the
4//! crate. Each protocol converts `LaserPoint` to its own wire format inside
5//! its own module — `LaserPoint` itself does not know about wire formats.
6//! The `coord_*` and `color_*` helpers are crate-private utilities shared by
7//! protocol backends with the same target encoding.
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12/// A DAC-agnostic laser point with full-precision f32 coordinates.
13///
14/// Coordinates are normalized:
15/// - x: -1.0 (left) to 1.0 (right)
16/// - y: -1.0 (bottom) to 1.0 (top)
17///
18/// Colors are 16-bit (0-65535) to support high-resolution DACs.
19/// DACs with lower resolution (8-bit) will downscale automatically.
20///
21/// This allows each DAC to convert to its native format:
22/// - Helios: 12-bit unsigned (0-4095), inverted
23/// - EtherDream: 16-bit signed (-32768 to 32767)
24#[derive(Debug, Clone, Copy, PartialEq, Default)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26pub struct LaserPoint {
27    /// X coordinate, -1.0 to 1.0
28    pub x: f32,
29    /// Y coordinate, -1.0 to 1.0
30    pub y: f32,
31    /// Red channel (0-65535)
32    pub r: u16,
33    /// Green channel (0-65535)
34    pub g: u16,
35    /// Blue channel (0-65535)
36    pub b: u16,
37    /// Intensity (0-65535)
38    pub intensity: u16,
39}
40
41impl LaserPoint {
42    /// Creates a new laser point.
43    pub fn new(x: f32, y: f32, r: u16, g: u16, b: u16, intensity: u16) -> Self {
44        Self {
45            x,
46            y,
47            r,
48            g,
49            b,
50            intensity,
51        }
52    }
53
54    /// Creates a blanked point (laser off) at the given position.
55    pub fn blanked(x: f32, y: f32) -> Self {
56        Self {
57            x,
58            y,
59            ..Default::default()
60        }
61    }
62
63    // =========================================================================
64    // Coordinate conversion helpers (shared across protocol backends)
65    // =========================================================================
66
67    /// Convert a coordinate from [-1.0, 1.0] to 12-bit unsigned (0-4095) with axis inversion.
68    ///
69    /// Used by Helios and LaserCube WiFi backends.
70    #[cfg(any(feature = "helios", feature = "lasercube-wifi"))]
71    #[inline]
72    pub(crate) fn coord_to_u12_inverted(v: f32) -> u16 {
73        ((1.0 - (v + 1.0) / 2.0).clamp(0.0, 1.0) * 4095.0).round() as u16
74    }
75
76    /// Convert a coordinate from [-1.0, 1.0] to 12-bit unsigned (0-4095).
77    ///
78    /// Used by LaserCube USB and LaserCube WiFi backends.
79    #[cfg(any(feature = "lasercube-usb", feature = "lasercube-wifi"))]
80    #[inline]
81    pub(crate) fn coord_to_u12(v: f32) -> u16 {
82        (((v.clamp(-1.0, 1.0) + 1.0) / 2.0) * 4095.0).round() as u16
83    }
84
85    /// Convert a coordinate from [-1.0, 1.0] to signed 16-bit (-32767 to 32767) with inversion.
86    ///
87    /// Used by Ether Dream and IDN backends.
88    #[inline]
89    pub(crate) fn coord_to_i16_inverted(v: f32) -> i16 {
90        (v.clamp(-1.0, 1.0) * -32767.0).round() as i16
91    }
92
93    /// Downscale a u16 color channel (0-65535) to u8 (0-255).
94    #[inline]
95    pub(crate) fn color_to_u8(v: u16) -> u8 {
96        (v >> 8) as u8
97    }
98
99    /// Downscale a u16 color channel (0-65535) to 12-bit (0-4095).
100    #[cfg(feature = "lasercube-wifi")]
101    #[inline]
102    pub(crate) fn color_to_u12(v: u16) -> u16 {
103        v >> 4
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_laser_point_blanked_sets_all_colors_to_zero() {
113        // blanked() should set all color channels to 0 while preserving position
114        let point = LaserPoint::blanked(0.25, 0.75);
115        assert_eq!(point.x, 0.25);
116        assert_eq!(point.y, 0.75);
117        assert_eq!(point.r, 0);
118        assert_eq!(point.g, 0);
119        assert_eq!(point.b, 0);
120        assert_eq!(point.intensity, 0);
121    }
122}