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