浏览代码

BRL.RectPacker. Initial Import.

Brucey 11 月之前
父节点
当前提交
0a8c7f1040

+ 24 - 0
rectpacker.mod/examples/example_01.bmx

@@ -0,0 +1,24 @@
+SuperStrict
+
+Framework BRL.standardio
+Import BRL.RectPacker
+
+Local packer:TRectPacker = New TRectPacker
+
+packer.Add(32, 32, 0)
+packer.Add(64, 64, 1)
+packer.Add(128, 128, 2)
+packer.Add(256, 256, 3)
+packer.Add(512, 512, 4)
+packer.Add(1024, 1024, 5)
+
+Local sheets:TPackedSheet[] = packer.Pack()
+
+For Local i:Int = 0 Until sheets.Length
+	Local sheet:TPackedSheet = sheets[i]
+	Print "Sheet: " + i + " : " + sheet.width + " " + sheet.height
+	For Local j:Int = 0 Until sheet.rects.Length
+		Local rect:SPackedRect = sheet.rects[j]
+		Print "  Rect: " + j + " " + rect.x + " " + rect.y + " " + rect.width + " " + rect.height
+	Next
+Next

+ 72 - 0
rectpacker.mod/glue.cpp

@@ -0,0 +1,72 @@
+/*
+  Copyright (c) 2024 Bruce A Henderson
+  
+  This software is provided 'as-is', without any express or implied
+  warranty. In no event will the authors be held liable for any damages
+  arising from the use of this software.
+  
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+  
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/ 
+#include "rect_pack.h"
+
+#include "brl.mod/blitz.mod/blitz.h"
+
+extern "C" {
+
+    void brl_rectpacker_TRectPacker__GetSize(BBObject * packer, int index, int * width, int * height, int * id);
+    BBArray * brl_rectpacker_TRectPacker__NewSheetArray(int size);
+    void brl_rectpacker_TRectPacker__SetSheet(BBArray * sheets, int index, BBObject * sheet);
+    BBObject * brl_rectpacker_TPackedSheet__Create(int width, int height, int size);
+    void brl_rectpacker_TPackedSheet__SetRect(BBObject * sheet, int index, int id, int x, int y, int width, int height, int rotated);
+
+    BBArray * bmx_rectpacker_pack(BBObject * packer, int packingMethod, int maxSheets, int powerOfTwo, int square, int allowRotate, int alignWidth, int borderPadding, int overAllocate, int minWidth, int minHeight, int maxWidth, int maxHeight, int count);
+}
+
+BBArray * bmx_rectpacker_pack(BBObject * packer, int packingMethod, int maxSheets, int powerOfTwo, int square, int allowRotate, int alignWidth, int borderPadding, int overAllocate, int minWidth, int minHeight, int maxWidth, int maxHeight, int count) {
+    rect_pack::Settings settings;
+    settings.method = static_cast<rect_pack::Method>(packingMethod);
+    settings.max_sheets = maxSheets;
+    settings.power_of_two = static_cast<bool>(powerOfTwo);
+    settings.square = static_cast<bool>(square);
+    settings.allow_rotate = static_cast<bool>(allowRotate);
+    settings.align_width = static_cast<bool>(alignWidth);
+    settings.border_padding = borderPadding;
+    settings.over_allocate = overAllocate;
+    settings.min_width = minWidth;
+    settings.min_height = minHeight;
+    settings.max_width = maxWidth;
+    settings.max_height = maxHeight;
+
+    std::vector<rect_pack::Size> sizes;
+
+    for (int i = 0; i < count; i++) {
+        rect_pack::Size s;
+        brl_rectpacker_TRectPacker__GetSize(packer, i, &s.width, &s.height, &s.id);
+        sizes.push_back(s);
+    }
+
+    std::vector<rect_pack::Sheet> sheets = rect_pack::pack(settings, sizes);
+
+    BBArray * result = brl_rectpacker_TRectPacker__NewSheetArray(sheets.size());
+
+    for (int i = 0; i < sheets.size(); i++) {
+        BBObject * sheet = brl_rectpacker_TPackedSheet__Create(sheets[i].width, sheets[i].height, sheets[i].rects.size());
+        for (int j = 0; j < sheets[i].rects.size(); j++) {
+            rect_pack::Rect r = sheets[i].rects[j];
+            brl_rectpacker_TPackedSheet__SetRect(sheet, j, r.id, r.x, r.y, r.width, r.height, r.rotated);
+        }
+        brl_rectpacker_TRectPacker__SetSheet(result, i, sheet);
+    }
+
+    return result;
+}

+ 24 - 0
rectpacker.mod/rect_pack/LICENSE

@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <https://unlicense.org>

+ 583 - 0
rectpacker.mod/rect_pack/MaxRectsBinPack.cpp

@@ -0,0 +1,583 @@
+/** @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 <algorithm>
+#include <utility>
+#include <iostream>
+#include <limits>
+
+#include <cassert>
+#include <cstring>
+#include <cmath>
+
+#include "MaxRectsBinPack.h"
+
+#define RBP_ENABLE_OPTIMIZATIONS
+#define RBP_REVERSE_ORDER
+
+namespace rbp {
+
+#if defined(RBP_ENABLE_OPTIMIZATIONS)
+// order of elements after erased element is not stable
+template<typename C, typename It>
+void erase_unstable(C& container, const It& it) {
+	std::swap(*it, container.back());
+	container.pop_back();
+}
+#endif
+
+using namespace std;
+
+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;
+}
+
+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<int>::max();
+	int score2 = std::numeric_limits<int>::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))
+		{
+#if defined(RBP_ENABLE_OPTIMIZATIONS)
+			erase_unstable(freeRectangles, freeRectangles.begin() + i);
+#else
+			freeRectangles.erase(freeRectangles.begin() + i);
+#endif
+			--i;
+			--numRectanglesToProcess;
+		}
+	}
+
+	PruneFreeList();
+
+	usedRectangles.push_back(newNode);
+	return newNode;
+}
+
+void MaxRectsBinPack::Insert(std::vector<RectSize> &rects, std::vector<Rect> &dst, FreeRectChoiceHeuristic method)
+{
+	dst.clear();
+
+	while(rects.size() > 0)
+	{
+		int bestScore1 = std::numeric_limits<int>::max();
+		int bestScore2 = std::numeric_limits<int>::max();
+		int bestRectIndex = -1;
+		Rect bestNode;
+
+#if defined(RBP_REVERSE_ORDER)
+		for(int i = static_cast<int>(rects.size()) - 1; i >= 0; --i)
+#else
+		for(size_t i = 0; i < rects.size(); ++i)
+#endif
+		{
+			int score1;
+			int score2;
+			Rect newNode = ScoreRect(rects[i].width, rects[i].height, method, score1, score2);
+			newNode.id = rects[i].id;
+
+			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);
+
+#if defined(RBP_ENABLE_OPTIMIZATIONS)
+		erase_unstable(rects, rects.begin() + bestRectIndex);
+#else
+		rects.erase(rects.begin() + bestRectIndex);
+#endif
+	}
+}
+
+void MaxRectsBinPack::PlaceRect(const Rect &node)
+{
+	size_t numRectanglesToProcess = freeRectangles.size();
+	for(size_t i = 0; i < numRectanglesToProcess; ++i)
+	{
+		if (SplitFreeNode(freeRectangles[i], node))
+		{
+#if defined(RBP_ENABLE_OPTIMIZATIONS)
+			const auto current = freeRectangles.begin() + i;
+			const auto last = freeRectangles.begin() + numRectanglesToProcess - 1;
+			std::swap(*current, *last);
+			erase_unstable(freeRectangles, last);
+#else
+			freeRectangles.erase(freeRectangles.begin() + i);
+#endif
+			--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<int>::max();
+	score2 = std::numeric_limits<int>::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<int>::max();
+		score2 = std::numeric_limits<int>::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);
+}
+
+std::pair<int, int> MaxRectsBinPack::BottomRight() const
+{
+	int x = 0;
+	int y = 0;
+	for(size_t i = 0; i < usedRectangles.size(); ++i) {
+		x = std::max(x, usedRectangles[i].x + usedRectangles[i].width);
+		y = std::max(y, usedRectangles[i].y + usedRectangles[i].height);
+  }
+	return { x, y };
+}
+
+Rect MaxRectsBinPack::FindPositionForNewNodeBottomLeft(int width, int height, int &bestY, int &bestX) const
+{
+	Rect bestNode;
+	memset(&bestNode, 0, sizeof(Rect));
+
+	bestY = std::numeric_limits<int>::max();
+	bestX = std::numeric_limits<int>::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<int>::max();
+	bestLongSideFit = std::numeric_limits<int>::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<int>::max();
+	bestLongSideFit = std::numeric_limits<int>::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<int>::max();
+	bestShortSideFit = std::numeric_limits<int>::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 (binAllowFlip && 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]))
+			{
+#if defined(RBP_ENABLE_OPTIMIZATIONS)
+				erase_unstable(freeRectangles, freeRectangles.begin()+i);
+#else
+				freeRectangles.erase(freeRectangles.begin()+i);
+#endif
+				--i;
+				break;
+			}
+			if (IsContainedIn(freeRectangles[j], freeRectangles[i]))
+			{
+#if defined(RBP_ENABLE_OPTIMIZATIONS)
+				erase_unstable(freeRectangles, freeRectangles.begin()+j);
+#else
+				freeRectangles.erase(freeRectangles.begin()+j);
+#endif
+				--j;
+			}
+		}
+}
+
+}

+ 107 - 0
rectpacker.mod/rect_pack/MaxRectsBinPack.h

@@ -0,0 +1,107 @@
+/** @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 <vector>
+#include <utility>
+
+namespace rbp {
+
+struct RectSize
+{
+	int width;
+	int height;
+
+	int id;
+};
+
+struct Rect
+{
+	int x;
+	int y;
+	int width;
+	int height;
+
+	int id;
+};
+
+/** 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<RectSize> &rects, std::vector<Rect> &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;
+
+  std::pair<int, int> BottomRight() const;
+
+private:
+	int binWidth;
+	int binHeight;
+
+	bool binAllowFlip;
+
+	std::vector<Rect> usedRectangles;
+	std::vector<Rect> 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();
+};
+
+}

+ 70 - 0
rectpacker.mod/rect_pack/README.md

@@ -0,0 +1,70 @@
+# Multi-sheet rectangle packing
+
+A C++17 library for packing rectangles to one or more sprite sheets/atlases with optional constraints.
+
+It is part of the [spright](https://github.com/houmain/spright) project and utilizing [Sean T. Barrett's Skyline](https://github.com/nothings/stb) and  [Jukka Jylänki's MaxRects](https://github.com/juj/RectangleBinPack) packing algorithm implementations.
+
+Simply pass your sheet constraints and the rectangle sizes to the _pack_ function. It will return one or more sheets with rectangle positions. The _id_ can be used to correlate in- and output (there are no rectangles for sizes which did not fit).
+
+For now the header may serve as documentation:
+
+## rect_pack.h
+
+```cpp
+#include <vector>
+
+namespace rect_pack {
+
+enum class Method {
+  Best,
+  Best_Skyline,
+  Best_MaxRects,
+  Skyline_BottomLeft,
+  Skyline_BestFit,
+  MaxRects_BestShortSideFit,
+  MaxRects_BestLongSideFit,
+  MaxRects_BestAreaFit,
+  MaxRects_BottomLeftRule,
+  MaxRects_ContactPointRule
+};
+
+struct Size {
+  int id;
+  int width;
+  int height;
+};
+
+struct Rect {
+  int id;
+  int x;
+  int y;
+  int width;
+  int height;
+  bool rotated;
+};
+
+struct Sheet {
+  int width;
+  int height;
+  std::vector<Rect> rects;
+};
+
+struct Settings {
+  Method method;
+  int max_sheets;
+  bool power_of_two;
+  bool square;
+  bool allow_rotate;
+  int align_width;
+  int border_padding;
+  int over_allocate;
+  int min_width;
+  int min_height;
+  int max_width;
+  int max_height;
+};
+
+std::vector<Sheet> pack(Settings settings, std::vector<Size> sizes);
+
+} // namespace
+```

+ 596 - 0
rectpacker.mod/rect_pack/rect_pack.cpp

@@ -0,0 +1,596 @@
+
+#define STBRP_LARGE_RECTS
+#include "rect_pack.h"
+#include "MaxRectsBinPack.h"
+#include "stb_rect_pack.h"
+#include <optional>
+#include <algorithm>
+#include <cmath>
+#include <cassert>
+
+namespace rect_pack {
+
+namespace {
+  const auto first_Skyline_method = Method::Skyline_BottomLeft;
+  const auto last_Skyline_method = Method::Skyline_BestFit;
+  const auto first_MaxRects_method = Method::MaxRects_BestShortSideFit;
+  const auto last_MaxRects_method = Method::MaxRects_ContactPointRule;
+
+  int floor(int v, int q) { return (v / q) * q; };
+  int ceil(int v, int q) { return ((v + q - 1) / q) * q; };
+  int sqrt(int a) { return static_cast<int>(std::sqrt(a)); }
+  int div_ceil(int a, int b) { return (b > 0 ? (a + b - 1) / b : -1); }
+
+  int ceil_to_pot(int value) {
+    for (auto pot = 1; ; pot <<= 1)
+      if (pot >= value)
+        return pot;
+  }
+
+  int floor_to_pot(int value) {
+    for (auto pot = 1; ; pot <<= 1)
+      if (pot > value)
+        return (pot >> 1);
+  }
+
+  bool is_stb_method(Method method) {
+    const auto first = static_cast<int>(first_Skyline_method);
+    const auto last = static_cast<int>(last_Skyline_method);
+    const auto index = static_cast<int>(method);
+    return (index >= first && index <= last);
+  }
+
+  bool is_rbp_method(Method method) {
+    const auto first = static_cast<int>(first_MaxRects_method);
+    const auto last = static_cast<int>(last_MaxRects_method);
+    const auto index = static_cast<int>(method);
+    return (index >= first && index <= last);
+  }
+
+  int to_stb_method(Method method) {
+    assert(is_stb_method(method));
+    return static_cast<int>(method) - static_cast<int>(first_Skyline_method);
+  }
+
+  rbp::MaxRectsBinPack::FreeRectChoiceHeuristic to_rbp_method(Method method) {
+    assert(is_rbp_method(method));
+    return static_cast<rbp::MaxRectsBinPack::FreeRectChoiceHeuristic>(
+      static_cast<int>(method) - static_cast<int>(first_MaxRects_method));
+  }
+
+  std::vector<Method> get_concrete_methods(Method settings_method) {
+    auto methods = std::vector<Method>();
+    const auto add_skyline_methods = [&]() {
+      methods.insert(end(methods), {
+        Method::Skyline_BottomLeft,
+        Method::Skyline_BestFit
+      });
+    };
+    const auto add_maxrect_methods = [&]() {
+      methods.insert(end(methods), {
+        Method::MaxRects_BestShortSideFit,
+        Method::MaxRects_BestLongSideFit,
+        Method::MaxRects_BestAreaFit,
+        Method::MaxRects_BottomLeftRule,
+        // do not automatically try costy contact point rule
+        // Method::MaxRects_ContactPointRule,
+      });
+    };
+    switch (settings_method) {
+      case Method::Best:
+        add_skyline_methods();
+        add_maxrect_methods();
+        break;
+
+      case Method::Best_Skyline:
+        add_skyline_methods();
+        break;
+
+      case Method::Best_MaxRects:
+        add_maxrect_methods();
+        break;
+
+      default:
+        methods.push_back(settings_method);
+        break;
+    }
+    return methods;
+  }
+
+  bool can_fit(const Settings& settings, int width, int height) {
+    return ((width <= settings.max_width &&
+             height <= settings.max_height) ||
+             (settings.allow_rotate &&
+              width <= settings.max_height &&
+              height <= settings.max_width));
+  }
+
+  void apply_padding(const Settings& settings, int& width, int& height, bool indent) {
+    const auto dir = (indent ? 1 : -1);
+    width -= dir * settings.border_padding * 2;
+    height -= dir * settings.border_padding * 2;
+    width += dir * settings.over_allocate;
+    height += dir * settings.over_allocate;
+  }
+
+  bool correct_settings(Settings& settings, std::vector<Size>& sizes) {
+    // clamp max to far less than numeric_limits<int>::max() to prevent overflow
+    const auto size_limit = 1'000'000'000;
+    if (settings.max_width <= 0 || settings.max_width > size_limit)
+      settings.max_width = size_limit;
+    if (settings.max_height <= 0 || settings.max_height > size_limit)
+      settings.max_height = size_limit;
+
+    if (settings.min_width < 0 ||
+        settings.min_height < 0 ||
+        settings.min_width > settings.max_width ||
+        settings.min_height > settings.max_height)
+      return false;
+
+    // immediately apply padding and over allocation, only relevant for power-of-two and alignment constraint
+    apply_padding(settings, settings.min_width, settings.min_height, true);
+    apply_padding(settings, settings.max_width, settings.max_height, true);
+
+    auto max_rect_width = 0;
+    auto max_rect_height = 0;
+    for (auto it = begin(sizes); it != end(sizes); )
+      if (it->width <= 0 ||
+          it->height <= 0 ||
+          !can_fit(settings, it->width, it->height)) {
+        it = sizes.erase(it);
+      }
+      else {
+        if (settings.allow_rotate && 
+            it->height > it->width && 
+            it->height <= settings.max_width &&
+            it->width <= settings.max_height) {
+          max_rect_width = std::max(max_rect_width, it->height);
+          max_rect_height = std::max(max_rect_height, it->width);
+        }
+        else {
+          max_rect_width = std::max(max_rect_width, it->width);
+          max_rect_height = std::max(max_rect_height, it->height);
+        }
+        ++it;
+      }
+
+    settings.min_width = std::max(settings.min_width, max_rect_width);
+    settings.min_height = std::max(settings.min_height, max_rect_height);
+
+    // clamp min to max and still pack the sprites which fit
+    settings.min_width = std::min(settings.min_width, settings.max_width);
+    settings.min_height = std::min(settings.min_height, settings.max_height);
+    return true;
+  }
+
+  struct Run {
+    Method method;
+    int width;
+    int height;
+    std::vector<Sheet> sheets;
+    int total_area;
+  };
+
+  void correct_size(const Settings& settings, int& width, int& height) {
+    width = std::max(width, settings.min_width);
+    height = std::max(height, settings.min_height);
+    apply_padding(settings, width, height, false);
+
+    if (settings.power_of_two) {
+      width = ceil_to_pot(width);
+      height = ceil_to_pot(height);
+    }
+
+    if (settings.align_width)
+      width = ceil(width, settings.align_width);
+
+    if (settings.square)
+      width = height = std::max(width, height);
+
+    apply_padding(settings, width, height, true);
+    width = std::min(width, settings.max_width);
+    height = std::min(height, settings.max_height);
+    apply_padding(settings, width, height, false);
+
+    if (settings.power_of_two) {
+      width = floor_to_pot(width);
+      height = floor_to_pot(height);
+    }
+
+    if (settings.align_width)
+      width = floor(width, settings.align_width);
+
+    if (settings.square)
+      width = height = std::min(width, height);
+
+    apply_padding(settings, width, height, true);
+  }
+
+  bool is_better_than(const Run& a, const Run& b, bool a_incomplete = false) {
+    if (a_incomplete) {
+      if (b.sheets.size() <= a.sheets.size())
+        return false;
+    }
+    else {
+      if (a.sheets.size() < b.sheets.size())
+        return true;
+      if (b.sheets.size() < a.sheets.size())
+        return false;
+    }
+    return (a.total_area < b.total_area);
+  }
+
+  int get_perfect_area(const std::vector<Size>& sizes) {
+    auto area = 0;
+    for (const auto& size : sizes)
+      area += size.width * size.height;
+    return area;
+  }
+
+  std::pair<int, int> get_run_size(const Settings& settings, int area) {
+    auto width = sqrt(area);
+    auto height = div_ceil(area, width);
+    if (width < settings.min_width || width > settings.max_width) {
+      width = std::clamp(width, settings.min_width, settings.max_width);
+      height = div_ceil(area, width);
+    }
+    else if (height < settings.min_height || height > settings.max_height) {
+      height = std::clamp(height, settings.min_height, settings.max_height);
+      width = div_ceil(area, height);
+    }
+    correct_size(settings, width, height);
+    return { width, height };
+  }
+
+  std::pair<int, int> get_initial_run_size(const Settings& settings, int perfect_area) {
+    return get_run_size(settings, perfect_area * 5 / 4);
+  }
+
+  enum class OptimizationStage {
+    first_run,
+    minimize_sheet_count,
+    shrink_square,
+    shrink_width_fast,
+    shrink_height_fast,
+    shrink_width_slow,
+    shrink_height_slow,
+    end
+  };
+
+  struct OptimizationState {
+    const int perfect_area;
+    int width;
+    int height;
+    OptimizationStage stage;
+    int iteration;
+  };
+
+  bool advance(OptimizationStage& stage) {
+    if (stage == OptimizationStage::end)
+      return false;
+    stage = static_cast<OptimizationStage>(static_cast<int>(stage) + 1);
+    return true;
+  }
+
+  // returns true when stage should be kept, false to advance
+  bool optimize_stage(OptimizationState& state,
+      const Settings& pack_settings, const Run& best_run) {
+
+    switch (state.stage) {
+      case OptimizationStage::first_run:
+      case OptimizationStage::end:
+        return false;
+
+      case OptimizationStage::minimize_sheet_count: {
+        if (best_run.sheets.size() <= 1 ||
+            state.iteration > 5)
+          return false;
+
+        const auto& last_sheet = best_run.sheets.back();
+        auto area = last_sheet.width * last_sheet.height;
+        for (auto i = 0; area > 0; ++i) {
+          if (state.width == pack_settings.max_width &&
+              state.height == pack_settings.max_height)
+            break;
+          if (state.height == pack_settings.max_height ||
+              (state.width < pack_settings.max_width && i % 2)) {
+            ++state.width;
+            area -= state.height;
+          }
+          else {
+            ++state.height;
+            area -= state.width;
+          }
+        }
+        return true;
+      }
+
+      case OptimizationStage::shrink_square: {
+        if (state.width != best_run.width ||
+            state.height != best_run.height ||
+            state.iteration > 5)
+          return false;
+
+        const auto [width, height] = get_run_size(pack_settings, state.perfect_area);
+        state.width = (state.width + width) / 2;
+        state.height = (state.height + height) / 2;
+        return true;
+      }
+
+      case OptimizationStage::shrink_width_fast:
+      case OptimizationStage::shrink_height_fast:
+      case OptimizationStage::shrink_width_slow:
+      case OptimizationStage::shrink_height_slow: {
+        if (state.iteration > 5)
+          return false;
+
+        const auto [width, height] = get_run_size(pack_settings, state.perfect_area);
+        switch (state.stage) {
+          default:
+          case OptimizationStage::shrink_width_fast:
+            if (state.width > width + 4)
+              state.width = (state.width + width) / 2;
+            break;
+          case OptimizationStage::shrink_height_fast:
+            if (state.height > height + 4)
+              state.height = (state.height + height) / 2;
+            break;
+          case OptimizationStage::shrink_width_slow:
+            if (state.width > width)
+              --state.width;
+            break;
+          case OptimizationStage::shrink_height_slow:
+            if (state.height > height)
+              --state.height;
+            break;
+        }
+        return true;
+      }
+    }
+    return false;
+  }
+
+  bool optimize_run_settings(OptimizationState& state,
+      const Settings& pack_settings, const Run& best_run) {
+
+    const auto previous_state = state;
+    for (;;) {
+      if (!optimize_stage(state, pack_settings, best_run))
+        if (advance(state.stage)) {
+          state.width = best_run.width;
+          state.height = best_run.height;
+          state.iteration = 0;
+          continue;
+        }
+
+      if (state.stage == OptimizationStage::end)
+        return false;
+
+      ++state.iteration;
+
+      auto width = state.width;
+      auto height = state.height;
+      correct_size(pack_settings, width, height);
+      if (width != previous_state.width ||
+          height != previous_state.height) {
+        state.width = width;
+        state.height = height;
+        return true;
+      }
+    }
+  }
+
+  template<typename T>
+  void copy_vector(const T& source, T& dest) {
+    dest.resize(source.size());
+    std::copy(begin(source), end(source), begin(dest));
+  }
+
+  struct RbpState {
+    rbp::MaxRectsBinPack max_rects;
+    std::vector<rbp::Rect> rects;
+    std::vector<rbp::RectSize> rect_sizes;
+    std::vector<rbp::RectSize> run_rect_sizes;
+  };
+
+  RbpState init_rbp_state(const std::vector<Size>& sizes) {
+    auto rbp = RbpState();
+    rbp.rects.reserve(sizes.size());
+    rbp.rect_sizes.reserve(sizes.size());
+    for (const auto& size : sizes)
+      rbp.rect_sizes.push_back({ size.width, size.height,
+        static_cast<int>(rbp.rect_sizes.size()) });
+
+    // to preserve order of identical rects (RBP_REVERSE_ORDER is also defined)
+    std::reverse(begin(rbp.rect_sizes), end(rbp.rect_sizes));
+    return rbp;
+  }
+
+  bool run_rbp_method(RbpState& rbp, const Settings& settings, Run& run,
+      const std::optional<Run>& best_run, const std::vector<Size>& sizes) {
+    copy_vector(rbp.rect_sizes, rbp.run_rect_sizes);
+    auto cancelled = false;
+    while (!rbp.run_rect_sizes.empty()) {
+      rbp.rects.clear();
+      rbp.max_rects.Init(run.width, run.height, settings.allow_rotate);
+      rbp.max_rects.Insert(rbp.run_rect_sizes, rbp.rects, to_rbp_method(run.method));
+      auto [width, height] = rbp.max_rects.BottomRight();
+
+      correct_size(settings, width, height);
+      run.total_area += width * height;
+
+      apply_padding(settings, width, height, false);
+      auto& sheet = run.sheets.emplace_back(Sheet{ width, height, { } });
+
+      // cancel when not making any progress
+      if (rbp.rects.empty())
+        return false;
+
+      // cancel when already worse than best run
+      const auto done = rbp.run_rect_sizes.empty();
+      if (best_run && !is_better_than(run, *best_run, !done)) {
+        cancelled = true;
+        break;
+      }
+
+      sheet.rects.reserve(rbp.rects.size());
+      for (auto& rbp_rect : rbp.rects) {
+        const auto& size = sizes[static_cast<size_t>(rbp_rect.id)];
+        sheet.rects.push_back({
+          size.id,
+          rbp_rect.x + settings.border_padding,
+          rbp_rect.y + settings.border_padding,
+          rbp_rect.width,
+          rbp_rect.height,
+          (rbp_rect.width != size.width)
+        });
+      }
+    }
+    return !cancelled;
+  }
+
+  struct StbState {
+    stbrp_context context{ };
+    std::vector<stbrp_node> nodes;
+    std::vector<stbrp_rect> rects;
+    std::vector<stbrp_rect> run_rects;
+  };
+
+  StbState init_stb_state(const Settings& settings, const std::vector<Size>& sizes) {
+    auto stb = StbState{ };
+    stb.rects.reserve(sizes.size());
+    stb.run_rects.reserve(sizes.size());
+    for (const auto& size : sizes)
+      stb.rects.push_back({ static_cast<int>(stb.rects.size()),
+        size.width, size.height, 0, 0, false });
+
+    if (settings.allow_rotate)
+      for (auto& rect : stb.rects)
+        if (rect.w > settings.max_width || rect.h > settings.max_height)
+          std::swap(rect.w, rect.h);
+
+    return stb;
+  }
+
+  bool run_stb_method(StbState& stb, const Settings& settings, Run& run,
+      const std::optional<Run>& best_run, const std::vector<Size>& sizes) {
+    copy_vector(stb.rects, stb.run_rects);
+    stb.nodes.resize(std::max(stb.nodes.size(), static_cast<size_t>(run.width)));
+
+    auto cancelled = false;
+    while (!stb.run_rects.empty()) {
+      stbrp_init_target(&stb.context, run.width, run.height,
+        stb.nodes.data(), static_cast<int>(stb.nodes.size()));
+      stbrp_setup_heuristic(&stb.context, to_stb_method(run.method));
+
+      [[maybe_unused]] const auto all_packed =
+        (stbrp_pack_rects(&stb.context, stb.run_rects.data(),
+          static_cast<int>(stb.run_rects.size())) == 1);
+
+      auto width = 0;
+      auto height = 0;
+      auto rects = std::vector<Rect>();
+      rects.reserve(stb.run_rects.size());
+      stb.run_rects.erase(std::remove_if(begin(stb.run_rects), end(stb.run_rects),
+        [&](const stbrp_rect& stb_rect) {
+          if (!stb_rect.was_packed)
+            return false;
+
+          width = std::max(width, stb_rect.x + stb_rect.w);
+          height = std::max(height, stb_rect.y + stb_rect.h);
+
+          const auto& size = sizes[static_cast<size_t>(stb_rect.id)];
+          rects.push_back({
+            size.id,
+            stb_rect.x + settings.border_padding,
+            stb_rect.y + settings.border_padding,
+            stb_rect.w, stb_rect.h,
+            (stb_rect.w != size.width)
+          });
+          return true;
+        }), end(stb.run_rects));
+
+      correct_size(settings, width, height);
+      run.total_area += width * height;
+
+      apply_padding(settings, width, height, false);
+      const auto& sheet = run.sheets.emplace_back(Sheet{ width, height, std::move(rects) });
+      const auto done = stb.run_rects.empty();
+      if (sheet.rects.empty() ||
+          (best_run && !is_better_than(run, *best_run, !done))) {
+        cancelled = true;
+        break;
+      }
+    }
+    return !cancelled;
+  }
+} // namespace
+
+std::vector<Sheet> pack(Settings settings, std::vector<Size> sizes) {
+  if (!correct_settings(settings, sizes))
+    return { };
+
+  if (sizes.empty())
+    return { };
+
+  auto stb_state = std::optional<StbState>();
+  if (settings.method == Method::Best ||
+      settings.method == Method::Best_Skyline ||
+      is_stb_method(settings.method))
+    stb_state.emplace(init_stb_state(settings, sizes));
+
+  auto rbp_state = std::optional<RbpState>();
+  if (settings.method == Method::Best ||
+      settings.method == Method::Best_MaxRects ||
+      is_rbp_method(settings.method))
+    rbp_state.emplace(init_rbp_state(sizes));
+
+  const auto perfect_area = get_perfect_area(sizes);
+  const auto target_area = perfect_area + perfect_area / 100;
+  const auto [initial_width, initial_height] = get_initial_run_size(settings, perfect_area);
+
+  auto total_best_run = std::optional<Run>{ };
+  const auto methods = get_concrete_methods(settings.method);
+  for (const auto& method : methods) {
+    auto best_run = std::optional<Run>{ };
+    auto state = OptimizationState{
+      perfect_area,
+      initial_width,
+      initial_height,
+      OptimizationStage::first_run,
+      0,
+    };
+    for (;;) {
+      if (best_run.has_value() &&
+          best_run->sheets.size() == 1 &&
+          best_run->total_area <= target_area)
+        break;
+
+      auto run = Run{ method, state.width, state.height, { }, 0 };
+      const auto succeeded = is_rbp_method(run.method) ?
+        run_rbp_method(*rbp_state, settings, run, best_run, sizes) :
+        run_stb_method(*stb_state, settings, run, best_run, sizes);
+
+      if (succeeded && (!best_run || is_better_than(run, *best_run)))
+        best_run = std::move(run);
+
+      if (!best_run.has_value() ||
+          !optimize_run_settings(state, settings, *best_run))
+        break;
+    }
+    if (best_run && (!total_best_run || is_better_than(*best_run, *total_best_run))) {
+      total_best_run = std::move(best_run);
+    }
+  }
+
+  if (!total_best_run)
+    return { };
+
+  if (settings.max_sheets &&
+      settings.max_sheets < static_cast<int>(total_best_run->sheets.size()))
+    total_best_run->sheets.resize(static_cast<size_t>(settings.max_sheets));
+
+  return std::move(total_best_run->sheets);
+}
+
+} // namespace

+ 109 - 0
rectpacker.mod/rect_pack/rect_pack.h

@@ -0,0 +1,109 @@
+// rect_pack.h - public domain - rectangle packing
+// Albert Kalchmair 2021
+//
+// Useful for e.g. packing rectangular textures into one or multiple atlases.
+//
+// LICENSE
+//
+//   See end of file for license information.
+
+#pragma once
+
+#include <vector>
+
+namespace rect_pack {
+
+enum class Method {
+  Best,
+  Best_Skyline,
+  Best_MaxRects,
+  Skyline_BottomLeft,
+  Skyline_BestFit,
+  MaxRects_BestShortSideFit,
+  MaxRects_BestLongSideFit,
+  MaxRects_BestAreaFit,
+  MaxRects_BottomLeftRule,
+  MaxRects_ContactPointRule
+};
+
+struct Size {
+  int id;
+  int width;
+  int height;
+};
+
+struct Rect {
+  int id;
+  int x;
+  int y;
+  int width;
+  int height;
+  bool rotated;
+};
+
+struct Sheet {
+  int width;
+  int height;
+  std::vector<Rect> rects;
+};
+
+struct Settings {
+  Method method;
+  int max_sheets;
+  bool power_of_two;
+  bool square;
+  bool allow_rotate;
+  int align_width;
+  int border_padding;
+  int over_allocate;
+  int min_width;
+  int min_height;
+  int max_width;
+  int max_height;
+};
+
+std::vector<Sheet> pack(Settings settings, std::vector<Size> sizes);
+
+} // namespace
+
+/*
+------------------------------------------------------------------------------
+This software is available under 2 licenses -- choose whichever you prefer.
+------------------------------------------------------------------------------
+ALTERNATIVE A - MIT License
+Copyright (c) 2021 Albert Kalchmair
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+------------------------------------------------------------------------------
+ALTERNATIVE B - Public Domain (www.unlicense.org)
+This is free and unencumbered software released into the public domain.
+Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
+software, either in source code form or as a compiled binary, for any purpose,
+commercial or non-commercial, and by any means.
+In jurisdictions that recognize copyright laws, the author or authors of this
+software dedicate any and all copyright interest in the software to the public
+domain. We make this dedication for the benefit of the public at large and to
+the detriment of our heirs and successors. We intend this dedication to be an
+overt act of relinquishment in perpetuity of all present and future rights to
+this software under copyright law.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+------------------------------------------------------------------------------
+*/

