1use std::collections::{BTreeMap, HashMap};
4
5
6use fontdue::{
9 Font,
10 layout::Layout
11};
12use three_d::{
13 Vec2,
14 Srgba, Context,
15 CpuTexture, CpuMaterial,
16 TextureData, Wrapping
17};
18use rectangle_pack::{
19 GroupedRectsToPlace, RectToInsert, TargetBin, RectanglePackOk
20};
21
22
23use crate::TextRef;
26use crate::builder::TextBuilderSettings;
27use crate::geometry::material::TextMaterial;
28use crate::glyph::{CachedGlyph, PositionedGlyph};
29
30
31pub struct GlyphCache {
53 raster_size: f32,
54
55 atlas_is_exhausted: bool,
56 atlas_requires_update: bool,
57 atlas_fill_ratio: f32,
58 atlas_max_size: u32,
59 atlas_rects: GroupedRectsToPlace<char>,
60
61 glyph_padding: u32,
62 glyph_alpha_transform: Option<&'static dyn Fn(u8) -> u8>,
63 glyphs: HashMap<char, CachedGlyph>,
64
65 cpu_material: CpuMaterial,
66 gpu_material: Option<TextMaterial>
67}
68
69impl GlyphCache {
70 pub fn is_empty(&self) -> bool {
72 self.glyphs.is_empty()
73 }
74
75 pub fn len(&self) -> usize {
77 self.glyphs.len()
78 }
79
80 pub fn size(&self) -> (u32, u32) {
82 let texture = self.cpu_material.albedo_texture.as_ref().expect("cpu material must always be present");
83 (texture.width, texture.height)
84 }
85
86 pub fn fill_ratio(&self) -> f32 {
88 self.atlas_fill_ratio
89 }
90
91 pub fn is_exhausted(&self) -> bool {
97 self.atlas_is_exhausted
98 }
99
100 pub fn material(&self) -> Option<&TextMaterial> {
104 self.gpu_material.as_ref()
105 }
106
107 pub(crate) fn index(size: f32) -> u32 {
108 (size * 10.0) as u32
109 }
110
111 pub(crate) fn new(size: u32, settings: &TextBuilderSettings) -> Self {
112 let u = settings.cache_atlas_min_size;
113 let bytes = (u * u) as usize;
114 Self {
115 raster_size: size as f32 * 0.1,
116
117 atlas_is_exhausted: false,
118 atlas_requires_update: true,
119 atlas_fill_ratio: 0.0,
120 atlas_max_size: settings.cache_atlas_max_size,
121 atlas_rects: GroupedRectsToPlace::new(),
122
123 glyph_padding: settings.cache_atlas_glyph_padding,
124 glyph_alpha_transform: settings.glyph_alpha_transform,
125 glyphs: HashMap::with_capacity(32),
126
127 cpu_material: CpuMaterial {
128 albedo: Srgba::from([1.0, 1.0, 1.0, 1.0]),
129 albedo_texture: Some(CpuTexture {
130 name: format!("text_renderer_cache_{size}"),
131 data: TextureData::RU8(std::iter::repeat(0).take(bytes).collect()),
132 width: u,
133 height: u,
134 min_filter: settings.texture_filter,
135 mag_filter: settings.texture_filter,
136 mip_map_filter: None,
137 wrap_s: Wrapping::ClampToEdge,
138 wrap_t: Wrapping::ClampToEdge
139 }),
140 ..Default::default()
141 },
142 gpu_material: None
143 }
144 }
145
146 pub(crate) fn get_glyph(&self, character: char) -> Option<&CachedGlyph> {
147 self.glyphs.get(&character)
148 }
149
150 pub(crate) fn layout_glyphs(
151 &mut self,
152 font: &Font,
153 layout: &mut Layout,
154 line_height: f32,
155 text: &TextRef
156
157 ) -> (Vec<PositionedGlyph>, Vec2) {
158 text.walk_glyphs(font, layout, line_height, |index, count, glyph, transform, color| {
159 let is_known_glyph = self.glyphs.contains_key(&glyph.parent);
161
162 if glyph.parent.is_whitespace() ||
164 self.atlas_is_exhausted && !is_known_glyph {
166 None
167
168 } else if !is_known_glyph {
170 let (metrics, pixels) = font.rasterize(glyph.parent, self.raster_size);
171 self.atlas_rects.push_rect(
172 glyph.parent,
173 None,
174 RectToInsert::new(
175 metrics.width as u32 + self.glyph_padding * 2,
176 metrics.height as u32 + self.glyph_padding * 2,
177 1
178 )
179 );
180 self.glyphs.insert(glyph.parent, CachedGlyph::new(metrics, pixels));
181 self.atlas_requires_update = true;
182 Some(PositionedGlyph::new(
183 glyph,
184 transform.map(|t| t(index, count, glyph.parent)),
185 color.map(|t| t(index, count, glyph.parent))
186 ))
187
188 } else {
190 Some(PositionedGlyph::new(
191 glyph,
192 transform.map(|t| t(index, count, glyph.parent)),
193 color.map(|t| t(index, count, glyph.parent))
194 ))
195 }
196 })
197 }
198
199 pub(crate) fn update_material(&mut self, context: &Context) -> TextMaterial {
200 if self.atlas_requires_update {
202 self.atlas_requires_update = false;
203 match self.update_atlas() {
204 Some(()) => {
205 self.gpu_material = None;
206 },
207 None => {
208 self.atlas_is_exhausted = true;
209 }
210 }
211 }
212
213 if self.gpu_material.is_none() {
215 self.gpu_material = TextMaterial::new(context, &self.cpu_material);
216 }
217
218 self.gpu_material.as_ref().unwrap().clone()
220 }
221
222 fn update_atlas(&mut self) -> Option<()> {
223 let texture = self.cpu_material.albedo_texture.as_ref().unwrap();
226 let (packing, required_size) = self.repack_atlas(texture.width)?;
227
228 let texture = self.cpu_material.albedo_texture.as_mut().unwrap();
230 if let TextureData::RU8(ref mut texture_data) = texture.data {
231 if texture.width != required_size {
233 let current = (texture.width * texture.height) as usize;
234 let target = (required_size * required_size) as usize;
235 let missing = target - current;
236 texture_data.extend_from_slice(&vec![0; missing]);
237 texture.width = required_size;
238 texture.height = required_size;
239
240 for glyph in self.glyphs.values_mut() {
242 glyph.invalidate();
243 }
244 }
245
246 let mut filled_pixels = 0;
248 for (character, (_, location)) in packing.packed_locations() {
249 if let Some(glyph) = self.glyphs.get_mut(character) {
250 filled_pixels += glyph.render_to_texture(
251 texture_data,
252 texture.width,
253 location,
254 self.glyph_padding,
255 self.glyph_alpha_transform
256 );
257 }
258 }
259 let available_pixels = texture.width * texture.height;
260 self.atlas_fill_ratio = if available_pixels > 0 {
261 1.0 / available_pixels as f32 * filled_pixels as f32
262
263 } else {
264 1.0
265 };
266 }
267 Some(())
268 }
269
270 fn repack_atlas(&mut self, mut current_size: u32) -> Option<(RectanglePackOk<char, i32>, u32)> {
271 loop {
272 let mut target_bins = BTreeMap::new();
273 target_bins.insert(0, TargetBin::new(current_size, current_size, 1));
274
275 if let Ok(result) = rectangle_pack::pack_rects(
277 &self.atlas_rects,
278 &mut target_bins,
279 &rectangle_pack::volume_heuristic,
280 &rectangle_pack::contains_smallest_box
281 ) {
282 return Some((result, current_size));
283
284 } else if current_size < self.atlas_max_size {
286 current_size *= 2;
287
288 } else {
292 return None;
293 }
294 };
295 }
296}
297