// 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; } } }