Files
game_engine/crates/voltex_editor/src/glyph_cache.rs
2026-03-26 14:10:16 +09:00

198 lines
6.1 KiB
Rust

use std::collections::HashMap;
pub const ATLAS_SIZE: u32 = 1024;
#[derive(Clone, Debug)]
pub struct GlyphInfo {
pub uv: [f32; 4], // u0, v0, u1, v1
pub width: f32,
pub height: f32,
pub advance: f32,
pub bearing_x: f32,
pub bearing_y: f32,
}
pub struct GlyphCache {
pub atlas_data: Vec<u8>,
pub atlas_width: u32,
pub atlas_height: u32,
glyphs: HashMap<char, GlyphInfo>,
cursor_x: u32,
cursor_y: u32,
row_height: u32,
pub dirty: bool,
}
impl GlyphCache {
pub fn new(width: u32, height: u32) -> Self {
GlyphCache {
atlas_data: vec![0u8; (width * height) as usize],
atlas_width: width,
atlas_height: height,
glyphs: HashMap::new(),
cursor_x: 0,
cursor_y: 0,
row_height: 0,
dirty: false,
}
}
pub fn get(&self, ch: char) -> Option<&GlyphInfo> {
self.glyphs.get(&ch)
}
/// Insert a rasterized glyph bitmap into the atlas.
/// Returns reference to the cached GlyphInfo.
pub fn insert(
&mut self,
ch: char,
bitmap: &[u8],
bmp_w: u32,
bmp_h: u32,
advance: f32,
bearing_x: f32,
bearing_y: f32,
) -> &GlyphInfo {
// Handle zero-size glyphs (e.g., space)
if bmp_w == 0 || bmp_h == 0 {
self.glyphs.insert(ch, GlyphInfo {
uv: [0.0, 0.0, 0.0, 0.0],
width: 0.0,
height: 0.0,
advance,
bearing_x,
bearing_y,
});
return self.glyphs.get(&ch).unwrap();
}
// Check if we need to wrap to next row
if self.cursor_x + bmp_w > self.atlas_width {
self.cursor_y += self.row_height + 1; // +1 pixel gap
self.cursor_x = 0;
self.row_height = 0;
}
// Check if atlas is full (would overflow vertically)
if self.cursor_y + bmp_h > self.atlas_height {
// Atlas full — insert with zero UV (glyph won't render but won't crash)
self.glyphs.insert(ch, GlyphInfo {
uv: [0.0, 0.0, 0.0, 0.0],
width: bmp_w as f32,
height: bmp_h as f32,
advance,
bearing_x,
bearing_y,
});
return self.glyphs.get(&ch).unwrap();
}
// Copy bitmap into atlas
for row in 0..bmp_h {
let src_start = (row * bmp_w) as usize;
let src_end = src_start + bmp_w as usize;
let dst_y = self.cursor_y + row;
let dst_x = self.cursor_x;
let dst_start = (dst_y * self.atlas_width + dst_x) as usize;
if src_end <= bitmap.len() && dst_start + bmp_w as usize <= self.atlas_data.len() {
self.atlas_data[dst_start..dst_start + bmp_w as usize]
.copy_from_slice(&bitmap[src_start..src_end]);
}
}
// Calculate UV coordinates
let u0 = self.cursor_x as f32 / self.atlas_width as f32;
let v0 = self.cursor_y as f32 / self.atlas_height as f32;
let u1 = (self.cursor_x + bmp_w) as f32 / self.atlas_width as f32;
let v1 = (self.cursor_y + bmp_h) as f32 / self.atlas_height as f32;
let info = GlyphInfo {
uv: [u0, v0, u1, v1],
width: bmp_w as f32,
height: bmp_h as f32,
advance,
bearing_x,
bearing_y,
};
// Advance cursor
self.cursor_x += bmp_w + 1; // +1 pixel gap
if bmp_h > self.row_height {
self.row_height = bmp_h;
}
self.dirty = true;
self.glyphs.insert(ch, info);
self.glyphs.get(&ch).unwrap()
}
pub fn clear_dirty(&mut self) {
self.dirty = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_and_get() {
let mut cache = GlyphCache::new(256, 256);
let bitmap = vec![255u8; 10 * 12]; // 10x12 glyph
cache.insert('A', &bitmap, 10, 12, 11.0, 0.5, 10.0);
let info = cache.get('A');
assert!(info.is_some());
let info = info.unwrap();
assert!((info.width - 10.0).abs() < 0.1);
assert!((info.advance - 11.0).abs() < 0.1);
assert!(info.uv[0] >= 0.0 && info.uv[2] <= 1.0);
}
#[test]
fn test_cache_hit() {
let mut cache = GlyphCache::new(256, 256);
let bitmap = vec![255u8; 8 * 8];
cache.insert('B', &bitmap, 8, 8, 9.0, 0.0, 8.0);
cache.clear_dirty();
// Second access should be cache hit (no dirty)
let _info = cache.get('B').unwrap();
assert!(!cache.dirty);
}
#[test]
fn test_row_wrap() {
let mut cache = GlyphCache::new(64, 64);
let bitmap = vec![255u8; 20 * 10];
// Insert 4 glyphs of width 20 in a 64-wide atlas
// 3 fit in first row (20+1 + 20+1 + 20 = 62), 4th wraps
cache.insert('A', &bitmap, 20, 10, 21.0, 0.0, 10.0);
cache.insert('B', &bitmap, 20, 10, 21.0, 0.0, 10.0);
cache.insert('C', &bitmap, 20, 10, 21.0, 0.0, 10.0);
cache.insert('D', &bitmap, 20, 10, 21.0, 0.0, 10.0);
let d = cache.get('D').unwrap();
// D should be on a different row (v0 > 0)
assert!(d.uv[1] > 0.0, "D should be on second row, v0={}", d.uv[1]);
}
#[test]
fn test_uv_range() {
let mut cache = GlyphCache::new(256, 256);
let bitmap = vec![128u8; 15 * 20];
cache.insert('X', &bitmap, 15, 20, 16.0, 1.0, 18.0);
let info = cache.get('X').unwrap();
assert!(info.uv[0] >= 0.0 && info.uv[0] < 1.0);
assert!(info.uv[1] >= 0.0 && info.uv[1] < 1.0);
assert!(info.uv[2] > info.uv[0] && info.uv[2] <= 1.0);
assert!(info.uv[3] > info.uv[1] && info.uv[3] <= 1.0);
}
#[test]
fn test_zero_size_glyph() {
let mut cache = GlyphCache::new(256, 256);
cache.insert(' ', &[], 0, 0, 5.0, 0.0, 0.0);
let info = cache.get(' ').unwrap();
assert!((info.advance - 5.0).abs() < 0.1);
assert!((info.width - 0.0).abs() < 0.1);
}
}