diff --git a/OpenNest.Engine/Strategies/CircleFillStrategy.cs b/OpenNest.Engine/Strategies/CircleFillStrategy.cs new file mode 100644 index 00000000..94b98c2e --- /dev/null +++ b/OpenNest.Engine/Strategies/CircleFillStrategy.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using OpenNest.Engine.CirclePacking; +using OpenNest.Engine.Fill; +using OpenNest.Engine.Jobs; +using OpenNest.Engine.Jobs.Cutouts; +using OpenNest.Geometry; + +namespace OpenNest.Engine.Strategies; + +/// +/// Staggers round parts edge to edge with and , +/// which stretch a row's pitch to the sheet so the next row drops deeper than a fixed lattice does. +/// +public class CircleFillStrategy : IFillStrategy +{ + public string Name => "Circle"; + public NestPhase Phase => NestPhase.Custom; + public int Order => 170; + + public List Fill(FillContext context) + { + if (context.PartType != PartType.Circle) + return null; + + var item = Item.TryCreate(PartGeometrySnapshot.FromProgram(context.Item.Drawing.Program)); + if (item == null) + return null; + + // Half the gap on every disc, and the work area grown to match, so discs keep the spacing + // from each other and only their own radius from the work area's edges. + var gap = context.Plate.PartSpacing + CutoutCirclePacker.Margin; + var spaced = item.Clone(); + spaced.Radius += gap * 0.5; + var area = context.WorkArea.Offset(gap * 0.5); + + var comparer = context.Policy?.Comparer ?? new DefaultFillComparer(); + var odd = Pack(area, spaced, bin => new FillEndOdd(bin), item, context.Item.Drawing); + var even = Pack(area, spaced, bin => new FillEndEven(bin), item, context.Item.Drawing); + + return comparer.IsBetter(even, odd, context.WorkArea) ? even : odd; + } + + private static List Pack( + Box area, + Item spaced, + System.Func engine, + Item item, + Drawing drawing + ) + { + var bin = new Bin { Location = area.Location, Size = area.Size }; + engine(bin).Fill(spaced); + return bin.Items.Select(i => new Part(drawing, i.Center + item.PartOffset)).ToList(); + } +} diff --git a/OpenNest.Engine/Strategies/FillStrategyRegistry.cs b/OpenNest.Engine/Strategies/FillStrategyRegistry.cs index 30b5b4d6..a670eea2 100644 --- a/OpenNest.Engine/Strategies/FillStrategyRegistry.cs +++ b/OpenNest.Engine/Strategies/FillStrategyRegistry.cs @@ -17,6 +17,7 @@ namespace OpenNest.Engine.Strategies new PairsFillStrategy(), new RowFillStrategy(), new ColumnFillStrategy(), + new CircleFillStrategy(), new RectBestFitStrategy(), new ExtentsFillStrategy(), new LinearFillStrategy(), diff --git a/OpenNest.Tests/Strategies/CircleFillStrategyTests.cs b/OpenNest.Tests/Strategies/CircleFillStrategyTests.cs new file mode 100644 index 00000000..1c795e77 --- /dev/null +++ b/OpenNest.Tests/Strategies/CircleFillStrategyTests.cs @@ -0,0 +1,99 @@ +using System.Linq; +using System.Threading; +using OpenNest.CNC; +using OpenNest.Engine; +using OpenNest.Engine.Fill; +using OpenNest.Engine.Jobs.Placement.Fillers; +using OpenNest.Engine.Strategies; +using OpenNest.Geometry; +using OpenNest.Math; + +namespace OpenNest.Tests.Strategies; + +public class CircleFillStrategyTests +{ + private const double Radius = 4; + private const double Gap = 0.25; + + private static Drawing MakeCircleDrawing() + { + var pgm = new Program(); + var start = new Vector(Radius, 0); + pgm.Codes.Add(new RapidMove(start)); + pgm.Codes.Add(new ArcMove(start, new Vector(0, 0), RotationType.CCW)); + return new Drawing("circle", pgm); + } + + private static Plate MakePlate() + { + var plate = new Plate(60, 120) { PartSpacing = Gap }; + plate.EdgeSpacing = new Spacing(Gap, Gap, Gap, Gap); + return plate; + } + + [Fact] + public void Fill_EightInchCirclesOnASixtyByOneTwenty_BeatsTheStripeLayout() + { + var plate = MakePlate(); + var filler = new PlateFiller(plate); + var item = new NestItem { Drawing = MakeCircleDrawing() }; + + var parts = filler.Fill(item, plate.WorkArea(), null, CancellationToken.None); + + Assert.Equal(112, parts.Count); + Assert.Equal(NestPhase.Custom, filler.WinnerPhase); + AssertSpacedInside(parts, plate.WorkArea()); + } + + [Fact] + public void Fill_RoundPartOffItsOrigin_PlacesItAtTheFilledCenter() + { + var pgm = new Program(); + var start = new Vector(25, 10); + pgm.Codes.Add(new RapidMove(start)); + pgm.Codes.Add(new ArcMove(start, new Vector(21, 10), RotationType.CCW)); + var plate = MakePlate(); + var filler = new PlateFiller(plate); + + var parts = filler.Fill( + new NestItem { Drawing = new Drawing("offset", pgm) }, + plate.WorkArea(), + null, + CancellationToken.None + ); + + Assert.Equal(112, parts.Count); + var work = plate.WorkArea(); + Assert.All(parts, p => Assert.True(work.Contains(p.BoundingBox.Offset(-Tolerance.Epsilon)))); + } + + [Fact] + public void Strategy_SkipsPartsThatAreNotRound() + { + var plate = MakePlate(); + var context = new FillContext + { + Item = new NestItem { Drawing = TestHelpers.MakeRectDrawing(20, 10) }, + WorkArea = plate.WorkArea(), + Plate = plate, + PartType = PartType.Rectangle, + }; + + Assert.Null(new CircleFillStrategy().Fill(context)); + } + + private static void AssertSpacedInside(System.Collections.Generic.List parts, Box work) + { + var centers = parts.Select(p => p.BoundingBox.Center).ToList(); + for (var i = 0; i < centers.Count; i++) + { + Assert.True(centers[i].X - Radius >= work.Left - Tolerance.Epsilon); + Assert.True(centers[i].X + Radius <= work.Right + Tolerance.Epsilon); + Assert.True(centers[i].Y - Radius >= work.Bottom - Tolerance.Epsilon); + Assert.True(centers[i].Y + Radius <= work.Top + Tolerance.Epsilon); + + for (var j = i + 1; j < centers.Count; j++) + Assert.True(centers[i].DistanceTo(centers[j]) >= 2 * Radius + Gap - Tolerance.Epsilon); + } + } +} diff --git a/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs b/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs index 6f98c54e..2e7f52e7 100644 --- a/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs +++ b/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs @@ -9,11 +9,11 @@ public class FillStrategyRegistryTests public void Registry_ListsBuiltInStrategiesInPipelineOrder() { Assert.Equal( - ["Pairs", "Row", "Column", "RectBestFit", "Extents", "Linear", "TiltedRemnant"], + ["Pairs", "Row", "Column", "Circle", "RectBestFit", "Extents", "Linear", "TiltedRemnant"], FillStrategyRegistry.AllStrategies.Select(s => s.Name) ); Assert.Equal( - [100, 150, 160, 200, 300, 400, 500], + [100, 150, 160, 170, 200, 300, 400, 500], FillStrategyRegistry.AllStrategies.Select(s => s.Order) ); }