Finally disabled wasted area calculation for good in the 'cygon' packer because benchmarks show it as being counterproductive; implemented rectangle placement optimization as it is done in the original 'arevalo' packer - the C# version should now produce the exact identical results the C++ version does
git-svn-id: file:///srv/devel/repo-conversion/nusu@25 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
parent
dbc1da27a8
commit
9157bf8454
|
@ -31,7 +31,7 @@
|
|||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\x86\Release</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<DefineConstants>TRACE;UNITTEST</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<NoStdLib>true</NoStdLib>
|
||||
|
|
|
@ -35,11 +35,23 @@ namespace Nuclex.Support.Packing {
|
|||
/// <summary>Tests the packer's efficiency using a deterministic benchmark</summary>
|
||||
[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");
|
||||
}
|
||||
|
||||
/// <summary>Tests the packer's stability by running a complete benchmark</summary>
|
||||
[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
|
||||
|
|
|
@ -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 {
|
|||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="placement">Placement to be optimized</param>
|
||||
/// <param name="rectangleWidth">Width of the rectangle to be optimized</param>
|
||||
/// <param name="rectangleHeight">Height of the rectangle to be optimized</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for a free anchor and enlarges the packing area if none can be found
|
||||
/// </summary>
|
||||
/// <param name="rectangleWidth">Width of the rectangle to be placed</param>
|
||||
/// <param name="rectangleHeight">Height of the rectangle to be placed</param>
|
||||
/// <param name="packingAreaWidth">Total width of the packing area</param>
|
||||
/// <param name="packingAreaHeight">Total height of the packing area</param>
|
||||
/// <param name="testedPackingAreaWidth">Width of the tested packing area</param>
|
||||
/// <param name="testedPackingAreaHeight">Height of the tested packing area</param>
|
||||
/// <returns>
|
||||
/// Index of the anchor the rectangle is to be placed at or -1 if the rectangle
|
||||
/// does not fit in the packing area anymore
|
||||
/// </returns>
|
||||
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;
|
||||
|
||||
|
|
|
@ -35,11 +35,23 @@ namespace Nuclex.Support.Packing {
|
|||
/// <summary>Tests the packer's efficiency using a deterministic benchmark</summary>
|
||||
[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");
|
||||
}
|
||||
|
||||
/// <summary>Tests the packer's stability by running a complete benchmark</summary>
|
||||
[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
|
||||
|
|
|
@ -41,6 +41,32 @@ namespace Nuclex.Support.Packing {
|
|||
/// </remarks>
|
||||
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.
|
||||
|
||||
/// <summary>By how much the wasted area influences a placement's score</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
|
@ -61,7 +87,9 @@ namespace Nuclex.Support.Packing {
|
|||
/// packing problems is a matter of trial and error, as it seems :)
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
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;
|
||||
|
|
|
@ -27,6 +27,10 @@ namespace Nuclex.Support.Packing {
|
|||
/// <summary>Base class for unit testing the rectangle packers</summary>
|
||||
public abstract class RectanglePackerTest {
|
||||
|
||||
/// <summary>Delegate for a Rectangle Packer factory method</summary>
|
||||
/// <returns>A new rectangle packer</returns>
|
||||
protected delegate RectanglePacker BuildRectanglePacker();
|
||||
|
||||
/// <summary>Determines the efficiency of a packer with a packing area of 70x70</summary>
|
||||
/// <param name="packer">Packer with a packing area of 70x70 units</param>
|
||||
/// <returns>The efficiency factor of the packer</returns>
|
||||
|
@ -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.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Benchmarks the provided rectangle packer using random data</summary>
|
||||
/// <param name="packerBuilder">
|
||||
/// Rectangle packer builder returning new rectangle packers
|
||||
/// with an area of 1024 x 1024
|
||||
/// </param>
|
||||
/// <returns>The achieved benchmark score</returns>
|
||||
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
|
||||
|
|
|
@ -35,11 +35,23 @@ namespace Nuclex.Support.Packing {
|
|||
/// <summary>Tests the packer's efficiency using a deterministic benchmark</summary>
|
||||
[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");
|
||||
}
|
||||
|
||||
/// <summary>Tests the packer's stability by running a complete benchmark</summary>
|
||||
[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
|
||||
|
|
Loading…
Reference in New Issue
Block a user