diff --git a/OpenNest.Engine/BestFit/BestFitResult.cs b/OpenNest.Engine/BestFit/BestFitResult.cs index a154b21..169f478 100644 --- a/OpenNest.Engine/BestFit/BestFitResult.cs +++ b/OpenNest.Engine/BestFit/BestFitResult.cs @@ -1,3 +1,7 @@ +using System.Collections.Generic; +using OpenNest.Geometry; +using OpenNest.Math; + namespace OpenNest.Engine.BestFit { public class BestFitResult @@ -25,6 +29,30 @@ namespace OpenNest.Engine.BestFit { get { return System.Math.Min(BoundingWidth, BoundingHeight); } } + + public List BuildParts(Drawing drawing) + { + var part1 = Part.CreateAtOrigin(drawing); + + var part2 = Part.CreateAtOrigin(drawing, Candidate.Part2Rotation); + part2.Location = Candidate.Part2Offset; + part2.UpdateBounds(); + + if (!OptimalRotation.IsEqualTo(0)) + { + var pairBounds = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + var center = pairBounds.Center; + part1.Rotate(-OptimalRotation, center); + part2.Rotate(-OptimalRotation, center); + } + + var finalBounds = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + var offset = new Vector(-finalBounds.Left, -finalBounds.Bottom); + part1.Offset(offset); + part2.Offset(offset); + + return new List { part1, part2 }; + } } public enum BestFitSortField diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index 16b3dee..e829e2c 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using OpenNest.Converters; using OpenNest.Engine.BestFit; using OpenNest.Geometry; using OpenNest.Math; @@ -35,7 +34,7 @@ namespace OpenNest public bool Fill(NestItem item, Box workArea) { - var bestRotation = FindBestRotation(item); + var bestRotation = RotationAnalysis.FindBestRotation(item); var engine = new FillLinear(workArea, Plate.PartSpacing); @@ -89,7 +88,7 @@ namespace OpenNest return false; var engine = new FillLinear(workArea, Plate.PartSpacing); - var angles = FindHullEdgeAngles(groupParts); + var angles = RotationAnalysis.FindHullEdgeAngles(groupParts); var best = FillPattern(engine, groupParts, angles); Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}"); @@ -127,21 +126,13 @@ namespace OpenNest public bool PackArea(Box box, List items) { - var binItems = ConvertToRectangleItems(items); - - var bin = new Bin - { - Location = box.Location, - Size = box.Size - }; - - bin.Width += Plate.PartSpacing; - bin.Height += Plate.PartSpacing; + var binItems = BinConverter.ToItems(items, Plate.PartSpacing, Plate.Area()); + var bin = BinConverter.CreateBin(box, Plate.PartSpacing); var engine = new PackBottomLeft(bin); engine.Pack(binItems); - var parts = ConvertToParts(bin, items); + var parts = BinConverter.ToParts(bin, items); Plate.Parts.AddRange(parts); return parts.Count > 0; @@ -149,22 +140,13 @@ namespace OpenNest private List FillRectangleBestFit(NestItem item, Box workArea) { - var binItem = ConvertToRectangleItem(item); - - var bin = new Bin - { - Location = workArea.Location, - Size = workArea.Size - }; - - bin.Width += Plate.PartSpacing; - bin.Height += Plate.PartSpacing; + var binItem = BinConverter.ToItem(item, Plate.PartSpacing); + var bin = BinConverter.CreateBin(workArea, Plate.PartSpacing); var engine = new FillBestFit(bin); engine.Fill(binItem); - var nestItems = new List { item }; - return ConvertToParts(bin, nestItems); + return BinConverter.ToParts(bin, new List { item }); } private List FillWithPairs(NestItem item, Box workArea) @@ -188,8 +170,8 @@ namespace OpenNest System.Threading.Tasks.Parallel.For(0, keptResults.Count, i => { var result = keptResults[i]; - var pairParts = BuildPairParts(result, item.Drawing); - var angles = FindHullEdgeAngles(pairParts); + var pairParts = result.BuildParts(item.Drawing); + var angles = RotationAnalysis.FindHullEdgeAngles(pairParts); var engine = new FillLinear(workArea, Plate.PartSpacing); var filled = FillPattern(engine, pairParts, angles); @@ -211,32 +193,6 @@ namespace OpenNest return best ?? new List(); } - public static List BuildPairParts(BestFitResult bestFit, Drawing drawing) - { - var candidate = bestFit.Candidate; - - var part1 = Part.CreateAtOrigin(drawing); - - var part2 = Part.CreateAtOrigin(drawing, candidate.Part2Rotation); - part2.Location = candidate.Part2Offset; - part2.UpdateBounds(); - - 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); - } - - var finalBounds = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); - var offset = new Vector(-finalBounds.Left, -finalBounds.Bottom); - part1.Offset(offset); - part2.Offset(offset); - - return new List { part1, part2 }; - } - private bool HasOverlaps(List parts, double spacing) { if (parts == null || parts.Count <= 1) @@ -282,53 +238,6 @@ namespace OpenNest return IsBetterFill(candidate, current); } - private List FindHullEdgeAngles(List parts) - { - var points = new List(); - - foreach (var part in parts) - { - var entities = ConvertProgram.ToGeometry(part.Program) - .Where(e => e.Layer != SpecialLayers.Rapid); - - var shapes = Helper.GetShapes(entities); - - foreach (var shape in shapes) - { - var polygon = shape.ToPolygonWithTolerance(0.1); - - foreach (var vertex in polygon.Vertices) - points.Add(vertex + part.Location); - } - } - - if (points.Count < 3) - return new List { 0 }; - - var hull = ConvexHull.Compute(points); - var vertices = hull.Vertices; - var n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count; - - var angles = new List { 0 }; - - for (var i = 0; i < n; i++) - { - var next = (i + 1) % n; - var dx = vertices[next].X - vertices[i].X; - var dy = vertices[next].Y - vertices[i].Y; - - if (dx * dx + dy * dy < Tolerance.Epsilon) - continue; - - var angle = -System.Math.Atan2(dy, dx); - - if (!angles.Any(a => a.IsEqualTo(angle))) - angles.Add(angle); - } - - return angles; - } - private Pattern BuildRotatedPattern(List groupParts, double angle) { var pattern = new Pattern(); @@ -373,112 +282,5 @@ namespace OpenNest return best; } - private double FindBestRotation(NestItem item) - { - var entities = ConvertProgram.ToGeometry(item.Drawing.Program) - .Where(e => e.Layer != SpecialLayers.Rapid); - - var shapes = Helper.GetShapes(entities); - - if (shapes.Count == 0) - return 0; - - // Find the largest shape (outer profile). - Shape largest = shapes[0]; - double largestArea = largest.Area(); - - for (int i = 1; i < shapes.Count; i++) - { - var area = shapes[i].Area(); - if (area > largestArea) - { - largest = shapes[i]; - largestArea = area; - } - } - - // Convert to polygon so arcs are properly represented as line segments. - // Shape.FindBestRotation() uses Entity cardinal points which are incorrect - // for arcs that don't sweep through all 4 cardinal directions. - var polygon = largest.ToPolygonWithTolerance(0.1); - - BoundingRectangleResult result; - - if (item.RotationStart.IsEqualTo(0) && item.RotationEnd.IsEqualTo(0)) - result = polygon.FindBestRotation(); - else - result = polygon.FindBestRotation(item.RotationStart, item.RotationEnd); - - // Negate the angle to align the minimum bounding rectangle with the axes. - return -result.Angle; - } - - private List ConvertToParts(Bin bin, List items) - { - var parts = new List(); - - foreach (var item in bin.Items) - { - var nestItem = items[item.Id]; - var part = ConvertToPart(item, nestItem.Drawing); - parts.Add(part); - } - - return parts; - } - - private Part ConvertToPart(Item item, Drawing dwg) - { - var part = new Part(dwg); - - if (item.IsRotated) - part.Rotate(Angle.HalfPI); - - var boundingBox = part.Program.BoundingBox(); - var offset = item.Location - boundingBox.Location; - - part.Offset(offset); - - return part; - } - - private List ConvertToRectangleItems(List items) - { - var binItems = new List(); - - for (int i = 0; i < items.Count; i++) - { - var item = items[i]; - var binItem = ConvertToRectangleItem(item, i); - - int maxQty = (int)System.Math.Floor(Plate.Area() / binItem.Area()); - - int qty = item.Quantity < maxQty - ? item.Quantity - : maxQty; - - for (int j = 0; j < qty; j++) - binItems.Add(binItem.Clone() as Item); - } - - return binItems; - } - - private Item ConvertToRectangleItem(NestItem item, int id = 0) - { - var box = item.Drawing.Program.BoundingBox(); - - box.Width += Plate.PartSpacing; - box.Height += Plate.PartSpacing; - - - - return new Item - { - Id = id, - Location = box.Location, - Size = box.Size - }; - } } } diff --git a/OpenNest.Engine/RectanglePacking/BinConverter.cs b/OpenNest.Engine/RectanglePacking/BinConverter.cs new file mode 100644 index 0000000..930f180 --- /dev/null +++ b/OpenNest.Engine/RectanglePacking/BinConverter.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using OpenNest.Geometry; +using OpenNest.Math; + +namespace OpenNest.RectanglePacking +{ + internal static class BinConverter + { + public static Bin CreateBin(Box area, double partSpacing) + { + var bin = new Bin + { + Location = area.Location, + Size = area.Size + }; + + bin.Width += partSpacing; + bin.Height += partSpacing; + + return bin; + } + + public static Item ToItem(NestItem item, double partSpacing, int id = 0) + { + var box = item.Drawing.Program.BoundingBox(); + + box.Width += partSpacing; + box.Height += partSpacing; + + return new Item + { + Id = id, + Location = box.Location, + Size = box.Size + }; + } + + public static List ToItems(List items, double partSpacing, double plateArea) + { + var binItems = new List(); + + for (var i = 0; i < items.Count; i++) + { + var item = items[i]; + var binItem = ToItem(item, partSpacing, i); + + var maxQty = (int)System.Math.Floor(plateArea / binItem.Area()); + var qty = item.Quantity < maxQty ? item.Quantity : maxQty; + + for (var j = 0; j < qty; j++) + binItems.Add(binItem.Clone() as Item); + } + + return binItems; + } + + public static List ToParts(Bin bin, List items) + { + var parts = new List(); + + foreach (var item in bin.Items) + { + var nestItem = items[item.Id]; + var part = ToPart(item, nestItem.Drawing); + parts.Add(part); + } + + return parts; + } + + private static Part ToPart(Item item, Drawing dwg) + { + var part = new Part(dwg); + + if (item.IsRotated) + part.Rotate(Angle.HalfPI); + + var boundingBox = part.Program.BoundingBox(); + var offset = item.Location - boundingBox.Location; + + part.Offset(offset); + + return part; + } + } +} diff --git a/OpenNest.Engine/RotationAnalysis.cs b/OpenNest.Engine/RotationAnalysis.cs new file mode 100644 index 0000000..65dac9d --- /dev/null +++ b/OpenNest.Engine/RotationAnalysis.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.Linq; +using OpenNest.Converters; +using OpenNest.Geometry; +using OpenNest.Math; + +namespace OpenNest +{ + internal static class RotationAnalysis + { + /// + /// Finds the rotation angle that minimizes the bounding rectangle of a drawing's + /// largest shape, constrained by the NestItem's rotation range. + /// + public static double FindBestRotation(NestItem item) + { + var entities = ConvertProgram.ToGeometry(item.Drawing.Program) + .Where(e => e.Layer != SpecialLayers.Rapid); + + var shapes = Helper.GetShapes(entities); + + if (shapes.Count == 0) + return 0; + + // Find the largest shape (outer profile). + var largest = shapes[0]; + var largestArea = largest.Area(); + + for (var i = 1; i < shapes.Count; i++) + { + var area = shapes[i].Area(); + if (area > largestArea) + { + largest = shapes[i]; + largestArea = area; + } + } + + // Convert to polygon so arcs are properly represented as line segments. + // Shape.FindBestRotation() uses Entity cardinal points which are incorrect + // for arcs that don't sweep through all 4 cardinal directions. + var polygon = largest.ToPolygonWithTolerance(0.1); + + BoundingRectangleResult result; + + if (item.RotationStart.IsEqualTo(0) && item.RotationEnd.IsEqualTo(0)) + result = polygon.FindBestRotation(); + else + result = polygon.FindBestRotation(item.RotationStart, item.RotationEnd); + + // Negate the angle to align the minimum bounding rectangle with the axes. + return -result.Angle; + } + + /// + /// Computes the convex hull of the parts' geometry and returns the unique + /// edge angles, suitable for use as candidate rotation angles. + /// + public static List FindHullEdgeAngles(List parts) + { + var points = new List(); + + foreach (var part in parts) + { + var entities = ConvertProgram.ToGeometry(part.Program) + .Where(e => e.Layer != SpecialLayers.Rapid); + + var shapes = Helper.GetShapes(entities); + + foreach (var shape in shapes) + { + var polygon = shape.ToPolygonWithTolerance(0.1); + + foreach (var vertex in polygon.Vertices) + points.Add(vertex + part.Location); + } + } + + if (points.Count < 3) + return new List { 0 }; + + var hull = ConvexHull.Compute(points); + var vertices = hull.Vertices; + var n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count; + + var angles = new List { 0 }; + + for (var i = 0; i < n; i++) + { + var next = (i + 1) % n; + var dx = vertices[next].X - vertices[i].X; + var dy = vertices[next].Y - vertices[i].Y; + + if (dx * dx + dy * dy < Tolerance.Epsilon) + continue; + + var angle = -System.Math.Atan2(dy, dx); + + if (!angles.Any(a => a.IsEqualTo(angle))) + angles.Add(angle); + } + + return angles; + } + } +} diff --git a/OpenNest/Forms/BestFitViewerForm.cs b/OpenNest/Forms/BestFitViewerForm.cs index 90f70ff..7ca9b45 100644 --- a/OpenNest/Forms/BestFitViewerForm.cs +++ b/OpenNest/Forms/BestFitViewerForm.cs @@ -126,7 +126,7 @@ namespace OpenNest.Forms result.BoundingWidth, result.BoundingHeight); - var parts = NestEngine.BuildPairParts(result, drawing); + var parts = result.BuildParts(drawing); foreach (var part in parts) view.Plate.Parts.Add(part); diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 99918fa..4554049 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -537,7 +537,7 @@ namespace OpenNest.Forms { if (form.ShowDialog(this) == DialogResult.OK && form.SelectedResult != null) { - var parts = NestEngine.BuildPairParts(form.SelectedResult, drawing); + var parts = form.SelectedResult.BuildParts(drawing); activeForm.PlateView.SetAction(typeof(ActionClone), parts); } }