three_d_text_builder/builder.rs
1// STD Dependencies -----------------------------------------------------------
2// ----------------------------------------------------------------------------
3use std::error::Error;
4use std::collections::HashMap;
5
6
7// External Dependencies ------------------------------------------------------
8// ----------------------------------------------------------------------------
9use fontdue::{
10 Font, FontSettings,
11 layout::{CoordinateSystem, Layout}
12};
13use three_d::{
14 Context, Gm,
15 Interpolation, Mat4, SquareMatrix,
16 Viewport, Vec2, vec2, Srgba
17};
18
19
20// Internal Dependencies ------------------------------------------------------
21// ----------------------------------------------------------------------------
22use crate::TextRef;
23use crate::cache::GlyphCache;
24use crate::geometry::material::TextMaterial;
25use crate::geometry::mesh::{TextMesh, TextMeshData};
26use crate::glyph::PositionedGlyph;
27
28
29// Types ----------------------------------------------------------------------
30// ----------------------------------------------------------------------------
31type TextLayoutResult<'a> = (u32, &'a TextRef<'a>, (Vec<PositionedGlyph>, Vec2));
32
33
34///
35/// Controls the behaviour of a [``TextBuilder``] and its corresponding [``GlyphCache``]s.
36///
37pub struct TextBuilderSettings {
38 /// Default font scale to use - see [``fontdue::FontSettings``] for more details
39 pub default_font_scale: f32,
40 /// Default line height scaling to render with
41 pub default_line_height: f32,
42 /// Minimum size a glyph cache's texture starts and is allocated with
43 pub cache_atlas_min_size: u32,
44 /// Maximum size a glyph cache's texture before no further glyphs are added
45 pub cache_atlas_max_size: u32,
46 /// Number of pixels to be inserted on all sides of a glyph during rasterization;
47 /// used to prevent UV aliasing artifacts.
48 pub cache_atlas_glyph_padding: u32,
49 /// Function to modify the pixel alpha values of all glyphs
50 pub glyph_alpha_transform: Option<&'static dyn Fn(u8) -> u8>,
51 /// Texture interpolation used by a glyph cache's texture
52 pub texture_filter: Interpolation
53}
54
55impl Default for TextBuilderSettings {
56 fn default() -> Self {
57 Self {
58 default_font_scale: 32.0,
59 default_line_height: 1.0,
60 cache_atlas_min_size: 64,
61 cache_atlas_max_size: 4096,
62 cache_atlas_glyph_padding: 2,
63 glyph_alpha_transform: None,
64 texture_filter: Interpolation::Linear,
65 }
66 }
67}
68
69
70///
71/// Builder for creating models from a slice of [``TextRef``]'s.
72///
73pub struct TextBuilder {
74 font: Font,
75 settings: TextBuilderSettings,
76 glyph_cache: HashMap<u32, GlyphCache>,
77 glyph_layout: Layout,
78 edge_color: Option<Srgba>,
79 viewport: Vec2,
80 projection_view: Mat4
81}
82
83impl TextBuilder {
84 ///
85 /// Create a new builder from the given TrueType Font bytes and settings.
86 ///
87 /// # Errors
88 ///
89 /// See [``fontdue::Font::from_bytes``] for possible error return values.
90 ///
91 pub fn new(font_bytes: &[u8], settings: TextBuilderSettings) -> Result<Self, Box<dyn Error>> {
92 Ok(Self {
93 font: Font::from_bytes(font_bytes, FontSettings {
94 scale: settings.default_font_scale,
95 ..Default::default()
96 })?,
97 settings,
98 glyph_cache: HashMap::with_capacity(8),
99 glyph_layout: Layout::new(CoordinateSystem::PositiveYUp),
100 edge_color: None,
101 viewport: vec2(0.0, 0.0),
102 projection_view: Mat4::identity()
103 })
104 }
105
106 /// Gets the underlying glyph cache for the given font size.
107 pub fn get_glyph_cache(&mut self, font_render_size: f32) -> Option<&mut GlyphCache> {
108 self.glyph_cache.get_mut(&GlyphCache::index(font_render_size))
109 }
110
111 /// Sets the viewport used by [``crate::TextAlign::Viewport``]
112 /// and [``crate::TextAlign::Scene``].
113 pub fn set_viewport(&mut self, viewport: Viewport) {
114 self.viewport = vec2(viewport.width as f32, viewport.height as f32);
115 }
116
117 /// Sets the projection matrix used by [``crate::TextAlign::Scene``].
118 pub fn set_projection_view(&mut self, matrix: Mat4) {
119 self.projection_view = matrix;
120 }
121
122 /// Sets the color used for rendering debug edges of glyphs quads and text meshes.
123 ///
124 /// This is mostly useful for debugging purposes e.g. verifying text and
125 /// character alignments.
126 pub fn set_edge_color(&mut self, color: Option<Srgba>) {
127 self.edge_color = color;
128 }
129
130 ///
131 /// Clears all underlying glyph caches and their corresponding CPU textures.
132 ///
133 /// > **Note:** GPU materials of any models created via
134 /// > [[``TextBuilder::build``]] will **not** be invalided by this.
135 ///
136 pub fn clear_caches(&mut self) {
137 self.glyph_cache.clear();
138 }
139
140 ///
141 /// Computes the pixel size of a given [``TextRef``].
142 ///
143 pub fn compute_pixel_size(&mut self, text: &TextRef) -> Vec2 {
144 text.walk_glyphs::<(), _>(
145 &self.font,
146 &mut self.glyph_layout,
147 self.settings.default_line_height,
148 |_, _, _, _, _| None,
149 ).1
150 }
151
152 ///
153 /// Builds one ore more text models the a slice of [``TextRef``]'s.
154 ///
155 pub fn build<'a>(
156 &mut self,
157 context: &'a Context,
158 texts: &[TextRef]
159
160 ) -> impl Iterator<Item=Gm<TextMesh, TextMaterial>> + 'a {
161 // Layout all glyphs and render them into the atlas texture of the corresponding caches
162 let text_layout_results: Vec<TextLayoutResult> = texts.iter().filter(|l| !l.text.is_empty()).map(|text| {
163 let cache_index = GlyphCache::index(text.glyph_raster_size());
164 let cache = self.glyph_cache.entry(cache_index).or_insert_with(|| {
165 GlyphCache::new(cache_index, &self.settings)
166 });
167 (
168 cache_index,
169 text,
170 cache.layout_glyphs(
171 &self.font,
172 &mut self.glyph_layout,
173 self.settings.default_line_height,
174 text
175 )
176 )
177
178 }).collect();
179
180 // Once the caches have all the required glyphs, update their atlases and materials.
181 //
182 // Then create the meshes with the material UVs which are now guranteed
183 // be and stay stable
184 text_layout_results.into_iter().fold(
185 HashMap::with_capacity(texts.len().min(4)),
186 |mut meshes, (cache_index, text, (positioned_glyphs, dimensions))| {
187
188 let cache = self.glyph_cache
189 .get_mut(&cache_index)
190 .expect("cache must be created before mesh creation");
191
192 // In case the atlas has changed, a new GPU texture will get uploaded.
193 // Leaving the old one untouched and thus keeping any previously
194 // generated models valid
195 let mut material = cache.update_material(context);
196 material.set_edge_color(self.edge_color);
197
198 // Merge all texts sharing the same font size into a single mesh
199 let index_count = positioned_glyphs.len() * 6;
200 let vertex_count = positioned_glyphs.len() * 4;
201 let mesh_data = meshes
202 .entry(cache_index)
203 .or_insert_with(|| TextMeshData {
204 material,
205 indices: Vec::new(),
206 positions: Vec::new(),
207 colors: Vec::new(),
208 quad_uvs: Vec::new(),
209 glyph_uvs: Vec::new()
210 });
211
212 // Extend the mesh with the new glyphs
213 mesh_data.indices.reserve(index_count);
214 mesh_data.positions.reserve(vertex_count);
215 mesh_data.colors.reserve(vertex_count);
216 mesh_data.quad_uvs.reserve(vertex_count);
217 mesh_data.glyph_uvs.reserve(vertex_count);
218
219 // Compute offsets - for scene alignments these might be none if
220 // they would lie outside the viewport
221 if let Some((
222 global_offset,
223 local_offset
224
225 )) = text.align.into_global_and_local_offset(
226 self.projection_view,
227 self.viewport,
228 dimensions
229 ) {
230 // Build optional debug mesh to show full text bounds
231 if self.edge_color.is_some() {
232 text.extend_mesh(
233 mesh_data,
234 self.viewport,
235 global_offset,
236 local_offset,
237 dimensions
238 );
239 }
240
241 // Build individual glyph meshes
242 for p in positioned_glyphs {
243 p.extend_mesh(
244 mesh_data,
245 cache,
246 text,
247 self.viewport,
248 global_offset,
249 local_offset
250 );
251 }
252 }
253 meshes
254
255 // Return an iterator over the combined meshes
256 }).into_values().filter(|data| !data.indices.is_empty()).map(|data| Gm::new(
257 TextMesh::new(context, &data),
258 data.material
259 ))
260 }
261}
262