fix: orient pair short side along primary axis before convergence
The convergence loop now ensures the pair starts with its short side parallel to the primary axis, maximizing the number of pairs that fit. Also adds ConvergeStripeAngleShrink to try N+1 narrower pairs, and evaluates both expand and shrink results to pick the better grid. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,41 +49,31 @@ public class StripeFiller
|
||||
var candidate = bestFits[i];
|
||||
var pairParts = candidate.BuildParts(drawing);
|
||||
|
||||
var (angle, waste, count) = ConvergeStripeAngle(
|
||||
// Try both expand (N wider pairs) and shrink (N+1 narrower pairs)
|
||||
var expandResult = ConvergeStripeAngle(
|
||||
pairParts, sheetSpan, spacing, _primaryAxis, _context.Token);
|
||||
var shrinkResult = ConvergeStripeAngleShrink(
|
||||
pairParts, sheetSpan, spacing, _primaryAxis, _context.Token);
|
||||
|
||||
if (count <= 0)
|
||||
continue;
|
||||
|
||||
var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
||||
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
|
||||
var stripeBox = MakeStripeBox(workArea, perpDim, _primaryAxis);
|
||||
var stripeEngine = new FillLinear(stripeBox, spacing);
|
||||
var stripeParts = stripeEngine.Fill(rotatedPattern, _primaryAxis);
|
||||
|
||||
if (stripeParts == null || stripeParts.Count == 0)
|
||||
continue;
|
||||
|
||||
var stripePattern = new Pattern();
|
||||
stripePattern.Parts.AddRange(stripeParts);
|
||||
stripePattern.UpdateBounds();
|
||||
|
||||
var gridEngine = new FillLinear(workArea, spacing);
|
||||
var gridParts = gridEngine.Fill(stripePattern, perpAxis);
|
||||
|
||||
if (gridParts == null || gridParts.Count == 0)
|
||||
continue;
|
||||
|
||||
var allParts = new List<Part>(gridParts);
|
||||
var remnantParts = FillRemnant(gridParts, drawing, angle, workArea, spacing);
|
||||
if (remnantParts != null)
|
||||
allParts.AddRange(remnantParts);
|
||||
|
||||
var score = FillScore.Compute(allParts, workArea);
|
||||
if (bestParts == null || score > bestScore)
|
||||
// Evaluate both convergence results
|
||||
foreach (var (angle, waste, count) in new[] { expandResult, shrinkResult })
|
||||
{
|
||||
bestParts = allParts;
|
||||
bestScore = score;
|
||||
if (count <= 0)
|
||||
continue;
|
||||
|
||||
var result = BuildGrid(pairParts, angle, workArea, spacing, perpAxis, drawing);
|
||||
if (result == null || result.Count == 0)
|
||||
continue;
|
||||
|
||||
var score = FillScore.Compute(result, workArea);
|
||||
Debug.WriteLine($"[StripeFiller] {strategyName} candidate {i}: angle={Angle.ToDegrees(angle):F1}°, " +
|
||||
$"pairsAcross={count}, waste={waste:F2}, grid={result.Count} parts");
|
||||
|
||||
if (bestParts == null || score > bestScore)
|
||||
{
|
||||
bestParts = result;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
NestEngineBase.ReportProgress(_context.Progress, NestPhase.Custom,
|
||||
@@ -94,6 +84,44 @@ public class StripeFiller
|
||||
return bestParts ?? new List<Part>();
|
||||
}
|
||||
|
||||
private List<Part> BuildGrid(List<Part> pairParts, double angle,
|
||||
Box workArea, double spacing, NestDirection perpAxis, Drawing drawing)
|
||||
{
|
||||
var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
||||
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
|
||||
var stripeBox = MakeStripeBox(workArea, perpDim, _primaryAxis);
|
||||
var stripeEngine = new FillLinear(stripeBox, spacing);
|
||||
var stripeParts = stripeEngine.Fill(rotatedPattern, _primaryAxis);
|
||||
|
||||
if (stripeParts == null || stripeParts.Count == 0)
|
||||
return null;
|
||||
|
||||
Debug.WriteLine($"[StripeFiller] Stripe: {stripeParts.Count} parts, " +
|
||||
$"box={stripeBox.Width:F2}x{stripeBox.Length:F2}");
|
||||
|
||||
var stripePattern = new Pattern();
|
||||
stripePattern.Parts.AddRange(stripeParts);
|
||||
stripePattern.UpdateBounds();
|
||||
|
||||
var gridEngine = new FillLinear(workArea, spacing);
|
||||
var gridParts = gridEngine.Fill(stripePattern, perpAxis);
|
||||
|
||||
if (gridParts == null || gridParts.Count == 0)
|
||||
return null;
|
||||
|
||||
Debug.WriteLine($"[StripeFiller] Grid: {gridParts.Count} parts");
|
||||
|
||||
var allParts = new List<Part>(gridParts);
|
||||
var remnantParts = FillRemnant(gridParts, drawing, angle, workArea, spacing);
|
||||
if (remnantParts != null)
|
||||
{
|
||||
Debug.WriteLine($"[StripeFiller] Remnant: {remnantParts.Count} parts");
|
||||
allParts.AddRange(remnantParts);
|
||||
}
|
||||
|
||||
return allParts;
|
||||
}
|
||||
|
||||
private List<BestFitResult> GetPairCandidates()
|
||||
{
|
||||
List<BestFitResult> bestFits;
|
||||
@@ -200,18 +228,36 @@ public class StripeFiller
|
||||
return bestAngle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the rotation angle that orients the pair with its short side
|
||||
/// along the given axis. Returns 0 if already oriented, PI/2 if rotated.
|
||||
/// </summary>
|
||||
private static double OrientShortSideAlong(List<Part> patternParts, NestDirection axis)
|
||||
{
|
||||
var box = FillHelpers.BuildRotatedPattern(patternParts, 0).BoundingBox;
|
||||
var span0 = GetDimension(box, axis);
|
||||
var perpSpan0 = axis == NestDirection.Horizontal ? box.Length : box.Width;
|
||||
|
||||
// Short side already along axis
|
||||
if (span0 <= perpSpan0)
|
||||
return 0;
|
||||
|
||||
// Rotate 90° to put short side along axis
|
||||
return Angle.HalfPI;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iteratively finds the rotation angle where N copies of the pattern
|
||||
/// span the given dimension with minimal waste.
|
||||
/// span the given dimension with minimal waste by expanding pair width.
|
||||
/// 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 currentAngle = OrientShortSideAlong(patternParts, axis);
|
||||
var bestWaste = double.MaxValue;
|
||||
var bestAngle = 0.0;
|
||||
var bestAngle = currentAngle;
|
||||
var bestCount = 0;
|
||||
var tolerance = sheetSpan * 0.001;
|
||||
|
||||
@@ -251,6 +297,88 @@ public class StripeFiller
|
||||
return (bestAngle, bestWaste, bestCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries fitting N+1 narrower pairs by shrinking the pair width.
|
||||
/// Complements ConvergeStripeAngle which only expands.
|
||||
/// </summary>
|
||||
public static (double Angle, double Waste, int Count) ConvergeStripeAngleShrink(
|
||||
List<Part> patternParts, double sheetSpan, double spacing,
|
||||
NestDirection axis, CancellationToken token = default)
|
||||
{
|
||||
// Orient short side along axis before computing N
|
||||
var baseAngle = OrientShortSideAlong(patternParts, axis);
|
||||
var naturalPattern = FillHelpers.BuildRotatedPattern(patternParts, baseAngle);
|
||||
var naturalSpan = GetDimension(naturalPattern.BoundingBox, axis);
|
||||
|
||||
if (naturalSpan + spacing <= 0)
|
||||
return (0, double.MaxValue, 0);
|
||||
|
||||
var naturalN = (int)System.Math.Floor((sheetSpan + spacing) / (naturalSpan + spacing));
|
||||
var targetN = naturalN + 1;
|
||||
|
||||
// Target span for N+1 pairs
|
||||
var targetSpan = (sheetSpan + spacing) / targetN - spacing;
|
||||
|
||||
if (targetSpan <= 0)
|
||||
return (0, double.MaxValue, 0);
|
||||
|
||||
// Find the angle that shrinks the pair to targetSpan
|
||||
var angle = FindAngleForTargetSpan(patternParts, targetSpan, axis);
|
||||
|
||||
// Verify the actual span at this angle
|
||||
var rotated = FillHelpers.BuildRotatedPattern(patternParts, angle);
|
||||
var actualSpan = GetDimension(rotated.BoundingBox, axis);
|
||||
var actualN = (int)System.Math.Floor((sheetSpan + spacing) / (actualSpan + spacing));
|
||||
|
||||
if (actualN <= 0)
|
||||
return (0, double.MaxValue, 0);
|
||||
|
||||
var usedSpan = actualN * (actualSpan + spacing) - spacing;
|
||||
var waste = sheetSpan - usedSpan;
|
||||
|
||||
// Now converge from this starting point
|
||||
var bestWaste = waste;
|
||||
var bestAngle = angle;
|
||||
var bestCount = actualN;
|
||||
var tolerance = sheetSpan * 0.001;
|
||||
var currentAngle = angle;
|
||||
|
||||
for (var iteration = 0; iteration < MaxConvergenceIterations; iteration++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (bestWaste <= tolerance)
|
||||
break;
|
||||
|
||||
rotated = FillHelpers.BuildRotatedPattern(patternParts, currentAngle);
|
||||
var pairSpan = GetDimension(rotated.BoundingBox, axis);
|
||||
|
||||
var n = (int)System.Math.Floor((sheetSpan + spacing) / (pairSpan + spacing));
|
||||
if (n <= 0)
|
||||
break;
|
||||
|
||||
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 newTarget = pairSpan + delta;
|
||||
|
||||
currentAngle = FindAngleForTargetSpan(patternParts, newTarget, axis);
|
||||
}
|
||||
|
||||
return (bestAngle, bestWaste, bestCount);
|
||||
}
|
||||
|
||||
private static double BisectForTarget(
|
||||
List<Part> patternParts, double lo, double hi,
|
||||
double targetSpan, NestDirection axis)
|
||||
|
||||
@@ -114,12 +114,13 @@ public class StripeFillerTests
|
||||
[Fact]
|
||||
public void ConvergeStripeAngle_HandlesExactFit()
|
||||
{
|
||||
// 10x5 pattern: short side (5) oriented along axis, so more pairs fit
|
||||
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}");
|
||||
Assert.True(count >= 10, $"Expected at least 10 pairs, got {count}");
|
||||
Assert.True(waste < 1.0, $"Expected low waste, got {waste:F2}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user