Skip to main content

fastpack_core/algorithms/
maxrects.rs

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