diff --git a/OpenNest.Engine/Nfp/BottomLeftFill.cs b/OpenNest.Engine/Nfp/BottomLeftFill.cs index 48ecd1a..92d4b58 100644 --- a/OpenNest.Engine/Nfp/BottomLeftFill.cs +++ b/OpenNest.Engine/Nfp/BottomLeftFill.cs @@ -1,5 +1,8 @@ using OpenNest.Geometry; +using System; using System.Collections.Generic; +using System.IO; +using Clipper2Lib; namespace OpenNest.Engine.Nfp { @@ -10,6 +13,9 @@ namespace OpenNest.Engine.Nfp /// 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; @@ -21,55 +27,56 @@ namespace OpenNest.Engine.Nfp /// /// Places parts according to the given sequence using NFP-based BLF. - /// Each entry is (drawingId, rotation) determining what to place and how. /// Returns the list of successfully placed parts with their positions. /// - public List Fill(List<(int drawingId, double rotation, Drawing drawing)> sequence) + public List Fill(List sequence) { var placedParts = new List(); - foreach (var (drawingId, rotation, drawing) in sequence) + 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 polygon = nfpCache.GetPolygon(drawingId, rotation); - - if (polygon == null || polygon.Vertices.Count < 3) - continue; - - // Compute IFP for this part inside the work area. - var ifp = InnerFitPolygon.Compute(workArea, polygon); + var ifp = nfpCache.GetIfp(entry.DrawingId, entry.Rotation, workArea); if (ifp.Vertices.Count < 3) - continue; - - // Compute NFPs against all already-placed parts. - var nfps = new Polygon[placedParts.Count]; - - for (var i = 0; i < placedParts.Count; i++) { - var placed = placedParts[i]; - var nfp = nfpCache.Get(placed.DrawingId, placed.Rotation, drawingId, rotation); - - // Translate NFP to the placed part's position. - var translated = TranslatePolygon(nfp, placed.Position); - nfps[i] = translated; + log.WriteLine($"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} SKIPPED (IFP has {ifp.Vertices.Count} verts)"); + continue; } - // Compute feasible region and find bottom-left point. - var feasible = InnerFitPolygon.ComputeFeasibleRegion(ifp, nfps); + 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 = drawingId, - Rotation = rotation, + DrawingId = entry.DrawingId, + Rotation = entry.Rotation, Position = point, - Drawing = drawing + Drawing = entry.Drawing }); } + log.WriteLine($"[BLF] Total placed: {placedParts.Count}/{sequence.Count}"); return placedParts; } @@ -82,12 +89,12 @@ namespace OpenNest.Engine.Nfp foreach (var placed in placedParts) { - var part = new Part(placed.Drawing); - - if (placed.Rotation != 0) - part.Rotate(placed.Rotation); - - part.Location = placed.Position; + 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); } @@ -95,27 +102,31 @@ namespace OpenNest.Engine.Nfp } /// - /// Creates a translated copy of a polygon. + /// 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 static Polygon TranslatePolygon(Polygon polygon, Vector offset) + private PathsD ComputeNfpPaths(List placedParts, int drawingId, double rotation, Box ifpBounds) { - var result = new Polygon(); + var nfpPaths = new PathsD(placedParts.Count); - foreach (var v in polygon.Vertices) - result.Vertices.Add(new Vector(v.X + offset.X, v.Y + offset.Y)); + for (var i = 0; i < placedParts.Count; i++) + { + var placed = placedParts[i]; + var nfp = nfpCache.Get(placed.DrawingId, placed.Rotation, drawingId, rotation); - return result; + 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; } } - - /// - /// Represents a part that has been placed by the BLF algorithm. - /// - public class PlacedPart - { - public int DrawingId { get; set; } - public double Rotation { get; set; } - public Vector Position { get; set; } - public Drawing Drawing { get; set; } - } } diff --git a/OpenNest.Engine/Nfp/SimulatedAnnealing.cs b/OpenNest.Engine/Nfp/SimulatedAnnealing.cs index 45462b6..d84f1b8 100644 --- a/OpenNest.Engine/Nfp/SimulatedAnnealing.cs +++ b/OpenNest.Engine/Nfp/SimulatedAnnealing.cs @@ -20,11 +20,12 @@ namespace OpenNest.Engine.Nfp public OptimizationResult Optimize(List items, Box workArea, NfpCache cache, Dictionary> candidateRotations, + IProgress progress = null, CancellationToken cancellation = default) { var random = new Random(); - // Build initial sequence: expand NestItems into individual (drawingId, rotation, drawing) entries, + // Build initial sequence: expand NestItems into individual entries, // sorted by area descending. var sequence = BuildInitialSequence(items, candidateRotations); @@ -35,9 +36,9 @@ namespace OpenNest.Engine.Nfp var blf = new BottomLeftFill(workArea, cache); var bestPlaced = blf.Fill(sequence); var bestScore = FillScore.Compute(BottomLeftFill.ToNestParts(bestPlaced), workArea); - var bestSequence = new List<(int, double, Drawing)>(sequence); + var bestSequence = new List(sequence); - var currentSequence = new List<(int, double, Drawing)>(sequence); + var currentSequence = new List(sequence); var currentScore = bestScore; // Calibrate initial temperature so ~80% of worse moves are accepted. @@ -49,13 +50,16 @@ namespace OpenNest.Engine.Nfp Debug.WriteLine($"[SA] Initial: {bestScore.Count} parts, density={bestScore.Density:P1}, temp={initialTemp:F2}"); + ReportBest(progress, BottomLeftFill.ToNestParts(bestPlaced), workArea, + $"NFP: initial {bestScore.Count} parts, density={bestScore.Density:P1}"); + while (temperature > DefaultMinTemperature && noImprovement < DefaultMaxNoImprovement && !cancellation.IsCancellationRequested) { iteration++; - var candidate = new List<(int drawingId, double rotation, Drawing drawing)>(currentSequence); + var candidate = new List(currentSequence); Mutate(candidate, candidateRotations, random); var candidatePlaced = blf.Fill(candidate); @@ -72,10 +76,13 @@ namespace OpenNest.Engine.Nfp if (currentScore > bestScore) { bestScore = currentScore; - bestSequence = new List<(int, double, Drawing)>(currentSequence); + bestSequence = new List(currentSequence); noImprovement = 0; Debug.WriteLine($"[SA] New best at iter {iteration}: {bestScore.Count} parts, density={bestScore.Density:P1}"); + + ReportBest(progress, BottomLeftFill.ToNestParts(candidatePlaced), workArea, + $"NFP: iter {iteration}, {bestScore.Count} parts, density={bestScore.Density:P1}"); } else { @@ -118,10 +125,10 @@ namespace OpenNest.Engine.Nfp /// Builds the initial placement sequence sorted by drawing area descending. /// Each NestItem is expanded by its quantity. /// - private static List<(int drawingId, double rotation, Drawing drawing)> BuildInitialSequence( + private static List BuildInitialSequence( List items, Dictionary> candidateRotations) { - var sequence = new List<(int drawingId, double rotation, Drawing drawing)>(); + var sequence = new List(); // Sort items by area descending. var sorted = items.OrderByDescending(i => i.Drawing.Area).ToList(); @@ -135,7 +142,7 @@ namespace OpenNest.Engine.Nfp rotation = rotations[0]; for (var i = 0; i < qty; i++) - sequence.Add((item.Drawing.Id, rotation, item.Drawing)); + sequence.Add(new SequenceEntry(item.Drawing.Id, rotation, item.Drawing)); } return sequence; @@ -144,7 +151,7 @@ namespace OpenNest.Engine.Nfp /// /// Applies a random mutation to the sequence. /// - private static void Mutate(List<(int drawingId, double rotation, Drawing drawing)> sequence, + private static void Mutate(List sequence, Dictionary> candidateRotations, Random random) { if (sequence.Count < 2) @@ -169,7 +176,7 @@ namespace OpenNest.Engine.Nfp /// /// Swaps two random parts in the sequence. /// - private static void MutateSwap(List<(int, double, Drawing)> sequence, Random random) + private static void MutateSwap(List sequence, Random random) { var i = random.Next(sequence.Count); var j = random.Next(sequence.Count); @@ -183,23 +190,23 @@ namespace OpenNest.Engine.Nfp /// /// Changes a random part's rotation to another candidate angle. /// - private static void MutateRotate(List<(int drawingId, double rotation, Drawing drawing)> sequence, + private static void MutateRotate(List sequence, Dictionary> candidateRotations, Random random) { var idx = random.Next(sequence.Count); var entry = sequence[idx]; - if (!candidateRotations.TryGetValue(entry.drawingId, out var rotations) || rotations.Count <= 1) + if (!candidateRotations.TryGetValue(entry.DrawingId, out var rotations) || rotations.Count <= 1) return; var newRotation = rotations[random.Next(rotations.Count)]; - sequence[idx] = (entry.drawingId, newRotation, entry.drawing); + sequence[idx] = entry.WithRotation(newRotation); } /// /// Reverses a random contiguous subsequence. /// - private static void MutateReverse(List<(int, double, Drawing)> sequence, Random random) + private static void MutateReverse(List sequence, Random random) { var i = random.Next(sequence.Count); var j = random.Next(sequence.Count); @@ -221,7 +228,7 @@ namespace OpenNest.Engine.Nfp /// are accepted initially. /// private static double CalibrateTemperature( - List<(int drawingId, double rotation, Drawing drawing)> sequence, + List sequence, Box workArea, NfpCache cache, Dictionary> candidateRotations, Random random) { @@ -234,7 +241,7 @@ namespace OpenNest.Engine.Nfp for (var i = 0; i < samples; i++) { - var candidate = new List<(int, double, Drawing)>(sequence); + var candidate = new List(sequence); Mutate(candidate, candidateRotations, random); var placed = blf.Fill(candidate); @@ -266,5 +273,12 @@ namespace OpenNest.Engine.Nfp return countDiff * 10.0 + densityDiff; } + + private static void ReportBest(IProgress progress, List parts, + Box workArea, string description) + { + NestEngineBase.ReportProgress(progress, NestPhase.Nfp, 0, parts, workArea, + description, isOverallBest: true); + } } }