diff --git a/OpenNest.Engine/BestFit/BestFitFinder.cs b/OpenNest.Engine/BestFit/BestFitFinder.cs index 074c31d..89d0681 100644 --- a/OpenNest.Engine/BestFit/BestFitFinder.cs +++ b/OpenNest.Engine/BestFit/BestFitFinder.cs @@ -115,7 +115,7 @@ namespace OpenNest.Engine.BestFit foreach (var shape in shapes) { - var polygon = shape.ToPolygonWithTolerance(0.1); + var polygon = shape.ToPolygonWithTolerance(0.01); points.AddRange(polygon.Vertices); } diff --git a/OpenNest.Engine/FillLinear.cs b/OpenNest.Engine/FillLinear.cs index fab809c..b850f74 100644 --- a/OpenNest.Engine/FillLinear.cs +++ b/OpenNest.Engine/FillLinear.cs @@ -15,7 +15,7 @@ namespace OpenNest public Box WorkArea { get; } public double PartSpacing { get; } - + public double HalfSpacing => PartSpacing / 2; private static Vector MakeOffset(NestDirection direction, double distance) @@ -227,7 +227,9 @@ namespace OpenNest /// /// Tiles a pattern along the given axis, returning the cloned parts - /// (does not include the original pattern's parts). + /// (does not include the original pattern's parts). For multi-part + /// patterns, also adds individual parts from the next incomplete copy + /// that still fit within the work area. /// private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries) { @@ -255,6 +257,26 @@ namespace OpenNest count++; } + // For multi-part patterns, try to place individual parts from the + // next copy that didn't fit as a whole. This handles cases where + // e.g. a 2-part pair only partially fits — one part may still be + // within the work area even though the full pattern exceeds it. + if (basePattern.Parts.Count > 1) + { + var partialClone = basePattern.Clone(MakeOffset(direction, copyDistance * count)); + + foreach (var part in partialClone.Parts) + { + if (part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon && + part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon && + part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon && + part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon) + { + result.Add(part); + } + } + } + return result; } diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index 5b99476..0aba633 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using OpenNest.Engine.BestFit; @@ -35,23 +35,48 @@ namespace OpenNest var engine = new FillLinear(workArea, Plate.PartSpacing); - var configs = new[] + // Build candidate rotation angles — always try the best rotation and +90°. + var angles = new List { bestRotation, bestRotation + Angle.HalfPI }; + + // When the work area is narrow relative to the part, sweep rotation + // angles so we can find one that fits the part into the tight strip. + 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.Height); + var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Height); + + if (workAreaShortSide < partLongestSide) { - 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) - }; + // Try every 5° from 0 to 175° to find rotations that fit. + var step = Angle.ToRadians(5); + + for (var a = 0.0; a < System.Math.PI; a += step) + { + if (!angles.Any(existing => existing.IsEqualTo(a))) + angles.Add(a); + } + } List best = null; - foreach (var config in configs) + foreach (var angle in angles) { - if (IsBetterFill(config, best)) - best = config; + var h = engine.Fill(item.Drawing, angle, NestDirection.Horizontal); + var v = engine.Fill(item.Drawing, angle, NestDirection.Vertical); + + if (IsBetterFill(h, best)) + best = h; + + if (IsBetterFill(v, best)) + best = v; } - Debug.WriteLine($"[Fill(NestItem,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}"); + Debug.WriteLine($"[Fill(NestItem,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1} | Angles: {angles.Count}"); // Try rectangle best-fit (mixes orientations to fill remnant strips). var rectResult = FillRectangleBestFit(item, workArea); @@ -152,14 +177,14 @@ namespace OpenNest item.Drawing, Plate.Size.Width, Plate.Size.Height, Plate.PartSpacing); - var keptResults = bestFits.Where(r => r.Keep).Take(50).ToList(); - Debug.WriteLine($"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {keptResults.Count}"); + var candidates = SelectPairCandidates(bestFits, workArea); + Debug.WriteLine($"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); var resultBag = new System.Collections.Concurrent.ConcurrentBag<(int count, List parts)>(); - System.Threading.Tasks.Parallel.For(0, keptResults.Count, i => + System.Threading.Tasks.Parallel.For(0, candidates.Count, i => { - var result = keptResults[i]; + var result = candidates[i]; var pairParts = result.BuildParts(item.Drawing); var angles = RotationAnalysis.FindHullEdgeAngles(pairParts); var engine = new FillLinear(workArea, Plate.PartSpacing); @@ -181,6 +206,41 @@ namespace OpenNest return best ?? new List(); } + /// + /// Selects pair candidates to try for the given work area. Always includes + /// the top 50 by area. For narrow work areas, also includes all pairs whose + /// shortest side fits the strip width — these are candidates that can only + /// be evaluated by actually tiling them into the narrow space. + /// + private List SelectPairCandidates(List bestFits, Box workArea) + { + var kept = bestFits.Where(r => r.Keep).ToList(); + var top = kept.Take(50).ToList(); + + var workShortSide = System.Math.Min(workArea.Width, workArea.Height); + var plateShortSide = System.Math.Min(Plate.Size.Width, Plate.Size.Height); + + // When the work area is significantly narrower than the plate, + // include all pairs that fit the narrow dimension. + if (workShortSide < plateShortSide * 0.5) + { + var stripCandidates = kept + .Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon); + + var existing = new HashSet(top); + + foreach (var r in stripCandidates) + { + if (existing.Add(r)) + top.Add(r); + } + + Debug.WriteLine($"[SelectPairCandidates] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})"); + } + + return top; + } + private bool HasOverlaps(List parts, double spacing) { if (parts == null || parts.Count <= 1)