• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*******************************************************************************
2  * Copyright 2011 See AUTHORS file.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  ******************************************************************************/
16 
17 package com.badlogic.gdx.tools.texturepacker;
18 
19 import java.util.Comparator;
20 
21 import com.badlogic.gdx.math.MathUtils;
22 import com.badlogic.gdx.tools.texturepacker.TexturePacker.Packer;
23 import com.badlogic.gdx.tools.texturepacker.TexturePacker.Page;
24 import com.badlogic.gdx.tools.texturepacker.TexturePacker.Rect;
25 import com.badlogic.gdx.tools.texturepacker.TexturePacker.Settings;
26 import com.badlogic.gdx.utils.Array;
27 import com.badlogic.gdx.utils.Sort;
28 
29 /** Packs pages of images using the maximal rectangles bin packing algorithm by Jukka Jylänki. A brute force binary search is
30  * used to pack into the smallest bin possible.
31  * @author Nathan Sweet */
32 public class MaxRectsPacker implements Packer {
33 	private RectComparator rectComparator = new RectComparator();
34 	private FreeRectChoiceHeuristic[] methods = FreeRectChoiceHeuristic.values();
35 	private MaxRects maxRects = new MaxRects();
36 	Settings settings;
37 	private Sort sort = new Sort();
38 
MaxRectsPacker(Settings settings)39 	public MaxRectsPacker (Settings settings) {
40 		this.settings = settings;
41 		if (settings.minWidth > settings.maxWidth) throw new RuntimeException("Page min width cannot be higher than max width.");
42 		if (settings.minHeight > settings.maxHeight)
43 			throw new RuntimeException("Page min height cannot be higher than max height.");
44 	}
45 
pack(Array<Rect> inputRects)46 	public Array<Page> pack (Array<Rect> inputRects) {
47 		for (int i = 0, nn = inputRects.size; i < nn; i++) {
48 			Rect rect = inputRects.get(i);
49 			rect.width += settings.paddingX;
50 			rect.height += settings.paddingY;
51 		}
52 
53 		if (settings.fast) {
54 			if (settings.rotation) {
55 				// Sort by longest side if rotation is enabled.
56 				sort.sort(inputRects, new Comparator<Rect>() {
57 					public int compare (Rect o1, Rect o2) {
58 						int n1 = o1.width > o1.height ? o1.width : o1.height;
59 						int n2 = o2.width > o2.height ? o2.width : o2.height;
60 						return n2 - n1;
61 					}
62 				});
63 			} else {
64 				// Sort only by width (largest to smallest) if rotation is disabled.
65 				sort.sort(inputRects, new Comparator<Rect>() {
66 					public int compare (Rect o1, Rect o2) {
67 						return o2.width - o1.width;
68 					}
69 				});
70 			}
71 		}
72 
73 		Array<Page> pages = new Array();
74 		while (inputRects.size > 0)
75 
76 		{
77 			Page result = packPage(inputRects);
78 			pages.add(result);
79 			inputRects = result.remainingRects;
80 		}
81 		return pages;
82 
83 	}
84 
packPage(Array<Rect> inputRects)85 	private Page packPage (Array<Rect> inputRects) {
86 		int paddingX = settings.paddingX, paddingY = settings.paddingY;
87 		float maxWidth = settings.maxWidth, maxHeight = settings.maxHeight;
88 		int edgePaddingX = 0, edgePaddingY = 0;
89 		if (settings.edgePadding) {
90 			if (settings.duplicatePadding) { // If duplicatePadding, edges get only half padding.
91 				maxWidth -= paddingX;
92 				maxHeight -= paddingY;
93 			} else {
94 				maxWidth -= paddingX * 2;
95 				maxHeight -= paddingY * 2;
96 				edgePaddingX = paddingX;
97 				edgePaddingY = paddingY;
98 			}
99 		}
100 
101 		// Find min size.
102 		int minWidth = Integer.MAX_VALUE, minHeight = Integer.MAX_VALUE;
103 		for (int i = 0, nn = inputRects.size; i < nn; i++) {
104 			Rect rect = inputRects.get(i);
105 			minWidth = Math.min(minWidth, rect.width);
106 			minHeight = Math.min(minHeight, rect.height);
107 			float width = rect.width - paddingX, height = rect.height - paddingY;
108 			if (settings.rotation) {
109 				if ((width > maxWidth || height > maxHeight) && (width > maxHeight || height > maxWidth)) {
110 					String paddingMessage = (edgePaddingX > 0 || edgePaddingY > 0) ? (" and edge padding " + paddingX + "," + paddingY)
111 						: "";
112 					throw new RuntimeException("Image does not fit with max page size " + settings.maxWidth + "x" + settings.maxHeight
113 						+ paddingMessage + ": " + rect.name + "[" + width + "," + height + "]");
114 				}
115 			} else {
116 				if (width > maxWidth) {
117 					String paddingMessage = edgePaddingX > 0 ? (" and X edge padding " + paddingX) : "";
118 					throw new RuntimeException("Image does not fit with max page width " + settings.maxWidth + paddingMessage + ": "
119 						+ rect.name + "[" + width + "," + height + "]");
120 				}
121 				if (height > maxHeight && (!settings.rotation || width > maxHeight)) {
122 					String paddingMessage = edgePaddingY > 0 ? (" and Y edge padding " + paddingY) : "";
123 					throw new RuntimeException("Image does not fit in max page height " + settings.maxHeight + paddingMessage + ": "
124 						+ rect.name + "[" + width + "," + height + "]");
125 				}
126 			}
127 		}
128 		minWidth = Math.max(minWidth, settings.minWidth);
129 		minHeight = Math.max(minHeight, settings.minHeight);
130 
131 		if (!settings.silent) System.out.print("Packing");
132 
133 		// Find the minimal page size that fits all rects.
134 		Page bestResult = null;
135 		if (settings.square) {
136 			int minSize = Math.max(minWidth, minHeight);
137 			int maxSize = Math.min(settings.maxWidth, settings.maxHeight);
138 			BinarySearch sizeSearch = new BinarySearch(minSize, maxSize, settings.fast ? 25 : 15, settings.pot);
139 			int size = sizeSearch.reset(), i = 0;
140 			while (size != -1) {
141 				Page result = packAtSize(true, size - edgePaddingX, size - edgePaddingY, inputRects);
142 				if (!settings.silent) {
143 					if (++i % 70 == 0) System.out.println();
144 					System.out.print(".");
145 				}
146 				bestResult = getBest(bestResult, result);
147 				size = sizeSearch.next(result == null);
148 			}
149 			if (!settings.silent) System.out.println();
150 			// Rects don't fit on one page. Fill a whole page and return.
151 			if (bestResult == null) bestResult = packAtSize(false, maxSize - edgePaddingX, maxSize - edgePaddingY, inputRects);
152 			sort.sort(bestResult.outputRects, rectComparator);
153 			bestResult.width = Math.max(bestResult.width, bestResult.height);
154 			bestResult.height = Math.max(bestResult.width, bestResult.height);
155 			return bestResult;
156 		} else {
157 			BinarySearch widthSearch = new BinarySearch(minWidth, settings.maxWidth, settings.fast ? 25 : 15, settings.pot);
158 			BinarySearch heightSearch = new BinarySearch(minHeight, settings.maxHeight, settings.fast ? 25 : 15, settings.pot);
159 			int width = widthSearch.reset(), i = 0;
160 			int height = settings.square ? width : heightSearch.reset();
161 			while (true) {
162 				Page bestWidthResult = null;
163 				while (width != -1) {
164 					Page result = packAtSize(true, width - edgePaddingX, height - edgePaddingY, inputRects);
165 					if (!settings.silent) {
166 						if (++i % 70 == 0) System.out.println();
167 						System.out.print(".");
168 					}
169 					bestWidthResult = getBest(bestWidthResult, result);
170 					width = widthSearch.next(result == null);
171 					if (settings.square) height = width;
172 				}
173 				bestResult = getBest(bestResult, bestWidthResult);
174 				if (settings.square) break;
175 				height = heightSearch.next(bestWidthResult == null);
176 				if (height == -1) break;
177 				width = widthSearch.reset();
178 			}
179 			if (!settings.silent) System.out.println();
180 			// Rects don't fit on one page. Fill a whole page and return.
181 			if (bestResult == null)
182 				bestResult = packAtSize(false, settings.maxWidth - edgePaddingX, settings.maxHeight - edgePaddingY, inputRects);
183 			sort.sort(bestResult.outputRects, rectComparator);
184 			return bestResult;
185 		}
186 	}
187 
188 	/** @param fully If true, the only results that pack all rects will be considered. If false, all results are considered, not
189 	 *           all rects may be packed. */
packAtSize(boolean fully, int width, int height, Array<Rect> inputRects)190 	private Page packAtSize (boolean fully, int width, int height, Array<Rect> inputRects) {
191 		Page bestResult = null;
192 		for (int i = 0, n = methods.length; i < n; i++) {
193 			maxRects.init(width, height);
194 			Page result;
195 			if (!settings.fast) {
196 				result = maxRects.pack(inputRects, methods[i]);
197 			} else {
198 				Array<Rect> remaining = new Array();
199 				for (int ii = 0, nn = inputRects.size; ii < nn; ii++) {
200 					Rect rect = inputRects.get(ii);
201 					if (maxRects.insert(rect, methods[i]) == null) {
202 						while (ii < nn)
203 							remaining.add(inputRects.get(ii++));
204 					}
205 				}
206 				result = maxRects.getResult();
207 				result.remainingRects = remaining;
208 			}
209 			if (fully && result.remainingRects.size > 0) continue;
210 			if (result.outputRects.size == 0) continue;
211 			bestResult = getBest(bestResult, result);
212 		}
213 		return bestResult;
214 	}
215 
getBest(Page result1, Page result2)216 	private Page getBest (Page result1, Page result2) {
217 		if (result1 == null) return result2;
218 		if (result2 == null) return result1;
219 		return result1.occupancy > result2.occupancy ? result1 : result2;
220 	}
221 
222 	static class BinarySearch {
223 		int min, max, fuzziness, low, high, current;
224 		boolean pot;
225 
BinarySearch(int min, int max, int fuzziness, boolean pot)226 		public BinarySearch (int min, int max, int fuzziness, boolean pot) {
227 			this.pot = pot;
228 			this.fuzziness = pot ? 0 : fuzziness;
229 			this.min = pot ? (int)(Math.log(MathUtils.nextPowerOfTwo(min)) / Math.log(2)) : min;
230 			this.max = pot ? (int)(Math.log(MathUtils.nextPowerOfTwo(max)) / Math.log(2)) : max;
231 		}
232 
reset()233 		public int reset () {
234 			low = min;
235 			high = max;
236 			current = (low + high) >>> 1;
237 			return pot ? (int)Math.pow(2, current) : current;
238 		}
239 
next(boolean result)240 		public int next (boolean result) {
241 			if (low >= high) return -1;
242 			if (result)
243 				low = current + 1;
244 			else
245 				high = current - 1;
246 			current = (low + high) >>> 1;
247 			if (Math.abs(low - high) < fuzziness) return -1;
248 			return pot ? (int)Math.pow(2, current) : current;
249 		}
250 	}
251 
252 	/** Maximal rectangles bin packing algorithm. Adapted from this C++ public domain source:
253 	 * http://clb.demon.fi/projects/even-more-rectangle-bin-packing
254 	 * @author Jukka Jyl�nki
255 	 * @author Nathan Sweet */
256 	class MaxRects {
257 		private int binWidth;
258 		private int binHeight;
259 		private final Array<Rect> usedRectangles = new Array();
260 		private final Array<Rect> freeRectangles = new Array();
261 
init(int width, int height)262 		public void init (int width, int height) {
263 			binWidth = width;
264 			binHeight = height;
265 
266 			usedRectangles.clear();
267 			freeRectangles.clear();
268 			Rect n = new Rect();
269 			n.x = 0;
270 			n.y = 0;
271 			n.width = width;
272 			n.height = height;
273 			freeRectangles.add(n);
274 		}
275 
276 		/** Packs a single image. Order is defined externally. */
insert(Rect rect, FreeRectChoiceHeuristic method)277 		public Rect insert (Rect rect, FreeRectChoiceHeuristic method) {
278 			Rect newNode = scoreRect(rect, method);
279 			if (newNode.height == 0) return null;
280 
281 			int numRectanglesToProcess = freeRectangles.size;
282 			for (int i = 0; i < numRectanglesToProcess; ++i) {
283 				if (splitFreeNode(freeRectangles.get(i), newNode)) {
284 					freeRectangles.removeIndex(i);
285 					--i;
286 					--numRectanglesToProcess;
287 				}
288 			}
289 
290 			pruneFreeList();
291 
292 			Rect bestNode = new Rect();
293 			bestNode.set(rect);
294 			bestNode.score1 = newNode.score1;
295 			bestNode.score2 = newNode.score2;
296 			bestNode.x = newNode.x;
297 			bestNode.y = newNode.y;
298 			bestNode.width = newNode.width;
299 			bestNode.height = newNode.height;
300 			bestNode.rotated = newNode.rotated;
301 
302 			usedRectangles.add(bestNode);
303 			return bestNode;
304 		}
305 
306 		/** For each rectangle, packs each one then chooses the best and packs that. Slow! */
pack(Array<Rect> rects, FreeRectChoiceHeuristic method)307 		public Page pack (Array<Rect> rects, FreeRectChoiceHeuristic method) {
308 			rects = new Array(rects);
309 			while (rects.size > 0) {
310 				int bestRectIndex = -1;
311 				Rect bestNode = new Rect();
312 				bestNode.score1 = Integer.MAX_VALUE;
313 				bestNode.score2 = Integer.MAX_VALUE;
314 
315 				// Find the next rectangle that packs best.
316 				for (int i = 0; i < rects.size; i++) {
317 					Rect newNode = scoreRect(rects.get(i), method);
318 					if (newNode.score1 < bestNode.score1 || (newNode.score1 == bestNode.score1 && newNode.score2 < bestNode.score2)) {
319 						bestNode.set(rects.get(i));
320 						bestNode.score1 = newNode.score1;
321 						bestNode.score2 = newNode.score2;
322 						bestNode.x = newNode.x;
323 						bestNode.y = newNode.y;
324 						bestNode.width = newNode.width;
325 						bestNode.height = newNode.height;
326 						bestNode.rotated = newNode.rotated;
327 						bestRectIndex = i;
328 					}
329 				}
330 
331 				if (bestRectIndex == -1) break;
332 
333 				placeRect(bestNode);
334 				rects.removeIndex(bestRectIndex);
335 			}
336 
337 			Page result = getResult();
338 			result.remainingRects = rects;
339 			return result;
340 		}
341 
getResult()342 		public Page getResult () {
343 			int w = 0, h = 0;
344 			for (int i = 0; i < usedRectangles.size; i++) {
345 				Rect rect = usedRectangles.get(i);
346 				w = Math.max(w, rect.x + rect.width);
347 				h = Math.max(h, rect.y + rect.height);
348 			}
349 			Page result = new Page();
350 			result.outputRects = new Array(usedRectangles);
351 			result.occupancy = getOccupancy();
352 			result.width = w;
353 			result.height = h;
354 			return result;
355 		}
356 
placeRect(Rect node)357 		private void placeRect (Rect node) {
358 			int numRectanglesToProcess = freeRectangles.size;
359 			for (int i = 0; i < numRectanglesToProcess; i++) {
360 				if (splitFreeNode(freeRectangles.get(i), node)) {
361 					freeRectangles.removeIndex(i);
362 					--i;
363 					--numRectanglesToProcess;
364 				}
365 			}
366 
367 			pruneFreeList();
368 
369 			usedRectangles.add(node);
370 		}
371 
scoreRect(Rect rect, FreeRectChoiceHeuristic method)372 		private Rect scoreRect (Rect rect, FreeRectChoiceHeuristic method) {
373 			int width = rect.width;
374 			int height = rect.height;
375 			int rotatedWidth = height - settings.paddingY + settings.paddingX;
376 			int rotatedHeight = width - settings.paddingX + settings.paddingY;
377 			boolean rotate = rect.canRotate && settings.rotation;
378 
379 			Rect newNode = null;
380 			switch (method) {
381 			case BestShortSideFit:
382 				newNode = findPositionForNewNodeBestShortSideFit(width, height, rotatedWidth, rotatedHeight, rotate);
383 				break;
384 			case BottomLeftRule:
385 				newNode = findPositionForNewNodeBottomLeft(width, height, rotatedWidth, rotatedHeight, rotate);
386 				break;
387 			case ContactPointRule:
388 				newNode = findPositionForNewNodeContactPoint(width, height, rotatedWidth, rotatedHeight, rotate);
389 				newNode.score1 = -newNode.score1; // Reverse since we are minimizing, but for contact point score bigger is better.
390 				break;
391 			case BestLongSideFit:
392 				newNode = findPositionForNewNodeBestLongSideFit(width, height, rotatedWidth, rotatedHeight, rotate);
393 				break;
394 			case BestAreaFit:
395 				newNode = findPositionForNewNodeBestAreaFit(width, height, rotatedWidth, rotatedHeight, rotate);
396 				break;
397 			}
398 
399 			// Cannot fit the current rectangle.
400 			if (newNode.height == 0) {
401 				newNode.score1 = Integer.MAX_VALUE;
402 				newNode.score2 = Integer.MAX_VALUE;
403 			}
404 
405 			return newNode;
406 		}
407 
408 		// / Computes the ratio of used surface area.
getOccupancy()409 		private float getOccupancy () {
410 			int usedSurfaceArea = 0;
411 			for (int i = 0; i < usedRectangles.size; i++)
412 				usedSurfaceArea += usedRectangles.get(i).width * usedRectangles.get(i).height;
413 			return (float)usedSurfaceArea / (binWidth * binHeight);
414 		}
415 
findPositionForNewNodeBottomLeft(int width, int height, int rotatedWidth, int rotatedHeight, boolean rotate)416 		private Rect findPositionForNewNodeBottomLeft (int width, int height, int rotatedWidth, int rotatedHeight, boolean rotate) {
417 			Rect bestNode = new Rect();
418 
419 			bestNode.score1 = Integer.MAX_VALUE; // best y, score2 is best x
420 
421 			for (int i = 0; i < freeRectangles.size; i++) {
422 				// Try to place the rectangle in upright (non-rotated) orientation.
423 				if (freeRectangles.get(i).width >= width && freeRectangles.get(i).height >= height) {
424 					int topSideY = freeRectangles.get(i).y + height;
425 					if (topSideY < bestNode.score1 || (topSideY == bestNode.score1 && freeRectangles.get(i).x < bestNode.score2)) {
426 						bestNode.x = freeRectangles.get(i).x;
427 						bestNode.y = freeRectangles.get(i).y;
428 						bestNode.width = width;
429 						bestNode.height = height;
430 						bestNode.score1 = topSideY;
431 						bestNode.score2 = freeRectangles.get(i).x;
432 						bestNode.rotated = false;
433 					}
434 				}
435 				if (rotate && freeRectangles.get(i).width >= rotatedWidth && freeRectangles.get(i).height >= rotatedHeight) {
436 					int topSideY = freeRectangles.get(i).y + rotatedHeight;
437 					if (topSideY < bestNode.score1 || (topSideY == bestNode.score1 && freeRectangles.get(i).x < bestNode.score2)) {
438 						bestNode.x = freeRectangles.get(i).x;
439 						bestNode.y = freeRectangles.get(i).y;
440 						bestNode.width = rotatedWidth;
441 						bestNode.height = rotatedHeight;
442 						bestNode.score1 = topSideY;
443 						bestNode.score2 = freeRectangles.get(i).x;
444 						bestNode.rotated = true;
445 					}
446 				}
447 			}
448 			return bestNode;
449 		}
450 
findPositionForNewNodeBestShortSideFit(int width, int height, int rotatedWidth, int rotatedHeight, boolean rotate)451 		private Rect findPositionForNewNodeBestShortSideFit (int width, int height, int rotatedWidth, int rotatedHeight,
452 			boolean rotate) {
453 			Rect bestNode = new Rect();
454 			bestNode.score1 = Integer.MAX_VALUE;
455 
456 			for (int i = 0; i < freeRectangles.size; i++) {
457 				// Try to place the rectangle in upright (non-rotated) orientation.
458 				if (freeRectangles.get(i).width >= width && freeRectangles.get(i).height >= height) {
459 					int leftoverHoriz = Math.abs(freeRectangles.get(i).width - width);
460 					int leftoverVert = Math.abs(freeRectangles.get(i).height - height);
461 					int shortSideFit = Math.min(leftoverHoriz, leftoverVert);
462 					int longSideFit = Math.max(leftoverHoriz, leftoverVert);
463 
464 					if (shortSideFit < bestNode.score1 || (shortSideFit == bestNode.score1 && longSideFit < bestNode.score2)) {
465 						bestNode.x = freeRectangles.get(i).x;
466 						bestNode.y = freeRectangles.get(i).y;
467 						bestNode.width = width;
468 						bestNode.height = height;
469 						bestNode.score1 = shortSideFit;
470 						bestNode.score2 = longSideFit;
471 						bestNode.rotated = false;
472 					}
473 				}
474 
475 				if (rotate && freeRectangles.get(i).width >= rotatedWidth && freeRectangles.get(i).height >= rotatedHeight) {
476 					int flippedLeftoverHoriz = Math.abs(freeRectangles.get(i).width - rotatedWidth);
477 					int flippedLeftoverVert = Math.abs(freeRectangles.get(i).height - rotatedHeight);
478 					int flippedShortSideFit = Math.min(flippedLeftoverHoriz, flippedLeftoverVert);
479 					int flippedLongSideFit = Math.max(flippedLeftoverHoriz, flippedLeftoverVert);
480 
481 					if (flippedShortSideFit < bestNode.score1
482 						|| (flippedShortSideFit == bestNode.score1 && flippedLongSideFit < bestNode.score2)) {
483 						bestNode.x = freeRectangles.get(i).x;
484 						bestNode.y = freeRectangles.get(i).y;
485 						bestNode.width = rotatedWidth;
486 						bestNode.height = rotatedHeight;
487 						bestNode.score1 = flippedShortSideFit;
488 						bestNode.score2 = flippedLongSideFit;
489 						bestNode.rotated = true;
490 					}
491 				}
492 			}
493 
494 			return bestNode;
495 		}
496 
findPositionForNewNodeBestLongSideFit(int width, int height, int rotatedWidth, int rotatedHeight, boolean rotate)497 		private Rect findPositionForNewNodeBestLongSideFit (int width, int height, int rotatedWidth, int rotatedHeight,
498 			boolean rotate) {
499 			Rect bestNode = new Rect();
500 
501 			bestNode.score2 = Integer.MAX_VALUE;
502 
503 			for (int i = 0; i < freeRectangles.size; i++) {
504 				// Try to place the rectangle in upright (non-rotated) orientation.
505 				if (freeRectangles.get(i).width >= width && freeRectangles.get(i).height >= height) {
506 					int leftoverHoriz = Math.abs(freeRectangles.get(i).width - width);
507 					int leftoverVert = Math.abs(freeRectangles.get(i).height - height);
508 					int shortSideFit = Math.min(leftoverHoriz, leftoverVert);
509 					int longSideFit = Math.max(leftoverHoriz, leftoverVert);
510 
511 					if (longSideFit < bestNode.score2 || (longSideFit == bestNode.score2 && shortSideFit < bestNode.score1)) {
512 						bestNode.x = freeRectangles.get(i).x;
513 						bestNode.y = freeRectangles.get(i).y;
514 						bestNode.width = width;
515 						bestNode.height = height;
516 						bestNode.score1 = shortSideFit;
517 						bestNode.score2 = longSideFit;
518 						bestNode.rotated = false;
519 					}
520 				}
521 
522 				if (rotate && freeRectangles.get(i).width >= rotatedWidth && freeRectangles.get(i).height >= rotatedHeight) {
523 					int leftoverHoriz = Math.abs(freeRectangles.get(i).width - rotatedWidth);
524 					int leftoverVert = Math.abs(freeRectangles.get(i).height - rotatedHeight);
525 					int shortSideFit = Math.min(leftoverHoriz, leftoverVert);
526 					int longSideFit = Math.max(leftoverHoriz, leftoverVert);
527 
528 					if (longSideFit < bestNode.score2 || (longSideFit == bestNode.score2 && shortSideFit < bestNode.score1)) {
529 						bestNode.x = freeRectangles.get(i).x;
530 						bestNode.y = freeRectangles.get(i).y;
531 						bestNode.width = rotatedWidth;
532 						bestNode.height = rotatedHeight;
533 						bestNode.score1 = shortSideFit;
534 						bestNode.score2 = longSideFit;
535 						bestNode.rotated = true;
536 					}
537 				}
538 			}
539 			return bestNode;
540 		}
541 
findPositionForNewNodeBestAreaFit(int width, int height, int rotatedWidth, int rotatedHeight, boolean rotate)542 		private Rect findPositionForNewNodeBestAreaFit (int width, int height, int rotatedWidth, int rotatedHeight,
543 			boolean rotate) {
544 			Rect bestNode = new Rect();
545 
546 			bestNode.score1 = Integer.MAX_VALUE; // best area fit, score2 is best short side fit
547 
548 			for (int i = 0; i < freeRectangles.size; i++) {
549 				int areaFit = freeRectangles.get(i).width * freeRectangles.get(i).height - width * height;
550 
551 				// Try to place the rectangle in upright (non-rotated) orientation.
552 				if (freeRectangles.get(i).width >= width && freeRectangles.get(i).height >= height) {
553 					int leftoverHoriz = Math.abs(freeRectangles.get(i).width - width);
554 					int leftoverVert = Math.abs(freeRectangles.get(i).height - height);
555 					int shortSideFit = Math.min(leftoverHoriz, leftoverVert);
556 
557 					if (areaFit < bestNode.score1 || (areaFit == bestNode.score1 && shortSideFit < bestNode.score2)) {
558 						bestNode.x = freeRectangles.get(i).x;
559 						bestNode.y = freeRectangles.get(i).y;
560 						bestNode.width = width;
561 						bestNode.height = height;
562 						bestNode.score2 = shortSideFit;
563 						bestNode.score1 = areaFit;
564 						bestNode.rotated = false;
565 					}
566 				}
567 
568 				if (rotate && freeRectangles.get(i).width >= rotatedWidth && freeRectangles.get(i).height >= rotatedHeight) {
569 					int leftoverHoriz = Math.abs(freeRectangles.get(i).width - rotatedWidth);
570 					int leftoverVert = Math.abs(freeRectangles.get(i).height - rotatedHeight);
571 					int shortSideFit = Math.min(leftoverHoriz, leftoverVert);
572 
573 					if (areaFit < bestNode.score1 || (areaFit == bestNode.score1 && shortSideFit < bestNode.score2)) {
574 						bestNode.x = freeRectangles.get(i).x;
575 						bestNode.y = freeRectangles.get(i).y;
576 						bestNode.width = rotatedWidth;
577 						bestNode.height = rotatedHeight;
578 						bestNode.score2 = shortSideFit;
579 						bestNode.score1 = areaFit;
580 						bestNode.rotated = true;
581 					}
582 				}
583 			}
584 			return bestNode;
585 		}
586 
587 		// / Returns 0 if the two intervals i1 and i2 are disjoint, or the length of their overlap otherwise.
commonIntervalLength(int i1start, int i1end, int i2start, int i2end)588 		private int commonIntervalLength (int i1start, int i1end, int i2start, int i2end) {
589 			if (i1end < i2start || i2end < i1start) return 0;
590 			return Math.min(i1end, i2end) - Math.max(i1start, i2start);
591 		}
592 
contactPointScoreNode(int x, int y, int width, int height)593 		private int contactPointScoreNode (int x, int y, int width, int height) {
594 			int score = 0;
595 
596 			if (x == 0 || x + width == binWidth) score += height;
597 			if (y == 0 || y + height == binHeight) score += width;
598 
599 			Array<Rect> usedRectangles = this.usedRectangles;
600 			for (int i = 0, n = usedRectangles.size; i < n; i++) {
601 				Rect rect = usedRectangles.get(i);
602 				if (rect.x == x + width || rect.x + rect.width == x)
603 					score += commonIntervalLength(rect.y, rect.y + rect.height, y, y + height);
604 				if (rect.y == y + height || rect.y + rect.height == y)
605 					score += commonIntervalLength(rect.x, rect.x + rect.width, x, x + width);
606 			}
607 			return score;
608 		}
609 
findPositionForNewNodeContactPoint(int width, int height, int rotatedWidth, int rotatedHeight, boolean rotate)610 		private Rect findPositionForNewNodeContactPoint (int width, int height, int rotatedWidth, int rotatedHeight,
611 			boolean rotate) {
612 
613 			Rect bestNode = new Rect();
614 			bestNode.score1 = -1; // best contact score
615 
616 			Array<Rect> freeRectangles = this.freeRectangles;
617 			for (int i = 0, n = freeRectangles.size; i < n; i++) {
618 				// Try to place the rectangle in upright (non-rotated) orientation.
619 				Rect free = freeRectangles.get(i);
620 				if (free.width >= width && free.height >= height) {
621 					int score = contactPointScoreNode(free.x, free.y, width, height);
622 					if (score > bestNode.score1) {
623 						bestNode.x = free.x;
624 						bestNode.y = free.y;
625 						bestNode.width = width;
626 						bestNode.height = height;
627 						bestNode.score1 = score;
628 						bestNode.rotated = false;
629 					}
630 				}
631 				if (rotate && free.width >= rotatedWidth && free.height >= rotatedHeight) {
632 					int score = contactPointScoreNode(free.x, free.y, rotatedWidth, rotatedHeight);
633 					if (score > bestNode.score1) {
634 						bestNode.x = free.x;
635 						bestNode.y = free.y;
636 						bestNode.width = rotatedWidth;
637 						bestNode.height = rotatedHeight;
638 						bestNode.score1 = score;
639 						bestNode.rotated = true;
640 					}
641 				}
642 			}
643 			return bestNode;
644 		}
645 
splitFreeNode(Rect freeNode, Rect usedNode)646 		private boolean splitFreeNode (Rect freeNode, Rect usedNode) {
647 			// Test with SAT if the rectangles even intersect.
648 			if (usedNode.x >= freeNode.x + freeNode.width || usedNode.x + usedNode.width <= freeNode.x
649 				|| usedNode.y >= freeNode.y + freeNode.height || usedNode.y + usedNode.height <= freeNode.y) return false;
650 
651 			if (usedNode.x < freeNode.x + freeNode.width && usedNode.x + usedNode.width > freeNode.x) {
652 				// New node at the top side of the used node.
653 				if (usedNode.y > freeNode.y && usedNode.y < freeNode.y + freeNode.height) {
654 					Rect newNode = new Rect(freeNode);
655 					newNode.height = usedNode.y - newNode.y;
656 					freeRectangles.add(newNode);
657 				}
658 
659 				// New node at the bottom side of the used node.
660 				if (usedNode.y + usedNode.height < freeNode.y + freeNode.height) {
661 					Rect newNode = new Rect(freeNode);
662 					newNode.y = usedNode.y + usedNode.height;
663 					newNode.height = freeNode.y + freeNode.height - (usedNode.y + usedNode.height);
664 					freeRectangles.add(newNode);
665 				}
666 			}
667 
668 			if (usedNode.y < freeNode.y + freeNode.height && usedNode.y + usedNode.height > freeNode.y) {
669 				// New node at the left side of the used node.
670 				if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.width) {
671 					Rect newNode = new Rect(freeNode);
672 					newNode.width = usedNode.x - newNode.x;
673 					freeRectangles.add(newNode);
674 				}
675 
676 				// New node at the right side of the used node.
677 				if (usedNode.x + usedNode.width < freeNode.x + freeNode.width) {
678 					Rect newNode = new Rect(freeNode);
679 					newNode.x = usedNode.x + usedNode.width;
680 					newNode.width = freeNode.x + freeNode.width - (usedNode.x + usedNode.width);
681 					freeRectangles.add(newNode);
682 				}
683 			}
684 
685 			return true;
686 		}
687 
pruneFreeList()688 		private void pruneFreeList () {
689 			/*
690 			 * /// Would be nice to do something like this, to avoid a Theta(n^2) loop through each pair. /// But unfortunately it
691 			 * doesn't quite cut it, since we also want to detect containment. /// Perhaps there's another way to do this faster than
692 			 * Theta(n^2).
693 			 *
694 			 * if (freeRectangles.size > 0) clb::sort::QuickSort(&freeRectangles[0], freeRectangles.size, NodeSortCmp);
695 			 *
696 			 * for(int i = 0; i < freeRectangles.size-1; i++) if (freeRectangles[i].x == freeRectangles[i+1].x && freeRectangles[i].y
697 			 * == freeRectangles[i+1].y && freeRectangles[i].width == freeRectangles[i+1].width && freeRectangles[i].height ==
698 			 * freeRectangles[i+1].height) { freeRectangles.erase(freeRectangles.begin() + i); --i; }
699 			 */
700 
701 			// Go through each pair and remove any rectangle that is redundant.
702 			Array<Rect> freeRectangles = this.freeRectangles;
703 			for (int i = 0, n = freeRectangles.size; i < n; i++)
704 				for (int j = i + 1; j < n; ++j) {
705 					Rect rect1 = freeRectangles.get(i);
706 					Rect rect2 = freeRectangles.get(j);
707 					if (isContainedIn(rect1, rect2)) {
708 						freeRectangles.removeIndex(i);
709 						--i;
710 						--n;
711 						break;
712 					}
713 					if (isContainedIn(rect2, rect1)) {
714 						freeRectangles.removeIndex(j);
715 						--j;
716 						--n;
717 					}
718 				}
719 		}
720 
isContainedIn(Rect a, Rect b)721 		private boolean isContainedIn (Rect a, Rect b) {
722 			return a.x >= b.x && a.y >= b.y && a.x + a.width <= b.x + b.width && a.y + a.height <= b.y + b.height;
723 		}
724 	}
725 
726 	static public enum FreeRectChoiceHeuristic {
727 		// BSSF: Positions the rectangle against the short side of a free rectangle into which it fits the best.
728 		BestShortSideFit,
729 		// BLSF: Positions the rectangle against the long side of a free rectangle into which it fits the best.
730 		BestLongSideFit,
731 		// BAF: Positions the rectangle into the smallest free rect into which it fits.
732 		BestAreaFit,
733 		// BL: Does the Tetris placement.
734 		BottomLeftRule,
735 		// CP: Choosest the placement where the rectangle touches other rects as much as possible.
736 		ContactPointRule
737 	};
738 
739 	class RectComparator implements Comparator<Rect> {
compare(Rect o1, Rect o2)740 		public int compare (Rect o1, Rect o2) {
741 			return Rect.getAtlasName(o1.name, settings.flattenPaths).compareTo(Rect.getAtlasName(o2.name, settings.flattenPaths));
742 		}
743 	}
744 }
745