diff --git a/Nuclex.Support (PC).csproj b/Nuclex.Support (PC).csproj
index 6232ffb..e4c7929 100644
--- a/Nuclex.Support (PC).csproj
+++ b/Nuclex.Support (PC).csproj
@@ -31,7 +31,7 @@
pdbonly
true
bin\x86\Release
- TRACE
+ TRACE;UNITTEST
prompt
4
true
diff --git a/Source/Packing/ArevaloRectanglePacker.Test.cs b/Source/Packing/ArevaloRectanglePacker.Test.cs
index d6131a9..5b66cf6 100644
--- a/Source/Packing/ArevaloRectanglePacker.Test.cs
+++ b/Source/Packing/ArevaloRectanglePacker.Test.cs
@@ -35,11 +35,23 @@ namespace Nuclex.Support.Packing {
/// Tests the packer's efficiency using a deterministic benchmark
[Test]
public void TestSpaceEfficiency() {
- float efficiency = calculateEfficiency(new ArevaloRectanglePacker(70, 70));
+ float efficiency = CalculateEfficiency(new ArevaloRectanglePacker(70, 70));
Assert.GreaterOrEqual(efficiency, 0.75, "Packer achieves 75% efficiency");
}
+ /// Tests the packer's stability by running a complete benchmark
+ [Test]
+ public void TestStability() {
+ float score = Benchmark(
+ delegate() { return new ArevaloRectanglePacker(1024, 1024); }
+ );
+
+ // This is mainly a stability and performance test. It fails when the
+ // packer crashes on its own and is otherwise only there to tell how long
+ // it takes to complete the benchmark.
+ }
+
}
} // namespace Nuclex.Support.Packing
diff --git a/Source/Packing/ArevaloRectanglePacker.cs b/Source/Packing/ArevaloRectanglePacker.cs
index 2f30229..21ee988 100644
--- a/Source/Packing/ArevaloRectanglePacker.cs
+++ b/Source/Packing/ArevaloRectanglePacker.cs
@@ -137,12 +137,30 @@ namespace Nuclex.Support.Packing {
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
- this.anchors.RemoveAt(anchorIndex);
- this.anchors.Add(new Point(placement.X + rectangleWidth, placement.Y));
- this.anchors.Add(new Point(placement.X, placement.Y + rectangleHeight));
+ {
+ // The anchor is only removed if the placement optimization didn't
+ // move the rectangle so far that the anchor isn't used at all
+ 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
+ this.anchors.Add(new Point(placement.X + rectangleWidth, placement.Y));
+ this.anchors.Add(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)
);
@@ -151,55 +169,98 @@ namespace Nuclex.Support.Packing {
}
+ ///
+ /// 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((leftMost - placement.X) > (topMost - placement.Y))
+ placement.X = leftMost;
+ else
+ placement.Y = topMost;
+ }
+
///
/// Searches for a free anchor and enlarges the packing area if none can be found
///
/// 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
+ /// 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 packingAreaWidth, int packingAreaHeight
+ int testedPackingAreaWidth, int testedPackingAreaHeight
) {
// Try to locate an anchor point where the rectangle fits in
int freeAnchorIndex = findFirstFreeAnchor(
- rectangleWidth, rectangleHeight, packingAreaWidth, packingAreaHeight
+ 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 = packingAreaWidth;
- this.actualPackingAreaHeight = packingAreaHeight;
+ 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.
+ //
- bool canEnlargeWidth = (packingAreaWidth < PackingAreaWidth);
- bool canEnlargeHeight = (packingAreaHeight < PackingAreaHeight);
+ // 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);
// Try to enlarge the smaller of the two dimensions first (unless the smaller
// dimension is already at its maximum size)
if(
- (
- (packingAreaHeight < packingAreaWidth) || !canEnlargeWidth
- ) && canEnlargeHeight
- ) {
+ canEnlargeHeight && (
+ (testedPackingAreaHeight < testedPackingAreaWidth) || !canEnlargeWidth
+ )
+ ) {
// Try to double the height of the packing area
return selectAnchorRecursive(
rectangleWidth, rectangleHeight,
- packingAreaWidth, Math.Min(packingAreaHeight * 2, PackingAreaHeight)
+ testedPackingAreaWidth, Math.Min(testedPackingAreaHeight * 2, PackingAreaHeight)
);
} else if(canEnlargeWidth) {
@@ -207,7 +268,7 @@ namespace Nuclex.Support.Packing {
// Try to double the width of the packing area
return selectAnchorRecursive(
rectangleWidth, rectangleHeight,
- Math.Min(packingAreaWidth * 2, PackingAreaWidth), packingAreaHeight
+ Math.Min(testedPackingAreaWidth * 2, PackingAreaWidth), testedPackingAreaHeight
);
} else {
@@ -262,14 +323,14 @@ namespace Nuclex.Support.Packing {
ref Rectangle rectangle, int packingAreaWidth, int packingAreaHeight
) {
+ // 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 >= packingAreaWidth) ||
(rectangle.Bottom >= packingAreaHeight);
- // If the rectangle is partially or completely outside of the packing
- // area, it can't be placed at its current location
if(leavesPackingArea)
return false;
diff --git a/Source/Packing/CygonRectanglePacker.Test.cs b/Source/Packing/CygonRectanglePacker.Test.cs
index 95c906a..46acceb 100644
--- a/Source/Packing/CygonRectanglePacker.Test.cs
+++ b/Source/Packing/CygonRectanglePacker.Test.cs
@@ -35,11 +35,23 @@ namespace Nuclex.Support.Packing {
/// Tests the packer's efficiency using a deterministic benchmark
[Test]
public void TestSpaceEfficiency() {
- float efficiency = calculateEfficiency(new CygonRectanglePacker(70, 70));
+ float efficiency = CalculateEfficiency(new CygonRectanglePacker(70, 70));
Assert.GreaterOrEqual(efficiency, 0.75f, "Packer achieves 75% efficiency");
}
+ /// Tests the packer's stability by running a complete benchmark
+ [Test]
+ public void TestStability() {
+ float score = Benchmark(
+ delegate() { return new CygonRectanglePacker(1024, 1024); }
+ );
+
+ // This is mainly a stability and performance test. It fails when the
+ // packer crashes on its own and is otherwise only there to tell how long
+ // it takes to complete the benchmark.
+ }
+
}
} // namespace Nuclex.Support.Packing
diff --git a/Source/Packing/CygonRectanglePacker.cs b/Source/Packing/CygonRectanglePacker.cs
index 9b0fb4a..4abd0d7 100644
--- a/Source/Packing/CygonRectanglePacker.cs
+++ b/Source/Packing/CygonRectanglePacker.cs
@@ -41,6 +41,32 @@ namespace Nuclex.Support.Packing {
///
public class CygonRectanglePacker : RectanglePacker {
+ #if USE_WASTED_AREA
+ // An optimization idea of mine. With this, the packer not only tries to place
+ // rectangles as low in the packing area as possible, it also tried to choose
+ // locations where it doesn't block gaps where other rectangles might still fit
+ // in. This turned out to be counter-productive and a marginal improvement in
+ // space efficiency could be achieved by deliberately choosing positions where
+ // gaps where blocked for future rectangles.
+ //
+ // These are the results of a benchmark with different wastedAreaScoreWeights
+ //
+ // -10 579.315
+ // -5 582.140
+ // -4 582.886
+ // -3 583.166
+ // -2 583.792
+ // -1 583.975 (best)
+ // 0 583.791
+ // 1 583.960
+ // 2 583.469
+ // 3 582.444
+ // 4 580.259
+ // 5 578.400
+ // 10 570.467
+ //
+ // Needless to say, I chose to disable this splendid optimization.
+
/// By how much the wasted area influences a placement's score
///
///
@@ -61,7 +87,9 @@ namespace Nuclex.Support.Packing {
/// packing problems is a matter of trial and error, as it seems :)
///
///
- private const int WastedAreaScoreWeight = 3;
+ private const int WastedAreaScoreWeight = 10;
+
+ #endif // USE_WASTED_AREA
#region class SliceStartComparer
@@ -161,7 +189,7 @@ namespace Nuclex.Support.Packing {
if((highest + rectangleHeight < PackingAreaHeight)) {
int score = highest;
- // WASTED AREA CALCULATION --------------------------------------------------
+ #if USE_WASTED_AREA // --------------------------------------------------------
// Calculate the amount of space that would go to waste if the rectangle
// would be placed at this location
@@ -185,7 +213,7 @@ namespace Nuclex.Support.Packing {
// Alter the score by the amount of wasted area in relation to
score += (wastedArea * WastedAreaScoreWeight / rectangleArea);
- // WASTED AREA CALCULATION --------------------------------------------------
+ #endif // USE_WASTED_AREA -----------------------------------------------------
if(score < bestScore) {
bestSliceIndex = leftSliceIndex;
diff --git a/Source/Packing/RectanglePacker.Test.cs b/Source/Packing/RectanglePacker.Test.cs
index df78863..d4d8c95 100644
--- a/Source/Packing/RectanglePacker.Test.cs
+++ b/Source/Packing/RectanglePacker.Test.cs
@@ -27,6 +27,10 @@ namespace Nuclex.Support.Packing {
/// Base class for unit testing the rectangle packers
public abstract class RectanglePackerTest {
+ /// Delegate for a Rectangle Packer factory method
+ /// A new rectangle packer
+ protected delegate RectanglePacker BuildRectanglePacker();
+
/// Determines the efficiency of a packer with a packing area of 70x70
/// Packer with a packing area of 70x70 units
/// The efficiency factor of the packer
@@ -37,7 +41,7 @@ namespace Nuclex.Support.Packing {
/// the efficiency rating is to 1.0, the better, with 0.99 being the
/// mathematically best rating achievable.
///
- public float calculateEfficiency(RectanglePacker packer) {
+ protected float CalculateEfficiency(RectanglePacker packer) {
// If we take a 1x1 square, a 2x2 square, etc. up to a 24x24 square,
// the sum of the areas of these squares is 4900, which is 70². This
// is the only nontrivial sum of consecutive squares starting with
@@ -54,6 +58,44 @@ namespace Nuclex.Support.Packing {
return (float)areaCovered / 4900.0f;
}
+ /// Benchmarks the provided rectangle packer using random data
+ ///
+ /// Rectangle packer builder returning new rectangle packers
+ /// with an area of 1024 x 1024
+ ///
+ /// The achieved benchmark score
+ protected float Benchmark(BuildRectanglePacker packerBuilder) {
+ // How many runs to perform for getting a stable average
+ const int averagingRuns = 200;
+
+ // Generates the random number seeds. This is used so that each run produces
+ // the same number sequences and makes the comparison of different algorithms
+ // a little bit more stable.
+ Random seedGenerator = new Random(12345);
+ int rectanglesPacked = 0;
+
+ // Perform a number of runs to get a semi-stable average score
+ for(int averagingRun = 0; averagingRun < averagingRuns; ++averagingRun) {
+ Random dimensionGenerator = new Random(seedGenerator.Next());
+ RectanglePacker packer = packerBuilder();
+
+ // Try to cramp as many rectangles into the packing area as possible
+ for(;; ++rectanglesPacked) {
+ Point placement;
+
+ int width = dimensionGenerator.Next(16, 64);
+ int height = dimensionGenerator.Next(16, 64);
+
+ // As soon as the packer rejects the first rectangle, the run is over
+ if(!packer.TryPack(width, height, out placement))
+ break;
+ }
+ }
+
+ // Return the average score achieved by the packer
+ return (float)rectanglesPacked / (float)averagingRuns;
+ }
+
}
} // namespace Nuclex.Support.Packing
diff --git a/Source/Packing/SimpleRectanglePacker.Test.cs b/Source/Packing/SimpleRectanglePacker.Test.cs
index 0a2de4a..ee028c4 100644
--- a/Source/Packing/SimpleRectanglePacker.Test.cs
+++ b/Source/Packing/SimpleRectanglePacker.Test.cs
@@ -35,11 +35,23 @@ namespace Nuclex.Support.Packing {
/// Tests the packer's efficiency using a deterministic benchmark
[Test]
public void TestSpaceEfficiency() {
- float efficiency = calculateEfficiency(new SimpleRectanglePacker(70, 70));
+ float efficiency = CalculateEfficiency(new SimpleRectanglePacker(70, 70));
Assert.GreaterOrEqual(efficiency, 0.75, "Packer achieves 75% efficiency");
}
+ /// Tests the packer's stability by running a complete benchmark
+ [Test]
+ public void TestStability() {
+ float score = Benchmark(
+ delegate() { return new SimpleRectanglePacker(1024, 1024); }
+ );
+
+ // This is mainly a stability and performance test. It fails when the
+ // packer crashes on its own and is otherwise only there to tell how long
+ // it takes to complete the benchmark.
+ }
+
}
} // namespace Nuclex.Support.Packing