Skip to main content

oxitext_sdf/
atlas.rs

1//! SDF atlas packer: bin-packs SDF glyph tiles into a GPU-ready texture.
2//!
3//! Provides shelf-packing and MaxRects packing for both single-channel
4//! ([`SdfAtlas`]) and multi-channel ([`MsdfAtlas`]) distance-field tiles.
5
6use std::collections::HashMap;
7
8use crate::edt::SdfError;
9
10/// A single SDF glyph tile produced by the SDF pipeline.
11#[derive(Clone, Debug)]
12pub struct SdfTile {
13    /// Glyph ID within the font.
14    pub glyph_id: u16,
15    /// Tile width in pixels.
16    pub width: u32,
17    /// Tile height in pixels.
18    pub height: u32,
19    /// SDF pixel data (`width × height` bytes, values 0–255).
20    pub data: Vec<u8>,
21    /// Left bearing in pixels (signed).
22    pub bearing_x: i32,
23    /// Top bearing in pixels (signed).
24    pub bearing_y: i32,
25    /// Horizontal advance in pixels.
26    pub advance_x: f32,
27}
28
29impl SdfTile {
30    /// Create an SDF tile from a pre-rasterized floating-point coverage bitmap.
31    ///
32    /// # Arguments
33    /// - `glyph_id` — glyph index within the font.
34    /// - `coverage` — greyscale coverage values, `width × height`, values in `[0, 1]`.
35    /// - `width`, `height` — bitmap dimensions (must be non-zero).
36    /// - `spread` — maximum SDF distance in pixels; maps ±spread to \[0, 255\].
37    /// - `bearing_x`, `bearing_y` — glyph bearing in pixels (signed).
38    /// - `advance_x` — horizontal advance in pixels.
39    ///
40    /// # Errors
41    /// Returns [`SdfError::ZeroSize`] when `width` or `height` is zero.
42    /// Returns [`SdfError::InvalidInput`] when `coverage.len() != width * height`.
43    #[allow(clippy::too_many_arguments)]
44    pub fn from_coverage(
45        glyph_id: u16,
46        coverage: &[f32],
47        width: usize,
48        height: usize,
49        spread: f32,
50        bearing_x: i32,
51        bearing_y: i32,
52        advance_x: f32,
53    ) -> Result<Self, SdfError> {
54        if width == 0 || height == 0 {
55            return Err(SdfError::ZeroSize);
56        }
57        if coverage.len() != width * height {
58            return Err(SdfError::InvalidInput(format!(
59                "coverage length {} != width({}) * height({})",
60                coverage.len(),
61                width,
62                height
63            )));
64        }
65        // Convert f32 coverage [0, 1] → u8 coverage [0, 255].
66        let coverage_u8: Vec<u8> = coverage
67            .iter()
68            .map(|&v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
69            .collect();
70        let sdf_data = crate::edt::compute_sdf(&coverage_u8, width, height, spread, 0)?;
71        Ok(Self {
72            glyph_id,
73            width: width as u32,
74            height: height as u32,
75            data: sdf_data,
76            bearing_x,
77            bearing_y,
78            advance_x,
79        })
80    }
81}
82
83/// A UV rectangle within the atlas texture (all values in [0, 1]).
84#[derive(Clone, Debug)]
85pub struct UvRect {
86    /// Left edge (U coordinate).
87    pub u_min: f32,
88    /// Top edge (V coordinate).
89    pub v_min: f32,
90    /// Right edge (U coordinate).
91    pub u_max: f32,
92    /// Bottom edge (V coordinate).
93    pub v_max: f32,
94}
95
96// ─── Packing options and statistics ─────────────────────────────────────────
97
98/// Packing algorithm selection.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
100pub enum PackingAlgorithm {
101    /// Left-to-right shelf packing (existing behavior).
102    #[default]
103    Shelf,
104    /// Best Short-Side Fit (BSSF) MaxRects algorithm.
105    MaxRects,
106    /// Skyline algorithm: tracks a fill-level skyline for better utilization.
107    Skyline,
108}
109
110/// Options for atlas packing.
111#[derive(Debug, Clone)]
112pub struct AtlasOptions {
113    /// Atlas texture size in pixels (width and height).
114    pub atlas_size: u32,
115    /// Pixel padding between tiles.
116    pub padding: u32,
117    /// Optional maximum atlas size (used for dynamic growth).
118    pub max_size: Option<u32>,
119    /// Bin-packing algorithm to use.
120    pub algorithm: PackingAlgorithm,
121}
122
123impl Default for AtlasOptions {
124    fn default() -> Self {
125        Self {
126            atlas_size: 512,
127            padding: 1,
128            max_size: None,
129            algorithm: PackingAlgorithm::Shelf,
130        }
131    }
132}
133
134/// Statistics returned from an atlas packing operation.
135#[derive(Debug, Clone, Default)]
136pub struct AtlasStats {
137    /// Number of tiles successfully packed.
138    pub tiles_packed: usize,
139    /// Number of tiles that did not fit and were dropped.
140    pub tiles_dropped: usize,
141    /// Fraction of the atlas area that is occupied (0.0 – 1.0).
142    pub utilization: f32,
143    /// Number of unused pixels (total atlas area minus packed tile pixels).
144    pub wasted_pixels: u32,
145}
146
147// ─── Internal packing result ──────────────────────────────────────────────────
148
149struct PackResult {
150    texture: Vec<u8>,
151    uv_map: HashMap<u16, UvRect>,
152    /// Number of tiles that were dropped (overflow).
153    dropped: usize,
154    /// Sum of packed tile pixel areas (width × height for each placed tile).
155    used_pixels: u32,
156    /// Shelf state for incremental `add_tile` calls (only meaningful for Shelf algorithm).
157    cursor_x: u32,
158    shelf_y: u32,
159    shelf_max_h: u32,
160}
161
162// ─── Shelf packing ────────────────────────────────────────────────────────────
163
164/// Core shelf-packing routine shared by `SdfAtlas::pack` and `SdfAtlas::pack_with_options`.
165///
166/// `atlas_w`/`atlas_h` are the final dimensions of the texture.
167/// `padding` adds a gap (in pixels) between tiles both horizontally and vertically.
168fn pack_inner(tiles: &[SdfTile], atlas_w: u32, atlas_h: u32, padding: u32) -> PackResult {
169    let tex_len = atlas_w as usize * atlas_h as usize;
170    let mut texture = vec![0u8; tex_len];
171    let mut uv_map: HashMap<u16, UvRect> = HashMap::new();
172
173    let mut cx: u32 = padding;
174    let mut cy: u32 = padding;
175    let mut row_h: u32 = 0;
176    let mut dropped: usize = 0;
177    let mut used_pixels: u32 = 0;
178
179    for tile in tiles {
180        let needed_w = tile.width + padding;
181        // Try to start a new shelf if the tile doesn't fit horizontally.
182        if cx + tile.width > atlas_w.saturating_sub(padding) {
183            cx = padding;
184            cy += row_h + padding;
185            row_h = 0;
186        }
187
188        // Drop tiles that overflow the atlas height.
189        if cy + tile.height > atlas_h.saturating_sub(padding) {
190            dropped += 1;
191            continue;
192        }
193
194        // Blit single-channel tile data into the atlas.
195        for y in 0..tile.height {
196            for x in 0..tile.width {
197                let src_idx = (y * tile.width + x) as usize;
198                let dst_idx = ((cy + y) * atlas_w + (cx + x)) as usize;
199                if dst_idx < texture.len() && src_idx < tile.data.len() {
200                    texture[dst_idx] = tile.data[src_idx];
201                }
202            }
203        }
204
205        uv_map.insert(
206            tile.glyph_id,
207            UvRect {
208                u_min: cx as f32 / atlas_w as f32,
209                v_min: cy as f32 / atlas_h as f32,
210                u_max: (cx + tile.width) as f32 / atlas_w as f32,
211                v_max: (cy + tile.height) as f32 / atlas_h as f32,
212            },
213        );
214
215        used_pixels += tile.width * tile.height;
216        cx += needed_w;
217        row_h = row_h.max(tile.height);
218    }
219
220    PackResult {
221        texture,
222        uv_map,
223        dropped,
224        used_pixels,
225        cursor_x: cx,
226        shelf_y: cy,
227        shelf_max_h: row_h,
228    }
229}
230
231// ─── MaxRects packing ─────────────────────────────────────────────────────────
232
233/// Axis-aligned rectangle used internally by the MaxRects packer.
234#[derive(Clone, Copy, Debug)]
235struct Rect {
236    x: u32,
237    y: u32,
238    w: u32,
239    h: u32,
240}
241
242impl Rect {
243    /// Returns `true` if `other` is entirely contained within `self`.
244    fn contains(&self, other: &Rect) -> bool {
245        other.x >= self.x
246            && other.y >= self.y
247            && other.x + other.w <= self.x + self.w
248            && other.y + other.h <= self.y + self.h
249    }
250}
251
252/// Mutable state for the MaxRects packer.
253struct MaxRectsState {
254    free_rects: Vec<Rect>,
255}
256
257impl MaxRectsState {
258    /// Create state initialised with a single free rect covering the whole atlas.
259    fn new(atlas_w: u32, atlas_h: u32) -> Self {
260        Self {
261            free_rects: vec![Rect {
262                x: 0,
263                y: 0,
264                w: atlas_w,
265                h: atlas_h,
266            }],
267        }
268    }
269}
270
271/// Attempt to insert a tile of `tile_w × tile_h` (plus padding on each side)
272/// into the MaxRects state.
273///
274/// Uses the Best Short-Side Fit (BSSF) heuristic: among all free rectangles
275/// large enough to hold the tile, choose the one where
276/// `min(free.w - tile_w, free.h - tile_h)` is smallest (i.e. wastes the least
277/// space along the shorter axis).
278///
279/// Returns the placed [`Rect`] on success, or `None` when no free rect is large
280/// enough.
281fn insert_maxrects(
282    state: &mut MaxRectsState,
283    tile_w: u32,
284    tile_h: u32,
285    padding: u32,
286) -> Option<Rect> {
287    // The region we actually need (tile + padding on every side).
288    let needed_w = tile_w + padding;
289    let needed_h = tile_h + padding;
290
291    // Find the free rect that minimises the "short side fit" score.
292    let best_idx = state
293        .free_rects
294        .iter()
295        .enumerate()
296        .filter(|(_, r)| r.w >= needed_w && r.h >= needed_h)
297        .min_by_key(|(_, r)| {
298            let leftover_w = r.w.saturating_sub(needed_w);
299            let leftover_h = r.h.saturating_sub(needed_h);
300            leftover_w.min(leftover_h)
301        })
302        .map(|(i, _)| i);
303
304    let idx = best_idx?;
305    let chosen = state.free_rects[idx];
306
307    // The placed rect occupies only the tile pixels (no padding baked in).
308    let placed = Rect {
309        x: chosen.x,
310        y: chosen.y,
311        w: tile_w,
312        h: tile_h,
313    };
314
315    // Split the chosen free rect into up to 4 new free rects around the
316    // placed tile (with padding absorbed into the split).  We generate the
317    // classic two-split variants (horizontal and vertical) and keep all four
318    // non-degenerate results.
319    let mut new_rects: Vec<Rect> = Vec::with_capacity(4);
320
321    // Right of placed tile (+ padding)
322    if chosen.x + chosen.w > placed.x + needed_w {
323        let nr = Rect {
324            x: placed.x + needed_w,
325            y: chosen.y,
326            w: chosen.x + chosen.w - (placed.x + needed_w),
327            h: chosen.h,
328        };
329        if nr.w > 0 && nr.h > 0 {
330            new_rects.push(nr);
331        }
332    }
333    // Below placed tile (+ padding)
334    if chosen.y + chosen.h > placed.y + needed_h {
335        let nr = Rect {
336            x: chosen.x,
337            y: placed.y + needed_h,
338            w: chosen.w,
339            h: chosen.y + chosen.h - (placed.y + needed_h),
340        };
341        if nr.w > 0 && nr.h > 0 {
342            new_rects.push(nr);
343        }
344    }
345    // Left of placed tile (padding from atlas edge)
346    if placed.x > chosen.x {
347        let nr = Rect {
348            x: chosen.x,
349            y: chosen.y,
350            w: placed.x - chosen.x,
351            h: chosen.h,
352        };
353        if nr.w > 0 && nr.h > 0 {
354            new_rects.push(nr);
355        }
356    }
357    // Above placed tile (padding from atlas edge)
358    if placed.y > chosen.y {
359        let nr = Rect {
360            x: chosen.x,
361            y: chosen.y,
362            w: chosen.w,
363            h: placed.y - chosen.y,
364        };
365        if nr.w > 0 && nr.h > 0 {
366            new_rects.push(nr);
367        }
368    }
369
370    // Remove the chosen rect (it is now consumed) and append the new splits.
371    state.free_rects.remove(idx);
372    state.free_rects.extend_from_slice(&new_rects);
373
374    // Prune free rects that are fully contained inside another free rect.
375    // We do a pairwise check — this is O(n²) but n stays small in practice.
376    let mut to_remove: Vec<bool> = vec![false; state.free_rects.len()];
377    let len = state.free_rects.len();
378    for i in 0..len {
379        if to_remove[i] {
380            continue;
381        }
382        for j in 0..len {
383            if i == j || to_remove[j] {
384                continue;
385            }
386            if state.free_rects[j].contains(&state.free_rects[i]) {
387                to_remove[i] = true;
388                break;
389            }
390        }
391    }
392    let mut keep_idx = 0;
393    state.free_rects.retain(|_| {
394        let keep = !to_remove[keep_idx];
395        keep_idx += 1;
396        keep
397    });
398
399    Some(placed)
400}
401
402/// MaxRects packing routine.
403fn pack_inner_maxrects(tiles: &[SdfTile], atlas_w: u32, atlas_h: u32, padding: u32) -> PackResult {
404    let tex_len = atlas_w as usize * atlas_h as usize;
405    let mut texture = vec![0u8; tex_len];
406    let mut uv_map: HashMap<u16, UvRect> = HashMap::new();
407    let mut dropped: usize = 0;
408    let mut used_pixels: u32 = 0;
409
410    let mut state = MaxRectsState::new(atlas_w, atlas_h);
411
412    for tile in tiles {
413        match insert_maxrects(&mut state, tile.width, tile.height, padding) {
414            None => {
415                dropped += 1;
416            }
417            Some(placed) => {
418                // Blit tile data into the atlas texture.
419                for y in 0..tile.height {
420                    for x in 0..tile.width {
421                        let src_idx = (y * tile.width + x) as usize;
422                        let dst_idx = ((placed.y + y) * atlas_w + (placed.x + x)) as usize;
423                        if dst_idx < texture.len() && src_idx < tile.data.len() {
424                            texture[dst_idx] = tile.data[src_idx];
425                        }
426                    }
427                }
428
429                uv_map.insert(
430                    tile.glyph_id,
431                    UvRect {
432                        u_min: placed.x as f32 / atlas_w as f32,
433                        v_min: placed.y as f32 / atlas_h as f32,
434                        u_max: (placed.x + placed.w) as f32 / atlas_w as f32,
435                        v_max: (placed.y + placed.h) as f32 / atlas_h as f32,
436                    },
437                );
438
439                used_pixels += tile.width * tile.height;
440            }
441        }
442    }
443
444    PackResult {
445        texture,
446        uv_map,
447        dropped,
448        used_pixels,
449        cursor_x: 0,
450        shelf_y: 0,
451        shelf_max_h: 0,
452    }
453}
454
455// ─── Skyline packing ──────────────────────────────────────────────────────────
456
457/// A single segment of the skyline, tracking the current height at a horizontal range.
458struct SkylineSegment {
459    /// Left-most x coordinate of this segment.
460    x: u32,
461    /// Current fill height at this x range (top = highest occupied y + 1).
462    y_top: u32,
463    /// Width of this segment.
464    width: u32,
465}
466
467/// Mutable state for the Skyline packer.
468struct SkylineState {
469    segments: Vec<SkylineSegment>,
470    atlas_w: u32,
471    atlas_h: u32,
472}
473
474impl SkylineState {
475    /// Initialise with a single flat skyline at y = 0.
476    fn new(atlas_w: u32, atlas_h: u32) -> Self {
477        Self {
478            segments: vec![SkylineSegment {
479                x: 0,
480                y_top: 0,
481                width: atlas_w,
482            }],
483            atlas_w,
484            atlas_h,
485        }
486    }
487}
488
489/// Attempt to insert a tile of `tile_w × tile_h` (plus inter-tile `padding`)
490/// using the Skyline best-fit heuristic.
491///
492/// Returns `Some((x, y))` (top-left corner of the placed tile, in pixel coords)
493/// or `None` when no position is feasible.
494fn insert_skyline(
495    state: &mut SkylineState,
496    tile_w: u32,
497    tile_h: u32,
498    padding: u32,
499) -> Option<(u32, u32)> {
500    let needed_w = tile_w + padding;
501    let n = state.segments.len();
502
503    // For each starting segment, check whether a run of segments wide enough
504    // to hold `needed_w` exists and is below the atlas ceiling.
505    let mut best_y = u32::MAX;
506    let mut best_x = 0u32;
507
508    'outer: for i in 0..n {
509        let x_start = state.segments[i].x;
510        // Tile must not start past the atlas right edge (minus a padding gap).
511        if x_start + needed_w > state.atlas_w.saturating_sub(padding) {
512            continue;
513        }
514
515        let mut max_y = 0u32;
516        let mut covered_w = 0u32;
517
518        for j in i..n {
519            let seg = &state.segments[j];
520            max_y = max_y.max(seg.y_top);
521            covered_w += seg.width;
522
523            if covered_w >= needed_w {
524                // Verify it fits vertically.
525                if max_y + tile_h + padding <= state.atlas_h && max_y < best_y {
526                    best_y = max_y;
527                    best_x = x_start;
528                }
529                continue 'outer;
530            }
531        }
532    }
533
534    if best_y == u32::MAX {
535        return None; // No valid placement found.
536    }
537
538    let place_x = best_x;
539    let place_y = best_y;
540    let new_top = best_y + tile_h + padding;
541    let tile_right = place_x + needed_w;
542
543    // Build the updated skyline: replace all segments that the tile spans with
544    // a single segment at the new height.
545    let mut new_segments: Vec<SkylineSegment> = Vec::with_capacity(state.segments.len() + 2);
546
547    for seg in &state.segments {
548        let seg_right = seg.x + seg.width;
549        if seg_right <= place_x || seg.x >= tile_right {
550            // Segment is entirely outside the placed tile's horizontal span.
551            new_segments.push(SkylineSegment {
552                x: seg.x,
553                y_top: seg.y_top,
554                width: seg.width,
555            });
556        } else {
557            // This segment overlaps the tile.  Emit left/right remnants (if any)
558            // and a new raised segment at `new_top`.
559            // Left remnant
560            if seg.x < place_x {
561                new_segments.push(SkylineSegment {
562                    x: seg.x,
563                    y_top: seg.y_top,
564                    width: place_x - seg.x,
565                });
566            }
567        }
568    }
569
570    // Insert the new raised segment for the placed tile.
571    new_segments.push(SkylineSegment {
572        x: place_x,
573        y_top: new_top,
574        width: needed_w,
575    });
576
577    // Emit right remnants of partially-covered segments.
578    for seg in &state.segments {
579        let seg_right = seg.x + seg.width;
580        if seg.x < tile_right && seg_right > tile_right {
581            new_segments.push(SkylineSegment {
582                x: tile_right,
583                y_top: seg.y_top,
584                width: seg_right - tile_right,
585            });
586        }
587    }
588
589    // Sort by x to maintain the invariant.
590    new_segments.sort_by_key(|s| s.x);
591
592    // Merge adjacent segments at the same height.
593    let mut merged: Vec<SkylineSegment> = Vec::with_capacity(new_segments.len());
594    for seg in new_segments {
595        if let Some(last) = merged.last_mut() {
596            if last.y_top == seg.y_top && last.x + last.width == seg.x {
597                last.width += seg.width;
598                continue;
599            }
600        }
601        merged.push(seg);
602    }
603
604    state.segments = merged;
605    Some((place_x, place_y))
606}
607
608/// Skyline packing routine.
609fn pack_inner_skyline(tiles: &[SdfTile], atlas_w: u32, atlas_h: u32, padding: u32) -> PackResult {
610    let tex_len = atlas_w as usize * atlas_h as usize;
611    let mut texture = vec![0u8; tex_len];
612    let mut uv_map: HashMap<u16, UvRect> = HashMap::new();
613    let mut dropped: usize = 0;
614    let mut used_pixels: u32 = 0;
615
616    let mut state = SkylineState::new(atlas_w, atlas_h);
617
618    for tile in tiles {
619        match insert_skyline(&mut state, tile.width, tile.height, padding) {
620            None => {
621                dropped += 1;
622            }
623            Some((px, py)) => {
624                for y in 0..tile.height {
625                    for x in 0..tile.width {
626                        let src_idx = (y * tile.width + x) as usize;
627                        let dst_idx = ((py + y) * atlas_w + (px + x)) as usize;
628                        if dst_idx < texture.len() && src_idx < tile.data.len() {
629                            texture[dst_idx] = tile.data[src_idx];
630                        }
631                    }
632                }
633
634                uv_map.insert(
635                    tile.glyph_id,
636                    UvRect {
637                        u_min: px as f32 / atlas_w as f32,
638                        v_min: py as f32 / atlas_h as f32,
639                        u_max: (px + tile.width) as f32 / atlas_w as f32,
640                        v_max: (py + tile.height) as f32 / atlas_h as f32,
641                    },
642                );
643
644                used_pixels += tile.width * tile.height;
645            }
646        }
647    }
648
649    PackResult {
650        texture,
651        uv_map,
652        dropped,
653        used_pixels,
654        cursor_x: 0,
655        shelf_y: 0,
656        shelf_max_h: 0,
657    }
658}
659
660// ─── Multi-page atlas ─────────────────────────────────────────────────────────
661
662/// An atlas that spans multiple pages when a single texture is not large enough.
663///
664/// Each page is an independent [`SdfAtlas`] of the same `page_size`.  Tiles are
665/// distributed greedily: when the current page is full a new one is opened.
666pub struct MultiPageAtlas {
667    /// Individual atlas pages (same dimensions, different tiles).
668    pub pages: Vec<SdfAtlas>,
669    /// Pixel size of each page (width == height).
670    pub page_size: u32,
671}
672
673impl MultiPageAtlas {
674    /// Pack `tiles` across as many pages as necessary.
675    ///
676    /// Tiles that are too large to fit a fresh page on their own are silently
677    /// dropped to avoid an infinite loop.  All other tiles are packed with the
678    /// Shelf algorithm.
679    pub fn pack(tiles: &[SdfTile], page_size: u32, padding: u32) -> Self {
680        let mut pages: Vec<SdfAtlas> = Vec::new();
681        let atlas_size = page_size.next_power_of_two().max(64);
682        // Work with an owned queue so we can mutate between iterations.
683        let mut queue: Vec<SdfTile> = tiles.to_vec();
684
685        while !queue.is_empty() {
686            let tex_len = atlas_size as usize * atlas_size as usize;
687            let mut page = SdfAtlas {
688                width: atlas_size,
689                height: atlas_size,
690                texture: vec![0u8; tex_len],
691                uv_map: HashMap::new(),
692                cursor_x: padding,
693                shelf_y: padding,
694                shelf_max_h: 0,
695                padding,
696            };
697
698            let mut any_packed = false;
699            let mut leftover: Vec<SdfTile> = Vec::new();
700
701            for tile in &queue {
702                // Tiles that would never fit in any page are permanently dropped.
703                if tile.width + 2 * padding > atlas_size || tile.height + 2 * padding > atlas_size {
704                    continue;
705                }
706                match page.add_tile(tile) {
707                    Some(_) => {
708                        any_packed = true;
709                    }
710                    None => {
711                        leftover.push(tile.clone());
712                    }
713                }
714            }
715
716            pages.push(page);
717
718            if !any_packed {
719                // Nothing fit even on a fresh page — avoid an infinite loop.
720                break;
721            }
722
723            queue = leftover;
724        }
725
726        Self { pages, page_size }
727    }
728
729    /// Find the UV rect for a glyph across all pages.
730    ///
731    /// Returns `(page_index, &UvRect)` if found, or `None`.
732    pub fn lookup(&self, glyph_id: u16) -> Option<(usize, &UvRect)> {
733        self.pages
734            .iter()
735            .enumerate()
736            .find_map(|(i, page)| page.uv_map.get(&glyph_id).map(|uv| (i, uv)))
737    }
738}
739
740// ─── Dynamic atlas growth ─────────────────────────────────────────────────────
741
742/// Pack tiles into an atlas that grows automatically until all tiles fit or
743/// `max_size` is reached.
744///
745/// # Algorithm
746/// 1. Start with `current_size = initial_size`.
747/// 2. Call [`SdfAtlas::pack_with_options`] with the given `algorithm`.
748/// 3. If any tiles were dropped *and* `current_size < max_size`, double
749///    `current_size` (capped at `max_size`) and retry.
750/// 4. Return the final atlas and statistics once either all tiles fit or
751///    `max_size` is reached.
752pub fn pack_growing(
753    tiles: &[SdfTile],
754    initial_size: u32,
755    max_size: u32,
756    padding: u32,
757    algorithm: PackingAlgorithm,
758) -> (SdfAtlas, AtlasStats) {
759    let mut current_size = initial_size.max(1);
760    loop {
761        let options = AtlasOptions {
762            atlas_size: current_size,
763            padding,
764            max_size: Some(max_size),
765            algorithm,
766        };
767        let (atlas, stats) = SdfAtlas::pack_with_options(tiles, &options);
768        if stats.tiles_dropped == 0 || current_size >= max_size {
769            return (atlas, stats);
770        }
771        // Double the atlas size, capped at max_size.
772        current_size = (current_size.saturating_mul(2)).min(max_size);
773    }
774}
775
776// ─── SdfAtlas ─────────────────────────────────────────────────────────────────
777
778/// A packed atlas of SDF glyph tiles, ready for GPU upload.
779///
780/// The atlas texture is stored as a single-channel byte slice (`texture`),
781/// where each byte is an SDF value (< 128 = outside, ≈ 128 = outline, > 128 = inside).
782///
783/// Shelf state (`cursor_x`, `shelf_y`, `shelf_max_h`) is maintained for incremental
784/// tile insertion via [`SdfAtlas::add_tile`].
785pub struct SdfAtlas {
786    /// Atlas width in pixels.
787    pub width: u32,
788    /// Atlas height in pixels.
789    pub height: u32,
790    /// Raw single-channel texture data (`width × height` bytes).
791    pub texture: Vec<u8>,
792    /// UV coordinates for each glyph ID.
793    pub uv_map: HashMap<u16, UvRect>,
794    /// Current X cursor position for the active shelf.
795    cursor_x: u32,
796    /// Y coordinate of the current shelf baseline.
797    shelf_y: u32,
798    /// Tallest tile seen in the current shelf row.
799    shelf_max_h: u32,
800    /// Inter-tile padding applied during packing.
801    padding: u32,
802}
803
804// ─── Binary serialization constants ──────────────────────────────────────────
805
806const MAGIC: &[u8; 4] = b"SDFA";
807const VERSION: u32 = 1;
808/// Bytes per entry in the binary format.
809const ENTRY_SIZE: usize = 28;
810/// Byte offset where entries start.
811const ENTRIES_OFFSET: usize = 20;
812
813impl SdfAtlas {
814    /// Create a blank atlas of the given dimensions, pre-allocated with zeroed texture data.
815    ///
816    /// The texture is zero-filled (`width × height` bytes).
817    /// Shelf state is reset to the origin (no padding).
818    pub fn new(width: u32, height: u32) -> Self {
819        let capacity = width as usize * height as usize;
820        Self {
821            width,
822            height,
823            texture: vec![0u8; capacity],
824            uv_map: HashMap::new(),
825            cursor_x: 0,
826            shelf_y: 0,
827            shelf_max_h: 0,
828            padding: 0,
829        }
830    }
831
832    /// Pre-allocate texture capacity for a known number of tiles.
833    ///
834    /// Uses `average_tile_area` as the estimated pixels per tile when reserving
835    /// additional texture capacity beyond what `new` already allocates.
836    ///
837    /// The actual texture length stays at `width × height`; only the internal
838    /// Vec capacity is grown, so no extra zeroing cost is paid for unused space.
839    pub fn with_capacity(
840        width: u32,
841        height: u32,
842        tile_count_hint: usize,
843        average_tile_area: usize,
844    ) -> Self {
845        let capacity = (tile_count_hint * average_tile_area).min((width * height) as usize);
846        let mut atlas = Self::new(width, height);
847        atlas
848            .texture
849            .reserve(capacity.saturating_sub(atlas.texture.len()));
850        atlas
851    }
852
853    /// Pack a set of SDF tiles into a power-of-2 atlas texture using a shelf-packing algorithm.
854    ///
855    /// Tiles are packed left-to-right. When a row is full a new shelf begins below.
856    /// Atlas dimensions are rounded up to the next power of two (minimum 256 × 256).
857    ///
858    /// If `tiles` is empty, returns a minimal 1×1 atlas.
859    pub fn pack(tiles: &[SdfTile]) -> Self {
860        if tiles.is_empty() {
861            return Self {
862                width: 1,
863                height: 1,
864                texture: vec![0],
865                uv_map: HashMap::new(),
866                cursor_x: 0,
867                shelf_y: 0,
868                shelf_max_h: 0,
869                padding: 0,
870            };
871        }
872
873        // Estimate atlas dimensions using a square grid layout.
874        let tile_w = tiles[0].width;
875        let tile_h = tiles[0].height;
876        let count = tiles.len() as u32;
877        let cols = (count as f32).sqrt().ceil() as u32;
878        let rows = count.div_ceil(cols);
879        let atlas_w = (cols * tile_w).next_power_of_two().max(256);
880        let atlas_h = (rows * tile_h).next_power_of_two().max(256);
881
882        let res = pack_inner(tiles, atlas_w, atlas_h, 0);
883
884        Self {
885            width: atlas_w,
886            height: atlas_h,
887            texture: res.texture,
888            uv_map: res.uv_map,
889            cursor_x: res.cursor_x,
890            shelf_y: res.shelf_y,
891            shelf_max_h: res.shelf_max_h,
892            padding: 0,
893        }
894    }
895
896    /// Pack tiles with configurable options, returning both the atlas and packing statistics.
897    ///
898    /// Respects [`AtlasOptions::padding`] to add spacing between tiles. The atlas size is
899    /// fixed at [`AtlasOptions::atlas_size`] (rounded up to the next power of two).
900    ///
901    /// Tiles that do not fit are counted in [`AtlasStats::tiles_dropped`].
902    pub fn pack_with_options(tiles: &[SdfTile], options: &AtlasOptions) -> (Self, AtlasStats) {
903        let atlas_size = options.atlas_size.next_power_of_two().max(64);
904
905        if tiles.is_empty() {
906            let atlas = Self {
907                width: atlas_size,
908                height: atlas_size,
909                texture: vec![0u8; (atlas_size as usize) * (atlas_size as usize)],
910                uv_map: HashMap::new(),
911                cursor_x: options.padding,
912                shelf_y: options.padding,
913                shelf_max_h: 0,
914                padding: options.padding,
915            };
916            return (
917                atlas,
918                AtlasStats {
919                    tiles_packed: 0,
920                    tiles_dropped: 0,
921                    utilization: 0.0,
922                    wasted_pixels: atlas_size * atlas_size,
923                },
924            );
925        }
926
927        let res = match options.algorithm {
928            PackingAlgorithm::Shelf => pack_inner(tiles, atlas_size, atlas_size, options.padding),
929            PackingAlgorithm::MaxRects => {
930                pack_inner_maxrects(tiles, atlas_size, atlas_size, options.padding)
931            }
932            PackingAlgorithm::Skyline => {
933                pack_inner_skyline(tiles, atlas_size, atlas_size, options.padding)
934            }
935        };
936
937        let total = atlas_size * atlas_size;
938        let tiles_packed = tiles.len() - res.dropped;
939        let utilization = res.used_pixels as f32 / total as f32;
940        let wasted_pixels = total.saturating_sub(res.used_pixels);
941
942        let stats = AtlasStats {
943            tiles_packed,
944            tiles_dropped: res.dropped,
945            utilization,
946            wasted_pixels,
947        };
948
949        let atlas = Self {
950            width: atlas_size,
951            height: atlas_size,
952            texture: res.texture,
953            uv_map: res.uv_map,
954            cursor_x: res.cursor_x,
955            shelf_y: res.shelf_y,
956            shelf_max_h: res.shelf_max_h,
957            padding: options.padding,
958        };
959
960        (atlas, stats)
961    }
962
963    /// Pack tiles into an atlas that doubles in size until all tiles fit or
964    /// `max_size` is reached, using the default Shelf algorithm.
965    ///
966    /// This is a convenience wrapper around the free function [`pack_growing`].
967    pub fn pack_growing(tiles: &[SdfTile], initial_size: u32, max_size: u32) -> (Self, AtlasStats) {
968        pack_growing(tiles, initial_size, max_size, 1, PackingAlgorithm::Shelf)
969    }
970
971    /// Incrementally add a single tile to the atlas without a full repack.
972    ///
973    /// Returns the [`UvRect`] of the inserted tile, or `None` if the atlas is full.
974    ///
975    /// Shelf state (`cursor_x`, `shelf_y`, `shelf_max_h`) is updated in-place so that
976    /// subsequent calls continue packing from where the last one left off.
977    pub fn add_tile(&mut self, tile: &SdfTile) -> Option<UvRect> {
978        let pad = self.padding;
979        let atlas_w = self.width;
980        let atlas_h = self.height;
981
982        // Try to start a new shelf if the tile doesn't fit horizontally.
983        if self.cursor_x + tile.width > atlas_w.saturating_sub(pad) {
984            self.cursor_x = pad;
985            self.shelf_y += self.shelf_max_h + pad;
986            self.shelf_max_h = 0;
987        }
988
989        // Return None if the tile would overflow the atlas height.
990        if self.shelf_y + tile.height > atlas_h.saturating_sub(pad) {
991            return None;
992        }
993
994        let cx = self.cursor_x;
995        let cy = self.shelf_y;
996
997        // Blit tile data into the existing texture.
998        for y in 0..tile.height {
999            for x in 0..tile.width {
1000                let src_idx = (y * tile.width + x) as usize;
1001                let dst_idx = ((cy + y) * atlas_w + (cx + x)) as usize;
1002                if dst_idx < self.texture.len() && src_idx < tile.data.len() {
1003                    self.texture[dst_idx] = tile.data[src_idx];
1004                }
1005            }
1006        }
1007
1008        let uv = UvRect {
1009            u_min: cx as f32 / atlas_w as f32,
1010            v_min: cy as f32 / atlas_h as f32,
1011            u_max: (cx + tile.width) as f32 / atlas_w as f32,
1012            v_max: (cy + tile.height) as f32 / atlas_h as f32,
1013        };
1014
1015        self.uv_map.insert(tile.glyph_id, uv.clone());
1016        self.cursor_x += tile.width + pad;
1017        self.shelf_max_h = self.shelf_max_h.max(tile.height);
1018
1019        Some(uv)
1020    }
1021
1022    /// Soft-delete a tile from the atlas by glyph ID.
1023    ///
1024    /// Removes the UV mapping so the glyph will no longer be found in the atlas.
1025    /// The texture pixels are *not* cleared or reclaimed; the space is "wasted"
1026    /// until a full repack is performed.
1027    ///
1028    /// Returns `true` if the glyph was present and has been removed.
1029    pub fn remove_tile(&mut self, glyph_id: u16) -> bool {
1030        self.uv_map.remove(&glyph_id).is_some()
1031    }
1032
1033    // ─── Binary serialization ─────────────────────────────────────────────────
1034
1035    /// Serialise the atlas to a compact binary representation.
1036    ///
1037    /// # Format (little-endian throughout)
1038    /// ```text
1039    /// [0..4]   magic:       b"SDFA"
1040    /// [4..8]   version:     u32 = 1
1041    /// [8..12]  atlas_w:     u32
1042    /// [12..16] atlas_h:     u32
1043    /// [16..20] num_entries: u32
1044    /// [20 + i*28 .. 20 + (i+1)*28] per entry:
1045    ///     glyph_id:   u16  (2 bytes)
1046    ///     _pad:       u16  (2 bytes, zeroed)
1047    ///     uv_u_min:   u32  (f32 bits)
1048    ///     uv_v_min:   u32  (f32 bits)
1049    ///     uv_u_max:   u32  (f32 bits)
1050    ///     uv_v_max:   u32  (f32 bits)
1051    ///     _reserved:  u64  (8 bytes, zeroed)
1052    /// [20 + num_entries*28 ..] texture bytes (atlas_w * atlas_h bytes)
1053    /// ```
1054    pub fn to_bytes(&self) -> Vec<u8> {
1055        let num_entries = self.uv_map.len() as u32;
1056        let texture_len = self.width as usize * self.height as usize;
1057        let total = ENTRIES_OFFSET + num_entries as usize * ENTRY_SIZE + texture_len;
1058        let mut buf = Vec::with_capacity(total);
1059
1060        // Header
1061        buf.extend_from_slice(MAGIC);
1062        buf.extend_from_slice(&VERSION.to_le_bytes());
1063        buf.extend_from_slice(&self.width.to_le_bytes());
1064        buf.extend_from_slice(&self.height.to_le_bytes());
1065        buf.extend_from_slice(&num_entries.to_le_bytes());
1066
1067        // Entries — sort by glyph_id for deterministic output.
1068        let mut entries: Vec<(&u16, &UvRect)> = self.uv_map.iter().collect();
1069        entries.sort_by_key(|(gid, _)| *gid);
1070
1071        for (glyph_id, uv) in entries {
1072            buf.extend_from_slice(&glyph_id.to_le_bytes()); // 2 bytes
1073            buf.extend_from_slice(&0u16.to_le_bytes()); // 2 bytes pad
1074            buf.extend_from_slice(&uv.u_min.to_bits().to_le_bytes()); // 4 bytes
1075            buf.extend_from_slice(&uv.v_min.to_bits().to_le_bytes()); // 4 bytes
1076            buf.extend_from_slice(&uv.u_max.to_bits().to_le_bytes()); // 4 bytes
1077            buf.extend_from_slice(&uv.v_max.to_bits().to_le_bytes()); // 4 bytes
1078            buf.extend_from_slice(&0u64.to_le_bytes()); // 8 bytes reserved
1079        }
1080
1081        // Texture
1082        buf.extend_from_slice(&self.texture);
1083
1084        buf
1085    }
1086
1087    /// Deserialise an atlas from the binary format produced by [`SdfAtlas::to_bytes`].
1088    ///
1089    /// # Errors
1090    /// Returns [`SdfError::InvalidData`] if:
1091    /// - The magic bytes are wrong.
1092    /// - The version is not `1`.
1093    /// - The buffer is too short for the declared number of entries and texture.
1094    pub fn from_bytes(data: &[u8]) -> Result<Self, SdfError> {
1095        // Need at least the fixed header.
1096        if data.len() < ENTRIES_OFFSET {
1097            return Err(SdfError::InvalidData(format!(
1098                "buffer too short: need at least {ENTRIES_OFFSET} bytes, got {}",
1099                data.len()
1100            )));
1101        }
1102
1103        // Magic
1104        if &data[0..4] != MAGIC {
1105            return Err(SdfError::InvalidData(format!(
1106                "bad magic: expected {:?}, got {:?}",
1107                MAGIC,
1108                &data[0..4]
1109            )));
1110        }
1111
1112        // Version
1113        let version = u32::from_le_bytes(
1114            data[4..8]
1115                .try_into()
1116                .map_err(|_| SdfError::InvalidData("cannot read version".into()))?,
1117        );
1118        if version != VERSION {
1119            return Err(SdfError::InvalidData(format!(
1120                "unsupported version {version}, expected {VERSION}"
1121            )));
1122        }
1123
1124        let atlas_w = u32::from_le_bytes(
1125            data[8..12]
1126                .try_into()
1127                .map_err(|_| SdfError::InvalidData("cannot read atlas_w".into()))?,
1128        );
1129        let atlas_h = u32::from_le_bytes(
1130            data[12..16]
1131                .try_into()
1132                .map_err(|_| SdfError::InvalidData("cannot read atlas_h".into()))?,
1133        );
1134        let num_entries = u32::from_le_bytes(
1135            data[16..20]
1136                .try_into()
1137                .map_err(|_| SdfError::InvalidData("cannot read num_entries".into()))?,
1138        ) as usize;
1139
1140        // Validate total length.
1141        let texture_len = atlas_w as usize * atlas_h as usize;
1142        let expected_len = ENTRIES_OFFSET + num_entries * ENTRY_SIZE + texture_len;
1143        if data.len() < expected_len {
1144            return Err(SdfError::InvalidData(format!(
1145                "buffer too short: expected {expected_len} bytes, got {}",
1146                data.len()
1147            )));
1148        }
1149
1150        // Read UV map entries.
1151        let mut uv_map: HashMap<u16, UvRect> = HashMap::with_capacity(num_entries);
1152        for i in 0..num_entries {
1153            let base = ENTRIES_OFFSET + i * ENTRY_SIZE;
1154            let glyph_id = u16::from_le_bytes(
1155                data[base..base + 2]
1156                    .try_into()
1157                    .map_err(|_| SdfError::InvalidData(format!("entry {i}: bad glyph_id")))?,
1158            );
1159            // Skip 2-byte pad at base+2..base+4
1160            let u_min = f32::from_bits(u32::from_le_bytes(
1161                data[base + 4..base + 8]
1162                    .try_into()
1163                    .map_err(|_| SdfError::InvalidData(format!("entry {i}: bad u_min")))?,
1164            ));
1165            let v_min = f32::from_bits(u32::from_le_bytes(
1166                data[base + 8..base + 12]
1167                    .try_into()
1168                    .map_err(|_| SdfError::InvalidData(format!("entry {i}: bad v_min")))?,
1169            ));
1170            let u_max = f32::from_bits(u32::from_le_bytes(
1171                data[base + 12..base + 16]
1172                    .try_into()
1173                    .map_err(|_| SdfError::InvalidData(format!("entry {i}: bad u_max")))?,
1174            ));
1175            let v_max = f32::from_bits(u32::from_le_bytes(
1176                data[base + 16..base + 20]
1177                    .try_into()
1178                    .map_err(|_| SdfError::InvalidData(format!("entry {i}: bad v_max")))?,
1179            ));
1180            // Skip 8-byte reserved at base+20..base+28
1181            uv_map.insert(
1182                glyph_id,
1183                UvRect {
1184                    u_min,
1185                    v_min,
1186                    u_max,
1187                    v_max,
1188                },
1189            );
1190        }
1191
1192        // Read texture.
1193        let tex_start = ENTRIES_OFFSET + num_entries * ENTRY_SIZE;
1194        let texture = data[tex_start..tex_start + texture_len].to_vec();
1195
1196        Ok(Self {
1197            width: atlas_w,
1198            height: atlas_h,
1199            texture,
1200            uv_map,
1201            cursor_x: 0,
1202            shelf_y: 0,
1203            shelf_max_h: 0,
1204            padding: 0,
1205        })
1206    }
1207
1208    /// Construct an [`SdfAtlas`] from statically-embedded bytes, e.g. produced by `include_bytes!`.
1209    ///
1210    /// This is the companion to [`SdfAtlas::to_bytes`] for build-time atlas pre-computation.
1211    /// The `'static` annotation signals to callers that this is intended for embedded data;
1212    /// internally it delegates to [`SdfAtlas::from_bytes`] since only the parsed structure is needed.
1213    ///
1214    /// # Example (in a build-generated file)
1215    /// ```rust,ignore
1216    /// static ATLAS_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/font_atlas.bin"));
1217    /// let atlas = SdfAtlas::from_static(ATLAS_BYTES).expect("parse atlas");
1218    /// ```
1219    ///
1220    /// # Errors
1221    /// Propagates any [`SdfError::InvalidData`] returned by [`SdfAtlas::from_bytes`].
1222    pub fn from_static(data: &'static [u8]) -> Result<Self, SdfError> {
1223        Self::from_bytes(data)
1224    }
1225
1226    /// Export the atlas texture as a PNG file for visualization.
1227    ///
1228    /// Each pixel value is the SDF distance encoded as a greyscale byte
1229    /// (`0` = far outside, `128` = boundary, `255` = far inside).
1230    pub fn export_png(&self, path: &std::path::Path) -> Result<(), SdfError> {
1231        use std::io::BufWriter;
1232        let file = std::fs::File::create(path).map_err(|e| SdfError::Io(e.to_string()))?;
1233        let w = BufWriter::new(file);
1234        let mut encoder = png::Encoder::new(w, self.width, self.height);
1235        encoder.set_color(png::ColorType::Grayscale);
1236        encoder.set_depth(png::BitDepth::Eight);
1237        let mut writer = encoder
1238            .write_header()
1239            .map_err(|e| SdfError::Io(e.to_string()))?;
1240        writer
1241            .write_image_data(&self.texture)
1242            .map_err(|e| SdfError::Io(e.to_string()))
1243    }
1244}
1245
1246// ─── MSDF atlas ───────────────────────────────────────────────────────────────
1247
1248/// A packed atlas of MSDF glyph tiles, ready for GPU upload.
1249///
1250/// The atlas texture is stored as an RGB byte slice (`texture`), three bytes
1251/// per pixel. Each pixel encodes signed distances in the R, G, and B channels
1252/// for the corresponding colored edge of the glyph outline.
1253pub struct MsdfAtlas {
1254    /// Atlas width in pixels.
1255    pub width: u32,
1256    /// Atlas height in pixels.
1257    pub height: u32,
1258    /// Raw RGB texture data: `width * height * 3` bytes.
1259    pub texture: Vec<u8>,
1260    /// UV coordinates for each glyph ID within the atlas.
1261    pub uv_map: HashMap<u16, UvRect>,
1262}
1263
1264impl MsdfAtlas {
1265    /// Pack a set of MSDF tiles into a fixed-size atlas texture.
1266    ///
1267    /// Uses the same left-to-right shelf-packing algorithm as [`SdfAtlas`],
1268    /// adapted for 3-bytes-per-pixel (RGB) tiles.
1269    ///
1270    /// If `tiles` is empty, returns a minimal 1×1 atlas.
1271    pub fn pack(tiles: &[crate::msdf::MsdfTile], atlas_size: u32) -> Self {
1272        if tiles.is_empty() {
1273            return Self {
1274                width: 1,
1275                height: 1,
1276                texture: vec![0u8; 3],
1277                uv_map: HashMap::new(),
1278            };
1279        }
1280
1281        let atlas_w = atlas_size.next_power_of_two().max(64);
1282        let atlas_h = atlas_size.next_power_of_two().max(64);
1283        let tex_len = atlas_w as usize * atlas_h as usize * 3;
1284        let mut texture = vec![0u8; tex_len];
1285        let mut uv_map = HashMap::new();
1286
1287        let mut cx = 0u32;
1288        let mut cy = 0u32;
1289        let mut row_h = 0u32;
1290
1291        for tile in tiles {
1292            // Start a new shelf when the current tile would exceed atlas width.
1293            if cx + tile.width > atlas_w {
1294                cx = 0;
1295                cy += row_h;
1296                row_h = 0;
1297            }
1298
1299            // Skip tiles that would overflow the atlas height.
1300            if cy + tile.height > atlas_h {
1301                continue;
1302            }
1303
1304            // Blit 3-channel tile data into the atlas.
1305            for y in 0..tile.height {
1306                for x in 0..tile.width {
1307                    let src_base = (y * tile.width + x) as usize * 3;
1308                    let dst_base = ((cy + y) * atlas_w + (cx + x)) as usize * 3;
1309                    if dst_base + 2 < texture.len() && src_base + 2 < tile.data.len() {
1310                        texture[dst_base] = tile.data[src_base];
1311                        texture[dst_base + 1] = tile.data[src_base + 1];
1312                        texture[dst_base + 2] = tile.data[src_base + 2];
1313                    }
1314                }
1315            }
1316
1317            uv_map.insert(
1318                tile.glyph_id,
1319                UvRect {
1320                    u_min: cx as f32 / atlas_w as f32,
1321                    v_min: cy as f32 / atlas_h as f32,
1322                    u_max: (cx + tile.width) as f32 / atlas_w as f32,
1323                    v_max: (cy + tile.height) as f32 / atlas_h as f32,
1324                },
1325            );
1326
1327            cx += tile.width;
1328            row_h = row_h.max(tile.height);
1329        }
1330
1331        Self {
1332            width: atlas_w,
1333            height: atlas_h,
1334            texture,
1335            uv_map,
1336        }
1337    }
1338
1339    /// Export the atlas texture as a PNG file for visualization.
1340    ///
1341    /// The texture is stored as RGB (3 bytes per pixel). Each channel encodes
1342    /// a signed distance for the corresponding colored edge of the glyph outline.
1343    pub fn export_png(&self, path: &std::path::Path) -> Result<(), SdfError> {
1344        use std::io::BufWriter;
1345        let file = std::fs::File::create(path).map_err(|e| SdfError::Io(e.to_string()))?;
1346        let w = BufWriter::new(file);
1347        let mut encoder = png::Encoder::new(w, self.width, self.height);
1348        encoder.set_color(png::ColorType::Rgb);
1349        encoder.set_depth(png::BitDepth::Eight);
1350        let mut writer = encoder
1351            .write_header()
1352            .map_err(|e| SdfError::Io(e.to_string()))?;
1353        writer
1354            .write_image_data(&self.texture)
1355            .map_err(|e| SdfError::Io(e.to_string()))
1356    }
1357}
1358
1359#[cfg(test)]
1360mod tests {
1361    use super::*;
1362    use crate::msdf::MsdfTile;
1363
1364    fn make_sdf_tile(glyph_id: u16, w: u32, h: u32) -> SdfTile {
1365        SdfTile {
1366            glyph_id,
1367            width: w,
1368            height: h,
1369            data: vec![128u8; (w * h) as usize],
1370            bearing_x: 0,
1371            bearing_y: 0,
1372            advance_x: w as f32,
1373        }
1374    }
1375
1376    #[test]
1377    fn msdf_atlas_packs_many_tiles() {
1378        let tiles: Vec<MsdfTile> = (0..20u16)
1379            .map(|i| MsdfTile {
1380                glyph_id: i,
1381                width: 16,
1382                height: 16,
1383                data: vec![128u8; 16 * 16 * 3],
1384                bearing_x: 0.0,
1385                bearing_y: 0.0,
1386                advance_x: 16.0,
1387            })
1388            .collect();
1389        let atlas = MsdfAtlas::pack(&tiles, 256);
1390        assert_eq!(atlas.uv_map.len(), 20);
1391        for uv in atlas.uv_map.values() {
1392            assert!(uv.u_min >= 0.0 && uv.u_max <= 1.0);
1393            assert!(uv.v_min >= 0.0 && uv.v_max <= 1.0);
1394        }
1395    }
1396
1397    #[test]
1398    fn pack_with_options_returns_stats() {
1399        let tiles: Vec<SdfTile> = (0..10u16).map(|i| make_sdf_tile(i, 16, 16)).collect();
1400        let opts = AtlasOptions {
1401            atlas_size: 128,
1402            padding: 1,
1403            ..Default::default()
1404        };
1405        let (atlas, stats) = SdfAtlas::pack_with_options(&tiles, &opts);
1406        assert_eq!(stats.tiles_packed + stats.tiles_dropped, 10);
1407        assert!(
1408            stats.utilization > 0.0 && stats.utilization <= 1.0,
1409            "utilization out of range: {}",
1410            stats.utilization
1411        );
1412        assert_eq!(atlas.uv_map.len(), stats.tiles_packed);
1413    }
1414
1415    #[test]
1416    fn remove_tile_removes_from_map() {
1417        let tiles = vec![make_sdf_tile(42, 16, 16)];
1418        let mut atlas = SdfAtlas::pack(&tiles);
1419        assert!(atlas.uv_map.contains_key(&42));
1420        assert!(atlas.remove_tile(42));
1421        assert!(!atlas.uv_map.contains_key(&42));
1422        // Second remove must return false.
1423        assert!(!atlas.remove_tile(42));
1424    }
1425
1426    #[test]
1427    fn add_tile_places_new_tile() {
1428        let tiles: Vec<SdfTile> = (0..4u16).map(|i| make_sdf_tile(i, 16, 16)).collect();
1429        let opts = AtlasOptions {
1430            atlas_size: 128,
1431            padding: 0,
1432            ..Default::default()
1433        };
1434        let (mut atlas, _) = SdfAtlas::pack_with_options(&tiles, &opts);
1435        let new_tile = make_sdf_tile(99, 16, 16);
1436        let uv = atlas.add_tile(&new_tile);
1437        assert!(uv.is_some(), "expected tile to be placed");
1438        assert!(atlas.uv_map.contains_key(&99));
1439    }
1440
1441    #[test]
1442    fn from_coverage_basic() {
1443        // Fully-filled 8×8 square: SDF should have center > 128.
1444        let coverage = vec![1.0f32; 8 * 8];
1445        let tile =
1446            SdfTile::from_coverage(7, &coverage, 8, 8, 4.0, 0, 0, 8.0).expect("from_coverage");
1447        assert_eq!(tile.glyph_id, 7);
1448        assert_eq!(tile.width, 8);
1449        assert_eq!(tile.height, 8);
1450        let center = tile.data[4 * 8 + 4];
1451        assert!(
1452            center > 128,
1453            "center of solid square should be inside, got {center}"
1454        );
1455    }
1456
1457    #[test]
1458    fn from_coverage_zero_size_errors() {
1459        let cov = vec![1.0f32; 0];
1460        assert!(SdfTile::from_coverage(0, &cov, 0, 8, 4.0, 0, 0, 0.0).is_err());
1461        assert!(SdfTile::from_coverage(0, &cov, 8, 0, 4.0, 0, 0, 0.0).is_err());
1462    }
1463
1464    #[test]
1465    fn test_maxrects_non_overlapping() {
1466        let tiles: Vec<SdfTile> = (0..20u16)
1467            .map(|id| SdfTile {
1468                glyph_id: id,
1469                width: 16,
1470                height: 16,
1471                data: vec![128u8; 256],
1472                bearing_x: 0,
1473                bearing_y: 0,
1474                advance_x: 16.0,
1475            })
1476            .collect();
1477        let options = AtlasOptions {
1478            atlas_size: 128,
1479            padding: 1,
1480            algorithm: PackingAlgorithm::MaxRects,
1481            ..Default::default()
1482        };
1483        let (atlas, stats) = SdfAtlas::pack_with_options(&tiles, &options);
1484        // Verify no two UV rects overlap.
1485        let uvs: Vec<_> = atlas.uv_map.values().collect();
1486        for i in 0..uvs.len() {
1487            for j in (i + 1)..uvs.len() {
1488                let a = uvs[i];
1489                let b = uvs[j];
1490                let overlap = a.u_min < b.u_max
1491                    && a.u_max > b.u_min
1492                    && a.v_min < b.v_max
1493                    && a.v_max > b.v_min;
1494                assert!(!overlap, "UV rects {:?} and {:?} overlap", a, b);
1495            }
1496        }
1497        let _ = stats;
1498    }
1499
1500    #[test]
1501    fn test_growing_pack_packs_all() {
1502        let tiles: Vec<SdfTile> = (0..50u16)
1503            .map(|id| SdfTile {
1504                glyph_id: id,
1505                width: 32,
1506                height: 32,
1507                data: vec![128u8; 32 * 32],
1508                bearing_x: 0,
1509                bearing_y: 0,
1510                advance_x: 32.0,
1511            })
1512            .collect();
1513        let (atlas, stats) = pack_growing(&tiles, 64, 1024, 1, PackingAlgorithm::Shelf);
1514        assert_eq!(stats.tiles_dropped, 0, "all tiles should fit after growing");
1515        assert_eq!(atlas.uv_map.len(), 50);
1516    }
1517
1518    #[test]
1519    fn test_sdf_atlas_serialization_roundtrip() {
1520        let tiles: Vec<SdfTile> = (0..5u16)
1521            .map(|id| SdfTile {
1522                glyph_id: id,
1523                width: 8,
1524                height: 8,
1525                data: vec![id as u8; 64],
1526                bearing_x: 0,
1527                bearing_y: 0,
1528                advance_x: 8.0,
1529            })
1530            .collect();
1531        let (atlas, _) = SdfAtlas::pack_with_options(
1532            &tiles,
1533            &AtlasOptions {
1534                atlas_size: 64,
1535                padding: 0,
1536                ..Default::default()
1537            },
1538        );
1539        let bytes = atlas.to_bytes();
1540        let restored = SdfAtlas::from_bytes(&bytes).expect("deserialization");
1541        assert_eq!(restored.width, atlas.width);
1542        assert_eq!(restored.height, atlas.height);
1543        assert_eq!(restored.uv_map.len(), atlas.uv_map.len());
1544        for (gid, uv) in &atlas.uv_map {
1545            let r = &restored.uv_map[gid];
1546            assert!((r.u_min - uv.u_min).abs() < 1e-5);
1547            assert!((r.u_max - uv.u_max).abs() < 1e-5);
1548        }
1549    }
1550
1551    #[test]
1552    fn test_export_png_produces_valid_file() {
1553        use std::env::temp_dir;
1554        let tmp = temp_dir().join("test_sdf_atlas.png");
1555        let mut atlas = SdfAtlas::new(4, 4);
1556        atlas.texture = vec![128u8; 16];
1557        atlas.export_png(&tmp).expect("export should succeed");
1558        assert!(tmp.exists());
1559        assert!(tmp.metadata().expect("metadata").len() > 0);
1560        std::fs::remove_file(&tmp).ok();
1561    }
1562
1563    #[test]
1564    fn test_msdf_export_png_produces_valid_file() {
1565        use std::env::temp_dir;
1566        let tmp = temp_dir().join("test_msdf_atlas.png");
1567        let atlas = MsdfAtlas {
1568            width: 2,
1569            height: 2,
1570            texture: vec![128u8; 2 * 2 * 3],
1571            uv_map: Default::default(),
1572        };
1573        atlas.export_png(&tmp).expect("msdf export should succeed");
1574        assert!(tmp.exists());
1575        assert!(tmp.metadata().expect("metadata").len() > 0);
1576        std::fs::remove_file(&tmp).ok();
1577    }
1578
1579    #[test]
1580    fn test_pre_allocated_atlas_capacity() {
1581        let atlas = SdfAtlas::with_capacity(256, 256, 100, 64);
1582        assert_eq!(atlas.width, 256);
1583        assert_eq!(atlas.height, 256);
1584        assert!(atlas.texture.capacity() >= atlas.texture.len());
1585    }
1586
1587    #[test]
1588    fn test_sdf_atlas_new_zeroed() {
1589        let atlas = SdfAtlas::new(8, 8);
1590        assert_eq!(atlas.width, 8);
1591        assert_eq!(atlas.height, 8);
1592        assert_eq!(atlas.texture.len(), 64);
1593        assert!(atlas.texture.iter().all(|&b| b == 0));
1594        assert!(atlas.uv_map.is_empty());
1595    }
1596
1597    #[test]
1598    fn test_from_static_roundtrip() {
1599        // Create a minimal atlas, serialize, then deserialize via from_static.
1600        // Box::leak gives us a 'static lifetime, simulating include_bytes! semantics.
1601        let atlas = SdfAtlas::new(64, 64);
1602        let bytes = atlas.to_bytes();
1603        let static_bytes: &'static [u8] = Box::leak(bytes.into_boxed_slice());
1604        let roundtrip = SdfAtlas::from_static(static_bytes).expect("from_static should succeed");
1605        assert_eq!(roundtrip.width, 64);
1606        assert_eq!(roundtrip.height, 64);
1607        assert_eq!(roundtrip.uv_map.len(), 0);
1608    }
1609}