/// <summary> /// Finds a way to pack all the given rectangles into a single bin. Performance can be traded for /// space efficiency by using the optional parameters. /// </summary> /// <param name="rectangles">The rectangles to pack. The result is saved onto this array.</param> /// <param name="bounds">The bounds of the resulting bin. This will always be at X=Y=0.</param> /// <param name="packingHint">Specifies hints for optimizing performance.</param> /// <param name="acceptableDensity">Searching stops once a bin is found with this density (usedArea/totalArea) or better.</param> /// <param name="stepSize">The amount by which to increment/decrement size when trying to pack another bin.</param> /// <remarks> /// The <see cref="PackingRectangle.Id"/> values are never touched. Use this to identify your rectangles. /// </remarks> public static void Pack(PackingRectangle[] rectangles, out PackingRectangle bounds, PackingHints packingHint = PackingHints.FindBest, float acceptableDensity = 1, uint stepSize = 1) { if (rectangles == null) { throw new ArgumentNullException(nameof(rectangles)); } if (stepSize == 0) { throw new ArgumentOutOfRangeException(nameof(stepSize), stepSize, nameof(stepSize) + " must be greater than 0."); } bounds = default; if (rectangles.Length == 0) { return; } // We separate the value in packingHint into the different options it specifies. Span <PackingHints> hints = stackalloc PackingHints[PackingHintExtensions.MaxHintCount]; PackingHintExtensions.GetFlagsFrom(packingHint, ref hints); if (hints.Length == 0) { throw new ArgumentException("No valid packing hints specified.", nameof(packingHint)); } // We'll try uint.MaxValue as initial bin size. The packing algoritm already tries to // use as little space as possible, so this will be QUICKLY cut down closer to the // final bin size. uint binSize = uint.MaxValue; // We turn the acceptableDensity parameter into an acceptableArea value, so we can // compare the area directly rather than having to calculate the density each time. uint totalArea = CalculateTotalArea(rectangles); acceptableDensity = Math.Clamp(acceptableDensity, 0.1f, 1); uint acceptableArea = (uint)Math.Ceiling(totalArea / acceptableDensity); // We get a list that will be used (and reused) by the packing algorithm. List <PackingRectangle> emptySpaces = GetList(rectangles.Length * 2); // We'll store the area of the best solution so far here. uint currentBestArea = uint.MaxValue; bool hasSolution = false; // In one array we'll store the current best solution, and we'll also need two temporary arrays. PackingRectangle[] currentBest = rectangles; PackingRectangle[] tmpBest = new PackingRectangle[rectangles.Length]; PackingRectangle[] tmpArray = new PackingRectangle[rectangles.Length]; // For each of the specified hints, we try to pack and see if we can find a better solution. for (int i = 0; i < hints.Length && currentBestArea > acceptableArea; i++) { // We copy the rectangles onto the tmpBest array, then sort them by what the packing hint says. currentBest.CopyTo(tmpBest, 0); PackingHintExtensions.SortByPackingHint(tmpBest, hints[i]); // We try to find the best bin for the rectangles in tmpBest. We give the function as // initial bin size the size of the best bin we got so far. The function never tries // bigger bin sizes, so if with a specified packingHint it can't pack smaller than // with the last solution, it simply stops. if (TryFindBestBin(emptySpaces, ref tmpBest, ref tmpArray, binSize - stepSize, stepSize, acceptableArea, out PackingRectangle boundsTmp)) { // We have a better solution! // We update the variables tracking the current best solution bounds = boundsTmp; currentBestArea = boundsTmp.Area; binSize = bounds.BiggerSide; // We swap tmpBest and currentBest PackingRectangle[] swaptmp = tmpBest; tmpBest = currentBest; currentBest = swaptmp; hasSolution = true; } } if (!hasSolution) { throw new Exception("Failed to find a solution. (Do your rectangles have a size close to uint.MaxValue or is your stepSize too high?)"); } // The solution should be in the "rectangles" array passed as parameter. if (currentBest != rectangles) { currentBest.CopyTo(rectangles, 0); } // We return the list so it can be used in subsequent pack operations. ReturnList(emptySpaces); }
/// <summary> /// Finds a way to pack all the given rectangles into a single bin. Performance can be traded for /// space efficiency by using the optional parameters. /// </summary> /// <param name="rectangles">The rectangles to pack. The result is saved onto this array.</param> /// <param name="bounds">The bounds of the resulting bin. This will always be at X=Y=0.</param> /// <param name="packingHint">Specifies hints for optimizing performance.</param> /// <param name="acceptableDensity">Searching stops once a bin is found with this density (usedArea/totalArea) or better.</param> /// <param name="stepSize">The amount by which to increment/decrement size when trying to pack another bin.</param> /// <remarks> /// The <see cref="PackingRectangle.Id"/> values are never touched. Use this to identify your rectangles. /// </remarks> public static void Pack(PackingRectangle[] rectangles, out PackingRectangle bounds, PackingHint packingHint = PackingHint.FindBest, float acceptableDensity = 1, uint stepSize = 1) { if (rectangles == null) { throw new ArgumentNullException(nameof(rectangles)); } if (stepSize == 0) { throw new ArgumentOutOfRangeException(nameof(stepSize), stepSize, nameof(stepSize) + " must be greater than 0."); } bounds = default; if (rectangles.Length == 0) { return; } // We separate the value in packingHint into the different options it specifies. Span <PackingHint> hints = stackalloc PackingHint[PackingHintExtensions.MaxHintCount]; PackingHintExtensions.GetFlagsFrom(packingHint, ref hints); if (hints.Length == 0) { throw new ArgumentException("No valid packing hints specified.", nameof(packingHint)); } // We calculate the initial bin size we'll try, alongisde the sum of the areas of the rectangles. uint totalArea = CalculateTotalArea(rectangles); uint binSize = (uint)Math.Ceiling(Math.Sqrt(totalArea) * 1.05); // We turn the acceptableDensity parameter into an acceptableArea value, so we can // compare the area directly rather than having to calculate the density. acceptableDensity = Math.Clamp(acceptableDensity, 0.1f, 1); uint acceptableArea = (uint)Math.Ceiling(totalArea / acceptableDensity); // We get a list that will be used by the packing algorithm. List <PackingRectangle> emptySpaces = GetList(rectangles.Length * 2); // We'll store the area of the best solution so far here. uint currentBestArea = uint.MaxValue; // In one array we'll store the current best solution, and we'll also need two temporary arrays. PackingRectangle[] currentBest = rectangles; PackingRectangle[] tmpBest = new PackingRectangle[rectangles.Length]; PackingRectangle[] tmpArray = new PackingRectangle[rectangles.Length]; // For each of the specified hints, we try to pack and see if we can find a better solution. for (int i = 0; i < hints.Length && currentBestArea > acceptableArea; i++) { // We copy the rectangles onto the tmpBest array, then sort them by what the packing hint says. currentBest.CopyTo(tmpBest, 0); PackingHintExtensions.SortByPackingHint(tmpBest, hints[i]); // We try to find the best bin for the rectangles in tmpBest. We give the function as // initial bin size, the size of the best bin we got so far. We only allow it to try // bigger bins if we don't have a solution yet (currentBestArea == uint.MaxValue). if (TryFindBestBin(emptySpaces, ref tmpBest, ref tmpArray, binSize, stepSize, currentBestArea == uint.MaxValue)) { // We have a possible solution! If it uses less area than our current best solution, // then we've got a new best solution. PackingRectangle boundsTmp = FindBounds(tmpBest); uint areaTmp = boundsTmp.Area; if (areaTmp < currentBestArea) { // We update the variables tracking the current best solution bounds = boundsTmp; currentBestArea = areaTmp; binSize = bounds.BiggerSide; // We swap tmpBest and currentBest PackingRectangle[] swaptmp = tmpBest; tmpBest = currentBest; currentBest = swaptmp; } } } if (currentBest != rectangles) { currentBest.CopyTo(rectangles, 0); } // We return the list so it can be used in subsequent pack operations. ReturnList(emptySpaces); }