diff --git a/OpenNest.Engine.Tests/CirclePacking/FillEndOverlapTests.cs b/OpenNest.Engine.Tests/CirclePacking/FillEndOverlapTests.cs new file mode 100644 index 00000000..2d03aa9b --- /dev/null +++ b/OpenNest.Engine.Tests/CirclePacking/FillEndOverlapTests.cs @@ -0,0 +1,69 @@ +using OpenNest.Engine.CirclePacking; +using OpenNest.Geometry; + +namespace OpenNest.Engine.Tests.CirclePacking; + +public class FillEndOverlapTests +{ + private const double Diameter = 10; + + public static IEnumerable BinSizes() + { + for (var width = 1.0; width <= 6.0; width += 0.05) + { + foreach (var length in new[] { 1.0, 1.5, 1.95, 2.5, 2.9, 3.7, 6.0 }) + yield return new object[] { width * Diameter, length * Diameter }; + } + } + + [Theory] + [MemberData(nameof(BinSizes))] + public void FillEndOdd_PlacesNoOverlappingCircles(double width, double length) + { + var bin = new Bin { Location = new Vector(0, 0), Size = new Size(width, length) }; + + new FillEndOdd(bin).Fill(CreateItem()); + + Assert.NotEmpty(bin.Items); + AssertNoOverlaps(bin.Items); + } + + [Theory] + [MemberData(nameof(BinSizes))] + public void FillEndEven_PlacesNoOverlappingCircles(double width, double length) + { + var bin = new Bin { Location = new Vector(0, 0), Size = new Size(width, length) }; + + new FillEndEven(bin).Fill(CreateItem()); + + Assert.NotEmpty(bin.Items); + Assert.All(bin.Items, i => Assert.Equal(Diameter, i.Diameter, 9)); + AssertNoOverlaps(bin.Items); + } + + [Fact] + public void FillEndOdd_SingleCircleWidth_StacksAlongTheOtherAxis() + { + var bin = new Bin { Location = new Vector(0, 0), Size = new Size(1.5 * Diameter, 1.5 * Diameter) }; + + new FillEndOdd(bin).Fill(CreateItem()); + + AssertNoOverlaps(bin.Items); + } + + private static Item CreateItem() => new Item { Radius = Diameter * 0.5, Center = new Vector(Diameter * 0.5, Diameter * 0.5) }; + + private static void AssertNoOverlaps(IReadOnlyList items) + { + for (var i = 0; i < items.Count; i++) + { + for (var j = i + 1; j < items.Count; j++) + { + var distance = items[i].Center.DistanceTo(items[j].Center); + Assert.True( + distance >= Diameter - 1e-6, + $"Circles {i} at {items[i].Center} and {j} at {items[j].Center} are {distance:F4} apart (< {Diameter})."); + } + } + } +} diff --git a/OpenNest.Engine/CirclePacking/FillEndEven.cs b/OpenNest.Engine/CirclePacking/FillEndEven.cs index 3f566a80..b1649600 100644 --- a/OpenNest.Engine/CirclePacking/FillEndEven.cs +++ b/OpenNest.Engine/CirclePacking/FillEndEven.cs @@ -1,4 +1,4 @@ -using System; +using System; using OpenNest.Geometry; using OpenNest.Math; @@ -18,6 +18,9 @@ namespace OpenNest.Engine.CirclePacking var rows = System.Math.Floor((Bin.Length + Tolerance.Epsilon) / (item.Diameter)); + if (rows == 0) + return; + var diameter = item.Diameter; var remaining = Bin.Length - diameter * rows; var radius = diameter * 0.5; @@ -35,7 +38,9 @@ namespace OpenNest.Engine.CirclePacking for (; y <= max.Y; y += yoffset) { - Bin.Items.Add(new Item { Center = new Vector(x, y) }); + var addedItem = item.Clone(); + addedItem.Center = new Vector(x, y); + Bin.Items.Add(addedItem); } column++; @@ -44,7 +49,9 @@ namespace OpenNest.Engine.CirclePacking else { var yoffset = (Bin.Length - diameter) / (2 * rows - 1); - var xoffset = Trigonometry.Base(yoffset, diameter); + // Adjacent columns touch diagonally, but columns two apart sit directly beside + // each other, so the column spacing can never drop below a radius. + var xoffset = System.Math.Max(Trigonometry.Base(yoffset, diameter), radius); var yodd = Bin.Y + yoffset; @@ -58,7 +65,9 @@ namespace OpenNest.Engine.CirclePacking for (; y <= max.Y; y += yoffset) { - Bin.Items.Add(new Item { Center = new Vector(x, y) }); + var addedItem = item.Clone(); + addedItem.Center = new Vector(x, y); + Bin.Items.Add(addedItem); } column++; diff --git a/OpenNest.Engine/CirclePacking/FillEndOdd.cs b/OpenNest.Engine/CirclePacking/FillEndOdd.cs index cae9afac..7fe55070 100644 --- a/OpenNest.Engine/CirclePacking/FillEndOdd.cs +++ b/OpenNest.Engine/CirclePacking/FillEndOdd.cs @@ -44,10 +44,16 @@ namespace OpenNest.Engine.CirclePacking if (count == 0) return bin; - var primaryOffset = (primarySize - item.Diameter) / (count - 1); - var secondaryOffset = horizontal - ? Trigonometry.Height(primaryOffset * 0.5, item.Diameter) - : Trigonometry.Base(primaryOffset * 0.5, item.Diameter); + // A single circle per stripe cannot stagger; stack the stripes a diameter apart. + var primaryOffset = count > 1 + ? (primarySize - item.Diameter) / (count - 1) + : double.PositiveInfinity; + + // Adjacent stripes touch diagonally, but stripes two apart sit directly over each + // other, so the stripe spacing can never drop below a radius. + var secondaryOffset = count > 1 + ? System.Math.Max(Trigonometry.Height(primaryOffset * 0.5, item.Diameter), item.Diameter * 0.5) + : item.Diameter; var outerStart = horizontal ? bin.Y : bin.X; var outerMax = horizontal ? max.Y : max.X; @@ -58,7 +64,7 @@ namespace OpenNest.Engine.CirclePacking for (var outer = outerStart; outer <= outerMax; outer += secondaryOffset) { - var inner = stripe.IsOdd() ? innerStart + primaryOffset * 0.5 : innerStart; + var inner = count > 1 && stripe.IsOdd() ? innerStart + primaryOffset * 0.5 : innerStart; for (; inner <= innerMax; inner += primaryOffset) {