diff --git a/Source/Packing/CygonRectanglePacker.Test.cs b/Source/Packing/CygonRectanglePacker.Test.cs index a89c710..95c906a 100644 --- a/Source/Packing/CygonRectanglePacker.Test.cs +++ b/Source/Packing/CygonRectanglePacker.Test.cs @@ -36,8 +36,8 @@ namespace Nuclex.Support.Packing { [Test] public void TestSpaceEfficiency() { float efficiency = calculateEfficiency(new CygonRectanglePacker(70, 70)); - - Assert.GreaterOrEqual(efficiency, 0.75, "Packer achieves 75% efficiency"); + + Assert.GreaterOrEqual(efficiency, 0.75f, "Packer achieves 75% efficiency"); } } diff --git a/Source/Packing/CygonRectanglePacker.cs b/Source/Packing/CygonRectanglePacker.cs index d70370a..1c6cc67 100644 --- a/Source/Packing/CygonRectanglePacker.cs +++ b/Source/Packing/CygonRectanglePacker.cs @@ -25,6 +25,13 @@ using Microsoft.Xna.Framework; namespace Nuclex.Support.Packing { /// Packer using a custom algorithm by Markus Ewald + /// + /// This algorithms always places as close to the top as possible. So, for any new + /// rectangle, the packer has to determine a X coordinate at which the rectangle + /// can be placed at the highest point. To quickly discover these locations, + /// the packer keeps a dynamically list of "height slices", which store the + /// baseline of the rectangles that have been placed so far. + /// public class CygonRectanglePacker : RectanglePacker { #region class SliceStartComparer @@ -68,22 +75,111 @@ namespace Nuclex.Support.Packing { public override bool TryAllocate( int rectangleWidth, int rectangleHeight, out Point placement ) { - integrateRectangle(0, 1, 5); - integrateRectangle(20, 5, 30); - integrateRectangle(10, 10, 50); - integrateRectangle(10, 15, 25); - integrateRectangle(35, 20, 25); - integrateRectangle(40, 25, 15); + int sliceIndex = findBestPosition(rectangleWidth, rectangleHeight); - placement = Point.Zero; - return false; + // TODO: Rectangle might not even fit there! + if(sliceIndex == -1) { + + placement = Point.Zero; + return false; + + } else { + + placement = this.heightSlices[sliceIndex]; + + integrateRectangle( + this.heightSlices[sliceIndex].X, + rectangleWidth, + this.heightSlices[sliceIndex].Y + rectangleHeight + ); + + return true; + + } + } + + /// Finds the best position for a rectangle of the given width + /// Width of the rectangle to find a position for + /// Height of the rectangle to find a position for + /// The best position for a rectangle with the specified width + private int findBestPosition(int rectangleWidth, int rectangleHeight) { + int leftSliceIndex = 0; + int rightSliceIndex = this.heightSlices.BinarySearch( + new Point(rectangleWidth, 0), SliceStartComparer.Default + ); + if(rightSliceIndex < 0) + rightSliceIndex = ~rightSliceIndex; + + int leastWastedSliceIndex = -1; + int leastWastedArea = MaxPackingAreaWidth * MaxPackingAreaHeight; + + while(rightSliceIndex <= this.heightSlices.Count) { + // final time (this is the special case where the rectangle is attempted + // to be placed at the rightmost end of the packing area) + + /* + // Determine the highest slice at this position. We cannot put the rectangle + // any lower than this without colliding into other rectangles + int highest = this.heightSlices[leftSliceIndex].Y; + for(int index = leftSliceIndex + 1; index < rightSliceIndex; ++index) + highest = Math.Max(highest, this.heightSlices[index].Y); + + // Calculate the amount of space that would go to waste if the rectangle + // would be placed at this location + int wastedArea = 0; + for(int index = leftSliceIndex; index < rightSliceIndex - 1; ++index) { + int sliceWidth = this.heightSlices[index + 1].X - this.heightSlices[index].X; + wastedArea += (highest - this.heightSlices[index].Y) * sliceWidth; + } + wastedArea += + (highest - this.heightSlices[rightSliceIndex - 1].Y) * + ( + (this.heightSlices[leftSliceIndex].X + rectangleWidth) - + this.heightSlices[rightSliceIndex - 1].X + ); + + // If this beats the current record for the least wasted area, remember this as + // being the best position found so far + if( + (wastedArea < leastWastedArea) && + (this.heightSlices[leftSliceIndex].Y + rectangleHeight < MaxPackingAreaHeight) + ) { + leastWastedArea = wastedArea; + leastWastedSliceIndex = leftSliceIndex; + + // No sense looking any further if we found the perfect place! + if(leastWastedArea == 0) + break; + } + */ + + + // If this already was the loop after the final slice, terminate it now! + if(rightSliceIndex == this.heightSlices.Count) + break; + + // Advance the starting slice to the next slice start + ++leftSliceIndex; + int rightEnd = this.heightSlices[leftSliceIndex].X + rectangleWidth; + + // Advance the ending slice to where the rectangle ends now + while(rightEnd > this.heightSlices[rightSliceIndex].X) { + ++rightSliceIndex; + + // If the end is reached, stop shifting and make the outer loop run one final time + if(rightSliceIndex == this.heightSlices.Count) + break; + } + } + + return leastWastedSliceIndex; } /// Integrates a new rectangle into the height slice table /// Position of the rectangle's left side - /// Position of the rectangle's lower side /// Width of the rectangle - private void integrateRectangle(int left, int bottom, int width) { + /// Position of the rectangle's lower side + private void integrateRectangle(int left, int width, int bottom) { // Find the first slice that is touched by the rectangle int startSlice = this.heightSlices.BinarySearch( @@ -97,7 +193,6 @@ namespace Nuclex.Support.Packing { // We scored a direct hit, so we can replace the slice we have hit firstSliceOriginalHeight = this.heightSlices[startSlice].Y; this.heightSlices[startSlice] = new Point(left, bottom); - ++startSlice; } else { // No direct hit, slice starts inside another slice @@ -108,58 +203,56 @@ namespace Nuclex.Support.Packing { } - // Special case, the rectangle was on the last slice, so we cannot - // use the start slice + 1 as start index for the binary search + int right = left + width; + ++startSlice; + + // Special case, the rectangle started on the last slice, so we cannot + // use the start slice + 1 for the binary search and the possibly already + // modified start slice height now only remains in our temporary + // firstSliceOriginalHeight variable if(startSlice >= this.heightSlices.Count) { - } else { - int right = left + width; + + // If the slice ends within the last slice (usual case, unless it has the + // exact same with the packing area has), add another slice to return to the + // original height at the end of the rectangle. + if(right < MaxPackingAreaWidth) + this.heightSlices.Add(new Point(right, firstSliceOriginalHeight)); + + } else { // The rectangle doesn't start on the last slice int endSlice = this.heightSlices.BinarySearch( startSlice, this.heightSlices.Count - startSlice, new Point(right, 0), SliceStartComparer.Default ); - } - //this.heightSlices.RemoveRange(startSlice, endSlice - startSlice); -/* - int nextSlice = firstSlice + 1; - bool isLastSlice = (nextSlice >= this.heightSlices.Count); + // Another direct hit on the final slice's end? + if(endSlice > 0) { -*/ -/* + this.heightSlices.RemoveRange(startSlice, endSlice - startSlice); + } else { // No direct hit, rectangle ends inside another slice - int nextSlice = firstSlice + 1; - bool isLastSlice = (nextSlice >= this.heightSlices.Count); + endSlice = ~endSlice; - // - bool endsInFirstSlice; - if(isLastSlice) - endsInFirstSlice = right < MaxPackingAreaWidth; - else - endsInFirstSlice = right < this.heightSlices[nextSlice].X; + // Find out to which height we need to return at the right end of + // the rectangle + int returnHeight; + if(endSlice == startSlice) + returnHeight = firstSliceOriginalHeight; + else + returnHeight = this.heightSlices[endSlice - 1].Y; + + // Remove all slices covered by the rectangle and began a new slice at + // its end to return to the height the slice in which the rectangle ends + // has had. + this.heightSlices.RemoveRange(startSlice, endSlice - startSlice); + if(right < MaxPackingAreaWidth) + this.heightSlices.Insert(startSlice, new Point(right, returnHeight)); - if(endsInFirstSlice) { - this.heightSlices.Insert( - nextSlice, new Point(right, this.heightSlices[firstSlice].Y) - ); - this.heightSlices[firstSlice] = new Point(left, bottom); - return; - } else { // Integrated rect continues beyond the discovered slice - this.heightSlices[firstSlice] = new Point(left, bottom); } - } else { - - firstSlice = ~firstSlice; - - //firstSliceOriginalHeight = this.heightSlices[firstSlice].Y; - - this.heightSlices.Insert(firstSlice, new Point(left, bottom)); - ++firstSlice; - } -*/ + } /// Stores the height silhouette of the rectangles