Compare commits
10 Commits
2ed02c2dae
...
2d1f2217e5
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d1f2217e5 | |||
| ae88c34361 | |||
| 708d895a04 | |||
| 884817c5f9 | |||
| cf1c5fe120 | |||
| a04586f7df | |||
| 069e966453 | |||
| d9d275b675 | |||
| 9411dd0fdd | |||
| facd07d7de |
@@ -74,6 +74,16 @@ namespace OpenNest.Geometry
|
|||||||
Location += voffset;
|
Location += voffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Box Translate(double x, double y)
|
||||||
|
{
|
||||||
|
return new Box(X + x, Y + y, Width, Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Box Translate(Vector offset)
|
||||||
|
{
|
||||||
|
return new Box(X + offset.X, Y + offset.Y, Width, Length);
|
||||||
|
}
|
||||||
|
|
||||||
public double Left
|
public double Left
|
||||||
{
|
{
|
||||||
get { return X; }
|
get { return X; }
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ namespace OpenNest.Geometry
|
|||||||
result.Vertices.Add(new Vector(ifpRight, ifpTop));
|
result.Vertices.Add(new Vector(ifpRight, ifpTop));
|
||||||
result.Vertices.Add(new Vector(ifpLeft, ifpTop));
|
result.Vertices.Add(new Vector(ifpLeft, ifpTop));
|
||||||
result.Close();
|
result.Close();
|
||||||
|
result.UpdateBounds();
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -62,36 +63,20 @@ namespace OpenNest.Geometry
|
|||||||
/// Returns the polygon representing valid placement positions, or an empty
|
/// Returns the polygon representing valid placement positions, or an empty
|
||||||
/// polygon if no valid position exists.
|
/// polygon if no valid position exists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps)
|
public static Polygon ComputeFeasibleRegion(Polygon ifp, PathsD nfpPaths)
|
||||||
{
|
{
|
||||||
if (ifp.Vertices.Count < 3)
|
if (ifp.Vertices.Count < 3)
|
||||||
return new Polygon();
|
return new Polygon();
|
||||||
|
|
||||||
if (nfps == null || nfps.Length == 0)
|
if (nfpPaths == null || nfpPaths.Count == 0)
|
||||||
return ifp;
|
return ifp;
|
||||||
|
|
||||||
var ifpPath = NoFitPolygon.ToClipperPath(ifp);
|
var ifpPath = NoFitPolygon.ToClipperPath(ifp);
|
||||||
var ifpPaths = new PathsD { ifpPath };
|
var ifpPaths = new PathsD { ifpPath };
|
||||||
|
|
||||||
// Union all NFPs.
|
// Subtract the NFPs from the IFP.
|
||||||
var nfpPaths = new PathsD();
|
// Clipper2 handles the implicit union of the clip paths.
|
||||||
|
var feasible = Clipper.Difference(ifpPaths, nfpPaths, FillRule.NonZero);
|
||||||
foreach (var nfp in nfps)
|
|
||||||
{
|
|
||||||
if (nfp.Vertices.Count >= 3)
|
|
||||||
{
|
|
||||||
var path = NoFitPolygon.ToClipperPath(nfp);
|
|
||||||
nfpPaths.Add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nfpPaths.Count == 0)
|
|
||||||
return ifp;
|
|
||||||
|
|
||||||
var nfpUnion = Clipper.Union(nfpPaths, FillRule.NonZero);
|
|
||||||
|
|
||||||
// Subtract the NFP union from the IFP.
|
|
||||||
var feasible = Clipper.Difference(ifpPaths, nfpUnion, FillRule.NonZero);
|
|
||||||
|
|
||||||
if (feasible.Count == 0)
|
if (feasible.Count == 0)
|
||||||
return new Polygon();
|
return new Polygon();
|
||||||
@@ -118,6 +103,25 @@ namespace OpenNest.Geometry
|
|||||||
return bestPath != null ? NoFitPolygon.FromClipperPath(bestPath) : new Polygon();
|
return bestPath != null ? NoFitPolygon.FromClipperPath(bestPath) : new Polygon();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the feasible region for placing a part given already-placed parts.
|
||||||
|
/// (Legacy overload for backward compatibility).
|
||||||
|
/// </summary>
|
||||||
|
public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps)
|
||||||
|
{
|
||||||
|
if (nfps == null || nfps.Length == 0)
|
||||||
|
return ifp;
|
||||||
|
|
||||||
|
var nfpPaths = new PathsD(nfps.Length);
|
||||||
|
foreach (var nfp in nfps)
|
||||||
|
{
|
||||||
|
if (nfp.Vertices.Count >= 3)
|
||||||
|
nfpPaths.Add(NoFitPolygon.ToClipperPath(nfp));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ComputeFeasibleRegion(ifp, nfpPaths);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds the bottom-left-most point on a polygon boundary.
|
/// Finds the bottom-left-most point on a polygon boundary.
|
||||||
/// "Bottom-left" means: minimize Y first, then minimize X.
|
/// "Bottom-left" means: minimize Y first, then minimize X.
|
||||||
|
|||||||
@@ -250,9 +250,9 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts an OpenNest Polygon to a Clipper2 PathD.
|
/// Converts an OpenNest Polygon to a Clipper2 PathD, with an optional offset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static PathD ToClipperPath(Polygon polygon)
|
public static PathD ToClipperPath(Polygon polygon, Vector offset = default)
|
||||||
{
|
{
|
||||||
var path = new PathD();
|
var path = new PathD();
|
||||||
var verts = polygon.Vertices;
|
var verts = polygon.Vertices;
|
||||||
@@ -263,7 +263,7 @@ namespace OpenNest.Geometry
|
|||||||
n--;
|
n--;
|
||||||
|
|
||||||
for (var i = 0; i < n; i++)
|
for (var i = 0; i < n; i++)
|
||||||
path.Add(new PointD(verts[i].X, verts[i].Y));
|
path.Add(new PointD(verts[i].X + offset.X, verts[i].Y + offset.Y));
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
@@ -271,7 +271,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a Clipper2 PathD to an OpenNest Polygon.
|
/// Converts a Clipper2 PathD to an OpenNest Polygon.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static Polygon FromClipperPath(PathD path)
|
public static Polygon FromClipperPath(PathD path)
|
||||||
{
|
{
|
||||||
var polygon = new Polygon();
|
var polygon = new Polygon();
|
||||||
|
|
||||||
@@ -279,6 +279,7 @@ namespace OpenNest.Geometry
|
|||||||
polygon.Vertices.Add(new Vector(pt.x, pt.y));
|
polygon.Vertices.Add(new Vector(pt.x, pt.y));
|
||||||
|
|
||||||
polygon.Close();
|
polygon.Close();
|
||||||
|
polygon.UpdateBounds();
|
||||||
return polygon;
|
return polygon;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -67,6 +68,15 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
var trueArea = drawing.Area * 2;
|
var trueArea = drawing.Area * 2;
|
||||||
|
|
||||||
|
// Normalize to landscape (width >= height) for consistent display.
|
||||||
|
if (bestHeight > bestWidth)
|
||||||
|
{
|
||||||
|
var tmp = bestWidth;
|
||||||
|
bestWidth = bestHeight;
|
||||||
|
bestHeight = tmp;
|
||||||
|
bestRotation += Angle.HalfPI;
|
||||||
|
}
|
||||||
|
|
||||||
return new BestFitResult
|
return new BestFitResult
|
||||||
{
|
{
|
||||||
Candidate = candidate,
|
Candidate = candidate,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
|
|
||||||
var angles = new List<double>(baseAngles);
|
var angles = new List<double>(baseAngles);
|
||||||
|
|
||||||
if (NeedsSweep(item, bestRotation, workArea))
|
if (ForceFullSweep)
|
||||||
AddSweepAngles(angles);
|
AddSweepAngles(angles);
|
||||||
|
|
||||||
if (!ForceFullSweep && angles.Count > 2)
|
if (!ForceFullSweep && angles.Count > 2)
|
||||||
@@ -36,18 +36,6 @@ namespace OpenNest.Engine.Fill
|
|||||||
return angles;
|
return angles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool NeedsSweep(NestItem item, double bestRotation, Box workArea)
|
|
||||||
{
|
|
||||||
var testPart = new Part(item.Drawing);
|
|
||||||
if (!bestRotation.IsEqualTo(0))
|
|
||||||
testPart.Rotate(bestRotation);
|
|
||||||
testPart.UpdateBounds();
|
|
||||||
|
|
||||||
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
|
|
||||||
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
|
||||||
return workAreaShortSide < partLongestSide || ForceFullSweep;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddSweepAngles(List<double> angles)
|
private static void AddSweepAngles(List<double> angles)
|
||||||
{
|
{
|
||||||
var step = Angle.ToRadians(5);
|
var step = Angle.ToRadians(5);
|
||||||
|
|||||||
@@ -174,5 +174,28 @@ namespace OpenNest.Engine.Fill
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repeatedly pushes parts left then down until total movement per
|
||||||
|
/// iteration falls below the given threshold.
|
||||||
|
/// </summary>
|
||||||
|
public static void Settle(List<Part> parts, Box workArea, double partSpacing,
|
||||||
|
double threshold = 0.01, int maxIterations = 20)
|
||||||
|
{
|
||||||
|
if (parts.Count < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var noObstacles = new List<Part>();
|
||||||
|
|
||||||
|
for (var i = 0; i < maxIterations; i++)
|
||||||
|
{
|
||||||
|
var moved = 0.0;
|
||||||
|
moved += Push(parts, noObstacles, workArea, partSpacing, PushDirection.Left);
|
||||||
|
moved += Push(parts, noObstacles, workArea, partSpacing, PushDirection.Down);
|
||||||
|
|
||||||
|
if (moved < threshold)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,35 +168,30 @@ namespace OpenNest.Engine.Fill
|
|||||||
private List<BestFitResult> SelectPairCandidates(List<BestFitResult> bestFits, Box workArea)
|
private List<BestFitResult> SelectPairCandidates(List<BestFitResult> bestFits, Box workArea)
|
||||||
{
|
{
|
||||||
var kept = bestFits.Where(r => r.Keep).ToList();
|
var kept = bestFits.Where(r => r.Keep).ToList();
|
||||||
var top = kept.Take(MaxTopCandidates).ToList();
|
|
||||||
|
|
||||||
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
||||||
var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length);
|
var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length);
|
||||||
|
|
||||||
if (workShortSide < plateShortSide * 0.5)
|
if (workShortSide < plateShortSide * 0.5)
|
||||||
{
|
{
|
||||||
var stripCandidates = bestFits
|
// Strip mode: prioritize candidates that fit the narrow dimension.
|
||||||
|
var stripCandidates = kept
|
||||||
.Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon
|
.Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon
|
||||||
&& r.Utilization >= MinStripUtilization)
|
&& r.Utilization >= MinStripUtilization)
|
||||||
.OrderByDescending(r => r.Utilization);
|
.ToList();
|
||||||
|
|
||||||
var existing = new HashSet<BestFitResult>(top);
|
SortByEstimatedCount(stripCandidates, workArea);
|
||||||
|
|
||||||
foreach (var r in stripCandidates)
|
var top = stripCandidates.Take(MaxStripCandidates).ToList();
|
||||||
{
|
|
||||||
if (top.Count >= MaxStripCandidates)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (existing.Add(r))
|
|
||||||
top.Add(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.WriteLine($"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})");
|
Debug.WriteLine($"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})");
|
||||||
|
return top;
|
||||||
}
|
}
|
||||||
|
|
||||||
SortByEstimatedCount(top, workArea);
|
var result = kept.Take(MaxTopCandidates).ToList();
|
||||||
|
SortByEstimatedCount(result, workArea);
|
||||||
|
|
||||||
return top;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SortByEstimatedCount(List<BestFitResult> candidates, Box workArea)
|
private void SortByEstimatedCount(List<BestFitResult> candidates, Box workArea)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Engine.Nfp;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -126,6 +127,12 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compact placed parts toward the origin to close gaps.
|
||||||
|
Compactor.Settle(allParts, Plate.WorkArea(), Plate.PartSpacing);
|
||||||
|
|
||||||
|
// NFP optimization pass — re-place parts using geometry-aware BLF.
|
||||||
|
allParts = AutoNester.Optimize(allParts, Plate);
|
||||||
|
|
||||||
return allParts;
|
return allParts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ namespace OpenNest
|
|||||||
Register("Strip",
|
Register("Strip",
|
||||||
"Strip-based nesting for mixed-drawing layouts",
|
"Strip-based nesting for mixed-drawing layouts",
|
||||||
plate => new StripNestEngine(plate));
|
plate => new StripNestEngine(plate));
|
||||||
|
|
||||||
|
Register("NFP",
|
||||||
|
"NFP-based mixed-part nesting with simulated annealing",
|
||||||
|
plate => new NfpNestEngine(plate));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyList<NestEngineInfo> AvailableEngines => engines;
|
public static IReadOnlyList<NestEngineInfo> AvailableEngines => engines;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ namespace OpenNest.Engine.Nfp
|
|||||||
public static class AutoNester
|
public static class AutoNester
|
||||||
{
|
{
|
||||||
public static List<Part> Nest(List<NestItem> items, Plate plate,
|
public static List<Part> Nest(List<NestItem> items, Plate plate,
|
||||||
|
IProgress<NestProgress> progress = null,
|
||||||
CancellationToken cancellation = default)
|
CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var workArea = plate.WorkArea();
|
var workArea = plate.WorkArea();
|
||||||
@@ -60,7 +63,7 @@ namespace OpenNest.Engine.Nfp
|
|||||||
|
|
||||||
// Run simulated annealing optimizer.
|
// Run simulated annealing optimizer.
|
||||||
var optimizer = new SimulatedAnnealing();
|
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)
|
if (result.Sequence == null || result.Sequence.Count == 0)
|
||||||
return new List<Part>();
|
return new List<Part>();
|
||||||
@@ -72,9 +75,129 @@ namespace OpenNest.Engine.Nfp
|
|||||||
|
|
||||||
Debug.WriteLine($"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations");
|
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;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-places already-positioned parts using NFP-based BLF.
|
||||||
|
/// Returns the tighter layout if BLF improves density without losing parts.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Part> Optimize(List<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
return Optimize(parts, plate.WorkArea(), plate.PartSpacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Part> Optimize(List<Part> 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<Part> 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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts the perimeter polygon from a drawing, inflated by half-spacing.
|
/// Extracts the perimeter polygon from a drawing, inflated by half-spacing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -98,7 +221,7 @@ namespace OpenNest.Engine.Nfp
|
|||||||
|
|
||||||
if (halfSpacing > 0)
|
if (halfSpacing > 0)
|
||||||
{
|
{
|
||||||
var offsetEntity = perimeter.OffsetEntity(halfSpacing, OffsetSide.Right);
|
var offsetEntity = perimeter.OffsetEntity(halfSpacing, OffsetSide.Left);
|
||||||
inflated = offsetEntity as Shape ?? perimeter;
|
inflated = offsetEntity as Shape ?? perimeter;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Clipper2Lib;
|
||||||
|
|
||||||
namespace OpenNest.Engine.Nfp
|
namespace OpenNest.Engine.Nfp
|
||||||
{
|
{
|
||||||
@@ -10,6 +13,9 @@ namespace OpenNest.Engine.Nfp
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BottomLeftFill
|
public class BottomLeftFill
|
||||||
{
|
{
|
||||||
|
private static readonly string DebugLogPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log");
|
||||||
|
|
||||||
private readonly Box workArea;
|
private readonly Box workArea;
|
||||||
private readonly NfpCache nfpCache;
|
private readonly NfpCache nfpCache;
|
||||||
|
|
||||||
@@ -21,55 +27,56 @@ namespace OpenNest.Engine.Nfp
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Places parts according to the given sequence using NFP-based BLF.
|
/// 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.
|
/// Returns the list of successfully placed parts with their positions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<PlacedPart> Fill(List<(int drawingId, double rotation, Drawing drawing)> sequence)
|
public List<PlacedPart> Fill(List<SequenceEntry> sequence)
|
||||||
{
|
{
|
||||||
var placedParts = new List<PlacedPart>();
|
var placedParts = new List<PlacedPart>();
|
||||||
|
|
||||||
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);
|
var ifp = nfpCache.GetIfp(entry.DrawingId, entry.Rotation, workArea);
|
||||||
|
|
||||||
if (polygon == null || polygon.Vertices.Count < 3)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Compute IFP for this part inside the work area.
|
|
||||||
var ifp = InnerFitPolygon.Compute(workArea, polygon);
|
|
||||||
|
|
||||||
if (ifp.Vertices.Count < 3)
|
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];
|
log.WriteLine($"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} SKIPPED (IFP has {ifp.Vertices.Count} verts)");
|
||||||
var nfp = nfpCache.Get(placed.DrawingId, placed.Rotation, drawingId, rotation);
|
continue;
|
||||||
|
|
||||||
// Translate NFP to the placed part's position.
|
|
||||||
var translated = TranslatePolygon(nfp, placed.Position);
|
|
||||||
nfps[i] = translated;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute feasible region and find bottom-left point.
|
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 feasible = InnerFitPolygon.ComputeFeasibleRegion(ifp, nfps);
|
|
||||||
|
var nfpPaths = ComputeNfpPaths(placedParts, entry.DrawingId, entry.Rotation, ifp.BoundingBox);
|
||||||
|
var feasible = InnerFitPolygon.ComputeFeasibleRegion(ifp, nfpPaths);
|
||||||
var point = InnerFitPolygon.FindBottomLeftPoint(feasible);
|
var point = InnerFitPolygon.FindBottomLeftPoint(feasible);
|
||||||
|
|
||||||
if (double.IsNaN(point.X))
|
if (double.IsNaN(point.X))
|
||||||
|
{
|
||||||
|
log.WriteLine($"[BLF] -> NO feasible point (NaN)");
|
||||||
continue;
|
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
|
placedParts.Add(new PlacedPart
|
||||||
{
|
{
|
||||||
DrawingId = drawingId,
|
DrawingId = entry.DrawingId,
|
||||||
Rotation = rotation,
|
Rotation = entry.Rotation,
|
||||||
Position = point,
|
Position = point,
|
||||||
Drawing = drawing
|
Drawing = entry.Drawing
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.WriteLine($"[BLF] Total placed: {placedParts.Count}/{sequence.Count}");
|
||||||
return placedParts;
|
return placedParts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,12 +89,12 @@ namespace OpenNest.Engine.Nfp
|
|||||||
|
|
||||||
foreach (var placed in placedParts)
|
foreach (var placed in placedParts)
|
||||||
{
|
{
|
||||||
var part = new Part(placed.Drawing);
|
var part = Part.CreateAtOrigin(placed.Drawing, placed.Rotation);
|
||||||
|
// CreateAtOrigin sets Location to compensate for the rotated program's
|
||||||
if (placed.Rotation != 0)
|
// bounding box offset. The BLF position is a displacement for the
|
||||||
part.Rotate(placed.Rotation);
|
// origin-normalized polygon, so we ADD it to the existing Location
|
||||||
|
// rather than replacing it.
|
||||||
part.Location = placed.Position;
|
part.Location = part.Location + placed.Position;
|
||||||
parts.Add(part);
|
parts.Add(part);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,27 +102,31 @@ namespace OpenNest.Engine.Nfp
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static Polygon TranslatePolygon(Polygon polygon, Vector offset)
|
private PathsD ComputeNfpPaths(List<PlacedPart> placedParts, int drawingId, double rotation, Box ifpBounds)
|
||||||
{
|
{
|
||||||
var result = new Polygon();
|
var nfpPaths = new PathsD(placedParts.Count);
|
||||||
|
|
||||||
foreach (var v in polygon.Vertices)
|
for (var i = 0; i < placedParts.Count; i++)
|
||||||
result.Vertices.Add(new Vector(v.X + offset.X, v.Y + offset.Y));
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return nfpPaths;
|
||||||
/// Represents a part that has been placed by the BLF algorithm.
|
}
|
||||||
/// </summary>
|
|
||||||
public class PlacedPart
|
|
||||||
{
|
|
||||||
public int DrawingId { get; set; }
|
|
||||||
public double Rotation { get; set; }
|
|
||||||
public Vector Position { get; set; }
|
|
||||||
public Drawing Drawing { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
@@ -11,9 +12,9 @@ namespace OpenNest.Engine.Nfp
|
|||||||
public class OptimizationResult
|
public class OptimizationResult
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The best sequence found: (drawingId, rotation, drawing) tuples in placement order.
|
/// The best placement sequence found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<(int drawingId, double rotation, Drawing drawing)> Sequence { get; set; }
|
public List<SequenceEntry> Sequence { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The score achieved by the best sequence.
|
/// The score achieved by the best sequence.
|
||||||
@@ -34,6 +35,7 @@ namespace OpenNest.Engine.Nfp
|
|||||||
{
|
{
|
||||||
OptimizationResult Optimize(List<NestItem> items, Box workArea, NfpCache cache,
|
OptimizationResult Optimize(List<NestItem> items, Box workArea, NfpCache cache,
|
||||||
Dictionary<int, List<double>> candidateRotations,
|
Dictionary<int, List<double>> candidateRotations,
|
||||||
|
IProgress<NestProgress> progress = null,
|
||||||
CancellationToken cancellation = default);
|
CancellationToken cancellation = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ namespace OpenNest.Engine.Nfp
|
|||||||
private readonly Dictionary<NfpKey, Polygon> cache = new Dictionary<NfpKey, Polygon>();
|
private readonly Dictionary<NfpKey, Polygon> cache = new Dictionary<NfpKey, Polygon>();
|
||||||
private readonly Dictionary<int, Dictionary<double, Polygon>> polygonCache
|
private readonly Dictionary<int, Dictionary<double, Polygon>> polygonCache
|
||||||
= new Dictionary<int, Dictionary<double, Polygon>>();
|
= new Dictionary<int, Dictionary<double, Polygon>>();
|
||||||
|
private readonly Dictionary<(int drawingId, double rotation), Polygon> ifpCache
|
||||||
|
= new Dictionary<(int drawingId, double rotation), Polygon>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers a pre-computed polygon for a drawing at a specific rotation.
|
/// Registers a pre-computed polygon for a drawing at a specific rotation.
|
||||||
@@ -28,6 +30,26 @@ namespace OpenNest.Engine.Nfp
|
|||||||
}
|
}
|
||||||
|
|
||||||
rotations[rotation] = polygon;
|
rotations[rotation] = polygon;
|
||||||
|
|
||||||
|
// Clear IFP cache if a polygon is updated (though usually they aren't).
|
||||||
|
ifpCache.Remove((drawingId, rotation));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or computes the IFP for a drawing at a specific rotation within a work area.
|
||||||
|
/// </summary>
|
||||||
|
public Polygon GetIfp(int drawingId, double rotation, Box workArea)
|
||||||
|
{
|
||||||
|
if (ifpCache.TryGetValue((drawingId, rotation), out var ifp))
|
||||||
|
return ifp;
|
||||||
|
|
||||||
|
var polygon = GetPolygon(drawingId, rotation);
|
||||||
|
if (polygon == null)
|
||||||
|
return new Polygon();
|
||||||
|
|
||||||
|
ifp = InnerFitPolygon.Compute(workArea, polygon);
|
||||||
|
ifpCache[(drawingId, rotation)] = ifp;
|
||||||
|
return ifp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Nfp
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a part that has been placed by the BLF algorithm.
|
||||||
|
/// </summary>
|
||||||
|
public class PlacedPart
|
||||||
|
{
|
||||||
|
public int DrawingId { get; set; }
|
||||||
|
public double Rotation { get; set; }
|
||||||
|
public Vector Position { get; set; }
|
||||||
|
public Drawing Drawing { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace OpenNest.Engine.Nfp
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An entry in a placement sequence — identifies which drawing to place and at what rotation.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct SequenceEntry
|
||||||
|
{
|
||||||
|
public int DrawingId { get; }
|
||||||
|
public double Rotation { get; }
|
||||||
|
public Drawing Drawing { get; }
|
||||||
|
|
||||||
|
public SequenceEntry(int drawingId, double rotation, Drawing drawing)
|
||||||
|
{
|
||||||
|
DrawingId = drawingId;
|
||||||
|
Rotation = rotation;
|
||||||
|
Drawing = drawing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SequenceEntry WithRotation(double rotation)
|
||||||
|
{
|
||||||
|
return new SequenceEntry(DrawingId, rotation, Drawing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,11 +20,12 @@ namespace OpenNest.Engine.Nfp
|
|||||||
|
|
||||||
public OptimizationResult Optimize(List<NestItem> items, Box workArea, NfpCache cache,
|
public OptimizationResult Optimize(List<NestItem> items, Box workArea, NfpCache cache,
|
||||||
Dictionary<int, List<double>> candidateRotations,
|
Dictionary<int, List<double>> candidateRotations,
|
||||||
|
IProgress<NestProgress> progress = null,
|
||||||
CancellationToken cancellation = default)
|
CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var random = new Random();
|
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.
|
// sorted by area descending.
|
||||||
var sequence = BuildInitialSequence(items, candidateRotations);
|
var sequence = BuildInitialSequence(items, candidateRotations);
|
||||||
|
|
||||||
@@ -35,9 +36,9 @@ namespace OpenNest.Engine.Nfp
|
|||||||
var blf = new BottomLeftFill(workArea, cache);
|
var blf = new BottomLeftFill(workArea, cache);
|
||||||
var bestPlaced = blf.Fill(sequence);
|
var bestPlaced = blf.Fill(sequence);
|
||||||
var bestScore = FillScore.Compute(BottomLeftFill.ToNestParts(bestPlaced), workArea);
|
var bestScore = FillScore.Compute(BottomLeftFill.ToNestParts(bestPlaced), workArea);
|
||||||
var bestSequence = new List<(int, double, Drawing)>(sequence);
|
var bestSequence = new List<SequenceEntry>(sequence);
|
||||||
|
|
||||||
var currentSequence = new List<(int, double, Drawing)>(sequence);
|
var currentSequence = new List<SequenceEntry>(sequence);
|
||||||
var currentScore = bestScore;
|
var currentScore = bestScore;
|
||||||
|
|
||||||
// Calibrate initial temperature so ~80% of worse moves are accepted.
|
// 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}");
|
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
|
while (temperature > DefaultMinTemperature
|
||||||
&& noImprovement < DefaultMaxNoImprovement
|
&& noImprovement < DefaultMaxNoImprovement
|
||||||
&& !cancellation.IsCancellationRequested)
|
&& !cancellation.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
iteration++;
|
iteration++;
|
||||||
|
|
||||||
var candidate = new List<(int drawingId, double rotation, Drawing drawing)>(currentSequence);
|
var candidate = new List<SequenceEntry>(currentSequence);
|
||||||
Mutate(candidate, candidateRotations, random);
|
Mutate(candidate, candidateRotations, random);
|
||||||
|
|
||||||
var candidatePlaced = blf.Fill(candidate);
|
var candidatePlaced = blf.Fill(candidate);
|
||||||
@@ -72,10 +76,13 @@ namespace OpenNest.Engine.Nfp
|
|||||||
if (currentScore > bestScore)
|
if (currentScore > bestScore)
|
||||||
{
|
{
|
||||||
bestScore = currentScore;
|
bestScore = currentScore;
|
||||||
bestSequence = new List<(int, double, Drawing)>(currentSequence);
|
bestSequence = new List<SequenceEntry>(currentSequence);
|
||||||
noImprovement = 0;
|
noImprovement = 0;
|
||||||
|
|
||||||
Debug.WriteLine($"[SA] New best at iter {iteration}: {bestScore.Count} parts, density={bestScore.Density:P1}");
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -118,10 +125,10 @@ namespace OpenNest.Engine.Nfp
|
|||||||
/// Builds the initial placement sequence sorted by drawing area descending.
|
/// Builds the initial placement sequence sorted by drawing area descending.
|
||||||
/// Each NestItem is expanded by its quantity.
|
/// Each NestItem is expanded by its quantity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static List<(int drawingId, double rotation, Drawing drawing)> BuildInitialSequence(
|
private static List<SequenceEntry> BuildInitialSequence(
|
||||||
List<NestItem> items, Dictionary<int, List<double>> candidateRotations)
|
List<NestItem> items, Dictionary<int, List<double>> candidateRotations)
|
||||||
{
|
{
|
||||||
var sequence = new List<(int drawingId, double rotation, Drawing drawing)>();
|
var sequence = new List<SequenceEntry>();
|
||||||
|
|
||||||
// Sort items by area descending.
|
// Sort items by area descending.
|
||||||
var sorted = items.OrderByDescending(i => i.Drawing.Area).ToList();
|
var sorted = items.OrderByDescending(i => i.Drawing.Area).ToList();
|
||||||
@@ -135,7 +142,7 @@ namespace OpenNest.Engine.Nfp
|
|||||||
rotation = rotations[0];
|
rotation = rotations[0];
|
||||||
|
|
||||||
for (var i = 0; i < qty; i++)
|
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;
|
return sequence;
|
||||||
@@ -144,7 +151,7 @@ namespace OpenNest.Engine.Nfp
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies a random mutation to the sequence.
|
/// Applies a random mutation to the sequence.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void Mutate(List<(int drawingId, double rotation, Drawing drawing)> sequence,
|
private static void Mutate(List<SequenceEntry> sequence,
|
||||||
Dictionary<int, List<double>> candidateRotations, Random random)
|
Dictionary<int, List<double>> candidateRotations, Random random)
|
||||||
{
|
{
|
||||||
if (sequence.Count < 2)
|
if (sequence.Count < 2)
|
||||||
@@ -169,7 +176,7 @@ namespace OpenNest.Engine.Nfp
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Swaps two random parts in the sequence.
|
/// Swaps two random parts in the sequence.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void MutateSwap(List<(int, double, Drawing)> sequence, Random random)
|
private static void MutateSwap(List<SequenceEntry> sequence, Random random)
|
||||||
{
|
{
|
||||||
var i = random.Next(sequence.Count);
|
var i = random.Next(sequence.Count);
|
||||||
var j = random.Next(sequence.Count);
|
var j = random.Next(sequence.Count);
|
||||||
@@ -183,23 +190,23 @@ namespace OpenNest.Engine.Nfp
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Changes a random part's rotation to another candidate angle.
|
/// Changes a random part's rotation to another candidate angle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void MutateRotate(List<(int drawingId, double rotation, Drawing drawing)> sequence,
|
private static void MutateRotate(List<SequenceEntry> sequence,
|
||||||
Dictionary<int, List<double>> candidateRotations, Random random)
|
Dictionary<int, List<double>> candidateRotations, Random random)
|
||||||
{
|
{
|
||||||
var idx = random.Next(sequence.Count);
|
var idx = random.Next(sequence.Count);
|
||||||
var entry = sequence[idx];
|
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;
|
return;
|
||||||
|
|
||||||
var newRotation = rotations[random.Next(rotations.Count)];
|
var newRotation = rotations[random.Next(rotations.Count)];
|
||||||
sequence[idx] = (entry.drawingId, newRotation, entry.drawing);
|
sequence[idx] = entry.WithRotation(newRotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reverses a random contiguous subsequence.
|
/// Reverses a random contiguous subsequence.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void MutateReverse(List<(int, double, Drawing)> sequence, Random random)
|
private static void MutateReverse(List<SequenceEntry> sequence, Random random)
|
||||||
{
|
{
|
||||||
var i = random.Next(sequence.Count);
|
var i = random.Next(sequence.Count);
|
||||||
var j = random.Next(sequence.Count);
|
var j = random.Next(sequence.Count);
|
||||||
@@ -221,7 +228,7 @@ namespace OpenNest.Engine.Nfp
|
|||||||
/// are accepted initially.
|
/// are accepted initially.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static double CalibrateTemperature(
|
private static double CalibrateTemperature(
|
||||||
List<(int drawingId, double rotation, Drawing drawing)> sequence,
|
List<SequenceEntry> sequence,
|
||||||
Box workArea, NfpCache cache,
|
Box workArea, NfpCache cache,
|
||||||
Dictionary<int, List<double>> candidateRotations, Random random)
|
Dictionary<int, List<double>> candidateRotations, Random random)
|
||||||
{
|
{
|
||||||
@@ -234,7 +241,7 @@ namespace OpenNest.Engine.Nfp
|
|||||||
|
|
||||||
for (var i = 0; i < samples; i++)
|
for (var i = 0; i < samples; i++)
|
||||||
{
|
{
|
||||||
var candidate = new List<(int, double, Drawing)>(sequence);
|
var candidate = new List<SequenceEntry>(sequence);
|
||||||
Mutate(candidate, candidateRotations, random);
|
Mutate(candidate, candidateRotations, random);
|
||||||
|
|
||||||
var placed = blf.Fill(candidate);
|
var placed = blf.Fill(candidate);
|
||||||
@@ -266,5 +273,12 @@ namespace OpenNest.Engine.Nfp
|
|||||||
|
|
||||||
return countDiff * 10.0 + densityDiff;
|
return countDiff * 10.0 + densityDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ReportBest(IProgress<NestProgress> progress, List<Part> parts,
|
||||||
|
Box workArea, string description)
|
||||||
|
{
|
||||||
|
NestEngineBase.ReportProgress(progress, NestPhase.Nfp, 0, parts, workArea,
|
||||||
|
description, isOverallBest: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Part> Fill(NestItem item, Box workArea,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
var inner = new DefaultNestEngine(Plate);
|
||||||
|
return inner.Fill(item, workArea, progress, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override List<Part> Fill(List<Part> groupParts, Box workArea,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
var inner = new DefaultNestEngine(Plate);
|
||||||
|
return inner.Fill(groupParts, workArea, progress, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override List<Part> PackArea(Box box, List<NestItem> items,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
var inner = new DefaultNestEngine(Plate);
|
||||||
|
return inner.PackArea(box, items, progress, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override List<Part> Nest(List<NestItem> items,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (items == null || items.Count == 0)
|
||||||
|
return new List<Part>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Engine.Nfp;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -92,12 +93,7 @@ namespace OpenNest
|
|||||||
allParts.AddRange(shrinkResult.Parts);
|
allParts.AddRange(shrinkResult.Parts);
|
||||||
|
|
||||||
// Compact placed parts toward the origin to close gaps.
|
// Compact placed parts toward the origin to close gaps.
|
||||||
if (allParts.Count > 1)
|
Compactor.Settle(allParts, workArea, Plate.PartSpacing);
|
||||||
{
|
|
||||||
var noObstacles = new List<Part>();
|
|
||||||
Compactor.Push(allParts, noObstacles, workArea, Plate.PartSpacing, PushDirection.Left);
|
|
||||||
Compactor.Push(allParts, noObstacles, workArea, Plate.PartSpacing, PushDirection.Down);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add unfilled items to pack list.
|
// Add unfilled items to pack list.
|
||||||
packItems.AddRange(shrinkResult.Leftovers);
|
packItems.AddRange(shrinkResult.Leftovers);
|
||||||
@@ -127,6 +123,9 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NFP optimization pass — re-place parts using geometry-aware BLF.
|
||||||
|
allParts = AutoNester.Optimize(allParts, Plate);
|
||||||
|
|
||||||
// Deduct placed quantities from original items.
|
// Deduct placed quantities from original items.
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,18 +29,16 @@ public class AngleCandidateBuilderTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_NarrowWorkArea_ProducesMoreAngles()
|
public void Build_NarrowWorkArea_UsesBaseAnglesOnly()
|
||||||
{
|
{
|
||||||
var builder = new AngleCandidateBuilder();
|
var builder = new AngleCandidateBuilder();
|
||||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||||
var wideArea = new Box(0, 0, 100, 100);
|
|
||||||
var narrowArea = new Box(0, 0, 100, 8); // narrower than part's longest side
|
var narrowArea = new Box(0, 0, 100, 8); // narrower than part's longest side
|
||||||
|
|
||||||
var wideAngles = builder.Build(item, 0, wideArea);
|
var angles = builder.Build(item, 0, narrowArea);
|
||||||
var narrowAngles = builder.Build(item, 0, narrowArea);
|
|
||||||
|
|
||||||
Assert.True(narrowAngles.Count > wideAngles.Count,
|
// Without ForceFullSweep, narrow areas use only base angles (0° and 90°)
|
||||||
$"Narrow ({narrowAngles.Count}) should have more angles than wide ({wideAngles.Count})");
|
Assert.Equal(2, angles.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Collections;
|
using OpenNest.Collections;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Engine.Nfp;
|
||||||
using OpenNest.Forms;
|
using OpenNest.Forms;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
@@ -955,8 +956,13 @@ namespace OpenNest.Controls
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var engine = NestEngineRegistry.Create(Plate);
|
var engine = NestEngineRegistry.Create(Plate);
|
||||||
|
var spacing = Plate.PartSpacing;
|
||||||
var parts = await Task.Run(() =>
|
var parts = await Task.Run(() =>
|
||||||
engine.Fill(groupParts, workArea, progress, cts.Token));
|
{
|
||||||
|
var result = engine.Fill(groupParts, workArea, progress, cts.Token);
|
||||||
|
Compactor.Settle(result, workArea, spacing);
|
||||||
|
return AutoNester.Optimize(result, workArea, spacing);
|
||||||
|
});
|
||||||
|
|
||||||
if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted))
|
if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -291,8 +291,8 @@ namespace OpenNest.Forms
|
|||||||
cell.PartColor = partColor;
|
cell.PartColor = partColor;
|
||||||
cell.Dock = DockStyle.Fill;
|
cell.Dock = DockStyle.Fill;
|
||||||
cell.Plate.Size = new Geometry.Size(
|
cell.Plate.Size = new Geometry.Size(
|
||||||
result.BoundingHeight,
|
result.BoundingWidth,
|
||||||
result.BoundingWidth);
|
result.BoundingHeight);
|
||||||
|
|
||||||
var parts = result.BuildParts(drawing);
|
var parts = result.BuildParts(drawing);
|
||||||
|
|
||||||
|
|||||||
@@ -774,6 +774,9 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
private void drawingListUpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
private void drawingListUpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (!drawingListBox1.IsHandleCreated)
|
||||||
|
return;
|
||||||
|
|
||||||
drawingListBox1.Invoke(new MethodInvoker(() =>
|
drawingListBox1.Invoke(new MethodInvoker(() =>
|
||||||
{
|
{
|
||||||
drawingListBox1.Refresh();
|
drawingListBox1.Refresh();
|
||||||
|
|||||||
Reference in New Issue
Block a user