diff --git a/OpenNest.Engine/NestEngineRegistry.cs b/OpenNest.Engine/NestEngineRegistry.cs index 82973ea..2a7e9da 100644 --- a/OpenNest.Engine/NestEngineRegistry.cs +++ b/OpenNest.Engine/NestEngineRegistry.cs @@ -20,6 +20,10 @@ namespace OpenNest Register("Strip", "Strip-based nesting for mixed-drawing layouts", plate => new StripNestEngine(plate)); + + Register("NFP", + "NFP-based mixed-part nesting with simulated annealing", + plate => new NfpNestEngine(plate)); } public static IReadOnlyList AvailableEngines => engines; diff --git a/OpenNest.Engine/Nfp/AutoNester.cs b/OpenNest.Engine/Nfp/AutoNester.cs index 55d1d38..a439c5a 100644 --- a/OpenNest.Engine/Nfp/AutoNester.cs +++ b/OpenNest.Engine/Nfp/AutoNester.cs @@ -1,8 +1,10 @@ using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; +using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; @@ -15,6 +17,7 @@ namespace OpenNest.Engine.Nfp public static class AutoNester { public static List Nest(List items, Plate plate, + IProgress progress = null, CancellationToken cancellation = default) { var workArea = plate.WorkArea(); @@ -60,7 +63,7 @@ namespace OpenNest.Engine.Nfp // Run simulated annealing optimizer. var optimizer = new SimulatedAnnealing(); - var result = optimizer.Optimize(items, workArea, nfpCache, candidateRotations, cancellation); + var result = optimizer.Optimize(items, workArea, nfpCache, candidateRotations, progress, cancellation); if (result.Sequence == null || result.Sequence.Count == 0) return new List(); @@ -72,9 +75,129 @@ namespace OpenNest.Engine.Nfp Debug.WriteLine($"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations"); + NestEngineBase.ReportProgress(progress, NestPhase.Nfp, 0, parts, workArea, + $"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. /// @@ -98,7 +221,7 @@ namespace OpenNest.Engine.Nfp if (halfSpacing > 0) { - var offsetEntity = perimeter.OffsetEntity(halfSpacing, OffsetSide.Right); + var offsetEntity = perimeter.OffsetEntity(halfSpacing, OffsetSide.Left); inflated = offsetEntity as Shape ?? perimeter; } else diff --git a/OpenNest.Engine/NfpNestEngine.cs b/OpenNest.Engine/NfpNestEngine.cs new file mode 100644 index 0000000..9617820 --- /dev/null +++ b/OpenNest.Engine/NfpNestEngine.cs @@ -0,0 +1,68 @@ +using OpenNest.Engine.Fill; +using OpenNest.Engine.Nfp; +using OpenNest.Geometry; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace OpenNest +{ + public class NfpNestEngine : NestEngineBase + { + public NfpNestEngine(Plate plate) : base(plate) + { + } + + public override string Name => "NFP"; + + public override string Description => "NFP-based mixed-part nesting with simulated annealing"; + + public override List Fill(NestItem item, Box workArea, + IProgress progress, CancellationToken token) + { + var inner = new DefaultNestEngine(Plate); + return inner.Fill(item, workArea, progress, token); + } + + public override List Fill(List groupParts, Box workArea, + IProgress progress, CancellationToken token) + { + var inner = new DefaultNestEngine(Plate); + return inner.Fill(groupParts, workArea, progress, token); + } + + public override List PackArea(Box box, List items, + IProgress progress, CancellationToken token) + { + var inner = new DefaultNestEngine(Plate); + return inner.PackArea(box, items, progress, token); + } + + public override List Nest(List items, + IProgress progress, CancellationToken token) + { + if (items == null || items.Count == 0) + return new List(); + + var parts = AutoNester.Nest(items, Plate, progress, token); + + // Compact placed parts toward the origin to close gaps. + Compactor.Settle(parts, Plate.WorkArea(), Plate.PartSpacing); + + // NFP optimization pass — re-place parts using geometry-aware BLF. + parts = AutoNester.Optimize(parts, Plate); + + // Deduct placed quantities from original items. + foreach (var item in items) + { + if (item.Quantity <= 0) + continue; + + var placed = parts.FindAll(p => p.BaseDrawing.Name == item.Drawing.Name).Count; + item.Quantity = System.Math.Max(0, item.Quantity - placed); + } + + return parts; + } + } +}