// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.util; import java.awt.Dimension; import java.awt.Rectangle; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * Implementation of a two-dimensional rectangle bin packing algorithm. * (Port of Jukka Jylänki's C++ implementation of RectangleBinPack->MaxRectsBinPack.) * <br><br> * Can be used to pack multiple rectangles of arbitrary size into a "bin" of rectangular shape * with the goal to add as many rectangles as possible into the bin. */ public class BinPack2D { /** Specifies the different heuristic rules that can be used when deciding where to place a new rectangle. */ public enum HeuristicRules { /** BSSF: Positions the rectangle against the short side of a free rectangle into which it fits the best. */ BEST_SHORT_SIDE_FIT, /** BLSF: Positions the rectangle against the long side of a free rectangle into which it fits the best. */ BEST_LONG_SIDE_FIT, /** BAF: Positions the rectangle into the smallest free rect into which it fits. */ BEST_AREA_FIT, /** BL: Does the Tetris placement. */ BOTTOM_LEFT_RULE, /** CP: Chooses the placement where the rectangle touches other rects as much as possible. */ CONTACT_POINT_RULE, } private final List<Rectangle> usedRectangles = new ArrayList<Rectangle>(); private final List<Rectangle> freeRectangles = new ArrayList<Rectangle>(); private int binWidth, binHeight; /** * Instantiates a bin of size (0,0). Call Init to create a new bin. */ public BinPack2D() { binWidth = binHeight = 0; } /** * Instantiates a bin of the given size. * @param width Width of the bin. * @param height Height of the bin. */ public BinPack2D(int width, int height) { init(width, height); } /** * (Re)initializes the packer to an empty bin of width x height units. Call whenever * you need to restart with a new bin. * @param width Width of the bin. * @param height Height of the bin. */ public void init(int width, int height) { binWidth = width; binHeight = height; usedRectangles.clear(); freeRectangles.clear(); freeRectangles.add(new Rectangle(0, 0, width, height)); } /** Returns the width of the current bin. */ public int getBinWidth() { return binWidth; } /** Returns the height of the current bin. */ public int getBinHeight() { return binHeight; } /** * Attempts to shrink the current bin as much as possible. * @param binary If {@code true}, the shrinking process will always try to reduce dimensions * by 50% for each iteration. */ public void shrinkBin(boolean binary) { if (usedRectangles.isEmpty()) { return; } int minX = Integer.MAX_VALUE; int minY = Integer.MAX_VALUE; int maxX = Integer.MIN_VALUE; int maxY = Integer.MIN_VALUE; // finding borders for (int i = 0, size = usedRectangles.size(); i < size; i++) { Rectangle r = usedRectangles.get(i); minX = Math.min(minX, r.x); minY = Math.min(minY, r.y); maxX = Math.max(maxX, r.x + r.width); maxY = Math.max(maxY, r.y + r.height); } int newWidth = maxX - minX; int newHeight = maxY - minY; if (binary) { // attempt to shrink to the next lower power of two int curWidth = binWidth; int curHeight = binHeight; while (newWidth <= (curWidth >>> 1)) { curWidth >>>= 1; } newWidth = curWidth; while (newHeight <= (curHeight >>> 1)) { curHeight >>>= 1; } newHeight = curHeight; } // adjusting rectangle positions if ((newWidth != binWidth || newHeight != binHeight) && (minX > 0 || minY > 0)) { Iterator<Rectangle> iterFree = freeRectangles.iterator(); Iterator<Rectangle> iterUsed = usedRectangles.iterator(); while (iterFree.hasNext() && iterUsed.hasNext()) { if (iterFree.hasNext()) { Rectangle r = iterFree.next(); r.x -= minX; r.y -= minY; } if (iterUsed.hasNext()) { Rectangle r = iterUsed.next(); r.x -= minX; r.y -= minY; } } } binWidth = newWidth; binHeight = newHeight; } /** * Inserts the given list of rectangles in an offline/batch mode. * @param rects The list of rectangles to insert. This list will be destroyed in the process. * @param dst (out) This list will be filled with the packed rectangles. The indices will not * correspond to that of rects. * @param rule The rectangle placement rule to use when packing. */ public void insert(List<Dimension> rects, List<Rectangle> dst, HeuristicRules rule) throws NullPointerException { if (rects != null && dst != null) { dst.clear(); while (rects.size() > 0) { int bestScore1 = Integer.MAX_VALUE; int bestScore2 = Integer.MAX_VALUE; int bestRectIndex = -1; Rectangle bestNode = null; Pair<Integer> score = new Pair<Integer>(0, 0); for (int i = 0, size = rects.size(); i < size; i++) { Dimension d = rects.get(i); Rectangle newNode = scoreRect(d.width, d.height, rule, score); if (score.getFirst() < bestScore1 || (score.getFirst() == bestScore1 && score.getSecond() < bestScore2)) { bestScore1 = score.getFirst(); bestScore2 = score.getSecond(); bestNode = newNode; bestRectIndex = i; } } if (bestRectIndex == -1) { return; } placeRect(bestNode); rects.remove(bestRectIndex); } } else { throw new NullPointerException(); } } /** * Inserts a single rectangle into the bin. * @param width Width of the rectangle to insert. * @param height Height of the rectangle to insert. * @param rule The rectangle placement rule to use when packing. * @return Returns the resulting packed rectangle. Returns empty rectangle if no fit found. */ public Rectangle insert(int width, int height, HeuristicRules rule) { Rectangle newNode = null; switch (rule) { case BEST_SHORT_SIDE_FIT: newNode = findPositionForNewNodeBestShortSideFit(width, height, null); break; case BOTTOM_LEFT_RULE: newNode = findPositionForNewNodeBottomLeft(width, height, null); break; case CONTACT_POINT_RULE: newNode = findPositionForNewNodeContactPoint(width, height, null); break; case BEST_LONG_SIDE_FIT: newNode = findPositionForNewNodeBestLongSideFit(width, height, null); break; case BEST_AREA_FIT: newNode = findPositionForNewNodeBestAreaFit(width, height, null); break; } if (newNode.height == 0) { return newNode; } for (int i = 0, size = freeRectangles.size(); i < size; i++) { if (splitFreeNode(freeRectangles.get(i), newNode)) { freeRectangles.remove(i); i--; size--; } } pruneFreeList(); usedRectangles.add(newNode); return newNode; } /** * Computes the ratio of used surface area to the total bin area. * @return The ratio of used surface area to the total bin area. */ public float getOccupancy() { long usedSurfaceArea = 0L; for (int i = 0, size = usedRectangles.size(); i < size; i++) { Rectangle r = usedRectangles.get(i); usedSurfaceArea += r.width * r.height; } return (float)(usedSurfaceArea) / (float)(binWidth*binHeight); } /** * Computes the placement score for placing the given rectangle with the given method. * @param width Width of the rectangle. * @param height Height of the rectangle. * @param rule The selected rectangle placement rule. * @param score (out) Returns the primary and secondary placement score. * @return This struct identifies where the rectangle would be placed if it were placed. */ private Rectangle scoreRect(int width, int height, HeuristicRules rule, Pair<Integer> score) { if (score == null) { score = new Pair<Integer>(0, 0); } Rectangle newNode = null; score.setFirst(Integer.MAX_VALUE); score.setSecond(Integer.MAX_VALUE); switch (rule) { case BEST_SHORT_SIDE_FIT: newNode = findPositionForNewNodeBestShortSideFit(width, height, score); break; case BOTTOM_LEFT_RULE: newNode = findPositionForNewNodeBottomLeft(width, height, score); break; case CONTACT_POINT_RULE: newNode = findPositionForNewNodeContactPoint(width, height, score); score.setFirst(-score.getFirst()); break; case BEST_LONG_SIDE_FIT: newNode = findPositionForNewNodeBestLongSideFit(width, height, score); break; case BEST_AREA_FIT: newNode = findPositionForNewNodeBestAreaFit(width, height, score); break; } // cannot fit the current rectangle if (newNode.height == 0) { score.setFirst(Integer.MAX_VALUE); score.setSecond(Integer.MAX_VALUE); } return newNode; } /** * Places the given rectangle into the bin. * @param node The rectangle to place. */ private void placeRect(Rectangle node) { for (int i = 0, size = freeRectangles.size(); i < size; i++) { if (splitFreeNode(freeRectangles.get(i), node)) { freeRectangles.remove(i); i--; size--; } } pruneFreeList(); usedRectangles.add(node); } /** * Computes the placement score for the "CP" variant. */ private int contactPointScoreNode(int x, int y, int width, int height) { int score = 0; if (x == 0 || x + width == binWidth) { score += height; } if (y == 0 || y + height == binHeight) { score += width; } for (int i = 0, size = usedRectangles.size(); i < size; i++) { Rectangle r = usedRectangles.get(i); if (r.x == x + width || r.x + r.width == x) { score += commonIntervalLength(r.y, r.y + r.height, y, y + height); } if (r.y == y + height || r.y + r.height == y) { score += commonIntervalLength(r.x, r.x + r.width, x, x + width); } } return score; } // bestPos.getFirst(): bestY, bestPos.getSecond(): bestX private Rectangle findPositionForNewNodeBottomLeft(int width, int height, Pair<Integer> bestPos) { if (bestPos == null) { bestPos = new Pair<Integer>(0, 0); } Rectangle bestNode = new Rectangle(); bestPos.setFirst(Integer.MAX_VALUE); for (int i = 0, size = freeRectangles.size(); i < size; i++) { // Try to place the rectangle in upright (non-flipped) orientation. Rectangle r = freeRectangles.get(i); if (r.width >= width && r.height >= height) { int topSideY = r.y + height; if (topSideY < bestPos.getFirst() || (topSideY == bestPos.getFirst() && r.x < bestPos.getSecond())) { bestNode.x = r.x; bestNode.y = r.y; bestNode.width = width; bestNode.height = height; bestPos.setSecond(r.x); bestPos.setFirst(topSideY); } } } return bestNode; } // bestFit.getFirst(): short side, bestFit.getSecond(): long side private Rectangle findPositionForNewNodeBestShortSideFit(int width, int height, Pair<Integer> bestFit) { if (bestFit == null) { bestFit = new Pair<Integer>(0, 0); } Rectangle bestNode = new Rectangle(); bestFit.setFirst(Integer.MAX_VALUE); for (int i = 0, size = freeRectangles.size(); i < size; i++) { // Try to place the rectangle in upright (non-flipped) orientation. Rectangle r = freeRectangles.get(i); if (r.width >= width && r.height >= height) { int leftoverHoriz = Math.abs(r.width - width); int leftoverVert = Math.abs(r.height - height); int shortSideFit = Math.min(leftoverHoriz, leftoverVert); int longSideFit = Math.max(leftoverHoriz, leftoverVert); if (shortSideFit < bestFit.getFirst() || (shortSideFit == bestFit.getFirst() && longSideFit < bestFit.getSecond())) { bestNode.x = r.x; bestNode.y = r.y; bestNode.width = width; bestNode.height = height; bestFit.setFirst(shortSideFit); bestFit.setSecond(longSideFit); } } } return bestNode; } // bestFit.getFirst(): short side, bestFit.getSecond(): long side private Rectangle findPositionForNewNodeBestLongSideFit(int width, int height, Pair<Integer> bestFit) { if (bestFit == null) { bestFit = new Pair<Integer>(0, 0); } Rectangle bestNode = new Rectangle(); bestFit.setSecond(Integer.MAX_VALUE); for (int i = 0, size = freeRectangles.size(); i < size; i++) { // Try to place the rectangle in upright (non-flipped) orientation. Rectangle r = freeRectangles.get(i); if (r.width >= width && r.height >= height) { int leftoverHoriz = Math.abs(r.width - width); int leftoverVert = Math.abs(r.height - height); int shortSideFit = Math.min(leftoverHoriz, leftoverVert); int longSideFit = Math.max(leftoverHoriz, leftoverVert); if (longSideFit < bestFit.getSecond() || (longSideFit == bestFit.getSecond() && shortSideFit < bestFit.getFirst())) { bestNode.x = r.x; bestNode.y = r.y; bestNode.width = width; bestNode.height = height; bestFit.setFirst(shortSideFit); bestFit.setSecond(longSideFit); } } } return bestNode; } // bestFit.getFirst(): area, bestFit.getSecond(): short side private Rectangle findPositionForNewNodeBestAreaFit(int width, int height, Pair<Integer> bestFit) { if (bestFit == null) { bestFit = new Pair<Integer>(0, 0); } Rectangle bestNode = new Rectangle(); bestFit.setFirst(Integer.MAX_VALUE); for (int i = 0, size = freeRectangles.size(); i < size; i++) { Rectangle r = freeRectangles.get(i); int areaFit = r.width*r.height - width*height; // Try to place the rectangle in upright (non-flipped) orientation. if (r.width >= width && r.height >= height) { int leftoverHoriz = Math.abs(r.width - width); int leftoverVert = Math.abs(r.height - height); int shortSideFit = Math.min(leftoverHoriz, leftoverVert); if (areaFit < bestFit.getFirst() || (areaFit == bestFit.getFirst() && shortSideFit < bestFit.getSecond())) { bestNode.x = r.x; bestNode.y = r.y; bestNode.width = width; bestNode.height = height; bestFit.setFirst(areaFit); bestFit.setSecond(shortSideFit); } } } return bestNode; } // bestScore.getFirst(): contact, bestScore.getSecond(): unused private Rectangle findPositionForNewNodeContactPoint(int width, int height, Pair<Integer> bestScore) { if (bestScore == null) { bestScore = new Pair<Integer>(0, 0); } Rectangle bestNode = new Rectangle(); bestScore.setFirst(-1); for (int i = 0, size = freeRectangles.size(); i < size; i++) { // Try to place the rectangle in upright (non-flipped) orientation. Rectangle r = freeRectangles.get(i); if (r.width >= width && r.height >= height) { int score = contactPointScoreNode(r.x, r.y, width, height); if (score > bestScore.getFirst()) { bestNode.x = r.x; bestNode.y = r.y; bestNode.width = width; bestNode.height = height; bestScore.setFirst(score); } } } return bestNode; } /** Returns {@code true} if the free node was split. */ private boolean splitFreeNode(Rectangle freeNode, Rectangle usedNode) { // Test with SAT if the rectangles even intersect. if (usedNode.x >= freeNode.x + freeNode.width || usedNode.x + usedNode.width <= freeNode.x || usedNode.y >= freeNode.y + freeNode.height || usedNode.y + usedNode.height <= freeNode.y) { return false; } if (usedNode.x < freeNode.x + freeNode.width && usedNode.x + usedNode.width > freeNode.x) { // New node at the top side of the used node. if (usedNode.y > freeNode.y && usedNode.y < freeNode.y + freeNode.height) { Rectangle newNode = (Rectangle)freeNode.clone(); newNode.height = usedNode.y - newNode.y; freeRectangles.add(newNode); } // New node at the bottom side of the used node. if (usedNode.y + usedNode.height < freeNode.y + freeNode.height) { Rectangle newNode = (Rectangle)freeNode.clone(); newNode.y = usedNode.y + usedNode.height; newNode.height = freeNode.y + freeNode.height - (usedNode.y + usedNode.height); freeRectangles.add(newNode); } } if (usedNode.y < freeNode.y + freeNode.height && usedNode.y + usedNode.height > freeNode.y) { // New node at the left side of the used node. if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.width) { Rectangle newNode = (Rectangle)freeNode.clone(); newNode.width = usedNode.x - newNode.x; freeRectangles.add(newNode); } // New node at the right side of the used node. if (usedNode.x + usedNode.width < freeNode.x + freeNode.width) { Rectangle newNode = (Rectangle)freeNode.clone(); newNode.x = usedNode.x + usedNode.width; newNode.width = freeNode.x + freeNode.width - (usedNode.x + usedNode.width); freeRectangles.add(newNode); } } return true; } /** Goes through the free rectangle list and removes any redundant entries. */ private void pruneFreeList() { // Go through each pair and remove any rectangle that is redundant. for(int i = 0, size1 = freeRectangles.size(); i < size1; i++) { Rectangle r1 = freeRectangles.get(i); for(int j = i+1, size2 = freeRectangles.size(); j < size2; j++) { Rectangle r2 = freeRectangles.get(j); if (isContainedIn(r1, r2)) { freeRectangles.remove(i); i--; size1--; size2--; break; } if (isContainedIn(r2, r1)) { freeRectangles.remove(j); j--; size1--; size2--; } } } } // Returns 0 if the two intervals i1 and i2 are disjoint, or the length of their overlap otherwise. private int commonIntervalLength(int i1start, int i1end, int i2start, int i2end) { if (i1end < i2start || i2end < i1start) { return 0; } return Math.min(i1end, i2end) - Math.max(i1start, i2start); } // Returns true if a is contained in b. private boolean isContainedIn(Rectangle a, Rectangle b) { if (a != null && b != null) { 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; } return false; } }