Another project
1use bone_types::{IconId, IconTile};
2
3pub const TILE_PAD: u32 = 2;
4
5#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
6pub struct GridCell {
7 pub col: usize,
8 pub row: usize,
9}
10
11#[derive(Copy, Clone, Debug, PartialEq, Eq)]
12pub struct AtlasGrid {
13 cols: usize,
14 rows: usize,
15}
16
17impl AtlasGrid {
18 pub const DEFAULT: Self = Self { cols: 7, rows: 7 };
19
20 #[must_use]
21 pub const fn new(cols: usize, rows: usize) -> Self {
22 Self { cols, rows }
23 }
24
25 #[must_use]
26 pub const fn cols(self) -> usize {
27 self.cols
28 }
29
30 #[must_use]
31 pub const fn rows(self) -> usize {
32 self.rows
33 }
34
35 #[must_use]
36 pub const fn capacity(self) -> usize {
37 self.cols * self.rows
38 }
39
40 #[must_use]
41 pub fn cell(self, tile: IconTile) -> GridCell {
42 let index = tile.as_usize();
43 GridCell {
44 col: index % self.cols,
45 row: index / self.cols,
46 }
47 }
48
49 #[must_use]
50 pub fn pixel_size(self, page: AtlasPage) -> (u32, u32) {
51 let cell = page.cell_px();
52 let Ok(cols) = u32::try_from(self.cols) else {
53 panic!("atlas grid columns fit a u32");
54 };
55 let Ok(rows) = u32::try_from(self.rows) else {
56 panic!("atlas grid rows fit a u32");
57 };
58 (cols * cell, rows * cell)
59 }
60
61 #[must_use]
62 pub fn tile_origin(self, cell: GridCell, page: AtlasPage) -> (u32, u32) {
63 let stride = page.cell_px();
64 let Ok(col) = u32::try_from(cell.col) else {
65 panic!("atlas cell column fits a u32");
66 };
67 let Ok(row) = u32::try_from(cell.row) else {
68 panic!("atlas cell row fits a u32");
69 };
70 (col * stride + TILE_PAD, row * stride + TILE_PAD)
71 }
72}
73
74const _: () = assert!(
75 IconId::COUNT <= AtlasGrid::DEFAULT.capacity(),
76 "icon count exceeds the default atlas grid; grow AtlasGrid::DEFAULT",
77);
78
79#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
80pub enum AtlasPage {
81 Px16,
82 Px24,
83 Px32,
84 Px48,
85}
86
87impl AtlasPage {
88 pub const ALL: [AtlasPage; 4] = [
89 AtlasPage::Px16,
90 AtlasPage::Px24,
91 AtlasPage::Px32,
92 AtlasPage::Px48,
93 ];
94
95 #[must_use]
96 pub const fn side_px(self) -> u32 {
97 match self {
98 AtlasPage::Px16 => 16,
99 AtlasPage::Px24 => 24,
100 AtlasPage::Px32 => 32,
101 AtlasPage::Px48 => 48,
102 }
103 }
104
105 #[must_use]
106 pub const fn cell_px(self) -> u32 {
107 self.side_px() + 2 * TILE_PAD
108 }
109
110 #[must_use]
111 pub fn nearest(requested_px: u32) -> AtlasPage {
112 AtlasPage::ALL
113 .into_iter()
114 .filter(|page| page.side_px() >= requested_px)
115 .min_by_key(|page| page.side_px())
116 .unwrap_or(AtlasPage::Px48)
117 }
118}
119
120const _: () = assert!(
121 AtlasPage::Px16.side_px() < AtlasPage::Px24.side_px()
122 && AtlasPage::Px24.side_px() < AtlasPage::Px32.side_px()
123 && AtlasPage::Px32.side_px() < AtlasPage::Px48.side_px(),
124 "AtlasPage sizes must stay distinct and ascending so Px48 is the largest fallback for nearest",
125);
126
127#[cfg(test)]
128mod tests {
129 use super::{AtlasGrid, AtlasPage, GridCell};
130 use bone_types::IconId;
131
132 #[test]
133 fn default_grid_holds_every_icon() {
134 assert_eq!(AtlasGrid::DEFAULT.capacity(), 49);
135 assert!(IconId::COUNT <= AtlasGrid::DEFAULT.capacity());
136 }
137
138 #[test]
139 fn cell_is_row_major() {
140 let grid = AtlasGrid::DEFAULT;
141 assert_eq!(grid.cell(IconId::Point.tile()), GridCell { col: 0, row: 0 });
142 assert_eq!(
143 grid.cell(IconId::PerimeterCircle.tile()),
144 GridCell { col: 6, row: 0 }
145 );
146 assert_eq!(
147 grid.cell(IconId::CornerRectangle.tile()),
148 GridCell { col: 0, row: 1 }
149 );
150 assert_eq!(
151 grid.cell(IconId::CenterRectangle.tile()),
152 GridCell { col: 1, row: 1 }
153 );
154 }
155
156 #[test]
157 fn every_icon_cell_is_within_the_grid() {
158 let grid = AtlasGrid::DEFAULT;
159 IconId::ALL.iter().for_each(|icon| {
160 let cell = grid.cell(icon.tile());
161 assert!(cell.col < grid.cols(), "{icon:?} column out of range");
162 assert!(cell.row < grid.rows(), "{icon:?} row out of range");
163 });
164 }
165
166 #[test]
167 fn nearest_page_picks_the_smallest_sufficient_size() {
168 assert_eq!(AtlasPage::nearest(14), AtlasPage::Px16);
169 assert_eq!(AtlasPage::nearest(16), AtlasPage::Px16);
170 assert_eq!(AtlasPage::nearest(17), AtlasPage::Px24);
171 assert_eq!(AtlasPage::nearest(48), AtlasPage::Px48);
172 assert_eq!(AtlasPage::nearest(1000), AtlasPage::Px48);
173 }
174
175 #[test]
176 fn page_sides_match_logical_sizes() {
177 assert_eq!(AtlasPage::Px16.side_px(), 16);
178 assert_eq!(AtlasPage::Px24.side_px(), 24);
179 assert_eq!(AtlasPage::Px32.side_px(), 32);
180 assert_eq!(AtlasPage::Px48.side_px(), 48);
181 }
182}