Compare commits
2 Commits
560105f952
...
93a8981d0a
| Author | SHA1 | Date | |
|---|---|---|---|
| 93a8981d0a | |||
| 00e7866506 |
@@ -28,14 +28,16 @@ namespace OpenNest.Engine.Fill
|
||||
private const int EarlyExitMinTried = 10;
|
||||
private const int EarlyExitStaleLimit = 10;
|
||||
|
||||
private readonly Plate plate;
|
||||
private readonly Size plateSize;
|
||||
private readonly double partSpacing;
|
||||
private readonly IFillComparer comparer;
|
||||
|
||||
public PairFiller(Size plateSize, double partSpacing, IFillComparer comparer = null)
|
||||
public PairFiller(Plate plate, IFillComparer comparer = null)
|
||||
{
|
||||
this.plateSize = plateSize;
|
||||
this.partSpacing = partSpacing;
|
||||
this.plate = plate;
|
||||
this.plateSize = plate.Size;
|
||||
this.partSpacing = plate.PartSpacing;
|
||||
this.comparer = comparer ?? new DefaultFillComparer();
|
||||
}
|
||||
|
||||
@@ -73,7 +75,7 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var filled = EvaluateCandidate(candidates[i], drawing, effectiveWorkArea);
|
||||
var filled = EvaluateCandidate(candidates[i], drawing, effectiveWorkArea, token);
|
||||
|
||||
if (comparer.IsBetter(filled, best, effectiveWorkArea))
|
||||
{
|
||||
@@ -142,12 +144,141 @@ namespace OpenNest.Engine.Fill
|
||||
System.Math.Min(newTop - workArea.Y, workArea.Length));
|
||||
}
|
||||
|
||||
private List<Part> EvaluateCandidate(BestFitResult candidate, Drawing drawing, Box workArea)
|
||||
private List<Part> EvaluateCandidate(BestFitResult candidate, Drawing drawing,
|
||||
Box workArea, CancellationToken token)
|
||||
{
|
||||
var pairParts = candidate.BuildParts(drawing);
|
||||
var engine = new FillLinear(workArea, partSpacing);
|
||||
var angles = BuildTilingAngles(candidate);
|
||||
return FillHelpers.FillPattern(engine, pairParts, angles, workArea, comparer);
|
||||
|
||||
// Phase 1: evaluate all grids (fast)
|
||||
var grids = new List<(List<Part> Parts, NestDirection Dir)>();
|
||||
foreach (var angle in angles)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var pattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
||||
if (pattern.Parts.Count == 0)
|
||||
continue;
|
||||
|
||||
var engine = new FillLinear(workArea, partSpacing);
|
||||
foreach (var dir in new[] { NestDirection.Horizontal, NestDirection.Vertical })
|
||||
{
|
||||
var gridParts = engine.Fill(pattern, dir);
|
||||
if (gridParts != null && gridParts.Count > 0)
|
||||
grids.Add((gridParts, dir));
|
||||
}
|
||||
}
|
||||
|
||||
if (grids.Count == 0)
|
||||
return null;
|
||||
|
||||
// Sort by count descending so we try the best grids first
|
||||
grids.Sort((a, b) => b.Parts.Count.CompareTo(a.Parts.Count));
|
||||
|
||||
// Phase 2: try remnant for each grid, skip if grid is too far behind
|
||||
List<Part> best = null;
|
||||
var maxRemnantEstimate = EstimateMaxRemnantParts(drawing, workArea);
|
||||
|
||||
foreach (var (gridParts, dir) in grids)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// If this grid + max possible remnant can't beat current best, skip
|
||||
if (best != null && gridParts.Count + maxRemnantEstimate <= best.Count)
|
||||
break; // sorted descending, so remaining are even smaller
|
||||
|
||||
var remnantParts = FillRemnant(gridParts, drawing, workArea, token);
|
||||
List<Part> total;
|
||||
if (remnantParts != null && remnantParts.Count > 0)
|
||||
{
|
||||
total = new List<Part>(gridParts.Count + remnantParts.Count);
|
||||
total.AddRange(gridParts);
|
||||
total.AddRange(remnantParts);
|
||||
}
|
||||
else
|
||||
{
|
||||
total = gridParts;
|
||||
}
|
||||
|
||||
if (comparer.IsBetter(total, best, workArea))
|
||||
best = total;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static int EstimateMaxRemnantParts(Drawing drawing, Box workArea)
|
||||
{
|
||||
var partBox = drawing.Program.BoundingBox();
|
||||
var partArea = System.Math.Max(partBox.Width * partBox.Length, 1);
|
||||
var remnantArea = workArea.Area() * 0.3; // remnant is at most ~30% of work area
|
||||
return (int)(remnantArea / partArea) + 1;
|
||||
}
|
||||
|
||||
private List<Part> FillRemnant(List<Part> gridParts, Drawing drawing,
|
||||
Box workArea, CancellationToken token)
|
||||
{
|
||||
var gridBox = ((IEnumerable<IBoundable>)gridParts).GetBoundingBox();
|
||||
var partBox = drawing.Program.BoundingBox();
|
||||
var minDim = System.Math.Min(partBox.Width, partBox.Length) + 2 * partSpacing;
|
||||
|
||||
List<Part> bestRemnant = null;
|
||||
|
||||
// Try top remnant (full width, above grid)
|
||||
var topY = gridBox.Top + partSpacing;
|
||||
var topLength = workArea.Top - topY;
|
||||
if (topLength >= minDim)
|
||||
{
|
||||
var topBox = new Box(workArea.X, topY, workArea.Width, topLength);
|
||||
var parts = FillRemnantBox(drawing, topBox, token);
|
||||
if (parts != null && parts.Count > (bestRemnant?.Count ?? 0))
|
||||
bestRemnant = parts;
|
||||
}
|
||||
|
||||
// Try right remnant (full height, right of grid)
|
||||
var rightX = gridBox.Right + partSpacing;
|
||||
var rightWidth = workArea.Right - rightX;
|
||||
if (rightWidth >= minDim)
|
||||
{
|
||||
var rightBox = new Box(rightX, workArea.Y, rightWidth, workArea.Length);
|
||||
var parts = FillRemnantBox(drawing, rightBox, token);
|
||||
if (parts != null && parts.Count > (bestRemnant?.Count ?? 0))
|
||||
bestRemnant = parts;
|
||||
}
|
||||
|
||||
return bestRemnant;
|
||||
}
|
||||
|
||||
private List<Part> FillRemnantBox(Drawing drawing, Box remnantBox, CancellationToken token)
|
||||
{
|
||||
var cachedResult = FillResultCache.Get(drawing, remnantBox, partSpacing);
|
||||
if (cachedResult != null)
|
||||
{
|
||||
Debug.WriteLine($"[PairFiller] Remnant CACHE HIT: {cachedResult.Count} parts");
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
FillStrategyRegistry.SetEnabled("Pairs", "RectBestFit", "Extents", "Linear");
|
||||
try
|
||||
{
|
||||
var remnantEngine = NestEngineRegistry.Create(plate);
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
var parts = remnantEngine.Fill(item, remnantBox, null, token);
|
||||
|
||||
Debug.WriteLine($"[PairFiller] Remnant: {parts?.Count ?? 0} parts in " +
|
||||
$"{remnantBox.Width:F2}x{remnantBox.Length:F2}");
|
||||
|
||||
if (parts != null && parts.Count > 0)
|
||||
{
|
||||
FillResultCache.Store(drawing, remnantBox, partSpacing, parts);
|
||||
return parts;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
FillStrategyRegistry.SetEnabled(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<double> BuildTilingAngles(BestFitResult candidate)
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace OpenNest.Engine.Strategies
|
||||
private static readonly List<IFillStrategy> strategies = new();
|
||||
private static List<IFillStrategy> sorted;
|
||||
private static HashSet<string> enabledFilter;
|
||||
private static readonly HashSet<string> disabled = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static FillStrategyRegistry()
|
||||
{
|
||||
@@ -19,9 +20,36 @@ namespace OpenNest.Engine.Strategies
|
||||
}
|
||||
|
||||
public static IReadOnlyList<IFillStrategy> Strategies =>
|
||||
sorted ??= (enabledFilter != null
|
||||
? strategies.Where(s => enabledFilter.Contains(s.Name)).OrderBy(s => s.Order).ToList()
|
||||
: strategies.OrderBy(s => s.Order).ToList());
|
||||
sorted ??= FilterStrategies();
|
||||
|
||||
private static List<IFillStrategy> FilterStrategies()
|
||||
{
|
||||
var source = enabledFilter != null
|
||||
? strategies.Where(s => enabledFilter.Contains(s.Name))
|
||||
: strategies.Where(s => !disabled.Contains(s.Name));
|
||||
return source.OrderBy(s => s.Order).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Permanently disables strategies by name. They remain registered
|
||||
/// but are excluded from the default pipeline.
|
||||
/// </summary>
|
||||
public static void Disable(params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
disabled.Add(name);
|
||||
sorted = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-enables a previously disabled strategy.
|
||||
/// </summary>
|
||||
public static void Enable(params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
disabled.Remove(name);
|
||||
sorted = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restricts the active strategies to only those whose names are listed.
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace OpenNest.Engine.Strategies
|
||||
public List<Part> Fill(FillContext context)
|
||||
{
|
||||
var comparer = context.Policy?.Comparer;
|
||||
var filler = new PairFiller(context.Plate.Size, context.Plate.PartSpacing, comparer);
|
||||
var filler = new PairFiller(context.Plate, comparer);
|
||||
var result = filler.Fill(context.Item, context.WorkArea,
|
||||
context.PlateNumber, context.Token, context.Progress);
|
||||
|
||||
|
||||
@@ -16,11 +16,15 @@ public class PairFillerTests
|
||||
return new Drawing("rect", pgm);
|
||||
}
|
||||
|
||||
private static Plate MakePlate(double width, double length, double spacing = 0.5)
|
||||
{
|
||||
return new Plate { Size = new Size(width, length), PartSpacing = spacing };
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_ReturnsPartsForSimpleDrawing()
|
||||
{
|
||||
var plateSize = new Size(120, 60);
|
||||
var filler = new PairFiller(plateSize, 0.5);
|
||||
var filler = new PairFiller(MakePlate(120, 60));
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var workArea = new Box(0, 0, 120, 60);
|
||||
|
||||
@@ -33,8 +37,7 @@ public class PairFillerTests
|
||||
[Fact]
|
||||
public void Fill_EmptyResult_WhenPartTooLarge()
|
||||
{
|
||||
var plateSize = new Size(10, 10);
|
||||
var filler = new PairFiller(plateSize, 0.5);
|
||||
var filler = new PairFiller(MakePlate(10, 10));
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 20) };
|
||||
var workArea = new Box(0, 0, 10, 10);
|
||||
|
||||
@@ -50,8 +53,7 @@ public class PairFillerTests
|
||||
var cts = new System.Threading.CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var plateSize = new Size(120, 60);
|
||||
var filler = new PairFiller(plateSize, 0.5);
|
||||
var filler = new PairFiller(MakePlate(120, 60));
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var workArea = new Box(0, 0, 120, 60);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Strategies;
|
||||
@@ -24,8 +25,8 @@ public class FillPipelineTests
|
||||
|
||||
engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(engine.PhaseResults.Count >= 6,
|
||||
$"Expected phase results from all strategies, got {engine.PhaseResults.Count}");
|
||||
Assert.True(engine.PhaseResults.Count >= FillStrategyRegistry.Strategies.Count,
|
||||
$"Expected phase results from all active strategies, got {engine.PhaseResults.Count}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user