feat: add StripeFiller.ConvergeStripeAngle iterative convergence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 07:37:59 -04:00
parent 904d30d05d
commit 2ae1d513cf
2 changed files with 84 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Math;
@@ -75,6 +76,57 @@ public class StripeFiller
return bestAngle;
}
/// <summary>
/// Iteratively finds the rotation angle where N copies of the pattern
/// span the given dimension with minimal waste.
/// Returns (angle, waste, pairCount).
/// </summary>
public static (double Angle, double Waste, int Count) ConvergeStripeAngle(
List<Part> patternParts, double sheetSpan, double spacing,
NestDirection axis, CancellationToken token = default)
{
var currentAngle = 0.0;
var bestWaste = double.MaxValue;
var bestAngle = 0.0;
var bestCount = 0;
var tolerance = sheetSpan * 0.001;
for (var iteration = 0; iteration < MaxConvergenceIterations; iteration++)
{
token.ThrowIfCancellationRequested();
var rotated = FillHelpers.BuildRotatedPattern(patternParts, currentAngle);
var pairSpan = GetDimension(rotated.BoundingBox, axis);
if (pairSpan + spacing <= 0)
break;
var n = (int)System.Math.Floor((sheetSpan + spacing) / (pairSpan + spacing));
if (n <= 0)
break;
var usedSpan = n * (pairSpan + spacing) - spacing;
var remaining = sheetSpan - usedSpan;
if (remaining < bestWaste)
{
bestWaste = remaining;
bestAngle = currentAngle;
bestCount = n;
}
if (remaining <= tolerance)
break;
var delta = remaining / n;
var targetSpan = pairSpan + delta;
currentAngle = FindAngleForTargetSpan(patternParts, targetSpan, axis);
}
return (bestAngle, bestWaste, bestCount);
}
private static double BisectForTarget(
List<Part> patternParts, double lo, double hi,
double targetSpan, NestDirection axis)

View File

@@ -60,4 +60,36 @@ public class StripeFillerTests
Assert.True(angle >= 0 && angle <= System.Math.PI / 2);
}
[Fact]
public void ConvergeStripeAngle_ReducesWaste()
{
var pattern = MakeRectPattern(20, 10);
var (angle, waste, count) = StripeFiller.ConvergeStripeAngle(
pattern.Parts, 120.0, 0.5, NestDirection.Horizontal);
Assert.True(count >= 5, $"Expected at least 5 pairs, got {count}");
Assert.True(waste < 18.0, $"Expected waste < 18, got {waste:F2}");
}
[Fact]
public void ConvergeStripeAngle_HandlesExactFit()
{
var pattern = MakeRectPattern(10, 5);
var (angle, waste, count) = StripeFiller.ConvergeStripeAngle(
pattern.Parts, 100.0, 0.0, NestDirection.Horizontal);
Assert.Equal(10, count);
Assert.True(waste < 0.2, $"Expected near-zero waste, got {waste:F2}");
}
[Fact]
public void ConvergeStripeAngle_Vertical()
{
var pattern = MakeRectPattern(10, 20);
var (angle, waste, count) = StripeFiller.ConvergeStripeAngle(
pattern.Parts, 120.0, 0.5, NestDirection.Vertical);
Assert.True(count >= 5, $"Expected at least 5 pairs, got {count}");
}
}