three_d/renderer/
text.rs

1use crate::*;
2use lyon::math::Point;
3use lyon::path::Path;
4use lyon::tessellation::*;
5use std::collections::HashMap;
6use swash::zeno::{Command, PathData};
7use swash::{scale::ScaleContext, shape::ShapeContext, FontRef, GlyphId};
8
9///
10/// Options for text layout.
11///
12#[derive(Debug, Clone, Copy)]
13pub struct TextLayoutOptions {
14    ///
15    /// The line height multiplier where 1.0 corresponds to the maximum height of the font.
16    /// Default is 1.2.
17    ///
18    pub line_height: f32,
19}
20
21impl Default for TextLayoutOptions {
22    fn default() -> Self {
23        Self { line_height: 1.2 }
24    }
25}
26
27///
28/// A utility struct for generating a [CpuMesh] from a text string with a given font.
29///
30pub struct TextGenerator<'a> {
31    map: HashMap<GlyphId, CpuMesh>,
32    font: FontRef<'a>,
33    max_height: f32,
34    size: f32,
35}
36
37impl<'a> TextGenerator<'a> {
38    ///
39    /// Creates a new TextGenerator with the given font and size in pixels per em.
40    /// The index indicates the specific font in a font collection. Set to 0 if unsure.
41    ///
42    pub fn new(font_bytes: &'a [u8], font_index: u32, size: f32) -> Result<Self, RendererError> {
43        let font = FontRef::from_index(font_bytes, font_index as usize)
44            .ok_or(RendererError::MissingFont(font_index))?;
45        let mut context = ScaleContext::new();
46        let mut scaler = context.builder(font).size(size).build();
47        let mut map = HashMap::new();
48        let mut max_height: f32 = 0.0;
49        font.charmap().enumerate(|_, id| {
50            if let Some(outline) = scaler.scale_outline(id) {
51                let mut builder = Path::builder();
52                for command in outline.path().commands() {
53                    match command {
54                        Command::MoveTo(p) => {
55                            builder.begin(Point::new(p.x, p.y));
56                        }
57                        Command::LineTo(p) => {
58                            builder.line_to(Point::new(p.x, p.y));
59                        }
60                        Command::CurveTo(p1, p2, p3) => {
61                            builder.cubic_bezier_to(
62                                Point::new(p1.x, p1.y),
63                                Point::new(p2.x, p2.y),
64                                Point::new(p3.x, p3.y),
65                            );
66                        }
67                        Command::QuadTo(p1, p2) => {
68                            builder.quadratic_bezier_to(
69                                Point::new(p1.x, p1.y),
70                                Point::new(p2.x, p2.y),
71                            );
72                        }
73                        Command::Close => builder.close(),
74                    }
75                }
76                let path = builder.build();
77
78                let mut tessellator = FillTessellator::new();
79                let mut geometry: VertexBuffers<Vec3, u32> = VertexBuffers::new();
80                let options = FillOptions::default();
81                if tessellator
82                    .tessellate_path(
83                        &path,
84                        &options,
85                        &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
86                            vec3(vertex.position().x, vertex.position().y, 0.0)
87                        }),
88                    )
89                    .is_ok()
90                {
91                    let mesh = CpuMesh {
92                        positions: Positions::F32(geometry.vertices),
93                        indices: Indices::U32(geometry.indices),
94                        ..Default::default()
95                    };
96                    max_height = max_height.max(mesh.compute_aabb().size().y);
97                    map.insert(id, mesh);
98                }
99            }
100        });
101        Ok(Self {
102            map,
103            font,
104            max_height,
105            size,
106        })
107    }
108
109    ///
110    /// Generates a [CpuMesh] from the given text string.
111    ///
112    pub fn generate(&self, text: &str, options: TextLayoutOptions) -> CpuMesh {
113        let mut shape_context = ShapeContext::new();
114        let mut shaper = shape_context.builder(self.font).size(self.size).build();
115        let mut positions = Vec::new();
116        let mut indices = Vec::new();
117        let mut position = vec2(0.0, 0.0);
118
119        shaper.add_str(text);
120        shaper.shape_with(|cluster| {
121            let t = text.get(cluster.source.to_range());
122            if matches!(t, Some("\n")) {
123                // Move to the next line
124                position.y -= self.max_height * options.line_height;
125                position.x = 0.0;
126            }
127            for glyph in cluster.glyphs {
128                let mesh = self.map.get(&glyph.id).unwrap();
129
130                let index_offset = positions.len() as u32;
131                let Indices::U32(mesh_indices) = &mesh.indices else {
132                    unreachable!()
133                };
134                indices.extend(mesh_indices.iter().map(|i| i + index_offset));
135
136                let position_offset = (position + vec2(glyph.x, glyph.y)).extend(0.0);
137                let Positions::F32(mesh_positions) = &mesh.positions else {
138                    unreachable!()
139                };
140                positions.extend(mesh_positions.iter().map(|p| p + position_offset));
141            }
142            position.x += cluster.advance();
143        });
144
145        CpuMesh {
146            positions: Positions::F32(positions),
147            indices: Indices::U32(indices),
148            ..Default::default()
149        }
150    }
151}