refactor(engine): extract PairFiller from DefaultNestEngine
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -177,7 +177,7 @@ namespace OpenNest
|
||||
|
||||
// --- Protected utilities ---
|
||||
|
||||
protected static void ReportProgress(
|
||||
internal static void ReportProgress(
|
||||
IProgress<NestProgress> progress,
|
||||
NestPhase phase,
|
||||
int plateNumber,
|
||||
|
||||
126
OpenNest.Engine/PairFiller.cs
Normal file
126
OpenNest.Engine/PairFiller.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// Fills a work area using interlocking part pairs from BestFitCache.
|
||||
/// Extracted from DefaultNestEngine.FillWithPairs.
|
||||
/// </summary>
|
||||
public class PairFiller
|
||||
{
|
||||
private readonly Size plateSize;
|
||||
private readonly double partSpacing;
|
||||
|
||||
public PairFiller(Size plateSize, double partSpacing)
|
||||
{
|
||||
this.plateSize = plateSize;
|
||||
this.partSpacing = partSpacing;
|
||||
}
|
||||
|
||||
public List<Part> Fill(NestItem item, Box workArea,
|
||||
int plateNumber = 0,
|
||||
CancellationToken token = default,
|
||||
IProgress<NestProgress> progress = null)
|
||||
{
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
item.Drawing, plateSize.Width, plateSize.Length, partSpacing);
|
||||
|
||||
var candidates = SelectPairCandidates(bestFits, workArea);
|
||||
Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}");
|
||||
Debug.WriteLine($"[PairFiller] Plate: {plateSize.Width:F2}x{plateSize.Length:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}");
|
||||
|
||||
List<Part> best = null;
|
||||
var bestScore = default(FillScore);
|
||||
var sinceImproved = 0;
|
||||
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var result = candidates[i];
|
||||
var pairParts = result.BuildParts(item.Drawing);
|
||||
var angles = result.HullAngles;
|
||||
var engine = new FillLinear(workArea, partSpacing);
|
||||
var filled = DefaultNestEngine.FillPattern(engine, pairParts, angles, workArea);
|
||||
|
||||
if (filled != null && filled.Count > 0)
|
||||
{
|
||||
var score = FillScore.Compute(filled, workArea);
|
||||
if (best == null || score > bestScore)
|
||||
{
|
||||
best = filled;
|
||||
bestScore = score;
|
||||
sinceImproved = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
sinceImproved++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sinceImproved++;
|
||||
}
|
||||
|
||||
NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea,
|
||||
$"Pairs: {i + 1}/{candidates.Count} candidates, best = {bestScore.Count} parts");
|
||||
|
||||
// Early exit: stop if we've tried enough candidates without improvement.
|
||||
if (i >= 9 && sinceImproved >= 10)
|
||||
{
|
||||
Debug.WriteLine($"[PairFiller] Early exit at {i + 1}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine("[PairFiller] Cancelled mid-phase, using results so far");
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[PairFiller] Best pair result: {bestScore.Count} parts, density={bestScore.Density:P1}");
|
||||
return best ?? new List<Part>();
|
||||
}
|
||||
|
||||
private List<BestFitResult> SelectPairCandidates(List<BestFitResult> bestFits, Box workArea)
|
||||
{
|
||||
var kept = bestFits.Where(r => r.Keep).ToList();
|
||||
var top = kept.Take(50).ToList();
|
||||
|
||||
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
||||
var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length);
|
||||
|
||||
if (workShortSide < plateShortSide * 0.5)
|
||||
{
|
||||
var stripCandidates = bestFits
|
||||
.Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon
|
||||
&& r.Utilization >= 0.3)
|
||||
.OrderByDescending(r => r.Utilization);
|
||||
|
||||
var existing = new HashSet<BestFitResult>(top);
|
||||
|
||||
foreach (var r in stripCandidates)
|
||||
{
|
||||
if (top.Count >= 100)
|
||||
break;
|
||||
|
||||
if (existing.Add(r))
|
||||
top.Add(r);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})");
|
||||
}
|
||||
|
||||
return top;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
OpenNest.Tests/PairFillerTests.cs
Normal file
63
OpenNest.Tests/PairFillerTests.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class PairFillerTests
|
||||
{
|
||||
private static Drawing MakeRectDrawing(double w, double h)
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("rect", pgm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_ReturnsPartsForSimpleDrawing()
|
||||
{
|
||||
var plateSize = new Size(120, 60);
|
||||
var filler = new PairFiller(plateSize, 0.5);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var workArea = new Box(0, 0, 120, 60);
|
||||
|
||||
var parts = filler.Fill(item, workArea);
|
||||
|
||||
Assert.NotNull(parts);
|
||||
// Pair filling may or may not find interlocking pairs for rectangles,
|
||||
// but should return a non-null list.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_EmptyResult_WhenPartTooLarge()
|
||||
{
|
||||
var plateSize = new Size(10, 10);
|
||||
var filler = new PairFiller(plateSize, 0.5);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 20) };
|
||||
var workArea = new Box(0, 0, 10, 10);
|
||||
|
||||
var parts = filler.Fill(item, workArea);
|
||||
|
||||
Assert.NotNull(parts);
|
||||
Assert.Empty(parts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_RespectsCancellation()
|
||||
{
|
||||
var cts = new System.Threading.CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var plateSize = new Size(120, 60);
|
||||
var filler = new PairFiller(plateSize, 0.5);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var workArea = new Box(0, 0, 120, 60);
|
||||
|
||||
var parts = filler.Fill(item, workArea, token: cts.Token);
|
||||
|
||||
// Should return empty or partial — not throw
|
||||
Assert.NotNull(parts);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user