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:
2026-03-07 21:31:15 -05:00
parent d3704378c2
commit b738d4c72c
7 changed files with 27 additions and 312 deletions

View File

@@ -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>

View File

@@ -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();

View File

@@ -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;

View File

@@ -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)

View File

@@ -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();

View File

@@ -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++)
{

View File

@@ -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))