diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index b3f40fba2..1a1e2fc87 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -71,6 +71,10 @@ set(RenderFiles render/nuklear_sdl_gl3.cpp render/nuklear_sdl_gl3.h render/misc.h + ../extern/RectangleBinPack/MaxRectsBinPack.cpp + ../extern/RectangleBinPack/MaxRectsBinPack.h + ../extern/RectangleBinPack/Rect.cpp + ../extern/RectangleBinPack/Rect.h ) add_library(Render ${RenderFiles}) target_link_libraries(Render PUBLIC Cel Levels nuklear SDL2::SDL2 SDL_image::SDL_image ${OPENGL_LIBRARIES}) diff --git a/components/render/sdl2backend.cpp b/components/render/sdl2backend.cpp index c9c5b589c..c9225e471 100644 --- a/components/render/sdl2backend.cpp +++ b/components/render/sdl2backend.cpp @@ -21,6 +21,7 @@ // clang-format off #include #include "../../extern/jo_gif/jo_gif.cpp" +#include "../../extern/RectangleBinPack/MaxRectsBinPack.h" #include // clang-format on @@ -101,6 +102,9 @@ namespace Render // GLint estimatedRequiredTextures = (1uLL << 29) / (mTextureWidth * mTextureHeight); mTextureLayers = std::min(estimatedRequiredTextures, maxArrayTextureLayers); + for (int32_t layer = 0; layer < mTextureLayers; layer++) + mBinPacker.push_back(rbp::MaxRectsBinPack(mTextureWidth, mTextureHeight, false)); + glGenTextures(1, &mTextureArrayId); glBindTexture(GL_TEXTURE_2D_ARRAY, mTextureArrayId); @@ -138,40 +142,28 @@ namespace Render size_t addTexture(int32_t width, int32_t height, const void* data) { - // Note: This simple simple scan line layout isn't very efficient for big - // textures (especially fonts textures which can be around 32x7000 pixels). - release_assert(width <= mTextureWidth && height <= mTextureHeight); // Texture size too small... - if (mX + width > mTextureWidth) - { - mX = 0; - mY = mNextY; - } - if (mY + height > mTextureHeight) + rbp::Rect packedPos; + int32_t layer; + for (layer = 0; layer < mTextureLayers; layer++) { - mX = mY = mNextY = 0; - mLayer++; - release_assert(mLayer < mTextureLayers); // Run out of memory... + packedPos = mBinPacker[layer].Insert(width, height, rbp::MaxRectsBinPack::RectBestAreaFit); + if (packedPos.height != 0) + break; } + release_assert(layer < mTextureLayers); // Run out of memory... glBindTexture(GL_TEXTURE_2D_ARRAY, mTextureArrayId); - glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, mX, mY, mLayer, width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE, data); + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, packedPos.x, packedPos.y, layer, width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE, data); // TODO: Sometimes adding glFlush/glFinish fixes tiles a bit // when texture is compressed but not really sure why... // glFlush(); // glFinish(); - auto id = mNextTextureId; - mLookupMap[id] = TextureAtlasEntry(mX, mY, mLayer, width, height); - - mNextTextureId++; - mX += width; - mNextY = std::max(mNextY, mY + height); - - // Diagnostic only. - mUtilisedArea += width * height; + auto id = mNextTextureId++; + mLookupMap[id] = TextureAtlasEntry(packedPos.x, packedPos.y, layer, width, height); return id; } @@ -182,10 +174,12 @@ namespace Render GLint getTextureHeight() const { return mTextureHeight; } const std::map& getLookupMap() const { return mLookupMap; } - float getEfficiency() const + float getOccupancy() const { - uint64_t usedArea = (uint64_t)mTextureWidth * mTextureHeight * mLayer + (uint64_t)mTextureWidth * mY + (uint64_t)mX * (mNextY - mY); - return (float)mUtilisedArea / usedArea * 100; + float summedOccupancy = 0; + for (auto& bp : mBinPacker) + summedOccupancy += bp.Occupancy(); + return summedOccupancy / mBinPacker.size() * 100; } private: @@ -193,10 +187,9 @@ namespace Render GLint mTextureWidth; GLint mTextureHeight; GLint mTextureLayers; - int32_t mX = 0, mY = 0, mLayer = 0, mNextY = 0; std::map mLookupMap; size_t mNextTextureId = 1; - uint64_t mUtilisedArea = 0; + std::vector mBinPacker; }; /* Caches level sprites/positions etc in a format that can be directly injected into GL VBOs. */ @@ -1505,7 +1498,7 @@ namespace Render static size_t loop = 0; if ((loop++ % 1000) == 0) { - printf("Texture atlas efficiency %.1f%%\n", textureAtlas->getEfficiency()); + printf("Texture atlas occupancy %.1f%%\n", textureAtlas->getOccupancy()); auto latest = textureAtlas->getLookupMap().rbegin(); printf("Latest texture atlas entry: %zu, %d, %d, %d, %d, %d\n", latest->first, diff --git a/extern/RectangleBinPack/MaxRectsBinPack.cpp b/extern/RectangleBinPack/MaxRectsBinPack.cpp new file mode 100644 index 000000000..f7dfd1ce8 --- /dev/null +++ b/extern/RectangleBinPack/MaxRectsBinPack.cpp @@ -0,0 +1,524 @@ +/** @file MaxRectsBinPack.cpp + @author Jukka Jylänki + + @brief Implements different bin packer algorithms that use the MAXRECTS data structure. + + This work is released to Public Domain, do whatever you want with it. +*/ +#include +#include +#include +#include + +#include +#include +#include + +#include "MaxRectsBinPack.h" + +namespace rbp { + +using namespace std; + +MaxRectsBinPack::MaxRectsBinPack() +:binWidth(0), +binHeight(0) +{ +} + +MaxRectsBinPack::MaxRectsBinPack(int width, int height, bool allowFlip) +{ + Init(width, height, allowFlip); +} + +void MaxRectsBinPack::Init(int width, int height, bool allowFlip) +{ + binAllowFlip = allowFlip; + binWidth = width; + binHeight = height; + + Rect n; + n.x = 0; + n.y = 0; + n.width = width; + n.height = height; + + usedRectangles.clear(); + + freeRectangles.clear(); + freeRectangles.push_back(n); +} + +Rect MaxRectsBinPack::Insert(int width, int height, FreeRectChoiceHeuristic method) +{ + Rect newNode; + // Unused in this function. We don't need to know the score after finding the position. + int score1 = std::numeric_limits::max(); + int score2 = std::numeric_limits::max(); + switch(method) + { + case RectBestShortSideFit: newNode = FindPositionForNewNodeBestShortSideFit(width, height, score1, score2); break; + case RectBottomLeftRule: newNode = FindPositionForNewNodeBottomLeft(width, height, score1, score2); break; + case RectContactPointRule: newNode = FindPositionForNewNodeContactPoint(width, height, score1); break; + case RectBestLongSideFit: newNode = FindPositionForNewNodeBestLongSideFit(width, height, score2, score1); break; + case RectBestAreaFit: newNode = FindPositionForNewNodeBestAreaFit(width, height, score1, score2); break; + } + + if (newNode.height == 0) + return newNode; + + size_t numRectanglesToProcess = freeRectangles.size(); + for(size_t i = 0; i < numRectanglesToProcess; ++i) + { + if (SplitFreeNode(freeRectangles[i], newNode)) + { + freeRectangles.erase(freeRectangles.begin() + i); + --i; + --numRectanglesToProcess; + } + } + + PruneFreeList(); + + usedRectangles.push_back(newNode); + return newNode; +} + +void MaxRectsBinPack::Insert(std::vector &rects, std::vector &dst, FreeRectChoiceHeuristic method) +{ + dst.clear(); + + while(rects.size() > 0) + { + int bestScore1 = std::numeric_limits::max(); + int bestScore2 = std::numeric_limits::max(); + int bestRectIndex = -1; + Rect bestNode; + + for(size_t i = 0; i < rects.size(); ++i) + { + int score1; + int score2; + Rect newNode = ScoreRect(rects[i].width, rects[i].height, method, score1, score2); + + if (score1 < bestScore1 || (score1 == bestScore1 && score2 < bestScore2)) + { + bestScore1 = score1; + bestScore2 = score2; + bestNode = newNode; + bestRectIndex = i; + } + } + + if (bestRectIndex == -1) + return; + + PlaceRect(bestNode); + dst.push_back(bestNode); + rects.erase(rects.begin() + bestRectIndex); + } +} + +void MaxRectsBinPack::PlaceRect(const Rect &node) +{ + size_t numRectanglesToProcess = freeRectangles.size(); + for(size_t i = 0; i < numRectanglesToProcess; ++i) + { + if (SplitFreeNode(freeRectangles[i], node)) + { + freeRectangles.erase(freeRectangles.begin() + i); + --i; + --numRectanglesToProcess; + } + } + + PruneFreeList(); + + usedRectangles.push_back(node); +} + +Rect MaxRectsBinPack::ScoreRect(int width, int height, FreeRectChoiceHeuristic method, int &score1, int &score2) const +{ + Rect newNode; + score1 = std::numeric_limits::max(); + score2 = std::numeric_limits::max(); + switch(method) + { + case RectBestShortSideFit: newNode = FindPositionForNewNodeBestShortSideFit(width, height, score1, score2); break; + case RectBottomLeftRule: newNode = FindPositionForNewNodeBottomLeft(width, height, score1, score2); break; + case RectContactPointRule: newNode = FindPositionForNewNodeContactPoint(width, height, score1); + score1 = -score1; // Reverse since we are minimizing, but for contact point score bigger is better. + break; + case RectBestLongSideFit: newNode = FindPositionForNewNodeBestLongSideFit(width, height, score2, score1); break; + case RectBestAreaFit: newNode = FindPositionForNewNodeBestAreaFit(width, height, score1, score2); break; + } + + // Cannot fit the current rectangle. + if (newNode.height == 0) + { + score1 = std::numeric_limits::max(); + score2 = std::numeric_limits::max(); + } + + return newNode; +} + +/// Computes the ratio of used surface area. +float MaxRectsBinPack::Occupancy() const +{ + unsigned long usedSurfaceArea = 0; + for(size_t i = 0; i < usedRectangles.size(); ++i) + usedSurfaceArea += usedRectangles[i].width * usedRectangles[i].height; + + return (float)usedSurfaceArea / (binWidth * binHeight); +} + +Rect MaxRectsBinPack::FindPositionForNewNodeBottomLeft(int width, int height, int &bestY, int &bestX) const +{ + Rect bestNode; + memset(&bestNode, 0, sizeof(Rect)); + + bestY = std::numeric_limits::max(); + bestX = std::numeric_limits::max(); + + for(size_t i = 0; i < freeRectangles.size(); ++i) + { + // Try to place the rectangle in upright (non-flipped) orientation. + if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) + { + int topSideY = freeRectangles[i].y + height; + if (topSideY < bestY || (topSideY == bestY && freeRectangles[i].x < bestX)) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = width; + bestNode.height = height; + bestY = topSideY; + bestX = freeRectangles[i].x; + } + } + if (binAllowFlip && freeRectangles[i].width >= height && freeRectangles[i].height >= width) + { + int topSideY = freeRectangles[i].y + width; + if (topSideY < bestY || (topSideY == bestY && freeRectangles[i].x < bestX)) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = height; + bestNode.height = width; + bestY = topSideY; + bestX = freeRectangles[i].x; + } + } + } + return bestNode; +} + +Rect MaxRectsBinPack::FindPositionForNewNodeBestShortSideFit(int width, int height, + int &bestShortSideFit, int &bestLongSideFit) const +{ + Rect bestNode; + memset(&bestNode, 0, sizeof(Rect)); + + bestShortSideFit = std::numeric_limits::max(); + bestLongSideFit = std::numeric_limits::max(); + + for(size_t i = 0; i < freeRectangles.size(); ++i) + { + // Try to place the rectangle in upright (non-flipped) orientation. + if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) + { + int leftoverHoriz = abs(freeRectangles[i].width - width); + int leftoverVert = abs(freeRectangles[i].height - height); + int shortSideFit = min(leftoverHoriz, leftoverVert); + int longSideFit = max(leftoverHoriz, leftoverVert); + + if (shortSideFit < bestShortSideFit || (shortSideFit == bestShortSideFit && longSideFit < bestLongSideFit)) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = width; + bestNode.height = height; + bestShortSideFit = shortSideFit; + bestLongSideFit = longSideFit; + } + } + + if (binAllowFlip && freeRectangles[i].width >= height && freeRectangles[i].height >= width) + { + int flippedLeftoverHoriz = abs(freeRectangles[i].width - height); + int flippedLeftoverVert = abs(freeRectangles[i].height - width); + int flippedShortSideFit = min(flippedLeftoverHoriz, flippedLeftoverVert); + int flippedLongSideFit = max(flippedLeftoverHoriz, flippedLeftoverVert); + + if (flippedShortSideFit < bestShortSideFit || (flippedShortSideFit == bestShortSideFit && flippedLongSideFit < bestLongSideFit)) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = height; + bestNode.height = width; + bestShortSideFit = flippedShortSideFit; + bestLongSideFit = flippedLongSideFit; + } + } + } + return bestNode; +} + +Rect MaxRectsBinPack::FindPositionForNewNodeBestLongSideFit(int width, int height, + int &bestShortSideFit, int &bestLongSideFit) const +{ + Rect bestNode; + memset(&bestNode, 0, sizeof(Rect)); + + bestShortSideFit = std::numeric_limits::max(); + bestLongSideFit = std::numeric_limits::max(); + + for(size_t i = 0; i < freeRectangles.size(); ++i) + { + // Try to place the rectangle in upright (non-flipped) orientation. + if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) + { + int leftoverHoriz = abs(freeRectangles[i].width - width); + int leftoverVert = abs(freeRectangles[i].height - height); + int shortSideFit = min(leftoverHoriz, leftoverVert); + int longSideFit = max(leftoverHoriz, leftoverVert); + + if (longSideFit < bestLongSideFit || (longSideFit == bestLongSideFit && shortSideFit < bestShortSideFit)) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = width; + bestNode.height = height; + bestShortSideFit = shortSideFit; + bestLongSideFit = longSideFit; + } + } + + if (binAllowFlip && freeRectangles[i].width >= height && freeRectangles[i].height >= width) + { + int leftoverHoriz = abs(freeRectangles[i].width - height); + int leftoverVert = abs(freeRectangles[i].height - width); + int shortSideFit = min(leftoverHoriz, leftoverVert); + int longSideFit = max(leftoverHoriz, leftoverVert); + + if (longSideFit < bestLongSideFit || (longSideFit == bestLongSideFit && shortSideFit < bestShortSideFit)) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = height; + bestNode.height = width; + bestShortSideFit = shortSideFit; + bestLongSideFit = longSideFit; + } + } + } + return bestNode; +} + +Rect MaxRectsBinPack::FindPositionForNewNodeBestAreaFit(int width, int height, + int &bestAreaFit, int &bestShortSideFit) const +{ + Rect bestNode; + memset(&bestNode, 0, sizeof(Rect)); + + bestAreaFit = std::numeric_limits::max(); + bestShortSideFit = std::numeric_limits::max(); + + for(size_t i = 0; i < freeRectangles.size(); ++i) + { + int areaFit = freeRectangles[i].width * freeRectangles[i].height - width * height; + + // Try to place the rectangle in upright (non-flipped) orientation. + if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) + { + int leftoverHoriz = abs(freeRectangles[i].width - width); + int leftoverVert = abs(freeRectangles[i].height - height); + int shortSideFit = min(leftoverHoriz, leftoverVert); + + if (areaFit < bestAreaFit || (areaFit == bestAreaFit && shortSideFit < bestShortSideFit)) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = width; + bestNode.height = height; + bestShortSideFit = shortSideFit; + bestAreaFit = areaFit; + } + } + + if (binAllowFlip && freeRectangles[i].width >= height && freeRectangles[i].height >= width) + { + int leftoverHoriz = abs(freeRectangles[i].width - height); + int leftoverVert = abs(freeRectangles[i].height - width); + int shortSideFit = min(leftoverHoriz, leftoverVert); + + if (areaFit < bestAreaFit || (areaFit == bestAreaFit && shortSideFit < bestShortSideFit)) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = height; + bestNode.height = width; + bestShortSideFit = shortSideFit; + bestAreaFit = areaFit; + } + } + } + return bestNode; +} + +/// Returns 0 if the two intervals i1 and i2 are disjoint, or the length of their overlap otherwise. +int CommonIntervalLength(int i1start, int i1end, int i2start, int i2end) +{ + if (i1end < i2start || i2end < i1start) + return 0; + return min(i1end, i2end) - max(i1start, i2start); +} + +int MaxRectsBinPack::ContactPointScoreNode(int x, int y, int width, int height) const +{ + int score = 0; + + if (x == 0 || x + width == binWidth) + score += height; + if (y == 0 || y + height == binHeight) + score += width; + + for(size_t i = 0; i < usedRectangles.size(); ++i) + { + if (usedRectangles[i].x == x + width || usedRectangles[i].x + usedRectangles[i].width == x) + score += CommonIntervalLength(usedRectangles[i].y, usedRectangles[i].y + usedRectangles[i].height, y, y + height); + if (usedRectangles[i].y == y + height || usedRectangles[i].y + usedRectangles[i].height == y) + score += CommonIntervalLength(usedRectangles[i].x, usedRectangles[i].x + usedRectangles[i].width, x, x + width); + } + return score; +} + +Rect MaxRectsBinPack::FindPositionForNewNodeContactPoint(int width, int height, int &bestContactScore) const +{ + Rect bestNode; + memset(&bestNode, 0, sizeof(Rect)); + + bestContactScore = -1; + + for(size_t i = 0; i < freeRectangles.size(); ++i) + { + // Try to place the rectangle in upright (non-flipped) orientation. + if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) + { + int score = ContactPointScoreNode(freeRectangles[i].x, freeRectangles[i].y, width, height); + if (score > bestContactScore) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = width; + bestNode.height = height; + bestContactScore = score; + } + } + if (freeRectangles[i].width >= height && freeRectangles[i].height >= width) + { + int score = ContactPointScoreNode(freeRectangles[i].x, freeRectangles[i].y, height, width); + if (score > bestContactScore) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = height; + bestNode.height = width; + bestContactScore = score; + } + } + } + return bestNode; +} + +bool MaxRectsBinPack::SplitFreeNode(Rect freeNode, const Rect &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) + { + Rect newNode = freeNode; + newNode.height = usedNode.y - newNode.y; + freeRectangles.push_back(newNode); + } + + // New node at the bottom side of the used node. + if (usedNode.y + usedNode.height < freeNode.y + freeNode.height) + { + Rect newNode = freeNode; + newNode.y = usedNode.y + usedNode.height; + newNode.height = freeNode.y + freeNode.height - (usedNode.y + usedNode.height); + freeRectangles.push_back(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) + { + Rect newNode = freeNode; + newNode.width = usedNode.x - newNode.x; + freeRectangles.push_back(newNode); + } + + // New node at the right side of the used node. + if (usedNode.x + usedNode.width < freeNode.x + freeNode.width) + { + Rect newNode = freeNode; + newNode.x = usedNode.x + usedNode.width; + newNode.width = freeNode.x + freeNode.width - (usedNode.x + usedNode.width); + freeRectangles.push_back(newNode); + } + } + + return true; +} + +void MaxRectsBinPack::PruneFreeList() +{ + /* + /// Would be nice to do something like this, to avoid a Theta(n^2) loop through each pair. + /// But unfortunately it doesn't quite cut it, since we also want to detect containment. + /// Perhaps there's another way to do this faster than Theta(n^2). + + if (freeRectangles.size() > 0) + clb::sort::QuickSort(&freeRectangles[0], freeRectangles.size(), NodeSortCmp); + + for(size_t i = 0; i < freeRectangles.size()-1; ++i) + if (freeRectangles[i].x == freeRectangles[i+1].x && + freeRectangles[i].y == freeRectangles[i+1].y && + freeRectangles[i].width == freeRectangles[i+1].width && + freeRectangles[i].height == freeRectangles[i+1].height) + { + freeRectangles.erase(freeRectangles.begin() + i); + --i; + } + */ + + /// Go through each pair and remove any rectangle that is redundant. + for(size_t i = 0; i < freeRectangles.size(); ++i) + for(size_t j = i+1; j < freeRectangles.size(); ++j) + { + if (IsContainedIn(freeRectangles[i], freeRectangles[j])) + { + freeRectangles.erase(freeRectangles.begin()+i); + --i; + break; + } + if (IsContainedIn(freeRectangles[j], freeRectangles[i])) + { + freeRectangles.erase(freeRectangles.begin()+j); + --j; + } + } +} + +} diff --git a/extern/RectangleBinPack/MaxRectsBinPack.h b/extern/RectangleBinPack/MaxRectsBinPack.h new file mode 100644 index 000000000..c21a7859e --- /dev/null +++ b/extern/RectangleBinPack/MaxRectsBinPack.h @@ -0,0 +1,88 @@ +/** @file MaxRectsBinPack.h + @author Jukka Jyl�nki + + @brief Implements different bin packer algorithms that use the MAXRECTS data structure. + + This work is released to Public Domain, do whatever you want with it. +*/ +#pragma once + +#include + +#include "Rect.h" + +namespace rbp { + +/** MaxRectsBinPack implements the MAXRECTS data structure and different bin packing algorithms that + use this structure. */ +class MaxRectsBinPack +{ +public: + /// Instantiates a bin of size (0,0). Call Init to create a new bin. + MaxRectsBinPack(); + + /// Instantiates a bin of the given size. + /// @param allowFlip Specifies whether the packing algorithm is allowed to rotate the input rectangles by 90 degrees to consider a better placement. + MaxRectsBinPack(int width, int height, bool allowFlip = true); + + /// (Re)initializes the packer to an empty bin of width x height units. Call whenever + /// you need to restart with a new bin. + void Init(int width, int height, bool allowFlip = true); + + /// Specifies the different heuristic rules that can be used when deciding where to place a new rectangle. + enum FreeRectChoiceHeuristic + { + RectBestShortSideFit, ///< -BSSF: Positions the rectangle against the short side of a free rectangle into which it fits the best. + RectBestLongSideFit, ///< -BLSF: Positions the rectangle against the long side of a free rectangle into which it fits the best. + RectBestAreaFit, ///< -BAF: Positions the rectangle into the smallest free rect into which it fits. + RectBottomLeftRule, ///< -BL: Does the Tetris placement. + RectContactPointRule ///< -CP: Choosest the placement where the rectangle touches other rects as much as possible. + }; + + /// Inserts the given list of rectangles in an offline/batch mode, possibly rotated. + /// @param rects The list of rectangles to insert. This vector will be destroyed in the process. + /// @param dst [out] This list will contain the packed rectangles. The indices will not correspond to that of rects. + /// @param method The rectangle placement rule to use when packing. + void Insert(std::vector &rects, std::vector &dst, FreeRectChoiceHeuristic method); + + /// Inserts a single rectangle into the bin, possibly rotated. + Rect Insert(int width, int height, FreeRectChoiceHeuristic method); + + /// Computes the ratio of used surface area to the total bin area. + float Occupancy() const; + +private: + int binWidth; + int binHeight; + + bool binAllowFlip; + + std::vector usedRectangles; + std::vector freeRectangles; + + /// Computes the placement score for placing the given rectangle with the given method. + /// @param score1 [out] The primary placement score will be outputted here. + /// @param score2 [out] The secondary placement score will be outputted here. This isu sed to break ties. + /// @return This struct identifies where the rectangle would be placed if it were placed. + Rect ScoreRect(int width, int height, FreeRectChoiceHeuristic method, int &score1, int &score2) const; + + /// Places the given rectangle into the bin. + void PlaceRect(const Rect &node); + + /// Computes the placement score for the -CP variant. + int ContactPointScoreNode(int x, int y, int width, int height) const; + + Rect FindPositionForNewNodeBottomLeft(int width, int height, int &bestY, int &bestX) const; + Rect FindPositionForNewNodeBestShortSideFit(int width, int height, int &bestShortSideFit, int &bestLongSideFit) const; + Rect FindPositionForNewNodeBestLongSideFit(int width, int height, int &bestShortSideFit, int &bestLongSideFit) const; + Rect FindPositionForNewNodeBestAreaFit(int width, int height, int &bestAreaFit, int &bestShortSideFit) const; + Rect FindPositionForNewNodeContactPoint(int width, int height, int &contactScore) const; + + /// @return True if the free node was split. + bool SplitFreeNode(Rect freeNode, const Rect &usedNode); + + /// Goes through the free rectangle list and removes any redundant entries. + void PruneFreeList(); +}; + +} diff --git a/extern/RectangleBinPack/Rect.cpp b/extern/RectangleBinPack/Rect.cpp new file mode 100644 index 000000000..9b28cfc99 --- /dev/null +++ b/extern/RectangleBinPack/Rect.cpp @@ -0,0 +1,51 @@ +/** @file Rect.cpp + @author Jukka Jyl�nki + + This work is released to Public Domain, do whatever you want with it. +*/ +#include + +#include "Rect.h" + +namespace rbp { + +/* +#include "clb/Algorithm/Sort.h" + +int CompareRectShortSide(const Rect &a, const Rect &b) +{ + using namespace std; + + int smallerSideA = min(a.width, a.height); + int smallerSideB = min(b.width, b.height); + + if (smallerSideA != smallerSideB) + return clb::sort::TriCmp(smallerSideA, smallerSideB); + + // Tie-break on the larger side. + int largerSideA = max(a.width, a.height); + int largerSideB = max(b.width, b.height); + + return clb::sort::TriCmp(largerSideA, largerSideB); +} +*/ +/* +int NodeSortCmp(const Rect &a, const Rect &b) +{ + if (a.x != b.x) + return clb::sort::TriCmp(a.x, b.x); + if (a.y != b.y) + return clb::sort::TriCmp(a.y, b.y); + if (a.width != b.width) + return clb::sort::TriCmp(a.width, b.width); + return clb::sort::TriCmp(a.height, b.height); +} +*/ +bool IsContainedIn(const Rect &a, const Rect &b) +{ + 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; +} + +} diff --git a/extern/RectangleBinPack/Rect.h b/extern/RectangleBinPack/Rect.h new file mode 100644 index 000000000..40916a0f9 --- /dev/null +++ b/extern/RectangleBinPack/Rect.h @@ -0,0 +1,94 @@ +/** @file Rect.h + @author Jukka Jyl�nki + + This work is released to Public Domain, do whatever you want with it. +*/ +#pragma once + +#include +#include +#include + +//#ifdef _DEBUG +///// debug_assert is an assert that also requires debug mode to be defined. +//#define debug_assert(x) assert(x) +//#else +//#define debug_assert(x) +//#endif + +//using namespace std; + +namespace rbp { + +struct RectSize +{ + int width; + int height; +}; + +struct Rect +{ + int x; + int y; + int width; + int height; +}; + +/// Performs a lexicographic compare on (rect short side, rect long side). +/// @return -1 if the smaller side of a is shorter than the smaller side of b, 1 if the other way around. +/// If they are equal, the larger side length is used as a tie-breaker. +/// If the rectangles are of same size, returns 0. +int CompareRectShortSide(const Rect &a, const Rect &b); + +/// Performs a lexicographic compare on (x, y, width, height). +int NodeSortCmp(const Rect &a, const Rect &b); + +/// Returns true if a is contained in b. +bool IsContainedIn(const Rect &a, const Rect &b); + +class DisjointRectCollection +{ +public: + std::vector rects; + + bool Add(const Rect &r) + { + // Degenerate rectangles are ignored. + if (r.width == 0 || r.height == 0) + return true; + + if (!Disjoint(r)) + return false; + rects.push_back(r); + return true; + } + + void Clear() + { + rects.clear(); + } + + bool Disjoint(const Rect &r) const + { + // Degenerate rectangles are ignored. + if (r.width == 0 || r.height == 0) + return true; + + for(size_t i = 0; i < rects.size(); ++i) + if (!Disjoint(rects[i], r)) + return false; + return true; + } + + static bool Disjoint(const Rect &a, const Rect &b) + { + if (a.x + a.width <= b.x || + b.x + b.width <= a.x || + a.y + a.height <= b.y || + b.y + b.height <= a.y) + return true; + return false; + } +}; + +}