1use ecolor::Color32;
2use emath::{Rect, remap_clamp};
3
4use crate::{ColorImage, ImageDelta, TextOptions};
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7struct Rectu {
8 min_x: usize,
10
11 min_y: usize,
13
14 max_x: usize,
16
17 max_y: usize,
19}
20
21impl Rectu {
22 const NOTHING: Self = Self {
23 min_x: usize::MAX,
24 min_y: usize::MAX,
25 max_x: 0,
26 max_y: 0,
27 };
28 const EVERYTHING: Self = Self {
29 min_x: 0,
30 min_y: 0,
31 max_x: usize::MAX,
32 max_y: usize::MAX,
33 };
34}
35
36#[derive(Copy, Clone, Debug)]
37struct PrerasterizedDisc {
38 r: f32,
39 uv: Rectu,
40}
41
42#[derive(Copy, Clone, Debug)]
44pub struct PreparedDisc {
45 pub r: f32,
47
48 pub w: f32,
50
51 pub uv: Rect,
54}
55
56#[derive(Clone)]
60pub struct TextureAtlas {
61 image: ColorImage,
62
63 dirty: Rectu,
65
66 cursor: (usize, usize),
68
69 row_height: usize,
70
71 overflowed: bool,
73
74 discs: Vec<PrerasterizedDisc>,
76
77 options: TextOptions,
79}
80
81impl TextureAtlas {
82 pub fn new(size: [usize; 2], options: TextOptions) -> Self {
83 assert!(size[0] >= 1024, "Tiny texture atlas");
84 let mut atlas = Self {
85 image: ColorImage::filled(size, Color32::TRANSPARENT),
86 dirty: Rectu::EVERYTHING,
87 cursor: (0, 0),
88 row_height: 0,
89 overflowed: false,
90 discs: vec![], options,
92 };
93
94 let (pos, image) = atlas.allocate((1, 1));
96 assert_eq!(
97 pos,
98 (0, 0),
99 "Expected the first allocation to be at (0, 0), but was at {pos:?}"
100 );
101 image[pos] = Color32::WHITE;
102
103 const LARGEST_CIRCLE_RADIUS: f32 = 8.0; for i in 0.. {
111 let r = 2.0_f32.powf(i as f32 / 2.0 - 1.0);
112 if r > LARGEST_CIRCLE_RADIUS {
113 break;
114 }
115 let hw = (r + 0.5).ceil() as i32;
116 let w = (2 * hw + 1) as usize;
117 let ((x, y), image) = atlas.allocate((w, w));
118 for dx in -hw..=hw {
119 for dy in -hw..=hw {
120 let distance_to_center = ((dx * dx + dy * dy) as f32).sqrt();
121 let coverage =
122 remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0);
123 image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] = options
124 .color_transfer_function
125 .color_from_coverage(coverage);
126 }
127 }
128 atlas.discs.push(PrerasterizedDisc {
129 r,
130 uv: Rectu {
131 min_x: x,
132 min_y: y,
133 max_x: x + w,
134 max_y: y + w,
135 },
136 });
137 }
138
139 atlas
140 }
141
142 pub fn options(&self) -> &TextOptions {
143 &self.options
144 }
145
146 pub fn size(&self) -> [usize; 2] {
147 self.image.size
148 }
149
150 pub fn prepared_discs(&self) -> Vec<PreparedDisc> {
152 let size = self.size();
153 let inv_w = 1.0 / size[0] as f32;
154 let inv_h = 1.0 / size[1] as f32;
155 self.discs
156 .iter()
157 .map(|disc| {
158 let r = disc.r;
159 let Rectu {
160 min_x,
161 min_y,
162 max_x,
163 max_y,
164 } = disc.uv;
165 let w = max_x - min_x;
166 let uv = Rect::from_min_max(
167 emath::pos2(min_x as f32 * inv_w, min_y as f32 * inv_h),
168 emath::pos2(max_x as f32 * inv_w, max_y as f32 * inv_h),
169 );
170 PreparedDisc { r, w: w as f32, uv }
171 })
172 .collect()
173 }
174
175 fn max_height(&self) -> usize {
176 self.image.height().max(self.image.width())
178 }
179
180 pub fn fill_ratio(&self) -> f32 {
182 if self.overflowed {
183 1.0
184 } else {
185 (self.cursor.1 + self.row_height) as f32 / self.max_height() as f32
186 }
187 }
188
189 #[inline]
191 pub fn texture_options() -> crate::textures::TextureOptions {
192 crate::textures::TextureOptions::LINEAR
193 }
194
195 #[inline]
197 pub fn image(&self) -> &ColorImage {
198 &self.image
199 }
200
201 pub fn take_delta(&mut self) -> Option<ImageDelta> {
203 let texture_options = Self::texture_options();
204
205 let dirty = core::mem::replace(&mut self.dirty, Rectu::NOTHING);
206 if dirty == Rectu::NOTHING {
207 None
208 } else if dirty == Rectu::EVERYTHING {
209 Some(ImageDelta::full(self.image.clone(), texture_options))
210 } else {
211 let pos = [dirty.min_x, dirty.min_y];
212 let size = [dirty.max_x - dirty.min_x, dirty.max_y - dirty.min_y];
213 let region = self.image.region_by_pixels(pos, size);
214 Some(ImageDelta::partial(pos, region, texture_options))
215 }
216 }
217
218 pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut ColorImage) {
221 const PADDING: usize = 1;
225
226 assert!(
227 w <= self.image.width(),
228 "Tried to allocate a {} wide glyph in a {} wide texture atlas",
229 w,
230 self.image.width()
231 );
232 if self.cursor.0 + w > self.image.width() {
233 self.cursor.0 = 0;
235 self.cursor.1 += self.row_height + PADDING;
236 self.row_height = 0;
237 }
238
239 self.row_height = self.row_height.max(h);
240
241 let required_height = self.cursor.1 + self.row_height;
242
243 if required_height > self.max_height() {
244 log::warn!("epaint texture atlas overflowed!");
247
248 self.cursor = (0, self.image.height() / 3); self.overflowed = true; } else if resize_to_min_height(&mut self.image, required_height) {
251 self.dirty = Rectu::EVERYTHING;
252 }
253
254 let pos = self.cursor;
255 self.cursor.0 += w + PADDING;
256
257 self.dirty.min_x = self.dirty.min_x.min(pos.0);
258 self.dirty.min_y = self.dirty.min_y.min(pos.1);
259 self.dirty.max_x = self.dirty.max_x.max(pos.0 + w);
260 self.dirty.max_y = self.dirty.max_y.max(pos.1 + h);
261
262 (pos, &mut self.image)
263 }
264}
265
266fn resize_to_min_height(image: &mut ColorImage, required_height: usize) -> bool {
267 while required_height >= image.height() {
268 image.size[1] *= 2; }
270
271 if image.width() * image.height() > image.pixels.len() {
272 image
273 .pixels
274 .resize(image.width() * image.height(), Color32::TRANSPARENT);
275 true
276 } else {
277 false
278 }
279}