beamterm_renderer/gl/
atlas.rs

1use std::collections::HashMap;
2
3use beamterm_data::{FontAtlasData, FontStyle};
4use compact_str::{CompactString, ToCompactString};
5use web_sys::console;
6
7use crate::{error::Error, gl::GL};
8
9/// A texture atlas containing font glyphs for efficient WebGL text rendering.
10///
11/// `FontAtlas` manages a WebGL 2D texture array where each layer contains a single
12/// character glyph. This design enables efficient instanced rendering of text by
13/// allowing the GPU to select the appropriate character layer for each rendered cell.
14///
15/// # Architecture
16/// The atlas uses a **WebGL 2D texture array** where:
17/// - Each layer contains one character glyph
18/// - ASCII characters use their ASCII value as the layer index
19/// - Non-ASCII characters are stored in a hash map for layer lookup
20/// - All glyphs have uniform cell dimensions for consistent spacing
21#[derive(Debug)]
22pub struct FontAtlas {
23    /// The underlying texture
24    texture: crate::gl::texture::Texture,
25    /// Symbol to 3d texture index
26    glyph_coords: HashMap<CompactString, u16>,
27    /// The size of each character cell in pixels
28    cell_size: (i32, i32),
29    /// The number of slices in the atlas texture
30    num_slices: u32,
31}
32
33impl FontAtlas {
34    /// Loads the default embedded font atlas.
35    pub fn load_default(gl: &web_sys::WebGl2RenderingContext) -> Result<Self, Error> {
36        let config = FontAtlasData::default();
37        Self::load(gl, config)
38    }
39
40    /// Creates a TextureAtlas from a grid of equal-sized cells
41    pub fn load(
42        gl: &web_sys::WebGl2RenderingContext,
43        config: FontAtlasData,
44    ) -> Result<Self, Error> {
45        let texture = crate::gl::texture::Texture::from_font_atlas_data(gl, GL::RGBA, &config)?;
46        let num_slices = config.texture_dimensions.2;
47
48        let texture_layers = config.glyphs.iter().map(|g| g.id as i32).max().unwrap_or(0) + 1;
49        console::log_1(
50            &format!("Creating atlas grid with {}/{texture_layers} layers", config.glyphs.len())
51                .into(),
52        );
53
54        let (cell_width, cell_height) = config.cell_size;
55        let mut layers = HashMap::new();
56
57        // we only store the normal-styled glyphs (incl emoji) in the atlas lookup,
58        // as the correct layer id can be derived from the base glyph id plus font style
59        config.glyphs.iter()
60            .filter(|g| g.style == FontStyle::Normal) // only normal style glyphs
61            .filter(|g| !g.is_ascii())                // only non-ascii glyphs
62            .for_each(|g| {
63                layers.insert(g.symbol.to_compact_string(), g.id);
64            });
65
66        Ok(Self {
67            texture,
68            glyph_coords: layers,
69            cell_size: (cell_width, cell_height),
70            num_slices: num_slices as u32,
71        })
72    }
73
74    /// Binds the atlas texture to the specified texture unit
75    pub fn bind(&self, gl: &web_sys::WebGl2RenderingContext, texture_unit: u32) {
76        self.texture.bind(gl, texture_unit);
77    }
78
79    pub fn cell_size(&self) -> (i32, i32) {
80        let (w, h) = self.cell_size;
81        (w - 2 * FontAtlasData::PADDING, h - 2 * FontAtlasData::PADDING)
82        // self.cell_size
83    }
84
85    /// Returns the texture array z-offset for the given key
86    pub(super) fn get_glyph_coord(&self, key: &str, font_style: FontStyle) -> Option<u16> {
87        if key.len() == 1 {
88            let ch = key.chars().next().unwrap();
89            if ch.is_ascii() {
90                // 0x00..0x7f double as layer
91                let id = ch as u16 | font_style.style_mask();
92                return Some(id);
93            }
94        }
95
96        self.glyph_coords.get(key).copied().map(|id| id | font_style.style_mask())
97    }
98
99    /// Returns the base glyph identifier for the given key
100    pub(super) fn get_base_glyph_id(&self, key: &str) -> Option<u16> {
101        if key.len() == 1 {
102            let ch = key.chars().next().unwrap();
103            if ch.is_ascii() {
104                // 0x00..0x7f double as layer
105                let id = ch as u16;
106                return Some(id);
107            }
108        }
109
110        self.glyph_coords.get(key).copied()
111    }
112}