1use retroglyph_core::grid::Pos;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct CellGeometry {
15 pub glyph_w: u16,
17 pub glyph_h: u16,
19 pub scale: u16,
21}
22
23impl CellGeometry {
24 #[must_use]
26 pub const fn new(glyph_w: u16, glyph_h: u16, scale: u16) -> Self {
27 Self {
28 glyph_w,
29 glyph_h,
30 scale,
31 }
32 }
33
34 #[must_use]
38 pub const fn cell_size(&self) -> (u32, u32) {
39 (
42 self.glyph_w as u32 * self.scale as u32,
43 self.glyph_h as u32 * self.scale as u32,
44 )
45 }
46
47 #[must_use]
49 pub const fn surface_size(&self, cols: u16, rows: u16) -> (u32, u32) {
50 let (cell_w, cell_h) = self.cell_size();
51 (cols as u32 * cell_w, rows as u32 * cell_h)
52 }
53
54 #[must_use]
60 pub fn pixel_to_cell(&self, x: f64, y: f64) -> Pos {
61 let (cell_w, cell_h) = self.cell_size();
62 Pos {
63 x: pixel_to_cell_axis(x, cell_w),
64 y: pixel_to_cell_axis(y, cell_h),
65 }
66 }
67}
68
69#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
71fn pixel_to_cell_axis(px: f64, cell: u32) -> u16 {
72 let index =
75 u32::checked_div(px.max(0.0) as u32, cell).map_or(0, |v| v.min(u32::from(u16::MAX)));
76 u16::try_from(index).unwrap_or(u16::MAX)
77}
78
79#[cfg(test)]
80mod tests {
81 use super::CellGeometry;
82 use retroglyph_core::grid::Pos;
83
84 #[test]
85 fn cell_size_is_glyph_times_scale() {
86 assert_eq!(CellGeometry::new(8, 16, 1).cell_size(), (8, 16));
87 assert_eq!(CellGeometry::new(8, 16, 2).cell_size(), (16, 32));
88 assert_eq!(CellGeometry::new(6, 12, 3).cell_size(), (18, 36));
89 }
90
91 #[test]
92 fn surface_size_is_grid_times_cell() {
93 assert_eq!(CellGeometry::new(8, 16, 1).surface_size(80, 25), (640, 400));
95 assert_eq!(
96 CellGeometry::new(8, 16, 2).surface_size(80, 25),
97 (1280, 800)
98 );
99 }
100
101 #[test]
102 fn zero_grid_is_zero_surface() {
103 assert_eq!(CellGeometry::new(8, 16, 2).surface_size(0, 0), (0, 0));
104 }
105
106 #[test]
109 fn pixel_to_cell_basic() {
110 let geometry = CellGeometry::new(8, 16, 1);
112 assert_eq!(geometry.pixel_to_cell(20.0, 48.0), Pos { x: 2, y: 3 });
113 }
114
115 #[test]
116 fn pixel_to_cell_origin() {
117 let geometry = CellGeometry::new(8, 16, 1);
118 assert_eq!(geometry.pixel_to_cell(0.0, 0.0), Pos { x: 0, y: 0 });
119 }
120
121 #[test]
122 fn pixel_to_cell_negative_coords_clamp_to_zero() {
123 let geometry = CellGeometry::new(8, 16, 1);
125 assert_eq!(geometry.pixel_to_cell(-5.0, -10.0), Pos { x: 0, y: 0 });
126 }
127
128 #[test]
129 fn pixel_to_cell_zero_cell_size_returns_origin() {
130 let geometry = CellGeometry::new(0, 0, 1);
132 assert_eq!(geometry.pixel_to_cell(100.0, 200.0), Pos { x: 0, y: 0 });
133 }
134
135 #[test]
136 fn pixel_to_cell_accounts_for_scale() {
137 let geometry = CellGeometry::new(8, 16, 2);
139 assert_eq!(geometry.pixel_to_cell(20.0, 48.0), Pos { x: 1, y: 1 });
140 }
141
142 #[test]
143 fn pixel_to_cell_clamps_to_u16_max() {
144 let geometry = CellGeometry::new(1, 1, 1);
146 assert_eq!(
147 geometry.pixel_to_cell(f64::from(u32::MAX), f64::from(u32::MAX)),
148 Pos {
149 x: u16::MAX,
150 y: u16::MAX
151 }
152 );
153 }
154}