// Copyright (c) Xenko contributors (https://xenko.com) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using System; using System.Collections.Generic; using System.Runtime.Serialization; namespace math { /// /// Implementation of a "Guillotine" packer. /// More information at http://clb.demon.fi/files/RectangleBinPack.pdf. /// public class GuillotinePacker { private readonly List freeRectangles = new List(); private readonly List tempFreeRectangles = new List(); /// /// A delegate callback used by /// /// The index of the rectangle /// The rectangle found public delegate void InsertRectangleCallback(int cascadeIndex, ref Rectangle rectangle); /// /// Current width used by the packer. /// public int Width { get; private set; } /// /// Current height used by the packer. /// public int Height { get; private set; } /// /// Clears the specified region. /// /// The width. /// The height. public void Clear(int width, int height) { freeRectangles.Clear(); freeRectangles.Add(new Rectangle { X = 0, Y = 0, Width = width, Height = height }); Width = width; Height = height; } /// /// Clears the whole region. /// public virtual void Clear() { Clear(Width, Height); } /// /// Frees the specified old rectangle. /// /// The old rectangle. public void Free(ref Rectangle oldRectangle) { freeRectangles.Add(oldRectangle); } /// /// Tries to fit a single rectangle with the specified width and height. /// /// Width requested. /// Height requested /// Fill with the rectangle if it was successfully inserted. /// true if it was successfully inserted. public bool Insert(int width, int height, ref Rectangle bestRectangle) { return Insert(width, height, freeRectangles, ref bestRectangle); } /// /// Tries to fit multiple rectangle with (width, height). /// /// Width requested. /// Height requested /// The number of rectangle to fit. /// A callback called for each rectangle successfully fitted. /// true if all rectangles were successfully fitted. public bool TryInsert(int width, int height, int count, InsertRectangleCallback inserted) { var bestRectangle = new Rectangle(); tempFreeRectangles.Clear(); foreach (var freeRectangle in freeRectangles) { tempFreeRectangles.Add(freeRectangle); } for (var i = 0; i < count; ++i) { if (!Insert(width, height, tempFreeRectangles, ref bestRectangle)) { tempFreeRectangles.Clear(); return false; } inserted(i, ref bestRectangle); } // if the insertion went well, use the new configuration freeRectangles.Clear(); foreach (var tempFreeRectangle in tempFreeRectangles) { freeRectangles.Add(tempFreeRectangle); } tempFreeRectangles.Clear(); return true; } private static bool Insert(int width, int height, List freeRectanglesList, ref Rectangle bestRectangle) { // Info on algorithm: http://clb.demon.fi/files/RectangleBinPack.pdf int bestScore = int.MaxValue; int freeRectangleIndex = -1; // Find space for new rectangle for (int i = 0; i < freeRectanglesList.Count; ++i) { var currentFreeRectangle = freeRectanglesList[i]; if (width == currentFreeRectangle.Width && height == currentFreeRectangle.Height) { // Perfect fit bestRectangle.X = currentFreeRectangle.X; bestRectangle.Y = currentFreeRectangle.Y; bestRectangle.Width = width; bestRectangle.Height = height; freeRectangleIndex = i; break; } if (width <= currentFreeRectangle.Width && height <= currentFreeRectangle.Height) { // Can fit inside // Use "BAF" heuristic (best area fit) var score = currentFreeRectangle.Width * currentFreeRectangle.Height - width * height; if (score < bestScore) { bestRectangle.X = currentFreeRectangle.X; bestRectangle.Y = currentFreeRectangle.Y; bestRectangle.Width = width; bestRectangle.Height = height; bestScore = score; freeRectangleIndex = i; } } } // No space could be found if (freeRectangleIndex == -1) return false; var freeRectangle = freeRectanglesList[freeRectangleIndex]; // Choose an axis to split (trying to minimize the smaller area "MINAS") int w = freeRectangle.Width - bestRectangle.Width; int h = freeRectangle.Height - bestRectangle.Height; var splitHorizontal = (bestRectangle.Width * h > w * bestRectangle.Height); // Form the two new rectangles. var bottom = new Rectangle { X = freeRectangle.X, Y = freeRectangle.Y + bestRectangle.Height, Width = splitHorizontal ? freeRectangle.Width : bestRectangle.Width, Height = h }; var right = new Rectangle { X = freeRectangle.X + bestRectangle.Width, Y = freeRectangle.Y, Width = w, Height = splitHorizontal ? bestRectangle.Height : freeRectangle.Height }; if (bottom.Width > 0 && bottom.Height > 0) freeRectanglesList.Add(bottom); if (right.Width > 0 && right.Height > 0) freeRectanglesList.Add(right); // Remove previously selected freeRectangle if (freeRectangleIndex != freeRectanglesList.Count - 1) freeRectanglesList[freeRectangleIndex] = freeRectanglesList[freeRectanglesList.Count - 1]; freeRectanglesList.RemoveAt(freeRectanglesList.Count - 1); return true; } } }