use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct MsdfGlyph {
pub uv_x: f32,
pub uv_y: f32,
pub uv_w: f32,
pub uv_h: f32,
pub advance: f32,
pub width: f32,
pub height: f32,
pub offset_x: f32,
pub offset_y: f32,
}
#[derive(Debug, Clone)]
pub struct MsdfFont {
pub texture_id: u32,
pub atlas_width: u32,
pub atlas_height: u32,
pub font_size: f32,
pub line_height: f32,
pub distance_range: f32,
pub glyphs: HashMap<u32, MsdfGlyph>,
}
impl MsdfFont {
pub fn get_glyph(&self, ch: char) -> Option<&MsdfGlyph> {
self.glyphs.get(&(ch as u32))
}
pub fn measure_width(&self, text: &str, font_size: f32) -> f32 {
let scale = font_size / self.font_size;
let mut width = 0.0f32;
for ch in text.chars() {
if let Some(glyph) = self.get_glyph(ch) {
width += glyph.advance * scale;
}
}
width
}
}
#[derive(Clone)]
pub struct MsdfFontStore {
fonts: HashMap<u32, MsdfFont>,
next_id: u32,
}
impl MsdfFontStore {
pub fn new() -> Self {
Self {
fonts: HashMap::new(),
next_id: 1,
}
}
pub fn register(&mut self, font: MsdfFont) -> u32 {
let id = self.next_id;
self.next_id += 1;
self.fonts.insert(id, font);
id
}
pub fn register_with_id(&mut self, id: u32, font: MsdfFont) {
self.fonts.insert(id, font);
if id >= self.next_id {
self.next_id = id + 1;
}
}
pub fn get(&self, id: u32) -> Option<&MsdfFont> {
self.fonts.get(&id)
}
}
pub const MSDF_FRAGMENT_SOURCE: &str = include_str!("shaders/msdf.wgsl");
const SDF_PAD: u32 = 4;
const SRC_GLYPH_W: u32 = 8;
const SRC_GLYPH_H: u32 = 8;
const DST_GLYPH_W: u32 = SRC_GLYPH_W + 2 * SDF_PAD;
const DST_GLYPH_H: u32 = SRC_GLYPH_H + 2 * SDF_PAD;
const ATLAS_COLS: u32 = 16;
const ATLAS_ROWS: u32 = 6;
const DIST_RANGE: f32 = 4.0;
pub fn generate_builtin_msdf_font() -> (Vec<u8>, u32, u32, MsdfFont) {
let atlas_w = ATLAS_COLS * DST_GLYPH_W;
let atlas_h = ATLAS_ROWS * DST_GLYPH_H;
let font_data = super::font::generate_builtin_font();
let (bmp_pixels, bmp_w, _bmp_h) = font_data;
let mut atlas_pixels = vec![0u8; (atlas_w * atlas_h * 4) as usize];
let mut glyphs = HashMap::new();
for glyph_idx in 0..96u32 {
let src_col = glyph_idx % 16;
let src_row = glyph_idx / 16;
let src_base_x = src_col * SRC_GLYPH_W;
let src_base_y = src_row * SRC_GLYPH_H;
let dst_col = glyph_idx % ATLAS_COLS;
let dst_row = glyph_idx / ATLAS_COLS;
let dst_base_x = dst_col * DST_GLYPH_W;
let dst_base_y = dst_row * DST_GLYPH_H;
let mut src_bits = [[false; SRC_GLYPH_W as usize]; SRC_GLYPH_H as usize];
for py in 0..SRC_GLYPH_H {
for px in 0..SRC_GLYPH_W {
let bmp_offset =
(((src_base_y + py) * bmp_w + (src_base_x + px)) * 4 + 3) as usize;
src_bits[py as usize][px as usize] = bmp_pixels[bmp_offset] > 0;
}
}
for dy in 0..DST_GLYPH_H {
for dx in 0..DST_GLYPH_W {
let sx = dx as f32 - SDF_PAD as f32 + 0.5;
let sy = dy as f32 - SDF_PAD as f32 + 0.5;
let dist = compute_signed_distance(&src_bits, sx, sy);
let normalized = 0.5 + dist / (2.0 * DIST_RANGE);
let clamped = normalized.clamp(0.0, 1.0);
let byte_val = (clamped * 255.0) as u8;
let out_x = dst_base_x + dx;
let out_y = dst_base_y + dy;
let offset = ((out_y * atlas_w + out_x) * 4) as usize;
atlas_pixels[offset] = byte_val; atlas_pixels[offset + 1] = byte_val; atlas_pixels[offset + 2] = byte_val; atlas_pixels[offset + 3] = 255; }
}
let char_code = glyph_idx + 32; glyphs.insert(
char_code,
MsdfGlyph {
uv_x: dst_base_x as f32 / atlas_w as f32,
uv_y: dst_base_y as f32 / atlas_h as f32,
uv_w: DST_GLYPH_W as f32 / atlas_w as f32,
uv_h: DST_GLYPH_H as f32 / atlas_h as f32,
advance: SRC_GLYPH_W as f32,
width: SRC_GLYPH_W as f32,
height: SRC_GLYPH_H as f32,
offset_x: 0.0,
offset_y: 0.0,
},
);
}
let font = MsdfFont {
texture_id: 0, atlas_width: atlas_w,
atlas_height: atlas_h,
font_size: SRC_GLYPH_H as f32,
line_height: SRC_GLYPH_H as f32,
distance_range: DIST_RANGE,
glyphs,
};
(atlas_pixels, atlas_w, atlas_h, font)
}
fn compute_signed_distance(bits: &[[bool; 8]; 8], sx: f32, sy: f32) -> f32 {
let w = SRC_GLYPH_W as i32;
let h = SRC_GLYPH_H as i32;
let ix = sx.floor() as i32;
let iy = sy.floor() as i32;
let inside = if ix >= 0 && ix < w && iy >= 0 && iy < h {
bits[iy as usize][ix as usize]
} else {
false
};
let mut min_dist_sq = f32::MAX;
for py in -1..=h {
for px in -1..=w {
let is_filled = if px >= 0 && px < w && py >= 0 && py < h {
bits[py as usize][px as usize]
} else {
false
};
let right_filled = if (px + 1) >= 0 && (px + 1) < w && py >= 0 && py < h {
bits[py as usize][(px + 1) as usize]
} else {
false
};
if is_filled != right_filled {
let edge_x = (px + 1) as f32;
let edge_y_min = py as f32;
let edge_y_max = (py + 1) as f32;
let dist_sq = point_to_segment_dist_sq(
sx, sy, edge_x, edge_y_min, edge_x, edge_y_max,
);
if dist_sq < min_dist_sq {
min_dist_sq = dist_sq;
}
}
let bottom_filled = if px >= 0 && px < w && (py + 1) >= 0 && (py + 1) < h {
bits[(py + 1) as usize][px as usize]
} else {
false
};
if is_filled != bottom_filled {
let edge_y = (py + 1) as f32;
let edge_x_min = px as f32;
let edge_x_max = (px + 1) as f32;
let dist_sq = point_to_segment_dist_sq(
sx, sy, edge_x_min, edge_y, edge_x_max, edge_y,
);
if dist_sq < min_dist_sq {
min_dist_sq = dist_sq;
}
}
}
}
let dist = min_dist_sq.sqrt();
if inside { dist } else { -dist }
}
fn point_to_segment_dist_sq(px: f32, py: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
let dx = x2 - x1;
let dy = y2 - y1;
let len_sq = dx * dx + dy * dy;
if len_sq < 1e-10 {
let ex = px - x1;
let ey = py - y1;
return ex * ex + ey * ey;
}
let t = ((px - x1) * dx + (py - y1) * dy) / len_sq;
let t = t.clamp(0.0, 1.0);
let closest_x = x1 + t * dx;
let closest_y = y1 + t * dy;
let ex = px - closest_x;
let ey = py - closest_y;
ex * ex + ey * ey
}
pub fn parse_msdf_metrics(json: &str, texture_id: u32) -> Result<MsdfFont, String> {
if json.contains("\"chars\"") {
parse_msdf_bmfont_format(json, texture_id)
} else {
parse_msdf_atlas_gen_format(json, texture_id)
}
}
fn parse_msdf_bmfont_format(json: &str, texture_id: u32) -> Result<MsdfFont, String> {
let atlas_width = extract_nested_number(json, "\"common\"", "\"scaleW\"")
.ok_or("Missing common.scaleW")? as u32;
let atlas_height = extract_nested_number(json, "\"common\"", "\"scaleH\"")
.ok_or("Missing common.scaleH")? as u32;
let font_size = extract_nested_number(json, "\"info\"", "\"size\"")
.unwrap_or(32.0);
let distance_range = extract_nested_number(json, "\"distanceField\"", "\"distanceRange\"")
.unwrap_or(4.0);
let line_height = extract_nested_number(json, "\"common\"", "\"lineHeight\"")
.unwrap_or(font_size * 1.2);
let mut glyphs = HashMap::new();
if let Some(chars_start) = json.find("\"chars\"") {
let rest = &json[chars_start..];
if let Some(arr_start) = rest.find('[') {
let arr_rest = &rest[arr_start + 1..];
let mut depth = 0i32;
let mut obj_start = None;
for (i, ch) in arr_rest.char_indices() {
match ch {
'{' => {
if depth == 0 {
obj_start = Some(i);
}
depth += 1;
}
'}' => {
depth -= 1;
if depth == 0 {
if let Some(start) = obj_start {
let obj = &arr_rest[start..=i];
if let Some(glyph) = parse_bmfont_char(
obj,
atlas_width as f32,
atlas_height as f32,
) {
glyphs.insert(glyph.0, glyph.1);
}
}
}
}
']' if depth == 0 => break,
_ => {}
}
}
}
}
Ok(MsdfFont {
texture_id,
atlas_width,
atlas_height,
font_size,
line_height,
distance_range,
glyphs,
})
}
fn parse_bmfont_char(obj: &str, atlas_w: f32, atlas_h: f32) -> Option<(u32, MsdfGlyph)> {
let id = extract_number(obj, "\"id\"")? as u32;
let x = extract_number(obj, "\"x\"").unwrap_or(0.0);
let y = extract_number(obj, "\"y\"").unwrap_or(0.0);
let width = extract_number(obj, "\"width\"").unwrap_or(0.0);
let height = extract_number(obj, "\"height\"").unwrap_or(0.0);
let xoffset = extract_number(obj, "\"xoffset\"").unwrap_or(0.0);
let yoffset = extract_number(obj, "\"yoffset\"").unwrap_or(0.0);
let xadvance = extract_number(obj, "\"xadvance\"").unwrap_or(width);
Some((
id,
MsdfGlyph {
uv_x: x / atlas_w,
uv_y: y / atlas_h,
uv_w: width / atlas_w,
uv_h: height / atlas_h,
advance: xadvance,
width,
height,
offset_x: xoffset,
offset_y: yoffset,
},
))
}
fn parse_msdf_atlas_gen_format(json: &str, texture_id: u32) -> Result<MsdfFont, String> {
let atlas_width = extract_number(json, "\"width\"")
.ok_or("Missing atlas width")? as u32;
let atlas_height = extract_number(json, "\"height\"")
.ok_or("Missing atlas height")? as u32;
let distance_range = extract_number(json, "\"distanceRange\"")
.unwrap_or(4.0);
let font_size = extract_number(json, "\"size\"")
.unwrap_or(32.0);
let line_height_factor = extract_number(json, "\"lineHeight\"")
.unwrap_or(1.2);
let line_height = font_size * line_height_factor as f32;
let mut glyphs = HashMap::new();
if let Some(glyphs_start) = json.find("\"glyphs\"") {
let rest = &json[glyphs_start..];
if let Some(arr_start) = rest.find('[') {
let arr_rest = &rest[arr_start + 1..];
let mut depth = 0i32;
let mut obj_start = None;
for (i, ch) in arr_rest.char_indices() {
match ch {
'{' => {
if depth == 0 {
obj_start = Some(i);
}
depth += 1;
}
'}' => {
depth -= 1;
if depth == 0 {
if let Some(start) = obj_start {
let obj = &arr_rest[start..=i];
if let Some(glyph) = parse_glyph_object(
obj,
atlas_width as f32,
atlas_height as f32,
font_size,
) {
glyphs.insert(glyph.0, glyph.1);
}
}
}
}
']' if depth == 0 => break,
_ => {}
}
}
}
}
Ok(MsdfFont {
texture_id,
atlas_width,
atlas_height,
font_size,
line_height,
distance_range,
glyphs,
})
}
fn parse_glyph_object(
obj: &str,
atlas_w: f32,
atlas_h: f32,
font_size: f32,
) -> Option<(u32, MsdfGlyph)> {
let unicode = extract_number(obj, "\"unicode\"")? as u32;
let advance = extract_number(obj, "\"advance\"").unwrap_or(0.0);
let ab_left = extract_nested_number(obj, "\"atlasBounds\"", "\"left\"").unwrap_or(0.0);
let ab_bottom = extract_nested_number(obj, "\"atlasBounds\"", "\"bottom\"").unwrap_or(0.0);
let ab_right = extract_nested_number(obj, "\"atlasBounds\"", "\"right\"").unwrap_or(0.0);
let ab_top = extract_nested_number(obj, "\"atlasBounds\"", "\"top\"").unwrap_or(0.0);
let pb_left = extract_nested_number(obj, "\"planeBounds\"", "\"left\"").unwrap_or(0.0);
let pb_bottom = extract_nested_number(obj, "\"planeBounds\"", "\"bottom\"").unwrap_or(0.0);
let pb_right = extract_nested_number(obj, "\"planeBounds\"", "\"right\"").unwrap_or(0.0);
let pb_top = extract_nested_number(obj, "\"planeBounds\"", "\"top\"").unwrap_or(0.0);
let uv_x = ab_left / atlas_w;
let uv_y = ab_top / atlas_h;
let uv_w = (ab_right - ab_left) / atlas_w;
let uv_h = (ab_bottom - ab_top) / atlas_h;
let glyph_w = (pb_right - pb_left) * font_size;
let glyph_h = (pb_top - pb_bottom) * font_size;
Some((
unicode,
MsdfGlyph {
uv_x,
uv_y,
uv_w,
uv_h,
advance: advance * font_size,
width: glyph_w,
height: glyph_h,
offset_x: pb_left * font_size,
offset_y: pb_top * font_size,
},
))
}
fn extract_number(json: &str, key: &str) -> Option<f32> {
let key_pos = json.find(key)?;
let after_key = &json[key_pos + key.len()..];
let value_start = after_key.find(|c: char| c.is_ascii_digit() || c == '-' || c == '.')?;
let value_str = &after_key[value_start..];
let value_end = value_str
.find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != 'e' && c != 'E' && c != '+')
.unwrap_or(value_str.len());
value_str[..value_end].parse::<f32>().ok()
}
fn extract_nested_number(json: &str, outer_key: &str, inner_key: &str) -> Option<f32> {
let outer_pos = json.find(outer_key)?;
let rest = &json[outer_pos..];
let brace_pos = rest.find('{')?;
let end_pos = rest[brace_pos..].find('}')? + brace_pos;
let inner = &rest[brace_pos..=end_pos];
extract_number(inner, inner_key)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_msdf_font_generates() {
let (pixels, w, h, font) = generate_builtin_msdf_font();
assert_eq!(w, ATLAS_COLS * DST_GLYPH_W);
assert_eq!(h, ATLAS_ROWS * DST_GLYPH_H);
assert_eq!(pixels.len(), (w * h * 4) as usize);
assert_eq!(font.glyphs.len(), 96);
assert!(font.distance_range > 0.0);
}
#[test]
fn builtin_msdf_has_expected_glyphs() {
let (_, _, _, font) = generate_builtin_msdf_font();
assert!(font.get_glyph(' ').is_some());
assert!(font.get_glyph('A').is_some());
assert!(font.get_glyph('z').is_some());
assert!(font.get_glyph('\x01').is_none());
}
#[test]
fn builtin_msdf_glyph_uvs_valid() {
let (_, _, _, font) = generate_builtin_msdf_font();
for glyph in font.glyphs.values() {
assert!(glyph.uv_x >= 0.0 && glyph.uv_x <= 1.0, "uv_x out of range");
assert!(glyph.uv_y >= 0.0 && glyph.uv_y <= 1.0, "uv_y out of range");
assert!(glyph.uv_w > 0.0 && glyph.uv_w <= 1.0, "uv_w out of range");
assert!(glyph.uv_h > 0.0 && glyph.uv_h <= 1.0, "uv_h out of range");
}
}
#[test]
fn builtin_msdf_distance_field_correctness() {
let (pixels, w, _h, _font) = generate_builtin_msdf_font();
let glyph_x = 1 * DST_GLYPH_W;
let glyph_y = 2 * DST_GLYPH_H;
let mut has_outside = false; let mut has_edge = false; let mut has_inside = false;
for py in 0..DST_GLYPH_H {
for px in 0..DST_GLYPH_W {
let offset = (((glyph_y + py) * w + (glyph_x + px)) * 4) as usize;
let val = pixels[offset]; if val < 110 { has_outside = true; }
if val > 110 && val < 170 { has_edge = true; }
if val > 140 { has_inside = true; }
}
}
assert!(has_outside, "'A' glyph should have outside distance values");
assert!(has_edge, "'A' glyph should have edge distance values");
assert!(has_inside, "'A' glyph should have inside distance values");
}
#[test]
fn space_glyph_is_outside() {
let (pixels, w, _h, _font) = generate_builtin_msdf_font();
let glyph_x = 0;
let glyph_y = 0;
let cx = glyph_x + DST_GLYPH_W / 2;
let cy = glyph_y + DST_GLYPH_H / 2;
let offset = ((cy * w + cx) * 4) as usize;
let val = pixels[offset];
assert!(val < 128, "Space center should be outside (val={val}, expected < 128)");
}
#[test]
fn measure_width_works() {
let (_, _, _, font) = generate_builtin_msdf_font();
let width = font.measure_width("Hello", 8.0);
assert!((width - 40.0).abs() < 0.01, "Expected ~40, got {width}");
}
#[test]
fn measure_width_with_scale() {
let (_, _, _, font) = generate_builtin_msdf_font();
let width = font.measure_width("AB", 16.0);
assert!((width - 32.0).abs() < 0.01, "Expected ~32, got {width}");
}
#[test]
fn parse_metrics_basic() {
let json = r#"{
"atlas": { "width": 256, "height": 256, "distanceRange": 4, "size": 32 },
"metrics": { "lineHeight": 1.2 },
"glyphs": [
{
"unicode": 65,
"advance": 0.6,
"atlasBounds": { "left": 0, "bottom": 32, "right": 24, "top": 0 },
"planeBounds": { "left": 0, "bottom": -0.1, "right": 0.6, "top": 0.9 }
}
]
}"#;
let font = parse_msdf_metrics(json, 42).unwrap();
assert_eq!(font.texture_id, 42);
assert_eq!(font.atlas_width, 256);
assert_eq!(font.atlas_height, 256);
assert!((font.distance_range - 4.0).abs() < 0.01);
assert_eq!(font.glyphs.len(), 1);
let glyph = font.get_glyph('A').unwrap();
assert!((glyph.advance - 19.2).abs() < 0.1); }
#[test]
fn font_store_register_and_get() {
let mut store = MsdfFontStore::new();
let font = MsdfFont {
texture_id: 1,
atlas_width: 128,
atlas_height: 128,
font_size: 16.0,
line_height: 20.0,
distance_range: 4.0,
glyphs: HashMap::new(),
};
let id = store.register(font);
assert!(store.get(id).is_some());
assert!(store.get(id + 1).is_none());
}
#[test]
fn point_to_segment_distance() {
let d = point_to_segment_dist_sq(0.0, 0.5, 1.0, 0.0, 1.0, 1.0);
assert!((d - 1.0).abs() < 0.001);
let d = point_to_segment_dist_sq(0.5, 0.0, 0.0, 0.0, 1.0, 0.0);
assert!(d < 0.001);
}
}