Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 5.0 kB View raw
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}