Example #1
0
        /// <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);
        }
Example #2
0
        /// <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);
        }