From b738d4c72ce891ed183dcb764145a6e47444854f Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 7 Mar 2026 21:31:15 -0500 Subject: [PATCH] =?UTF-8?q?refactor:=20clean=20up=20NestEngine=20=E2=80=94?= =?UTF-8?q?=20collapse=20overloads,=20extract=20helper,=20remove=20dead=20?= =?UTF-8?q?code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fill(NestItem) and Fill(List) 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 --- OpenNest.Core/Part.cs | 17 ++ OpenNest.Engine/BestFit/PairEvaluator.cs | 14 +- .../BestFit/RotationSlideStrategy.cs | 16 +- OpenNest.Engine/NestEngine.cs | 246 +----------------- OpenNest.Engine/Pattern.cs | 20 -- .../RectanglePacking/FillNoRotation.cs | 1 - .../RectanglePacking/PackBottomLeft.cs | 25 -- 7 files changed, 27 insertions(+), 312 deletions(-) diff --git a/OpenNest.Core/Part.cs b/OpenNest.Core/Part.cs index e88a60c..1c8ef21 100644 --- a/OpenNest.Core/Part.cs +++ b/OpenNest.Core/Part.cs @@ -103,6 +103,23 @@ namespace OpenNest BoundingBox.Offset(voffset); } + /// + /// Creates a part normalized to the origin with optional rotation. + /// + 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; + } + /// /// Updates the bounding box of the part. /// diff --git a/OpenNest.Engine/BestFit/PairEvaluator.cs b/OpenNest.Engine/BestFit/PairEvaluator.cs index c82df0c..a91f0c6 100644 --- a/OpenNest.Engine/BestFit/PairEvaluator.cs +++ b/OpenNest.Engine/BestFit/PairEvaluator.cs @@ -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(); diff --git a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs index 8cb6162..6079385 100644 --- a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs +++ b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs @@ -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(); - // 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; diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index db809e7..16b3dee 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -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 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 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(); - 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(); - nestItems.Add(item); - - var parts = ConvertToParts(bin, nestItems); - Plate.Parts.AddRange(parts); - - return parts.Count > 0; - } - public bool Pack(List items) { var workArea = Plate.WorkArea(); @@ -314,11 +167,6 @@ namespace OpenNest return ConvertToParts(bin, nestItems); } - private List FillWithPairs(NestItem item) - { - return FillWithPairs(item, Plate.WorkArea()); - } - private List 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 { part1, part2 }; } - private List ConvertTileResultToParts(TileResult tileResult, Drawing drawing) - { - var parts = new List(); - 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)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)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)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 parts, double spacing) { if (parts == null || parts.Count <= 1) @@ -497,23 +276,10 @@ namespace OpenNest private bool IsBetterValidFill(List candidate, List 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)candidate).GetBoundingBox(); - var currentBox = ((IEnumerable)current).GetBoundingBox(); - - return candidateBox.Area() < currentBox.Area(); + return IsBetterFill(candidate, current); } private List FindHullEdgeAngles(List parts) diff --git a/OpenNest.Engine/Pattern.cs b/OpenNest.Engine/Pattern.cs index 1cdb2e4..c5fb8ed 100644 --- a/OpenNest.Engine/Pattern.cs +++ b/OpenNest.Engine/Pattern.cs @@ -19,26 +19,6 @@ namespace OpenNest BoundingBox = Parts.GetBoundingBox(); } - public List GetLines(PushDirection facingDirection) - { - var lines = new List(); - - foreach (var part in Parts) - lines.AddRange(Helper.GetPartLines(part, facingDirection)); - - return lines; - } - - public List GetOffsetLines(double spacing, PushDirection facingDirection) - { - var lines = new List(); - - foreach (var part in Parts) - lines.AddRange(Helper.GetOffsetPartLines(part, spacing, facingDirection)); - - return lines; - } - public Pattern Clone(Vector offset) { var pattern = new Pattern(); diff --git a/OpenNest.Engine/RectanglePacking/FillNoRotation.cs b/OpenNest.Engine/RectanglePacking/FillNoRotation.cs index 2f6e73a..f248276 100644 --- a/OpenNest.Engine/RectanglePacking/FillNoRotation.cs +++ b/OpenNest.Engine/RectanglePacking/FillNoRotation.cs @@ -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++) { diff --git a/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs b/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs index cfe99a3..7ce8a6a 100644 --- a/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs +++ b/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs @@ -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))