three_d_text_builder/
lib.rs

1#![warn(clippy::all)]
2#![warn(missing_docs)]
3#![warn(unsafe_code)]
4//!
5//! Glyph atlas based text rendering for the [three-d](https://crates.io/crates/three-d)
6//! crate, using [fontdue](https://crates.io/crates/fontdue).
7//!
8//! This crate's aim is to provide decent performance and quality of the rendered text.
9//!
10//! It is suited for both large static text elements as well as dynamically
11//! formatted text that quickly changes.
12//!
13//! # Quick Example
14//!
15//! ```rust
16//! // Create a text builder from a TTF font
17//! let mut text_builder = TextBuilder::new(
18//!     &include_str!("font.ttf"),
19//!     TextBuilderSettings::default()
20//!
21//! ).unwrap();
22//!
23//! // Create some text
24//! let text = TextRef {
25//!     // The text to render
26//!     text: "The quick brown fox jumps over the lazy dog",
27//!     // Set the color
28//!     color: Srgba::RED,
29//!     // Align to the lower center edge of the viewport
30//!     align: TextAlign::Viewport(0, -1),
31//!     // Add some padding
32//!     padding: vec2(0.0, 8.0),
33//!     // Move up by 25% of the viewport's height
34//!     position: TextPosition::Percentage(vec2(0.0, 0.25)),
35//!     // Add a simple shadow effect
36//!     shadow: Some((Srgba::BLACK, vec2(1.0, -1.0))),
37//!     ..Default::default()
38//! };
39//!
40//! // Build a 3D model for our text - this could also be cached for later usage
41//! // to avoid building the same model multiple times
42//! let text_model = text_builder.build(&context, &[text]);
43//!
44//! // Setup the viewport so our alignment works as expected
45//! text_builder.set_viewport(viewport);
46//!
47//! // Then render it to a target
48//! render_target.render(&camera, text_model, &[]);
49//! ```
50//!
51//! For more use cases, check out the standalone examples.
52//!
53//!
54//! # Implementation Notes
55//!
56//! Each [``TextBuilder``] uses a set of [``GlyphCache``]s (one for each font size)
57//! which produce materials from their glyph atlases. These get mapped to
58//! the UVs of generated character quads, which in turn get combined into a
59//! set of meshes. Text meshes that share the same underlying atlas material
60//! get combined into a single mesh in order to improve drawing performance.
61//!
62//! In addition, a copy on write approach is employed to keep the number of
63//! uploaded GPU materials to a minimum, while also allow generated models to
64//! be cached by the application code.
65//!
66
67// STD Dependencies -----------------------------------------------------------
68// ----------------------------------------------------------------------------
69use std::cmp::Ordering;
70
71
72// External Dependencies ------------------------------------------------------
73// ----------------------------------------------------------------------------
74use three_d::{Mat4, vec2, Vec2, Vec3, Transform};
75
76
77// Modules --------------------------------------------------------------------
78// ----------------------------------------------------------------------------
79mod builder;
80mod cache;
81mod geometry;
82mod glyph;
83mod text;
84mod text_ref;
85
86
87// Re-Exports -----------------------------------------------------------------
88// ----------------------------------------------------------------------------
89pub use builder::{TextBuilder, TextBuilderSettings};
90pub use cache::GlyphCache;
91pub use geometry::material::TextMaterial;
92pub use geometry::mesh::TextMesh;
93pub use text::Text;
94pub use text_ref::TextRef;
95
96
97///
98/// Specifies the unit used by the `position` property of a [``TextRef``].
99///
100#[derive(Debug, Clone, Copy)]
101pub enum TextPosition {
102    ///
103    /// Position is interpreted as pixel values.
104    ///
105    /// # Examples
106    ///
107    /// - `TextPosition::Pixels(vec2(0.0, 64.0))`: Move the text up by exactly `64` pixels
108    ///
109    Pixels(Vec2),
110    ///
111    /// Position is interpreted as a percentage of the viewport's size.
112    ///
113    /// # Examples
114    ///
115    /// - `TextPosition::Percentage(vec2(0.5, 0.0))`: Move the text to the right by half the viewport's width in pixels
116    ///
117    Percentage(Vec2)
118}
119
120impl TextPosition {
121    fn to_pixels(self, viewport: Vec2) -> Vec2 {
122        match self {
123            Self::Pixels(p) => p,
124            Self::Percentage(p) => vec2(viewport.x * p.x, viewport.y * p.y)
125        }
126    }
127}
128
129///
130/// Specifies how the `position` property of a [``TextRef``] is applied.
131///
132#[derive(Debug, Clone, Copy)]
133pub enum TextAlign {
134    ///
135    /// Positions the text relative to the bottom left corner of the screen,
136    /// then anchors it with respect to its bounding box.
137    ///
138    /// # Examples
139    ///
140    /// - `TextAlign::Screen(0, 0)`: Anchors at the center
141    /// - `TextAlign::Screen(1, -1)`: Anchors at the upper left corner
142    ///
143    ScreenAbsolute(i32, i32),
144
145    ///
146    /// Positions the text relative to the center of the screen,
147    /// then anchors it with respect to its bounding box.
148    ///
149    /// # Examples
150    ///
151    /// - `TextAlign::Screen(0, 0)`: Anchors at the center
152    /// - `TextAlign::Screen(1, -1)`: Anchors at the upper left corner
153    ///
154    ScreenRelative(i32, i32),
155
156    ///
157    /// Anchors the text to the edges of the viewport (as set by
158    /// [``TextBuilder::set_viewport``]),
159    /// then positions it with respect to its bounding box.
160    ///
161    /// # Examples
162    ///
163    /// - `TextAlign::Viewport(0, 0)`: Anchors at the lower left corner of the viewport
164    /// - `TextAlign::Viewport(1, 0)`: Anchors at the right edge of the viewport
165    ///
166    Viewport(i32, i32),
167
168    ///
169    /// Anchors the text to the projected position of the 3D point (as described by
170    /// [``TextBuilder::set_projection_view``]),
171    /// then positions it with respect to its bounding box.
172    ///
173    /// # Examples
174    ///
175    /// - `TextAlign::Scene(vec(0.0, 0.0, 0.0), 0, 0)`: Projects the scene's origin then anchors at the center
176    ///
177    Scene(Vec3, i32, i32)
178}
179
180impl Default for TextAlign {
181    fn default() -> Self {
182        Self::ScreenRelative(0, 0)
183    }
184}
185
186impl TextAlign {
187    /// Calculates the global and local offets required to align a rectangle with
188    /// the given bounds inside the specified viewport.
189    ///
190    /// > **Important:** This assumes that the rectangle's lower left corner is
191    /// > at the coordinates `0, 0`.
192    pub fn into_global_and_local_offset(
193        self,
194        world_to_screen: Mat4,
195        viewport: Vec2,
196        bounds: Vec2
197
198    ) -> Option<(Vec2, Vec2)> {
199        fn align_bounds(axis: i32, value: f32) -> f32 {
200            match axis.cmp(&0) {
201                // CEIL here so right-aligned text is stable in its position
202                Ordering::Equal => -(value * 0.5).ceil(),
203                Ordering::Greater => 0.0,
204                Ordering::Less => -value
205            }
206        }
207
208        fn align_viewport(axis: i32, value: f32) -> f32 {
209            // FLOOR here so right-aligned text is stable in its position
210            (axis as f32 * value * 0.5).floor()
211        }
212
213        fn edges(axis: i32, value: f32) -> (f32, f32) {
214            match axis.cmp(&0) {
215                Ordering::Equal => ((value * 0.5).ceil(), -(value * 0.5).floor()),
216                Ordering::Greater => (value, 0.0),
217                Ordering::Less => (0.0, -value)
218            }
219        }
220
221        match self {
222            Self::ScreenAbsolute(x, y) => Some((
223                // Offset from the bottom left of the viewport
224                vec2(align_bounds(0, viewport.x), align_bounds(0, viewport.y)),
225                // Center on the given position
226                vec2(align_bounds(x, bounds.x), -align_bounds(-y, bounds.y))
227            )),
228            Self::ScreenRelative(x, y) => Some((
229                // Offset from the center of the viewport
230                vec2(0.0, 0.0),
231                // Center on the given position
232                vec2(align_bounds(x, bounds.x), -align_bounds(-y, bounds.y))
233            )),
234            Self::Viewport(x, y) => Some((
235                // Offset from the edges of the viewport
236                vec2(
237                    align_viewport(x, viewport.x) - align_viewport(x, bounds.x),
238                    align_viewport(y, viewport.y) - align_viewport(y, bounds.y)
239                ),
240                // Center on the given position
241                vec2(align_bounds(0, bounds.x), -align_bounds(0, bounds.y))
242            )),
243            Self::Scene(p, x, y) => {
244                // Project the scene point onto the screen
245                let p = world_to_screen.transform_point(three_d::Point3::new(p.x, p.y, p.z));
246
247                // Check if in front of the camera
248                if p.z < 1.0 {
249                    let center = vec2(align_bounds(0, viewport.x), align_bounds(0, viewport.y));
250                    let sx = p.x * 0.5 + 0.5;
251                    let sy = p.y * 0.5 + 0.5;
252
253                    // Calculate position on screen
254                    let global_offset = vec2(sx * viewport.x, sy * viewport.y);
255                    let local_offset = vec2(align_bounds(x, bounds.x), -align_bounds(-y, bounds.y));
256
257                    // Clip bounds against viewport
258                    let (t, b) = edges(y, bounds.y);
259                    let (r, l) = edges(x, bounds.x);
260                    let within_x = global_offset.x + r >= 0.0 && global_offset.x + l < viewport.x;
261                    let within_y = global_offset.y + t >= 0.0 && global_offset.y + b < viewport.y;
262                    if within_x && within_y {
263                        Some((
264                            // Offset from the bottom left of the viewport
265                            center + global_offset,
266                            // Center on the given position
267                            local_offset
268                        ))
269
270                    } else {
271                        None
272                    }
273
274                } else {
275                    None
276                }
277            }
278        }
279    }
280}
281