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> {
336 if json.contains("\"chars\"") {
338 parse_msdf_bmfont_format(json, texture_id)
339 } else {
340 parse_msdf_atlas_gen_format(json, texture_id)
341 }
342}
343
344fn parse_msdf_bmfont_format(json: &str, texture_id: u32) -> Result<MsdfFont, String> {
346 let atlas_width = extract_nested_number(json, "\"common\"", "\"scaleW\"")
348 .ok_or("Missing common.scaleW")? as u32;
349 let atlas_height = extract_nested_number(json, "\"common\"", "\"scaleH\"")
350 .ok_or("Missing common.scaleH")? as u32;
351
352 let font_size = extract_nested_number(json, "\"info\"", "\"size\"")
354 .unwrap_or(32.0);
355
356 let distance_range = extract_nested_number(json, "\"distanceField\"", "\"distanceRange\"")
358 .unwrap_or(4.0);
359
360 let line_height = extract_nested_number(json, "\"common\"", "\"lineHeight\"")
362 .unwrap_or(font_size * 1.2);
363
364 let mut glyphs = HashMap::new();
365
366 if let Some(chars_start) = json.find("\"chars\"") {
368 let rest = &json[chars_start..];
369 if let Some(arr_start) = rest.find('[') {
370 let arr_rest = &rest[arr_start + 1..];
371 let mut depth = 0i32;
372 let mut obj_start = None;
373
374 for (i, ch) in arr_rest.char_indices() {
375 match ch {
376 '{' => {
377 if depth == 0 {
378 obj_start = Some(i);
379 }
380 depth += 1;
381 }
382 '}' => {
383 depth -= 1;
384 if depth == 0 {
385 if let Some(start) = obj_start {
386 let obj = &arr_rest[start..=i];
387 if let Some(glyph) = parse_bmfont_char(
388 obj,
389 atlas_width as f32,
390 atlas_height as f32,
391 ) {
392 glyphs.insert(glyph.0, glyph.1);
393 }
394 }
395 }
396 }
397 ']' if depth == 0 => break,
398 _ => {}
399 }
400 }
401 }
402 }
403
404 Ok(MsdfFont {
405 texture_id,
406 atlas_width,
407 atlas_height,
408 font_size,
409 line_height,
410 distance_range,
411 glyphs,
412 })
413}
414
415fn parse_bmfont_char(obj: &str, atlas_w: f32, atlas_h: f32) -> Option<(u32, MsdfGlyph)> {
417 let id = extract_number(obj, "\"id\"")? as u32;
418 let x = extract_number(obj, "\"x\"").unwrap_or(0.0);
419 let y = extract_number(obj, "\"y\"").unwrap_or(0.0);
420 let width = extract_number(obj, "\"width\"").unwrap_or(0.0);
421 let height = extract_number(obj, "\"height\"").unwrap_or(0.0);
422 let xoffset = extract_number(obj, "\"xoffset\"").unwrap_or(0.0);
423 let yoffset = extract_number(obj, "\"yoffset\"").unwrap_or(0.0);
424 let xadvance = extract_number(obj, "\"xadvance\"").unwrap_or(width);
425
426 Some((
427 id,
428 MsdfGlyph {
429 uv_x: x / atlas_w,
430 uv_y: y / atlas_h,
431 uv_w: width / atlas_w,
432 uv_h: height / atlas_h,
433 advance: xadvance,
434 width,
435 height,
436 offset_x: xoffset,
437 offset_y: yoffset,
438 },
439 ))
440}
441
442fn parse_msdf_atlas_gen_format(json: &str, texture_id: u32) -> Result<MsdfFont, String> {
444 let atlas_width = extract_number(json, "\"width\"")
445 .ok_or("Missing atlas width")? as u32;
446 let atlas_height = extract_number(json, "\"height\"")
447 .ok_or("Missing atlas height")? as u32;
448 let distance_range = extract_number(json, "\"distanceRange\"")
449 .unwrap_or(4.0);
450 let font_size = extract_number(json, "\"size\"")
451 .unwrap_or(32.0);
452 let line_height_factor = extract_number(json, "\"lineHeight\"")
453 .unwrap_or(1.2);
454
455 let line_height = font_size * line_height_factor as f32;
456
457 let mut glyphs = HashMap::new();
458
459 if let Some(glyphs_start) = json.find("\"glyphs\"") {
461 let rest = &json[glyphs_start..];
462 if let Some(arr_start) = rest.find('[') {
463 let arr_rest = &rest[arr_start + 1..];
464 let mut depth = 0i32;
465 let mut obj_start = None;
466
467 for (i, ch) in arr_rest.char_indices() {
468 match ch {
469 '{' => {
470 if depth == 0 {
471 obj_start = Some(i);
472 }
473 depth += 1;
474 }
475 '}' => {
476 depth -= 1;
477 if depth == 0 {
478 if let Some(start) = obj_start {
479 let obj = &arr_rest[start..=i];
480 if let Some(glyph) = parse_glyph_object(
481 obj,
482 atlas_width as f32,
483 atlas_height as f32,
484 font_size,
485 ) {
486 glyphs.insert(glyph.0, glyph.1);
487 }
488 }
489 }
490 }
491 ']' if depth == 0 => break,
492 _ => {}
493 }
494 }
495 }
496 }
497
498 Ok(MsdfFont {
499 texture_id,
500 atlas_width,
501 atlas_height,
502 font_size,
503 line_height,
504 distance_range,
505 glyphs,
506 })
507}
508
509fn parse_glyph_object(
511 obj: &str,
512 atlas_w: f32,
513 atlas_h: f32,
514 font_size: f32,
515) -> Option<(u32, MsdfGlyph)> {
516 let unicode = extract_number(obj, "\"unicode\"")? as u32;
517 let advance = extract_number(obj, "\"advance\"").unwrap_or(0.0);
518
519 let ab_left = extract_nested_number(obj, "\"atlasBounds\"", "\"left\"").unwrap_or(0.0);
521 let ab_bottom = extract_nested_number(obj, "\"atlasBounds\"", "\"bottom\"").unwrap_or(0.0);
522 let ab_right = extract_nested_number(obj, "\"atlasBounds\"", "\"right\"").unwrap_or(0.0);
523 let ab_top = extract_nested_number(obj, "\"atlasBounds\"", "\"top\"").unwrap_or(0.0);
524
525 let pb_left = extract_nested_number(obj, "\"planeBounds\"", "\"left\"").unwrap_or(0.0);
527 let pb_bottom = extract_nested_number(obj, "\"planeBounds\"", "\"bottom\"").unwrap_or(0.0);
528 let pb_right = extract_nested_number(obj, "\"planeBounds\"", "\"right\"").unwrap_or(0.0);
529 let pb_top = extract_nested_number(obj, "\"planeBounds\"", "\"top\"").unwrap_or(0.0);
530
531 let uv_x = ab_left / atlas_w;
532 let uv_y = ab_top / atlas_h;
533 let uv_w = (ab_right - ab_left) / atlas_w;
534 let uv_h = (ab_bottom - ab_top) / atlas_h;
535
536 let glyph_w = (pb_right - pb_left) * font_size;
537 let glyph_h = (pb_top - pb_bottom) * font_size;
538
539 Some((
540 unicode,
541 MsdfGlyph {
542 uv_x,
543 uv_y,
544 uv_w,
545 uv_h,
546 advance: advance * font_size,
547 width: glyph_w,
548 height: glyph_h,
549 offset_x: pb_left * font_size,
550 offset_y: pb_top * font_size,
551 },
552 ))
553}
554
555fn extract_number(json: &str, key: &str) -> Option<f32> {
557 let key_pos = json.find(key)?;
558 let after_key = &json[key_pos + key.len()..];
559 let value_start = after_key.find(|c: char| c.is_ascii_digit() || c == '-' || c == '.')?;
561 let value_str = &after_key[value_start..];
562 let value_end = value_str
563 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != 'e' && c != 'E' && c != '+')
564 .unwrap_or(value_str.len());
565 value_str[..value_end].parse::<f32>().ok()
566}
567
568fn extract_nested_number(json: &str, outer_key: &str, inner_key: &str) -> Option<f32> {
570 let outer_pos = json.find(outer_key)?;
571 let rest = &json[outer_pos..];
572 let brace_pos = rest.find('{')?;
573 let end_pos = rest[brace_pos..].find('}')? + brace_pos;
574 let inner = &rest[brace_pos..=end_pos];
575 extract_number(inner, inner_key)
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[test]
583 fn builtin_msdf_font_generates() {
584 let (pixels, w, h, font) = generate_builtin_msdf_font();
585 assert_eq!(w, ATLAS_COLS * DST_GLYPH_W);
586 assert_eq!(h, ATLAS_ROWS * DST_GLYPH_H);
587 assert_eq!(pixels.len(), (w * h * 4) as usize);
588 assert_eq!(font.glyphs.len(), 96);
589 assert!(font.distance_range > 0.0);
590 }
591
592 #[test]
593 fn builtin_msdf_has_expected_glyphs() {
594 let (_, _, _, font) = generate_builtin_msdf_font();
595 assert!(font.get_glyph(' ').is_some());
597 assert!(font.get_glyph('A').is_some());
598 assert!(font.get_glyph('z').is_some());
599 assert!(font.get_glyph('\x01').is_none());
601 }
602
603 #[test]
604 fn builtin_msdf_glyph_uvs_valid() {
605 let (_, _, _, font) = generate_builtin_msdf_font();
606 for glyph in font.glyphs.values() {
607 assert!(glyph.uv_x >= 0.0 && glyph.uv_x <= 1.0, "uv_x out of range");
608 assert!(glyph.uv_y >= 0.0 && glyph.uv_y <= 1.0, "uv_y out of range");
609 assert!(glyph.uv_w > 0.0 && glyph.uv_w <= 1.0, "uv_w out of range");
610 assert!(glyph.uv_h > 0.0 && glyph.uv_h <= 1.0, "uv_h out of range");
611 }
612 }
613
614 #[test]
615 fn builtin_msdf_distance_field_correctness() {
616 let (pixels, w, _h, _font) = generate_builtin_msdf_font();
617 let glyph_x = 1 * DST_GLYPH_W;
623 let glyph_y = 2 * DST_GLYPH_H;
624
625 let mut has_outside = false; let mut has_edge = false; let mut has_inside = false; for py in 0..DST_GLYPH_H {
630 for px in 0..DST_GLYPH_W {
631 let offset = (((glyph_y + py) * w + (glyph_x + px)) * 4) as usize;
632 let val = pixels[offset]; if val < 110 { has_outside = true; }
634 if val > 110 && val < 170 { has_edge = true; }
635 if val > 140 { has_inside = true; }
636 }
637 }
638
639 assert!(has_outside, "'A' glyph should have outside distance values");
640 assert!(has_edge, "'A' glyph should have edge distance values");
641 assert!(has_inside, "'A' glyph should have inside distance values");
642 }
643
644 #[test]
645 fn space_glyph_is_outside() {
646 let (pixels, w, _h, _font) = generate_builtin_msdf_font();
647 let glyph_x = 0;
650 let glyph_y = 0;
651
652 let cx = glyph_x + DST_GLYPH_W / 2;
654 let cy = glyph_y + DST_GLYPH_H / 2;
655 let offset = ((cy * w + cx) * 4) as usize;
656 let val = pixels[offset];
657 assert!(val < 128, "Space center should be outside (val={val}, expected < 128)");
658 }
659
660 #[test]
661 fn measure_width_works() {
662 let (_, _, _, font) = generate_builtin_msdf_font();
663 let width = font.measure_width("Hello", 8.0);
664 assert!((width - 40.0).abs() < 0.01, "Expected ~40, got {width}");
666 }
667
668 #[test]
669 fn measure_width_with_scale() {
670 let (_, _, _, font) = generate_builtin_msdf_font();
671 let width = font.measure_width("AB", 16.0);
672 assert!((width - 32.0).abs() < 0.01, "Expected ~32, got {width}");
674 }
675
676 #[test]
677 fn parse_metrics_basic() {
678 let json = r#"{
679 "atlas": { "width": 256, "height": 256, "distanceRange": 4, "size": 32 },
680 "metrics": { "lineHeight": 1.2 },
681 "glyphs": [
682 {
683 "unicode": 65,
684 "advance": 0.6,
685 "atlasBounds": { "left": 0, "bottom": 32, "right": 24, "top": 0 },
686 "planeBounds": { "left": 0, "bottom": -0.1, "right": 0.6, "top": 0.9 }
687 }
688 ]
689 }"#;
690
691 let font = parse_msdf_metrics(json, 42).unwrap();
692 assert_eq!(font.texture_id, 42);
693 assert_eq!(font.atlas_width, 256);
694 assert_eq!(font.atlas_height, 256);
695 assert!((font.distance_range - 4.0).abs() < 0.01);
696 assert_eq!(font.glyphs.len(), 1);
697
698 let glyph = font.get_glyph('A').unwrap();
699 assert!((glyph.advance - 19.2).abs() < 0.1); }
701
702 #[test]
703 fn font_store_register_and_get() {
704 let mut store = MsdfFontStore::new();
705 let font = MsdfFont {
706 texture_id: 1,
707 atlas_width: 128,
708 atlas_height: 128,
709 font_size: 16.0,
710 line_height: 20.0,
711 distance_range: 4.0,
712 glyphs: HashMap::new(),
713 };
714 let id = store.register(font);
715 assert!(store.get(id).is_some());
716 assert!(store.get(id + 1).is_none());
717 }
718
719 #[test]
720 fn point_to_segment_distance() {
721 let d = point_to_segment_dist_sq(0.0, 0.5, 1.0, 0.0, 1.0, 1.0);
723 assert!((d - 1.0).abs() < 0.001);
724
725 let d = point_to_segment_dist_sq(0.5, 0.0, 0.0, 0.0, 1.0, 0.0);
727 assert!(d < 0.001);
728 }
729}