use crate::font::{BitmapFont, FontChain};
pub const ATLAS_COLS: u32 = 16;
pub const ATLAS_ROWS: u32 = 16;
pub const SLOTS_PER_LAYER: u32 = ATLAS_COLS * ATLAS_ROWS;
pub const MAX_SLOTS: u32 = u16::MAX as u32 + 1;
#[must_use]
pub fn addressable_glyphs(font: &BitmapFont) -> u32 {
u32::from(font.glyph_count()).min(256)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct AtlasGeometry {
pub cell_w: u32,
pub cell_h: u32,
pub layers: u32,
}
impl AtlasGeometry {
#[must_use]
pub const fn new(cell_w: u32, cell_h: u32, capacity: u32) -> Self {
let layers = capacity.div_ceil(SLOTS_PER_LAYER);
Self {
cell_w,
cell_h,
layers: if layers == 0 { 1 } else { layers },
}
}
#[must_use]
pub const fn tex_w(&self) -> u32 {
self.cell_w * ATLAS_COLS
}
#[must_use]
pub const fn tex_h(&self) -> u32 {
self.cell_h * ATLAS_ROWS
}
#[must_use]
pub const fn locate(slot: u32) -> (u32, u32, u32) {
let layer = slot / SLOTS_PER_LAYER;
let within = slot % SLOTS_PER_LAYER;
(layer, within % ATLAS_COLS, within / ATLAS_COLS)
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct AtlasData {
pub geometry: AtlasGeometry,
pub coverage: Vec<u8>,
}
impl AtlasData {
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn build(fonts: &FontChain<'static>, cell_size: (u32, u32)) -> Self {
let (cell_w, cell_h) = cell_size;
let count: u32 = fonts.fonts().map(addressable_glyphs).sum();
let geometry = AtlasGeometry::new(cell_w, cell_h, count);
let tex_w = geometry.tex_w();
let tex_h = geometry.tex_h();
let mut coverage = vec![0u8; (tex_w * tex_h * geometry.layers) as usize];
let mut slot = 0;
for font in fonts.fonts() {
for index in 0..addressable_glyphs(font) {
let (layer, gcol, grow) = AtlasGeometry::locate(slot);
let (ox, oy) = (gcol * cell_w, grow * cell_h);
for (x, y) in font.glyph_pixels(index as u8) {
let px = ox + u32::from(x);
let py = oy + u32::from(y);
let idx = ((layer * tex_h + py) * tex_w + px) as usize;
coverage[idx] = 0xFF;
}
slot += 1;
}
}
Self { geometry, coverage }
}
}
#[derive(Clone, Debug)]
pub struct GlyphAtlas {
fonts: FontChain<'static>,
bases: Vec<u32>,
cell_w: u32,
cell_h: u32,
space_slot: u16,
}
impl GlyphAtlas {
#[must_use]
pub fn new(fonts: FontChain<'static>, glyph_size: (u8, u8)) -> Self {
let mut bases = Vec::with_capacity(fonts.font_count());
let mut next = 0;
for font in fonts.fonts() {
bases.push(next);
next += addressable_glyphs(font);
}
let mut atlas = Self {
fonts,
bases,
cell_w: u32::from(glyph_size.0),
cell_h: u32::from(glyph_size.1),
space_slot: 0,
};
atlas.space_slot = atlas.resolve(' ').unwrap_or(0);
atlas
}
#[must_use]
pub const fn cell_size(&self) -> (u32, u32) {
(self.cell_w, self.cell_h)
}
#[must_use]
pub const fn space_slot(&self) -> u16 {
self.space_slot
}
#[must_use]
pub fn slot_count(&self) -> u32 {
self.fonts.fonts().map(addressable_glyphs).sum()
}
#[must_use]
pub const fn fonts(&self) -> &FontChain<'static> {
&self.fonts
}
#[must_use]
pub fn data(&self) -> AtlasData {
AtlasData::build(&self.fonts, self.cell_size())
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn resolve(&self, ch: char) -> Option<u16> {
let glyph = self.fonts.resolve(ch)?;
Some((self.bases[glyph.font_index()] + u32::from(glyph.index())) as u16)
}
}
#[cfg(test)]
mod tests {
use super::{ATLAS_COLS, AtlasData, AtlasGeometry, GlyphAtlas, MAX_SLOTS, SLOTS_PER_LAYER};
use crate::font::{BitmapFont, FontChain};
#[test]
fn geometry_layers_cover_capacity() {
assert_eq!(AtlasGeometry::new(8, 16, 256).layers, 1);
assert_eq!(AtlasGeometry::new(8, 16, 257).layers, 2);
assert_eq!(AtlasGeometry::new(8, 16, 4096).layers, 16);
assert_eq!(AtlasGeometry::new(8, 16, 0).layers, 1);
}
#[test]
fn a_full_slot_space_stays_within_the_256_layer_floor() {
assert_eq!(AtlasGeometry::new(8, 16, MAX_SLOTS).layers, 256);
}
#[test]
fn locate_walks_row_major_then_layer() {
assert_eq!(AtlasGeometry::locate(0), (0, 0, 0));
assert_eq!(AtlasGeometry::locate(1), (0, 1, 0));
assert_eq!(AtlasGeometry::locate(ATLAS_COLS), (0, 0, 1));
assert_eq!(AtlasGeometry::locate(SLOTS_PER_LAYER), (1, 0, 0));
assert_eq!(AtlasGeometry::locate(SLOTS_PER_LAYER + 1), (1, 1, 0));
}
#[test]
fn tex_dims_are_grid_times_cell() {
let g = AtlasGeometry::new(8, 16, 256);
assert_eq!(g.tex_w(), 8 * 16);
assert_eq!(g.tex_h(), 16 * 16);
assert_eq!(g.layers, 1);
}
#[test]
fn a_chain_packs_each_font_back_to_back() {
static PRIMARY_DATA: [u8; 256 * 2] = [0; 256 * 2];
const PRIMARY: BitmapFont = BitmapFont::new(&PRIMARY_DATA, 8, 2, 256);
static FALLBACK_DATA: [u8; 2] = [0xFF, 0x00];
const CHARSET: [(char, u8); 1] = [('▘', 0)];
static FALLBACKS: [BitmapFont; 1] =
[BitmapFont::with_charset(&FALLBACK_DATA, 8, 2, 1, &CHARSET)];
let atlas = AtlasData::build(&FontChain::new(PRIMARY, &FALLBACKS), (8, 2));
assert_eq!(atlas.geometry.layers, 2);
let (layer, gcol, grow) = AtlasGeometry::locate(256);
assert_eq!((layer, gcol, grow), (1, 0, 0));
let tex_w = atlas.geometry.tex_w();
let tex_h = atlas.geometry.tex_h();
let row0 = ((layer * tex_h) * tex_w) as usize;
assert!(
atlas.coverage[row0..row0 + 8].iter().all(|&c| c == 0xFF),
"the fallback glyph's top row is covered at its own slot"
);
assert!(
atlas.coverage[..row0].iter().all(|&c| c == 0),
"the primary font's blank glyphs are untouched"
);
}
#[test]
fn fallback_font_glyphs_get_slots_after_the_primary_font() {
static PRIMARY_DATA: [u8; 256 * 16] = [0; 256 * 16];
const PRIMARY: BitmapFont = BitmapFont::new(&PRIMARY_DATA, 8, 16, 256);
static QUADRANT_DATA: [u8; 2 * 16] = [0; 2 * 16];
const CHARSET: [(char, u8); 2] = [('▘', 0), ('▝', 1)];
static FALLBACKS: [BitmapFont; 1] =
[BitmapFont::with_charset(&QUADRANT_DATA, 8, 16, 2, &CHARSET)];
let atlas = GlyphAtlas::new(FontChain::new(PRIMARY, &FALLBACKS), (8, 16));
assert_eq!(atlas.slot_count(), 258);
assert_eq!(atlas.resolve('A'), Some(u16::from(b'A')));
assert_eq!(atlas.resolve('▘'), Some(256));
assert_eq!(atlas.resolve('▝'), Some(257));
assert_eq!(atlas.resolve('あ'), Some(0xDB));
}
#[test]
fn coverage_is_strictly_binary() {
static DATA: [u8; 4] = [0b1010_1010, 0x00, 0xFF, 0b0000_1111];
const FONT: BitmapFont = BitmapFont::new(&DATA, 8, 4, 1);
let atlas = AtlasData::build(&FontChain::from(FONT), (8, 4));
assert!(
atlas.coverage.contains(&0xFF),
"the fixture should cover some texels, or this proves nothing"
);
for (index, &byte) in atlas.coverage.iter().enumerate() {
assert!(
byte == 0x00 || byte == 0xFF,
"texel {index} has partial coverage ({byte:#04x}); see this module's docs on why \
that reopens the colour-space question for every backend"
);
}
}
#[cfg(feature = "default-font")]
#[test]
fn coverage_is_strictly_binary_for_the_bundled_font() {
use crate::font::unscii16;
let atlas = AtlasData::build(&FontChain::from(unscii16::FONT), (8, 16));
assert!(
atlas.coverage.iter().all(|&c| c == 0x00 || c == 0xFF),
"the bundled font produced partial coverage"
);
assert!(atlas.coverage.contains(&0xFF));
}
#[cfg(feature = "default-font")]
#[test]
fn unscii16_packs_into_one_layer() {
use crate::font::unscii16;
let atlas = AtlasData::build(&FontChain::from(unscii16::FONT), (8, 16));
assert_eq!(atlas.geometry.cell_w, 8);
assert_eq!(atlas.geometry.cell_h, 16);
assert_eq!(atlas.geometry.layers, 1);
assert_eq!(
atlas.coverage.len(),
(atlas.geometry.tex_w() * atlas.geometry.tex_h()) as usize
);
}
#[cfg(feature = "default-font")]
#[test]
fn space_is_blank_and_full_block_is_solid_in_their_cells() {
use crate::font::unscii16;
let atlas = AtlasData::build(&FontChain::from(unscii16::FONT), (8, 16));
let g = atlas.geometry;
let tex_w = g.tex_w();
let cell_covered = |slot: u32| -> (bool, bool) {
let (_, col, row) = AtlasGeometry::locate(slot);
let (ox, oy) = (col * g.cell_w, row * g.cell_h);
let mut any = false;
let mut all = true;
for y in 0..g.cell_h {
for x in 0..g.cell_w {
let idx = (((oy + y) * tex_w) + ox + x) as usize;
let set = atlas.coverage[idx] != 0;
any |= set;
all &= set;
}
}
(any, all)
};
assert!(!cell_covered(0x20).0, "space must be blank");
assert!(cell_covered(0xDB).1, "full block must be solid");
}
#[cfg(feature = "default-font")]
#[test]
fn atlas_maps_char_to_font_index() {
use crate::font::unscii16;
let atlas = GlyphAtlas::new(FontChain::from(unscii16::FONT), (8, 16));
assert_eq!(
atlas.resolve('A'),
unscii16::FONT.glyph_index('A').map(u16::from)
);
assert_eq!(
atlas.space_slot(),
unscii16::FONT.glyph_index(' ').map(u16::from).unwrap()
);
assert_eq!(atlas.cell_size(), (8, 16));
}
}