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)