+ 658 - 0
rectpacker.mod/rect_pack/rect_pack_11.cpp

@@ -0,0 +1,658 @@
+// c++11 friendly version of rect_pack.cpp
+// Bruce A Henderson 2024
+
+#define STBRP_LARGE_RECTS
+#include "rect_pack.h"
+#include "MaxRectsBinPack.h"
+#include "stb_rect_pack.h"
+#include <algorithm>
+#include <cmath>
+#include <cassert>
+#include <memory> // for std::unique_ptr
+
+namespace rect_pack
+{
+    namespace
+    {
+        const auto first_Skyline_method = Method::Skyline_BottomLeft;
+        const auto last_Skyline_method = Method::Skyline_BestFit;
+        const auto first_MaxRects_method = Method::MaxRects_BestShortSideFit;
+        const auto last_MaxRects_method = Method::MaxRects_ContactPointRule;
+
+        int floor(int v, int q) { return (v / q) * q; };
+        int ceil(int v, int q) { return ((v + q - 1) / q) * q; };
+        int sqrt(int a) { return static_cast<int>(std::sqrt(a)); }
+        int div_ceil(int a, int b) { return (b > 0 ? (a + b - 1) / b : -1); }
+
+        template <typename T>
+        T clamp(T v, T lo, T hi)
+        {
+            return std::min(hi, std::max(lo, v));
+        }
+
+        int ceil_to_pot(int value)
+        {
+            for (int pot = 1;; pot <<= 1)
+                if (pot >= value)
+                    return pot;
+        }
+
+        int floor_to_pot(int value)
+        {
+            for (int pot = 1;; pot <<= 1)
+                if (pot > value)
+                    return (pot >> 1);
+        }
+
+        bool is_stb_method(Method method)
+        {
+            const auto first = static_cast<int>(first_Skyline_method);
+            const auto last = static_cast<int>(last_Skyline_method);
+            const auto index = static_cast<int>(method);
+            return (index >= first && index <= last);
+        }
+
+        bool is_rbp_method(Method method)
+        {
+            const auto first = static_cast<int>(first_MaxRects_method);
+            const auto last = static_cast<int>(last_MaxRects_method);
+            const auto index = static_cast<int>(method);
+            return (index >= first && index <= last);
+        }
+
+        int to_stb_method(Method method)
+        {
+            assert(is_stb_method(method));
+            return static_cast<int>(method) - static_cast<int>(first_Skyline_method);
+        }
+
+        rbp::MaxRectsBinPack::FreeRectChoiceHeuristic to_rbp_method(Method method)
+        {
+            assert(is_rbp_method(method));
+            return static_cast<rbp::MaxRectsBinPack::FreeRectChoiceHeuristic>(
+                static_cast<int>(method) - static_cast<int>(first_MaxRects_method));
+        }
+
+        std::vector<Method> get_concrete_methods(Method settings_method)
+        {
+            std::vector<Method> methods;
+            const auto add_skyline_methods = [&methods]()
+            {
+                methods.insert(end(methods), {Method::Skyline_BottomLeft,
+                                              Method::Skyline_BestFit});
+            };
+            const auto add_maxrect_methods = [&methods]()
+            {
+                methods.insert(end(methods), {
+                                                 Method::MaxRects_BestShortSideFit,
+                                                 Method::MaxRects_BestLongSideFit,
+                                                 Method::MaxRects_BestAreaFit,
+                                                 Method::MaxRects_BottomLeftRule,
+                                                 // do not automatically try costy contact point rule
+                                                 // Method::MaxRects_ContactPointRule,
+                                             });
+            };
+            switch (settings_method)
+            {
+            case Method::Best:
+                add_skyline_methods();
+                add_maxrect_methods();
+                break;
+
+            case Method::Best_Skyline:
+                add_skyline_methods();
+                break;
+
+            case Method::Best_MaxRects:
+                add_maxrect_methods();
+                break;
+
+            default:
+                methods.push_back(settings_method);
+                break;
+            }
+            return methods;
+        }
+
+        bool can_fit(const Settings &settings, int width, int height)
+        {
+            return ((width <= settings.max_width &&
+                     height <= settings.max_height) ||
+                    (settings.allow_rotate &&
+                     width <= settings.max_height &&
+                     height <= settings.max_width));
+        }
+
+        void apply_padding(const Settings &settings, int &width, int &height, bool indent)
+        {
+            const auto dir = (indent ? 1 : -1);
+            width -= dir * settings.border_padding * 2;
+            height -= dir * settings.border_padding * 2;
+            width += dir * settings.over_allocate;
+            height += dir * settings.over_allocate;
+        }
+
+        bool correct_settings(Settings &settings, std::vector<Size> &sizes)
+        {
+            // clamp max to far less than numeric_limits<int>::max() to prevent overflow
+            const auto size_limit = 1000000000;
+            if (settings.max_width <= 0 || settings.max_width > size_limit)
+                settings.max_width = size_limit;
+            if (settings.max_height <= 0 || settings.max_height > size_limit)
+                settings.max_height = size_limit;
+
+            if (settings.min_width < 0 ||
+                settings.min_height < 0 ||
+                settings.min_width > settings.max_width ||
+                settings.min_height > settings.max_height)
+                return false;
+
+            // immediately apply padding and over allocation, only relevant for power-of-two and alignment constraint
+            apply_padding(settings, settings.min_width, settings.min_height, true);
+            apply_padding(settings, settings.max_width, settings.max_height, true);
+
+            int max_rect_width = 0;
+            int max_rect_height = 0;
+            for (auto it = begin(sizes); it != end(sizes);)
+                if (it->width <= 0 ||
+                    it->height <= 0 ||
+                    !can_fit(settings, it->width, it->height))
+                {
+                    it = sizes.erase(it);
+                }
+                else
+                {
+                    if (settings.allow_rotate &&
+                        it->height > it->width &&
+                        it->height <= settings.max_width &&
+                        it->width <= settings.max_height)
+                    {
+                        max_rect_width = std::max(max_rect_width, it->height);
+                        max_rect_height = std::max(max_rect_height, it->width);
+                    }
+                    else
+                    {
+                        max_rect_width = std::max(max_rect_width, it->width);
+                        max_rect_height = std::max(max_rect_height, it->height);
+                    }
+                    ++it;
+                }
+
+            settings.min_width = std::max(settings.min_width, max_rect_width);
+            settings.min_height = std::max(settings.min_height, max_rect_height);
+
+            // clamp min to max and still pack the sprites which fit
+            settings.min_width = std::min(settings.min_width, settings.max_width);
+            settings.min_height = std::min(settings.min_height, settings.max_height);
+            return true;
+        }
+
+        struct Run
+        {
+            Method method;
+            int width;
+            int height;
+            std::vector<Sheet> sheets;
+            int total_area;
+        };
+
+        void correct_size(const Settings &settings, int &width, int &height)
+        {
+            width = std::max(width, settings.min_width);
+            height = std::max(height, settings.min_height);
+            apply_padding(settings, width, height, false);
+
+            if (settings.power_of_two)
+            {
+                width = ceil_to_pot(width);
+                height = ceil_to_pot(height);
+            }
+
+            if (settings.align_width)
+                width = ceil(width, settings.align_width);
+
+            if (settings.square)
+                width = height = std::max(width, height);
+
+            apply_padding(settings, width, height, true);
+            width = std::min(width, settings.max_width);
+            height = std::min(height, settings.max_height);
+            apply_padding(settings, width, height, false);
+
+            if (settings.power_of_two)
+            {
+                width = floor_to_pot(width);
+                height = floor_to_pot(height);
+            }
+
+            if (settings.align_width)
+                width = floor(width, settings.align_width);
+
+            if (settings.square)
+                width = height = std::min(width, height);
+
+            apply_padding(settings, width, height, true);
+        }
+
+        bool is_better_than(const Run &a, const Run &b, bool a_incomplete = false)
+        {
+            if (a_incomplete)
+            {
+                if (b.sheets.size() <= a.sheets.size())
+                    return false;
+            }
+            else
+            {
+                if (a.sheets.size() < b.sheets.size())
+                    return true;
+                if (b.sheets.size() < a.sheets.size())
+                    return false;
+            }
+            return (a.total_area < b.total_area);
+        }
+
+        int get_perfect_area(const std::vector<Size> &sizes)
+        {
+            int area = 0;
+            for (const auto &size : sizes)
+                area += size.width * size.height;
+            return area;
+        }
+
+        std::pair<int, int> get_run_size(const Settings &settings, int area)
+        {
+            int width = sqrt(area);
+            int height = div_ceil(area, width);
+            if (width < settings.min_width || width > settings.max_width)
+            {
+                width = clamp(width, settings.min_width, settings.max_width);
+                height = div_ceil(area, width);
+            }
+            else if (height < settings.min_height || height > settings.max_height)
+            {
+                height = clamp(height, settings.min_height, settings.max_height);
+                width = div_ceil(area, height);
+            }
+            correct_size(settings, width, height);
+            return std::make_pair(width, height);
+        }
+
+        std::pair<int, int> get_initial_run_size(const Settings &settings, int perfect_area)
+        {
+            return get_run_size(settings, perfect_area * 5 / 4);
+        }
+
+        enum class OptimizationStage
+        {
+            first_run,
+            minimize_sheet_count,
+            shrink_square,
+            shrink_width_fast,
+            shrink_height_fast,
+            shrink_width_slow,
+            shrink_height_slow,
+            end
+        };
+
+        struct OptimizationState
+        {
+            const int perfect_area;
+            int width;
+            int height;
+            OptimizationStage stage;
+            int iteration;
+        };
+
+        bool advance(OptimizationStage &stage)
+        {
+            if (stage == OptimizationStage::end)
+                return false;
+            stage = static_cast<OptimizationStage>(static_cast<int>(stage) + 1);
+            return true;
+        }
+
+        // returns true when stage should be kept, false to advance
+        bool optimize_stage(OptimizationState &state,
+                            const Settings &pack_settings, const Run &best_run)
+        {
+            switch (state.stage)
+            {
+            case OptimizationStage::first_run:
+            case OptimizationStage::end:
+                return false;
+
+            case OptimizationStage::minimize_sheet_count:
+            {
+                if (best_run.sheets.size() <= 1 ||
+                    state.iteration > 5)
+                    return false;
+
+                const auto &last_sheet = best_run.sheets.back();
+                int area = last_sheet.width * last_sheet.height;
+                for (int i = 0; area > 0; ++i)
+                {
+                    if (state.width == pack_settings.max_width &&
+                        state.height == pack_settings.max_height)
+                        break;
+                    if (state.height == pack_settings.max_height ||
+                        (state.width < pack_settings.max_width && i % 2))
+                    {
+                        ++state.width;
+                        area -= state.height;
+                    }
+                    else
+                    {
+                        ++state.height;
+                        area -= state.width;
+                    }
+                }
+                return true;
+            }
+
+            case OptimizationStage::shrink_square:
+            {
+                if (state.width != best_run.width ||
+                    state.height != best_run.height ||
+                    state.iteration > 5)
+                    return false;
+
+                std::pair<int, int> size = get_run_size(pack_settings, state.perfect_area);
+                state.width = (state.width + size.first) / 2;
+                state.height = (state.height + size.second) / 2;
+                return true;
+            }
+
+            case OptimizationStage::shrink_width_fast:
+            case OptimizationStage::shrink_height_fast:
+            case OptimizationStage::shrink_width_slow:
+            case OptimizationStage::shrink_height_slow:
+            {
+                if (state.iteration > 5)
+                    return false;
+
+                std::pair<int, int> size = get_run_size(pack_settings, state.perfect_area);
+                switch (state.stage)
+                {
+                default:
+                case OptimizationStage::shrink_width_fast:
+                    if (state.width > size.first + 4)
+                        state.width = (state.width + size.first) / 2;
+                    break;
+                case OptimizationStage::shrink_height_fast:
+                    if (state.height > size.second + 4)
+                        state.height = (state.height + size.second) / 2;
+                    break;
+                case OptimizationStage::shrink_width_slow:
+                    if (state.width > size.first)
+                        --state.width;
+                    break;
+                case OptimizationStage::shrink_height_slow:
+                    if (state.height > size.second)
+                        --state.height;
+                    break;
+                }
+                return true;
+            }
+            }
+            return false;
+        }
+
+        bool optimize_run_settings(OptimizationState &state,
+                                   const Settings &pack_settings, const Run &best_run)
+        {
+            const auto previous_state = state;
+            for (;;)
+            {
+                if (!optimize_stage(state, pack_settings, best_run))
+                    if (advance(state.stage))
+                    {
+                        state.width = best_run.width;
+                        state.height = best_run.height;
+                        state.iteration = 0;
+                        continue;
+                    }
+
+                if (state.stage == OptimizationStage::end)
+                    return false;
+
+                ++state.iteration;
+
+                int width = state.width;
+                int height = state.height;
+                correct_size(pack_settings, width, height);
+
+                if (width != previous_state.width || height != previous_state.height)
+                {
+                    state.width = width;
+                    state.height = height;
+                    return true;
+                }
+            }
+        }
+
+        template <typename T>
+        void copy_vector(const T &source, T &dest)
+        {
+            dest.resize(source.size());
+            std::copy(begin(source), end(source), begin(dest));
+        }
+
+        struct RbpState
+        {
+            rbp::MaxRectsBinPack max_rects;
+            std::vector<rbp::Rect> rects;
+            std::vector<rbp::RectSize> rect_sizes;
+            std::vector<rbp::RectSize> run_rect_sizes;
+        };
+
+        RbpState init_rbp_state(const std::vector<Size> &sizes)
+        {
+            RbpState rbp;
+            rbp.rects.reserve(sizes.size());
+            rbp.rect_sizes.reserve(sizes.size());
+            for (const auto &size : sizes)
+                rbp.rect_sizes.push_back({size.width, size.height,
+                                          static_cast<int>(rbp.rect_sizes.size())});
+
+            // to preserve order of identical rects (RBP_REVERSE_ORDER is also defined)
+            std::reverse(begin(rbp.rect_sizes), end(rbp.rect_sizes));
+            return rbp;
+        }
+
+        bool run_rbp_method(RbpState &rbp, const Settings &settings, Run &run,
+                            const std::unique_ptr<Run> &best_run, const std::vector<Size> &sizes)
+        {
+            copy_vector(rbp.rect_sizes, rbp.run_rect_sizes);
+            bool cancelled = false;
+            while (!rbp.run_rect_sizes.empty())
+            {
+                rbp.rects.clear();
+                rbp.max_rects.Init(run.width, run.height, settings.allow_rotate);
+                rbp.max_rects.Insert(rbp.run_rect_sizes, rbp.rects, to_rbp_method(run.method));
+                int width, height;
+                std::tie(width, height) = rbp.max_rects.BottomRight();
+
+                correct_size(settings, width, height);
+                run.total_area += width * height;
+
+                apply_padding(settings, width, height, false);
+                run.sheets.emplace_back(Sheet{width, height, {}});
+                auto &sheet = run.sheets.back(); // This is the updated line
+
+                // cancel when not making any progress
+                if (rbp.rects.empty())
+                    return false;
+
+                // cancel when already worse than best run
+                const bool done = rbp.run_rect_sizes.empty();
+                if (best_run && !is_better_than(run, *best_run, !done))
+                {
+                    cancelled = true;
+                    break;
+                }
+
+                sheet.rects.reserve(rbp.rects.size());
+                for (auto &rbp_rect : rbp.rects)
+                {
+                    const auto &size = sizes[static_cast<size_t>(rbp_rect.id)];
+                    sheet.rects.push_back({size.id,
+                                           rbp_rect.x + settings.border_padding,
+                                           rbp_rect.y + settings.border_padding,
+                                           rbp_rect.width,
+                                           rbp_rect.height,
+                                           (rbp_rect.width != size.width)});
+                }
+            }
+            return !cancelled;
+        }
+
+        struct StbState
+        {
+            stbrp_context context;
+            std::vector<stbrp_node> nodes;
+            std::vector<stbrp_rect> rects;
+            std::vector<stbrp_rect> run_rects;
+        };
+
+        StbState init_stb_state(const Settings &settings, const std::vector<Size> &sizes)
+        {
+            StbState stb;
+            stb.rects.reserve(sizes.size());
+            stb.run_rects.reserve(sizes.size());
+            for (const auto &size : sizes)
+                stb.rects.push_back({static_cast<int>(stb.rects.size()),
+                                     size.width, size.height, 0, 0, false});
+
+            if (settings.allow_rotate)
+                for (auto &rect : stb.rects)
+                    if (rect.w > settings.max_width || rect.h > settings.max_height)
+                        std::swap(rect.w, rect.h);
+
+            return stb;
+        }
+
+        bool run_stb_method(StbState &stb, const Settings &settings, Run &run,
+                            const std::unique_ptr<Run> &best_run, const std::vector<Size> &sizes)
+        {
+            copy_vector(stb.rects, stb.run_rects);
+            stb.nodes.resize(std::max(stb.nodes.size(), static_cast<size_t>(run.width)));
+
+            bool cancelled = false;
+            while (!stb.run_rects.empty())
+            {
+                stbrp_init_target(&stb.context, run.width, run.height,
+                                  stb.nodes.data(), static_cast<int>(stb.nodes.size()));
+                stbrp_setup_heuristic(&stb.context, to_stb_method(run.method));
+
+                const bool all_packed =
+                    (stbrp_pack_rects(&stb.context, stb.run_rects.data(),
+                                      static_cast<int>(stb.run_rects.size())) == 1);
+
+                int width = 0;
+                int height = 0;
+                std::vector<Rect> rects;
+                rects.reserve(stb.run_rects.size());
+                stb.run_rects.erase(std::remove_if(begin(stb.run_rects), end(stb.run_rects),
+                                                   [&](const stbrp_rect &stb_rect)
+                                                   {
+                                                       if (!stb_rect.was_packed)
+                                                           return false;
+
+                                                       width = std::max(width, stb_rect.x + stb_rect.w);
+                                                       height = std::max(height, stb_rect.y + stb_rect.h);
+
+                                                       const auto &size = sizes[static_cast<size_t>(stb_rect.id)];
+                                                       rects.push_back({size.id,
+                                                                        stb_rect.x + settings.border_padding,
+                                                                        stb_rect.y + settings.border_padding,
+                                                                        stb_rect.w, stb_rect.h,
+                                                                        (stb_rect.w != size.width)});
+                                                       return true;
+                                                   }),
+                                    end(stb.run_rects));
+
+                correct_size(settings, width, height);
+                run.total_area += width * height;
+
+                apply_padding(settings, width, height, false);
+                run.sheets.emplace_back(Sheet{width, height, std::move(rects)});
+                const bool done = stb.run_rects.empty();
+                if (run.sheets.back().rects.empty() ||
+                    (best_run && !is_better_than(run, *best_run, !done)))
+                {
+                    cancelled = true;
+                    break;
+                }
+            }
+            return !cancelled;
+        }
+    } // namespace
+
+    std::vector<Sheet> pack(Settings settings, std::vector<Size> sizes)
+    {
+        if (!correct_settings(settings, sizes))
+            return {};
+
+        if (sizes.empty())
+            return {};
+
+        std::unique_ptr<StbState> stb_state;
+        if (settings.method == Method::Best ||
+            settings.method == Method::Best_Skyline ||
+            is_stb_method(settings.method))
+            stb_state.reset(new StbState(init_stb_state(settings, sizes)));
+
+        std::unique_ptr<RbpState> rbp_state;
+        if (settings.method == Method::Best ||
+            settings.method == Method::Best_MaxRects ||
+            is_rbp_method(settings.method))
+            rbp_state.reset(new RbpState(init_rbp_state(sizes)));
+
+        const int perfect_area = get_perfect_area(sizes);
+        const int target_area = perfect_area + perfect_area / 100;
+        int initial_width, initial_height;
+        std::tie(initial_width, initial_height) = get_initial_run_size(settings, perfect_area);
+
+        std::unique_ptr<Run> total_best_run;
+        const std::vector<Method> methods = get_concrete_methods(settings.method);
+        for (const auto &method : methods)
+        {
+            std::unique_ptr<Run> best_run;
+            OptimizationState state{
+                perfect_area,
+                initial_width,
+                initial_height,
+                OptimizationStage::first_run,
+                0,
+            };
+            for (;;)
+            {
+                if (best_run && best_run->sheets.size() == 1 && best_run->total_area <= target_area)
+                    break;
+
+                Run run{method, state.width, state.height, {}, 0};
+                const bool succeeded = is_rbp_method(run.method) ? run_rbp_method(*rbp_state, settings, run, best_run, sizes) : run_stb_method(*stb_state, settings, run, best_run, sizes);
+
+                if (succeeded && (!best_run || is_better_than(run, *best_run)))
+                    best_run.reset(new Run(std::move(run)));
+
+                if (!best_run || !optimize_run_settings(state, settings, *best_run))
+                    break;
+            }
+            if (best_run && (!total_best_run || is_better_than(*best_run, *total_best_run)))
+            {
+                total_best_run.reset(new Run(std::move(*best_run)));
+            }
+        }
+
+        if (!total_best_run)
+            return {};
+
+        if (settings.max_sheets &&
+            settings.max_sheets < static_cast<int>(total_best_run->sheets.size()))
+            total_best_run->sheets.resize(static_cast<size_t>(settings.max_sheets));
+
+        return std::move(total_best_run->sheets);
+    }
+
+} // namespace rect_pack

