Integration of new rectangles into silhouette data structure seems to be working properly
git-svn-id: file:///srv/devel/repo-conversion/nusu@21 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
parent
62d54b5795
commit
5756ed94b3
|
@ -37,7 +37,7 @@ namespace Nuclex.Support.Packing {
|
||||||
public void TestSpaceEfficiency() {
|
public void TestSpaceEfficiency() {
|
||||||
float efficiency = calculateEfficiency(new CygonRectanglePacker(70, 70));
|
float efficiency = calculateEfficiency(new CygonRectanglePacker(70, 70));
|
||||||
|
|
||||||
Assert.GreaterOrEqual(efficiency, 0.75, "Packer achieves 75% efficiency");
|
Assert.GreaterOrEqual(efficiency, 0.75f, "Packer achieves 75% efficiency");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,13 @@ 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 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.
|
||||||
|
/// </remarks>
|
||||||
public class CygonRectanglePacker : RectanglePacker {
|
public class CygonRectanglePacker : RectanglePacker {
|
||||||
|
|
||||||
#region class SliceStartComparer
|
#region class SliceStartComparer
|
||||||
|
@ -68,22 +75,111 @@ namespace Nuclex.Support.Packing {
|
||||||
public override bool TryAllocate(
|
public override bool TryAllocate(
|
||||||
int rectangleWidth, int rectangleHeight, out Point placement
|
int rectangleWidth, int rectangleHeight, out Point placement
|
||||||
) {
|
) {
|
||||||
integrateRectangle(0, 1, 5);
|
int sliceIndex = findBestPosition(rectangleWidth, rectangleHeight);
|
||||||
integrateRectangle(20, 5, 30);
|
|
||||||
integrateRectangle(10, 10, 50);
|
|
||||||
integrateRectangle(10, 15, 25);
|
|
||||||
integrateRectangle(35, 20, 25);
|
|
||||||
integrateRectangle(40, 25, 15);
|
|
||||||
|
|
||||||
placement = Point.Zero;
|
// TODO: Rectangle might not even fit there!
|
||||||
return false;
|
if(sliceIndex == -1) {
|
||||||
|
|
||||||
|
placement = Point.Zero;
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
placement = this.heightSlices[sliceIndex];
|
||||||
|
|
||||||
|
integrateRectangle(
|
||||||
|
this.heightSlices[sliceIndex].X,
|
||||||
|
rectangleWidth,
|
||||||
|
this.heightSlices[sliceIndex].Y + rectangleHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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) {
|
||||||
|
int leftSliceIndex = 0;
|
||||||
|
int rightSliceIndex = this.heightSlices.BinarySearch(
|
||||||
|
new Point(rectangleWidth, 0), SliceStartComparer.Default
|
||||||
|
);
|
||||||
|
if(rightSliceIndex < 0)
|
||||||
|
rightSliceIndex = ~rightSliceIndex;
|
||||||
|
|
||||||
|
int leastWastedSliceIndex = -1;
|
||||||
|
int leastWastedArea = MaxPackingAreaWidth * MaxPackingAreaHeight;
|
||||||
|
|
||||||
|
while(rightSliceIndex <= this.heightSlices.Count) {
|
||||||
|
// final time (this is the special case where the rectangle is attempted
|
||||||
|
// to be placed at the rightmost end of the packing area)
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Determine the highest slice at this position. We cannot put the rectangle
|
||||||
|
// any lower than this without colliding into other rectangles
|
||||||
|
int highest = this.heightSlices[leftSliceIndex].Y;
|
||||||
|
for(int index = leftSliceIndex + 1; index < rightSliceIndex; ++index)
|
||||||
|
highest = Math.Max(highest, this.heightSlices[index].Y);
|
||||||
|
|
||||||
|
// Calculate the amount of space that would go to waste if the rectangle
|
||||||
|
// would be placed at this location
|
||||||
|
int wastedArea = 0;
|
||||||
|
for(int index = leftSliceIndex; index < rightSliceIndex - 1; ++index) {
|
||||||
|
int sliceWidth = this.heightSlices[index + 1].X - this.heightSlices[index].X;
|
||||||
|
wastedArea += (highest - this.heightSlices[index].Y) * sliceWidth;
|
||||||
|
}
|
||||||
|
wastedArea +=
|
||||||
|
(highest - this.heightSlices[rightSliceIndex - 1].Y) *
|
||||||
|
(
|
||||||
|
(this.heightSlices[leftSliceIndex].X + rectangleWidth) -
|
||||||
|
this.heightSlices[rightSliceIndex - 1].X
|
||||||
|
);
|
||||||
|
|
||||||
|
// If this beats the current record for the least wasted area, remember this as
|
||||||
|
// being the best position found so far
|
||||||
|
if(
|
||||||
|
(wastedArea < leastWastedArea) &&
|
||||||
|
(this.heightSlices[leftSliceIndex].Y + rectangleHeight < MaxPackingAreaHeight)
|
||||||
|
) {
|
||||||
|
leastWastedArea = wastedArea;
|
||||||
|
leastWastedSliceIndex = leftSliceIndex;
|
||||||
|
|
||||||
|
// No sense looking any further if we found the perfect place!
|
||||||
|
if(leastWastedArea == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// If this already was the loop after the final slice, terminate it now!
|
||||||
|
if(rightSliceIndex == this.heightSlices.Count)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Advance the starting slice to the next slice start
|
||||||
|
++leftSliceIndex;
|
||||||
|
int rightEnd = this.heightSlices[leftSliceIndex].X + rectangleWidth;
|
||||||
|
|
||||||
|
// Advance the ending slice to where the rectangle ends now
|
||||||
|
while(rightEnd > this.heightSlices[rightSliceIndex].X) {
|
||||||
|
++rightSliceIndex;
|
||||||
|
|
||||||
|
// If the end is reached, stop shifting and make the outer loop run one final time
|
||||||
|
if(rightSliceIndex == this.heightSlices.Count)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return leastWastedSliceIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Integrates a new rectangle into the height slice table</summary>
|
/// <summary>Integrates a new rectangle into the height slice table</summary>
|
||||||
/// <param name="left">Position of the rectangle's left side</param>
|
/// <param name="left">Position of the rectangle's left side</param>
|
||||||
/// <param name="bottom">Position of the rectangle's lower side</param>
|
|
||||||
/// <param name="width">Width of the rectangle</param>
|
/// <param name="width">Width of the rectangle</param>
|
||||||
private void integrateRectangle(int left, int bottom, int width) {
|
/// <param name="bottom">Position of the rectangle's lower side</param>
|
||||||
|
private void integrateRectangle(int left, int width, int bottom) {
|
||||||
|
|
||||||
// Find the first slice that is touched by the rectangle
|
// Find the first slice that is touched by the rectangle
|
||||||
int startSlice = this.heightSlices.BinarySearch(
|
int startSlice = this.heightSlices.BinarySearch(
|
||||||
|
@ -97,7 +193,6 @@ namespace Nuclex.Support.Packing {
|
||||||
// We scored a direct hit, so we can replace the slice we have hit
|
// We scored a direct hit, so we can replace the slice we have hit
|
||||||
firstSliceOriginalHeight = this.heightSlices[startSlice].Y;
|
firstSliceOriginalHeight = this.heightSlices[startSlice].Y;
|
||||||
this.heightSlices[startSlice] = new Point(left, bottom);
|
this.heightSlices[startSlice] = new Point(left, bottom);
|
||||||
++startSlice;
|
|
||||||
|
|
||||||
} else { // No direct hit, slice starts inside another slice
|
} else { // No direct hit, slice starts inside another slice
|
||||||
|
|
||||||
|
@ -108,58 +203,56 @@ namespace Nuclex.Support.Packing {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case, the rectangle was on the last slice, so we cannot
|
int right = left + width;
|
||||||
// use the start slice + 1 as start index for the binary search
|
++startSlice;
|
||||||
|
|
||||||
|
// Special case, the rectangle started on the last slice, so we cannot
|
||||||
|
// use the start slice + 1 for the binary search and the possibly already
|
||||||
|
// modified start slice height now only remains in our temporary
|
||||||
|
// firstSliceOriginalHeight variable
|
||||||
if(startSlice >= this.heightSlices.Count) {
|
if(startSlice >= this.heightSlices.Count) {
|
||||||
} else {
|
|
||||||
int right = left + width;
|
// If the slice ends within the last slice (usual case, unless it has the
|
||||||
|
// exact same with the packing area has), add another slice to return to the
|
||||||
|
// original height at the end of the rectangle.
|
||||||
|
if(right < MaxPackingAreaWidth)
|
||||||
|
this.heightSlices.Add(new Point(right, firstSliceOriginalHeight));
|
||||||
|
|
||||||
|
} else { // The rectangle doesn't start on the last slice
|
||||||
|
|
||||||
int endSlice = this.heightSlices.BinarySearch(
|
int endSlice = this.heightSlices.BinarySearch(
|
||||||
startSlice, this.heightSlices.Count - startSlice,
|
startSlice, this.heightSlices.Count - startSlice,
|
||||||
new Point(right, 0), SliceStartComparer.Default
|
new Point(right, 0), SliceStartComparer.Default
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
//this.heightSlices.RemoveRange(startSlice, endSlice - startSlice);
|
// Another direct hit on the final slice's end?
|
||||||
/*
|
if(endSlice > 0) {
|
||||||
int nextSlice = firstSlice + 1;
|
|
||||||
bool isLastSlice = (nextSlice >= this.heightSlices.Count);
|
|
||||||
|
|
||||||
*/
|
this.heightSlices.RemoveRange(startSlice, endSlice - startSlice);
|
||||||
/*
|
|
||||||
|
|
||||||
|
} else { // No direct hit, rectangle ends inside another slice
|
||||||
|
|
||||||
int nextSlice = firstSlice + 1;
|
endSlice = ~endSlice;
|
||||||
bool isLastSlice = (nextSlice >= this.heightSlices.Count);
|
|
||||||
|
|
||||||
//
|
// Find out to which height we need to return at the right end of
|
||||||
bool endsInFirstSlice;
|
// the rectangle
|
||||||
if(isLastSlice)
|
int returnHeight;
|
||||||
endsInFirstSlice = right < MaxPackingAreaWidth;
|
if(endSlice == startSlice)
|
||||||
else
|
returnHeight = firstSliceOriginalHeight;
|
||||||
endsInFirstSlice = right < this.heightSlices[nextSlice].X;
|
else
|
||||||
|
returnHeight = this.heightSlices[endSlice - 1].Y;
|
||||||
|
|
||||||
|
// Remove all slices covered by the rectangle and began a new slice at
|
||||||
|
// its end to return to the height the slice in which the rectangle ends
|
||||||
|
// has had.
|
||||||
|
this.heightSlices.RemoveRange(startSlice, endSlice - startSlice);
|
||||||
|
if(right < MaxPackingAreaWidth)
|
||||||
|
this.heightSlices.Insert(startSlice, new Point(right, returnHeight));
|
||||||
|
|
||||||
if(endsInFirstSlice) {
|
|
||||||
this.heightSlices.Insert(
|
|
||||||
nextSlice, new Point(right, this.heightSlices[firstSlice].Y)
|
|
||||||
);
|
|
||||||
this.heightSlices[firstSlice] = new Point(left, bottom);
|
|
||||||
return;
|
|
||||||
} else { // Integrated rect continues beyond the discovered slice
|
|
||||||
this.heightSlices[firstSlice] = new Point(left, bottom);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
firstSlice = ~firstSlice;
|
|
||||||
|
|
||||||
//firstSliceOriginalHeight = this.heightSlices[firstSlice].Y;
|
|
||||||
|
|
||||||
this.heightSlices.Insert(firstSlice, new Point(left, bottom));
|
|
||||||
++firstSlice;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Stores the height silhouette of the rectangles</summary>
|
/// <summary>Stores the height silhouette of the rectangles</summary>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user