Skip to main content

proof_engine/glyph/
sdf_generator.rs

1//! SDF (Signed Distance Field) generation from rasterized glyph bitmaps.
2//!
3//! Uses a dead-reckoning (8SSEDT) algorithm for O(n) per-pixel SDF computation,
4//! with optional multi-channel SDF (MSDF) for sharper corners.
5//!
6//! The pipeline:
7//!   1. Rasterize each glyph at high resolution (256px) via `ab_glyph`
8//!   2. Compute SDF at output resolution (typically 64px) using dead reckoning
9//!   3. Pack glyphs into an atlas using a shelf packing algorithm
10//!   4. Optionally cache the atlas to disk as a PNG for fast reload
11
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use ab_glyph::{Font, FontVec, PxScale, ScaleFont};
15
16use super::atlas::ATLAS_CHARS;
17
18// ── SDF Parameters ───────────────────────────────────────────────────────────
19
20/// Configuration for SDF generation.
21#[derive(Clone, Debug)]
22pub struct SdfConfig {
23    /// Resolution at which glyphs are rasterized before downsampling to SDF.
24    pub hires_size: u32,
25    /// Output SDF glyph cell size in pixels.
26    pub output_size: u32,
27    /// How many output pixels the distance field extends from the glyph edge.
28    pub spread: f32,
29    /// Whether to generate multi-channel SDF (sharper corners).
30    pub msdf: bool,
31    /// Optional path to cache the generated atlas on disk.
32    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// ── Per-glyph SDF result ─────────────────────────────────────────────────────
48
49/// SDF data for a single glyph, before atlas packing.
50#[derive(Clone, Debug)]
51pub struct SdfGlyphData {
52    /// Signed distance values, one per pixel, in [0, 255].
53    /// 128 = edge, 255 = deep inside, 0 = far outside.
54    pub pixels: Vec<u8>,
55    pub width: u32,
56    pub height: u32,
57    /// Horizontal advance in pixels at the hires size.
58    pub advance: f32,
59    /// Bearing (offset from baseline) at the hires size.
60    pub bearing_x: f32,
61    pub bearing_y: f32,
62    /// Bounding box size at hires size.
63    pub bbox_w: f32,
64    pub bbox_h: f32,
65}
66
67// ── MSDF channel data ────────────────────────────────────────────────────────
68
69/// Multi-channel SDF result: R, G, B channels each contain distance to a
70/// different edge segment class, producing sharper corners when median-filtered.
71#[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// ── Atlas packing result ─────────────────────────────────────────────────────
86
87/// UV rectangle for one glyph in the SDF atlas.
88#[derive(Copy, Clone, Debug)]
89pub struct SdfGlyphMetric {
90    /// UV coordinates in the atlas: [u_min, v_min, u_max, v_max].
91    pub uv_rect: [f32; 4],
92    /// Glyph bounding box size in pixels at generation size.
93    pub size: glam::Vec2,
94    /// Offset from baseline at generation size.
95    pub bearing: glam::Vec2,
96    /// Horizontal advance to next glyph at generation size.
97    pub advance: f32,
98}
99
100/// Complete result of SDF atlas generation.
101pub struct SdfAtlasData {
102    /// R8 pixel data (single channel SDF) or RGB8 (MSDF).
103    pub pixels: Vec<u8>,
104    pub width: u32,
105    pub height: u32,
106    /// Number of channels: 1 for SDF, 3 for MSDF.
107    pub channels: u32,
108    pub metrics: HashMap<char, SdfGlyphMetric>,
109    pub spread: f32,
110    pub font_size_px: f32,
111}
112
113// ── Dead Reckoning (8SSEDT) ─────────────────────────────────────────────────
114//
115// Sequential Signed Euclidean Distance Transform.  Two passes (forward/backward)
116// propagate (dx, dy) offset vectors.  The distance at each pixel is sqrt(dx² + dy²).
117
118/// 2D offset vector used by the dead-reckoning algorithm.
119#[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
134/// Compute an unsigned distance field from a binary bitmap using 8SSEDT.
135///
136/// `bitmap` is row-major. Returns, for each cell, the distance in pixels to
137/// the nearest `true` cell (0 for `true` cells).
138fn 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    // Seed every set pixel with distance zero, so the result is the
143    // distance to the nearest set pixel. Seeding only "boundary" pixels on
144    // both sides of the edge gave 0 inside and outside alike, so the signed
145    // field was 0 for any shape one pixel thick and wrong-signed near edges.
146    for (cell, &set) in grid.iter_mut().zip(bitmap.iter()) {
147        if set {
148            *cell = Offset::ZERO;
149        }
150    }
151
152    // Forward pass: top-left to bottom-right.
153    // Neighborhood offsets checked: (-1,-1), (0,-1), (1,-1), (-1,0)
154    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    // Backward pass: bottom-right to top-left.
184    // Neighborhood offsets checked: (1,1), (0,1), (-1,1), (1,0)
185    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
216/// Compute a signed distance field from a binary bitmap.
217///
218/// Positive inside, negative outside, zero at the edge.
219fn compute_sdf(bitmap: &[bool], w: usize, h: usize) -> Vec<f32> {
220    // UDF from outside (distance to nearest inside pixel)
221    let outside_dist = dead_reckoning_udf(bitmap, w, h);
222
223    // Invert bitmap and compute UDF from inside (distance to nearest outside pixel)
224    let inverted: Vec<bool> = bitmap.iter().map(|b| !b).collect();
225    let inside_dist = dead_reckoning_udf(&inverted, w, h);
226
227    // SDF = inside_dist - outside_dist  (positive inside, negative outside)
228    outside_dist
229        .iter()
230        .zip(inside_dist.iter())
231        .map(|(out_d, in_d)| *in_d - *out_d)
232        .collect()
233}
234
235// ── Glyph rasterization ─────────────────────────────────────────────────────
236
237/// Rasterize a single glyph at `hires_px` size, returning a coverage bitmap
238/// and metrics.
239fn 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        // Space or non-renderable glyph — create an empty cell.
285        Some((vec![0.0; 4], 2, 2, advance, 0.0, 0.0, 1.0, 1.0))
286    }
287}
288
289/// Generate SDF data for a single glyph.
290pub 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    // Threshold coverage to binary bitmap.
299    let bitmap: Vec<bool> = coverage.iter().map(|&v| v > 0.5).collect();
300
301    // Compute SDF at hires resolution.
302    let sdf_hires = compute_sdf(&bitmap, hi_w as usize, hi_h as usize);
303
304    // Downsample to output resolution.
305    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    // Add padding for the spread.
310    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            // Map output pixel back to hires space.
322            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            // Bilinear sample of the hires SDF.
326            let dist = sample_bilinear_f32(&sdf_hires, hi_w as usize, hi_h as usize, hx, hy);
327
328            // Scale distance to output pixel space and normalize to [0, 255].
329            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
349/// Generate MSDF data for a single glyph using Chlumsky's approach.
350///
351/// We approximate MSDF by computing the SDF three times with slightly different
352/// edge classifications based on the edge normal direction.  This produces
353/// sharper corners when the median of R, G, B is taken in the fragment shader.
354pub 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    // Classify edges into 3 channels based on gradient direction.
366    // Channel R: edges with gradient angle in [0°, 120°)
367    // Channel G: edges with gradient angle in [120°, 240°)
368    // Channel B: edges with gradient angle in [240°, 360°)
369    let bitmap: Vec<bool> = coverage.iter().map(|&v| v > 0.5).collect();
370
371    // Compute gradient direction at each pixel using Sobel filter.
372    let mut edge_class = vec![0u8; w * h]; // 0=R, 1=G, 2=B
373    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); // [-PI, PI]
382            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    // For each channel, create a bitmap that includes only edges of that class,
394    // plus all interior pixels.
395    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                    // Interior pixel — always inside in all channels.
401                    true
402                } else {
403                    // Outside pixel — check if nearest edge belongs to this channel.
404                    false
405                }
406            })
407            .collect();
408
409        let sdf = compute_sdf(&channel_bitmap, w, h);
410
411        // Blend with the full SDF: for edge pixels of a different class,
412        // slightly adjust the distance.
413        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                    // Slightly push the distance for edges not in this channel.
418                    full_sdf[i] + 0.5
419                } else {
420                    full_sdf[i]
421                }
422            })
423            .collect();
424
425        channels.push(blended);
426    }
427
428    // Downsample each channel to output resolution.
429    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
469/// Check if a pixel is on the edge (inside pixel adjacent to an outside pixel).
470fn 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
481/// Bilinear interpolation of a float buffer.
482fn 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
500// ── Shelf Packing ───────────────────────────────────────────────────────────
501
502/// Shelf-based atlas packer.  Glyphs are placed left-to-right in rows (shelves),
503/// starting a new shelf when the current one runs out of horizontal space.
504struct 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    /// Try to place a glyph of (w, h) pixels. Returns (x, y) in the atlas, or None.
524    fn pack(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
525        if w > self.atlas_width {
526            return None;
527        }
528
529        // Does it fit on the current shelf?
530        if self.shelf_x + w > self.atlas_width {
531            // Start a new shelf.
532            self.shelf_y += self.shelf_height;
533            self.shelf_x = 0;
534            self.shelf_height = 0;
535        }
536
537        // Does it fit vertically?
538        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
552// ── Full atlas generation ───────────────────────────────────────────────────
553
554/// Load a system font (same logic as atlas.rs).
555fn 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
576/// Generate the complete SDF atlas for all `ATLAS_CHARS`.
577pub fn generate_sdf_atlas(config: &SdfConfig) -> SdfAtlasData {
578    // Try loading from cache first.
579    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    // Generate SDF for each character.
590    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                // Fallback: small empty glyph.
598                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    // Determine atlas size: try to fit in 2048×2048, then 4096×4096.
618    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            // Blit glyph SDF into atlas.
636            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    // Save to cache.
673    if let Some(ref cache_path) = config.cache_path {
674        save_atlas_cache(cache_path, &atlas);
675    }
676
677    atlas
678}
679
680/// Generate a fallback SDF glyph (filled rectangle).
681fn 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    // Create a simple box SDF: inside is 255, edges fade out.
688    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
722// ── Disk cache ──────────────────────────────────────────────────────────────
723
724/// Simple cache format:
725///   - Header: "SDF1" magic + width(u32) + height(u32) + spread(f32) + font_size(f32) + num_glyphs(u32)
726///   - For each glyph: char(u32) + uv_rect([f32;4]) + size(Vec2) + bearing(Vec2) + advance(f32)
727///   - Atlas pixel data (R8)
728
729fn save_atlas_cache(path: &Path, atlas: &SdfAtlasData) {
730    let mut data = Vec::new();
731
732    // Magic.
733    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    // Check magic.
768    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    // Validate that the config matches.
799    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// ── Tests ───────────────────────────────────────────────────────────────────
837
838#[cfg(test)]
839mod tests {
840    use super::*;
841
842    #[test]
843    fn dead_reckoning_zero_for_boundary() {
844        // 3×3 bitmap with center pixel inside.
845        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        // Center pixel should have positive distance.
852        assert!(sdf[4] > 0.0);
853        // Corner pixel should have negative distance.
854        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        // Interior pixels have distance > 0 from the outside boundary —
862        // but since there IS no boundary, all pixels get FAR distance
863        // via the UDF from outside perspective.
864        // After compute_sdf, interior should be positive.
865        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); // fills most of first shelf
885        let pos2 = packer.pack(40, 30); // must start new shelf
886        assert!(pos2.is_some());
887        assert_eq!(pos2.unwrap().0, 0); // starts at x=0
888        assert_eq!(pos2.unwrap().1, 30); // y = previous shelf height
889    }
890
891    #[test]
892    fn shelf_packer_overflow() {
893        let mut packer = ShelfPacker::new(64, 64);
894        let _ = packer.pack(64, 64); // fills entire atlas
895        let pos = packer.pack(10, 10); // should fail
896        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        // Center pixel should be close to 255 (deep inside).
912        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}