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