/// <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); }
/// <summary> /// Tries to pack the rectangles in the given order into a bin of the specified size. /// </summary> /// <param name="emptySpaces">The list of empty spaces for reusing.</param> /// <param name="unpacked">The unpacked rectangles.</param> /// <param name="packed">Where the resulting rectangles will be written.</param> /// <param name="binWidth">The width of the bin.</param> /// <param name="binHeight">The height of the bin.</param> /// <returns>Whether the operation succeeded.</returns> /// <remarks>The unpacked and packed spans can be the same.</remarks> private static bool TryPackAsOrdered(List <PackingRectangle> emptySpaces, Span <PackingRectangle> unpacked, Span <PackingRectangle> packed, uint binWidth, uint binHeight) { // We clear the empty spaces list and add one space covering the entire bin. emptySpaces.Clear(); emptySpaces.Add(new PackingRectangle(0, 0, binWidth, binHeight)); // We loop through all the rectangles. for (int r = 0; r < unpacked.Length; r++) { // We try to find a space for the rectangle. If we can't, then we return false. if (!TryFindBestSpace(unpacked[r], emptySpaces, out int spaceIndex)) { return(false); } PackingRectangle oldSpace = emptySpaces[spaceIndex]; packed[r] = unpacked[r]; packed[r].X = oldSpace.X; packed[r].Y = oldSpace.Y; // We calculate the width and height of the rectangles from splitting the empty space uint freeWidth = oldSpace.Width - packed[r].Width; uint freeHeight = oldSpace.Height - packed[r].Height; if (freeWidth != 0 && freeHeight != 0) { emptySpaces.RemoveAt(spaceIndex); // Both freeWidth and freeHeight are different from 0. We need to split the // empty space into two (plus the image). We split it in such a way that the // bigger rectangle will be where there is the most space. if (freeWidth > freeHeight) { emptySpaces.AddSorted(new PackingRectangle(packed[r].Right, oldSpace.Y, freeWidth, oldSpace.Height)); emptySpaces.AddSorted(new PackingRectangle(oldSpace.X, packed[r].Bottom, packed[r].Width, freeHeight)); } else { emptySpaces.AddSorted(new PackingRectangle(oldSpace.X, packed[r].Bottom, oldSpace.Width, freeHeight)); emptySpaces.AddSorted(new PackingRectangle(packed[r].Right, oldSpace.Y, freeWidth, packed[r].Height)); } } else if (freeWidth == 0) { // We only need to change the Y and height of the space. oldSpace.Y += packed[r].Height; oldSpace.Height = freeHeight; emptySpaces[spaceIndex] = oldSpace; EnsureSorted(emptySpaces, spaceIndex); //emptySpaces.RemoveAt(spaceIndex); //emptySpaces.Add(new PackingRectangle(oldSpace.X, oldSpace.Y + packed[r].Height, oldSpace.Width, freeHeight)); } else if (freeHeight == 0) { // We only need to change the X and width of the space. oldSpace.X += packed[r].Width; oldSpace.Width = freeWidth; emptySpaces[spaceIndex] = oldSpace; EnsureSorted(emptySpaces, spaceIndex); //emptySpaces.RemoveAt(spaceIndex); //emptySpaces.Add(new PackingRectangle(oldSpace.X + packed[r].Width, oldSpace.Y, freeWidth, oldSpace.Height)); } else // The rectangle uses up the entire empty space. { emptySpaces.RemoveAt(spaceIndex); } } return(true); }