1use std::num::NonZeroU32;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct Allocation {
8 pub x: u32,
9 pub y: u32,
10 pub width: u32,
11 pub height: u32,
12}
13
14#[derive(Debug)]
15pub struct ShelfAtlas {
16 width: u32,
17 height: u32,
18 shelves: Vec<Shelf>,
19 next_shelf_y: u32,
20}
21
22#[derive(Debug)]
23struct Shelf {
24 y_origin: u32,
25 height: u32,
26 x_cursor: u32,
27}
28
29impl ShelfAtlas {
30 #[must_use]
31 pub const fn new(width: NonZeroU32, height: NonZeroU32) -> Self {
32 Self {
33 width: width.get(),
34 height: height.get(),
35 shelves: Vec::new(),
36 next_shelf_y: 0,
37 }
38 }
39
40 #[must_use]
41 pub const fn width(&self) -> u32 {
42 self.width
43 }
44
45 #[must_use]
46 pub const fn height(&self) -> u32 {
47 self.height
48 }
49
50 pub fn alloc(&mut self, width: u32, height: u32) -> Option<Allocation> {
51 if width == 0 || height == 0 {
52 return Some(Allocation {
53 x: 0,
54 y: 0,
55 width,
56 height,
57 });
58 }
59 if width > self.width || height > self.height {
60 return None;
61 }
62 for shelf in &mut self.shelves {
63 if height <= shelf.height && shelf.x_cursor + width <= self.width {
64 let alloc = Allocation {
65 x: shelf.x_cursor,
66 y: shelf.y_origin,
67 width,
68 height,
69 };
70 shelf.x_cursor += width;
71 return Some(alloc);
72 }
73 }
74 if self.next_shelf_y.saturating_add(height) > self.height {
75 return None;
76 }
77 let y = self.next_shelf_y;
78 self.shelves.push(Shelf {
79 y_origin: y,
80 height,
81 x_cursor: width,
82 });
83 self.next_shelf_y += height;
84 Some(Allocation {
85 x: 0,
86 y,
87 width,
88 height,
89 })
90 }
91
92 pub fn reset(&mut self) {
93 self.shelves.clear();
94 self.next_shelf_y = 0;
95 }
96
97 #[must_use]
98 pub const fn is_empty(&self) -> bool {
99 self.shelves.is_empty()
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 fn nz(v: u32) -> NonZeroU32 {
108 NonZeroU32::new(v).unwrap()
109 }
110
111 fn overlap(a: Allocation, b: Allocation) -> bool {
112 let ax2 = a.x + a.width;
113 let ay2 = a.y + a.height;
114 let bx2 = b.x + b.width;
115 let by2 = b.y + b.height;
116 a.x < bx2 && b.x < ax2 && a.y < by2 && b.y < ay2
117 }
118
119 #[test]
120 fn first_alloc_lands_at_origin() {
121 let mut atlas = ShelfAtlas::new(nz(64), nz(64));
122 let a = atlas.alloc(10, 12).unwrap();
123 assert_eq!(
124 a,
125 Allocation {
126 x: 0,
127 y: 0,
128 width: 10,
129 height: 12
130 }
131 );
132 }
133
134 #[test]
135 fn fills_first_shelf_then_opens_a_second() {
136 let mut atlas = ShelfAtlas::new(nz(20), nz(20));
137 let a = atlas.alloc(10, 8).unwrap();
138 let b = atlas.alloc(10, 8).unwrap();
139 assert_eq!(a.y, b.y, "two items of equal height share a shelf");
140 assert_eq!(b.x, 10);
141
142 let c = atlas.alloc(10, 5).unwrap();
143 assert_eq!(c.y, 8);
144 assert_eq!(c.x, 0);
145 }
146
147 #[test]
148 fn returns_none_when_atlas_is_full() {
149 let mut atlas = ShelfAtlas::new(nz(8), nz(8));
150 assert!(atlas.alloc(8, 8).is_some());
151 assert!(atlas.alloc(1, 1).is_none(), "no shelves left to open");
152 }
153
154 #[test]
155 fn rejects_request_larger_than_atlas() {
156 let mut atlas = ShelfAtlas::new(nz(16), nz(16));
157 assert!(atlas.alloc(17, 1).is_none());
158 assert!(atlas.alloc(1, 17).is_none());
159 }
160
161 #[test]
162 fn zero_sized_alloc_succeeds_without_consuming_space() {
163 let mut atlas = ShelfAtlas::new(nz(8), nz(8));
164 let a = atlas.alloc(0, 0).unwrap();
165 assert_eq!(
166 a,
167 Allocation {
168 x: 0,
169 y: 0,
170 width: 0,
171 height: 0
172 }
173 );
174 assert!(atlas.is_empty());
175 }
176
177 #[test]
178 fn reset_makes_the_atlas_reusable() {
179 let mut atlas = ShelfAtlas::new(nz(8), nz(8));
180 atlas.alloc(8, 8).unwrap();
181 assert!(atlas.alloc(1, 1).is_none());
182 atlas.reset();
183 assert!(atlas.alloc(8, 8).is_some());
184 }
185
186 #[test]
187 fn shelf_height_is_locked_by_first_item() {
188 let mut atlas = ShelfAtlas::new(nz(20), nz(30));
189 let _tall = atlas.alloc(5, 10).unwrap();
190 let next = atlas.alloc(5, 12).unwrap();
191 assert_eq!(next.y, 10, "new shelf opens immediately above the first");
192 }
193
194 #[test]
195 fn allocations_never_overlap_for_a_realistic_glyph_mix() {
196 let mut atlas = ShelfAtlas::new(nz(128), nz(128));
197 let sizes = [
198 (8, 12),
199 (8, 12),
200 (8, 12),
201 (12, 12),
202 (8, 14),
203 (8, 14),
204 (10, 16),
205 (10, 16),
206 (8, 12),
207 (8, 12),
208 (16, 20),
209 (8, 12),
210 ];
211 let mut allocs = Vec::new();
212 for (w, h) in sizes {
213 let a = atlas.alloc(w, h).expect("realistic mix should fit");
214 for prev in &allocs {
215 assert!(!overlap(a, *prev), "{a:?} overlaps {prev:?}");
216 }
217 assert!(a.x + a.width <= atlas.width());
218 assert!(a.y + a.height <= atlas.height());
219 allocs.push(a);
220 }
221 }
222}