+ 16 - 0
rectpacker.mod/rect_pack/stb_rect_pack.cpp

@@ -0,0 +1,16 @@
+
+#include <array>
+#include <algorithm>
+
+template<size_t size, typename T, typename C>
+void my_stbrp_sort(T* ptr, std::size_t count, const C& comp) {
+  static_assert(sizeof(T) == size);
+  const auto begin = static_cast<T*>(ptr);
+  const auto end = begin + count;
+  std::sort(begin, end, [&](const T& a, const T& b) { return (comp(&a, &b) < 0); });
+}
+#define STBRP_SORT(PTR, COUNT, SIZE, COMP) my_stbrp_sort<(SIZE)>((PTR), (COUNT), (COMP))
+
+#define STB_RECT_PACK_IMPLEMENTATION
+#define STBRP_LARGE_RECTS
+#include "stb_rect_pack.h"

+ 623 - 0
rectpacker.mod/rect_pack/stb_rect_pack.h

@@ -0,0 +1,623 @@
+// stb_rect_pack.h - v1.01 - public domain - rectangle packing
+// Sean Barrett 2014
+//
+// Useful for e.g. packing rectangular textures into an atlas.
+// Does not do rotation.
+//
+// Before #including,
+//
+//    #define STB_RECT_PACK_IMPLEMENTATION
+//
+// in the file that you want to have the implementation.
+//
+// Not necessarily the awesomest packing method, but better than
+// the totally naive one in stb_truetype (which is primarily what
+// this is meant to replace).
+//
+// Has only had a few tests run, may have issues.
+//
+// More docs to come.
+//
+// No memory allocations; uses qsort() and assert() from stdlib.
+// Can override those by defining STBRP_SORT and STBRP_ASSERT.
+//
+// This library currently uses the Skyline Bottom-Left algorithm.
+//
+// Please note: better rectangle packers are welcome! Please
+// implement them to the same API, but with a different init
+// function.
+//
+// Credits
+//
+//  Library
+//    Sean Barrett
+//  Minor features
+//    Martins Mozeiko
+//    github:IntellectualKitty
+//
+//  Bugfixes / warning fixes
+//    Jeremy Jaussaud
+//    Fabian Giesen
+//
+// Version history:
+//
+//     1.01  (2021-07-11)  always use large rect mode, expose STBRP__MAXVAL in public section
+//     1.00  (2019-02-25)  avoid small space waste; gracefully fail too-wide rectangles
+//     0.99  (2019-02-07)  warning fixes
+//     0.11  (2017-03-03)  return packing success/fail result
+//     0.10  (2016-10-25)  remove cast-away-const to avoid warnings
+//     0.09  (2016-08-27)  fix compiler warnings
+//     0.08  (2015-09-13)  really fix bug with empty rects (w=0 or h=0)
+//     0.07  (2015-09-13)  fix bug with empty rects (w=0 or h=0)
+//     0.06  (2015-04-15)  added STBRP_SORT to allow replacing qsort
+//     0.05:  added STBRP_ASSERT to allow replacing assert
+//     0.04:  fixed minor bug in STBRP_LARGE_RECTS support
+//     0.01:  initial release
+//
+// LICENSE
+//
+//   See end of file for license information.
+
+//////////////////////////////////////////////////////////////////////////////
+//
+//       INCLUDE SECTION
+//
+
+#ifndef STB_INCLUDE_STB_RECT_PACK_H
+#define STB_INCLUDE_STB_RECT_PACK_H
+
+#define STB_RECT_PACK_VERSION  1
+
+#ifdef STBRP_STATIC
+#define STBRP_DEF static
+#else
+#define STBRP_DEF extern
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct stbrp_context stbrp_context;
+typedef struct stbrp_node    stbrp_node;
+typedef struct stbrp_rect    stbrp_rect;
+
+typedef int            stbrp_coord;
+
+#define STBRP__MAXVAL  0x7fffffff
+// Mostly for internal use, but this is the maximum supported coordinate value.
+
+STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects);
+// Assign packed locations to rectangles. The rectangles are of type
+// 'stbrp_rect' defined below, stored in the array 'rects', and there
+// are 'num_rects' many of them.
+//
+// Rectangles which are successfully packed have the 'was_packed' flag
+// set to a non-zero value and 'x' and 'y' store the minimum location
+// on each axis (i.e. bottom-left in cartesian coordinates, top-left
+// if you imagine y increasing downwards). Rectangles which do not fit
+// have the 'was_packed' flag set to 0.
+//
+// You should not try to access the 'rects' array from another thread
+// while this function is running, as the function temporarily reorders
+// the array while it executes.
+//
+// To pack into another rectangle, you need to call stbrp_init_target
+// again. To continue packing into the same rectangle, you can call
+// this function again. Calling this multiple times with multiple rect
+// arrays will probably produce worse packing results than calling it
+// a single time with the full rectangle array, but the option is
+// available.
+//
+// The function returns 1 if all of the rectangles were successfully
+// packed and 0 otherwise.
+
+struct stbrp_rect
+{
+   // reserved for your use:
+   int            id;
+
+   // input:
+   stbrp_coord    w, h;
+
+   // output:
+   stbrp_coord    x, y;
+   int            was_packed;  // non-zero if valid packing
+
+}; // 16 bytes, nominally
+
+
+STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes);
+// Initialize a rectangle packer to:
+//    pack a rectangle that is 'width' by 'height' in dimensions
+//    using temporary storage provided by the array 'nodes', which is 'num_nodes' long
+//
+// You must call this function every time you start packing into a new target.
+//
+// There is no "shutdown" function. The 'nodes' memory must stay valid for
+// the following stbrp_pack_rects() call (or calls), but can be freed after
+// the call (or calls) finish.
+//
+// Note: to guarantee best results, either:
+//       1. make sure 'num_nodes' >= 'width'
+//   or  2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1'
+//
+// If you don't do either of the above things, widths will be quantized to multiples
+// of small integers to guarantee the algorithm doesn't run out of temporary storage.
+//
+// If you do #2, then the non-quantized algorithm will be used, but the algorithm
+// may run out of temporary storage and be unable to pack some rectangles.
+
+STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem);
+// Optionally call this function after init but before doing any packing to
+// change the handling of the out-of-temp-memory scenario, described above.
+// If you call init again, this will be reset to the default (false).
+
+
+STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic);
+// Optionally select which packing heuristic the library should use. Different
+// heuristics will produce better/worse results for different data sets.
+// If you call init again, this will be reset to the default.
+
+enum
+{
+   STBRP_HEURISTIC_Skyline_default=0,
+   STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default,
+   STBRP_HEURISTIC_Skyline_BF_sortHeight
+};
+
+
+//////////////////////////////////////////////////////////////////////////////
+//
+// the details of the following structures don't matter to you, but they must
+// be visible so you can handle the memory allocations for them
+
+struct stbrp_node
+{
+   stbrp_coord  x,y;
+   stbrp_node  *next;
+};
+
+struct stbrp_context
+{
+   int width;
+   int height;
+   int align;
+   int init_mode;
+   int heuristic;
+   int num_nodes;
+   stbrp_node *active_head;
+   stbrp_node *free_head;
+   stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2'
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
+//////////////////////////////////////////////////////////////////////////////
+//
+//     IMPLEMENTATION SECTION
+//
+
+#ifdef STB_RECT_PACK_IMPLEMENTATION
+#ifndef STBRP_SORT
+#include <stdlib.h>
+#define STBRP_SORT qsort
+#endif
+
+#ifndef STBRP_ASSERT
+#include <assert.h>
+#define STBRP_ASSERT assert
+#endif
+
+#ifdef _MSC_VER
+#define STBRP__NOTUSED(v)  (void)(v)
+#define STBRP__CDECL       __cdecl
+#else
+#define STBRP__NOTUSED(v)  (void)sizeof(v)
+#define STBRP__CDECL
+#endif
+
+enum
+{
+   STBRP__INIT_skyline = 1
+};
+
+STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic)
+{
+   switch (context->init_mode) {
+      case STBRP__INIT_skyline:
+         STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight);
+         context->heuristic = heuristic;
+         break;
+      default:
+         STBRP_ASSERT(0);
+   }
+}
+
+STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem)
+{
+   if (allow_out_of_mem)
+      // if it's ok to run out of memory, then don't bother aligning them;
+      // this gives better packing, but may fail due to OOM (even though
+      // the rectangles easily fit). @TODO a smarter approach would be to only
+      // quantize once we've hit OOM, then we could get rid of this parameter.
+      context->align = 1;
+   else {
+      // if it's not ok to run out of memory, then quantize the widths
+      // so that num_nodes is always enough nodes.
+      //
+      // I.e. num_nodes * align >= width
+      //                  align >= width / num_nodes
+      //                  align = ceil(width/num_nodes)
+
+      context->align = (context->width + context->num_nodes-1) / context->num_nodes;
+   }
+}
+
+STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes)
+{
+   int i;
+
+   for (i=0; i < num_nodes-1; ++i)
+      nodes[i].next = &nodes[i+1];
+   nodes[i].next = NULL;
+   context->init_mode = STBRP__INIT_skyline;
+   context->heuristic = STBRP_HEURISTIC_Skyline_default;
+   context->free_head = &nodes[0];
+   context->active_head = &context->extra[0];
+   context->width = width;
+   context->height = height;
+   context->num_nodes = num_nodes;
+   stbrp_setup_allow_out_of_mem(context, 0);
+
+   // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly)
+   context->extra[0].x = 0;
+   context->extra[0].y = 0;
+   context->extra[0].next = &context->extra[1];
+   context->extra[1].x = (stbrp_coord) width;
+   context->extra[1].y = (1<<30);
+   context->extra[1].next = NULL;
+}
+
+// find minimum y position if it starts at x1
+static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste)
+{
+   stbrp_node *node = first;
+   int x1 = x0 + width;
+   int min_y, visited_width, waste_area;
+
+   STBRP__NOTUSED(c);
+
+   STBRP_ASSERT(first->x <= x0);
+
+   #if 0
+   // skip in case we're past the node
+   while (node->next->x <= x0)
+      ++node;
+   #else
+   STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency
+   #endif
+
+   STBRP_ASSERT(node->x <= x0);
+
+   min_y = 0;
+   waste_area = 0;
+   visited_width = 0;
+   while (node->x < x1) {
+      if (node->y > min_y) {
+         // raise min_y higher.
+         // we've accounted for all waste up to min_y,
+         // but we'll now add more waste for everything we've visted
+         waste_area += visited_width * (node->y - min_y);
+         min_y = node->y;
+         // the first time through, visited_width might be reduced
+         if (node->x < x0)
+            visited_width += node->next->x - x0;
+         else
+            visited_width += node->next->x - node->x;
+      } else {
+         // add waste area
+         int under_width = node->next->x - node->x;
+         if (under_width + visited_width > width)
+            under_width = width - visited_width;
+         waste_area += under_width * (min_y - node->y);
+         visited_width += under_width;
+      }
+      node = node->next;
+   }
+
+   *pwaste = waste_area;
+   return min_y;
+}
+
+typedef struct
+{
+   int x,y;
+   stbrp_node **prev_link;
+} stbrp__findresult;
+
+static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height)
+{
+   int best_waste = (1<<30), best_x, best_y = (1 << 30);
+   stbrp__findresult fr;
+   stbrp_node **prev, *node, *tail, **best = NULL;
+
+   // align to multiple of c->align
+   width = (width + c->align - 1);
+   width -= width % c->align;
+   STBRP_ASSERT(width % c->align == 0);
+
+   // if it can't possibly fit, bail immediately
+   if (width > c->width || height > c->height) {
+      fr.prev_link = NULL;
+      fr.x = fr.y = 0;
+      return fr;
+   }
+
+   node = c->active_head;
+   prev = &c->active_head;
+   while (node->x + width <= c->width) {
+      int y,waste;
+      y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste);
+      if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL
+         // bottom left
+         if (y < best_y) {
+            best_y = y;
+            best = prev;
+         }
+      } else {
+         // best-fit
+         if (y + height <= c->height) {
+            // can only use it if it first vertically
+            if (y < best_y || (y == best_y && waste < best_waste)) {
+               best_y = y;
+               best_waste = waste;
+               best = prev;
+            }
+         }
+      }
+      prev = &node->next;
+      node = node->next;
+   }
+
+   best_x = (best == NULL) ? 0 : (*best)->x;
+
+   // if doing best-fit (BF), we also have to try aligning right edge to each node position
+   //
+   // e.g, if fitting
+   //
+   //     ____________________
+   //    |____________________|
+   //
+   //            into
+   //
+   //   |                         |
+   //   |             ____________|
+   //   |____________|
+   //
+   // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned
+   //
+   // This makes BF take about 2x the time
+
+   if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) {
+      tail = c->active_head;
+      node = c->active_head;
+      prev = &c->active_head;
+      // find first node that's admissible
+      while (tail->x < width)
+         tail = tail->next;
+      while (tail) {
+         int xpos = tail->x - width;
+         int y,waste;
+         STBRP_ASSERT(xpos >= 0);
+         // find the left position that matches this
+         while (node->next->x <= xpos) {
+            prev = &node->next;
+            node = node->next;
+         }
+         STBRP_ASSERT(node->next->x > xpos && node->x <= xpos);
+         y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste);
+         if (y + height <= c->height) {
+            if (y <= best_y) {
+               if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) {
+                  best_x = xpos;
+                  STBRP_ASSERT(y <= best_y);
+                  best_y = y;
+                  best_waste = waste;
+                  best = prev;
+               }
+            }
+         }
+         tail = tail->next;
+      }
+   }
+
+   fr.prev_link = best;
+   fr.x = best_x;
+   fr.y = best_y;
+   return fr;
+}
+
+static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height)
+{
+   // find best position according to heuristic
+   stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height);
+   stbrp_node *node, *cur;
+
+   // bail if:
+   //    1. it failed
+   //    2. the best node doesn't fit (we don't always check this)
+   //    3. we're out of memory
+   if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) {
+      res.prev_link = NULL;
+      return res;
+   }
+
+   // on success, create new node
+   node = context->free_head;
+   node->x = (stbrp_coord) res.x;
+   node->y = (stbrp_coord) (res.y + height);
+
+   context->free_head = node->next;
+
+   // insert the new node into the right starting point, and
+   // let 'cur' point to the remaining nodes needing to be
+   // stiched back in
+
+   cur = *res.prev_link;
+   if (cur->x < res.x) {
+      // preserve the existing one, so start testing with the next one
+      stbrp_node *next = cur->next;
+      cur->next = node;
+      cur = next;
+   } else {
+      *res.prev_link = node;
+   }
+
+   // from here, traverse cur and free the nodes, until we get to one
+   // that shouldn't be freed
+   while (cur->next && cur->next->x <= res.x + width) {
+      stbrp_node *next = cur->next;
+      // move the current node to the free list
+      cur->next = context->free_head;
+      context->free_head = cur;
+      cur = next;
+   }
+
+   // stitch the list back in
+   node->next = cur;
+
+   if (cur->x < res.x + width)
+      cur->x = (stbrp_coord) (res.x + width);
+
+#ifdef _DEBUG
+   cur = context->active_head;
+   while (cur->x < context->width) {
+      STBRP_ASSERT(cur->x < cur->next->x);
+      cur = cur->next;
+   }
+   STBRP_ASSERT(cur->next == NULL);
+
+   {
+      int count=0;
+      cur = context->active_head;
+      while (cur) {
+         cur = cur->next;
+         ++count;
+      }
+      cur = context->free_head;
+      while (cur) {
+         cur = cur->next;
+         ++count;
+      }
+      STBRP_ASSERT(count == context->num_nodes+2);
+   }
+#endif
+
+   return res;
+}
+
+static int STBRP__CDECL rect_height_compare(const void *a, const void *b)
+{
+   const stbrp_rect *p = (const stbrp_rect *) a;
+   const stbrp_rect *q = (const stbrp_rect *) b;
+   if (p->h > q->h)
+      return -1;
+   if (p->h < q->h)
+      return  1;
+   return (p->w > q->w) ? -1 : (p->w < q->w);
+}
+
+static int STBRP__CDECL rect_original_order(const void *a, const void *b)
+{
+   const stbrp_rect *p = (const stbrp_rect *) a;
+   const stbrp_rect *q = (const stbrp_rect *) b;
+   return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed);
+}
+
+STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects)
+{
+   int i, all_rects_packed = 1;
+
+   // we use the 'was_packed' field internally to allow sorting/unsorting
+   for (i=0; i < num_rects; ++i) {
+      rects[i].was_packed = i;
+   }
+
+   // sort according to heuristic
+   STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare);
+
+   for (i=0; i < num_rects; ++i) {
+      if (rects[i].w == 0 || rects[i].h == 0) {
+         rects[i].x = rects[i].y = 0;  // empty rect needs no space
+      } else {
+         stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h);
+         if (fr.prev_link) {
+            rects[i].x = (stbrp_coord) fr.x;
+            rects[i].y = (stbrp_coord) fr.y;
+         } else {
+            rects[i].x = rects[i].y = STBRP__MAXVAL;
+         }
+      }
+   }
+
+   // unsort
+   STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order);
+
+   // set was_packed flags and all_rects_packed status
+   for (i=0; i < num_rects; ++i) {
+      rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL);
+      if (!rects[i].was_packed)
+         all_rects_packed = 0;
+   }
+
+   // return the all_rects_packed status
+   return all_rects_packed;
+}
+#endif
+
+/*
+------------------------------------------------------------------------------
+This software is available under 2 licenses -- choose whichever you prefer.
+------------------------------------------------------------------------------
+ALTERNATIVE A - MIT License
+Copyright (c) 2017 Sean Barrett
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+------------------------------------------------------------------------------
+ALTERNATIVE B - Public Domain (www.unlicense.org)
+This is free and unencumbered software released into the public domain.
+Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
+software, either in source code form or as a compiled binary, for any purpose,
+commercial or non-commercial, and by any means.
+In jurisdictions that recognize copyright laws, the author or authors of this
+software dedicate any and all copyright interest in the software to the public
+domain. We make this dedication for the benefit of the public at large and to
+the detriment of our heirs and successors. We intend this dedication to be an
+overt act of relinquishment in perpetuity of all present and future rights to
+this software under copyright law.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+------------------------------------------------------------------------------
+*/

