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 utilities ---
|
||||||
|
|
||||||
protected static void ReportProgress(
|
internal static void ReportProgress(
|
||||||
IProgress<NestProgress> progress,
|
IProgress<NestProgress> progress,
|
||||||
NestPhase phase,
|
NestPhase phase,
|
||||||
int plateNumber,
|
int plateNumber,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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