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);
+ }
}
}