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); }