test: add RemnantFinder edge cases and FillScore comparison tests
RemnantFinder: obstacle clipping, overlapping obstacles, iterative workflow, grid pattern, no-overlap invariant, constructor/AddObstacles. FillScore: count-vs-density ordering, operators, Compute edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
89
OpenNest.Tests/FillScoreTests.cs
Normal file
89
OpenNest.Tests/FillScoreTests.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
public class FillScoreTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void HigherCount_WinsOverLowerCount()
|
||||||
|
{
|
||||||
|
var a = new FillScore(10, 0.5);
|
||||||
|
var b = new FillScore(5, 0.9);
|
||||||
|
|
||||||
|
Assert.True(a > b);
|
||||||
|
Assert.False(b > a);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SameCount_HigherDensityWins()
|
||||||
|
{
|
||||||
|
var a = new FillScore(10, 0.8);
|
||||||
|
var b = new FillScore(10, 0.5);
|
||||||
|
|
||||||
|
Assert.True(a > b);
|
||||||
|
Assert.False(b > a);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EqualScores_AreNotGreaterOrLess()
|
||||||
|
{
|
||||||
|
var a = new FillScore(10, 0.5);
|
||||||
|
var b = new FillScore(10, 0.5);
|
||||||
|
|
||||||
|
Assert.False(a > b);
|
||||||
|
Assert.False(a < b);
|
||||||
|
Assert.True(a >= b);
|
||||||
|
Assert.True(a <= b);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Default_IsZero()
|
||||||
|
{
|
||||||
|
var score = default(FillScore);
|
||||||
|
|
||||||
|
Assert.Equal(0, score.Count);
|
||||||
|
Assert.Equal(0, score.Density);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Compute_NullParts_ReturnsDefault()
|
||||||
|
{
|
||||||
|
var score = FillScore.Compute(null, new Geometry.Box(0, 0, 100, 100));
|
||||||
|
|
||||||
|
Assert.Equal(0, score.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Compute_EmptyParts_ReturnsDefault()
|
||||||
|
{
|
||||||
|
var score = FillScore.Compute(new System.Collections.Generic.List<Part>(), new Geometry.Box(0, 0, 100, 100));
|
||||||
|
|
||||||
|
Assert.Equal(0, score.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Compute_WithParts_ReturnsCorrectCount()
|
||||||
|
{
|
||||||
|
var parts = new System.Collections.Generic.List<Part>
|
||||||
|
{
|
||||||
|
TestHelpers.MakePartAt(0, 0, 10),
|
||||||
|
TestHelpers.MakePartAt(20, 0, 10),
|
||||||
|
TestHelpers.MakePartAt(40, 0, 10)
|
||||||
|
};
|
||||||
|
var score = FillScore.Compute(parts, new Geometry.Box(0, 0, 100, 100));
|
||||||
|
|
||||||
|
Assert.Equal(3, score.Count);
|
||||||
|
Assert.True(score.Density > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CompareTo_IsConsistentWithOperators()
|
||||||
|
{
|
||||||
|
var a = new FillScore(10, 0.8);
|
||||||
|
var b = new FillScore(5, 0.9);
|
||||||
|
|
||||||
|
Assert.True(a.CompareTo(b) > 0);
|
||||||
|
Assert.True(a > b);
|
||||||
|
Assert.True(b < a);
|
||||||
|
Assert.True(a >= b);
|
||||||
|
Assert.True(b <= a);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -122,4 +122,151 @@ public class RemnantFinderTests
|
|||||||
Assert.True(remnants.Count >= 1);
|
Assert.True(remnants.Count >= 1);
|
||||||
Assert.True(remnants[0].Area() < plate.WorkArea().Area());
|
Assert.True(remnants[0].Area() < plate.WorkArea().Area());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObstacleOutsideWorkArea_IsIgnored()
|
||||||
|
{
|
||||||
|
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||||
|
finder.AddObstacle(new Box(200, 200, 50, 50));
|
||||||
|
var remnants = finder.FindRemnants();
|
||||||
|
|
||||||
|
Assert.Single(remnants);
|
||||||
|
Assert.Equal(100 * 100, remnants[0].Area(), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObstaclePartiallyOutsideWorkArea_IsClipped()
|
||||||
|
{
|
||||||
|
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||||
|
// Obstacle extends 20 units past the right edge
|
||||||
|
finder.AddObstacle(new Box(80, 0, 40, 100));
|
||||||
|
var remnants = finder.FindRemnants();
|
||||||
|
|
||||||
|
// Should find the 80x100 strip on the left
|
||||||
|
var left = remnants.FirstOrDefault(r =>
|
||||||
|
r.Width >= 79.9 && r.Width <= 80.1 &&
|
||||||
|
r.Length >= 99.9);
|
||||||
|
Assert.NotNull(left);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OverlappingObstacles_HandledCorrectly()
|
||||||
|
{
|
||||||
|
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||||
|
finder.AddObstacle(new Box(0, 0, 60, 60));
|
||||||
|
finder.AddObstacle(new Box(40, 40, 60, 60)); // overlaps first
|
||||||
|
var remnants = finder.FindRemnants();
|
||||||
|
|
||||||
|
// No remnant should overlap either obstacle
|
||||||
|
foreach (var r in remnants)
|
||||||
|
{
|
||||||
|
Assert.False(
|
||||||
|
r.Left < 60 && r.Right > 0 && r.Bottom < 60 && r.Top > 0
|
||||||
|
&& r.Left < 100 && r.Right > 40 && r.Bottom < 100 && r.Top > 40,
|
||||||
|
"Remnant should not overlap both obstacles simultaneously in their shared region");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total remnant area + obstacle coverage should not exceed work area
|
||||||
|
var totalRemnantArea = remnants.Sum(r => r.Area());
|
||||||
|
Assert.True(totalRemnantArea < 100 * 100);
|
||||||
|
Assert.True(totalRemnantArea > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConstructorWithObstaclesList()
|
||||||
|
{
|
||||||
|
var obstacles = new List<Box>
|
||||||
|
{
|
||||||
|
new Box(0, 0, 40, 100),
|
||||||
|
new Box(60, 0, 40, 100)
|
||||||
|
};
|
||||||
|
var finder = new RemnantFinder(new Box(0, 0, 100, 100), obstacles);
|
||||||
|
var remnants = finder.FindRemnants();
|
||||||
|
|
||||||
|
var gap = remnants.FirstOrDefault(r =>
|
||||||
|
r.Width >= 19.9 && r.Width <= 20.1);
|
||||||
|
Assert.NotNull(gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddObstacles_Plural_AddsMultiple()
|
||||||
|
{
|
||||||
|
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||||
|
finder.AddObstacles(new[]
|
||||||
|
{
|
||||||
|
new Box(0, 0, 40, 100),
|
||||||
|
new Box(60, 0, 40, 100)
|
||||||
|
});
|
||||||
|
var remnants = finder.FindRemnants();
|
||||||
|
|
||||||
|
var gap = remnants.FirstOrDefault(r =>
|
||||||
|
r.Width >= 19.9 && r.Width <= 20.1);
|
||||||
|
Assert.NotNull(gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IterativeWorkflow_AddObstacleThenRequery()
|
||||||
|
{
|
||||||
|
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||||
|
|
||||||
|
// First fill: obstacle in bottom-left
|
||||||
|
finder.AddObstacle(new Box(0, 0, 50, 50));
|
||||||
|
var pass1 = finder.FindRemnants();
|
||||||
|
Assert.True(pass1.Count >= 2);
|
||||||
|
|
||||||
|
// Simulate filling the largest remnant by adding another obstacle
|
||||||
|
var largest = pass1[0];
|
||||||
|
finder.AddObstacle(new Box(largest.X, largest.Y, largest.Width / 2, largest.Length));
|
||||||
|
var pass2 = finder.FindRemnants();
|
||||||
|
|
||||||
|
// Should have more, smaller remnants now
|
||||||
|
var pass2TotalArea = pass2.Sum(r => r.Area());
|
||||||
|
var pass1TotalArea = pass1.Sum(r => r.Area());
|
||||||
|
Assert.True(pass2TotalArea < pass1TotalArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NoRemnantOverlapsObstacle()
|
||||||
|
{
|
||||||
|
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||||
|
finder.AddObstacle(new Box(20, 20, 30, 30));
|
||||||
|
finder.AddObstacle(new Box(60, 10, 25, 80));
|
||||||
|
var remnants = finder.FindRemnants();
|
||||||
|
|
||||||
|
foreach (var r in remnants)
|
||||||
|
{
|
||||||
|
// Check no remnant overlaps obstacle 1
|
||||||
|
var overlaps1 = r.Left < 50 && r.Right > 20 && r.Bottom < 50 && r.Top > 20;
|
||||||
|
Assert.False(overlaps1, $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 1");
|
||||||
|
|
||||||
|
// Check no remnant overlaps obstacle 2
|
||||||
|
var overlaps2 = r.Left < 85 && r.Right > 60 && r.Bottom < 90 && r.Top > 10;
|
||||||
|
Assert.False(overlaps2, $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ManyObstacles_GridPattern()
|
||||||
|
{
|
||||||
|
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||||
|
|
||||||
|
// Place a 5x5 grid of 10x10 obstacles with 10-unit gaps
|
||||||
|
for (var row = 0; row < 5; row++)
|
||||||
|
for (var col = 0; col < 5; col++)
|
||||||
|
finder.AddObstacle(new Box(col * 20, row * 20, 10, 10));
|
||||||
|
|
||||||
|
var remnants = finder.FindRemnants();
|
||||||
|
|
||||||
|
// All remnants should be within the work area
|
||||||
|
foreach (var r in remnants)
|
||||||
|
{
|
||||||
|
Assert.True(r.Left >= 0);
|
||||||
|
Assert.True(r.Bottom >= 0);
|
||||||
|
Assert.True(r.Right <= 100);
|
||||||
|
Assert.True(r.Top <= 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should find gaps between obstacles
|
||||||
|
Assert.True(remnants.Count > 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user