using OpenNest.Geometry; using System; using System.Collections.Generic; using System.IO; using Clipper2Lib; namespace OpenNest.Engine.Nfp { /// /// NFP-based Bottom-Left Fill (BLF) placement engine. /// Places parts one at a time using feasible regions computed from /// the Inner-Fit Polygon minus the union of No-Fit Polygons. /// public class BottomLeftFill { private static readonly string DebugLogPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"); private readonly Box workArea; private readonly NfpCache nfpCache; public BottomLeftFill(Box workArea, NfpCache nfpCache) { this.workArea = workArea; this.nfpCache = nfpCache; } /// /// Places parts according to the given sequence using NFP-based BLF. /// Returns the list of successfully placed parts with their positions. /// public List Fill(List sequence) { var placedParts = new List(); using var log = new StreamWriter(DebugLogPath, false); log.WriteLine($"[BLF] {DateTime.Now:HH:mm:ss.fff} workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}"); log.WriteLine($"[BLF] Sequence count: {sequence.Count}"); foreach (var entry in sequence) { var ifp = nfpCache.GetIfp(entry.DrawingId, entry.Rotation, workArea); if (ifp.Vertices.Count < 3) { log.WriteLine($"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} SKIPPED (IFP has {ifp.Vertices.Count} verts)"); continue; } log.WriteLine($"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} IFP verts={ifp.Vertices.Count} bounds=({ifp.BoundingBox.X:F2},{ifp.BoundingBox.Y:F2},{ifp.BoundingBox.Width:F2},{ifp.BoundingBox.Length:F2})"); var nfpPaths = ComputeNfpPaths(placedParts, entry.DrawingId, entry.Rotation, ifp.BoundingBox); var feasible = InnerFitPolygon.ComputeFeasibleRegion(ifp, nfpPaths); var point = InnerFitPolygon.FindBottomLeftPoint(feasible); if (double.IsNaN(point.X)) { log.WriteLine($"[BLF] -> NO feasible point (NaN)"); continue; } // Clamp to IFP bounds to correct Clipper2 floating-point drift. var ifpBb = ifp.BoundingBox; point = new Vector( System.Math.Max(ifpBb.X, System.Math.Min(ifpBb.Right, point.X)), System.Math.Max(ifpBb.Y, System.Math.Min(ifpBb.Top, point.Y))); log.WriteLine($"[BLF] -> placed at ({point.X:F4}, {point.Y:F4}) nfpPaths={nfpPaths.Count} feasibleVerts={feasible.Vertices.Count}"); placedParts.Add(new PlacedPart { DrawingId = entry.DrawingId, Rotation = entry.Rotation, Position = point, Drawing = entry.Drawing }); } log.WriteLine($"[BLF] Total placed: {placedParts.Count}/{sequence.Count}"); return placedParts; } /// /// Converts placed parts to OpenNest Part instances positioned on the plate. /// public static List ToNestParts(List placedParts) { var parts = new List(placedParts.Count); foreach (var placed in placedParts) { var part = Part.CreateAtOrigin(placed.Drawing, placed.Rotation); // CreateAtOrigin sets Location to compensate for the rotated program's // bounding box offset. The BLF position is a displacement for the // origin-normalized polygon, so we ADD it to the existing Location // rather than replacing it. part.Location = part.Location + placed.Position; parts.Add(part); } return parts; } /// /// Computes NFPs for a candidate part against all already-placed parts, /// returned as Clipper paths with translations applied. /// Filters NFPs that don't intersect the target IFP. /// private PathsD ComputeNfpPaths(List placedParts, int drawingId, double rotation, Box ifpBounds) { var nfpPaths = new PathsD(placedParts.Count); for (var i = 0; i < placedParts.Count; i++) { var placed = placedParts[i]; var nfp = nfpCache.Get(placed.DrawingId, placed.Rotation, drawingId, rotation); if (nfp != null && nfp.Vertices.Count >= 3) { // Spatial pruning: only include NFPs that could actually subtract from the IFP. var nfpBounds = nfp.BoundingBox.Translate(placed.Position); if (nfpBounds.Intersects(ifpBounds)) { nfpPaths.Add(NoFitPolygon.ToClipperPath(nfp, placed.Position)); } } } return nfpPaths; } } }