1use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use ab_glyph::{Font, FontVec, PxScale, ScaleFont};
15
16use super::atlas::ATLAS_CHARS;
17
18#[derive(Clone, Debug)]
22pub struct SdfConfig {
23 pub hires_size: u32,
25 pub output_size: u32,
27 pub spread: f32,
29 pub msdf: bool,
31 pub cache_path: Option<PathBuf>,
33}
34
35impl Default for SdfConfig {
36 fn default() -> Self {
37 Self {
38 hires_size: 256,
39 output_size: 64,
40 spread: 8.0,
41 msdf: false,
42 cache_path: None,
43 }
44 }
45}
46
47#[derive(Clone, Debug)]
51pub struct SdfGlyphData {
52 pub pixels: Vec<u8>,
55 pub width: u32,
56 pub height: u32,
57 pub advance: f32,
59 pub bearing_x: f32,
61 pub bearing_y: f32,
62 pub bbox_w: f32,
64 pub bbox_h: f32,
65}
66
67#[derive(Clone, Debug)]
72pub struct MsdfGlyphData {
73 pub r_channel: Vec<u8>,
74 pub g_channel: Vec<u8>,
75 pub b_channel: Vec<u8>,
76 pub width: u32,
77 pub height: u32,
78 pub advance: f32,
79 pub bearing_x: f32,
80 pub bearing_y: f32,
81 pub bbox_w: f32,
82 pub bbox_h: f32,
83}
84
85#[derive(Copy, Clone, Debug)]
89pub struct SdfGlyphMetric {
90 pub uv_rect: [f32; 4],
92 pub size: glam::Vec2,
94 pub bearing: glam::Vec2,
96 pub advance: f32,
98}
99
100pub struct SdfAtlasData {
102 pub pixels: Vec<u8>,
104 pub width: u32,
105 pub height: u32,
106 pub channels: u32,
108 pub metrics: HashMap<char, SdfGlyphMetric>,
109 pub spread: f32,
110 pub font_size_px: f32,
111}
112
113#[derive(Copy, Clone)]
120struct Offset {
121 dx: i32,
122 dy: i32,
123}
124
125impl Offset {
126 const FAR: Self = Self { dx: 9999, dy: 9999 };
127 const ZERO: Self = Self { dx: 0, dy: 0 };
128
129 fn dist_sq(self) -> i32 {
130 self.dx * self.dx + self.dy * self.dy
131 }
132}
133
134fn dead_reckoning_udf(bitmap: &[bool], w: usize, h: usize) -> Vec<f32> {
139 let n = w * h;
140 let mut grid = vec![Offset::FAR; n];
141
142 for (cell, &set) in grid.iter_mut().zip(bitmap.iter()) {
147 if set {
148 *cell = Offset::ZERO;
149 }
150 }
151
152 for y in 0..h {
155 for x in 0..w {
156 let idx = y * w + x;
157 let cur = grid[idx];
158
159 macro_rules! check {
160 ($nx:expr, $ny:expr, $ddx:expr, $ddy:expr) => {
161 if $nx < w && $ny < h {
162 let nidx = $ny * w + $nx;
163 let candidate = Offset {
164 dx: grid[nidx].dx + $ddx,
165 dy: grid[nidx].dy + $ddy,
166 };
167 if candidate.dist_sq() < grid[idx].dist_sq() {
168 grid[idx] = candidate;
169 }
170 }
171 };
172 }
173
174 if y > 0 {
175 if x > 0 { check!(x - 1, y - 1, 1, 1); }
176 check!(x, y - 1, 0, 1);
177 if x + 1 < w { check!(x + 1, y - 1, -1, 1); }
178 }
179 if x > 0 { check!(x - 1, y, 1, 0); }
180 }
181 }
182
183 for y in (0..h).rev() {
186 for x in (0..w).rev() {
187 let idx = y * w + x;
188
189 macro_rules! check {
190 ($nx:expr, $ny:expr, $ddx:expr, $ddy:expr) => {
191 if $nx < w && $ny < h {
192 let nidx = $ny * w + $nx;
193 let candidate = Offset {
194 dx: grid[nidx].dx + $ddx,
195 dy: grid[nidx].dy + $ddy,
196 };
197 if candidate.dist_sq() < grid[idx].dist_sq() {
198 grid[idx] = candidate;
199 }
200 }
201 };
202 }
203
204 if y + 1 < h {
205 if x + 1 < w { check!(x + 1, y + 1, -1, -1); }
206 check!(x, y + 1, 0, -1);
207 if x > 0 { check!(x - 1, y + 1, 1, -1); }
208 }
209 if x + 1 < w { check!(x + 1, y, -1, 0); }
210 }
211 }
212
213 grid.iter().map(|o| (o.dist_sq() as f32).sqrt()).collect()
214}
215
216fn compute_sdf(bitmap: &[bool], w: usize, h: usize) -> Vec<f32> {
220 let outside_dist = dead_reckoning_udf(bitmap, w, h);
222
223 let inverted: Vec<bool> = bitmap.iter().map(|b| !b).collect();
225 let inside_dist = dead_reckoning_udf(&inverted, w, h);
226
227 outside_dist
229 .iter()
230 .zip(inside_dist.iter())
231 .map(|(out_d, in_d)| *in_d - *out_d)
232 .collect()
233}
234
235fn rasterize_glyph(
240 font: &FontVec,
241 ch: char,
242 hires_px: f32,
243) -> Option<(Vec<f32>, u32, u32, f32, f32, f32, f32, f32)> {
244 let scale = PxScale::from(hires_px);
245 let scaled = font.as_scaled(scale);
246
247 let glyph_id = font.glyph_id(ch);
248 if glyph_id.0 == 0 && ch != ' ' {
249 return None;
250 }
251
252 let advance = scaled.h_advance(glyph_id);
253 let ascent = scaled.ascent();
254
255 let glyph = glyph_id.with_scale_and_position(scale, ab_glyph::point(0.0, ascent));
256
257 if let Some(outlined) = font.outline_glyph(glyph) {
258 let bounds = outlined.px_bounds();
259 let w = (bounds.max.x - bounds.min.x).ceil() as u32 + 2;
260 let h = (bounds.max.y - bounds.min.y).ceil() as u32 + 2;
261 if w == 0 || h == 0 {
262 return None;
263 }
264
265 let mut coverage = vec![0.0_f32; (w * h) as usize];
266 let ox = bounds.min.x.floor() as i32;
267 let oy = bounds.min.y.floor() as i32;
268
269 outlined.draw(|x, y, v| {
270 let px = x as i32 - ox + 1;
271 let py = y as i32 - oy + 1;
272 if px >= 0 && py >= 0 && (px as u32) < w && (py as u32) < h {
273 coverage[(py as u32 * w + px as u32) as usize] = v;
274 }
275 });
276
277 let bearing_x = bounds.min.x;
278 let bearing_y = bounds.min.y;
279 let bbox_w = (bounds.max.x - bounds.min.x).max(1.0);
280 let bbox_h = (bounds.max.y - bounds.min.y).max(1.0);
281
282 Some((coverage, w, h, advance, bearing_x, bearing_y, bbox_w, bbox_h))
283 } else {
284 Some((vec![0.0; 4], 2, 2, advance, 0.0, 0.0, 1.0, 1.0))
286 }
287}
288
289pub fn generate_glyph_sdf(
291 font: &FontVec,
292 ch: char,
293 config: &SdfConfig,
294) -> Option<SdfGlyphData> {
295 let (coverage, hi_w, hi_h, advance, bearing_x, bearing_y, bbox_w, bbox_h) =
296 rasterize_glyph(font, ch, config.hires_size as f32)?;
297
298 let bitmap: Vec<bool> = coverage.iter().map(|&v| v > 0.5).collect();
300
301 let sdf_hires = compute_sdf(&bitmap, hi_w as usize, hi_h as usize);
303
304 let scale_factor = config.output_size as f32 / config.hires_size as f32;
306 let out_w = ((hi_w as f32 * scale_factor).ceil() as u32).max(1);
307 let out_h = ((hi_h as f32 * scale_factor).ceil() as u32).max(1);
308
309 let pad = (config.spread * 1.5).ceil() as u32;
311 let padded_w = out_w + pad * 2;
312 let padded_h = out_h + pad * 2;
313
314 let inv_scale = 1.0 / scale_factor;
315 let spread_pixels = config.spread;
316
317 let mut sdf_out = vec![128u8; (padded_w * padded_h) as usize];
318
319 for py in 0..padded_h {
320 for px in 0..padded_w {
321 let hx = ((px as f32 - pad as f32 + 0.5) * inv_scale).max(0.0);
323 let hy = ((py as f32 - pad as f32 + 0.5) * inv_scale).max(0.0);
324
325 let dist = sample_bilinear_f32(&sdf_hires, hi_w as usize, hi_h as usize, hx, hy);
327
328 let dist_scaled = dist * scale_factor;
330 let normalized = (dist_scaled / spread_pixels) * 0.5 + 0.5;
331 let byte = (normalized.clamp(0.0, 1.0) * 255.0) as u8;
332
333 sdf_out[(py * padded_w + px) as usize] = byte;
334 }
335 }
336
337 Some(SdfGlyphData {
338 pixels: sdf_out,
339 width: padded_w,
340 height: padded_h,
341 advance,
342 bearing_x,
343 bearing_y,
344 bbox_w,
345 bbox_h,
346 })
347}
348
349pub fn generate_glyph_msdf(
355 font: &FontVec,
356 ch: char,
357 config: &SdfConfig,
358) -> Option<MsdfGlyphData> {
359 let (coverage, hi_w, hi_h, advance, bearing_x, bearing_y, bbox_w, bbox_h) =
360 rasterize_glyph(font, ch, config.hires_size as f32)?;
361
362 let w = hi_w as usize;
363 let h = hi_h as usize;
364
365 let bitmap: Vec<bool> = coverage.iter().map(|&v| v > 0.5).collect();
370
371 let mut edge_class = vec![0u8; w * h]; for y in 1..h.saturating_sub(1) {
374 for x in 1..w.saturating_sub(1) {
375 let idx = y * w + x;
376 if !is_edge(&bitmap, w, h, x, y) {
377 continue;
378 }
379 let gx = coverage[idx + 1] - coverage[idx.saturating_sub(1)];
380 let gy = coverage[idx + w] - coverage[idx.saturating_sub(w)];
381 let angle = gy.atan2(gx); let angle_deg = (angle.to_degrees() + 360.0) % 360.0;
383 edge_class[idx] = if angle_deg < 120.0 {
384 0
385 } else if angle_deg < 240.0 {
386 1
387 } else {
388 2
389 };
390 }
391 }
392
393 let mut channels = Vec::new();
396 for ch_idx in 0..3u8 {
397 let channel_bitmap: Vec<bool> = (0..w * h)
398 .map(|i| {
399 if bitmap[i] {
400 true
402 } else {
403 false
405 }
406 })
407 .collect();
408
409 let sdf = compute_sdf(&channel_bitmap, w, h);
410
411 let full_sdf = compute_sdf(&bitmap, w, h);
414 let blended: Vec<f32> = (0..w * h)
415 .map(|i| {
416 if is_edge(&bitmap, w, h, i % w, i / w) && edge_class[i] != ch_idx {
417 full_sdf[i] + 0.5
419 } else {
420 full_sdf[i]
421 }
422 })
423 .collect();
424
425 channels.push(blended);
426 }
427
428 let scale_factor = config.output_size as f32 / config.hires_size as f32;
430 let out_w = ((hi_w as f32 * scale_factor).ceil() as u32).max(1);
431 let out_h = ((hi_h as f32 * scale_factor).ceil() as u32).max(1);
432 let pad = (config.spread * 1.5).ceil() as u32;
433 let padded_w = out_w + pad * 2;
434 let padded_h = out_h + pad * 2;
435 let inv_scale = 1.0 / scale_factor;
436
437 let mut r_out = vec![128u8; (padded_w * padded_h) as usize];
438 let mut g_out = vec![128u8; (padded_w * padded_h) as usize];
439 let mut b_out = vec![128u8; (padded_w * padded_h) as usize];
440
441 for py in 0..padded_h {
442 for px in 0..padded_w {
443 let hx = ((px as f32 - pad as f32 + 0.5) * inv_scale).max(0.0);
444 let hy = ((py as f32 - pad as f32 + 0.5) * inv_scale).max(0.0);
445
446 for (ch_idx, out) in [&mut r_out, &mut g_out, &mut b_out].iter_mut().enumerate() {
447 let dist = sample_bilinear_f32(&channels[ch_idx], w, h, hx, hy);
448 let dist_scaled = dist * scale_factor;
449 let normalized = (dist_scaled / config.spread) * 0.5 + 0.5;
450 out[(py * padded_w + px) as usize] = (normalized.clamp(0.0, 1.0) * 255.0) as u8;
451 }
452 }
453 }
454
455 Some(MsdfGlyphData {
456 r_channel: r_out,
457 g_channel: g_out,
458 b_channel: b_out,
459 width: padded_w,
460 height: padded_h,
461 advance,
462 bearing_x,
463 bearing_y,
464 bbox_w,
465 bbox_h,
466 })
467}
468
469fn is_edge(bitmap: &[bool], w: usize, h: usize, x: usize, y: usize) -> bool {
471 let idx = y * w + x;
472 if !bitmap[idx] {
473 return false;
474 }
475 (x > 0 && !bitmap[idx - 1])
476 || (x + 1 < w && !bitmap[idx + 1])
477 || (y > 0 && !bitmap[idx - w])
478 || (y + 1 < h && !bitmap[idx + w])
479}
480
481fn sample_bilinear_f32(data: &[f32], w: usize, h: usize, x: f32, y: f32) -> f32 {
483 let x0 = (x.floor() as usize).min(w.saturating_sub(1));
484 let y0 = (y.floor() as usize).min(h.saturating_sub(1));
485 let x1 = (x0 + 1).min(w.saturating_sub(1));
486 let y1 = (y0 + 1).min(h.saturating_sub(1));
487 let fx = x - x.floor();
488 let fy = y - y.floor();
489
490 let c00 = data[y0 * w + x0];
491 let c10 = data[y0 * w + x1];
492 let c01 = data[y1 * w + x0];
493 let c11 = data[y1 * w + x1];
494
495 let c0 = c00 + (c10 - c00) * fx;
496 let c1 = c01 + (c11 - c01) * fx;
497 c0 + (c1 - c0) * fy
498}
499
500struct ShelfPacker {
505 atlas_width: u32,
506 atlas_height: u32,
507 shelf_x: u32,
508 shelf_y: u32,
509 shelf_height: u32,
510}
511
512impl ShelfPacker {
513 fn new(atlas_width: u32, atlas_height: u32) -> Self {
514 Self {
515 atlas_width,
516 atlas_height,
517 shelf_x: 0,
518 shelf_y: 0,
519 shelf_height: 0,
520 }
521 }
522
523 fn pack(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
525 if w > self.atlas_width {
526 return None;
527 }
528
529 if self.shelf_x + w > self.atlas_width {
531 self.shelf_y += self.shelf_height;
533 self.shelf_x = 0;
534 self.shelf_height = 0;
535 }
536
537 if self.shelf_y + h > self.atlas_height {
539 return None;
540 }
541
542 let pos = (self.shelf_x, self.shelf_y);
543 self.shelf_x += w;
544 if h > self.shelf_height {
545 self.shelf_height = h;
546 }
547
548 Some(pos)
549 }
550}
551
552fn load_system_font() -> Option<FontVec> {
556 let paths: &[&str] = &[
557 r"C:\Windows\Fonts\consola.ttf",
558 r"C:\Windows\Fonts\cour.ttf",
559 r"C:\Windows\Fonts\lucon.ttf",
560 "/System/Library/Fonts/Menlo.ttc",
561 "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
562 "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
563 "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
564 ];
565 for p in paths {
566 if let Ok(data) = std::fs::read(p) {
567 if let Ok(f) = FontVec::try_from_vec(data) {
568 log::info!("SdfGenerator: loaded '{}'", p);
569 return Some(f);
570 }
571 }
572 }
573 None
574}
575
576pub fn generate_sdf_atlas(config: &SdfConfig) -> SdfAtlasData {
578 if let Some(ref cache_path) = config.cache_path {
580 if let Some(cached) = load_cached_atlas(cache_path, config) {
581 log::info!("SdfGenerator: loaded cached atlas from {:?}", cache_path);
582 return cached;
583 }
584 }
585
586 let font = load_system_font();
587 let chars: Vec<char> = ATLAS_CHARS.chars().collect();
588
589 let mut glyph_sdfs: Vec<(char, SdfGlyphData)> = Vec::new();
591
592 if let Some(ref font) = font {
593 for &ch in &chars {
594 if let Some(sdf) = generate_glyph_sdf(font, ch, config) {
595 glyph_sdfs.push((ch, sdf));
596 } else {
597 glyph_sdfs.push((ch, SdfGlyphData {
599 pixels: vec![0u8; 16],
600 width: 4,
601 height: 4,
602 advance: config.output_size as f32 * 0.5,
603 bearing_x: 0.0,
604 bearing_y: 0.0,
605 bbox_w: 4.0,
606 bbox_h: 4.0,
607 }));
608 }
609 }
610 } else {
611 log::warn!("SdfGenerator: no system font found, generating fallback SDF atlas");
612 for &ch in &chars {
613 glyph_sdfs.push((ch, generate_fallback_sdf(config)));
614 }
615 }
616
617 let max_glyph_w = glyph_sdfs.iter().map(|(_, g)| g.width).max().unwrap_or(64);
619 let max_glyph_h = glyph_sdfs.iter().map(|(_, g)| g.height).max().unwrap_or(64);
620 let cells_per_row = 2048 / max_glyph_w.max(1);
621 let rows_needed = (glyph_sdfs.len() as u32 + cells_per_row - 1) / cells_per_row.max(1);
622 let atlas_h_needed = rows_needed * max_glyph_h;
623
624 let atlas_w = (cells_per_row * max_glyph_w).max(256).min(4096);
625 let atlas_h = atlas_h_needed.max(256).min(4096);
626
627 let mut atlas_pixels = vec![0u8; (atlas_w * atlas_h) as usize];
628 let mut metrics = HashMap::new();
629 let mut packer = ShelfPacker::new(atlas_w, atlas_h);
630
631 let hires = config.hires_size as f32;
632
633 for (ch, glyph_sdf) in &glyph_sdfs {
634 if let Some((ax, ay)) = packer.pack(glyph_sdf.width, glyph_sdf.height) {
635 for gy in 0..glyph_sdf.height {
637 for gx in 0..glyph_sdf.width {
638 let src = (gy * glyph_sdf.width + gx) as usize;
639 let dst = ((ay + gy) * atlas_w + (ax + gx)) as usize;
640 if src < glyph_sdf.pixels.len() && dst < atlas_pixels.len() {
641 atlas_pixels[dst] = glyph_sdf.pixels[src];
642 }
643 }
644 }
645
646 metrics.insert(*ch, SdfGlyphMetric {
647 uv_rect: [
648 ax as f32 / atlas_w as f32,
649 ay as f32 / atlas_h as f32,
650 (ax + glyph_sdf.width) as f32 / atlas_w as f32,
651 (ay + glyph_sdf.height) as f32 / atlas_h as f32,
652 ],
653 size: glam::Vec2::new(glyph_sdf.bbox_w, glyph_sdf.bbox_h),
654 bearing: glam::Vec2::new(glyph_sdf.bearing_x, glyph_sdf.bearing_y),
655 advance: glyph_sdf.advance,
656 });
657 } else {
658 log::warn!("SdfGenerator: atlas full, could not pack glyph '{}'", ch);
659 }
660 }
661
662 let atlas = SdfAtlasData {
663 pixels: atlas_pixels,
664 width: atlas_w,
665 height: atlas_h,
666 channels: 1,
667 metrics,
668 spread: config.spread,
669 font_size_px: config.output_size as f32,
670 };
671
672 if let Some(ref cache_path) = config.cache_path {
674 save_atlas_cache(cache_path, &atlas);
675 }
676
677 atlas
678}
679
680fn generate_fallback_sdf(config: &SdfConfig) -> SdfGlyphData {
682 let size = config.output_size.max(8);
683 let pad = (config.spread * 1.5).ceil() as u32;
684 let total = size + pad * 2;
685 let mut pixels = vec![0u8; (total * total) as usize];
686
687 for y in 0..total {
689 for x in 0..total {
690 let dx = if x < pad {
691 pad as f32 - x as f32
692 } else if x >= size + pad {
693 (x - size - pad + 1) as f32
694 } else {
695 0.0
696 };
697 let dy = if y < pad {
698 pad as f32 - y as f32
699 } else if y >= size + pad {
700 (y - size - pad + 1) as f32
701 } else {
702 0.0
703 };
704 let dist = (dx * dx + dy * dy).sqrt();
705 let normalized = (-dist / config.spread) * 0.5 + 0.5;
706 pixels[(y * total + x) as usize] = (normalized.clamp(0.0, 1.0) * 255.0) as u8;
707 }
708 }
709
710 SdfGlyphData {
711 pixels,
712 width: total,
713 height: total,
714 advance: size as f32,
715 bearing_x: 0.0,
716 bearing_y: 0.0,
717 bbox_w: size as f32,
718 bbox_h: size as f32,
719 }
720}
721
722fn save_atlas_cache(path: &Path, atlas: &SdfAtlasData) {
730 let mut data = Vec::new();
731
732 data.extend_from_slice(b"SDF1");
734 data.extend_from_slice(&atlas.width.to_le_bytes());
735 data.extend_from_slice(&atlas.height.to_le_bytes());
736 data.extend_from_slice(&atlas.spread.to_le_bytes());
737 data.extend_from_slice(&atlas.font_size_px.to_le_bytes());
738 data.extend_from_slice(&(atlas.metrics.len() as u32).to_le_bytes());
739
740 for (&ch, metric) in &atlas.metrics {
741 data.extend_from_slice(&(ch as u32).to_le_bytes());
742 for &uv in &metric.uv_rect {
743 data.extend_from_slice(&uv.to_le_bytes());
744 }
745 data.extend_from_slice(&metric.size.x.to_le_bytes());
746 data.extend_from_slice(&metric.size.y.to_le_bytes());
747 data.extend_from_slice(&metric.bearing.x.to_le_bytes());
748 data.extend_from_slice(&metric.bearing.y.to_le_bytes());
749 data.extend_from_slice(&metric.advance.to_le_bytes());
750 }
751
752 data.extend_from_slice(&atlas.pixels);
753
754 if let Err(e) = std::fs::write(path, &data) {
755 log::warn!("SdfGenerator: failed to write cache to {:?}: {}", path, e);
756 } else {
757 log::info!("SdfGenerator: cached atlas to {:?} ({} bytes)", path, data.len());
758 }
759}
760
761fn load_cached_atlas(path: &Path, config: &SdfConfig) -> Option<SdfAtlasData> {
762 let data = std::fs::read(path).ok()?;
763 if data.len() < 24 {
764 return None;
765 }
766
767 if &data[0..4] != b"SDF1" {
769 return None;
770 }
771
772 let mut cursor = 4usize;
773
774 macro_rules! read_u32 {
775 () => {{
776 if cursor + 4 > data.len() { return None; }
777 let val = u32::from_le_bytes(data[cursor..cursor + 4].try_into().ok()?);
778 cursor += 4;
779 val
780 }};
781 }
782
783 macro_rules! read_f32 {
784 () => {{
785 if cursor + 4 > data.len() { return None; }
786 let val = f32::from_le_bytes(data[cursor..cursor + 4].try_into().ok()?);
787 cursor += 4;
788 val
789 }};
790 }
791
792 let width = read_u32!();
793 let height = read_u32!();
794 let spread = read_f32!();
795 let font_size_px = read_f32!();
796 let num_glyphs = read_u32!();
797
798 if (spread - config.spread).abs() > 0.01 || (font_size_px - config.output_size as f32).abs() > 0.01 {
800 return None;
801 }
802
803 let mut metrics = HashMap::new();
804 for _ in 0..num_glyphs {
805 let ch_u32 = read_u32!();
806 let ch = char::from_u32(ch_u32)?;
807 let uv_rect = [read_f32!(), read_f32!(), read_f32!(), read_f32!()];
808 let size = glam::Vec2::new(read_f32!(), read_f32!());
809 let bearing = glam::Vec2::new(read_f32!(), read_f32!());
810 let advance = read_f32!();
811 metrics.insert(ch, SdfGlyphMetric {
812 uv_rect,
813 size,
814 bearing,
815 advance,
816 });
817 }
818
819 let pixel_count = (width * height) as usize;
820 if cursor + pixel_count > data.len() {
821 return None;
822 }
823 let pixels = data[cursor..cursor + pixel_count].to_vec();
824
825 Some(SdfAtlasData {
826 pixels,
827 width,
828 height,
829 channels: 1,
830 metrics,
831 spread,
832 font_size_px,
833 })
834}
835
836#[cfg(test)]
839mod tests {
840 use super::*;
841
842 #[test]
843 fn dead_reckoning_zero_for_boundary() {
844 let bitmap = vec![
846 false, false, false,
847 false, true, false,
848 false, false, false,
849 ];
850 let sdf = compute_sdf(&bitmap, 3, 3);
851 assert!(sdf[4] > 0.0);
853 assert!(sdf[0] < 0.0);
855 }
856
857 #[test]
858 fn dead_reckoning_all_inside() {
859 let bitmap = vec![true; 9];
860 let udf = dead_reckoning_udf(&bitmap, 3, 3);
861 let sdf = compute_sdf(&bitmap, 3, 3);
866 for &d in &sdf {
867 assert!(d >= 0.0);
868 }
869 }
870
871 #[test]
872 fn shelf_packer_fits_glyphs() {
873 let mut packer = ShelfPacker::new(128, 128);
874 let pos1 = packer.pack(32, 32);
875 assert!(pos1.is_some());
876 let pos2 = packer.pack(32, 32);
877 assert!(pos2.is_some());
878 assert_ne!(pos1, pos2);
879 }
880
881 #[test]
882 fn shelf_packer_new_shelf() {
883 let mut packer = ShelfPacker::new(64, 128);
884 let _ = packer.pack(40, 30); let pos2 = packer.pack(40, 30); assert!(pos2.is_some());
887 assert_eq!(pos2.unwrap().0, 0); assert_eq!(pos2.unwrap().1, 30); }
890
891 #[test]
892 fn shelf_packer_overflow() {
893 let mut packer = ShelfPacker::new(64, 64);
894 let _ = packer.pack(64, 64); let pos = packer.pack(10, 10); assert!(pos.is_none());
897 }
898
899 #[test]
900 fn bilinear_center() {
901 let data = vec![0.0, 1.0, 0.0, 1.0];
902 let val = sample_bilinear_f32(&data, 2, 2, 0.5, 0.5);
903 assert!((val - 0.5).abs() < 0.01);
904 }
905
906 #[test]
907 fn fallback_sdf_nonzero() {
908 let config = SdfConfig { output_size: 16, spread: 4.0, ..SdfConfig::default() };
909 let glyph = generate_fallback_sdf(&config);
910 assert!(!glyph.pixels.is_empty());
911 let cx = glyph.width / 2;
913 let cy = glyph.height / 2;
914 let center = glyph.pixels[(cy * glyph.width + cx) as usize];
915 assert!(center > 100, "Center pixel should be > 100, got {}", center);
916 }
917}