1use std::collections::HashMap;
14
15#[derive(Debug, Clone)]
17pub struct MsdfGlyph {
18 pub uv_x: f32,
20 pub uv_y: f32,
21 pub uv_w: f32,
22 pub uv_h: f32,
23 pub advance: f32,
25 pub width: f32,
27 pub height: f32,
29 pub offset_x: f32,
31 pub offset_y: f32,
33}
34
35#[derive(Debug, Clone)]
37pub struct MsdfFont {
38 pub texture_id: u32,
40 pub atlas_width: u32,
42 pub atlas_height: u32,
44 pub font_size: f32,
46 pub line_height: f32,
48 pub distance_range: f32,
50 pub glyphs: HashMap<u32, MsdfGlyph>,
52}
53
54impl MsdfFont {
55 pub fn get_glyph(&self, ch: char) -> Option<&MsdfGlyph> {
57 self.glyphs.get(&(ch as u32))
58 }
59
60 pub fn measure_width(&self, text: &str, font_size: f32) -> f32 {
62 let scale = font_size / self.font_size;
63 let mut width = 0.0f32;
64 for ch in text.chars() {
65 if let Some(glyph) = self.get_glyph(ch) {
66 width += glyph.advance * scale;
67 }
68 }
69 width
70 }
71}
72
73#[derive(Clone)]
75pub struct MsdfFontStore {
76 fonts: HashMap<u32, MsdfFont>,
77 next_id: u32,
78}
79
80impl MsdfFontStore {
81 pub fn new() -> Self {
82 Self {
83 fonts: HashMap::new(),
84 next_id: 1,
85 }
86 }
87
88 pub fn register(&mut self, font: MsdfFont) -> u32 {
90 let id = self.next_id;
91 self.next_id += 1;
92 self.fonts.insert(id, font);
93 id
94 }
95
96 pub fn register_with_id(&mut self, id: u32, font: MsdfFont) {
98 self.fonts.insert(id, font);
99 if id >= self.next_id {
100 self.next_id = id + 1;
101 }
102 }
103
104 pub fn get(&self, id: u32) -> Option<&MsdfFont> {
106 self.fonts.get(&id)
107 }
108}
109
110pub const MSDF_FRAGMENT_SOURCE: &str = include_str!("shaders/msdf.wgsl");
114
115const SDF_PAD: u32 = 4;
121const SRC_GLYPH_W: u32 = 8;
123const SRC_GLYPH_H: u32 = 8;
124const DST_GLYPH_W: u32 = SRC_GLYPH_W + 2 * SDF_PAD;
126const DST_GLYPH_H: u32 = SRC_GLYPH_H + 2 * SDF_PAD;
127const ATLAS_COLS: u32 = 16;
129const ATLAS_ROWS: u32 = 6;
130const DIST_RANGE: f32 = 4.0;
132
133pub fn generate_builtin_msdf_font() -> (Vec<u8>, u32, u32, MsdfFont) {
140 let atlas_w = ATLAS_COLS * DST_GLYPH_W;
141 let atlas_h = ATLAS_ROWS * DST_GLYPH_H;
142
143 let font_data = super::font::generate_builtin_font();
145 let (bmp_pixels, bmp_w, _bmp_h) = font_data;
146
147 let mut atlas_pixels = vec![0u8; (atlas_w * atlas_h * 4) as usize];
148 let mut glyphs = HashMap::new();
149
150 for glyph_idx in 0..96u32 {
151 let src_col = glyph_idx % 16;
152 let src_row = glyph_idx / 16;
153 let src_base_x = src_col * SRC_GLYPH_W;
154 let src_base_y = src_row * SRC_GLYPH_H;
155
156 let dst_col = glyph_idx % ATLAS_COLS;
157 let dst_row = glyph_idx / ATLAS_COLS;
158 let dst_base_x = dst_col * DST_GLYPH_W;
159 let dst_base_y = dst_row * DST_GLYPH_H;
160
161 let mut src_bits = [[false; SRC_GLYPH_W as usize]; SRC_GLYPH_H as usize];
163 for py in 0..SRC_GLYPH_H {
164 for px in 0..SRC_GLYPH_W {
165 let bmp_offset =
166 (((src_base_y + py) * bmp_w + (src_base_x + px)) * 4 + 3) as usize;
167 src_bits[py as usize][px as usize] = bmp_pixels[bmp_offset] > 0;
168 }
169 }
170
171 for dy in 0..DST_GLYPH_H {
173 for dx in 0..DST_GLYPH_W {
174 let sx = dx as f32 - SDF_PAD as f32 + 0.5;
176 let sy = dy as f32 - SDF_PAD as f32 + 0.5;
177
178 let dist = compute_signed_distance(&src_bits, sx, sy);
180
181 let normalized = 0.5 + dist / (2.0 * DIST_RANGE);
183 let clamped = normalized.clamp(0.0, 1.0);
184 let byte_val = (clamped * 255.0) as u8;
185
186 let out_x = dst_base_x + dx;
187 let out_y = dst_base_y + dy;
188 let offset = ((out_y * atlas_w + out_x) * 4) as usize;
189
190 atlas_pixels[offset] = byte_val; atlas_pixels[offset + 1] = byte_val; atlas_pixels[offset + 2] = byte_val; atlas_pixels[offset + 3] = 255; }
199 }
200
201 let char_code = glyph_idx + 32; glyphs.insert(
204 char_code,
205 MsdfGlyph {
206 uv_x: dst_base_x as f32 / atlas_w as f32,
207 uv_y: dst_base_y as f32 / atlas_h as f32,
208 uv_w: DST_GLYPH_W as f32 / atlas_w as f32,
209 uv_h: DST_GLYPH_H as f32 / atlas_h as f32,
210 advance: SRC_GLYPH_W as f32,
211 width: SRC_GLYPH_W as f32,
212 height: SRC_GLYPH_H as f32,
213 offset_x: 0.0,
214 offset_y: 0.0,
215 },
216 );
217 }
218
219 let font = MsdfFont {
220 texture_id: 0, atlas_width: atlas_w,
222 atlas_height: atlas_h,
223 font_size: SRC_GLYPH_H as f32,
224 line_height: SRC_GLYPH_H as f32,
225 distance_range: DIST_RANGE,
226 glyphs,
227 };
228
229 (atlas_pixels, atlas_w, atlas_h, font)
230}
231
232fn compute_signed_distance(bits: &[[bool; 8]; 8], sx: f32, sy: f32) -> f32 {
235 let w = SRC_GLYPH_W as i32;
236 let h = SRC_GLYPH_H as i32;
237
238 let ix = sx.floor() as i32;
240 let iy = sy.floor() as i32;
241 let inside = if ix >= 0 && ix < w && iy >= 0 && iy < h {
242 bits[iy as usize][ix as usize]
243 } else {
244 false
245 };
246
247 let mut min_dist_sq = f32::MAX;
249
250 for py in -1..=h {
253 for px in -1..=w {
254 let is_filled = if px >= 0 && px < w && py >= 0 && py < h {
255 bits[py as usize][px as usize]
256 } else {
257 false
258 };
259
260 let right_filled = if (px + 1) >= 0 && (px + 1) < w && py >= 0 && py < h {
262 bits[py as usize][(px + 1) as usize]
263 } else {
264 false
265 };
266
267 if is_filled != right_filled {
268 let edge_x = (px + 1) as f32;
270 let edge_y_min = py as f32;
271 let edge_y_max = (py + 1) as f32;
272 let dist_sq = point_to_segment_dist_sq(
273 sx, sy, edge_x, edge_y_min, edge_x, edge_y_max,
274 );
275 if dist_sq < min_dist_sq {
276 min_dist_sq = dist_sq;
277 }
278 }
279
280 let bottom_filled = if px >= 0 && px < w && (py + 1) >= 0 && (py + 1) < h {
282 bits[(py + 1) as usize][px as usize]
283 } else {
284 false
285 };
286
287 if is_filled != bottom_filled {
288 let edge_y = (py + 1) as f32;
290 let edge_x_min = px as f32;
291 let edge_x_max = (px + 1) as f32;
292 let dist_sq = point_to_segment_dist_sq(
293 sx, sy, edge_x_min, edge_y, edge_x_max, edge_y,
294 );
295 if dist_sq < min_dist_sq {
296 min_dist_sq = dist_sq;
297 }
298 }
299 }
300 }
301
302 let dist = min_dist_sq.sqrt();
303 if inside { dist } else { -dist }
304}
305
306fn point_to_segment_dist_sq(px: f32, py: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
308 let dx = x2 - x1;
309 let dy = y2 - y1;
310 let len_sq = dx * dx + dy * dy;
311
312 if len_sq < 1e-10 {
313 let ex = px - x1;
315 let ey = py - y1;
316 return ex * ex + ey * ey;
317 }
318
319 let t = ((px - x1) * dx + (py - y1) * dy) / len_sq;
320 let t = t.clamp(0.0, 1.0);
321
322 let closest_x = x1 + t * dx;
323 let closest_y = y1 + t * dy;
324
325 let ex = px - closest_x;
326 let ey = py - closest_y;
327 ex * ex + ey * ey
328}
329
330pub fn parse_msdf_metrics(json: &str, texture_id: u32) -> Result<MsdfFont, String> {
348 let atlas_width = extract_number(json, "\"width\"")
352 .ok_or("Missing atlas width")? as u32;
353 let atlas_height = extract_number(json, "\"height\"")
354 .ok_or("Missing atlas height")? as u32;
355 let distance_range = extract_number(json, "\"distanceRange\"")
356 .unwrap_or(4.0);
357 let font_size = extract_number(json, "\"size\"")
358 .unwrap_or(32.0);
359 let line_height_factor = extract_number(json, "\"lineHeight\"")
360 .unwrap_or(1.2);
361
362 let line_height = font_size * line_height_factor as f32;
363
364 let mut glyphs = HashMap::new();
365
366 if let Some(glyphs_start) = json.find("\"glyphs\"") {
368 let rest = &json[glyphs_start..];
369 if let Some(arr_start) = rest.find('[') {
370 let arr_rest = &rest[arr_start + 1..];
371 let mut depth = 0i32;
373 let mut obj_start = None;
374
375 for (i, ch) in arr_rest.char_indices() {
376 match ch {
377 '{' => {
378 if depth == 0 {
379 obj_start = Some(i);
380 }
381 depth += 1;
382 }
383 '}' => {
384 depth -= 1;
385 if depth == 0 {
386 if let Some(start) = obj_start {
387 let obj = &arr_rest[start..=i];
388 if let Some(glyph) = parse_glyph_object(
389 obj,
390 atlas_width as f32,
391 atlas_height as f32,
392 font_size,
393 ) {
394 glyphs.insert(glyph.0, glyph.1);
395 }
396 }
397 }
398 }
399 ']' if depth == 0 => break,
400 _ => {}
401 }
402 }
403 }
404 }
405
406 Ok(MsdfFont {
407 texture_id,
408 atlas_width,
409 atlas_height,
410 font_size,
411 line_height,
412 distance_range,
413 glyphs,
414 })
415}
416
417fn parse_glyph_object(
419 obj: &str,
420 atlas_w: f32,
421 atlas_h: f32,
422 font_size: f32,
423) -> Option<(u32, MsdfGlyph)> {
424 let unicode = extract_number(obj, "\"unicode\"")? as u32;
425 let advance = extract_number(obj, "\"advance\"").unwrap_or(0.0);
426
427 let ab_left = extract_nested_number(obj, "\"atlasBounds\"", "\"left\"").unwrap_or(0.0);
429 let ab_bottom = extract_nested_number(obj, "\"atlasBounds\"", "\"bottom\"").unwrap_or(0.0);
430 let ab_right = extract_nested_number(obj, "\"atlasBounds\"", "\"right\"").unwrap_or(0.0);
431 let ab_top = extract_nested_number(obj, "\"atlasBounds\"", "\"top\"").unwrap_or(0.0);
432
433 let pb_left = extract_nested_number(obj, "\"planeBounds\"", "\"left\"").unwrap_or(0.0);
435 let pb_bottom = extract_nested_number(obj, "\"planeBounds\"", "\"bottom\"").unwrap_or(0.0);
436 let pb_right = extract_nested_number(obj, "\"planeBounds\"", "\"right\"").unwrap_or(0.0);
437 let pb_top = extract_nested_number(obj, "\"planeBounds\"", "\"top\"").unwrap_or(0.0);
438
439 let uv_x = ab_left / atlas_w;
440 let uv_y = ab_top / atlas_h;
441 let uv_w = (ab_right - ab_left) / atlas_w;
442 let uv_h = (ab_bottom - ab_top) / atlas_h;
443
444 let glyph_w = (pb_right - pb_left) * font_size;
445 let glyph_h = (pb_top - pb_bottom) * font_size;
446
447 Some((
448 unicode,
449 MsdfGlyph {
450 uv_x,
451 uv_y,
452 uv_w,
453 uv_h,
454 advance: advance * font_size,
455 width: glyph_w,
456 height: glyph_h,
457 offset_x: pb_left * font_size,
458 offset_y: pb_top * font_size,
459 },
460 ))
461}
462
463fn extract_number(json: &str, key: &str) -> Option<f32> {
465 let key_pos = json.find(key)?;
466 let after_key = &json[key_pos + key.len()..];
467 let value_start = after_key.find(|c: char| c.is_ascii_digit() || c == '-' || c == '.')?;
469 let value_str = &after_key[value_start..];
470 let value_end = value_str
471 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != 'e' && c != 'E' && c != '+')
472 .unwrap_or(value_str.len());
473 value_str[..value_end].parse::<f32>().ok()
474}
475
476fn extract_nested_number(json: &str, outer_key: &str, inner_key: &str) -> Option<f32> {
478 let outer_pos = json.find(outer_key)?;
479 let rest = &json[outer_pos..];
480 let brace_pos = rest.find('{')?;
481 let end_pos = rest[brace_pos..].find('}')? + brace_pos;
482 let inner = &rest[brace_pos..=end_pos];
483 extract_number(inner, inner_key)
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 #[test]
491 fn builtin_msdf_font_generates() {
492 let (pixels, w, h, font) = generate_builtin_msdf_font();
493 assert_eq!(w, ATLAS_COLS * DST_GLYPH_W);
494 assert_eq!(h, ATLAS_ROWS * DST_GLYPH_H);
495 assert_eq!(pixels.len(), (w * h * 4) as usize);
496 assert_eq!(font.glyphs.len(), 96);
497 assert!(font.distance_range > 0.0);
498 }
499
500 #[test]
501 fn builtin_msdf_has_expected_glyphs() {
502 let (_, _, _, font) = generate_builtin_msdf_font();
503 assert!(font.get_glyph(' ').is_some());
505 assert!(font.get_glyph('A').is_some());
506 assert!(font.get_glyph('z').is_some());
507 assert!(font.get_glyph('\x01').is_none());
509 }
510
511 #[test]
512 fn builtin_msdf_glyph_uvs_valid() {
513 let (_, _, _, font) = generate_builtin_msdf_font();
514 for glyph in font.glyphs.values() {
515 assert!(glyph.uv_x >= 0.0 && glyph.uv_x <= 1.0, "uv_x out of range");
516 assert!(glyph.uv_y >= 0.0 && glyph.uv_y <= 1.0, "uv_y out of range");
517 assert!(glyph.uv_w > 0.0 && glyph.uv_w <= 1.0, "uv_w out of range");
518 assert!(glyph.uv_h > 0.0 && glyph.uv_h <= 1.0, "uv_h out of range");
519 }
520 }
521
522 #[test]
523 fn builtin_msdf_distance_field_correctness() {
524 let (pixels, w, _h, _font) = generate_builtin_msdf_font();
525 let glyph_x = 1 * DST_GLYPH_W;
531 let glyph_y = 2 * DST_GLYPH_H;
532
533 let mut has_outside = false; let mut has_edge = false; let mut has_inside = false; for py in 0..DST_GLYPH_H {
538 for px in 0..DST_GLYPH_W {
539 let offset = (((glyph_y + py) * w + (glyph_x + px)) * 4) as usize;
540 let val = pixels[offset]; if val < 110 { has_outside = true; }
542 if val > 110 && val < 170 { has_edge = true; }
543 if val > 140 { has_inside = true; }
544 }
545 }
546
547 assert!(has_outside, "'A' glyph should have outside distance values");
548 assert!(has_edge, "'A' glyph should have edge distance values");
549 assert!(has_inside, "'A' glyph should have inside distance values");
550 }
551
552 #[test]
553 fn space_glyph_is_outside() {
554 let (pixels, w, _h, _font) = generate_builtin_msdf_font();
555 let glyph_x = 0;
558 let glyph_y = 0;
559
560 let cx = glyph_x + DST_GLYPH_W / 2;
562 let cy = glyph_y + DST_GLYPH_H / 2;
563 let offset = ((cy * w + cx) * 4) as usize;
564 let val = pixels[offset];
565 assert!(val < 128, "Space center should be outside (val={val}, expected < 128)");
566 }
567
568 #[test]
569 fn measure_width_works() {
570 let (_, _, _, font) = generate_builtin_msdf_font();
571 let width = font.measure_width("Hello", 8.0);
572 assert!((width - 40.0).abs() < 0.01, "Expected ~40, got {width}");
574 }
575
576 #[test]
577 fn measure_width_with_scale() {
578 let (_, _, _, font) = generate_builtin_msdf_font();
579 let width = font.measure_width("AB", 16.0);
580 assert!((width - 32.0).abs() < 0.01, "Expected ~32, got {width}");
582 }
583
584 #[test]
585 fn parse_metrics_basic() {
586 let json = r#"{
587 "atlas": { "width": 256, "height": 256, "distanceRange": 4, "size": 32 },
588 "metrics": { "lineHeight": 1.2 },
589 "glyphs": [
590 {
591 "unicode": 65,
592 "advance": 0.6,
593 "atlasBounds": { "left": 0, "bottom": 32, "right": 24, "top": 0 },
594 "planeBounds": { "left": 0, "bottom": -0.1, "right": 0.6, "top": 0.9 }
595 }
596 ]
597 }"#;
598
599 let font = parse_msdf_metrics(json, 42).unwrap();
600 assert_eq!(font.texture_id, 42);
601 assert_eq!(font.atlas_width, 256);
602 assert_eq!(font.atlas_height, 256);
603 assert!((font.distance_range - 4.0).abs() < 0.01);
604 assert_eq!(font.glyphs.len(), 1);
605
606 let glyph = font.get_glyph('A').unwrap();
607 assert!((glyph.advance - 19.2).abs() < 0.1); }
609
610 #[test]
611 fn font_store_register_and_get() {
612 let mut store = MsdfFontStore::new();
613 let font = MsdfFont {
614 texture_id: 1,
615 atlas_width: 128,
616 atlas_height: 128,
617 font_size: 16.0,
618 line_height: 20.0,
619 distance_range: 4.0,
620 glyphs: HashMap::new(),
621 };
622 let id = store.register(font);
623 assert!(store.get(id).is_some());
624 assert!(store.get(id + 1).is_none());
625 }
626
627 #[test]
628 fn point_to_segment_distance() {
629 let d = point_to_segment_dist_sq(0.0, 0.5, 1.0, 0.0, 1.0, 1.0);
631 assert!((d - 1.0).abs() < 0.001);
632
633 let d = point_to_segment_dist_sq(0.5, 0.0, 0.0, 0.0, 1.0, 0.0);
635 assert!(d < 0.001);
636 }
637}