Skip to main content

fastpack_core/algorithms/
maxrects.rs

1use crate::{
2    error::CoreError,
3    types::{
4        config::{LayoutConfig, MaxRectsHeuristic},
5        rect::{Rect, Size},
6        sprite::Sprite,
7    },
8};
9
10use super::packer::{PackInput, PackOutput, PlacedSprite, Placement};
11
12/// MaxRects rectangle bin-packing algorithm.
13///
14/// Maintains a list of maximal free rectangles. For each sprite the algorithm
15/// scores every free rect under the configured heuristic, places the sprite in
16/// the best-scoring position, then splits and prunes the free-rect list to
17/// maintain the invariant that no free rect overlaps any placed sprite.
18///
19/// `ContactPointRule` falls back to `BestAreaFit` scoring until the full contact
20/// computation is implemented in Phase 2.
21#[derive(Default)]
22pub struct MaxRects {
23    /// Placement scoring heuristic (default: `BestShortSideFit`).
24    pub heuristic: MaxRectsHeuristic,
25}
26
27impl super::packer::Packer for MaxRects {
28    fn pack(&self, input: PackInput) -> Result<PackOutput, CoreError> {
29        if input.sprites.is_empty() {
30            return Err(CoreError::NoSprites);
31        }
32        Ok(pack_sprites(input, self.heuristic))
33    }
34
35    fn name(&self) -> &'static str {
36        "maxrects"
37    }
38}
39
40fn pack_sprites(input: PackInput, heuristic: MaxRectsHeuristic) -> PackOutput {
41    use crate::types::config::PackMode;
42
43    let cfg = &input.config;
44    let mut sprites = input.sprites;
45    // Largest area first gives MaxRects the best chance at dense packing.
46    sprites.sort_unstable_by(|a, b| {
47        let area_a = a.image.width() * a.image.height();
48        let area_b = b.image.width() * b.image.height();
49        area_b.cmp(&area_a)
50    });
51
52    match cfg.pack_mode {
53        PackMode::Fast => pack_at_width(&sprites, cfg, heuristic, cfg.max_width),
54        PackMode::Good => binary_search_width(&sprites, cfg, heuristic),
55        PackMode::Best => exhaustive_width_search(&sprites, cfg, heuristic),
56    }
57}
58
59/// Minimum canvas width needed to fit the widest sprite (respects rotation).
60fn min_viable_width(sprites: &[Sprite], cfg: &LayoutConfig) -> u32 {
61    let bp = cfg.border_padding;
62    let sp = cfg.shape_padding;
63    sprites
64        .iter()
65        .map(|s| {
66            let w = s.image.width() + sp + bp * 2;
67            let h = s.image.height() + sp + bp * 2;
68            if cfg.allow_rotation { w.min(h) } else { w }
69        })
70        .max()
71        .unwrap_or(64)
72        .min(cfg.max_width)
73}
74
75fn binary_search_width(
76    sprites: &[Sprite],
77    cfg: &LayoutConfig,
78    heuristic: MaxRectsHeuristic,
79) -> PackOutput {
80    // Phase 1: find w_min — the narrowest width where no overflow occurs.
81    let lo_start = min_viable_width(sprites, cfg);
82    let mut lo = lo_start;
83    let mut hi = cfg.max_width;
84    let mut w_min = cfg.max_width;
85
86    while lo <= hi {
87        let mid = lo + (hi - lo) / 2;
88        let result = pack_at_width(sprites, cfg, heuristic, mid);
89        if result.overflow.is_empty() {
90            w_min = mid;
91            if mid == 0 {
92                break;
93            }
94            hi = mid - 1;
95        } else {
96            lo = mid + 1;
97        }
98    }
99
100    // Phase 2: find the width that minimises atlas area.
101    //
102    // The best result is often just above w_min (a few px above the overflow
103    // threshold) where the canvas constraint forces a different row structure.
104    // We do a fine-grained scan near w_min, then coarse doubling steps further
105    // out to catch wider local minima.
106    let step = (w_min / 64).max(2);
107    let fine_end = (w_min + w_min / 4).min(cfg.max_width);
108    let mut candidates: Vec<u32> = (w_min..=fine_end).step_by(step as usize).collect();
109    // Coarse doubling sweep from fine_end up to max_width.
110    let mut w = fine_end;
111    loop {
112        let next = ((w as f64 * 1.414) as u32).min(cfg.max_width);
113        if next == w {
114            break;
115        }
116        candidates.push(next);
117        w = next;
118        if w >= cfg.max_width {
119            break;
120        }
121    }
122
123    let mut best: Option<PackOutput> = None;
124    for cw in candidates {
125        let result = pack_at_width(sprites, cfg, heuristic, cw);
126        if result.overflow.is_empty() {
127            let area = result.atlas_size.w * result.atlas_size.h;
128            let best_area = best
129                .as_ref()
130                .map_or(u32::MAX, |b: &PackOutput| b.atlas_size.w * b.atlas_size.h);
131            if area < best_area {
132                best = Some(result);
133            }
134        }
135    }
136
137    best.unwrap_or_else(|| pack_at_width(sprites, cfg, heuristic, cfg.max_width))
138}
139
140fn exhaustive_width_search(
141    sprites: &[Sprite],
142    cfg: &LayoutConfig,
143    heuristic: MaxRectsHeuristic,
144) -> PackOutput {
145    let lo_start = min_viable_width(sprites, cfg);
146    // Start from the binary-search result and sweep nearby widths for the
147    // smallest atlas area.
148    let good = binary_search_width(sprites, cfg, heuristic);
149    let best_w = good.atlas_size.w;
150
151    let sweep_lo = lo_start.max(best_w.saturating_sub(best_w / 8));
152    let sweep_hi = (best_w + best_w / 8).min(cfg.max_width);
153
154    let mut best = good;
155    for w in sweep_lo..=sweep_hi {
156        let candidate = pack_at_width(sprites, cfg, heuristic, w);
157        if candidate.overflow.is_empty() {
158            let candidate_area = candidate.atlas_size.w * candidate.atlas_size.h;
159            let best_area = best.atlas_size.w * best.atlas_size.h;
160            if candidate_area < best_area {
161                best = candidate;
162            }
163        }
164    }
165    best
166}
167
168fn pack_at_width(
169    sprites: &[Sprite],
170    cfg: &LayoutConfig,
171    heuristic: MaxRectsHeuristic,
172    width: u32,
173) -> PackOutput {
174    let bp = cfg.border_padding;
175    let sp = cfg.shape_padding;
176
177    let canvas_w = width.saturating_sub(bp * 2);
178    let canvas_h = cfg.max_height.saturating_sub(bp * 2);
179    let mut free_rects: Vec<Rect> = vec![Rect::new(bp, bp, canvas_w, canvas_h)];
180
181    let mut placed: Vec<PlacedSprite> = Vec::with_capacity(sprites.len());
182    let mut overflow: Vec<Sprite> = Vec::new();
183
184    for sprite in sprites {
185        let sw = sprite.image.width();
186        let sh = sprite.image.height();
187        // Footprint includes shape_padding so gaps open between adjacent sprites.
188        let fw = sw + sp;
189        let fh = sh + sp;
190
191        match find_best(&free_rects, fw, fh, cfg.allow_rotation, heuristic) {
192            None => overflow.push(sprite.clone()),
193            Some((dest_x, dest_y, rotated)) => {
194                let (placed_w, placed_h) = if rotated { (sh, sw) } else { (sw, sh) };
195                let (foot_w, foot_h) = if rotated {
196                    (sh + sp, sw + sp)
197                } else {
198                    (fw, fh)
199                };
200                let dest = Rect::new(dest_x, dest_y, placed_w, placed_h);
201                let footprint = Rect::new(dest_x, dest_y, foot_w, foot_h);
202                split_and_prune(&mut free_rects, footprint);
203                placed.push(PlacedSprite {
204                    placement: Placement {
205                        sprite_id: sprite.id.clone(),
206                        dest,
207                        rotated,
208                    },
209                    sprite: sprite.clone(),
210                });
211            }
212        }
213    }
214
215    let atlas_size = compute_atlas_size(&placed, bp, cfg);
216    PackOutput {
217        placed,
218        atlas_size,
219        overflow,
220    }
221}
222
223fn find_best(
224    free_rects: &[Rect],
225    fw: u32,
226    fh: u32,
227    allow_rotation: bool,
228    heuristic: MaxRectsHeuristic,
229) -> Option<(u32, u32, bool)> {
230    let mut best_x = 0u32;
231    let mut best_y = 0u32;
232    let mut best_rotated = false;
233    let mut best_primary = i64::MAX;
234    let mut best_secondary = i64::MAX;
235    let mut found = false;
236
237    for r in free_rects {
238        if fw <= r.w && fh <= r.h {
239            let (p, s) = score(r, fw, fh, heuristic);
240            if p < best_primary || (p == best_primary && s < best_secondary) {
241                best_primary = p;
242                best_secondary = s;
243                best_x = r.x;
244                best_y = r.y;
245                best_rotated = false;
246                found = true;
247            }
248        }
249        if allow_rotation && fw != fh && fh <= r.w && fw <= r.h {
250            let (p, s) = score(r, fh, fw, heuristic);
251            if p < best_primary || (p == best_primary && s < best_secondary) {
252                best_primary = p;
253                best_secondary = s;
254                best_x = r.x;
255                best_y = r.y;
256                best_rotated = true;
257                found = true;
258            }
259        }
260    }
261
262    if found {
263        Some((best_x, best_y, best_rotated))
264    } else {
265        None
266    }
267}
268
269fn score(rect: &Rect, fw: u32, fh: u32, heuristic: MaxRectsHeuristic) -> (i64, i64) {
270    let lw = (rect.w - fw) as i64;
271    let lh = (rect.h - fh) as i64;
272    match heuristic {
273        MaxRectsHeuristic::BestShortSideFit => (lw.min(lh), lw.max(lh)),
274        MaxRectsHeuristic::BestLongSideFit => (lw.max(lh), lw.min(lh)),
275        MaxRectsHeuristic::BestAreaFit | MaxRectsHeuristic::ContactPointRule => {
276            let waste = (rect.w * rect.h - fw * fh) as i64;
277            (waste, lw.min(lh))
278        }
279        MaxRectsHeuristic::BottomLeftRule => (rect.y as i64, rect.x as i64),
280    }
281}
282
283fn split_and_prune(free_rects: &mut Vec<Rect>, placed: Rect) {
284    let mut splits: Vec<Rect> = Vec::new();
285    let mut i = 0;
286
287    while i < free_rects.len() {
288        if free_rects[i].intersects(&placed) {
289            let r = free_rects.remove(i);
290            if placed.x > r.x {
291                splits.push(Rect::new(r.x, r.y, placed.x - r.x, r.h));
292            }
293            if placed.right() < r.right() {
294                splits.push(Rect::new(
295                    placed.right(),
296                    r.y,
297                    r.right() - placed.right(),
298                    r.h,
299                ));
300            }
301            if placed.y > r.y {
302                splits.push(Rect::new(r.x, r.y, r.w, placed.y - r.y));
303            }
304            if placed.bottom() < r.bottom() {
305                splits.push(Rect::new(
306                    r.x,
307                    placed.bottom(),
308                    r.w,
309                    r.bottom() - placed.bottom(),
310                ));
311            }
312        } else {
313            i += 1;
314        }
315    }
316
317    free_rects.extend(splits);
318    prune(free_rects);
319}
320
321fn prune(rects: &mut Vec<Rect>) {
322    let mut i = 0;
323    while i < rects.len() {
324        let mut dominated = false;
325        for j in 0..rects.len() {
326            if i != j && rects[j].contains(&rects[i]) {
327                dominated = true;
328                break;
329            }
330        }
331        if dominated {
332            rects.remove(i);
333        } else {
334            i += 1;
335        }
336    }
337}
338
339fn compute_atlas_size(placed: &[PlacedSprite], border_padding: u32, cfg: &LayoutConfig) -> Size {
340    if placed.is_empty() {
341        return Size { w: 0, h: 0 };
342    }
343
344    let mut max_x = 0u32;
345    let mut max_y = 0u32;
346    for ps in placed {
347        max_x = max_x.max(ps.placement.dest.right());
348        max_y = max_y.max(ps.placement.dest.bottom());
349    }
350
351    let w = cfg.size_constraint.apply(max_x + border_padding);
352    let h = cfg.size_constraint.apply(max_y + border_padding);
353
354    let (w, h) = if cfg.force_square {
355        let side = w.max(h);
356        (side, side)
357    } else {
358        (w, h)
359    };
360
361    Size {
362        w: w.min(cfg.max_width),
363        h: h.min(cfg.max_height),
364    }
365}