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> /// <summary>Rectangle packer using an algorithm by Javier Arevalo</summary>
/// <remarks> /// <remarks>
/// <para> /// <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 /// 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 /// 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 /// rectangle as small as possible. This is fairly common when arranging characters
@ -95,10 +99,10 @@ namespace Nuclex.Support.Packing {
#endregion #endregion
/// <summary>Initializes a new rectangle packer</summary> /// <summary>Initializes a new rectangle packer</summary>
/// <param name="maxPackingAreaWidth">Maximum width of the packing area</param> /// <param name="packingAreaWidth">Maximum width of the packing area</param>
/// <param name="maxPackingAreaHeight">Maximum height of the packing area</param> /// <param name="packingAreaHeight">Maximum height of the packing area</param>
public ArevaloRectanglePacker(int maxPackingAreaWidth, int maxPackingAreaHeight) public ArevaloRectanglePacker(int packingAreaWidth, int packingAreaHeight)
: base(maxPackingAreaWidth, maxPackingAreaHeight) { : base(packingAreaWidth, packingAreaHeight) {
this.packedRectangles = new List<Rectangle>(); this.packedRectangles = new List<Rectangle>();
this.anchors = new List<Point>(); 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="rectangleHeight">Height of the rectangle to allocate</param>
/// <param name="placement">Output parameter receiving the rectangle's placement</param> /// <param name="placement">Output parameter receiving the rectangle's placement</param>
/// <returns>True if space for the rectangle could be allocated</returns> /// <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 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 // 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. // area and our only choice is to try and enlarge the packing area.
bool canEnlargeWidth = (packingAreaWidth < MaxPackingAreaWidth); bool canEnlargeWidth = (packingAreaWidth < PackingAreaWidth);
bool canEnlargeHeight = (packingAreaHeight < MaxPackingAreaHeight); bool canEnlargeHeight = (packingAreaHeight < PackingAreaHeight);
// Try to enlarge the smaller of the two dimensions first (unless the smaller // Try to enlarge the smaller of the two dimensions first (unless the smaller
// dimension is already at its maximum size) // dimension is already at its maximum size)
@ -195,7 +199,7 @@ namespace Nuclex.Support.Packing {
// Try to double the height of the packing area // Try to double the height of the packing area
return selectAnchorRecursive( return selectAnchorRecursive(
rectangleWidth, rectangleHeight, rectangleWidth, rectangleHeight,
packingAreaWidth, Math.Min(packingAreaHeight * 2, MaxPackingAreaHeight) packingAreaWidth, Math.Min(packingAreaHeight * 2, PackingAreaHeight)
); );
} else if(canEnlargeWidth) { } else if(canEnlargeWidth) {
@ -203,7 +207,7 @@ namespace Nuclex.Support.Packing {
// Try to double the width of the packing area // Try to double the width of the packing area
return selectAnchorRecursive( return selectAnchorRecursive(
rectangleWidth, rectangleHeight, rectangleWidth, rectangleHeight,
Math.Min(packingAreaWidth * 2, MaxPackingAreaWidth), packingAreaHeight Math.Min(packingAreaWidth * 2, PackingAreaWidth), packingAreaHeight
); );
} else { } else {

View File

@ -24,13 +24,20 @@ using Microsoft.Xna.Framework;
namespace Nuclex.Support.Packing { 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> /// <remarks>
/// This algorithms always places as close to the top as possible. So, for any new /// <para>
/// rectangle, the packer has to determine a X coordinate at which the rectangle /// Algorithm conceived by Markus Ewald (cygon at nuclex dot org), thought
/// can be placed at the highest point. To quickly discover these locations, /// I'm quite sure I'm not the first one to invent this algorithm :)
/// the packer keeps a dynamically list of "height slices", which store the /// </para>
/// baseline of the rectangles that have been placed so far. /// <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> /// </remarks>
public class CygonRectanglePacker : RectanglePacker { public class CygonRectanglePacker : RectanglePacker {
@ -55,10 +62,10 @@ namespace Nuclex.Support.Packing {
#endregion #endregion
/// <summary>Initializes a new rectangle packer</summary> /// <summary>Initializes a new rectangle packer</summary>
/// <param name="maxPackingAreaWidth">Maximum width of the packing area</param> /// <param name="packingAreaWidth">Maximum width of the packing area</param>
/// <param name="maxPackingAreaHeight">Maximum height of the packing area</param> /// <param name="packingAreaHeight">Maximum height of the packing area</param>
public CygonRectanglePacker(int maxPackingAreaWidth, int maxPackingAreaHeight) public CygonRectanglePacker(int packingAreaWidth, int packingAreaHeight)
: base(maxPackingAreaWidth, maxPackingAreaHeight) { : base(packingAreaWidth, packingAreaHeight) {
this.heightSlices = new List<Point>(); 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="rectangleHeight">Height of the rectangle to allocate</param>
/// <param name="placement">Output parameter receiving the rectangle's placement</param> /// <param name="placement">Output parameter receiving the rectangle's placement</param>
/// <returns>True if space for the rectangle could be allocated</returns> /// <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 rectangleWidth, int rectangleHeight, out Point placement
) { ) {
int sliceIndex = findBestPosition(rectangleWidth, rectangleHeight); // If the rectangle is larger than the packing area in any dimension,
// it will never fit!
// TODO: Rectangle might not even fit there! if(
if(sliceIndex == -1) { (rectangleWidth > PackingAreaWidth) || (rectangleHeight > PackingAreaHeight)
) {
placement = Point.Zero; placement = Point.Zero;
return false; 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> /// <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="rectangleWidth">Width of the rectangle to find a position for</param>
/// <param name="rectangleHeight">Height 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> /// <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 // Slice index, vertical position and score of the best placement we could find
int bestSliceIndex = -1; int bestSliceIndex = -1; // Slice index where the best placement was found
int bestScore = MaxPackingAreaWidth * MaxPackingAreaHeight; // lower == better! 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 // 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 // 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) if(this.heightSlices[index].Y > highest)
highest = this.heightSlices[index].Y; highest = this.heightSlices[index].Y;
if((highest + rectangleHeight < MaxPackingAreaHeight)) { if((highest + rectangleHeight < PackingAreaHeight)) {
int score = highest; int score = highest;
// TESTING -------------------------------------------------- // WASTED AREA CALCULATION --------------------------------------------------
/*
// Calculate the amount of space that would go to waste if the rectangle // Calculate the amount of space that would go to waste if the rectangle
// would be placed at this location // would be placed at this location
int wastedArea = 0; int wastedArea = 0;
@ -149,14 +152,13 @@ namespace Nuclex.Support.Packing {
this.heightSlices[rightSliceIndex - 1].X this.heightSlices[rightSliceIndex - 1].X
); );
//score += Math.Sign(wastedArea); score += (int)Math.Sqrt((double)wastedArea) / 10;
//score += (int)Math.Sqrt((double)wastedArea);
//score = wastedArea; // WASTED AREA CALCULATION --------------------------------------------------
*/
// TESTING --------------------------------------------------
if(score < bestScore) { if(score < bestScore) {
bestSliceIndex = leftSliceIndex; bestSliceIndex = leftSliceIndex;
bestSliceY = highest;
bestScore = score; bestScore = score;
} }
} }
@ -172,7 +174,7 @@ namespace Nuclex.Support.Packing {
for(; rightSliceIndex <= this.heightSlices.Count; ++rightSliceIndex) { for(; rightSliceIndex <= this.heightSlices.Count; ++rightSliceIndex) {
int rightSliceStart; int rightSliceStart;
if(rightSliceIndex == this.heightSlices.Count) if(rightSliceIndex == this.heightSlices.Count)
rightSliceStart = MaxPackingAreaWidth; rightSliceStart = PackingAreaWidth;
else else
rightSliceStart = this.heightSlices[rightSliceIndex].X; 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 // 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. // 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 // 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 // exact same width the packing area has), add another slice to return to
// the original height at the end of the rectangle. // the original height at the end of the rectangle.
if(right < MaxPackingAreaWidth) if(right < PackingAreaWidth)
this.heightSlices.Add(new Point(right, firstSliceOriginalHeight)); this.heightSlices.Add(new Point(right, firstSliceOriginalHeight));
} else { // The rectangle doesn't start on the last slice } 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 // 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. // to return back to the height of the slice on which the rectangle ends.
this.heightSlices.RemoveRange(startSlice, endSlice - startSlice); this.heightSlices.RemoveRange(startSlice, endSlice - startSlice);
if(right < MaxPackingAreaWidth) if(right < PackingAreaWidth)
this.heightSlices.Insert(startSlice, new Point(right, returnHeight)); 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) { for(int size = 24; size >= 1; --size) {
Point placement; Point placement;
if(packer.TryAllocate(size, size, out placement)) if(packer.TryPack(size, size, out placement))
areaCovered += size * size; areaCovered += size * size;
} }

View File

@ -32,28 +32,28 @@ namespace Nuclex.Support.Packing {
/// performant one for a given job. /// performant one for a given job.
/// </para> /// </para>
/// <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 /// http://www.csc.liv.ac.uk/~epa/surveyhtml.html
/// </para> /// </para>
/// </remarks> /// </remarks>
public abstract class RectanglePacker { public abstract class RectanglePacker {
/// <summary>Initializes a new rectangle packer</summary> /// <summary>Initializes a new rectangle packer</summary>
/// <param name="maxPackingAreaWidth">Maximum width of the packing area</param> /// <param name="packingAreaWidth">Width of the packing area</param>
/// <param name="maxPackingAreaHeight">Maximum height of the packing area</param> /// <param name="packingAreaHeight">Height of the packing area</param>
protected RectanglePacker(int maxPackingAreaWidth, int maxPackingAreaHeight) { protected RectanglePacker(int packingAreaWidth, int packingAreaHeight) {
this.maxPackingAreaWidth = maxPackingAreaWidth; this.packingAreaWidth = packingAreaWidth;
this.maxPackingAreaHeight = maxPackingAreaHeight; this.packingAreaHeight = packingAreaHeight;
} }
/// <summary>Allocates space for a rectangle in the packing area</summary> /// <summary>Allocates space for a rectangle in the packing area</summary>
/// <param name="rectangleWidth">Width of the rectangle to allocate</param> /// <param name="rectangleWidth">Width of the rectangle to allocate</param>
/// <param name="rectangleHeight">Height 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> /// <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; Point point;
if(!TryAllocate(rectangleWidth, rectangleHeight, out point)) if(!TryPack(rectangleWidth, rectangleHeight, out point))
throw new Exception("Rectangle does not fit in packing area"); throw new Exception("Rectangle does not fit in packing area");
return point; return point;
@ -64,24 +64,24 @@ namespace Nuclex.Support.Packing {
/// <param name="rectangleHeight">Height 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> /// <param name="placement">Output parameter receiving the rectangle's placement</param>
/// <returns>True if space for the rectangle could be allocated</returns> /// <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 int rectangleWidth, int rectangleHeight, out Point placement
); );
/// <summary>Maximum width the packing area is allowed to have</summary> /// <summary>Maximum width the packing area is allowed to have</summary>
protected int MaxPackingAreaWidth { protected int PackingAreaWidth {
get { return this.maxPackingAreaWidth; } get { return this.packingAreaWidth; }
} }
/// <summary>Maximum height the packing area is allowed to have</summary> /// <summary>Maximum height the packing area is allowed to have</summary>
protected int MaxPackingAreaHeight { protected int PackingAreaHeight {
get { return this.maxPackingAreaHeight; } get { return this.packingAreaHeight; }
} }
/// <summary>Maximum allowed width of the packing area</summary> /// <summary>Maximum allowed width of the packing area</summary>
private int maxPackingAreaWidth; private int packingAreaWidth;
/// <summary>Maximum allowed height of the packing area</summary> /// <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 { public class SimpleRectanglePacker : RectanglePacker {
/// <summary>Initializes a new rectangle packer</summary> /// <summary>Initializes a new rectangle packer</summary>
/// <param name="maxPackingAreaWidth">Maximum width of the packing area</param> /// <param name="packingAreaWidth">Maximum width of the packing area</param>
/// <param name="maxPackingAreaHeight">Maximum height of the packing area</param> /// <param name="packingAreaHeight">Maximum height of the packing area</param>
public SimpleRectanglePacker(int maxPackingAreaWidth, int maxPackingAreaHeight) public SimpleRectanglePacker(int packingAreaWidth, int packingAreaHeight)
: base(maxPackingAreaWidth, maxPackingAreaHeight) { } : base(packingAreaWidth, packingAreaHeight) { }
/// <summary>Tries to allocate space for a rectangle in the packing area</summary> /// <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="rectangleWidth">Width of the rectangle to allocate</param>
/// <param name="rectangleHeight">Height 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> /// <param name="placement">Output parameter receiving the rectangle's placement</param>
/// <returns>True if space for the rectangle could be allocated</returns> /// <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 rectangleWidth, int rectangleHeight, out Point placement
) { ) {
// If the rectangle is larger than the packing area in any dimension, // If the rectangle is larger than the packing area in any dimension,
// it will never fit! // it will never fit!
if( if(
(rectangleWidth > MaxPackingAreaWidth) || (rectangleHeight > MaxPackingAreaHeight) (rectangleWidth > PackingAreaWidth) || (rectangleHeight > PackingAreaHeight)
) { ) {
placement = Point.Zero; placement = Point.Zero;
return false; return false;
} }
// Do we have to start a new line ? // Do we have to start a new line ?
if(this.column + rectangleWidth > MaxPackingAreaWidth) { if(this.column + rectangleWidth > PackingAreaWidth) {
this.currentLine += this.lineHeight; this.currentLine += this.lineHeight;
this.lineHeight = 0; this.lineHeight = 0;
this.column = 0; this.column = 0;
} }
// If it doesn't fit vertically now, the packing area is considered full // 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; placement = Point.Zero;
return false; return false;
} }