+ 287 - 0
rectpacker.mod/rectpacker.bmx

@@ -0,0 +1,287 @@
+' Copyright (c) 2024 Bruce A Henderson
+' 
+' This software is provided 'as-is', without any express or implied
+' warranty. In no event will the authors be held liable for any damages
+' arising from the use of this software.
+' 
+' Permission is granted to anyone to use this software for any purpose,
+' including commercial applications, and to alter it and redistribute it
+' freely, subject to the following restrictions:
+' 
+' 1. The origin of this software must not be misrepresented; you must not
+'    claim that you wrote the original software. If you use this software
+'    in a product, an acknowledgment in the product documentation would be
+'    appreciated but is not required.
+' 2. Altered source versions must be plainly marked as such, and must not be
+'    misrepresented as being the original software.
+' 3. This notice may not be removed or altered from any source distribution.
+' 
+SuperStrict
+
+Rem
+bbdoc: A module for packing rectangles into sheets.
+about: Useful for creating texture atlases, sprite sheets, and other similar things.
+End Rem
+Module BRL.RectPacker
+
+ModuleInfo "Version: 1.00"
+ModuleInfo "License: zlib/libpng"
+ModuleInfo "Copyright: 2024 Bruce A Henderson"
+ModuleInfo "rect_pack: Albert Kalchmair 2021, Sean Barrett 2014, Jukka Jylänki"
+
+ModuleInfo "History: 1.00 Initial Release"
+
+ModuleInfo "CPP_OPTS: -std=c++11"
+
+Import BRL.Collections
+
+Import "source.bmx"
+
+Rem
+bbdoc: Packs rectangles into sheets.
+about: The packer provides a number of settings that can be used to control how the rectangles are packed.
+The rectangles are added to the packer using the #Add method, and then the #Pack method is called to pack them into sheets.
+The packer will return an array of #TPackedSheet objects, each of which contains the rectangles that have been packed into it.
+An @id can be assigned to each rectangle, which can be used to identify the rectangle in the packed sheets.
+End Rem
+Type TRectPacker
+
+	Rem
+	bbdoc: The packing method to use.
+	End Rem
+	Field packingMethod:EPackingMethod = EPackingMethod.Best
+
+	Rem
+	bbdoc: The maximum number of sheets to produce.
+	about: If the packer is unable to fit all the rectangles into the specified number of sheets, those that don't fit will be discarded.
+	End Rem
+	Field maxSheets:Int = 1
+
+	Rem
+	bbdoc: Whether to pack into power-of-two sized sheets.
+	about: If this is set to #True, the width and height of the sheets will be rounded up to the nearest power of two.
+	This is useful for creating sheets that are intended to be used for creating textures.
+	End Rem
+	Field powerOfTwo:Int = True
+
+	Rem
+	bbdoc: Whether to pack into square sheets.
+	about: If this is set to #True, the width and height of the sheets will be the same.
+	End Rem
+	Field square:Int = False
+
+	Rem
+	bbdoc: Whether to allow rectangles to be rotated.
+	about: If this is set to #True, the packer may attempt to rotate rectangles to help fit them into the sheets.
+	End Rem
+	Field allowRotate:Int = False
+
+	Rem
+	bbdoc: Whether to align the width of the rectangles.
+	about: If this is set to #True, the packer will attempt to align the width of the rectangles to the width of the sheet.
+	This can help to reduce the amount of wasted space in the sheet.
+	End Rem
+	Field alignWidth:Int = False
+
+	Rem
+	bbdoc: The amount of padding to add.
+	End Rem
+	Field borderPadding:Int
+
+	Rem
+	bbdoc: The amount to over-allocate the sheet by.
+	about: This is useful if you want to add a border around the sheet, or if you want to add some padding around the rectangles.
+	End Rem
+	Field overAllocate:Int
+
+	Rem
+	bbdoc: The minimum width of the sheets.
+	End Rem
+	Field minWidth:Int
+
+	Rem
+	bbdoc: The minimum height of the sheets.
+	End Rem
+	Field minHeight:Int
+
+	Rem
+	bbdoc: The maximum width of the sheets.
+	End Rem
+	Field maxWidth:Int
+
+	Rem
+	bbdoc: The maximum height of the sheets.
+	End Rem
+	Field maxHeight:Int
+
+	Field sizes:TArrayList<SRectSize> = New TArrayList<SRectSize>
+
+	Rem
+	bbdoc: Adds a rectangle with the given @id to the packer.
+	End Rem
+	Method Add(width:Int, height:Int, id:Int)
+
+		Local size:SRectSize = New SRectSize(width, height, id)
+		sizes.Add(size)
+
+	End Method
+
+	Rem
+	bbdoc: Packs the rectangles into sheets, based on the settings of the packer.
+	about: This method will return an array of #TPackedSheet objects, each of which contains the rectangles that have been packed into it.
+	Any rectangles that don't fit into the sheets will be discarded, and not be included in the returned array.
+	End Rem
+	Method Pack:TPackedSheet[]()
+		Return bmx_rectpacker_pack(Self, packingMethod, maxSheets, powerOfTwo, square, allowRotate, alignWidth, borderPadding, overAllocate, minWidth, minHeight, maxWidth, maxHeight, sizes.Count())
+	End Method
+
+Private
+	Function _GetSize(packer:TRectPacker, index:Int, width:Int Var, height:Int Var, id:Int Var) { nomangle }
+		Local size:SRectSize = packer.sizes[index]
+		width = size.width
+		height = size.height
+		id = size.id
+	End Function
+
+	Function _NewSheetArray:TPackedSheet[](size:Int) { nomangle }
+		Return New TPackedSheet[size]
+	End Function
+
+	Function _SetSheet(sheets:TPackedSheet[], index:Int, sheet:TPackedSheet) { nomangle }
+		sheets[index] = sheet
+	End Function
+
+End Type
+
+Struct SRectSize
+	Field width:Int
+	Field height:Int
+	Field id:Int
+
+	Method New(width:Int, height:Int, id:Int)
+		Self.width = width
+		Self.height = height
+		Self.id = id
+	End Method
+
+	Method Operator=:Int(other:SRectSize)
+		Return width = other.width And height = other.height And id = other.id
+	End Method
+End Struct
+
+Rem
+bbdoc: The packing method to use.
+about: The packing method determines how the rectangles are packed into the sheets.
+
+| Value                         | Description                                  |
+|-------------------------------|----------------------------------------------|
+| #Best                         | The best fitting from all of the available methods. |
+| #BestSkyline                  | The best available skyline method.           |
+| #BestMaxRects                 | The best available max rects method.         |
+| #SkylineBottomLeft            | The skyline bottom-left method.              |
+| #SkylineBestFit               | The skyline best-fit method.                 |
+| #MaxRectsBestShortSideFit     | The max rects best short-side fit method.    |
+| #MaxRectsBestLongSideFit      | The max rects best long-side fit method.     |
+| #MaxRectsBestAreaFit          | The max rects best area fit method.          |
+| #MaxRectsBottomLeftRule       | The max rects bottom-left rule method.       |
+| #MaxRectsContactPointRule     | The max rects contact-point rule method.     |
+End Rem
+Enum EPackingMethod
+	Best
+	BestSkyline
+	BestMaxRects
+	SkylineBottomLeft
+	SkylineBestFit
+	MaxRectsBestShortSideFit
+	MaxRectsBestLongSideFit
+	MaxRectsBestAreaFit
+	MaxRectsBottomLeftRule
+	MaxRectsContactPointRule
+End Enum
+
+Rem
+bbdoc: Represents a rectangle that has been packed into a sheet.
+End Rem
+Struct SPackedRect
+
+	Rem
+	bbdoc: The ID of the rectangle.
+	End Rem
+	Field id:Int
+
+	Rem
+	bbdoc: The X position of the rectangle.
+	End Rem
+	Field x:Int
+
+	Rem
+	bbdoc: The Y position of the rectangle.
+	End Rem
+	Field y:Int
+
+	Rem
+	bbdoc: The width of the rectangle.
+	End Rem
+	Field width:Int
+
+	Rem
+	bbdoc: The height of the rectangle.
+	End Rem
+	Field height:Int
+
+	Rem
+	bbdoc: Whether the rectangle has been rotated.
+	End Rem
+	Field rotated:Int
+
+	Method New(id:Int, x:Int, y:Int, width:Int, height:Int, rotated:Int)
+		Self.id = id
+		Self.x = x
+		Self.y = y
+		Self.width = width
+		Self.height = height
+		Self.rotated = rotated
+	End Method
+End Struct
+
+Rem
+bbdoc: Represents a sheet that has been packed with rectangles.
+End Rem
+Type TPackedSheet
+
+	Rem
+	bbdoc: The width of the sheet.
+	End Rem
+	Field width:Int
+
+	Rem
+	bbdoc: The height of the sheet.
+	End Rem
+	Field height:Int
+
+	Rem
+	bbdoc: The rectangles that have been packed into the sheet.
+	End Rem
+	Field rects:SPackedRect[]
+
+Private
+	Function _Create:TPackedSheet(width:Int, height:Int, size:Int) { nomangle }
+		Local sheet:TPackedSheet = New TPackedSheet
+		sheet.width = width
+		sheet.height = height
+		sheet.rects = New SPackedRect[size]
+		Return sheet
+	End Function
+
+	Function _SetRect(sheet:TPackedSheet, index:Int, id:Int, x:Int, y:Int, width:Int, height:Int, rotated:Int) { nomangle }
+		Local rect:SPackedRect = New SPackedRect(id, x, y, width, height, rotated)
+		sheet.rects[index] = rect
+	End Function
+
+End Type
+
+Extern
+
+	Function bmx_rectpacker_pack:TPackedSheet[](packer:TRectPacker, packingMethod:EPackingMethod, maxSheets:Int, powerOfTwo:Int, square:Int, allowRotate:Int, alignWidth:Int, borderPadding:Int, overAllocate:Int, minWidth:Int, minHeight:Int, maxWidth:Int, maxHeight:Int, count:Int)
+
+End Extern

+ 27 - 0
rectpacker.mod/source.bmx

@@ -0,0 +1,27 @@
+' Copyright (c) 2024 Bruce A Henderson
+' 
+' This software is provided 'as-is', without any express or implied
+' warranty. In no event will the authors be held liable for any damages
+' arising from the use of this software.
+' 
+' Permission is granted to anyone to use this software for any purpose,
+' including commercial applications, and to alter it and redistribute it
+' freely, subject to the following restrictions:
+' 
+' 1. The origin of this software must not be misrepresented; you must not
+'    claim that you wrote the original software. If you use this software
+'    in a product, an acknowledgment in the product documentation would be
+'    appreciated but is not required.
+' 2. Altered source versions must be plainly marked as such, and must not be
+'    misrepresented as being the original software.
+' 3. This notice may not be removed or altered from any source distribution.
+' 
+SuperStrict
+
+Import "rect_pack/*.h"
+
+Import "glue.cpp"
+
+Import "rect_pack/rect_pack_11.cpp"
+Import "rect_pack/MaxRectsBinPack.cpp"
+Import "rect_pack/stb_rect_pack.cpp"