diff --git a/OpenNest.Core/Geometry/Arc.cs b/OpenNest.Core/Geometry/Arc.cs
index dfd805f..5d7393d 100644
--- a/OpenNest.Core/Geometry/Arc.cs
+++ b/OpenNest.Core/Geometry/Arc.cs
@@ -203,20 +203,24 @@ namespace OpenNest.Geometry
///
/// Number of parts to divide the arc into.
///
- public List ToPoints(int segments = 1000)
+ public List ToPoints(int segments = 1000, bool circumscribe = false)
{
var points = new List();
var stepAngle = reversed
? -SweepAngle() / segments
: SweepAngle() / segments;
+ var r = circumscribe && segments > 0
+ ? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0)
+ : Radius;
+
for (int i = 0; i <= segments; ++i)
{
var angle = stepAngle * i + StartAngle;
points.Add(new Vector(
- System.Math.Cos(angle) * Radius + Center.X,
- System.Math.Sin(angle) * Radius + Center.Y));
+ System.Math.Cos(angle) * r + Center.X,
+ System.Math.Sin(angle) * r + Center.Y));
}
return points;
diff --git a/OpenNest.Core/Geometry/Circle.cs b/OpenNest.Core/Geometry/Circle.cs
index 5d57e9e..49f9979 100644
--- a/OpenNest.Core/Geometry/Circle.cs
+++ b/OpenNest.Core/Geometry/Circle.cs
@@ -135,18 +135,22 @@ namespace OpenNest.Geometry
return System.Math.Max(3, (int)System.Math.Ceiling(Angle.TwoPI / maxAngle));
}
- public List ToPoints(int segments = 1000)
+ public List ToPoints(int segments = 1000, bool circumscribe = false)
{
var points = new List();
var stepAngle = Angle.TwoPI / segments;
+ var r = circumscribe && segments > 0
+ ? Radius / System.Math.Cos(stepAngle / 2.0)
+ : Radius;
+
for (int i = 0; i <= segments; ++i)
{
var angle = stepAngle * i;
points.Add(new Vector(
- System.Math.Cos(angle) * Radius + Center.X,
- System.Math.Sin(angle) * Radius + Center.Y));
+ System.Math.Cos(angle) * r + Center.X,
+ System.Math.Sin(angle) * r + Center.Y));
}
return points;
diff --git a/OpenNest.Core/Geometry/Shape.cs b/OpenNest.Core/Geometry/Shape.cs
index 7f55f10..33fb910 100644
--- a/OpenNest.Core/Geometry/Shape.cs
+++ b/OpenNest.Core/Geometry/Shape.cs
@@ -247,7 +247,7 @@ namespace OpenNest.Geometry
/// Converts the shape to a polygon using a chord tolerance to determine
/// the number of segments per arc/circle.
///
- public Polygon ToPolygonWithTolerance(double tolerance)
+ public Polygon ToPolygonWithTolerance(double tolerance, bool circumscribe = false)
{
var polygon = new Polygon();
@@ -257,7 +257,7 @@ namespace OpenNest.Geometry
{
case EntityType.Arc:
var arc = (Arc)entity;
- polygon.Vertices.AddRange(arc.ToPoints(arc.SegmentsForTolerance(tolerance)));
+ polygon.Vertices.AddRange(arc.ToPoints(arc.SegmentsForTolerance(tolerance), circumscribe));
break;
case EntityType.Line:
@@ -271,7 +271,7 @@ namespace OpenNest.Geometry
case EntityType.Circle:
var circle = (Circle)entity;
- polygon.Vertices.AddRange(circle.ToPoints(circle.SegmentsForTolerance(tolerance)));
+ polygon.Vertices.AddRange(circle.ToPoints(circle.SegmentsForTolerance(tolerance), circumscribe));
break;
default:
diff --git a/OpenNest.Engine/FillLinear.cs b/OpenNest.Engine/FillLinear.cs
index c6cae63..63e32ae 100644
--- a/OpenNest.Engine/FillLinear.cs
+++ b/OpenNest.Engine/FillLinear.cs
@@ -63,7 +63,10 @@ namespace OpenNest
if (slideDistance >= double.MaxValue || slideDistance < 0)
return bboxDim + PartSpacing;
- return bboxDim - slideDistance;
+ // The geometry-aware slide can produce a copy distance smaller than
+ // the part itself when inflated corner/arc vertices interact spuriously.
+ // Clamp to bboxDim + PartSpacing to prevent bounding box overlap.
+ return System.Math.Max(bboxDim - slideDistance, bboxDim + PartSpacing);
}
///
@@ -82,7 +85,9 @@ namespace OpenNest
var stationaryLines = boundary.GetLines(partA.Location, opposite);
var slideDistance = Helper.DirectionalDistance(movingLines, stationaryLines, pushDir);
- return ComputeCopyDistance(bboxDim, slideDistance);
+ var copyDist = ComputeCopyDistance(bboxDim, slideDistance);
+ System.Diagnostics.Debug.WriteLine($"[FindCopyDistance] dir={direction} bboxDim={bboxDim:F4} slide={slideDistance:F4} copyDist={copyDist:F4} spacing={PartSpacing:F4} locA={partA.Location} locB={locationB} movingEdges={movingLines.Count} stationaryEdges={stationaryLines.Count}");
+ return copyDist;
}
///
@@ -171,7 +176,9 @@ namespace OpenNest
var stationaryLines = GetPatternLines(patternA, boundary, opposite);
var slideDistance = Helper.DirectionalDistance(movingLines, stationaryLines, pushDir);
- return ComputeCopyDistance(bboxDim, slideDistance);
+ var copyDist = ComputeCopyDistance(bboxDim, slideDistance);
+ System.Diagnostics.Debug.WriteLine($"[FindSinglePartPatternCopyDist] dir={direction} bboxDim={bboxDim:F4} slide={slideDistance:F4} copyDist={copyDist:F4} spacing={PartSpacing:F4} patternParts={patternA.Parts.Count} movingEdges={movingLines.Count} stationaryEdges={stationaryLines.Count}");
+ return copyDist;
}
///
@@ -341,10 +348,14 @@ namespace OpenNest
var perpAxis = PerpendicularAxis(direction);
var gridResult = FillRecursive(rowPattern, perpAxis, depth + 1);
+ System.Diagnostics.Debug.WriteLine($"[FillRecursive] Grid: {gridResult.Count} parts, rowSize={rowPattern.Parts.Count}, dir={direction}");
+
// Fill the remaining strip (after the last full row/column)
// with individual parts from the seed pattern.
var remaining = FillRemainingStrip(gridResult, pattern, perpAxis, direction);
+ System.Diagnostics.Debug.WriteLine($"[FillRecursive] Remainder: {remaining.Count} parts");
+
if (remaining.Count > 0)
gridResult.AddRange(remaining);
@@ -352,6 +363,8 @@ namespace OpenNest
// fit more parts than the extra row contained.
var fewerResult = TryFewerRows(gridResult, rowPattern, pattern, perpAxis, direction);
+ System.Diagnostics.Debug.WriteLine($"[FillRecursive] TryFewerRows: {fewerResult?.Count ?? -1} vs grid+remainder={gridResult.Count}");
+
if (fewerResult != null && fewerResult.Count > gridResult.Count)
return fewerResult;
@@ -377,9 +390,14 @@ namespace OpenNest
{
var rowPartCount = rowPattern.Parts.Count;
+ System.Diagnostics.Debug.WriteLine($"[TryFewerRows] fullResult={fullResult.Count}, rowPartCount={rowPartCount}, tiledAxis={tiledAxis}");
+
// Need at least 2 rows for this to make sense (remove 1, keep 1+).
if (fullResult.Count < rowPartCount * 2)
+ {
+ System.Diagnostics.Debug.WriteLine($"[TryFewerRows] Skipped: too few parts for 2 rows");
return null;
+ }
// Remove the last row's worth of parts.
var fewerParts = new List(fullResult.Count - rowPartCount);
@@ -387,8 +405,22 @@ namespace OpenNest
for (var i = 0; i < fullResult.Count - rowPartCount; i++)
fewerParts.Add(fullResult[i]);
+ // Find the top/right edge of the kept parts for logging.
+ var edge = double.MinValue;
+ foreach (var part in fewerParts)
+ {
+ var e = tiledAxis == NestDirection.Vertical
+ ? part.BoundingBox.Top
+ : part.BoundingBox.Right;
+ if (e > edge) edge = e;
+ }
+
+ System.Diagnostics.Debug.WriteLine($"[TryFewerRows] Kept {fewerParts.Count} parts, edge={edge:F2}, workArea={WorkArea}");
+
var remaining = FillRemainingStrip(fewerParts, seedPattern, tiledAxis, primaryAxis);
+ System.Diagnostics.Debug.WriteLine($"[TryFewerRows] Remainder fill: {remaining.Count} parts (need > {rowPartCount} to improve)");
+
if (remaining.Count <= rowPartCount)
return null;
@@ -493,16 +525,25 @@ namespace OpenNest
rotations.Add((seedPart.BaseDrawing, seedPart.Rotation));
}
- foreach (var (drawing, rotation) in rotations)
+ var bag = new System.Collections.Concurrent.ConcurrentBag>();
+
+ System.Threading.Tasks.Parallel.ForEach(rotations, entry =>
{
- var h = filler.Fill(drawing, rotation, NestDirection.Horizontal);
- var v = filler.Fill(drawing, rotation, NestDirection.Vertical);
+ var localFiller = new FillLinear(remainingStrip, PartSpacing);
+ var h = localFiller.Fill(entry.drawing, entry.rotation, NestDirection.Horizontal);
+ var v = localFiller.Fill(entry.drawing, entry.rotation, NestDirection.Vertical);
- if (h != null && h.Count > 0 && (best == null || h.Count > best.Count))
- best = h;
+ if (h != null && h.Count > 0)
+ bag.Add(h);
- if (v != null && v.Count > 0 && (best == null || v.Count > best.Count))
- best = v;
+ if (v != null && v.Count > 0)
+ bag.Add(v);
+ });
+
+ foreach (var candidate in bag)
+ {
+ if (best == null || candidate.Count > best.Count)
+ best = candidate;
}
return best ?? new List();
diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs
index 536e861..394660a 100644
--- a/OpenNest.Engine/NestEngine.cs
+++ b/OpenNest.Engine/NestEngine.cs
@@ -85,18 +85,31 @@ namespace OpenNest
}
}
- List best = null;
+ var linearBag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List parts)>();
- foreach (var angle in angles)
+ System.Threading.Tasks.Parallel.ForEach(angles, angle =>
{
- var h = engine.Fill(item.Drawing, angle, NestDirection.Horizontal);
- var v = engine.Fill(item.Drawing, angle, NestDirection.Vertical);
+ var localEngine = new FillLinear(workArea, Plate.PartSpacing);
+ var h = localEngine.Fill(item.Drawing, angle, NestDirection.Horizontal);
+ var v = localEngine.Fill(item.Drawing, angle, NestDirection.Vertical);
- if (IsBetterFill(h, best, workArea))
- best = h;
+ if (h != null && h.Count > 0)
+ linearBag.Add((FillScore.Compute(h, workArea), h));
- if (IsBetterFill(v, best, workArea))
- best = v;
+ if (v != null && v.Count > 0)
+ linearBag.Add((FillScore.Compute(v, workArea), v));
+ });
+
+ List best = null;
+ var bestScore = default(FillScore);
+
+ foreach (var (score, parts) in linearBag)
+ {
+ if (best == null || score > bestScore)
+ {
+ best = parts;
+ bestScore = score;
+ }
}
var bestLinearScore = best != null ? FillScore.Compute(best, workArea) : default;
@@ -294,7 +307,14 @@ namespace OpenNest
List pts;
if (parts[i].Intersects(parts[j], out pts))
+ {
+ var b1 = parts[i].BoundingBox;
+ var b2 = parts[j].BoundingBox;
+ Debug.WriteLine($"[HasOverlaps] Overlap: part[{i}] ({parts[i].BaseDrawing?.Name}) @ ({b1.Left:F2},{b1.Bottom:F2})-({b1.Right:F2},{b1.Top:F2}) rot={parts[i].Rotation:F2}" +
+ $" vs part[{j}] ({parts[j].BaseDrawing?.Name}) @ ({b2.Left:F2},{b2.Bottom:F2})-({b2.Right:F2},{b2.Top:F2}) rot={parts[j].Rotation:F2}" +
+ $" intersections={pts?.Count ?? 0}");
return true;
+ }
}
}
@@ -315,7 +335,10 @@ namespace OpenNest
private bool IsBetterValidFill(List candidate, List current, Box workArea)
{
if (candidate != null && candidate.Count > 0 && HasOverlaps(candidate, Plate.PartSpacing))
+ {
+ Debug.WriteLine($"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})");
return false;
+ }
return IsBetterFill(candidate, current, workArea);
}
@@ -475,23 +498,36 @@ namespace OpenNest
private List FillPattern(FillLinear engine, List groupParts, List angles, Box workArea)
{
- List best = null;
+ var bag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List parts)>();
- foreach (var angle in angles)
+ System.Threading.Tasks.Parallel.ForEach(angles, angle =>
{
var pattern = BuildRotatedPattern(groupParts, angle);
if (pattern.Parts.Count == 0)
- continue;
+ return;
- var h = engine.Fill(pattern, NestDirection.Horizontal);
- var v = engine.Fill(pattern, NestDirection.Vertical);
+ var localEngine = new FillLinear(workArea, engine.PartSpacing);
+ var h = localEngine.Fill(pattern, NestDirection.Horizontal);
+ var v = localEngine.Fill(pattern, NestDirection.Vertical);
- if (IsBetterValidFill(h, best, workArea))
- best = h;
+ if (h != null && h.Count > 0 && !HasOverlaps(h, engine.PartSpacing))
+ bag.Add((FillScore.Compute(h, workArea), h));
- if (IsBetterValidFill(v, best, workArea))
- best = v;
+ if (v != null && v.Count > 0 && !HasOverlaps(v, engine.PartSpacing))
+ bag.Add((FillScore.Compute(v, workArea), v));
+ });
+
+ List best = null;
+ var bestScore = default(FillScore);
+
+ foreach (var (score, parts) in bag)
+ {
+ if (best == null || score > bestScore)
+ {
+ best = parts;
+ bestScore = score;
+ }
}
return best;
diff --git a/OpenNest.Engine/PartBoundary.cs b/OpenNest.Engine/PartBoundary.cs
index 8aafa56..4ab96fc 100644
--- a/OpenNest.Engine/PartBoundary.cs
+++ b/OpenNest.Engine/PartBoundary.cs
@@ -13,7 +13,7 @@ namespace OpenNest
///
public class PartBoundary
{
- private const double ChordTolerance = 0.01;
+ private const double PolygonTolerance = 0.01;
private readonly List _polygons;
private readonly (Vector start, Vector end)[] _leftEdges;
@@ -29,12 +29,14 @@ namespace OpenNest
foreach (var shape in shapes)
{
- var offsetEntity = shape.OffsetEntity(spacing + ChordTolerance, OffsetSide.Left) as Shape;
+ var offsetEntity = shape.OffsetEntity(spacing, OffsetSide.Left) as Shape;
if (offsetEntity == null)
continue;
- var polygon = offsetEntity.ToPolygonWithTolerance(ChordTolerance);
+ // Circumscribe arcs so polygon vertices are always outside
+ // the true arc — guarantees the boundary never under-estimates.
+ var polygon = offsetEntity.ToPolygonWithTolerance(PolygonTolerance, circumscribe: true);
polygon.RemoveSelfIntersections();
_polygons.Add(polygon);
}