/// <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);
        }
Exemple #2
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);
        }