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