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}