#region CPL License /* Nuclex Framework Copyright (C) 2002-2011 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion using System; using System.Collections.Generic; using Microsoft.Xna.Framework; namespace Nuclex.Game.Packing { /// Rectangle packer using an algorithm by Javier Arevalo /// /// /// Original code by Javier Arevalo (jare at iguanademos dot com). Rewritten /// to C# / .NET by Markus Ewald (cygon at nuclex dot org). The following comments /// were written by the original author when he published his algorithm. /// /// /// You have a bunch of rectangular pieces. You need to arrange them in a /// rectangular surface so that they don't overlap, keeping the total area of the /// rectangle as small as possible. This is fairly common when arranging characters /// in a bitmapped font, lightmaps for a 3D engine, and I guess other situations as /// well. /// /// /// The idea of this algorithm is that, as we add rectangles, we can pre-select /// "interesting" places where we can try to add the next rectangles. For optimal /// results, the rectangles should be added in order. I initially tried using area /// as a sorting criteria, but it didn't work well with very tall or very flat /// rectangles. I then tried using the longest dimension as a selector, and it /// worked much better. So much for intuition... /// /// /// These "interesting" places are just to the right and just below the currently /// added rectangle. The first rectangle, obviously, goes at the top left, the next /// one would go either to the right or below this one, and so on. It is a weird way /// to do it, but it seems to work very nicely. /// /// /// The way we search here is fairly brute-force, the fact being that for most /// offline purposes the performance seems more than adequate. I have generated a /// japanese font with around 8500 characters and all the time was spent generating /// the bitmaps. /// /// /// Also, for all we care, we could grow the parent rectangle in a different way /// than power of two. It just happens that power of 2 is very convenient for /// graphics hardware textures. /// /// /// I'd be interested in hearing of other approaches to this problem. Make sure /// to post them on http://www.flipcode.com /// /// public class ArevaloRectanglePacker : RectanglePacker { #region class AnchorRankComparer /// Compares the 'rank' of anchoring points /// /// Anchoring points are potential locations for the placement of new rectangles. /// Each time a rectangle is inserted, an anchor point is generated on its upper /// right end and another one at its lower left end. The anchor points are kept /// in a list that is ordered by their closeness to the upper left corner of the /// packing area (their 'rank') so the packer favors positions that are closer to /// the upper left for new rectangles. /// private class AnchorRankComparer : IComparer { /// Provides a default instance for the anchor rank comparer public static AnchorRankComparer Default = new AnchorRankComparer(); /// Compares the rank of two anchors against each other /// Left anchor point that will be compared /// Right anchor point that will be compared /// The relation of the two anchor point's ranks to each other public int Compare(Point left, Point right) { //return Math.Min(left.X, left.Y) - Math.Min(right.X, right.Y); return (left.X + left.Y) - (right.X + right.Y); } } #endregion /// Initializes a new rectangle packer /// Maximum width of the packing area /// Maximum height of the packing area public ArevaloRectanglePacker(int packingAreaWidth, int packingAreaHeight) : base(packingAreaWidth, packingAreaHeight) { this.packedRectangles = new List(); this.anchors = new List(); this.anchors.Add(new Point(0, 0)); this.actualPackingAreaWidth = 1; this.actualPackingAreaHeight = 1; } /// Tries to allocate space for a rectangle in the packing area /// Width of the rectangle to allocate /// Height of the rectangle to allocate /// Output parameter receiving the rectangle's placement /// True if space for the rectangle could be allocated public override bool TryPack( int rectangleWidth, int rectangleHeight, out Point placement ) { // Try to find an anchor where the rectangle fits in, enlarging the packing // area and repeating the search recursively until it fits or the // maximum allowed size is exceeded. int anchorIndex = selectAnchorRecursive( rectangleWidth, rectangleHeight, this.actualPackingAreaWidth, this.actualPackingAreaHeight ); // No anchor could be found at which the rectangle did fit in if(anchorIndex == -1) { placement = Point.Zero; return false; } placement = this.anchors[anchorIndex]; // Move the rectangle either to the left or to the top until it collides with // a neightbouring rectangle. This is done to combat the effect of lining up // rectangles with gaps to the left or top of them because the anchor that // would allow placement there has been blocked by another rectangle optimizePlacement(ref placement, rectangleWidth, rectangleHeight); // Remove the used anchor and add new anchors at the upper right and lower left // positions of the new rectangle { // The anchor is only removed if the placement optimization didn't // move the rectangle so far that the anchor isn't blocked anymore bool blocksAnchor = ((placement.X + rectangleWidth) > this.anchors[anchorIndex].X) && ((placement.Y + rectangleHeight) > this.anchors[anchorIndex].Y); if(blocksAnchor) this.anchors.RemoveAt(anchorIndex); // Add new anchors at the upper right and lower left coordinates of the rectangle insertAnchor(new Point(placement.X + rectangleWidth, placement.Y)); insertAnchor(new Point(placement.X, placement.Y + rectangleHeight)); } // Finally, we can add the rectangle to our packed rectangles list this.packedRectangles.Add( new Rectangle(placement.X, placement.Y, rectangleWidth, rectangleHeight) ); return true; } /// /// Optimizes the rectangle's placement by moving it either left or up to fill /// any gaps resulting from rectangles blocking the anchors of the most optimal /// placements. /// /// Placement to be optimized /// Width of the rectangle to be optimized /// Height of the rectangle to be optimized private void optimizePlacement( ref Point placement, int rectangleWidth, int rectangleHeight ) { Rectangle rectangle = new Rectangle( placement.X, placement.Y, rectangleWidth, rectangleHeight ); // Try to move the rectangle to the left as far as possible int leftMost = placement.X; while(isFree(ref rectangle, PackingAreaWidth, PackingAreaHeight)) { leftMost = rectangle.X; --rectangle.X; } // Reset rectangle to original position rectangle.X = placement.X; // Try to move the rectangle upwards as far as possible int topMost = placement.Y; while(isFree(ref rectangle, PackingAreaWidth, PackingAreaHeight)) { topMost = rectangle.Y; --rectangle.Y; } // Use the dimension in which the rectangle could be moved farther if((placement.X - leftMost) > (placement.Y - topMost)) placement.X = leftMost; else placement.Y = topMost; } /// /// Searches for a free anchor and recursively enlarges the packing area /// if none can be found. /// /// Width of the rectangle to be placed /// Height of the rectangle to be placed /// Width of the tested packing area /// Height of the tested packing area /// /// Index of the anchor the rectangle is to be placed at or -1 if the rectangle /// does not fit in the packing area anymore. /// private int selectAnchorRecursive( int rectangleWidth, int rectangleHeight, int testedPackingAreaWidth, int testedPackingAreaHeight ) { // Try to locate an anchor point where the rectangle fits in int freeAnchorIndex = findFirstFreeAnchor( rectangleWidth, rectangleHeight, testedPackingAreaWidth, testedPackingAreaHeight ); // If a the rectangle fits without resizing packing area (any further in case // of a recursive call), take over the new packing area size and return the // anchor at which the rectangle can be placed. if(freeAnchorIndex != -1) { this.actualPackingAreaWidth = testedPackingAreaWidth; this.actualPackingAreaHeight = testedPackingAreaHeight; return freeAnchorIndex; } // // If we reach this point, the rectangle did not fit in the current packing // area and our only choice is to try and enlarge the packing area. // // For readability, determine whether the packing area can be enlarged // any further in its width and in its height bool canEnlargeWidth = (testedPackingAreaWidth < PackingAreaWidth); bool canEnlargeHeight = (testedPackingAreaHeight < PackingAreaHeight); bool shouldEnlargeHeight = (!canEnlargeWidth) || (testedPackingAreaHeight < testedPackingAreaWidth); // Try to enlarge the smaller of the two dimensions first (unless the smaller // dimension is already at its maximum size). 'shouldEnlargeHeight' is true // when the height was the smaller dimension or when the width is maxed out. if(canEnlargeHeight && shouldEnlargeHeight) { // Try to double the height of the packing area return selectAnchorRecursive( rectangleWidth, rectangleHeight, testedPackingAreaWidth, Math.Min(testedPackingAreaHeight * 2, PackingAreaHeight) ); } else if(canEnlargeWidth) { // Try to double the width of the packing area return selectAnchorRecursive( rectangleWidth, rectangleHeight, Math.Min(testedPackingAreaWidth * 2, PackingAreaWidth), testedPackingAreaHeight ); } else { // Both dimensions are at their maximum sizes and the rectangle still // didn't fit. We give up! return -1; } } /// Locates the first free anchor at which the rectangle fits /// Width of the rectangle to be placed /// Height of the rectangle to be placed /// Total width of the packing area /// Total height of the packing area /// The index of the first free anchor or -1 if none is found private int findFirstFreeAnchor( int rectangleWidth, int rectangleHeight, int testedPackingAreaWidth, int testedPackingAreaHeight ) { Rectangle potentialLocation = new Rectangle( 0, 0, rectangleWidth, rectangleHeight ); // Walk over all anchors (which are ordered by their distance to the // upper left corner of the packing area) until one is discovered that // can house the new rectangle. for(int index = 0; index < this.anchors.Count; ++index) { potentialLocation.X = this.anchors[index].X; potentialLocation.Y = this.anchors[index].Y; // See if the rectangle would fit in at this anchor point if(isFree(ref potentialLocation, testedPackingAreaWidth, testedPackingAreaHeight)) return index; } // No anchor points were found where the rectangle would fit in return -1; } /// /// Determines whether the rectangle can be placed in the packing area /// at its current location. /// /// Rectangle whose position to check /// Total width of the packing area /// Total height of the packing area /// True if the rectangle can be placed at its current position private bool isFree( ref Rectangle rectangle, int testedPackingAreaWidth, int testedPackingAreaHeight ) { // If the rectangle is partially or completely outside of the packing // area, it can't be placed at its current location bool leavesPackingArea = (rectangle.X < 0) || (rectangle.Y < 0) || (rectangle.Right > testedPackingAreaWidth) || (rectangle.Bottom > testedPackingAreaHeight); if(leavesPackingArea) return false; // Brute-force search whether the rectangle touches any of the other // rectangles already in the packing area for(int index = 0; index < this.packedRectangles.Count; ++index) { if(this.packedRectangles[index].Intersects(rectangle)) return false; } // Success! The rectangle is inside the packing area and doesn't overlap // with any other rectangles that have already been packed. return true; } /// Inserts a new anchor point into the anchor list /// Anchor point that will be inserted /// /// This method tries to keep the anchor list ordered by ranking the anchors /// depending on the distance from the top left corner in the packing area. /// private void insertAnchor(Point anchor) { // Find out where to insert the new anchor based on its rank (which is // calculated based on the anchor's distance to the top left corner of // the packing area). // // From MSDN on BinarySearch(): // "If the List does not contain the specified value, the method returns // a negative integer. You can apply the bitwise complement operation (~) to // this negative integer to get the index of the first element that is // larger than the search value." int insertIndex = this.anchors.BinarySearch(anchor, AnchorRankComparer.Default); if(insertIndex < 0) insertIndex = ~insertIndex; // Insert the anchor at the index matching its rank this.anchors.Insert(insertIndex, anchor); } /// Current width of the packing area private int actualPackingAreaWidth; /// Current height of the packing area private int actualPackingAreaHeight; /// Rectangles contained in the packing area private List packedRectangles; /// Anchoring points where new rectangles can potentially be placed private List anchors; } } // namespace Nuclex.Game.Packing