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;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
get { return X; }
|
||||
|
||||
@@ -52,6 +52,7 @@ namespace OpenNest.Geometry
|
||||
result.Vertices.Add(new Vector(ifpRight, ifpTop));
|
||||
result.Vertices.Add(new Vector(ifpLeft, ifpTop));
|
||||
result.Close();
|
||||
result.UpdateBounds();
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -62,36 +63,20 @@ namespace OpenNest.Geometry
|
||||
/// Returns the polygon representing valid placement positions, or an empty
|
||||
/// polygon if no valid position exists.
|
||||
/// </summary>
|
||||
public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps)
|
||||
public static Polygon ComputeFeasibleRegion(Polygon ifp, PathsD nfpPaths)
|
||||
{
|
||||
if (ifp.Vertices.Count < 3)
|
||||
return new Polygon();
|
||||
|
||||
if (nfps == null || nfps.Length == 0)
|
||||
if (nfpPaths == null || nfpPaths.Count == 0)
|
||||
return ifp;
|
||||
|
||||
var ifpPath = NoFitPolygon.ToClipperPath(ifp);
|
||||
var ifpPaths = new PathsD { ifpPath };
|
||||
|
||||
// Union all NFPs.
|
||||
var nfpPaths = new PathsD();
|
||||
|
||||
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);
|
||||
// Subtract the NFPs from the IFP.
|
||||
// Clipper2 handles the implicit union of the clip paths.
|
||||
var feasible = Clipper.Difference(ifpPaths, nfpPaths, FillRule.NonZero);
|
||||
|
||||
if (feasible.Count == 0)
|
||||
return new Polygon();
|
||||
@@ -118,6 +103,25 @@ namespace OpenNest.Geometry
|
||||
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>
|
||||
/// Finds the bottom-left-most point on a polygon boundary.
|
||||
/// "Bottom-left" means: minimize Y first, then minimize X.
|
||||
|
||||
@@ -250,9 +250,9 @@ namespace OpenNest.Geometry
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an OpenNest Polygon to a Clipper2 PathD.
|
||||
/// Converts an OpenNest Polygon to a Clipper2 PathD, with an optional offset.
|
||||
/// </summary>
|
||||
internal static PathD ToClipperPath(Polygon polygon)
|
||||
public static PathD ToClipperPath(Polygon polygon, Vector offset = default)
|
||||
{
|
||||
var path = new PathD();
|
||||
var verts = polygon.Vertices;
|
||||
@@ -263,7 +263,7 @@ namespace OpenNest.Geometry
|
||||
n--;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -271,7 +271,7 @@ namespace OpenNest.Geometry
|
||||
/// <summary>
|
||||
/// Converts a Clipper2 PathD to an OpenNest Polygon.
|
||||
/// </summary>
|
||||
internal static Polygon FromClipperPath(PathD path)
|
||||
public static Polygon FromClipperPath(PathD path)
|
||||
{
|
||||
var polygon = new Polygon();
|
||||
|
||||
@@ -279,6 +279,7 @@ namespace OpenNest.Geometry
|
||||
polygon.Vertices.Add(new Vector(pt.x, pt.y));
|
||||
|
||||
polygon.Close();
|
||||
polygon.UpdateBounds();
|
||||
return polygon;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -67,6 +68,15 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
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
|
||||
{
|
||||
Candidate = candidate,
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
var angles = new List<double>(baseAngles);
|
||||
|
||||
if (NeedsSweep(item, bestRotation, workArea))
|
||||
if (ForceFullSweep)
|
||||
AddSweepAngles(angles);
|
||||
|
||||
if (!ForceFullSweep && angles.Count > 2)
|
||||
@@ -36,18 +36,6 @@ namespace OpenNest.Engine.Fill
|
||||
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)
|
||||
{
|
||||
var step = Angle.ToRadians(5);
|
||||
|
||||
@@ -174,5 +174,28 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
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)
|
||||
{
|
||||
var kept = bestFits.Where(r => r.Keep).ToList();
|
||||
var top = kept.Take(MaxTopCandidates).ToList();
|
||||
|
||||
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
||||
var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length);
|
||||
|
||||
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
|
||||
&& r.Utilization >= MinStripUtilization)
|
||||
.OrderByDescending(r => r.Utilization);
|
||||
.ToList();
|
||||
|
||||
var existing = new HashSet<BestFitResult>(top);
|
||||
SortByEstimatedCount(stripCandidates, workArea);
|
||||
|
||||
foreach (var r in stripCandidates)
|
||||
{
|
||||
if (top.Count >= MaxStripCandidates)
|
||||
break;
|
||||
|
||||
if (existing.Add(r))
|
||||
top.Add(r);
|
||||
}
|
||||
var top = stripCandidates.Take(MaxStripCandidates).ToList();
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Nfp;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<NestEngineInfo> AvailableEngines => engines;
|
||||
|
||||
@@ -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<Part> Nest(List<NestItem> items, Plate plate,
|
||||
IProgress<NestProgress> 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<Part>();
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Extracts the perimeter polygon from a drawing, inflated by half-spacing.
|
||||
/// </summary>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </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>();
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// <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>
|
||||
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)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
@@ -11,9 +12,9 @@ namespace OpenNest.Engine.Nfp
|
||||
public class OptimizationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The best sequence found: (drawingId, rotation, drawing) tuples in placement order.
|
||||
/// The best placement sequence found.
|
||||
/// </summary>
|
||||
public List<(int drawingId, double rotation, Drawing drawing)> Sequence { get; set; }
|
||||
public List<SequenceEntry> Sequence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The score achieved by the best sequence.
|
||||
@@ -34,6 +35,7 @@ namespace OpenNest.Engine.Nfp
|
||||
{
|
||||
OptimizationResult Optimize(List<NestItem> items, Box workArea, NfpCache cache,
|
||||
Dictionary<int, List<double>> candidateRotations,
|
||||
IProgress<NestProgress> progress = null,
|
||||
CancellationToken cancellation = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace OpenNest.Engine.Nfp
|
||||
private readonly Dictionary<NfpKey, Polygon> cache = new Dictionary<NfpKey, Polygon>();
|
||||
private readonly Dictionary<int, Dictionary<double, Polygon>> polygonCache
|
||||
= new Dictionary<int, Dictionary<double, Polygon>>();
|
||||
private readonly Dictionary<(int drawingId, double rotation), Polygon> ifpCache
|
||||
= new Dictionary<(int drawingId, double rotation), Polygon>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a pre-computed polygon for a drawing at a specific rotation.
|
||||
@@ -28,6 +30,26 @@ namespace OpenNest.Engine.Nfp
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
15
OpenNest.Engine/Nfp/PlacedPart.cs
Normal file
15
OpenNest.Engine/Nfp/PlacedPart.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
24
OpenNest.Engine/Nfp/SequenceEntry.cs
Normal file
24
OpenNest.Engine/Nfp/SequenceEntry.cs
Normal file
@@ -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,
|
||||
Dictionary<int, List<double>> candidateRotations,
|
||||
IProgress<NestProgress> 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<SequenceEntry>(sequence);
|
||||
|
||||
var currentSequence = new List<(int, double, Drawing)>(sequence);
|
||||
var currentSequence = new List<SequenceEntry>(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<SequenceEntry>(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<SequenceEntry>(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.
|
||||
/// </summary>
|
||||
private static List<(int drawingId, double rotation, Drawing drawing)> BuildInitialSequence(
|
||||
private static List<SequenceEntry> BuildInitialSequence(
|
||||
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.
|
||||
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
|
||||
/// <summary>
|
||||
/// Applies a random mutation to the sequence.
|
||||
/// </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)
|
||||
{
|
||||
if (sequence.Count < 2)
|
||||
@@ -169,7 +176,7 @@ namespace OpenNest.Engine.Nfp
|
||||
/// <summary>
|
||||
/// Swaps two random parts in the sequence.
|
||||
/// </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 j = random.Next(sequence.Count);
|
||||
@@ -183,23 +190,23 @@ namespace OpenNest.Engine.Nfp
|
||||
/// <summary>
|
||||
/// Changes a random part's rotation to another candidate angle.
|
||||
/// </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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverses a random contiguous subsequence.
|
||||
/// </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 j = random.Next(sequence.Count);
|
||||
@@ -221,7 +228,7 @@ namespace OpenNest.Engine.Nfp
|
||||
/// are accepted initially.
|
||||
/// </summary>
|
||||
private static double CalibrateTemperature(
|
||||
List<(int drawingId, double rotation, Drawing drawing)> sequence,
|
||||
List<SequenceEntry> sequence,
|
||||
Box workArea, NfpCache cache,
|
||||
Dictionary<int, List<double>> 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<SequenceEntry>(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<NestProgress> progress, List<Part> parts,
|
||||
Box workArea, string description)
|
||||
{
|
||||
NestEngineBase.ReportProgress(progress, NestPhase.Nfp, 0, parts, workArea,
|
||||
description, isOverallBest: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
OpenNest.Engine/NfpNestEngine.cs
Normal file
68
OpenNest.Engine/NfpNestEngine.cs
Normal file
@@ -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.Nfp;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -92,12 +93,7 @@ namespace OpenNest
|
||||
allParts.AddRange(shrinkResult.Parts);
|
||||
|
||||
// Compact placed parts toward the origin to close gaps.
|
||||
if (allParts.Count > 1)
|
||||
{
|
||||
var noObstacles = new List<Part>();
|
||||
Compactor.Push(allParts, noObstacles, workArea, Plate.PartSpacing, PushDirection.Left);
|
||||
Compactor.Push(allParts, noObstacles, workArea, Plate.PartSpacing, PushDirection.Down);
|
||||
}
|
||||
Compactor.Settle(allParts, workArea, Plate.PartSpacing);
|
||||
|
||||
// Add unfilled items to pack list.
|
||||
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.
|
||||
foreach (var item in items)
|
||||
{
|
||||
|
||||
@@ -29,18 +29,16 @@ public class AngleCandidateBuilderTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NarrowWorkArea_ProducesMoreAngles()
|
||||
public void Build_NarrowWorkArea_UsesBaseAnglesOnly()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder();
|
||||
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 wideAngles = builder.Build(item, 0, wideArea);
|
||||
var narrowAngles = builder.Build(item, 0, narrowArea);
|
||||
var angles = builder.Build(item, 0, narrowArea);
|
||||
|
||||
Assert.True(narrowAngles.Count > wideAngles.Count,
|
||||
$"Narrow ({narrowAngles.Count}) should have more angles than wide ({wideAngles.Count})");
|
||||
// Without ForceFullSweep, narrow areas use only base angles (0° and 90°)
|
||||
Assert.Equal(2, angles.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Collections;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Nfp;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
@@ -955,8 +956,13 @@ namespace OpenNest.Controls
|
||||
try
|
||||
{
|
||||
var engine = NestEngineRegistry.Create(Plate);
|
||||
var spacing = Plate.PartSpacing;
|
||||
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))
|
||||
{
|
||||
|
||||
@@ -291,8 +291,8 @@ namespace OpenNest.Forms
|
||||
cell.PartColor = partColor;
|
||||
cell.Dock = DockStyle.Fill;
|
||||
cell.Plate.Size = new Geometry.Size(
|
||||
result.BoundingHeight,
|
||||
result.BoundingWidth);
|
||||
result.BoundingWidth,
|
||||
result.BoundingHeight);
|
||||
|
||||
var parts = result.BuildParts(drawing);
|
||||
|
||||
|
||||
@@ -774,6 +774,9 @@ namespace OpenNest.Forms
|
||||
|
||||
private void drawingListUpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
if (!drawingListBox1.IsHandleCreated)
|
||||
return;
|
||||
|
||||
drawingListBox1.Invoke(new MethodInvoker(() =>
|
||||
{
|
||||
drawingListBox1.Refresh();
|
||||
|
||||
Reference in New Issue
Block a user