fastpack_core/algorithms/
maxrects.rs1use 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#[derive(Default)]
24pub struct MaxRects {
25 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 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
61fn 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 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 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 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 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 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}