diff --git a/OpenNest.Engine/Fill/StripeFiller.cs b/OpenNest.Engine/Fill/StripeFiller.cs new file mode 100644 index 0000000..eab8473 --- /dev/null +++ b/OpenNest.Engine/Fill/StripeFiller.cs @@ -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 Fill() + { + // Placeholder — implemented in Task 3 + return new List(); + } + + public static double FindAngleForTargetSpan( + List 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 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 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; + } +} diff --git a/OpenNest.Tests/Strategies/StripeFillerTests.cs b/OpenNest.Tests/Strategies/StripeFillerTests.cs new file mode 100644 index 0000000..425285d --- /dev/null +++ b/OpenNest.Tests/Strategies/StripeFillerTests.cs @@ -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); + } +}