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)