fix: correct Width/Length axis mapping and add spiral center-fill
Box constructor and derived properties (Right, Top, Center, Translate, Offset) had Width and Length swapped — Length is X axis, Width is Y axis. Corrected across Core geometry, plate bounding box, rectangle packing, fill algorithms, tests, and UI renderers. Added FillSpiral with center remnant detection and recursive FillBest on the gap between the 4 spiral quadrants. RectFill.FillBest now compares spiral+center vs full best-fit fairly. BestCombination returns a CombinationResult record instead of out params. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,7 @@ public class NestProgressTests
|
||||
var parts = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5),
|
||||
TestHelpers.MakePartAt(10, 0, 5),
|
||||
TestHelpers.MakePartAt(0, 10, 5),
|
||||
};
|
||||
var progress = new NestProgress { BestParts = parts };
|
||||
Assert.Equal(15, progress.NestedWidth, precision: 4);
|
||||
@@ -61,7 +61,7 @@ public class NestProgressTests
|
||||
var parts = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5),
|
||||
TestHelpers.MakePartAt(0, 10, 5),
|
||||
TestHelpers.MakePartAt(10, 0, 5),
|
||||
};
|
||||
var progress = new NestProgress { BestParts = parts };
|
||||
Assert.Equal(15, progress.NestedLength, precision: 4);
|
||||
|
||||
@@ -6,61 +6,61 @@ public class BestCombinationTests
|
||||
public void BothFit_FindsZeroRemnant()
|
||||
{
|
||||
// 100 = 0*30 + 5*20 (algorithm iterates from countLength1=0, finds zero remnant first)
|
||||
var result = BestCombination.FindFrom2(30, 20, 100, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(30, 20, 100);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0.0, 100.0 - (c1 * 30.0 + c2 * 20.0), 5);
|
||||
Assert.True(result.Found);
|
||||
Assert.Equal(0.0, 100.0 - (result.Count1 * 30.0 + result.Count2 * 20.0), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyLength1Fits_ReturnsMaxCount1()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(10, 200, 50, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(10, 200, 50);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(5, c1);
|
||||
Assert.Equal(0, c2);
|
||||
Assert.True(result.Found);
|
||||
Assert.Equal(5, result.Count1);
|
||||
Assert.Equal(0, result.Count2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyLength2Fits_ReturnsMaxCount2()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(200, 10, 50, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(200, 10, 50);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0, c1);
|
||||
Assert.Equal(5, c2);
|
||||
Assert.True(result.Found);
|
||||
Assert.Equal(0, result.Count1);
|
||||
Assert.Equal(5, result.Count2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NeitherFits_ReturnsFalse()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(100, 200, 50, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(100, 200, 50);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, c1);
|
||||
Assert.Equal(0, c2);
|
||||
Assert.False(result.Found);
|
||||
Assert.Equal(0, result.Count1);
|
||||
Assert.Equal(0, result.Count2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Length1FillsExactly_ZeroRemnant()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(25, 10, 100, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(25, 10, 100);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0.0, 100.0 - (c1 * 25.0 + c2 * 10.0), 5);
|
||||
Assert.True(result.Found);
|
||||
Assert.Equal(0.0, 100.0 - (result.Count1 * 25.0 + result.Count2 * 10.0), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixMinimizesRemnant()
|
||||
{
|
||||
// 7 and 3 into 20: best is 2*7 + 2*3 = 20 (zero remnant)
|
||||
var result = BestCombination.FindFrom2(7, 3, 20, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(7, 3, 20);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(2, c1);
|
||||
Assert.Equal(2, c2);
|
||||
Assert.True(c1 * 7 + c2 * 3 <= 20);
|
||||
Assert.True(result.Found);
|
||||
Assert.Equal(2, result.Count1);
|
||||
Assert.Equal(2, result.Count2);
|
||||
Assert.True(result.Count1 * 7 + result.Count2 * 3 <= 20);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -68,28 +68,28 @@ public class BestCombinationTests
|
||||
{
|
||||
// 6 and 5 into 17:
|
||||
// all length1: 2*6=12, remnant=5 -> actually 2*6+1*5=17 perfect
|
||||
var result = BestCombination.FindFrom2(6, 5, 17, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(6, 5, 17);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0.0, 17.0 - (c1 * 6.0 + c2 * 5.0), 5);
|
||||
Assert.True(result.Found);
|
||||
Assert.Equal(0.0, 17.0 - (result.Count1 * 6.0 + result.Count2 * 5.0), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualLengths_FillsWithLength1()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(10, 10, 50, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(10, 10, 50);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(5, c1 + c2);
|
||||
Assert.True(result.Found);
|
||||
Assert.Equal(5, result.Count1 + result.Count2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmallLengths_LargeOverall()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(3, 7, 100, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(3, 7, 100);
|
||||
|
||||
Assert.True(result);
|
||||
var used = c1 * 3.0 + c2 * 7.0;
|
||||
Assert.True(result.Found);
|
||||
var used = result.Count1 * 3.0 + result.Count2 * 7.0;
|
||||
Assert.True(used <= 100);
|
||||
Assert.True(100 - used < 3); // remnant less than smallest piece
|
||||
}
|
||||
@@ -100,41 +100,41 @@ public class BestCombinationTests
|
||||
// length1=9, length2=5, overall=10:
|
||||
// length1 alone: 1*9=9 remnant=1
|
||||
// length2 alone: 2*5=10 remnant=0
|
||||
var result = BestCombination.FindFrom2(9, 5, 10, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(9, 5, 10);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0, c1);
|
||||
Assert.Equal(2, c2);
|
||||
Assert.True(result.Found);
|
||||
Assert.Equal(0, result.Count1);
|
||||
Assert.Equal(2, result.Count2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FractionalLengths_WorkCorrectly()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(2.5, 3.5, 12, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(2.5, 3.5, 12);
|
||||
|
||||
Assert.True(result);
|
||||
var used = c1 * 2.5 + c2 * 3.5;
|
||||
Assert.True(result.Found);
|
||||
var used = result.Count1 * 2.5 + result.Count2 * 3.5;
|
||||
Assert.True(used <= 12.0 + 0.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverallExactlyOneOfEach()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(40, 60, 100, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(40, 60, 100);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(1, c1);
|
||||
Assert.Equal(1, c2);
|
||||
Assert.True(result.Found);
|
||||
Assert.Equal(1, result.Count1);
|
||||
Assert.Equal(1, result.Count2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverallSmallerThanEither_ReturnsFalse()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(10, 20, 5, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(10, 20, 5);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, c1);
|
||||
Assert.Equal(0, c2);
|
||||
Assert.False(result.Found);
|
||||
Assert.Equal(0, result.Count1);
|
||||
Assert.Equal(0, result.Count2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -142,9 +142,9 @@ public class BestCombinationTests
|
||||
{
|
||||
// 4 and 6 into 24: 0*4+4*6=24 or 3*4+2*6=24 or 6*4+0*6=24
|
||||
// Algorithm iterates from 0 length1 upward, finds zero remnant and breaks
|
||||
var result = BestCombination.FindFrom2(4, 6, 24, out var c1, out var c2);
|
||||
var result = BestCombination.FindFrom2(4, 6, 24);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0.0, 24.0 - (c1 * 4.0 + c2 * 6.0), 5);
|
||||
Assert.True(result.Found);
|
||||
Assert.Equal(0.0, 24.0 - (result.Count1 * 4.0 + result.Count2 * 6.0), 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ public class PatternTilerTests
|
||||
|
||||
foreach (var part in result)
|
||||
{
|
||||
Assert.True(part.BoundingBox.Right <= plateSize.Width + 0.001);
|
||||
Assert.True(part.BoundingBox.Top <= plateSize.Length + 0.001);
|
||||
Assert.True(part.BoundingBox.Right <= plateSize.Length + 0.001);
|
||||
Assert.True(part.BoundingBox.Top <= plateSize.Width + 0.001);
|
||||
Assert.True(part.BoundingBox.Left >= -0.001);
|
||||
Assert.True(part.BoundingBox.Bottom >= -0.001);
|
||||
}
|
||||
@@ -87,8 +87,8 @@ public class PatternTilerTests
|
||||
|
||||
var maxRight = result.Max(p => p.BoundingBox.Right);
|
||||
var maxTop = result.Max(p => p.BoundingBox.Top);
|
||||
Assert.True(maxRight <= 50.001);
|
||||
Assert.True(maxTop <= 10.001);
|
||||
Assert.True(maxRight <= 10.001);
|
||||
Assert.True(maxTop <= 50.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -108,8 +108,8 @@ public class RemnantFinderTests
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
var gap = remnants.FirstOrDefault(r =>
|
||||
r.Width >= 19.9 && r.Width <= 20.1 &&
|
||||
r.Length >= 99.9);
|
||||
r.Length >= 19.9 && r.Length <= 20.1 &&
|
||||
r.Width >= 99.9);
|
||||
Assert.NotNull(gap);
|
||||
}
|
||||
|
||||
@@ -146,8 +146,8 @@ public class RemnantFinderTests
|
||||
|
||||
// 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);
|
||||
r.Length >= 79.9 && r.Length <= 80.1 &&
|
||||
r.Width >= 99.9);
|
||||
Assert.NotNull(left);
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ public class RemnantFinderTests
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
var gap = remnants.FirstOrDefault(r =>
|
||||
r.Width >= 19.9 && r.Width <= 20.1);
|
||||
r.Length >= 19.9 && r.Length <= 20.1);
|
||||
Assert.NotNull(gap);
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ public class RemnantFinderTests
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
var gap = remnants.FirstOrDefault(r =>
|
||||
r.Width >= 19.9 && r.Width <= 20.1);
|
||||
r.Length >= 19.9 && r.Length <= 20.1);
|
||||
Assert.NotNull(gap);
|
||||
}
|
||||
|
||||
@@ -280,9 +280,9 @@ public class RemnantFinderTests
|
||||
finder.AddObstacle(new Box(0, 47, 21, 6));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
var above = remnants.FirstOrDefault(r => r.Bottom >= 53 - 0.1 && r.Width > 50);
|
||||
var below = remnants.FirstOrDefault(r => r.Top <= 47 + 0.1 && r.Width > 50);
|
||||
var right = remnants.FirstOrDefault(r => r.Left >= 21 - 0.1 && r.Length > 50);
|
||||
var above = remnants.FirstOrDefault(r => r.Bottom >= 53 - 0.1 && r.Length > 50);
|
||||
var below = remnants.FirstOrDefault(r => r.Top <= 47 + 0.1 && r.Length > 50);
|
||||
var right = remnants.FirstOrDefault(r => r.Left >= 21 - 0.1 && r.Width > 50);
|
||||
|
||||
Assert.NotNull(above);
|
||||
Assert.NotNull(below);
|
||||
@@ -312,10 +312,10 @@ public class RemnantFinderTests
|
||||
var topGap = tiered.FirstOrDefault(t =>
|
||||
t.Box.Bottom > 50 && t.Box.Bottom < 55 &&
|
||||
t.Box.Left < 1 &&
|
||||
t.Box.Width > 100 &&
|
||||
t.Box.Length > 5);
|
||||
t.Box.Length > 100 &&
|
||||
t.Box.Width > 5);
|
||||
|
||||
Assert.True(topGap.Box.Width > 0, "Expected remnant above main grid");
|
||||
Assert.True(topGap.Box.Length > 0, "Expected remnant above main grid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -361,11 +361,11 @@ public class RemnantFinderTests
|
||||
|
||||
// The gap at x < 106 from y=53.14 to y=59.8 should be found.
|
||||
Assert.True(remnants.Count > 0, "Should find gap above main grid");
|
||||
var topRemnant = remnants.FirstOrDefault(r => r.Length >= 5.375 && r.Width > 50);
|
||||
var topRemnant = remnants.FirstOrDefault(r => r.Width >= 5.375 && r.Length > 50);
|
||||
Assert.NotNull(topRemnant);
|
||||
|
||||
// Verify dimensions are close to the expected ~104 x 6.6 gap.
|
||||
Assert.True(topRemnant.Width > 100, $"Expected width > 100, got {topRemnant.Width:F1}");
|
||||
Assert.True(topRemnant.Length > 6, $"Expected length > 6, got {topRemnant.Length:F1}");
|
||||
Assert.True(topRemnant.Length > 100, $"Expected length > 100, got {topRemnant.Length:F1}");
|
||||
Assert.True(topRemnant.Width > 6, $"Expected width > 6, got {topRemnant.Width:F1}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ public class PolygonHelperTests
|
||||
var rotated = PolygonHelper.RotatePolygon(polygon, Angle.HalfPI);
|
||||
rotated.UpdateBounds();
|
||||
|
||||
Assert.True(System.Math.Abs(rotated.BoundingBox.Width - 10) < 0.1);
|
||||
Assert.True(System.Math.Abs(rotated.BoundingBox.Length - 20) < 0.1);
|
||||
Assert.True(System.Math.Abs(rotated.BoundingBox.Length - 10) < 0.1);
|
||||
Assert.True(System.Math.Abs(rotated.BoundingBox.Width - 20) < 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ public class IsoscelesTriangleShapeTests
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(10, bbox.Width, 0.01);
|
||||
Assert.Equal(8, bbox.Length, 0.01);
|
||||
Assert.Equal(10, bbox.Length, 0.01);
|
||||
Assert.Equal(8, bbox.Width, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -11,8 +11,8 @@ public class LShapeTests
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(10, bbox.Width, 0.01);
|
||||
Assert.Equal(20, bbox.Length, 0.01);
|
||||
Assert.Equal(10, bbox.Length, 0.01);
|
||||
Assert.Equal(20, bbox.Width, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -11,8 +11,8 @@ public class RectangleShapeTests
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(10, bbox.Width, 0.01);
|
||||
Assert.Equal(5, bbox.Length, 0.01);
|
||||
Assert.Equal(10, bbox.Length, 0.01);
|
||||
Assert.Equal(5, bbox.Width, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -11,8 +11,8 @@ public class RightTriangleShapeTests
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(12, bbox.Width, 0.01);
|
||||
Assert.Equal(8, bbox.Length, 0.01);
|
||||
Assert.Equal(12, bbox.Length, 0.01);
|
||||
Assert.Equal(8, bbox.Width, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -11,8 +11,8 @@ public class RoundedRectangleShapeTests
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(20, bbox.Width, 0.1);
|
||||
Assert.Equal(10, bbox.Length, 0.1);
|
||||
Assert.Equal(20, bbox.Length, 0.1);
|
||||
Assert.Equal(10, bbox.Width, 0.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -11,8 +11,8 @@ public class TShapeTests
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(12, bbox.Width, 0.01);
|
||||
Assert.Equal(18, bbox.Length, 0.01);
|
||||
Assert.Equal(12, bbox.Length, 0.01);
|
||||
Assert.Equal(18, bbox.Width, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -11,8 +11,8 @@ public class TrapezoidShapeTests
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(20, bbox.Width, 0.01);
|
||||
Assert.Equal(8, bbox.Length, 0.01);
|
||||
Assert.Equal(20, bbox.Length, 0.01);
|
||||
Assert.Equal(8, bbox.Width, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -161,11 +161,11 @@ public class DrawingSplitterTests
|
||||
var bb2 = results[1].Program.BoundingBox();
|
||||
|
||||
// Piece lengths should sum to original length
|
||||
Assert.Equal(100.0, bb1.Width + bb2.Width, 1);
|
||||
Assert.Equal(100.0, bb1.Length + bb2.Length, 1);
|
||||
|
||||
// Both pieces should have the same width as the original
|
||||
Assert.Equal(100.0, bb1.Length, 1);
|
||||
Assert.Equal(100.0, bb2.Length, 1);
|
||||
Assert.Equal(100.0, bb1.Width, 1);
|
||||
Assert.Equal(100.0, bb2.Width, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -183,11 +183,11 @@ public class DrawingSplitterTests
|
||||
var bb2 = results[1].Program.BoundingBox();
|
||||
|
||||
// Piece widths should sum to original width
|
||||
Assert.Equal(100.0, bb1.Length + bb2.Length, 1);
|
||||
Assert.Equal(100.0, bb1.Width + bb2.Width, 1);
|
||||
|
||||
// Both pieces should have the same length as the original
|
||||
Assert.Equal(100.0, bb1.Width, 1);
|
||||
Assert.Equal(100.0, bb2.Width, 1);
|
||||
Assert.Equal(100.0, bb1.Length, 1);
|
||||
Assert.Equal(100.0, bb2.Length, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -287,8 +287,8 @@ public class DrawingSplitterTests
|
||||
var bb2 = results[1].Program.BoundingBox();
|
||||
|
||||
// Left piece should be 30 long, right piece should be 70 long
|
||||
Assert.Equal(30.0, bb1.Width, 1);
|
||||
Assert.Equal(70.0, bb2.Width, 1);
|
||||
Assert.Equal(30.0, bb1.Length, 1);
|
||||
Assert.Equal(70.0, bb2.Length, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -37,8 +37,8 @@ public class StripeFillerTests
|
||||
Drawing drawing, double spacing)
|
||||
{
|
||||
var bb = drawing.Program.BoundingBox();
|
||||
var w = bb.Width;
|
||||
var h = bb.Length;
|
||||
var w = bb.Length;
|
||||
var h = bb.Width;
|
||||
|
||||
var candidate = new PairCandidate
|
||||
{
|
||||
@@ -85,7 +85,7 @@ public class StripeFillerTests
|
||||
pattern.Parts, 22.0, NestDirection.Horizontal);
|
||||
|
||||
var rotated = FillHelpers.BuildRotatedPattern(pattern.Parts, angle);
|
||||
var span = rotated.BoundingBox.Width;
|
||||
var span = rotated.BoundingBox.Length;
|
||||
Assert.True(System.Math.Abs(span - 22.0) < 0.5,
|
||||
$"Expected span ~22, got {span:F2} at {OpenNest.Math.Angle.ToDegrees(angle):F1}°");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user