diff --git a/Nuclex.Support (PC).csproj b/Nuclex.Support (PC).csproj index c9181b2..6232ffb 100644 --- a/Nuclex.Support (PC).csproj +++ b/Nuclex.Support (PC).csproj @@ -150,6 +150,15 @@ RectanglePacker.Test RectanglePacker.cs + + false + SimpleRectanglePacker + + + false + SimpleRectanglePacker.Test + SimpleRectanglePacker.cs + false BinarySerializer.Test diff --git a/Source/Packing/ArevaloRectanglePacker.cs b/Source/Packing/ArevaloRectanglePacker.cs index 5529f39..0f2adcc 100644 --- a/Source/Packing/ArevaloRectanglePacker.cs +++ b/Source/Packing/ArevaloRectanglePacker.cs @@ -86,9 +86,8 @@ namespace Nuclex.Support.Packing { /// Right anchor point that will be compared /// The relation of the two anchor point's ranks to each other public int Compare(Point left, Point right) { - return 0; //return Math.Min(left.X, left.Y) - Math.Min(right.X, right.Y); - //return (left.X + left.Y) - (right.X + right.Y); + return (left.X + left.Y) - (right.X + right.Y); } } diff --git a/Source/Packing/CygonRectanglePacker.Test.cs b/Source/Packing/CygonRectanglePacker.Test.cs index bcea2ec..a89c710 100644 --- a/Source/Packing/CygonRectanglePacker.Test.cs +++ b/Source/Packing/CygonRectanglePacker.Test.cs @@ -28,7 +28,7 @@ using NUnit.Framework; namespace Nuclex.Support.Packing { - /// Unit test for the arevalo rectangle packer class + /// Unit test for the cygon rectangle packer class [TestFixture] public class CygonRectanglePackerTest : RectanglePackerTest { diff --git a/Source/Packing/CygonRectanglePacker.cs b/Source/Packing/CygonRectanglePacker.cs index ffaf234..d70370a 100644 --- a/Source/Packing/CygonRectanglePacker.cs +++ b/Source/Packing/CygonRectanglePacker.cs @@ -24,20 +24,41 @@ using Microsoft.Xna.Framework; namespace Nuclex.Support.Packing { - /// Simplified packer for rectangles which don't vary greatly in size - /// - /// This is a highly performant packer that sacrifices space efficiency for - /// low memory usage and runtime performance. It achieves good results with - /// near-uniform sized rectangles but will waste lots of space with rectangles - /// of varying dimensions. - /// + /// Packer using a custom algorithm by Markus Ewald public class CygonRectanglePacker : RectanglePacker { + #region class SliceStartComparer + + /// Compares the starting position of height slices + private class SliceStartComparer : IComparer { + + /// Provides a default instance for the anchor rank comparer + public static SliceStartComparer Default = new SliceStartComparer(); + + /// Compares the starting position of two height slices + /// Left slice start that will be compared + /// Right slice start that will be compared + /// The relation of the two slice starts ranks to each other + public int Compare(Point left, Point right) { + return left.X - right.X; + } + + } + + #endregion + /// Initializes a new rectangle packer /// Maximum width of the packing area /// Maximum height of the packing area public CygonRectanglePacker(int maxPackingAreaWidth, int maxPackingAreaHeight) - : base(maxPackingAreaWidth, maxPackingAreaHeight) { } + : base(maxPackingAreaWidth, maxPackingAreaHeight) { + + this.heightSlices = new List(); + + // At the beginning, the packing area is a single slice of height 0 + this.heightSlices.Add(new Point(0, 0)); + + } /// Tries to allocate space for a rectangle in the packing area /// Width of the rectangle to allocate @@ -47,45 +68,102 @@ namespace Nuclex.Support.Packing { public override bool TryAllocate( int rectangleWidth, int rectangleHeight, out Point placement ) { + integrateRectangle(0, 1, 5); + integrateRectangle(20, 5, 30); + integrateRectangle(10, 10, 50); + integrateRectangle(10, 15, 25); + integrateRectangle(35, 20, 25); + integrateRectangle(40, 25, 15); - // If the rectangle is larger than the packing area in any dimension, - // it will never fit! - if( - (rectangleWidth > MaxPackingAreaWidth) || (rectangleHeight > MaxPackingAreaHeight) - ) { - placement = Point.Zero; - return false; - } - - // Do we have to start a new line ? - if(this.column + rectangleWidth > MaxPackingAreaWidth) { - 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) { - placement = Point.Zero; - return false; - } - - // The rectangle appears to fit at the current location - placement = new Point(this.column, this.currentLine); - - this.column += rectangleWidth; // Can be larger than cache width till next run - if(rectangleHeight > this.lineHeight) - this.lineHeight = rectangleHeight; - - return true; + placement = Point.Zero; + return false; } - /// Current packing line - private int currentLine; - /// Height of the current packing line - private int lineHeight; - /// Current column in the current packing line - private int column; + /// Integrates a new rectangle into the height slice table + /// Position of the rectangle's left side + /// Position of the rectangle's lower side + /// Width of the rectangle + private void integrateRectangle(int left, int bottom, int width) { + + // Find the first slice that is touched by the rectangle + int startSlice = this.heightSlices.BinarySearch( + new Point(left, 0), SliceStartComparer.Default + ); + int firstSliceOriginalHeight; + + // Did we score a direct hit on an existing slice start? + if(startSlice >= 0) { + + // We scored a direct hit, so we can replace the slice we have hit + firstSliceOriginalHeight = this.heightSlices[startSlice].Y; + this.heightSlices[startSlice] = new Point(left, bottom); + ++startSlice; + + } else { // No direct hit, slice starts inside another slice + + // Add a new slice after the slice in which we start + startSlice = ~startSlice; + firstSliceOriginalHeight = this.heightSlices[startSlice - 1].Y; + this.heightSlices.Insert(startSlice, new Point(left, bottom)); + + } + + // Special case, the rectangle was on the last slice, so we cannot + // use the start slice + 1 as start index for the binary search + if(startSlice >= this.heightSlices.Count) { + } else { + int right = left + width; + + int endSlice = this.heightSlices.BinarySearch( + startSlice, this.heightSlices.Count - startSlice, + new Point(right, 0), SliceStartComparer.Default + ); + } + + //this.heightSlices.RemoveRange(startSlice, endSlice - startSlice); +/* + int nextSlice = firstSlice + 1; + bool isLastSlice = (nextSlice >= this.heightSlices.Count); + +*/ +/* + + + int nextSlice = firstSlice + 1; + bool isLastSlice = (nextSlice >= this.heightSlices.Count); + + // + bool endsInFirstSlice; + if(isLastSlice) + endsInFirstSlice = right < MaxPackingAreaWidth; + else + endsInFirstSlice = right < this.heightSlices[nextSlice].X; + + 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; + + } +*/ + } + + /// Stores the height silhouette of the rectangles + private List heightSlices; } diff --git a/Source/Packing/SimpleRectanglePacker.Test.cs b/Source/Packing/SimpleRectanglePacker.Test.cs new file mode 100644 index 0000000..0a2de4a --- /dev/null +++ b/Source/Packing/SimpleRectanglePacker.Test.cs @@ -0,0 +1,47 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2007 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion +using System; +using System.Collections.Generic; + +using Microsoft.Xna.Framework; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Packing { + + /// Unit test for the simple rectangle packer class + [TestFixture] + public class SimpleRectanglePackerTest : RectanglePackerTest { + + /// Tests the packer's efficiency using a deterministic benchmark + [Test] + public void TestSpaceEfficiency() { + float efficiency = calculateEfficiency(new SimpleRectanglePacker(70, 70)); + + Assert.GreaterOrEqual(efficiency, 0.75, "Packer achieves 75% efficiency"); + } + + } + +} // namespace Nuclex.Support.Packing + +#endif // UNITTEST diff --git a/Source/Packing/SimpleRectanglePacker.cs b/Source/Packing/SimpleRectanglePacker.cs new file mode 100644 index 0000000..5e98aa6 --- /dev/null +++ b/Source/Packing/SimpleRectanglePacker.cs @@ -0,0 +1,92 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2007 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion +using System; +using System.Collections.Generic; + +using Microsoft.Xna.Framework; + +namespace Nuclex.Support.Packing { + + /// Simplified packer for rectangles which don't vary greatly in size + /// + /// This is a highly performant packer that sacrifices space efficiency for + /// low memory usage and runtime performance. It achieves good results with + /// near-uniform sized rectangles but will waste lots of space with rectangles + /// of varying dimensions. + /// + public class SimpleRectanglePacker : RectanglePacker { + + /// Initializes a new rectangle packer + /// Maximum width of the packing area + /// Maximum height of the packing area + public SimpleRectanglePacker(int maxPackingAreaWidth, int maxPackingAreaHeight) + : base(maxPackingAreaWidth, maxPackingAreaHeight) { } + + /// Tries to allocate space for a rectangle in the packing area + /// Width of the rectangle to allocate + /// Height of the rectangle to allocate + /// Output parameter receiving the rectangle's placement + /// True if space for the rectangle could be allocated + public override bool TryAllocate( + 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) + ) { + placement = Point.Zero; + return false; + } + + // Do we have to start a new line ? + if(this.column + rectangleWidth > MaxPackingAreaWidth) { + 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) { + placement = Point.Zero; + return false; + } + + // The rectangle appears to fit at the current location + placement = new Point(this.column, this.currentLine); + + this.column += rectangleWidth; // Can be larger than cache width till next run + if(rectangleHeight > this.lineHeight) + this.lineHeight = rectangleHeight; + + return true; + } + + /// Current packing line + private int currentLine; + /// Height of the current packing line + private int lineHeight; + /// Current column in the current packing line + private int column; + + } + +} // namespace Nuclex.Support.Packing