using OpenNest.Geometry; using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; namespace OpenNest.Engine.Nfp { /// /// 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, IProgress progress = null, 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, progress, 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"); NestEngineBase.ReportProgress(progress, new ProgressReport { Phase = NestPhase.Nfp, PlateNumber = 0, Parts = parts, WorkArea = workArea, Description = $"NFP: {parts.Count} parts, {result.Iterations} iterations", IsOverallBest = true, }); return parts; } /// /// Re-places already-positioned parts using NFP-based BLF. /// Returns the tighter layout if BLF improves density without losing parts. /// public static List Optimize(List parts, Plate plate) { return Optimize(parts, plate.WorkArea(), plate.PartSpacing); } /// /// Re-places already-positioned parts using NFP-based BLF within the given work area. /// Returns the tighter layout if BLF improves density without losing parts. /// public static List Optimize(List parts, Box workArea, double partSpacing) { if (parts == null || parts.Count < 2) return parts; var halfSpacing = partSpacing / 2.0; var nfpCache = new NfpCache(); var registeredRotations = new HashSet<(int id, double rotation)>(); // Extract polygons for each unique drawing+rotation used by the placed parts. foreach (var part in parts) { var drawing = part.BaseDrawing; var rotation = part.Rotation; var key = (drawing.Id, rotation); if (registeredRotations.Contains(key)) continue; var perimeterPolygon = ExtractPerimeterPolygon(drawing, halfSpacing); if (perimeterPolygon == null) continue; var rotatedPolygon = RotatePolygon(perimeterPolygon, rotation); nfpCache.RegisterPolygon(drawing.Id, rotation, rotatedPolygon); registeredRotations.Add(key); } if (registeredRotations.Count == 0) return parts; nfpCache.PreComputeAll(); // Build BLF sequence sorted by area descending (largest first packs best). var sequence = parts .OrderByDescending(p => p.BaseDrawing.Area) .Select(p => new SequenceEntry(p.BaseDrawing.Id, p.Rotation, p.BaseDrawing)) .ToList(); var blf = new BottomLeftFill(workArea, nfpCache); var placed = blf.Fill(sequence); var optimized = BottomLeftFill.ToNestParts(placed); // Only use the NFP result if it kept all parts and improved density. if (optimized.Count < parts.Count) { Debug.WriteLine($"[AutoNest.Optimize] Rejected: placed {optimized.Count}/{parts.Count} parts"); return parts; } // Reject if any part landed outside the work area. if (!AllPartsInBounds(optimized, workArea)) { Debug.WriteLine("[AutoNest.Optimize] Rejected: parts outside work area"); return parts; } var originalScore = Fill.FillScore.Compute(parts, workArea); var optimizedScore = Fill.FillScore.Compute(optimized, workArea); if (optimizedScore > originalScore) { Debug.WriteLine($"[AutoNest.Optimize] Improved: density {originalScore.Density:P1} -> {optimizedScore.Density:P1}"); return optimized; } Debug.WriteLine($"[AutoNest.Optimize] No improvement: {originalScore.Density:P1} >= {optimizedScore.Density:P1}"); return parts; } private static bool AllPartsInBounds(List parts, Box workArea) { var logPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"); var allInBounds = true; // Append to the log that BLF already started using var log = new StreamWriter(logPath, true); log.WriteLine($"\n[Bounds] workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}"); foreach (var part in parts) { var bb = part.BoundingBox; var outLeft = bb.Left < workArea.X - Tolerance.Epsilon; var outBottom = bb.Bottom < workArea.Y - Tolerance.Epsilon; var outRight = bb.Right > workArea.Right + Tolerance.Epsilon; var outTop = bb.Top > workArea.Top + Tolerance.Epsilon; var oob = outLeft || outBottom || outRight || outTop; if (oob) { log.WriteLine($"[Bounds] OOB DrawingId={part.BaseDrawing.Id} \"{part.BaseDrawing.Name}\" loc=({part.Location.X:F4},{part.Location.Y:F4}) rot={part.Rotation:F3} bb=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4}) violations: {(outLeft ? "LEFT " : "")}{(outBottom ? "BOTTOM " : "")}{(outRight ? "RIGHT " : "")}{(outTop ? "TOP " : "")}"); allInBounds = false; } } if (allInBounds) log.WriteLine($"[Bounds] All {parts.Count} parts in bounds."); return allInBounds; } /// /// Extracts the perimeter polygon from a drawing, inflated by half-spacing. /// private static Polygon ExtractPerimeterPolygon(Drawing drawing, double halfSpacing) { return BestFit.PolygonHelper.ExtractPerimeterPolygon(drawing, halfSpacing).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) { return BestFit.PolygonHelper.RotatePolygon(polygon, angle); } } }