package com.glview.hwui.packer; import java.util.List; import java.util.Vector; import com.glview.graphics.Rect; /** * 纹理打包算法 * @author lijing.lj */ public class MaxRectsPacker implements Packer { int mWidth, mHeight; boolean mAllowRotation; Vector<PackerRect> mUsedRects = new Vector<PackerRect>(); Vector<Rect> mFreeRects = new Vector<Rect>(); Score mScore = new Score(); FreeRectangleChoiceHeuristic mDefaultMethod; class Score { int score1 = 0; // Unused in this function. We don't need to know // the score after finding the position. int score2 = 0; int bestShortSideFit; int bestLongSideFit; } public MaxRectsPacker(int width, int height) { this(width, height, false); } public MaxRectsPacker(int width, int height, boolean rotation) { this(width, height, rotation, FreeRectangleChoiceHeuristic.BestShortSideFit); } public MaxRectsPacker(int width, int height, boolean rotation, FreeRectangleChoiceHeuristic method) { mDefaultMethod = method; init(width, height, rotation); } private void init(int width, int height, boolean rotation) { if (!isPowerOf2(width) || !isPowerOf2(height)) throw new RuntimeException("Must be 2,4,8,16,32,...512,1024,..."); mWidth = width; mHeight = height; mAllowRotation = rotation; Rect root = new Rect(0, 0, width, height); mUsedRects.clear(); mFreeRects.clear(); mFreeRects.add(root); } public void reset() { Rect root = new Rect(0, 0, mWidth, mHeight); mUsedRects.clear(); mFreeRects.clear(); mFreeRects.add(root); } public String dump() { StringBuilder sb = new StringBuilder(); sb.append("us=" + mUsedRects.size()); sb.append("fs=" + mFreeRects.size()); sb.append("usedRect="); sb.append(mUsedRects); sb.append(", freeRect="); sb.append(mFreeRects); return sb.toString(); } private boolean isPowerOf2(int n) { if (n == 2) return true; if (n < 2 || n % 2 != 0) return false; return isPowerOf2(n / 2); } public PackerRect insert(int width, int height) { return insert(width, height, mDefaultMethod); } /** * Insert a new Rectangle * * @param width * @param height * @param method * @return * */ public PackerRect insert(int width, int height, FreeRectangleChoiceHeuristic method) { PackerRect newNode = null; switch (method) { case BestShortSideFit: newNode = findPositionForNewNodeBestShortSideFit(width, height, mScore); break; case BottomLeftRule: newNode = findPositionForNewNodeBottomLeft(width, height/*, score1, score2*/, mScore); break; case ContactPointRule: newNode = findPositionForNewNodeContactPoint(width, height/*, score1*/, mScore); break; case BestLongSideFit: newNode = findPositionForNewNodeBestLongSideFit(width, height/*, score2, score1*/, mScore); break; case BestAreaFit: newNode = findPositionForNewNodeBestAreaFit(width, height/*, score1, score2*/, mScore); break; default: break; } if (newNode == null || newNode.height() == 0) return null; placeRectangle(newNode); return newNode; } public List<Rect> insert2(List<Rect> rects, FreeRectangleChoiceHeuristic method) { while (rects.size() > 0) { int bestScore1 = Integer.MAX_VALUE; int bestScore2 = Integer.MAX_VALUE; int bestRectangleIndex = -1; PackerRect bestNode = new PackerRect(); for (int i = 0; i < rects.size(); ++i) { mScore.score1 = 0; mScore.score2 = 0; PackerRect newNode = scoreRectangle(rects.get(i).width(), rects .get(i).height(), method, mScore); if (newNode != null && (mScore.score1 < bestScore1 || (mScore.score1 == bestScore1 && mScore.score2 < bestScore2))) { bestScore1 = mScore.score1; bestScore2 = mScore.score2; bestNode = newNode; bestRectangleIndex = i; } } if (bestRectangleIndex == -1) return rects; placeRectangle(bestNode); rects.remove(bestRectangleIndex); } return rects; } private PackerRect scoreRectangle(int width, int height, FreeRectangleChoiceHeuristic method, Score score) { PackerRect newNode = null; score.score1 = Integer.MAX_VALUE; score.score2 = Integer.MAX_VALUE; switch (method) { case BestShortSideFit: newNode = findPositionForNewNodeBestShortSideFit(width, height, score); break; case BottomLeftRule: newNode = findPositionForNewNodeBottomLeft(width, height/*, score1, score2*/, score); break; case ContactPointRule: newNode = findPositionForNewNodeContactPoint(width, height/*, score1*/, score); // todo: reverse score.score1 = - score.score1; // Reverse since we are minimizing, but for // contact point score bigger is better. break; case BestLongSideFit: newNode = findPositionForNewNodeBestLongSideFit(width, height, /*score2, score1*/score); break; case BestAreaFit: newNode = findPositionForNewNodeBestAreaFit(width, height, /*score1, score2*/score); break; } // Cannot fit the current Rectangle. if (newNode == null || newNode.height() == 0) { score.score1 = Integer.MAX_VALUE; score.score2 = Integer.MAX_VALUE; return null; } return newNode; } private PackerRect bestRect(PackerRect bestNode, int x, int y, int width, int height, boolean rotation) { bestNode.set(x, y, x + width, y + height); bestNode.rotation = rotation; return bestNode; } private PackerRect findPositionForNewNodeBestShortSideFit(int width, int height, Score iscore) { PackerRect bestNode = new PackerRect(); // memset(&bestNode, 0, sizeof(Rectangle)); iscore.bestShortSideFit = Integer.MAX_VALUE; iscore.bestLongSideFit = iscore.score2; Rect rect; int leftoverHoriz; int leftoverVert; int shortSideFit; int longSideFit; for (int i = 0; i < mFreeRects.size(); i++) { rect = mFreeRects.get(i); // Try to place the Rectangle in upright (non-flipped) orientation. if (rect.width() >= width && rect.height() >= height) { leftoverHoriz = Math.abs(rect.width() - width); leftoverVert = Math.abs(rect.height() - height); shortSideFit = Math.min(leftoverHoriz, leftoverVert); longSideFit = Math.max(leftoverHoriz, leftoverVert); if (shortSideFit < iscore.bestShortSideFit || (shortSideFit == iscore.bestShortSideFit && longSideFit < iscore.bestLongSideFit)) { bestRect(bestNode, rect.left, rect.top, width, height, false); /*bestNode.left = rect.left; bestNode.top = rect.top; bestNode.right = bestNode.left + width; bestNode.bottom = bestNode.top + height;*/ iscore.bestShortSideFit = shortSideFit; iscore.bestLongSideFit = longSideFit; } } int flippedLeftoverHoriz; int flippedLeftoverVert; int flippedShortSideFit; int flippedLongSideFit; if (mAllowRotation && rect.width() >= height && rect.height() >= width) { flippedLeftoverHoriz = Math.abs(rect.width() - height); flippedLeftoverVert = Math.abs(rect.height() - width); flippedShortSideFit = Math.min(flippedLeftoverHoriz, flippedLeftoverVert); flippedLongSideFit = Math.max(flippedLeftoverHoriz, flippedLeftoverVert); if (flippedShortSideFit < iscore.bestShortSideFit || (flippedShortSideFit == iscore.bestShortSideFit && flippedLongSideFit < iscore.bestLongSideFit)) { bestRect(bestNode, rect.left, rect.top, height, width, true); /*bestNode.left = rect.left; bestNode.top = rect.top; bestNode.right = bestNode.top + height; bestNode.bottom = bestNode.left + width;*/ iscore.bestShortSideFit = flippedShortSideFit; iscore.bestLongSideFit = flippedLongSideFit; } } } return bestNode; } private PackerRect findPositionForNewNodeBottomLeft(int width, int height/*, int bestY, int bestX*/, Score iscore) { PackerRect bestNode = new PackerRect(); // memset(bestNode, 0, sizeof(Rectangle)); iscore.score1 = Integer.MAX_VALUE; Rect rect; int topSideY; for (int i = 0; i < mFreeRects.size(); i++) { rect = mFreeRects.get(i); // Try to place the Rectangle in upright (non-flipped) orientation. if (rect.width() >= width && rect.height() >= height) { topSideY = rect.top + height; if (topSideY < iscore.score1 || (topSideY == iscore.score1 && rect.left < iscore.score2)) { bestRect(bestNode, rect.left, rect.top, width, height, false); /*bestNode.left = rect.left; bestNode.top = rect.top; bestNode.right = bestNode.left + width; bestNode.bottom = bestNode.top + height;*/ iscore.score1 = topSideY; iscore.score2 = rect.left; } } if (mAllowRotation && rect.width() >= height && rect.height() >= width) { topSideY = rect.top + width; if (topSideY < iscore.score1 || (topSideY == iscore.score1 && rect.left < iscore.score2)) { bestRect(bestNode, rect.left, rect.top, height, width, true); /*bestNode.left = rect.left; bestNode.top = rect.top; bestNode.right = bestNode.left + height; bestNode.bottom = bestNode.top + width;*/ iscore.score1 = topSideY; iscore.score2 = rect.left; } } } return bestNode; } // / 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); } private int contactPointScoreNode(int x, int y, int width, int height) { int score = 0; if (x == 0 || x + width == mWidth) score += height; if (y == 0 || y + height == mHeight) score += width; Rect rect; for (int i = 0; i < mUsedRects.size(); i++) { rect = mUsedRects.get(i).rect(); if (rect.left == x + width || rect.right == x) score += commonIntervalLength(rect.top, rect.bottom, y, y + height); if (rect.top == y + height || rect.bottom == y) score += commonIntervalLength(rect.left, rect.right, x, x + width); } return score; } private PackerRect findPositionForNewNodeContactPoint(int width, int height/*, int bestContactScore*/, Score iscore) { PackerRect bestNode = new PackerRect(); // memset(&bestNode, 0, sizeof(Rectangle)); iscore.score1 = -1; Rect rect; int score; for (int i = 0; i < mFreeRects.size(); i++) { rect = mFreeRects.get(i); // Try to place the Rectangle in upright (non-flipped) orientation. if (rect.width() >= width && rect.height() >= height) { score = contactPointScoreNode(rect.left, rect.top, width, height); if (score > iscore.score1) { bestRect(bestNode, rect.left, rect.top, width, height, false); /*bestNode.left = rect.left; bestNode.top = rect.top; bestNode.right = bestNode.left + width; bestNode.bottom = bestNode.top + height;*/ iscore.score1 = score; } } if (mAllowRotation && rect.width() >= height && rect.height() >= width) { score = contactPointScoreNode(rect.left, rect.top, height, width); if (score > iscore.score1) { bestRect(bestNode, rect.left, rect.top, height, width, true); /*bestNode.left = rect.left; bestNode.top = rect.top; bestNode.right = bestNode.left + height; bestNode.bottom = bestNode.top + width;*/ iscore.score1 = score; } } } return bestNode; } private PackerRect findPositionForNewNodeBestLongSideFit(int width, int height/*, int bestShortSideFit, int bestLongSideFit*/, Score iscore) { PackerRect bestNode = new PackerRect(); // memset(&bestNode, 0, sizeof(Rectangle)); iscore.score1 = Integer.MAX_VALUE; Rect rect; int leftoverHoriz; int leftoverVert; int shortSideFit; int longSideFit; for (int i = 0; i < mFreeRects.size(); i++) { rect = mFreeRects.get(i); // Try to place the Rectangle in upright (non-flipped) orientation. if (rect.width() >= width && rect.height() >= height) { leftoverHoriz = Math.abs(rect.width() - width); leftoverVert = Math.abs(rect.height() - height); shortSideFit = Math.min(leftoverHoriz, leftoverVert); longSideFit = Math.max(leftoverHoriz, leftoverVert); if (longSideFit < iscore.score1 || (longSideFit == iscore.score1 && shortSideFit < iscore.score2)) { bestRect(bestNode, rect.left, rect.top, width, height, false); /*bestNode.left = rect.left; bestNode.top = rect.top; bestNode.right = bestNode.left + width; bestNode.bottom = bestNode.top + height;*/ iscore.score2 = shortSideFit; iscore.score1 = longSideFit; } } if (mAllowRotation && rect.width() >= height && rect.height() >= width) { leftoverHoriz = Math.abs(rect.width() - height); leftoverVert = Math.abs(rect.height() - width); shortSideFit = Math.min(leftoverHoriz, leftoverVert); longSideFit = Math.max(leftoverHoriz, leftoverVert); if (longSideFit < iscore.score1 || (longSideFit == iscore.score1 && shortSideFit < iscore.score2)) { bestRect(bestNode, rect.left, rect.top, height, width, true); /*bestNode.left = rect.left; bestNode.top = rect.top; bestNode.right = bestNode.left + height; bestNode.bottom = bestNode.top + width;*/ iscore.score2 = shortSideFit; iscore.score1 = longSideFit; } } } return bestNode; } private PackerRect findPositionForNewNodeBestAreaFit(int width, int height/*, int bestAreaFit, int bestShortSideFit*/, Score iscore) { PackerRect bestNode = new PackerRect(); // memset(&bestNode, 0, sizeof(Rectangle)); // bestAreaFit = Integer.MAX_VALUE; iscore.score1 = Integer.MAX_VALUE; Rect rect; int leftoverHoriz; int leftoverVert; int shortSideFit; int areaFit; for (int i = 0; i < mFreeRects.size(); i++) { rect = mFreeRects.get(i); areaFit = rect.width() * rect.height() - width * height; // Try to place the Rectangle in upright (non-flipped) orientation. if (rect.width() >= width && rect.height() >= height) { leftoverHoriz = Math.abs(rect.width() - width); leftoverVert = Math.abs(rect.height() - height); shortSideFit = Math.min(leftoverHoriz, leftoverVert); if (areaFit < iscore.score1 || (areaFit == iscore.score1 && shortSideFit < iscore.score2)) { bestRect(bestNode, rect.left, rect.top, width, height, false); /*bestNode.left = rect.left; bestNode.top = rect.top; bestNode.right = bestNode.left + width; bestNode.bottom = bestNode.top + height;*/ iscore.score2 = shortSideFit; iscore.score1 = areaFit; } } if (mAllowRotation && rect.width() >= height && rect.height() >= width) { leftoverHoriz = Math.abs(rect.width() - height); leftoverVert = Math.abs(rect.height() - width); shortSideFit = Math.min(leftoverHoriz, leftoverVert); if (areaFit < iscore.score1 || (areaFit == iscore.score1 && shortSideFit < iscore.score2)) { bestRect(bestNode, rect.left, rect.top, height, width, true); /*bestNode.left = rect.left; bestNode.top = rect.top; bestNode.right = bestNode.left + height; bestNode.bottom = bestNode.top + width;*/ iscore.score2 = shortSideFit; iscore.score1 = areaFit; } } } return bestNode; } private void placeRectangle(PackerRect node) { int numRectanglesToProcess = mFreeRects.size(); for (int i = 0; i < numRectanglesToProcess; i++) { if (splitFreeNode(mFreeRects.get(i), node.rect())) { mFreeRects.remove(i); --i; --numRectanglesToProcess; } } pruneFreeList(); mUsedRects.add(node); } private boolean splitFreeNode(Rect freeNode, Rect usedNode) { // Test with SAT if the Rectangles even intersect. if (usedNode.left >= freeNode.right || usedNode.right <= freeNode.left || usedNode.top >= freeNode.bottom || usedNode.bottom <= freeNode.top) return false; Rect newNode; if (usedNode.left < freeNode.right && usedNode.right > freeNode.left) { // New node at the top side of the used node. if (usedNode.top > freeNode.top && usedNode.top < freeNode.bottom) { newNode = new Rect(freeNode); newNode.bottom = newNode.top + (usedNode.top - newNode.top); mFreeRects.add(newNode); } // New node at the bottom side of the used node. if (usedNode.bottom < freeNode.bottom) { newNode = new Rect(freeNode); newNode.top = usedNode.bottom; newNode.bottom = newNode.top + (freeNode.bottom - usedNode.bottom); mFreeRects.add(newNode); } } if (usedNode.top < freeNode.bottom && usedNode.bottom > freeNode.top) { // New node at the left side of the used node. if (usedNode.left > freeNode.left && usedNode.left < freeNode.right) { newNode = new Rect(freeNode); newNode.right = newNode.left + (usedNode.left - newNode.left); mFreeRects.add(newNode); } // New node at the right side of the used node. if (usedNode.right < freeNode.right) { newNode = new Rect(freeNode); newNode.left = usedNode.right; newNode.right = newNode.left + (freeNode.right - usedNode.right); mFreeRects.add(newNode); } } return true; } private void pruneFreeList() { int numRectanglesToProcess = mFreeRects.size(); for (int i = 0; i < numRectanglesToProcess; i++) { for (int j = i + 1; j < numRectanglesToProcess; j ++) { if (isContainedIn(mFreeRects.get(i), mFreeRects.get(j))) { mFreeRects.remove(i); --i; --numRectanglesToProcess; break; } if (isContainedIn(mFreeRects.get(j), mFreeRects.get(i))) { mFreeRects.remove(j); --j; --numRectanglesToProcess; } } } } private boolean isContainedIn(Rect a, Rect b) { return b.contains(a); } public static enum FreeRectangleChoiceHeuristic { BestShortSideFit, BottomLeftRule, ContactPointRule, BestLongSideFit, BestAreaFit } }