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