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 17package com.badlogic.gdx.tools.texturepacker; 18 19import java.util.Comparator; 20 21import com.badlogic.gdx.math.MathUtils; 22import com.badlogic.gdx.tools.texturepacker.TexturePacker.Packer; 23import com.badlogic.gdx.tools.texturepacker.TexturePacker.Page; 24import com.badlogic.gdx.tools.texturepacker.TexturePacker.Rect; 25import com.badlogic.gdx.tools.texturepacker.TexturePacker.Settings; 26import com.badlogic.gdx.utils.Array; 27import 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 */ 32public 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 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 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 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. */ 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 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 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 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 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 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. */ 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! */ 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 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 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 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. 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 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 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 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 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. 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 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 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 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 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 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> { 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