Skip to main content

fastpack_core/algorithms/
grid.rs

1use crate::{
2    error::CoreError,
3    types::rect::{Rect, Size},
4};
5
6use super::packer::{PackInput, PackOutput, Packer, PlacedSprite, Placement};
7
8/// Grid packing algorithm.
9///
10/// Every sprite occupies an identically sized cell. Sprites are placed
11/// left-to-right then top-to-bottom. Cell dimensions default to the maximum
12/// sprite width and height across all input sprites; they can be overridden via
13/// `cell_width` / `cell_height` for fixed-cell sprite sheets.
14///
15/// Sprites that are larger than the cell are sent to `overflow` without
16/// cropping. The atlas width is determined by the number of columns that fit
17/// within `max_width`; the height grows with each row added.
18#[derive(Default)]
19pub struct Grid {
20    /// Fixed cell width in pixels. `None` → widest sprite.
21    pub cell_width: Option<u32>,
22    /// Fixed cell height in pixels. `None` → tallest sprite.
23    pub cell_height: Option<u32>,
24}
25
26impl Packer for Grid {
27    fn pack(&self, input: PackInput) -> Result<PackOutput, CoreError> {
28        if input.sprites.is_empty() {
29            return Err(CoreError::NoSprites);
30        }
31        Ok(pack_grid(input, self.cell_width, self.cell_height))
32    }
33
34    fn name(&self) -> &'static str {
35        "grid"
36    }
37}
38
39fn pack_grid(input: PackInput, fixed_cw: Option<u32>, fixed_ch: Option<u32>) -> PackOutput {
40    let cfg = &input.config;
41    let bp = cfg.border_padding;
42    let sp = cfg.shape_padding;
43
44    let cell_w = fixed_cw.unwrap_or_else(|| {
45        input
46            .sprites
47            .iter()
48            .map(|s| s.image.width())
49            .max()
50            .unwrap_or(1)
51    });
52    let cell_h = fixed_ch.unwrap_or_else(|| {
53        input
54            .sprites
55            .iter()
56            .map(|s| s.image.height())
57            .max()
58            .unwrap_or(1)
59    });
60
61    let step_x = cell_w + sp;
62    let step_y = cell_h + sp;
63
64    // How many columns fit in the usable canvas width.
65    let usable_w = cfg.max_width.saturating_sub(bp * 2);
66    let cols = ((usable_w + sp) / step_x).max(1);
67
68    let mut placed = Vec::with_capacity(input.sprites.len());
69    let mut overflow = Vec::new();
70    let mut max_row = 0u32;
71
72    for (idx, sprite) in input.sprites.into_iter().enumerate() {
73        let col = idx as u32 % cols;
74        let row = idx as u32 / cols;
75        let x = bp + col * step_x;
76        let y = bp + row * step_y;
77
78        // Vertical overflow check.
79        if y + cell_h + bp > cfg.max_height {
80            overflow.push(sprite);
81            continue;
82        }
83
84        // Center the sprite within its cell.
85        let sw = sprite.image.width();
86        let sh = sprite.image.height();
87        let dest_x = x + (cell_w.saturating_sub(sw)) / 2;
88        let dest_y = y + (cell_h.saturating_sub(sh)) / 2;
89
90        max_row = max_row.max(row);
91
92        placed.push(PlacedSprite {
93            placement: Placement {
94                sprite_id: sprite.id.clone(),
95                dest: Rect::new(dest_x, dest_y, sw, sh),
96                rotated: false,
97            },
98            sprite,
99        });
100    }
101
102    let row_count = if placed.is_empty() { 0 } else { max_row + 1 };
103
104    // Atlas covers exactly the occupied columns × rows, with padding.
105    let raw_w = if row_count == 0 {
106        0
107    } else {
108        bp + cols * step_x - sp + bp
109    };
110    let raw_h = if row_count == 0 {
111        0
112    } else {
113        bp + row_count * step_y - sp + bp
114    };
115
116    let mut w = cfg.size_constraint.apply(raw_w).min(cfg.max_width);
117    let mut h = cfg.size_constraint.apply(raw_h).min(cfg.max_height);
118
119    if cfg.force_square {
120        let side = w.max(h);
121        w = side;
122        h = side;
123    }
124
125    PackOutput {
126        placed,
127        atlas_size: Size { w, h },
128        overflow,
129    }
130}