feat: add StripeFiller.FindAngleForTargetSpan with scan-then-bisect
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
124
OpenNest.Engine/Fill/StripeFiller.cs
Normal file
124
OpenNest.Engine/Fill/StripeFiller.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace OpenNest.Engine.Fill;
|
||||
|
||||
public class StripeFiller
|
||||
{
|
||||
private const int MaxPairCandidates = 5;
|
||||
private const int MaxConvergenceIterations = 20;
|
||||
private const int AngleSamples = 36;
|
||||
|
||||
private readonly FillContext _context;
|
||||
private readonly NestDirection _primaryAxis;
|
||||
|
||||
public StripeFiller(FillContext context, NestDirection primaryAxis)
|
||||
{
|
||||
_context = context;
|
||||
_primaryAxis = primaryAxis;
|
||||
}
|
||||
|
||||
public List<Part> Fill()
|
||||
{
|
||||
// Placeholder — implemented in Task 3
|
||||
return new List<Part>();
|
||||
}
|
||||
|
||||
public static double FindAngleForTargetSpan(
|
||||
List<Part> patternParts, double targetSpan, NestDirection axis)
|
||||
{
|
||||
var bestAngle = 0.0;
|
||||
var bestDiff = double.MaxValue;
|
||||
var samples = new (double angle, double span)[AngleSamples + 1];
|
||||
|
||||
for (var i = 0; i <= AngleSamples; i++)
|
||||
{
|
||||
var angle = i * Angle.HalfPI / AngleSamples;
|
||||
var span = GetRotatedSpan(patternParts, angle, axis);
|
||||
samples[i] = (angle, span);
|
||||
|
||||
var diff = System.Math.Abs(span - targetSpan);
|
||||
if (diff < bestDiff)
|
||||
{
|
||||
bestDiff = diff;
|
||||
bestAngle = angle;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDiff < Tolerance.Epsilon)
|
||||
return bestAngle;
|
||||
|
||||
for (var i = 0; i < samples.Length - 1; i++)
|
||||
{
|
||||
var (a1, s1) = samples[i];
|
||||
var (a2, s2) = samples[i + 1];
|
||||
|
||||
if ((s1 <= targetSpan && targetSpan <= s2) ||
|
||||
(s2 <= targetSpan && targetSpan <= s1))
|
||||
{
|
||||
var result = BisectForTarget(patternParts, a1, a2, targetSpan, axis);
|
||||
var resultSpan = GetRotatedSpan(patternParts, result, axis);
|
||||
var resultDiff = System.Math.Abs(resultSpan - targetSpan);
|
||||
|
||||
if (resultDiff < bestDiff)
|
||||
{
|
||||
bestDiff = resultDiff;
|
||||
bestAngle = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestAngle;
|
||||
}
|
||||
|
||||
private static double BisectForTarget(
|
||||
List<Part> patternParts, double lo, double hi,
|
||||
double targetSpan, NestDirection axis)
|
||||
{
|
||||
var bestAngle = lo;
|
||||
var bestDiff = double.MaxValue;
|
||||
|
||||
for (var i = 0; i < 30; i++)
|
||||
{
|
||||
var mid = (lo + hi) / 2;
|
||||
var span = GetRotatedSpan(patternParts, mid, axis);
|
||||
var diff = System.Math.Abs(span - targetSpan);
|
||||
|
||||
if (diff < bestDiff)
|
||||
{
|
||||
bestDiff = diff;
|
||||
bestAngle = mid;
|
||||
}
|
||||
|
||||
if (diff < Tolerance.Epsilon)
|
||||
break;
|
||||
|
||||
var loSpan = GetRotatedSpan(patternParts, lo, axis);
|
||||
if ((loSpan < targetSpan && span < targetSpan) ||
|
||||
(loSpan > targetSpan && span > targetSpan))
|
||||
lo = mid;
|
||||
else
|
||||
hi = mid;
|
||||
}
|
||||
|
||||
return bestAngle;
|
||||
}
|
||||
|
||||
private static double GetRotatedSpan(
|
||||
List<Part> patternParts, double angle, NestDirection axis)
|
||||
{
|
||||
var rotated = FillHelpers.BuildRotatedPattern(patternParts, angle);
|
||||
return axis == NestDirection.Horizontal
|
||||
? rotated.BoundingBox.Width
|
||||
: rotated.BoundingBox.Length;
|
||||
}
|
||||
|
||||
private static double GetDimension(Box box, NestDirection axis)
|
||||
{
|
||||
return axis == NestDirection.Horizontal ? box.Width : box.Length;
|
||||
}
|
||||
}
|
||||
63
OpenNest.Tests/Strategies/StripeFillerTests.cs
Normal file
63
OpenNest.Tests/Strategies/StripeFillerTests.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Strategies;
|
||||
|
||||
public class StripeFillerTests
|
||||
{
|
||||
private static Drawing MakeRectDrawing(double w, double h, string name = "rect")
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
return new Drawing(name, pgm);
|
||||
}
|
||||
|
||||
private static Pattern MakeRectPattern(double w, double h)
|
||||
{
|
||||
var drawing = MakeRectDrawing(w, h);
|
||||
var part = Part.CreateAtOrigin(drawing);
|
||||
var pattern = new Pattern();
|
||||
pattern.Parts.Add(part);
|
||||
pattern.UpdateBounds();
|
||||
return pattern;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindAngleForTargetSpan_ZeroAngle_WhenAlreadyMatches()
|
||||
{
|
||||
var pattern = MakeRectPattern(20, 10);
|
||||
var angle = StripeFiller.FindAngleForTargetSpan(
|
||||
pattern.Parts, 20.0, NestDirection.Horizontal);
|
||||
|
||||
Assert.True(System.Math.Abs(angle) < 0.05,
|
||||
$"Expected angle near 0, got {OpenNest.Math.Angle.ToDegrees(angle):F1}°");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindAngleForTargetSpan_FindsLargerSpan()
|
||||
{
|
||||
var pattern = MakeRectPattern(20, 10);
|
||||
var angle = StripeFiller.FindAngleForTargetSpan(
|
||||
pattern.Parts, 22.0, NestDirection.Horizontal);
|
||||
|
||||
var rotated = FillHelpers.BuildRotatedPattern(pattern.Parts, angle);
|
||||
var span = rotated.BoundingBox.Width;
|
||||
Assert.True(System.Math.Abs(span - 22.0) < 0.5,
|
||||
$"Expected span ~22, got {span:F2} at {OpenNest.Math.Angle.ToDegrees(angle):F1}°");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindAngleForTargetSpan_ReturnsClosest_WhenUnreachable()
|
||||
{
|
||||
var pattern = MakeRectPattern(20, 10);
|
||||
var angle = StripeFiller.FindAngleForTargetSpan(
|
||||
pattern.Parts, 30.0, NestDirection.Horizontal);
|
||||
|
||||
Assert.True(angle >= 0 && angle <= System.Math.PI / 2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user