mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 13:41:37 -05:00
feat(engine): stagger round parts in plate fills
FillEndOdd and FillEndEven had no production callers, so round parts only got stripe layouts. A Circle strategy now runs them in the fill pipeline: 8in circles on a 60x120 go from 98 to 112. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ec8fd6f8ed
commit
8cc8d04b64
4 files changed
+158
-2
No files matched your search
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Staggers round parts edge to edge with <see cref="FillEndOdd"/> and <see cref="FillEndEven"/>,
|
||||
/// which stretch a row's pitch to the sheet so the next row drops deeper than a fixed lattice does.
|
||||
/// </summary>
|
||||
public class CircleFillStrategy : IFillStrategy
|
||||
{
|
||||
public string Name => "Circle";
|
||||
public NestPhase Phase => NestPhase.Custom;
|
||||
public int Order => 170;
|
||||
|
||||
public List<Part> 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<Part> Pack(
|
||||
Box area,
|
||||
Item spaced,
|
||||
System.Func<Bin, FillEngine> 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();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ namespace OpenNest.Engine.Strategies
|
||||
new PairsFillStrategy(),
|
||||
new RowFillStrategy(),
|
||||
new ColumnFillStrategy(),
|
||||
new CircleFillStrategy(),
|
||||
new RectBestFitStrategy(),
|
||||
new ExtentsFillStrategy(),
|
||||
new LinearFillStrategy(),
|
||||
|
||||
@@ -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<Part> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user