mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-05 06:51:34 -05:00
feat(circle-fill): end staggered rows with straight rows when that fits more
The Circle fill strategy's stripe layouts (FillEndOdd, FillEndEven) now try switching from staggered to straight stripes, a diameter apart over the last staggered stripe, at every stripe, all staggered and all straight included, and keep the switch that places the most; a tie stays staggered. A 9" circle on a 60x120 sheet at 0.2 spacing now fills 89 (five staggered rows, two straight) instead of 88. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8700ae187a
commit
dffb06a9e3
6 files changed
+190
-46
No files matched your search
@@ -72,8 +72,10 @@ public class FillEndOverlapTests
|
||||
|
||||
new FillEndOdd(bin).Fill(new Item { Radius = 4.125 });
|
||||
|
||||
// Staggered all the way places 108; the last stripes stacked straight fit one more.
|
||||
AssertNoOverlaps(bin.Items, 8.25);
|
||||
Assert.Equal(108, bin.Items.Count);
|
||||
AssertInside(bin);
|
||||
Assert.Equal(109, bin.Items.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -87,6 +89,42 @@ public class FillEndOverlapTests
|
||||
Assert.Equal(112, bin.Items.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillEndOdd_EndsWithStraightStripesWhenTheyFitMore()
|
||||
{
|
||||
// 13 discs to a stripe: seven staggered stripes place 88 and six straight ones 78, but
|
||||
// five staggered and two straight over the fifth place 89.
|
||||
var bin = new Bin { Location = new Vector(0, 0), Size = new Size(59.8, 119.8) };
|
||||
|
||||
new FillEndOdd(bin).Fill(new Item { Radius = 4.6 });
|
||||
|
||||
AssertNoOverlaps(bin.Items, 9.2);
|
||||
AssertInside(bin);
|
||||
Assert.Equal(89, bin.Items.Count);
|
||||
var stripes = bin.Items.GroupBy(i => System.Math.Round(i.Center.Y, 6)).OrderBy(g => g.Key).ToList();
|
||||
Assert.Equal(new[] { 13, 12, 13, 12, 13, 13, 13 }, stripes.Select(s => s.Count()));
|
||||
var fifth = stripes[4].Select(i => i.Center.X).Order().ToList();
|
||||
for (var stripe = 5; stripe < stripes.Count; stripe++)
|
||||
{
|
||||
Assert.Equal(9.2, stripes[stripe].Key - stripes[stripe - 1].Key, 6);
|
||||
Assert.Equal(fifth, stripes[stripe].Select(i => i.Center.X).Order());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillEndOdd_KeepsStaggeredStripesWhenStraightOnesFitNoMore()
|
||||
{
|
||||
// Seven staggered stripes place 88; too short for the mixed 89, straight stripes from
|
||||
// any switch point place at most 78.
|
||||
var bin = new Bin { Location = new Vector(0, 0), Size = new Size(57.2, 119.8) };
|
||||
|
||||
new FillEndOdd(bin).Fill(new Item { Radius = 4.6 });
|
||||
|
||||
Assert.Equal(88, bin.Items.Count);
|
||||
var stripes = bin.Items.GroupBy(i => System.Math.Round(i.Center.Y, 6)).Select(g => g.Count());
|
||||
Assert.Equal(new[] { 13, 12, 13, 12, 13, 12, 13 }, stripes);
|
||||
}
|
||||
|
||||
private static Item CreateItem() => new Item { Radius = Diameter * 0.5, Center = new Vector(Diameter * 0.5, Diameter * 0.5) };
|
||||
|
||||
private static void AssertInside(Bin bin)
|
||||
|
||||
@@ -68,28 +68,15 @@ namespace OpenNest.Engine.CirclePacking
|
||||
outerOffset = System.Math.Max(Trigonometry.Base(oddShift, diameter), radius);
|
||||
}
|
||||
|
||||
var outerStart = (horizontal ? bin.X : bin.Y) + radius;
|
||||
var outerMax = horizontal ? max.X : max.Y;
|
||||
var innerStart = (horizontal ? bin.Y : bin.X) + radius;
|
||||
var innerMax = horizontal ? max.Y : max.X;
|
||||
|
||||
var stripe = 0;
|
||||
|
||||
for (var outer = outerStart; outer <= outerMax; outer += outerOffset)
|
||||
{
|
||||
var inner = stripe.IsOdd() ? innerStart + oddShift : innerStart;
|
||||
|
||||
for (; inner <= innerMax; inner += innerOffset)
|
||||
{
|
||||
var addedItem = item.Clone();
|
||||
addedItem.Center = horizontal
|
||||
? new Vector(outer, inner)
|
||||
: new Vector(inner, outer);
|
||||
bin.Items.Add(addedItem);
|
||||
}
|
||||
|
||||
stripe++;
|
||||
}
|
||||
AddStripes(bin, item, new Stripes(
|
||||
InnerAlongX: !horizontal,
|
||||
OuterStart: (horizontal ? bin.X : bin.Y) + radius,
|
||||
OuterMax: horizontal ? max.X : max.Y,
|
||||
OuterStep: outerOffset,
|
||||
InnerStart: (horizontal ? bin.Y : bin.X) + radius,
|
||||
InnerMax: horizontal ? max.Y : max.X,
|
||||
InnerStep: innerOffset,
|
||||
Shift: oddShift));
|
||||
|
||||
return bin;
|
||||
}
|
||||
|
||||
@@ -56,28 +56,15 @@ namespace OpenNest.Engine.CirclePacking
|
||||
? System.Math.Max(Trigonometry.Height(primaryOffset * 0.5, item.Diameter), item.Diameter * 0.5)
|
||||
: item.Diameter;
|
||||
|
||||
var outerStart = (horizontal ? bin.Y : bin.X) + item.Radius;
|
||||
var outerMax = horizontal ? max.Y : max.X;
|
||||
var innerStart = (horizontal ? bin.X : bin.Y) + item.Radius;
|
||||
var innerMax = horizontal ? max.X : max.Y;
|
||||
|
||||
var stripe = 0;
|
||||
|
||||
for (var outer = outerStart; outer <= outerMax; outer += secondaryOffset)
|
||||
{
|
||||
var inner = count > 1 && stripe.IsOdd() ? innerStart + primaryOffset * 0.5 : innerStart;
|
||||
|
||||
for (; inner <= innerMax; inner += primaryOffset)
|
||||
{
|
||||
var addedItem = item.Clone();
|
||||
addedItem.Center = horizontal
|
||||
? new Vector(inner, outer)
|
||||
: new Vector(outer, inner);
|
||||
bin.Items.Add(addedItem);
|
||||
}
|
||||
|
||||
stripe++;
|
||||
}
|
||||
AddStripes(bin, item, new Stripes(
|
||||
InnerAlongX: horizontal,
|
||||
OuterStart: (horizontal ? bin.Y : bin.X) + item.Radius,
|
||||
OuterMax: horizontal ? max.Y : max.X,
|
||||
OuterStep: secondaryOffset,
|
||||
InnerStart: (horizontal ? bin.X : bin.Y) + item.Radius,
|
||||
InnerMax: horizontal ? max.X : max.Y,
|
||||
InnerStep: primaryOffset,
|
||||
Shift: count > 1 ? primaryOffset * 0.5 : 0));
|
||||
|
||||
return bin;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
namespace OpenNest.Engine.CirclePacking
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.CirclePacking
|
||||
{
|
||||
internal abstract class FillEngine
|
||||
{
|
||||
@@ -12,5 +16,91 @@
|
||||
public abstract void Fill(Item item);
|
||||
|
||||
public abstract void Fill(Item item, int maxCount);
|
||||
|
||||
/// <summary>
|
||||
/// Lays staggered stripes <see cref="Stripes.OuterStep"/> apart into <paramref name="bin"/>,
|
||||
/// odd stripes shifted along their length by <see cref="Stripes.Shift"/>. The last stripes
|
||||
/// may instead stack straight: a diameter apart, each repeating the last staggered stripe's
|
||||
/// positions. Straight stacking climbs faster per stripe than staggering, so where the
|
||||
/// sheet leaves too little for one more staggered stripe, a few straight ones can fit more.
|
||||
/// Every switch point is tried, all staggered and all straight included, and the one placing
|
||||
/// the most is kept; a tie keeps the stripes staggered.
|
||||
/// </summary>
|
||||
protected static void AddStripes(Bin bin, Item item, Stripes stripes)
|
||||
{
|
||||
var staggered = new List<double>();
|
||||
for (var outer = stripes.OuterStart; outer <= stripes.OuterMax; outer += stripes.OuterStep)
|
||||
staggered.Add(outer);
|
||||
|
||||
var counts = new int[staggered.Count];
|
||||
var total = 0;
|
||||
for (var stripe = 0; stripe < counts.Length; stripe++)
|
||||
{
|
||||
for (var inner = InnerStart(stripes, stripe); inner <= stripes.InnerMax; inner += stripes.InnerStep)
|
||||
counts[stripe]++;
|
||||
total += counts[stripe];
|
||||
}
|
||||
|
||||
var switchAt = staggered.Count;
|
||||
var straightCount = 0;
|
||||
var best = total;
|
||||
var placed = 0;
|
||||
|
||||
for (var stripe = 0; stripe < counts.Length; stripe++)
|
||||
{
|
||||
placed += counts[stripe];
|
||||
var straight = 0;
|
||||
for (var outer = staggered[stripe] + item.Diameter; outer <= stripes.OuterMax; outer += item.Diameter)
|
||||
straight++;
|
||||
|
||||
if (placed + straight * counts[stripe] > best)
|
||||
{
|
||||
best = placed + straight * counts[stripe];
|
||||
switchAt = stripe + 1;
|
||||
straightCount = straight;
|
||||
}
|
||||
}
|
||||
|
||||
for (var stripe = 0; stripe < switchAt; stripe++)
|
||||
AddStripe(bin, item, stripes, staggered[stripe], InnerStart(stripes, stripe));
|
||||
|
||||
var last = switchAt - 1;
|
||||
var straightOuter = last >= 0 ? staggered[last] : 0;
|
||||
for (var stripe = 0; stripe < straightCount; stripe++)
|
||||
{
|
||||
straightOuter += item.Diameter;
|
||||
AddStripe(bin, item, stripes, straightOuter, InnerStart(stripes, last));
|
||||
}
|
||||
}
|
||||
|
||||
private static double InnerStart(Stripes stripes, int stripe) =>
|
||||
stripe.IsOdd() ? stripes.InnerStart + stripes.Shift : stripes.InnerStart;
|
||||
|
||||
private static void AddStripe(Bin bin, Item item, Stripes stripes, double outer, double innerStart)
|
||||
{
|
||||
for (var inner = innerStart; inner <= stripes.InnerMax; inner += stripes.InnerStep)
|
||||
{
|
||||
var addedItem = item.Clone();
|
||||
addedItem.Center = stripes.InnerAlongX
|
||||
? new Vector(inner, outer)
|
||||
: new Vector(outer, inner);
|
||||
bin.Items.Add(addedItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stripes run along the inner axis, X when <see cref="InnerAlongX"/>, and step along the
|
||||
/// other; starts and maxima are circle centers.
|
||||
/// </summary>
|
||||
protected readonly record struct Stripes(
|
||||
bool InnerAlongX,
|
||||
double OuterStart,
|
||||
double OuterMax,
|
||||
double OuterStep,
|
||||
double InnerStart,
|
||||
double InnerMax,
|
||||
double InnerStep,
|
||||
double Shift
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,40 @@ public class CircleFillStrategyTests
|
||||
AssertSpacedInside(parts, plate.WorkArea());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_NineInchCirclesOnASixtyByOneTwenty_EndsWithStraightRows()
|
||||
{
|
||||
// Seven staggered rows of 13 and 12 place 88; five staggered rows and two straight rows
|
||||
// of 13 over the fifth place 89.
|
||||
var pgm = new Program();
|
||||
var start = new Vector(4.5, 0);
|
||||
pgm.Codes.Add(new RapidMove(start));
|
||||
pgm.Codes.Add(new ArcMove(start, new Vector(0, 0), RotationType.CCW));
|
||||
var plate = new Plate(60, 120) { PartSpacing = 0.2 };
|
||||
plate.EdgeSpacing = new Spacing(0.2, 0.2, 0.2, 0.2);
|
||||
var filler = new PlateFiller(plate);
|
||||
|
||||
var parts = filler.Fill(
|
||||
new NestItem { Drawing = new Drawing("nine", pgm) },
|
||||
plate.WorkArea(),
|
||||
null,
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.Equal(89, parts.Count);
|
||||
Assert.Equal(NestPhase.Custom, filler.WinnerPhase);
|
||||
var rows = parts.GroupBy(p => System.Math.Round(p.BoundingBox.Center.Y, 6)).OrderBy(g => g.Key);
|
||||
Assert.Equal(new[] { 13, 12, 13, 12, 13, 13, 13 }, rows.Select(r => r.Count()));
|
||||
var work = plate.WorkArea();
|
||||
Assert.All(parts, p => Assert.True(work.Contains(p.BoundingBox.Offset(-Tolerance.Epsilon))));
|
||||
var centers = parts.Select(p => p.BoundingBox.Center).ToList();
|
||||
for (var i = 0; i < centers.Count; i++)
|
||||
{
|
||||
for (var j = i + 1; j < centers.Count; j++)
|
||||
Assert.True(centers[i].DistanceTo(centers[j]) >= 9.2 - Tolerance.Epsilon);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_RoundPartOffItsOrigin_PlacesItAtTheFilledCenter()
|
||||
{
|
||||
|
||||
@@ -107,6 +107,14 @@ detail reads "Computing best-fit pairs (may take a minute)...". Accepting or sto
|
||||
waiting for the pair computation, which finishes in the background and is cached for the next
|
||||
fill.
|
||||
|
||||
For round parts, Fill's Circle strategy staggers rows edge to edge, stretching each row's pitch
|
||||
to the sheet. Straight rows, a diameter apart over the last staggered row, climb further per row
|
||||
but fit more where the sheet leaves too little room for another staggered row, so it tries
|
||||
switching from staggered to straight rows at every row, all staggered and all straight
|
||||
included, and keeps whichever places the most; a tie stays staggered. A 9-inch circle on a
|
||||
60 x 120 sheet at 0.2 spacing fits 89 that way (five staggered rows of 13 and 12, then two
|
||||
straight rows of 13) against 88 staggered throughout.
|
||||
|
||||
Fill's last strategy, TiltedRemnant, adds tilted parts beside the best layout when the room
|
||||
left past it on the right or top is too narrow for the part at 0 or 90 degrees, such as a long
|
||||
part beside a grid of the same part turned 90 degrees. It moves the layout flush to the low end
|
||||
|
||||
Reference in new issue
Block a user