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