diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs
index c66deda..53bb30f 100644
--- a/OpenNest.Console/Program.cs
+++ b/OpenNest.Console/Program.cs
@@ -327,7 +327,7 @@ static class NestConsole
Console.WriteLine($"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts");
- var nestParts = NestEngine.AutoNest(nestItems, plate);
+ var nestParts = AutoNester.Nest(nestItems, plate);
plate.Parts.AddRange(nestParts);
success = nestParts.Count > 0;
}
diff --git a/OpenNest.Engine/AutoNester.cs b/OpenNest.Engine/AutoNester.cs
new file mode 100644
index 0000000..0140544
--- /dev/null
+++ b/OpenNest.Engine/AutoNester.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using OpenNest.Converters;
+using OpenNest.Geometry;
+using OpenNest.Math;
+
+namespace OpenNest
+{
+ ///
+ /// Mixed-part geometry-aware nesting using NFP-based collision avoidance
+ /// and simulated annealing optimization.
+ ///
+ public static class AutoNester
+ {
+ public static List Nest(List items, Plate plate,
+ CancellationToken cancellation = default)
+ {
+ var workArea = plate.WorkArea();
+ var halfSpacing = plate.PartSpacing / 2.0;
+ var nfpCache = new NfpCache();
+ var candidateRotations = new Dictionary>();
+
+ // Extract perimeter polygons for each unique drawing.
+ foreach (var item in items)
+ {
+ var drawing = item.Drawing;
+
+ if (candidateRotations.ContainsKey(drawing.Id))
+ continue;
+
+ var perimeterPolygon = ExtractPerimeterPolygon(drawing, halfSpacing);
+
+ if (perimeterPolygon == null)
+ {
+ Debug.WriteLine($"[AutoNest] Skipping drawing '{drawing.Name}': no valid perimeter");
+ continue;
+ }
+
+ // Compute candidate rotations for this drawing.
+ var rotations = ComputeCandidateRotations(item, perimeterPolygon, workArea);
+ candidateRotations[drawing.Id] = rotations;
+
+ // Register polygons at each candidate rotation.
+ foreach (var rotation in rotations)
+ {
+ var rotatedPolygon = RotatePolygon(perimeterPolygon, rotation);
+ nfpCache.RegisterPolygon(drawing.Id, rotation, rotatedPolygon);
+ }
+ }
+
+ if (candidateRotations.Count == 0)
+ return new List();
+
+ // Pre-compute all NFPs.
+ nfpCache.PreComputeAll();
+
+ Debug.WriteLine($"[AutoNest] NFP cache: {nfpCache.Count} entries for {candidateRotations.Count} drawings");
+
+ // Run simulated annealing optimizer.
+ var optimizer = new SimulatedAnnealing();
+ var result = optimizer.Optimize(items, workArea, nfpCache, candidateRotations, cancellation);
+
+ if (result.Sequence == null || result.Sequence.Count == 0)
+ return new List();
+
+ // Final BLF placement with the best solution.
+ var blf = new BottomLeftFill(workArea, nfpCache);
+ var placedParts = blf.Fill(result.Sequence);
+ var parts = BottomLeftFill.ToNestParts(placedParts);
+
+ Debug.WriteLine($"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations");
+
+ return parts;
+ }
+
+ ///
+ /// Extracts the perimeter polygon from a drawing, inflated by half-spacing.
+ ///
+ private static Polygon ExtractPerimeterPolygon(Drawing drawing, double halfSpacing)
+ {
+ var entities = ConvertProgram.ToGeometry(drawing.Program)
+ .Where(e => e.Layer != SpecialLayers.Rapid)
+ .ToList();
+
+ if (entities.Count == 0)
+ return null;
+
+ var definedShape = new ShapeProfile(entities);
+ var perimeter = definedShape.Perimeter;
+
+ if (perimeter == null)
+ return null;
+
+ // Inflate by half-spacing if spacing is non-zero.
+ Shape inflated;
+
+ if (halfSpacing > 0)
+ {
+ var offsetEntity = perimeter.OffsetEntity(halfSpacing, OffsetSide.Right);
+ inflated = offsetEntity as Shape ?? perimeter;
+ }
+ else
+ {
+ inflated = perimeter;
+ }
+
+ // Convert to polygon with circumscribed arcs for tight nesting.
+ var polygon = inflated.ToPolygonWithTolerance(0.01, circumscribe: true);
+
+ if (polygon.Vertices.Count < 3)
+ return null;
+
+ // Normalize: move reference point to origin.
+ polygon.UpdateBounds();
+ var bb = polygon.BoundingBox;
+ polygon.Offset(-bb.Left, -bb.Bottom);
+
+ return polygon;
+ }
+
+ ///
+ /// Computes candidate rotation angles for a drawing.
+ ///
+ private static List ComputeCandidateRotations(NestItem item,
+ Polygon perimeterPolygon, Box workArea)
+ {
+ var rotations = new List { 0 };
+
+ // Add hull-edge angles from the polygon itself.
+ var hullAngles = ComputeHullEdgeAngles(perimeterPolygon);
+
+ foreach (var angle in hullAngles)
+ {
+ if (!rotations.Any(r => r.IsEqualTo(angle)))
+ rotations.Add(angle);
+ }
+
+ // Add 90-degree rotation.
+ if (!rotations.Any(r => r.IsEqualTo(Angle.HalfPI)))
+ rotations.Add(Angle.HalfPI);
+
+ // For narrow work areas, add sweep angles.
+ var partBounds = perimeterPolygon.BoundingBox;
+ var partLongest = System.Math.Max(partBounds.Width, partBounds.Length);
+ var workShort = System.Math.Min(workArea.Width, workArea.Length);
+
+ if (workShort < partLongest)
+ {
+ var step = Angle.ToRadians(5);
+
+ for (var a = 0.0; a < System.Math.PI; a += step)
+ {
+ if (!rotations.Any(r => r.IsEqualTo(a)))
+ rotations.Add(a);
+ }
+ }
+
+ return rotations;
+ }
+
+ ///
+ /// Computes convex hull edge angles from a polygon for candidate rotations.
+ ///
+ private static List ComputeHullEdgeAngles(Polygon polygon)
+ {
+ var angles = new List();
+
+ if (polygon.Vertices.Count < 3)
+ return angles;
+
+ var hull = ConvexHull.Compute(polygon.Vertices);
+ var verts = hull.Vertices;
+ var n = hull.IsClosed() ? verts.Count - 1 : verts.Count;
+
+ for (var i = 0; i < n; i++)
+ {
+ var next = (i + 1) % n;
+ var dx = verts[next].X - verts[i].X;
+ var dy = verts[next].Y - verts[i].Y;
+
+ if (dx * dx + dy * dy < Tolerance.Epsilon)
+ continue;
+
+ var angle = -System.Math.Atan2(dy, dx);
+
+ if (!angles.Any(a => a.IsEqualTo(angle)))
+ angles.Add(angle);
+ }
+
+ return angles;
+ }
+
+ ///
+ /// Creates a rotated copy of a polygon around the origin.
+ ///
+ private static Polygon RotatePolygon(Polygon polygon, double angle)
+ {
+ if (angle.IsEqualTo(0))
+ return polygon;
+
+ var result = new Polygon();
+ var cos = System.Math.Cos(angle);
+ var sin = System.Math.Sin(angle);
+
+ foreach (var v in polygon.Vertices)
+ {
+ result.Vertices.Add(new Vector(
+ v.X * cos - v.Y * sin,
+ v.X * sin + v.Y * cos));
+ }
+
+ // Re-normalize to origin.
+ result.UpdateBounds();
+ var bb = result.BoundingBox;
+ result.Offset(-bb.Left, -bb.Bottom);
+
+ return result;
+ }
+ }
+}
diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs
index 1eaf05c..271629a 100644
--- a/OpenNest.Engine/NestEngine.cs
+++ b/OpenNest.Engine/NestEngine.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
-using OpenNest.Converters;
using OpenNest.Engine.BestFit;
using OpenNest.Engine.ML;
using OpenNest.Geometry;
@@ -33,16 +32,13 @@ namespace OpenNest
public List AngleResults { get; } = new();
+ // --- Public Fill API ---
+
public bool Fill(NestItem item)
{
return Fill(item, Plate.WorkArea());
}
- public bool Fill(List groupParts)
- {
- return Fill(groupParts, Plate.WorkArea());
- }
-
public bool Fill(NestItem item, Box workArea)
{
var parts = Fill(item, workArea, null, CancellationToken.None);
@@ -75,7 +71,7 @@ namespace OpenNest
best = improved;
WinnerPhase = NestPhase.Remainder;
PhaseResults.Add(new PhaseResult(NestPhase.Remainder, improved.Count, remainderSw.ElapsedMilliseconds));
- ReportProgress(progress, NestPhase.Remainder, PlateNumber, best, workArea);
+ ReportProgress(progress, NestPhase.Remainder, PlateNumber, best, workArea, BuildProgressSummary());
}
if (best == null || best.Count == 0)
@@ -87,187 +83,124 @@ namespace OpenNest
return best;
}
- private List FindBestFill(NestItem item, Box workArea)
+ public bool Fill(List groupParts)
{
- var bestRotation = RotationAnalysis.FindBestRotation(item);
-
- var engine = new FillLinear(workArea, Plate.PartSpacing);
-
- // Build candidate rotation angles — always try the best rotation and +90°.
- var angles = new List { bestRotation, bestRotation + Angle.HalfPI };
-
- // When the work area is narrow relative to the part, sweep rotation
- // angles so we can find one that fits the part into the tight strip.
- var testPart = new Part(item.Drawing);
-
- if (!bestRotation.IsEqualTo(0))
- testPart.Rotate(bestRotation);
-
- testPart.UpdateBounds();
-
- var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
- var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
-
- if (workAreaShortSide < partLongestSide)
- {
- // Try every 5° from 0 to 175° to find rotations that fit.
- var step = Angle.ToRadians(5);
-
- for (var a = 0.0; a < System.Math.PI; a += step)
- {
- if (!angles.Any(existing => existing.IsEqualTo(a)))
- angles.Add(a);
- }
- }
-
- if (ForceFullAngleSweep)
- {
- var step = Angle.ToRadians(5);
- for (var a = 0.0; a < System.Math.PI; a += step)
- {
- if (!angles.Any(existing => existing.IsEqualTo(a)))
- angles.Add(a);
- }
- }
-
- // When the work area triggers a full sweep (and we're not forcing it for training),
- // try ML angle prediction to reduce the sweep.
- if (!ForceFullAngleSweep && angles.Count > 2)
- {
- var features = FeatureExtractor.Extract(item.Drawing);
- if (features != null)
- {
- var predicted = AnglePredictor.PredictAngles(
- features, workArea.Width, workArea.Length);
-
- if (predicted != null)
- {
- // Use predicted angles, but always keep bestRotation and bestRotation + 90.
- var mlAngles = new List(predicted);
-
- if (!mlAngles.Any(a => a.IsEqualTo(bestRotation)))
- mlAngles.Add(bestRotation);
- if (!mlAngles.Any(a => a.IsEqualTo(bestRotation + Angle.HalfPI)))
- mlAngles.Add(bestRotation + Angle.HalfPI);
-
- Debug.WriteLine($"[FindBestFill] ML: {angles.Count} angles -> {mlAngles.Count} predicted");
- angles = mlAngles;
- }
- }
- }
-
- // Try pair-based approach first.
- var pairResult = FillWithPairs(item, workArea);
- var best = pairResult;
- var bestScore = FillScore.Compute(best, workArea);
-
- Debug.WriteLine($"[FindBestFill] Pair: {bestScore.Count} parts");
-
- // Try linear phase.
- var linearBag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List parts)>();
-
- System.Threading.Tasks.Parallel.ForEach(angles, angle =>
- {
- var localEngine = new FillLinear(workArea, Plate.PartSpacing);
- var h = localEngine.Fill(item.Drawing, angle, NestDirection.Horizontal);
- var v = localEngine.Fill(item.Drawing, angle, NestDirection.Vertical);
-
- if (h != null && h.Count > 0)
- linearBag.Add((FillScore.Compute(h, workArea), h));
-
- if (v != null && v.Count > 0)
- linearBag.Add((FillScore.Compute(v, workArea), v));
- });
-
- foreach (var (score, parts) in linearBag)
- {
- if (score > bestScore)
- {
- best = parts;
- bestScore = score;
- }
- }
-
- Debug.WriteLine($"[FindBestFill] Linear: {bestScore.Count} parts, density={bestScore.Density:P1} | WorkArea: {workArea.Width:F1}x{workArea.Length:F1} | Angles: {angles.Count}");
-
- // Try rectangle best-fit (mixes orientations to fill remnant strips).
- var rectResult = FillRectangleBestFit(item, workArea);
- var rectScore = rectResult != null ? FillScore.Compute(rectResult, workArea) : default;
-
- Debug.WriteLine($"[FindBestFill] RectBestFit: {rectScore.Count} parts");
-
- if (rectScore > bestScore)
- best = rectResult;
-
- return best;
+ return Fill(groupParts, Plate.WorkArea());
}
- private List FindBestFill(NestItem item, Box workArea,
+ public bool Fill(List groupParts, Box workArea)
+ {
+ var parts = Fill(groupParts, workArea, null, CancellationToken.None);
+
+ if (parts == null || parts.Count == 0)
+ return false;
+
+ Plate.Parts.AddRange(parts);
+ return true;
+ }
+
+ public List Fill(List groupParts, Box workArea,
IProgress progress, CancellationToken token)
+ {
+ if (groupParts == null || groupParts.Count == 0)
+ return new List();
+
+ PhaseResults.Clear();
+ var engine = new FillLinear(workArea, Plate.PartSpacing);
+ var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
+ var best = FillPattern(engine, groupParts, angles, workArea);
+ PhaseResults.Add(new PhaseResult(NestPhase.Linear, best?.Count ?? 0, 0));
+
+ Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}");
+
+ ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary());
+
+ if (groupParts.Count == 1)
+ {
+ try
+ {
+ token.ThrowIfCancellationRequested();
+
+ var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
+ var rectResult = FillRectangleBestFit(nestItem, workArea);
+ PhaseResults.Add(new PhaseResult(NestPhase.RectBestFit, rectResult?.Count ?? 0, 0));
+
+ Debug.WriteLine($"[Fill(groupParts,Box)] RectBestFit: {rectResult?.Count ?? 0} parts");
+
+ if (IsBetterFill(rectResult, best, workArea))
+ {
+ best = rectResult;
+ ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea, BuildProgressSummary());
+ }
+
+ token.ThrowIfCancellationRequested();
+
+ var pairResult = FillWithPairs(nestItem, workArea, token, progress);
+ PhaseResults.Add(new PhaseResult(NestPhase.Pairs, pairResult.Count, 0));
+
+ Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best, workArea) ? "Pair" : "Linear")}");
+
+ if (IsBetterFill(pairResult, best, workArea))
+ {
+ best = pairResult;
+ ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea, BuildProgressSummary());
+ }
+
+ // Try improving by filling the remainder strip separately.
+ var improved = TryRemainderImprovement(nestItem, workArea, best);
+
+ if (IsBetterFill(improved, best, workArea))
+ {
+ Debug.WriteLine($"[Fill(groupParts,Box)] Remainder improvement: {improved.Count} parts (was {best?.Count ?? 0})");
+ best = improved;
+ PhaseResults.Add(new PhaseResult(NestPhase.Remainder, improved.Count, 0));
+ ReportProgress(progress, NestPhase.Remainder, PlateNumber, best, workArea, BuildProgressSummary());
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ Debug.WriteLine("[Fill(groupParts,Box)] Cancelled, returning current best");
+ }
+ }
+
+ return best ?? new List();
+ }
+
+ // --- Pack API ---
+
+ public bool Pack(List items)
+ {
+ var workArea = Plate.WorkArea();
+ return PackArea(workArea, items);
+ }
+
+ public bool PackArea(Box box, List items)
+ {
+ var binItems = BinConverter.ToItems(items, Plate.PartSpacing, Plate.Area());
+ var bin = BinConverter.CreateBin(box, Plate.PartSpacing);
+
+ var engine = new PackBottomLeft(bin);
+ engine.Pack(binItems);
+
+ var parts = BinConverter.ToParts(bin, items);
+ Plate.Parts.AddRange(parts);
+
+ return parts.Count > 0;
+ }
+
+ // --- FindBestFill: core orchestration ---
+
+ private List FindBestFill(NestItem item, Box workArea,
+ IProgress progress = null, CancellationToken token = default)
{
List best = null;
try
{
var bestRotation = RotationAnalysis.FindBestRotation(item);
- var engine = new FillLinear(workArea, Plate.PartSpacing);
- var angles = new List { bestRotation, bestRotation + Angle.HalfPI };
+ var angles = BuildCandidateAngles(item, bestRotation, workArea);
- var testPart = new Part(item.Drawing);
- if (!bestRotation.IsEqualTo(0))
- testPart.Rotate(bestRotation);
- testPart.UpdateBounds();
-
- var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
- var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
-
- if (workAreaShortSide < partLongestSide)
- {
- var step = Angle.ToRadians(5);
- for (var a = 0.0; a < System.Math.PI; a += step)
- {
- if (!angles.Any(existing => existing.IsEqualTo(a)))
- angles.Add(a);
- }
- }
-
- if (ForceFullAngleSweep)
- {
- var step = Angle.ToRadians(5);
- for (var a = 0.0; a < System.Math.PI; a += step)
- {
- if (!angles.Any(existing => existing.IsEqualTo(a)))
- angles.Add(a);
- }
- }
-
- // When the work area triggers a full sweep (and we're not forcing it for training),
- // try ML angle prediction to reduce the sweep.
- if (!ForceFullAngleSweep && angles.Count > 2)
- {
- var features = FeatureExtractor.Extract(item.Drawing);
- if (features != null)
- {
- var predicted = AnglePredictor.PredictAngles(
- features, workArea.Width, workArea.Length);
-
- if (predicted != null)
- {
- // Use predicted angles, but always keep bestRotation and bestRotation + 90.
- var mlAngles = new List(predicted);
-
- if (!mlAngles.Any(a => a.IsEqualTo(bestRotation)))
- mlAngles.Add(bestRotation);
- if (!mlAngles.Any(a => a.IsEqualTo(bestRotation + Angle.HalfPI)))
- mlAngles.Add(bestRotation + Angle.HalfPI);
-
- Debug.WriteLine($"[FindBestFill] ML: {angles.Count} angles -> {mlAngles.Count} predicted");
- angles = mlAngles;
- }
- }
- }
-
- // Pairs phase first
+ // Pairs phase
var pairSw = Stopwatch.StartNew();
var pairResult = FillWithPairs(item, workArea, token, progress);
pairSw.Stop();
@@ -277,14 +210,13 @@ namespace OpenNest
PhaseResults.Add(new PhaseResult(NestPhase.Pairs, pairResult.Count, pairSw.ElapsedMilliseconds));
Debug.WriteLine($"[FindBestFill] Pair: {bestScore.Count} parts");
- ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea);
+ ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea, BuildProgressSummary());
token.ThrowIfCancellationRequested();
// Linear phase
var linearSw = Stopwatch.StartNew();
var linearBag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List parts)>();
var angleBag = new System.Collections.Concurrent.ConcurrentBag();
-
var anglesCompleted = 0;
System.Threading.Tasks.Parallel.ForEach(angles,
@@ -335,7 +267,7 @@ namespace OpenNest
Debug.WriteLine($"[FindBestFill] Linear: {bestScore.Count} parts, density={bestScore.Density:P1} | WorkArea: {workArea.Width:F1}x{workArea.Length:F1} | Angles: {angles.Count}");
- ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea);
+ ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary());
token.ThrowIfCancellationRequested();
// RectBestFit phase
@@ -350,7 +282,7 @@ namespace OpenNest
{
best = rectResult;
WinnerPhase = NestPhase.RectBestFit;
- ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea);
+ ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea, BuildProgressSummary());
}
}
catch (OperationCanceledException)
@@ -361,98 +293,62 @@ namespace OpenNest
return best ?? new List();
}
- public bool Fill(List groupParts, Box workArea)
+ // --- Angle building ---
+
+ private List BuildCandidateAngles(NestItem item, double bestRotation, Box workArea)
{
- var parts = Fill(groupParts, workArea, null, CancellationToken.None);
+ var angles = new List { bestRotation, bestRotation + Angle.HalfPI };
- if (parts == null || parts.Count == 0)
- return false;
+ // When the work area is narrow relative to the part, sweep rotation
+ // angles so we can find one that fits the part into the tight strip.
+ var testPart = new Part(item.Drawing);
+ if (!bestRotation.IsEqualTo(0))
+ testPart.Rotate(bestRotation);
+ testPart.UpdateBounds();
- Plate.Parts.AddRange(parts);
- return true;
- }
+ var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
+ var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
+ var needsSweep = workAreaShortSide < partLongestSide || ForceFullAngleSweep;
- public List Fill(List groupParts, Box workArea,
- IProgress progress, CancellationToken token)
- {
- if (groupParts == null || groupParts.Count == 0)
- return new List();
-
- var engine = new FillLinear(workArea, Plate.PartSpacing);
- var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
- var best = FillPattern(engine, groupParts, angles, workArea);
-
- Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}");
-
- ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea);
-
- if (groupParts.Count == 1)
+ if (needsSweep)
{
- try
+ var step = Angle.ToRadians(5);
+ for (var a = 0.0; a < System.Math.PI; a += step)
{
- token.ThrowIfCancellationRequested();
-
- var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
- var rectResult = FillRectangleBestFit(nestItem, workArea);
-
- Debug.WriteLine($"[Fill(groupParts,Box)] RectBestFit: {rectResult?.Count ?? 0} parts");
-
- if (IsBetterFill(rectResult, best, workArea))
- {
- best = rectResult;
- ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea);
- }
-
- token.ThrowIfCancellationRequested();
-
- var pairResult = FillWithPairs(nestItem, workArea, token);
-
- Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best, workArea) ? "Pair" : "Linear")}");
-
- if (IsBetterFill(pairResult, best, workArea))
- {
- best = pairResult;
- ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea);
- }
-
- // Try improving by filling the remainder strip separately.
- var improved = TryRemainderImprovement(nestItem, workArea, best);
-
- if (IsBetterFill(improved, best, workArea))
- {
- Debug.WriteLine($"[Fill(groupParts,Box)] Remainder improvement: {improved.Count} parts (was {best?.Count ?? 0})");
- best = improved;
- ReportProgress(progress, NestPhase.Remainder, PlateNumber, best, workArea);
- }
- }
- catch (OperationCanceledException)
- {
- Debug.WriteLine("[Fill(groupParts,Box)] Cancelled, returning current best");
+ if (!angles.Any(existing => existing.IsEqualTo(a)))
+ angles.Add(a);
}
}
- return best ?? new List();
+ // When the work area triggers a full sweep (and we're not forcing it for training),
+ // try ML angle prediction to reduce the sweep.
+ if (!ForceFullAngleSweep && angles.Count > 2)
+ {
+ var features = FeatureExtractor.Extract(item.Drawing);
+ if (features != null)
+ {
+ var predicted = AnglePredictor.PredictAngles(
+ features, workArea.Width, workArea.Length);
+
+ if (predicted != null)
+ {
+ var mlAngles = new List(predicted);
+
+ if (!mlAngles.Any(a => a.IsEqualTo(bestRotation)))
+ mlAngles.Add(bestRotation);
+ if (!mlAngles.Any(a => a.IsEqualTo(bestRotation + Angle.HalfPI)))
+ mlAngles.Add(bestRotation + Angle.HalfPI);
+
+ Debug.WriteLine($"[BuildCandidateAngles] ML: {angles.Count} angles -> {mlAngles.Count} predicted");
+ angles = mlAngles;
+ }
+ }
+ }
+
+ return angles;
}
- public bool Pack(List items)
- {
- var workArea = Plate.WorkArea();
- return PackArea(workArea, items);
- }
-
- public bool PackArea(Box box, List items)
- {
- var binItems = BinConverter.ToItems(items, Plate.PartSpacing, Plate.Area());
- var bin = BinConverter.CreateBin(box, Plate.PartSpacing);
-
- var engine = new PackBottomLeft(bin);
- engine.Pack(binItems);
-
- var parts = BinConverter.ToParts(bin, items);
- Plate.Parts.AddRange(parts);
-
- return parts.Count > 0;
- }
+ // --- Fill strategies ---
private List FillRectangleBestFit(NestItem item, Box workArea)
{
@@ -465,46 +361,8 @@ namespace OpenNest
return BinConverter.ToParts(bin, new List { item });
}
- private List FillWithPairs(NestItem item, Box workArea)
- {
- var bestFits = BestFitCache.GetOrCompute(
- item.Drawing, Plate.Size.Width, Plate.Size.Length,
- Plate.PartSpacing);
-
- var candidates = SelectPairCandidates(bestFits, workArea);
- Debug.WriteLine($"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}");
-
- var resultBag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List parts)>();
-
- System.Threading.Tasks.Parallel.For(0, candidates.Count, i =>
- {
- var result = candidates[i];
- var pairParts = result.BuildParts(item.Drawing);
- var angles = result.HullAngles;
- var engine = new FillLinear(workArea, Plate.PartSpacing);
- var filled = FillPattern(engine, pairParts, angles, workArea);
-
- if (filled != null && filled.Count > 0)
- resultBag.Add((FillScore.Compute(filled, workArea), filled));
- });
-
- List best = null;
- var bestScore = default(FillScore);
-
- foreach (var (score, parts) in resultBag)
- {
- if (best == null || score > bestScore)
- {
- best = parts;
- bestScore = score;
- }
- }
-
- Debug.WriteLine($"[FillWithPairs] Best pair result: {bestScore.Count} parts, remnant={bestScore.UsableRemnantArea:F1}, density={bestScore.Density:P1}");
- return best ?? new List();
- }
-
- private List FillWithPairs(NestItem item, Box workArea, CancellationToken token, IProgress progress = null)
+ private List FillWithPairs(NestItem item, Box workArea,
+ CancellationToken token = default, IProgress progress = null)
{
var bestFits = BestFitCache.GetOrCompute(
item.Drawing, Plate.Size.Width, Plate.Size.Length,
@@ -567,8 +425,7 @@ namespace OpenNest
///
/// Selects pair candidates to try for the given work area. Always includes
/// the top 50 by area. For narrow work areas, also includes all pairs whose
- /// shortest side fits the strip width — these are candidates that can only
- /// be evaluated by actually tiling them into the narrow space.
+ /// shortest side fits the strip width.
///
private List SelectPairCandidates(List bestFits, Box workArea)
{
@@ -599,104 +456,82 @@ namespace OpenNest
return top;
}
- private bool HasOverlaps(List parts, double spacing)
+ // --- Pattern helpers ---
+
+ private Pattern BuildRotatedPattern(List groupParts, double angle)
{
- if (parts == null || parts.Count <= 1)
- return false;
+ var pattern = new Pattern();
+ var center = ((IEnumerable)groupParts).GetBoundingBox().Center;
- for (var i = 0; i < parts.Count; i++)
+ foreach (var part in groupParts)
{
- var box1 = parts[i].BoundingBox;
+ var clone = (Part)part.Clone();
+ clone.UpdateBounds();
- for (var j = i + 1; j < parts.Count; j++)
+ if (!angle.IsEqualTo(0))
+ clone.Rotate(angle, center);
+
+ pattern.Parts.Add(clone);
+ }
+
+ pattern.UpdateBounds();
+ return pattern;
+ }
+
+ private List FillPattern(FillLinear engine, List groupParts, List angles, Box workArea)
+ {
+ List best = null;
+ var bestScore = default(FillScore);
+
+ foreach (var angle in angles)
+ {
+ var pattern = BuildRotatedPattern(groupParts, angle);
+
+ if (pattern.Parts.Count == 0)
+ continue;
+
+ var h = engine.Fill(pattern, NestDirection.Horizontal);
+ var scoreH = h != null && h.Count > 0 ? FillScore.Compute(h, workArea) : default;
+
+ if (scoreH.Count > 0 && (best == null || scoreH > bestScore))
{
- var box2 = parts[j].BoundingBox;
+ best = h;
+ bestScore = scoreH;
+ }
- // Fast bounding box rejection — if boxes don't overlap,
- // the parts can't intersect. Eliminates nearly all pairs
- // in grid layouts.
- if (box1.Right < box2.Left || box2.Right < box1.Left ||
- box1.Top < box2.Bottom || box2.Top < box1.Bottom)
- continue;
+ var v = engine.Fill(pattern, NestDirection.Vertical);
+ var scoreV = v != null && v.Count > 0 ? FillScore.Compute(v, workArea) : default;
- List pts;
-
- if (parts[i].Intersects(parts[j], out pts))
- {
- var b1 = parts[i].BoundingBox;
- var b2 = parts[j].BoundingBox;
- Debug.WriteLine($"[HasOverlaps] Overlap: part[{i}] ({parts[i].BaseDrawing?.Name}) @ ({b1.Left:F2},{b1.Bottom:F2})-({b1.Right:F2},{b1.Top:F2}) rot={parts[i].Rotation:F2}" +
- $" vs part[{j}] ({parts[j].BaseDrawing?.Name}) @ ({b2.Left:F2},{b2.Bottom:F2})-({b2.Right:F2},{b2.Top:F2}) rot={parts[j].Rotation:F2}" +
- $" intersections={pts?.Count ?? 0}");
- return true;
- }
+ if (scoreV.Count > 0 && (best == null || scoreV > bestScore))
+ {
+ best = v;
+ bestScore = scoreV;
}
}
- return false;
+ return best;
}
- private bool IsBetterFill(List candidate, List current, Box workArea)
+ // --- Remainder improvement ---
+
+ private List TryRemainderImprovement(NestItem item, Box workArea, List currentBest)
{
- if (candidate == null || candidate.Count == 0)
- return false;
+ if (currentBest == null || currentBest.Count < 3)
+ return null;
- if (current == null || current.Count == 0)
- return true;
+ List best = null;
- return FillScore.Compute(candidate, workArea) > FillScore.Compute(current, workArea);
- }
+ var hResult = TryStripRefill(item, workArea, currentBest, horizontal: true);
- private bool IsBetterValidFill(List candidate, List current, Box workArea)
- {
- if (candidate != null && candidate.Count > 0 && HasOverlaps(candidate, Plate.PartSpacing))
- {
- Debug.WriteLine($"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})");
- return false;
- }
+ if (IsBetterFill(hResult, best, workArea))
+ best = hResult;
- return IsBetterFill(candidate, current, workArea);
- }
+ var vResult = TryStripRefill(item, workArea, currentBest, horizontal: false);
- ///
- /// Groups parts into positional clusters along the given axis.
- /// Parts whose center positions are separated by more than half
- /// the part dimension start a new cluster.
- ///
- private static List> ClusterParts(List parts, bool horizontal)
- {
- var sorted = horizontal
- ? parts.OrderBy(p => p.BoundingBox.Center.X).ToList()
- : parts.OrderBy(p => p.BoundingBox.Center.Y).ToList();
+ if (IsBetterFill(vResult, best, workArea))
+ best = vResult;
- var refDim = horizontal
- ? sorted.Max(p => p.BoundingBox.Width)
- : sorted.Max(p => p.BoundingBox.Length);
- var gapThreshold = refDim * 0.5;
-
- var clusters = new List>();
- var current = new List { sorted[0] };
-
- for (var i = 1; i < sorted.Count; i++)
- {
- var prevCenter = horizontal
- ? sorted[i - 1].BoundingBox.Center.X
- : sorted[i - 1].BoundingBox.Center.Y;
- var currCenter = horizontal
- ? sorted[i].BoundingBox.Center.X
- : sorted[i].BoundingBox.Center.Y;
-
- if (currCenter - prevCenter > gapThreshold)
- {
- clusters.Add(current);
- current = new List();
- }
-
- current.Add(sorted[i]);
- }
-
- clusters.Add(current);
- return clusters;
+ return best;
}
private List TryStripRefill(NestItem item, Box workArea, List parts, bool horizontal)
@@ -771,86 +606,115 @@ namespace OpenNest
return combined;
}
- private List TryRemainderImprovement(NestItem item, Box workArea, List currentBest)
+ ///
+ /// Groups parts into positional clusters along the given axis.
+ /// Parts whose center positions are separated by more than half
+ /// the part dimension start a new cluster.
+ ///
+ private static List> ClusterParts(List parts, bool horizontal)
{
- if (currentBest == null || currentBest.Count < 3)
- return null;
+ var sorted = horizontal
+ ? parts.OrderBy(p => p.BoundingBox.Center.X).ToList()
+ : parts.OrderBy(p => p.BoundingBox.Center.Y).ToList();
- List best = null;
+ var refDim = horizontal
+ ? sorted.Max(p => p.BoundingBox.Width)
+ : sorted.Max(p => p.BoundingBox.Length);
+ var gapThreshold = refDim * 0.5;
- var hResult = TryStripRefill(item, workArea, currentBest, horizontal: true);
+ var clusters = new List>();
+ var current = new List { sorted[0] };
- if (IsBetterFill(hResult, best, workArea))
- best = hResult;
-
- var vResult = TryStripRefill(item, workArea, currentBest, horizontal: false);
-
- if (IsBetterFill(vResult, best, workArea))
- best = vResult;
-
- return best;
- }
-
- private Pattern BuildRotatedPattern(List groupParts, double angle)
- {
- var pattern = new Pattern();
- var center = ((IEnumerable)groupParts).GetBoundingBox().Center;
-
- foreach (var part in groupParts)
+ for (var i = 1; i < sorted.Count; i++)
{
- var clone = (Part)part.Clone();
- clone.UpdateBounds();
+ var prevCenter = horizontal
+ ? sorted[i - 1].BoundingBox.Center.X
+ : sorted[i - 1].BoundingBox.Center.Y;
+ var currCenter = horizontal
+ ? sorted[i].BoundingBox.Center.X
+ : sorted[i].BoundingBox.Center.Y;
- if (!angle.IsEqualTo(0))
- clone.Rotate(angle, center);
-
- pattern.Parts.Add(clone);
- }
-
- pattern.UpdateBounds();
- return pattern;
- }
-
- private List FillPattern(FillLinear engine, List groupParts, List angles, Box workArea)
- {
- List best = null;
- var bestScore = default(FillScore);
-
- foreach (var angle in angles)
- {
- var pattern = BuildRotatedPattern(groupParts, angle);
-
- if (pattern.Parts.Count == 0)
- continue;
-
- var h = engine.Fill(pattern, NestDirection.Horizontal);
- var scoreH = h != null && h.Count > 0 ? FillScore.Compute(h, workArea) : default;
-
- if (scoreH.Count > 0 && (best == null || scoreH > bestScore))
+ if (currCenter - prevCenter > gapThreshold)
{
- best = h;
- bestScore = scoreH;
+ clusters.Add(current);
+ current = new List();
}
- var v = engine.Fill(pattern, NestDirection.Vertical);
- var scoreV = v != null && v.Count > 0 ? FillScore.Compute(v, workArea) : default;
+ current.Add(sorted[i]);
+ }
- if (scoreV.Count > 0 && (best == null || scoreV > bestScore))
+ clusters.Add(current);
+ return clusters;
+ }
+
+ // --- Scoring / comparison ---
+
+ private bool IsBetterFill(List candidate, List current, Box workArea)
+ {
+ if (candidate == null || candidate.Count == 0)
+ return false;
+
+ if (current == null || current.Count == 0)
+ return true;
+
+ return FillScore.Compute(candidate, workArea) > FillScore.Compute(current, workArea);
+ }
+
+ private bool IsBetterValidFill(List candidate, List current, Box workArea)
+ {
+ if (candidate != null && candidate.Count > 0 && HasOverlaps(candidate, Plate.PartSpacing))
+ {
+ Debug.WriteLine($"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})");
+ return false;
+ }
+
+ return IsBetterFill(candidate, current, workArea);
+ }
+
+ private bool HasOverlaps(List parts, double spacing)
+ {
+ if (parts == null || parts.Count <= 1)
+ return false;
+
+ for (var i = 0; i < parts.Count; i++)
+ {
+ var box1 = parts[i].BoundingBox;
+
+ for (var j = i + 1; j < parts.Count; j++)
{
- best = v;
- bestScore = scoreV;
+ var box2 = parts[j].BoundingBox;
+
+ // Fast bounding box rejection.
+ if (box1.Right < box2.Left || box2.Right < box1.Left ||
+ box1.Top < box2.Bottom || box2.Top < box1.Bottom)
+ continue;
+
+ List pts;
+
+ if (parts[i].Intersects(parts[j], out pts))
+ {
+ var b1 = parts[i].BoundingBox;
+ var b2 = parts[j].BoundingBox;
+ Debug.WriteLine($"[HasOverlaps] Overlap: part[{i}] ({parts[i].BaseDrawing?.Name}) @ ({b1.Left:F2},{b1.Bottom:F2})-({b1.Right:F2},{b1.Top:F2}) rot={parts[i].Rotation:F2}" +
+ $" vs part[{j}] ({parts[j].BaseDrawing?.Name}) @ ({b2.Left:F2},{b2.Bottom:F2})-({b2.Right:F2},{b2.Top:F2}) rot={parts[j].Rotation:F2}" +
+ $" intersections={pts?.Count ?? 0}");
+ return true;
+ }
}
}
- return best;
+ return false;
}
+ // --- Progress reporting ---
+
private static void ReportProgress(
IProgress progress,
NestPhase phase,
int plateNumber,
List best,
- Box workArea)
+ Box workArea,
+ string description)
{
if (progress == null || best == null || best.Count == 0)
return;
@@ -873,227 +737,36 @@ namespace OpenNest
BestDensity = score.Density,
UsableRemnantArea = workArea.Area() - totalPartArea,
BestParts = clonedParts,
- Description = null
+ Description = description
});
}
- ///
- /// Mixed-part geometry-aware nesting using NFP-based collision avoidance
- /// and simulated annealing optimization.
- ///
- public List AutoNest(List items, CancellationToken cancellation = default)
+ private string BuildProgressSummary()
{
- return AutoNest(items, Plate, cancellation);
- }
-
- ///
- /// Mixed-part geometry-aware nesting using NFP-based collision avoidance
- /// and simulated annealing optimization.
- ///
- public static List AutoNest(List items, Plate plate,
- CancellationToken cancellation = default)
- {
- var workArea = plate.WorkArea();
- var halfSpacing = plate.PartSpacing / 2.0;
- var nfpCache = new NfpCache();
- var candidateRotations = new Dictionary>();
-
- // Extract perimeter polygons for each unique drawing.
- foreach (var item in items)
- {
- var drawing = item.Drawing;
-
- if (candidateRotations.ContainsKey(drawing.Id))
- continue;
-
- var perimeterPolygon = ExtractPerimeterPolygon(drawing, halfSpacing);
-
- if (perimeterPolygon == null)
- {
- Debug.WriteLine($"[AutoNest] Skipping drawing '{drawing.Name}': no valid perimeter");
- continue;
- }
-
- // Compute candidate rotations for this drawing.
- var rotations = ComputeCandidateRotations(item, perimeterPolygon, workArea);
- candidateRotations[drawing.Id] = rotations;
-
- // Register polygons at each candidate rotation.
- foreach (var rotation in rotations)
- {
- var rotatedPolygon = RotatePolygon(perimeterPolygon, rotation);
- nfpCache.RegisterPolygon(drawing.Id, rotation, rotatedPolygon);
- }
- }
-
- if (candidateRotations.Count == 0)
- return new List();
-
- // Pre-compute all NFPs.
- nfpCache.PreComputeAll();
-
- Debug.WriteLine($"[AutoNest] NFP cache: {nfpCache.Count} entries for {candidateRotations.Count} drawings");
-
- // Run simulated annealing optimizer.
- var optimizer = new SimulatedAnnealing();
- var result = optimizer.Optimize(items, workArea, nfpCache, candidateRotations, cancellation);
-
- if (result.Sequence == null || result.Sequence.Count == 0)
- return new List();
-
- // Final BLF placement with the best solution.
- var blf = new BottomLeftFill(workArea, nfpCache);
- var placedParts = blf.Fill(result.Sequence);
- var parts = BottomLeftFill.ToNestParts(placedParts);
-
- Debug.WriteLine($"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations");
-
- return parts;
- }
-
- ///
- /// Extracts the perimeter polygon from a drawing, inflated by half-spacing.
- ///
- private static Polygon ExtractPerimeterPolygon(Drawing drawing, double halfSpacing)
- {
- var entities = ConvertProgram.ToGeometry(drawing.Program)
- .Where(e => e.Layer != SpecialLayers.Rapid)
- .ToList();
-
- if (entities.Count == 0)
+ if (PhaseResults.Count == 0)
return null;
- var definedShape = new ShapeProfile(entities);
- var perimeter = definedShape.Perimeter;
+ var parts = new List(PhaseResults.Count);
- if (perimeter == null)
- return null;
+ foreach (var r in PhaseResults)
+ parts.Add($"{FormatPhaseName(r.Phase)}: {r.PartCount}");
- // Inflate by half-spacing if spacing is non-zero.
- Shape inflated;
-
- if (halfSpacing > 0)
- {
- var offsetEntity = perimeter.OffsetEntity(halfSpacing, OffsetSide.Right);
- inflated = offsetEntity as Shape ?? perimeter;
- }
- else
- {
- inflated = perimeter;
- }
-
- // Convert to polygon with circumscribed arcs for tight nesting.
- var polygon = inflated.ToPolygonWithTolerance(0.01, circumscribe: true);
-
- if (polygon.Vertices.Count < 3)
- return null;
-
- // Normalize: move reference point to origin.
- polygon.UpdateBounds();
- var bb = polygon.BoundingBox;
- polygon.Offset(-bb.Left, -bb.Bottom);
-
- return polygon;
+ return string.Join(" | ", parts);
}
- ///
- /// Computes candidate rotation angles for a drawing.
- ///
- private static List ComputeCandidateRotations(NestItem item,
- Polygon perimeterPolygon, Box workArea)
+ private static string FormatPhaseName(NestPhase phase)
{
- var rotations = new List { 0 };
-
- // Add hull-edge angles from the polygon itself.
- var hullAngles = ComputeHullEdgeAngles(perimeterPolygon);
-
- foreach (var angle in hullAngles)
+ switch (phase)
{
- if (!rotations.Any(r => r.IsEqualTo(angle)))
- rotations.Add(angle);
+ case NestPhase.Pairs: return "Pairs";
+ case NestPhase.Linear: return "Linear";
+ case NestPhase.RectBestFit: return "BestFit";
+ case NestPhase.Remainder: return "Remainder";
+ default: return phase.ToString();
}
-
- // Add 90-degree rotation.
- if (!rotations.Any(r => r.IsEqualTo(Angle.HalfPI)))
- rotations.Add(Angle.HalfPI);
-
- // For narrow work areas, add sweep angles.
- var partBounds = perimeterPolygon.BoundingBox;
- var partLongest = System.Math.Max(partBounds.Width, partBounds.Length);
- var workShort = System.Math.Min(workArea.Width, workArea.Length);
-
- if (workShort < partLongest)
- {
- var step = Angle.ToRadians(5);
-
- for (var a = 0.0; a < System.Math.PI; a += step)
- {
- if (!rotations.Any(r => r.IsEqualTo(a)))
- rotations.Add(a);
- }
- }
-
- return rotations;
}
- ///
- /// Computes convex hull edge angles from a polygon for candidate rotations.
- ///
- private static List ComputeHullEdgeAngles(Polygon polygon)
- {
- var angles = new List();
-
- if (polygon.Vertices.Count < 3)
- return angles;
-
- var hull = ConvexHull.Compute(polygon.Vertices);
- var verts = hull.Vertices;
- var n = hull.IsClosed() ? verts.Count - 1 : verts.Count;
-
- for (var i = 0; i < n; i++)
- {
- var next = (i + 1) % n;
- var dx = verts[next].X - verts[i].X;
- var dy = verts[next].Y - verts[i].Y;
-
- if (dx * dx + dy * dy < Tolerance.Epsilon)
- continue;
-
- var angle = -System.Math.Atan2(dy, dx);
-
- if (!angles.Any(a => a.IsEqualTo(angle)))
- angles.Add(angle);
- }
-
- return angles;
- }
-
- ///
- /// Creates a rotated copy of a polygon around the origin.
- ///
- private static Polygon RotatePolygon(Polygon polygon, double angle)
- {
- if (angle.IsEqualTo(0))
- return polygon;
-
- var result = new Polygon();
- var cos = System.Math.Cos(angle);
- var sin = System.Math.Sin(angle);
-
- foreach (var v in polygon.Vertices)
- {
- result.Vertices.Add(new Vector(
- v.X * cos - v.Y * sin,
- v.X * sin + v.Y * cos));
- }
-
- // Re-normalize to origin.
- result.UpdateBounds();
- var bb = result.BoundingBox;
- result.Offset(-bb.Left, -bb.Bottom);
-
- return result;
- }
+ // --- Utilities ---
private static void InterlockedMax(ref int location, int value)
{
diff --git a/OpenNest.Mcp/Tools/NestingTools.cs b/OpenNest.Mcp/Tools/NestingTools.cs
index cdd96ce..99d8aef 100644
--- a/OpenNest.Mcp/Tools/NestingTools.cs
+++ b/OpenNest.Mcp/Tools/NestingTools.cs
@@ -233,7 +233,7 @@ namespace OpenNest.Mcp.Tools
items.Add(new NestItem { Drawing = drawing, Quantity = qtys[i] });
}
- var parts = NestEngine.AutoNest(items, plate);
+ var parts = AutoNester.Nest(items, plate);
plate.Parts.AddRange(parts);
var sb = new StringBuilder();
diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs
index 6f526e0..0ef8fac 100644
--- a/OpenNest/Forms/MainForm.cs
+++ b/OpenNest/Forms/MainForm.cs
@@ -762,7 +762,7 @@ namespace OpenNest.Forms
activeForm.LoadLastPlate();
var parts = await Task.Run(() =>
- NestEngine.AutoNest(remaining, plate, token));
+ AutoNester.Nest(remaining, plate, token));
if (parts.Count == 0)
break;
@@ -862,7 +862,9 @@ namespace OpenNest.Forms
var progress = new Progress(p =>
{
progressForm.UpdateProgress(p);
- activeForm.PlateView.SetTemporaryParts(p.BestParts);
+
+ if (p.BestParts != null)
+ activeForm.PlateView.SetTemporaryParts(p.BestParts);
});
progressForm.Show(this);
@@ -922,7 +924,9 @@ namespace OpenNest.Forms
var progress = new Progress(p =>
{
progressForm.UpdateProgress(p);
- activeForm.PlateView.SetTemporaryParts(p.BestParts);
+
+ if (p.BestParts != null)
+ activeForm.PlateView.SetTemporaryParts(p.BestParts);
});
Action> onComplete = parts =>