Renamed Allocate() methods to Pack() in RectanglePacker; fixed an oversight in Nuclex.Support that made overlapping rectangles possible; reactivated wasted area calculation for 'cygon' rectangle packer

git-svn-id: file:///srv/devel/repo-conversion/nusu@23 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
Markus Ewald 2007-05-21 19:04:48 +00:00
parent 4fd0680ae7
commit 116fb53b0a
5 changed files with 90 additions and 78 deletions

View File

@ -27,6 +27,10 @@ namespace Nuclex.Support.Packing {
/// <summary>Rectangle packer using an algorithm by Javier Arevalo</summary>
/// <remarks>
/// <para>
/// Original code by Javier Arevalo (jare at iguanademos dot com). Rewritten
/// to C# / .NET by Markus Ewald (cygon at nuclex dot org).
/// </para>
/// <para>
/// You have a bunch of rectangular pieces. You need to arrange them in a
/// rectangular surface so that they don't overlap, keeping the total area of the
/// rectangle as small as possible. This is fairly common when arranging characters
@ -95,10 +99,10 @@ namespace Nuclex.Support.Packing {
#endregion
/// <summary>Initializes a new rectangle packer</summary>
/// <param name="maxPackingAreaWidth">Maximum width of the packing area</param>
/// <param name="maxPackingAreaHeight">Maximum height of the packing area</param>
public ArevaloRectanglePacker(int maxPackingAreaWidth, int maxPackingAreaHeight)
: base(maxPackingAreaWidth, maxPackingAreaHeight) {
/// <param name="packingAreaWidth">Maximum width of the packing area</param>
/// <param name="packingAreaHeight">Maximum height of the packing area</param>
public ArevaloRectanglePacker(int packingAreaWidth, int packingAreaHeight)
: base(packingAreaWidth, packingAreaHeight) {
this.packedRectangles = new List<Rectangle>();
this.anchors = new List<Point>();
@ -113,7 +117,7 @@ namespace Nuclex.Support.Packing {
/// <param name="rectangleHeight">Height of the rectangle to allocate</param>
/// <param name="placement">Output parameter receiving the rectangle's placement</param>
/// <returns>True if space for the rectangle could be allocated</returns>
public override bool TryAllocate(
public override bool TryPack(
int rectangleWidth, int rectangleHeight, out Point placement
) {
@ -181,8 +185,8 @@ namespace Nuclex.Support.Packing {
// 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 < MaxPackingAreaWidth);
bool canEnlargeHeight = (packingAreaHeight < MaxPackingAreaHeight);
bool canEnlargeWidth = (packingAreaWidth < PackingAreaWidth);
bool canEnlargeHeight = (packingAreaHeight < PackingAreaHeight);
// Try to enlarge the smaller of the two dimensions first (unless the smaller
// dimension is already at its maximum size)
@ -195,7 +199,7 @@ namespace Nuclex.Support.Packing {
// Try to double the height of the packing area
return selectAnchorRecursive(
rectangleWidth, rectangleHeight,
packingAreaWidth, Math.Min(packingAreaHeight * 2, MaxPackingAreaHeight)
packingAreaWidth, Math.Min(packingAreaHeight * 2, PackingAreaHeight)
);
} else if(canEnlargeWidth) {
@ -203,7 +207,7 @@ namespace Nuclex.Support.Packing {
// Try to double the width of the packing area
return selectAnchorRecursive(
rectangleWidth, rectangleHeight,
Math.Min(packingAreaWidth * 2, MaxPackingAreaWidth), packingAreaHeight
Math.Min(packingAreaWidth * 2, PackingAreaWidth), packingAreaHeight
);
} else {

View File

@ -24,13 +24,20 @@ using Microsoft.Xna.Framework;
namespace Nuclex.Support.Packing {
/// <summary>Packer using a custom algorithm by Markus Ewald</summary>
/// <summary>Packer using a custom algorithm by Markus 'Cygon' Ewald</summary>
/// <remarks>
/// 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.
/// <para>
/// Algorithm conceived by Markus Ewald (cygon at nuclex dot org), thought
/// I'm quite sure I'm not the first one to invent this algorithm :)
/// </para>
/// <para>
/// This algorithm always places rectangles as low as possible. So, for any
/// new rectangle that is to be added into the packing area, the packer has
/// to determine the X coordinate at which the rectangle has the lowest height.
/// To quickly discover these locations, the packer keeps a dynamically updated
/// list of "height slices" which store the silhouette of the rectangles that
/// have been placed so far.
/// </para>
/// </remarks>
public class CygonRectanglePacker : RectanglePacker {
@ -55,10 +62,10 @@ namespace Nuclex.Support.Packing {
#endregion
/// <summary>Initializes a new rectangle packer</summary>
/// <param name="maxPackingAreaWidth">Maximum width of the packing area</param>
/// <param name="maxPackingAreaHeight">Maximum height of the packing area</param>
public CygonRectanglePacker(int maxPackingAreaWidth, int maxPackingAreaHeight)
: base(maxPackingAreaWidth, maxPackingAreaHeight) {
/// <param name="packingAreaWidth">Maximum width of the packing area</param>
/// <param name="packingAreaHeight">Maximum height of the packing area</param>
public CygonRectanglePacker(int packingAreaWidth, int packingAreaHeight)
: base(packingAreaWidth, packingAreaHeight) {
this.heightSlices = new List<Point>();
@ -72,41 +79,37 @@ namespace Nuclex.Support.Packing {
/// <param name="rectangleHeight">Height of the rectangle to allocate</param>
/// <param name="placement">Output parameter receiving the rectangle's placement</param>
/// <returns>True if space for the rectangle could be allocated</returns>
public override bool TryAllocate(
public override bool TryPack(
int rectangleWidth, int rectangleHeight, out Point placement
) {
int sliceIndex = findBestPosition(rectangleWidth, rectangleHeight);
// TODO: Rectangle might not even fit there!
if(sliceIndex == -1) {
// If the rectangle is larger than the packing area in any dimension,
// it will never fit!
if(
(rectangleWidth > PackingAreaWidth) || (rectangleHeight > PackingAreaHeight)
) {
placement = Point.Zero;
return false;
} else {
placement = this.heightSlices[sliceIndex];
integrateRectangle(
this.heightSlices[sliceIndex].X,
rectangleWidth,
this.heightSlices[sliceIndex].Y + rectangleHeight
);
return true;
}
bool fits = findBestPosition(rectangleWidth, rectangleHeight, out placement);
if(fits)
integrateRectangle(placement.X, rectangleWidth, placement.Y + rectangleHeight);
return fits;
}
/// <summary>Finds the best position for a rectangle of the given width</summary>
/// <param name="rectangleWidth">Width of the rectangle to find a position for</param>
/// <param name="rectangleHeight">Height of the rectangle to find a position for</param>
/// <returns>The best position for a rectangle with the specified width</returns>
private int findBestPosition(int rectangleWidth, int rectangleHeight) {
private bool findBestPosition(
int rectangleWidth, int rectangleHeight, out Point placement
) {
// Index and score of the best slice we could find for the rectangle
int bestSliceIndex = -1;
int bestScore = MaxPackingAreaWidth * MaxPackingAreaHeight; // lower == better!
// Slice index, vertical position and score of the best placement we could find
int bestSliceIndex = -1; // Slice index where the best placement was found
int bestSliceY = 0; // Y position of the best placement found
int bestScore = PackingAreaWidth * PackingAreaHeight; // lower == better!
// This is the counter for the currently checked position. The search works by
// skipping from slice to slice, determining the suitability of the location for the
@ -130,11 +133,11 @@ namespace Nuclex.Support.Packing {
if(this.heightSlices[index].Y > highest)
highest = this.heightSlices[index].Y;
if((highest + rectangleHeight < MaxPackingAreaHeight)) {
if((highest + rectangleHeight < PackingAreaHeight)) {
int score = highest;
// TESTING --------------------------------------------------
/*
// WASTED AREA CALCULATION --------------------------------------------------
// Calculate the amount of space that would go to waste if the rectangle
// would be placed at this location
int wastedArea = 0;
@ -149,14 +152,13 @@ namespace Nuclex.Support.Packing {
this.heightSlices[rightSliceIndex - 1].X
);
//score += Math.Sign(wastedArea);
//score += (int)Math.Sqrt((double)wastedArea);
//score = wastedArea;
*/
// TESTING --------------------------------------------------
score += (int)Math.Sqrt((double)wastedArea) / 10;
// WASTED AREA CALCULATION --------------------------------------------------
if(score < bestScore) {
bestSliceIndex = leftSliceIndex;
bestSliceY = highest;
bestScore = score;
}
}
@ -172,7 +174,7 @@ namespace Nuclex.Support.Packing {
for(; rightSliceIndex <= this.heightSlices.Count; ++rightSliceIndex) {
int rightSliceStart;
if(rightSliceIndex == this.heightSlices.Count)
rightSliceStart = MaxPackingAreaWidth;
rightSliceStart = PackingAreaWidth;
else
rightSliceStart = this.heightSlices[rightSliceIndex].X;
@ -190,7 +192,13 @@ namespace Nuclex.Support.Packing {
// Return the index of the best slice we found for this rectangle. If the rectangle
// didn't fit, this variable will still have its initialization value of -1.
return bestSliceIndex;
if(bestSliceIndex == -1) {
placement = Point.Zero;
return false;
} else {
placement = new Point(this.heightSlices[bestSliceIndex].X, bestSliceY);
return true;
}
}
@ -234,7 +242,7 @@ namespace Nuclex.Support.Packing {
// If the slice ends within the last slice (usual case, unless it has the
// exact same width the packing area has), add another slice to return to
// the original height at the end of the rectangle.
if(right < MaxPackingAreaWidth)
if(right < PackingAreaWidth)
this.heightSlices.Add(new Point(right, firstSliceOriginalHeight));
} else { // The rectangle doesn't start on the last slice
@ -264,7 +272,7 @@ namespace Nuclex.Support.Packing {
// Remove all slices covered by the rectangle and begin a new slice at its end
// to return back to the height of the slice on which the rectangle ends.
this.heightSlices.RemoveRange(startSlice, endSlice - startSlice);
if(right < MaxPackingAreaWidth)
if(right < PackingAreaWidth)
this.heightSlices.Insert(startSlice, new Point(right, returnHeight));
}

View File

@ -47,7 +47,7 @@ namespace Nuclex.Support.Packing {
for(int size = 24; size >= 1; --size) {
Point placement;
if(packer.TryAllocate(size, size, out placement))
if(packer.TryPack(size, size, out placement))
areaCovered += size * size;
}

View File

@ -32,28 +32,28 @@ namespace Nuclex.Support.Packing {
/// performant one for a given job.
/// </para>
/// <para>
/// An almost exhaustive list of rectangle packers can be found here:
/// An almost exhaustive list of packing algorithms can be found here:
/// http://www.csc.liv.ac.uk/~epa/surveyhtml.html
/// </para>
/// </remarks>
public abstract class RectanglePacker {
/// <summary>Initializes a new rectangle packer</summary>
/// <param name="maxPackingAreaWidth">Maximum width of the packing area</param>
/// <param name="maxPackingAreaHeight">Maximum height of the packing area</param>
protected RectanglePacker(int maxPackingAreaWidth, int maxPackingAreaHeight) {
this.maxPackingAreaWidth = maxPackingAreaWidth;
this.maxPackingAreaHeight = maxPackingAreaHeight;
/// <param name="packingAreaWidth">Width of the packing area</param>
/// <param name="packingAreaHeight">Height of the packing area</param>
protected RectanglePacker(int packingAreaWidth, int packingAreaHeight) {
this.packingAreaWidth = packingAreaWidth;
this.packingAreaHeight = packingAreaHeight;
}
/// <summary>Allocates space for a rectangle in the packing area</summary>
/// <param name="rectangleWidth">Width of the rectangle to allocate</param>
/// <param name="rectangleHeight">Height of the rectangle to allocate</param>
/// <returns>The location at which the rectangle has been placed</returns>
public virtual Point Allocate(int rectangleWidth, int rectangleHeight) {
public virtual Point Pack(int rectangleWidth, int rectangleHeight) {
Point point;
if(!TryAllocate(rectangleWidth, rectangleHeight, out point))
if(!TryPack(rectangleWidth, rectangleHeight, out point))
throw new Exception("Rectangle does not fit in packing area");
return point;
@ -64,24 +64,24 @@ namespace Nuclex.Support.Packing {
/// <param name="rectangleHeight">Height of the rectangle to allocate</param>
/// <param name="placement">Output parameter receiving the rectangle's placement</param>
/// <returns>True if space for the rectangle could be allocated</returns>
public abstract bool TryAllocate(
public abstract bool TryPack(
int rectangleWidth, int rectangleHeight, out Point placement
);
/// <summary>Maximum width the packing area is allowed to have</summary>
protected int MaxPackingAreaWidth {
get { return this.maxPackingAreaWidth; }
protected int PackingAreaWidth {
get { return this.packingAreaWidth; }
}
/// <summary>Maximum height the packing area is allowed to have</summary>
protected int MaxPackingAreaHeight {
get { return this.maxPackingAreaHeight; }
protected int PackingAreaHeight {
get { return this.packingAreaHeight; }
}
/// <summary>Maximum allowed width of the packing area</summary>
private int maxPackingAreaWidth;
private int packingAreaWidth;
/// <summary>Maximum allowed height of the packing area</summary>
private int maxPackingAreaHeight;
private int packingAreaHeight;
}

View File

@ -34,38 +34,38 @@ namespace Nuclex.Support.Packing {
public class SimpleRectanglePacker : RectanglePacker {
/// <summary>Initializes a new rectangle packer</summary>
/// <param name="maxPackingAreaWidth">Maximum width of the packing area</param>
/// <param name="maxPackingAreaHeight">Maximum height of the packing area</param>
public SimpleRectanglePacker(int maxPackingAreaWidth, int maxPackingAreaHeight)
: base(maxPackingAreaWidth, maxPackingAreaHeight) { }
/// <param name="packingAreaWidth">Maximum width of the packing area</param>
/// <param name="packingAreaHeight">Maximum height of the packing area</param>
public SimpleRectanglePacker(int packingAreaWidth, int packingAreaHeight)
: base(packingAreaWidth, packingAreaHeight) { }
/// <summary>Tries to allocate space for a rectangle in the packing area</summary>
/// <param name="rectangleWidth">Width of the rectangle to allocate</param>
/// <param name="rectangleHeight">Height of the rectangle to allocate</param>
/// <param name="placement">Output parameter receiving the rectangle's placement</param>
/// <returns>True if space for the rectangle could be allocated</returns>
public override bool TryAllocate(
public override bool TryPack(
int rectangleWidth, int rectangleHeight, out Point placement
) {
// If the rectangle is larger than the packing area in any dimension,
// it will never fit!
if(
(rectangleWidth > MaxPackingAreaWidth) || (rectangleHeight > MaxPackingAreaHeight)
(rectangleWidth > PackingAreaWidth) || (rectangleHeight > PackingAreaHeight)
) {
placement = Point.Zero;
return false;
}
// Do we have to start a new line ?
if(this.column + rectangleWidth > MaxPackingAreaWidth) {
if(this.column + rectangleWidth > PackingAreaWidth) {
this.currentLine += this.lineHeight;
this.lineHeight = 0;
this.column = 0;
}
// If it doesn't fit vertically now, the packing area is considered full
if(this.currentLine + rectangleHeight > MaxPackingAreaHeight) {
if(this.currentLine + rectangleHeight > PackingAreaHeight) {
placement = Point.Zero;
return false;
}