From c18259a3487e689abaf08cc75b13e2cd16fe793a Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 13 Mar 2026 10:21:26 -0400 Subject: [PATCH 1/5] feat(engine): add Nfp to NestPhase enum Co-Authored-By: Claude Opus 4.6 --- OpenNest.Engine/NestProgress.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/OpenNest.Engine/NestProgress.cs b/OpenNest.Engine/NestProgress.cs index 90a3fc9..97b8bc5 100644 --- a/OpenNest.Engine/NestProgress.cs +++ b/OpenNest.Engine/NestProgress.cs @@ -7,6 +7,7 @@ namespace OpenNest Linear, RectBestFit, Pairs, + Nfp, Remainder } From bbc3466bc8db80c1c387ec3ad3091f9ffa9e4465 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 13 Mar 2026 10:22:07 -0400 Subject: [PATCH 2/5] feat(engine): add FillNfpBestFit method for NFP-based single-drawing fill Co-Authored-By: Claude Opus 4.6 --- OpenNest.Engine/NestEngine.cs | 82 +++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index ea5d92a..9032353 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -473,6 +473,88 @@ namespace OpenNest return top; } + private List FillNfpBestFit(NestItem item, Box workArea) + { + var halfSpacing = Plate.PartSpacing / 2.0; + var drawing = item.Drawing; + + // Extract offset perimeter polygon. + var polygon = ExtractPerimeterPolygon(drawing, halfSpacing); + + if (polygon == null) + return new List(); + + // Rectangularity gate: skip if bounding-box fill ratio > 0.95. + var polyArea = polygon.Area(); + var bboxArea = polygon.BoundingBox.Area(); + + if (bboxArea > 0 && polyArea / bboxArea > 0.95) + return new List(); + + // Compute candidate rotations and filter by rotation constraints. + var rotations = ComputeCandidateRotations(item, polygon, workArea); + + if (item.RotationStart != 0 || item.RotationEnd != 0) + { + rotations = rotations + .Where(a => a >= item.RotationStart && a <= item.RotationEnd) + .ToList(); + } + + if (rotations.Count == 0) + return new List(); + + // Build NFP cache with all rotation variants of this single drawing. + var nfpCache = new NfpCache(); + + foreach (var rotation in rotations) + { + var rotatedPolygon = RotatePolygon(polygon, rotation); + nfpCache.RegisterPolygon(drawing.Id, rotation, rotatedPolygon); + } + + nfpCache.PreComputeAll(); + + // Estimate max copies that could fit. + var maxN = (int)(workArea.Area() / polyArea); + maxN = System.Math.Min(maxN, 500); + + if (item.Quantity > 0) + maxN = System.Math.Min(maxN, item.Quantity); + + if (maxN <= 0) + return new List(); + + // Try each rotation and keep the best BLF result. + List bestParts = null; + var bestScore = default(FillScore); + + foreach (var rotation in rotations) + { + var sequence = new List<(int drawingId, double rotation, Drawing drawing)>(); + + for (var i = 0; i < maxN; i++) + sequence.Add((drawing.Id, rotation, drawing)); + + var blf = new BottomLeftFill(workArea, nfpCache); + var placedParts = blf.Fill(sequence); + + if (placedParts.Count == 0) + continue; + + var parts = BottomLeftFill.ToNestParts(placedParts); + var score = FillScore.Compute(parts, workArea); + + if (bestParts == null || score > bestScore) + { + bestParts = parts; + bestScore = score; + } + } + + return bestParts ?? new List(); + } + private bool HasOverlaps(List parts, double spacing) { if (parts == null || parts.Count <= 1) From c4d09f246617d2ffb892492236eb1669cbd379db Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 13 Mar 2026 10:22:39 -0400 Subject: [PATCH 3/5] feat(engine): integrate NFP phase into FindBestFill (sync overload) Co-Authored-By: Claude Opus 4.6 --- OpenNest.Engine/NestEngine.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index 9032353..a9264cc 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -151,6 +151,13 @@ namespace OpenNest if (IsBetterFill(pairResult, best, workArea)) best = pairResult; + // NFP phase (non-rectangular parts only) + var nfpResult = FillNfpBestFit(item, workArea); + Debug.WriteLine($"[FindBestFill] NFP: {nfpResult?.Count ?? 0} parts"); + + if (IsBetterFill(nfpResult, best, workArea)) + best = nfpResult; + return best; } From 56c9b17ff65bc0111b6da919678a0de056785f78 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 13 Mar 2026 10:23:05 -0400 Subject: [PATCH 4/5] feat(engine): integrate NFP phase into FindBestFill (async overload) Co-Authored-By: Claude Opus 4.6 --- OpenNest.Engine/NestEngine.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index a9264cc..2d3e649 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -245,6 +245,18 @@ namespace OpenNest best = pairResult; ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea); } + + token.ThrowIfCancellationRequested(); + + // NFP phase (non-rectangular parts only) + var nfpResult = FillNfpBestFit(item, workArea); + Debug.WriteLine($"[FindBestFill] NFP: {nfpResult?.Count ?? 0} parts"); + + if (IsBetterFill(nfpResult, best, workArea)) + { + best = nfpResult; + ReportProgress(progress, NestPhase.Nfp, PlateNumber, best, workArea); + } } catch (OperationCanceledException) { From 0c98b240c3e0aff6624385ffab8f4cc4af3580cd Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 13 Mar 2026 10:23:39 -0400 Subject: [PATCH 5/5] feat(engine): integrate NFP phase into Fill(groupParts) single-drawing path Co-Authored-By: Claude Opus 4.6 --- OpenNest.Engine/NestEngine.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index 2d3e649..8364c70 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -320,6 +320,18 @@ namespace OpenNest ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea); } + token.ThrowIfCancellationRequested(); + + // NFP phase (non-rectangular parts only) + var nfpResult = FillNfpBestFit(nestItem, workArea); + Debug.WriteLine($"[Fill(groupParts,Box)] NFP: {nfpResult?.Count ?? 0} parts"); + + if (IsBetterFill(nfpResult, best, workArea)) + { + best = nfpResult; + ReportProgress(progress, NestPhase.Nfp, PlateNumber, best, workArea); + } + // Try improving by filling the remainder strip separately. var improved = TryRemainderImprovement(nestItem, workArea, best);