refactor: clean up NestEngine — collapse overloads, extract helper, remove dead code
- Fill(NestItem) and Fill(List<Part>) now delegate to their Box overloads - Add Part.CreateAtOrigin() to replace repeated 4-line build-at-origin pattern used in NestEngine, RotationSlideStrategy, and PairEvaluator - Remove dead code: FillArea overloads, Fill(NestItem, int), FillWithPairs(NestItem), ConvertTileResultToParts, PackBottomLeft.FindPointHorizontal, Pattern.GetLines/GetOffsetLines, unused count variable in FillNoRotation - Simplify IsBetterValidFill to delegate to IsBetterFill after overlap check NestEngine reduced from 717 to 484 lines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,23 @@ namespace OpenNest
|
||||
BoundingBox.Offset(voffset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a part normalized to the origin with optional rotation.
|
||||
/// </summary>
|
||||
public static Part CreateAtOrigin(Drawing drawing, double rotation = 0)
|
||||
{
|
||||
var part = new Part(drawing);
|
||||
|
||||
if (!Math.Tolerance.IsEqualTo(rotation, 0))
|
||||
part.Rotate(rotation);
|
||||
|
||||
var bbox = part.Program.BoundingBox();
|
||||
part.Offset(-bbox.Location.X, -bbox.Location.Y);
|
||||
part.UpdateBounds();
|
||||
|
||||
return part;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the bounding box of the part.
|
||||
/// </summary>
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -28,18 +27,9 @@ namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
var drawing = candidate.Drawing;
|
||||
|
||||
// Build part1 at origin
|
||||
var part1 = new Part(drawing);
|
||||
var bbox1 = part1.Program.BoundingBox();
|
||||
part1.Offset(-bbox1.Location.X, -bbox1.Location.Y);
|
||||
part1.UpdateBounds();
|
||||
var part1 = Part.CreateAtOrigin(drawing);
|
||||
|
||||
// Build part2 with rotation, normalized to origin, then positioned
|
||||
var part2 = new Part(drawing);
|
||||
if (!candidate.Part2Rotation.IsEqualTo(0))
|
||||
part2.Rotate(candidate.Part2Rotation);
|
||||
var bbox2 = part2.Program.BoundingBox();
|
||||
part2.Offset(-bbox2.Location.X, -bbox2.Location.Y);
|
||||
var part2 = Part.CreateAtOrigin(drawing, candidate.Part2Rotation);
|
||||
part2.Location = candidate.Part2Offset;
|
||||
part2.UpdateBounds();
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -21,19 +20,8 @@ namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
var candidates = new List<PairCandidate>();
|
||||
|
||||
// Build part1 at origin
|
||||
var part1 = new Part(drawing);
|
||||
var bbox1 = part1.Program.BoundingBox();
|
||||
part1.Offset(-bbox1.Location.X, -bbox1.Location.Y);
|
||||
part1.UpdateBounds();
|
||||
|
||||
// Build part2 template with rotation, normalized to origin
|
||||
var part2Template = new Part(drawing);
|
||||
if (!Part2Rotation.IsEqualTo(0))
|
||||
part2Template.Rotate(Part2Rotation);
|
||||
var bbox2 = part2Template.Program.BoundingBox();
|
||||
part2Template.Offset(-bbox2.Location.X, -bbox2.Location.Y);
|
||||
part2Template.UpdateBounds();
|
||||
var part1 = Part.CreateAtOrigin(drawing);
|
||||
var part2Template = Part.CreateAtOrigin(drawing, Part2Rotation);
|
||||
|
||||
var testNumber = 0;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.BestFit.Tiling;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.RectanglePacking;
|
||||
@@ -26,96 +25,12 @@ namespace OpenNest
|
||||
|
||||
public bool Fill(NestItem item)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var workArea = Plate.WorkArea();
|
||||
var bestRotation = FindBestRotation(item);
|
||||
|
||||
var engine = new FillLinear(workArea, Plate.PartSpacing);
|
||||
|
||||
// Try 4 configurations: 2 rotations x 2 axes.
|
||||
var configs = new[]
|
||||
{
|
||||
engine.Fill(item.Drawing, bestRotation, NestDirection.Horizontal),
|
||||
engine.Fill(item.Drawing, bestRotation, NestDirection.Vertical),
|
||||
engine.Fill(item.Drawing, bestRotation + Angle.HalfPI, NestDirection.Horizontal),
|
||||
engine.Fill(item.Drawing, bestRotation + Angle.HalfPI, NestDirection.Vertical)
|
||||
};
|
||||
|
||||
// Pick the best linear configuration. FillLinear already ensures
|
||||
// geometry-aware spacing, so skip the redundant overlap check that
|
||||
// can produce false positives on arcs/curves.
|
||||
List<Part> linearBest = null;
|
||||
|
||||
foreach (var config in configs)
|
||||
{
|
||||
if (IsBetterFill(config, linearBest))
|
||||
linearBest = config;
|
||||
}
|
||||
|
||||
var linearMs = sw.ElapsedMilliseconds;
|
||||
|
||||
// Try rectangle best-fit (mixes orientations to fill remnant strips).
|
||||
var rectResult = FillRectangleBestFit(item, workArea);
|
||||
|
||||
var rectMs = sw.ElapsedMilliseconds - linearMs;
|
||||
|
||||
// Try pair-based approach.
|
||||
var pairResult = FillWithPairs(item);
|
||||
|
||||
var pairMs = sw.ElapsedMilliseconds - linearMs - rectMs;
|
||||
|
||||
// Pick whichever is the better fill.
|
||||
Debug.WriteLine($"[NestEngine.Fill] Linear: {linearBest?.Count ?? 0} parts ({linearMs}ms) | Rect: {rectResult?.Count ?? 0} parts ({rectMs}ms) | Pair: {pairResult.Count} parts ({pairMs}ms) | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}");
|
||||
var best = linearBest;
|
||||
|
||||
if (IsBetterFill(rectResult, best))
|
||||
best = rectResult;
|
||||
|
||||
if (IsBetterFill(pairResult, best))
|
||||
best = pairResult;
|
||||
|
||||
if (best == null || best.Count == 0)
|
||||
return false;
|
||||
|
||||
// Limit to requested quantity if specified.
|
||||
if (item.Quantity > 0 && best.Count > item.Quantity)
|
||||
best = best.Take(item.Quantity).ToList();
|
||||
|
||||
Plate.Parts.AddRange(best);
|
||||
return true;
|
||||
return Fill(item, Plate.WorkArea());
|
||||
}
|
||||
|
||||
public bool Fill(List<Part> groupParts)
|
||||
{
|
||||
if (groupParts == null || groupParts.Count == 0)
|
||||
return false;
|
||||
|
||||
var workArea = Plate.WorkArea();
|
||||
var engine = new FillLinear(workArea, Plate.PartSpacing);
|
||||
var angles = FindHullEdgeAngles(groupParts);
|
||||
var best = FillPattern(engine, groupParts, angles);
|
||||
|
||||
// For single-part groups, also try rectangle best-fit and pair-based filling.
|
||||
if (groupParts.Count == 1)
|
||||
{
|
||||
var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
|
||||
var rectResult = FillRectangleBestFit(nestItem, workArea);
|
||||
|
||||
if (IsBetterFill(rectResult, best))
|
||||
best = rectResult;
|
||||
|
||||
var pairResult = FillWithPairs(nestItem);
|
||||
|
||||
if (IsBetterFill(pairResult, best))
|
||||
best = pairResult;
|
||||
}
|
||||
|
||||
if (best == null || best.Count == 0)
|
||||
return false;
|
||||
|
||||
Plate.Parts.AddRange(best);
|
||||
return true;
|
||||
return Fill(groupParts, Plate.WorkArea());
|
||||
}
|
||||
|
||||
public bool Fill(NestItem item, Box workArea)
|
||||
@@ -204,68 +119,6 @@ namespace OpenNest
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Fill(NestItem item, int maxCount)
|
||||
{
|
||||
if (maxCount <= 0)
|
||||
return false;
|
||||
|
||||
var savedQty = item.Quantity;
|
||||
item.Quantity = maxCount;
|
||||
var result = Fill(item);
|
||||
item.Quantity = savedQty;
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool FillArea(Box box, NestItem item)
|
||||
{
|
||||
var binItem = ConvertToRectangleItem(item);
|
||||
|
||||
var bin = new Bin
|
||||
{
|
||||
Location = box.Location,
|
||||
Size = box.Size
|
||||
};
|
||||
|
||||
bin.Width += Plate.PartSpacing;
|
||||
bin.Height += Plate.PartSpacing;
|
||||
|
||||
var engine = new FillBestFit(bin);
|
||||
engine.Fill(binItem);
|
||||
|
||||
var nestItems = new List<NestItem>();
|
||||
nestItems.Add(item);
|
||||
|
||||
var parts = ConvertToParts(bin, nestItems);
|
||||
Plate.Parts.AddRange(parts);
|
||||
|
||||
return parts.Count > 0;
|
||||
}
|
||||
|
||||
public bool FillArea(Box box, NestItem item, int maxCount)
|
||||
{
|
||||
var binItem = ConvertToRectangleItem(item);
|
||||
|
||||
var bin = new Bin
|
||||
{
|
||||
Location = box.Location,
|
||||
Size = box.Size
|
||||
};
|
||||
|
||||
bin.Width += Plate.PartSpacing;
|
||||
bin.Height += Plate.PartSpacing;
|
||||
|
||||
var engine = new FillBestFit(bin);
|
||||
engine.Fill(binItem, maxCount);
|
||||
|
||||
var nestItems = new List<NestItem>();
|
||||
nestItems.Add(item);
|
||||
|
||||
var parts = ConvertToParts(bin, nestItems);
|
||||
Plate.Parts.AddRange(parts);
|
||||
|
||||
return parts.Count > 0;
|
||||
}
|
||||
|
||||
public bool Pack(List<NestItem> items)
|
||||
{
|
||||
var workArea = Plate.WorkArea();
|
||||
@@ -314,11 +167,6 @@ namespace OpenNest
|
||||
return ConvertToParts(bin, nestItems);
|
||||
}
|
||||
|
||||
private List<Part> FillWithPairs(NestItem item)
|
||||
{
|
||||
return FillWithPairs(item, Plate.WorkArea());
|
||||
}
|
||||
|
||||
private List<Part> FillWithPairs(NestItem item, Box workArea)
|
||||
{
|
||||
IPairEvaluator evaluator = null;
|
||||
@@ -367,16 +215,9 @@ namespace OpenNest
|
||||
{
|
||||
var candidate = bestFit.Candidate;
|
||||
|
||||
var part1 = new Part(drawing);
|
||||
var bbox1 = part1.Program.BoundingBox();
|
||||
part1.Offset(-bbox1.Location.X, -bbox1.Location.Y);
|
||||
part1.UpdateBounds();
|
||||
var part1 = Part.CreateAtOrigin(drawing);
|
||||
|
||||
var part2 = new Part(drawing);
|
||||
if (!candidate.Part2Rotation.IsEqualTo(0))
|
||||
part2.Rotate(candidate.Part2Rotation);
|
||||
var bbox2 = part2.Program.BoundingBox();
|
||||
part2.Offset(-bbox2.Location.X, -bbox2.Location.Y);
|
||||
var part2 = Part.CreateAtOrigin(drawing, candidate.Part2Rotation);
|
||||
part2.Location = candidate.Part2Offset;
|
||||
part2.UpdateBounds();
|
||||
|
||||
@@ -396,68 +237,6 @@ namespace OpenNest
|
||||
return new List<Part> { part1, part2 };
|
||||
}
|
||||
|
||||
private List<Part> ConvertTileResultToParts(TileResult tileResult, Drawing drawing)
|
||||
{
|
||||
var parts = new List<Part>();
|
||||
var bestFit = tileResult.BestFit;
|
||||
var candidate = bestFit.Candidate;
|
||||
var workArea = Plate.WorkArea();
|
||||
|
||||
foreach (var placement in tileResult.Placements)
|
||||
{
|
||||
// Build part1 at origin.
|
||||
var part1 = new Part(drawing);
|
||||
var bbox1 = part1.Program.BoundingBox();
|
||||
part1.Offset(-bbox1.Location.X, -bbox1.Location.Y);
|
||||
part1.UpdateBounds();
|
||||
|
||||
// Build part2 with rotation, positioned at offset.
|
||||
var part2 = new Part(drawing);
|
||||
|
||||
if (!candidate.Part2Rotation.IsEqualTo(0))
|
||||
part2.Rotate(candidate.Part2Rotation);
|
||||
|
||||
var bbox2 = part2.Program.BoundingBox();
|
||||
part2.Offset(-bbox2.Location.X, -bbox2.Location.Y);
|
||||
part2.Location = candidate.Part2Offset;
|
||||
part2.UpdateBounds();
|
||||
|
||||
// Apply optimal rotation to align pair to minimum bounding rectangle.
|
||||
if (!bestFit.OptimalRotation.IsEqualTo(0))
|
||||
{
|
||||
var pairBounds = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
||||
var center = pairBounds.Center;
|
||||
part1.Rotate(-bestFit.OptimalRotation, center);
|
||||
part2.Rotate(-bestFit.OptimalRotation, center);
|
||||
}
|
||||
|
||||
// Apply 90 degree rotation if the tiler chose the rotated orientation.
|
||||
if (tileResult.PairRotated)
|
||||
{
|
||||
var pairBounds = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
||||
var center = pairBounds.Center;
|
||||
part1.Rotate(Angle.HalfPI, center);
|
||||
part2.Rotate(Angle.HalfPI, center);
|
||||
}
|
||||
|
||||
// Normalize pair to origin.
|
||||
var finalBounds = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
||||
var normalizeOffset = new Vector(-finalBounds.Left, -finalBounds.Bottom);
|
||||
part1.Offset(normalizeOffset);
|
||||
part2.Offset(normalizeOffset);
|
||||
|
||||
// Offset to grid position plus work area origin.
|
||||
var plateOffset = placement.Position + workArea.Location;
|
||||
part1.Offset(plateOffset);
|
||||
part2.Offset(plateOffset);
|
||||
|
||||
parts.Add(part1);
|
||||
parts.Add(part2);
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
private bool HasOverlaps(List<Part> parts, double spacing)
|
||||
{
|
||||
if (parts == null || parts.Count <= 1)
|
||||
@@ -497,23 +276,10 @@ namespace OpenNest
|
||||
|
||||
private bool IsBetterValidFill(List<Part> candidate, List<Part> current)
|
||||
{
|
||||
if (candidate == null || candidate.Count == 0)
|
||||
if (candidate != null && candidate.Count > 0 && HasOverlaps(candidate, Plate.PartSpacing))
|
||||
return false;
|
||||
|
||||
// Reject candidate if it has overlapping parts.
|
||||
if (HasOverlaps(candidate, Plate.PartSpacing))
|
||||
return false;
|
||||
|
||||
if (current == null || current.Count == 0)
|
||||
return true;
|
||||
|
||||
if (candidate.Count != current.Count)
|
||||
return candidate.Count > current.Count;
|
||||
|
||||
var candidateBox = ((IEnumerable<IBoundable>)candidate).GetBoundingBox();
|
||||
var currentBox = ((IEnumerable<IBoundable>)current).GetBoundingBox();
|
||||
|
||||
return candidateBox.Area() < currentBox.Area();
|
||||
return IsBetterFill(candidate, current);
|
||||
}
|
||||
|
||||
private List<double> FindHullEdgeAngles(List<Part> parts)
|
||||
|
||||
@@ -19,26 +19,6 @@ namespace OpenNest
|
||||
BoundingBox = Parts.GetBoundingBox();
|
||||
}
|
||||
|
||||
public List<Line> GetLines(PushDirection facingDirection)
|
||||
{
|
||||
var lines = new List<Line>();
|
||||
|
||||
foreach (var part in Parts)
|
||||
lines.AddRange(Helper.GetPartLines(part, facingDirection));
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
public List<Line> GetOffsetLines(double spacing, PushDirection facingDirection)
|
||||
{
|
||||
var lines = new List<Line>();
|
||||
|
||||
foreach (var part in Parts)
|
||||
lines.AddRange(Helper.GetOffsetPartLines(part, spacing, facingDirection));
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
public Pattern Clone(Vector offset)
|
||||
{
|
||||
var pattern = new Pattern();
|
||||
|
||||
@@ -17,7 +17,6 @@ namespace OpenNest.RectanglePacking
|
||||
{
|
||||
var ycount = (int)System.Math.Floor((Bin.Height + Tolerance.Epsilon) / item.Height);
|
||||
var xcount = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Width);
|
||||
var count = ycount * xcount;
|
||||
|
||||
for (int i = 0; i < xcount; i++)
|
||||
{
|
||||
|
||||
@@ -75,31 +75,6 @@ namespace OpenNest.RectanglePacking
|
||||
return null;
|
||||
}
|
||||
|
||||
private Vector? FindPointHorizontal(Item item)
|
||||
{
|
||||
var pt = new Vector(double.MaxValue, double.MaxValue);
|
||||
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
{
|
||||
var point = points[i];
|
||||
|
||||
item.Location = point;
|
||||
|
||||
if (!IsValid(item))
|
||||
continue;
|
||||
|
||||
if (point.Y < pt.Y)
|
||||
pt = point;
|
||||
else if (point.Y.IsEqualTo(pt.Y) && point.X < pt.X)
|
||||
pt = point;
|
||||
}
|
||||
|
||||
if (pt.X != double.MaxValue && pt.Y != double.MaxValue)
|
||||
return pt;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsValid(Item item)
|
||||
{
|
||||
if (!Bin.Contains(item))
|
||||
|
||||
Reference in New Issue
Block a user