style: apply CSharpier formatting to all C# sources

Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and
line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests
pass after reformat, full solution builds 0 errors.

Added .csharpierignore so csproj/config XML keeps its existing layout
(CSharpier's XML wrapping churns attributes with zero benefit).

Formatting is now enforceable: dotnet csharpier check . passes.
This commit is contained in:
aj
2026-09-20 16:41:50 -04:00
parent 8e6fa677fb
commit aec0523062
476 changed files with 16592 additions and 7800 deletions
@@ -7,6 +7,7 @@ public class AccumulatingProgressTests
private class CapturingProgress : IProgress<NestProgress>
{
public NestProgress Last { get; private set; }
public void Report(NestProgress value) => Last = value;
}
@@ -18,8 +18,10 @@ public class AngleCandidateBuilderTests
return new Drawing("rect", pgm);
}
private static ClassificationResult MakeClassification(double primaryAngle = 0, PartType type = PartType.Irregular)
=> new ClassificationResult { PrimaryAngle = primaryAngle, Type = type };
private static ClassificationResult MakeClassification(
double primaryAngle = 0,
PartType type = PartType.Irregular
) => new ClassificationResult { PrimaryAngle = primaryAngle, Type = type };
[Fact]
public void Build_ReturnsAtLeastTwoAngles()
@@ -81,8 +83,10 @@ public class AngleCandidateBuilderTests
builder.ForceFullSweep = false;
var secondAngles = builder.Build(item, MakeClassification(), workArea);
Assert.True(secondAngles.Count < firstAngles.Count,
$"Pruned ({secondAngles.Count}) should be fewer than full ({firstAngles.Count})");
Assert.True(
secondAngles.Count < firstAngles.Count,
$"Pruned ({secondAngles.Count}) should be fewer than full ({firstAngles.Count})"
);
}
[Fact]
@@ -128,8 +132,10 @@ public class AngleCandidateBuilderTests
var angles = builder.Build(item, classification, workArea);
Assert.True(angles.Count > 2,
$"User constraints should override rect classification, got {angles.Count} angles");
Assert.True(
angles.Count > 2,
$"User constraints should override rect classification, got {angles.Count} angles"
);
}
[Fact]
@@ -149,7 +155,9 @@ public class AngleCandidateBuilderTests
var angles = builder.Build(item, classification, workArea);
// Start=0, End=PI is NOT "no constraints" — it's a real 0-180 range
Assert.True(angles.Count > 2,
$"0-to-PI constraint should produce multiple angles, got {angles.Count}");
Assert.True(
angles.Count > 2,
$"0-to-PI constraint should produce multiple angles, got {angles.Count}"
);
}
}
+100 -42
View File
@@ -1,8 +1,8 @@
using System.Collections.Generic;
using OpenNest;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using Xunit;
using System.Collections.Generic;
namespace OpenNest.Tests.Fill
{
@@ -31,8 +31,7 @@ namespace OpenNest.Tests.Fill
// Verify: after moving, the closest point on the arc should be within
// tolerance of the line, not past it.
var theta = System.Math.Atan2(
line.pt2.X - line.pt1.X, -(line.pt2.Y - line.pt1.Y));
var theta = System.Math.Atan2(line.pt2.X - line.pt1.X, -(line.pt2.Y - line.pt1.Y));
theta = OpenNest.Math.Angle.NormalizeRad(theta + System.Math.PI);
var qx = arc.Center.X + arc.Radius * System.Math.Cos(theta);
var qy = arc.Center.Y + arc.Radius * System.Math.Sin(theta) + dist;
@@ -41,9 +40,11 @@ namespace OpenNest.Tests.Fill
// Line equation: (y - 4) / (x - 3) = (6 - 4) / (7 - 3) = 0.5
// y = 0.5x + 2.5
var lineYAtQx = 0.5 * qx + 2.5;
Assert.True(qy <= lineYAtQx + 0.001,
$"Arc point ({qx:F4}, {qy:F4}) should not be past line (line Y={lineYAtQx:F4} at X={qx:F4}). " +
$"dist={dist:F6}, overshot by {qy - lineYAtQx:F6}");
Assert.True(
qy <= lineYAtQx + 0.001,
$"Arc point ({qx:F4}, {qy:F4}) should not be past line (line Y={lineYAtQx:F4} at X={qx:F4}). "
+ $"dist={dist:F6}, overshot by {qy - lineYAtQx:F6}"
);
}
[Fact]
@@ -57,17 +58,26 @@ namespace OpenNest.Tests.Fill
// Phase 1/2 vertex-only distance: sample arc endpoints + cardinal extreme.
var vertices = new[]
{
new Vector(7, 0), // arc endpoint θ=0
new Vector(3, 0), // arc endpoint θ=π
new Vector(5, 2), // cardinal extreme θ=π/2
new Vector(7, 0), // arc endpoint θ=0
new Vector(3, 0), // arc endpoint θ=π
new Vector(5, 2), // cardinal extreme θ=π/2
};
var vertexMin = double.MaxValue;
foreach (var v in vertices)
{
var d = SpatialQuery.RayEdgeDistance(v.X, v.Y,
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y, 0, 1);
if (d < vertexMin) vertexMin = d;
var d = SpatialQuery.RayEdgeDistance(
v.X,
v.Y,
line.pt1.X,
line.pt1.Y,
line.pt2.X,
line.pt2.Y,
0,
1
);
if (d < vertexMin)
vertexMin = d;
}
// Full directional distance (includes Phase 3 arc-to-line).
@@ -75,9 +85,12 @@ namespace OpenNest.Tests.Fill
var stationary = new List<Entity> { line };
var fullDist = SpatialQuery.DirectionalDistance(moving, stationary, new Vector(0, 1));
Assert.True(fullDist < vertexMin,
$"Full distance ({fullDist:F6}) should be less than vertex-only ({vertexMin:F6})");
Assert.True(
fullDist < vertexMin,
$"Full distance ({fullDist:F6}) should be less than vertex-only ({vertexMin:F6})"
);
}
private static Drawing MakeRectDrawing(double w, double h)
{
var pgm = new OpenNest.CNC.Program();
@@ -187,12 +200,24 @@ namespace OpenNest.Tests.Fill
// Push without spacing.
var obstacle1 = MakeRectPart(0, 0, 10, 10);
var part1 = MakeRectPart(50, 0, 10, 10);
var distNoSpacing = Compactor.Push(new List<Part> { part1 }, new List<Part> { obstacle1 }, workArea, 0, PushDirection.Left);
var distNoSpacing = Compactor.Push(
new List<Part> { part1 },
new List<Part> { obstacle1 },
workArea,
0,
PushDirection.Left
);
// Push with spacing.
var obstacle2 = MakeRectPart(0, 0, 10, 10);
var part2 = MakeRectPart(50, 0, 10, 10);
var distWithSpacing = Compactor.Push(new List<Part> { part2 }, new List<Part> { obstacle2 }, workArea, 2, PushDirection.Left);
var distWithSpacing = Compactor.Push(
new List<Part> { part2 },
new List<Part> { obstacle2 },
workArea,
2,
PushDirection.Left
);
// Spacing should cause the part to stop at a different position than without spacing.
Assert.NotEqual(distNoSpacing, distWithSpacing);
@@ -235,11 +260,19 @@ namespace OpenNest.Tests.Fill
public void Push_WithSpacing_StopsBeforeNearMissOutsideRawBounds(double degrees)
{
var obstacle = MakeRectPart(20, 20, 10, 10);
var moving = Part.CreateAtOrigin(MakeRectDrawing(10, 10), OpenNest.Math.Angle.ToRadians(degrees));
var moving = Part.CreateAtOrigin(
MakeRectDrawing(10, 10),
OpenNest.Math.Angle.ToRadians(degrees)
);
moving.Offset(60, 31);
Compactor.Push(new List<Part> { moving }, new List<Part> { obstacle },
new Box(0, 0, 100, 100), 2, PushDirection.Left);
Compactor.Push(
new List<Part> { moving },
new List<Part> { obstacle },
new Box(0, 0, 100, 100),
2,
PushDirection.Left
);
// Must stop at the first clearance boundary, not pass the obstacle
// and finish in a clear position on the far side.
@@ -253,8 +286,13 @@ namespace OpenNest.Tests.Fill
var obstacle = MakeRectPart(20, 20, 10, 10);
var moving = MakeRectPart(60, 20, 10, 10);
Compactor.Push(new List<Part> { moving }, new List<Part> { obstacle },
new Box(31, 0, 100, 100), 2, PushDirection.Left);
Compactor.Push(
new List<Part> { moving },
new List<Part> { obstacle },
new Box(31, 0, 100, 100),
2,
PushDirection.Left
);
AssertClearance(moving, obstacle, 2);
Assert.Equal(32, moving.BoundingBox.Left, 7);
@@ -267,31 +305,39 @@ namespace OpenNest.Tests.Fill
foreach (var b in PartGeometry.GetPartLines(obstacle))
{
Assert.False(Intersect.Intersects(a, b, out _));
clearance = System.Math.Min(clearance, a.StartPoint.DistanceTo(b.ClosestPointTo(a.StartPoint)));
clearance = System.Math.Min(clearance, b.StartPoint.DistanceTo(a.ClosestPointTo(b.StartPoint)));
clearance = System.Math.Min(
clearance,
a.StartPoint.DistanceTo(b.ClosestPointTo(a.StartPoint))
);
clearance = System.Math.Min(
clearance,
b.StartPoint.DistanceTo(a.ClosestPointTo(b.StartPoint))
);
}
Assert.True(clearance >= spacing - 1e-7, $"Clearance {clearance:R} is less than spacing {spacing:R}");
Assert.True(
clearance >= spacing - 1e-7,
$"Clearance {clearance:R} is less than spacing {spacing:R}"
);
}
[Fact]
public void Push_Up_AllowsSharedDiagonalEdgeToSeparate()
{
var workArea = new Box(0, 0, 20, 20);
var obstacle = MakeTrianglePart(
new Vector(0, 0),
new Vector(10, 0),
new Vector(0, 10));
var obstacle = MakeTrianglePart(new Vector(0, 0), new Vector(10, 0), new Vector(0, 10));
var movingPart = MakeTrianglePart(
new Vector(0, 10),
new Vector(10, 0),
new Vector(10, 10));
new Vector(10, 10)
);
var distance = Compactor.Push(
new List<Part> { movingPart },
new List<Part> { obstacle },
workArea,
0,
PushDirection.Up);
PushDirection.Up
);
Assert.True(distance > 0);
Assert.True(movingPart.BoundingBox.Top > 19.9);
@@ -303,15 +349,19 @@ namespace OpenNest.Tests.Fill
{
var workArea = new Box(0, 0, 24, 24);
var leftTriangle = MakeTrianglePart(
2, 2,
2,
2,
new Vector(0, 0),
new Vector(8, 0),
new Vector(4, 10));
new Vector(4, 10)
);
var rightTriangle = MakeTrianglePart(
14, 4,
14,
4,
new Vector(0, 10),
new Vector(8, 10),
new Vector(4, 0));
new Vector(4, 0)
);
var moving = new List<Part> { rightTriangle };
var obstacles = new List<Part> { leftTriangle };
@@ -333,21 +383,20 @@ namespace OpenNest.Tests.Fill
public void Push_Left_BlocksWhenSharedDiagonalEdgeWouldOverlap()
{
var workArea = new Box(0, 0, 20, 20);
var obstacle = MakeTrianglePart(
new Vector(0, 0),
new Vector(10, 0),
new Vector(0, 10));
var obstacle = MakeTrianglePart(new Vector(0, 0), new Vector(10, 0), new Vector(0, 10));
var movingPart = MakeTrianglePart(
new Vector(0, 10),
new Vector(10, 0),
new Vector(10, 10));
new Vector(10, 10)
);
var distance = Compactor.Push(
new List<Part> { movingPart },
new List<Part> { obstacle },
workArea,
0,
PushDirection.Left);
PushDirection.Left
);
Assert.Equal(0, distance);
Assert.Equal(0, movingPart.BoundingBox.Left);
@@ -362,7 +411,10 @@ namespace OpenNest.Tests.Fill
var obstacles = new List<Part>();
// direction = left
var direction = new Vector(System.Math.Cos(System.Math.PI), System.Math.Sin(System.Math.PI));
var direction = new Vector(
System.Math.Cos(System.Math.PI),
System.Math.Sin(System.Math.PI)
);
var distance = Compactor.Push(moving, obstacles, workArea, 0, direction);
Assert.True(distance > 0);
@@ -394,7 +446,13 @@ namespace OpenNest.Tests.Fill
var moving = new List<Part> { part };
var obstacles = new List<Part>();
var distance = Compactor.PushBoundingBox(moving, obstacles, workArea, 0, PushDirection.Left);
var distance = Compactor.PushBoundingBox(
moving,
obstacles,
workArea,
0,
PushDirection.Left
);
Assert.True(distance > 0);
Assert.True(part.BoundingBox.Left < 1);
+14 -14
View File
@@ -37,12 +37,12 @@ public class DefaultFillComparerTests
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(20, 0, 10),
TestHelpers.MakePartAt(40, 0, 10)
TestHelpers.MakePartAt(40, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(20, 0, 10)
TestHelpers.MakePartAt(20, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -53,12 +53,12 @@ public class DefaultFillComparerTests
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10)
TestHelpers.MakePartAt(12, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(50, 0, 10)
TestHelpers.MakePartAt(50, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -76,12 +76,12 @@ public class VerticalRemnantComparerTests
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(40, 0, 10),
TestHelpers.MakePartAt(80, 0, 10)
TestHelpers.MakePartAt(80, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10)
TestHelpers.MakePartAt(12, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -92,12 +92,12 @@ public class VerticalRemnantComparerTests
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10)
TestHelpers.MakePartAt(12, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(50, 0, 10)
TestHelpers.MakePartAt(50, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -108,12 +108,12 @@ public class VerticalRemnantComparerTests
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(40, 0, 10)
TestHelpers.MakePartAt(40, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(40, 40, 10)
TestHelpers.MakePartAt(40, 40, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -144,12 +144,12 @@ public class HorizontalRemnantComparerTests
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 12, 10)
TestHelpers.MakePartAt(0, 12, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 50, 10)
TestHelpers.MakePartAt(0, 50, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -161,12 +161,12 @@ public class HorizontalRemnantComparerTests
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 40, 10),
TestHelpers.MakePartAt(0, 80, 10)
TestHelpers.MakePartAt(0, 80, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 12, 10)
TestHelpers.MakePartAt(0, 12, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
+24 -12
View File
@@ -41,10 +41,14 @@ public class FillExtentsTests
foreach (var part in parts)
{
Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01,
$"Part right edge {part.BoundingBox.Right} exceeds work area {workArea.Right}");
Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01,
$"Part top edge {part.BoundingBox.Top} exceeds work area {workArea.Top}");
Assert.True(
part.BoundingBox.Right <= workArea.Right + 0.01,
$"Part right edge {part.BoundingBox.Right} exceeds work area {workArea.Right}"
);
Assert.True(
part.BoundingBox.Top <= workArea.Top + 0.01,
$"Part top edge {part.BoundingBox.Top} exceeds work area {workArea.Top}"
);
}
}
@@ -82,8 +86,10 @@ public class FillExtentsTests
// After adjustment, the gap should be small (within one part spacing).
var gap = workArea.Top - topEdge;
Assert.True(gap < 1.0,
$"Gap of {gap:F2} is too large — adjustment should fill close to the top");
Assert.True(
gap < 1.0,
$"Gap of {gap:F2} is too large — adjustment should fill close to the top"
);
}
[Fact]
@@ -96,8 +102,10 @@ public class FillExtentsTests
var parts = filler.Fill(drawing);
// With a 120-wide sheet and ~10-wide parts, we should get multiple columns.
Assert.True(parts.Count >= 8,
$"Expected multiple columns but got only {parts.Count} parts");
Assert.True(
parts.Count >= 8,
$"Expected multiple columns but got only {parts.Count} parts"
);
// Verify all parts are within bounds.
foreach (var part in parts)
@@ -136,10 +144,14 @@ public class FillExtentsTests
foreach (var part in parts)
{
Assert.True(part.BoundingBox.Left >= workArea.Left - 0.01,
$"Part left {part.BoundingBox.Left} below work area left {workArea.Left}");
Assert.True(part.BoundingBox.Bottom >= workArea.Bottom - 0.01,
$"Part bottom {part.BoundingBox.Bottom} below work area bottom {workArea.Bottom}");
Assert.True(
part.BoundingBox.Left >= workArea.Left - 0.01,
$"Part left {part.BoundingBox.Left} below work area left {workArea.Left}"
);
Assert.True(
part.BoundingBox.Bottom >= workArea.Bottom - 0.01,
$"Part bottom {part.BoundingBox.Bottom} below work area bottom {workArea.Bottom}"
);
Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01);
Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01);
}
+38 -18
View File
@@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest;
using OpenNest.CNC;
using OpenNest.Converters;
@@ -6,8 +8,6 @@ using OpenNest.Geometry;
using OpenNest.Math;
using Xunit;
using Xunit.Abstractions;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Tests.Fill
{
@@ -32,19 +32,23 @@ namespace OpenNest.Tests.Fill
// Outer circle (CCW)
var outerStart = new Vector(outerRadius * 2, outerRadius);
pgm.Codes.Add(new RapidMove(outerStart));
pgm.Codes.Add(new ArcMove(outerStart, new Vector(outerRadius, outerRadius), RotationType.CCW));
pgm.Codes.Add(
new ArcMove(outerStart, new Vector(outerRadius, outerRadius), RotationType.CCW)
);
// Inner circle (CW = hole)
var innerStart = new Vector(outerRadius + innerRadius, outerRadius);
pgm.Codes.Add(new RapidMove(innerStart));
pgm.Codes.Add(new ArcMove(innerStart, new Vector(outerRadius, outerRadius), RotationType.CW));
pgm.Codes.Add(
new ArcMove(innerStart, new Vector(outerRadius, outerRadius), RotationType.CW)
);
return new Drawing("ring", pgm);
}
[Theory]
[InlineData(2.0, 0.125)] // 4" diameter circle, 1/8" spacing
[InlineData(1.0, 0.125)] // 2" diameter circle
[InlineData(3.0, 0.0625)] // 6" diameter circle, 1/16" spacing
[InlineData(0.5, 0.25)] // 1" diameter circle, 1/4" spacing
[InlineData(2.0, 0.125)] // 4" diameter circle, 1/8" spacing
[InlineData(1.0, 0.125)] // 2" diameter circle
[InlineData(3.0, 0.0625)] // 6" diameter circle, 1/16" spacing
[InlineData(0.5, 0.25)] // 1" diameter circle, 1/4" spacing
public void CircleFill_OffsetBoundaries_DoNotOverlap(double radius, double spacing)
{
var drawing = MakeCircleDrawing(radius);
@@ -58,21 +62,31 @@ namespace OpenNest.Tests.Fill
}
[Theory]
[InlineData(2.0, 1.5, 0.125)] // Ring: outer R=2, inner R=1.5
[InlineData(1.5, 1.0, 0.125)] // Ring: outer R=1.5, inner R=1.0
public void RingFill_OffsetBoundaries_DoNotOverlap(double outerR, double innerR, double spacing)
[InlineData(2.0, 1.5, 0.125)] // Ring: outer R=2, inner R=1.5
[InlineData(1.5, 1.0, 0.125)] // Ring: outer R=1.5, inner R=1.0
public void RingFill_OffsetBoundaries_DoNotOverlap(
double outerR,
double innerR,
double spacing
)
{
var drawing = MakeRingDrawing(outerR, innerR);
var workArea = new Box(0, 0, 48, 48);
var engine = new FillLinear(workArea, spacing);
var parts = engine.Fill(drawing, 0, NestDirection.Horizontal);
_output.WriteLine($"Ring outerR={outerR}, innerR={innerR}, spacing={spacing}: {parts.Count} parts");
_output.WriteLine(
$"Ring outerR={outerR}, innerR={innerR}, spacing={spacing}: {parts.Count} parts"
);
AssertNoOffsetOverlap(parts, spacing, outerR * 2);
}
private void AssertNoOffsetOverlap(List<Part> parts, double spacing, double expectedDiameter)
private void AssertNoOffsetOverlap(
List<Part> parts,
double spacing,
double expectedDiameter
)
{
if (parts.Count < 2)
{
@@ -109,21 +123,27 @@ namespace OpenNest.Tests.Fill
violationCount++;
if (violationCount <= 5)
{
_output.WriteLine($" SPACING VIOLATION parts[{i}] vs parts[{j}]: " +
$"centerDist={centerDist:F6}, rawGap={rawGap:F6}, offsetGap={offsetGap:F6}, " +
$"expected>={spacing:F4}");
_output.WriteLine(
$" SPACING VIOLATION parts[{i}] vs parts[{j}]: "
+ $"centerDist={centerDist:F6}, rawGap={rawGap:F6}, offsetGap={offsetGap:F6}, "
+ $"expected>={spacing:F4}"
);
}
}
}
}
_output.WriteLine($" Min gap={minGap:F6}, expected>={spacing:F4}, violations={violationCount}");
_output.WriteLine(
$" Min gap={minGap:F6}, expected>={spacing:F4}, violations={violationCount}"
);
if (violationCount > 0)
{
var maxDeficit = spacing - minGap;
_output.WriteLine($" Max deficit={maxDeficit:F6}");
Assert.Fail($"{violationCount} pairs violate spacing: min gap={minGap:F6}, expected>={spacing}, deficit={maxDeficit:F6}");
Assert.Fail(
$"{violationCount} pairs violate spacing: min gap={minGap:F6}, expected>={spacing}, deficit={maxDeficit:F6}"
);
}
}
}
+28 -6
View File
@@ -13,12 +13,19 @@ public class FillWithDirectionPreferenceTests
[Fact]
public void NullPreference_TriesBothDirections_ReturnsBetter()
{
var hParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(12, 0, 10) };
var hParts = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10),
};
var vParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var result = FillHelpers.FillWithDirectionPreference(
dir => dir == NestDirection.Horizontal ? hParts : vParts,
null, comparer, workArea);
null,
comparer,
workArea
);
Assert.Equal(2, result.Count);
}
@@ -26,12 +33,24 @@ public class FillWithDirectionPreferenceTests
[Fact]
public void PreferredDirection_UsedFirst_WhenProducesResults()
{
var hParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(12, 0, 10) };
var vParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(0, 12, 10), TestHelpers.MakePartAt(0, 24, 10) };
var hParts = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10),
};
var vParts = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 12, 10),
TestHelpers.MakePartAt(0, 24, 10),
};
var result = FillHelpers.FillWithDirectionPreference(
dir => dir == NestDirection.Horizontal ? hParts : vParts,
NestDirection.Horizontal, comparer, workArea);
NestDirection.Horizontal,
comparer,
workArea
);
Assert.Equal(2, result.Count); // H has results, so H is returned (preferred)
}
@@ -43,7 +62,10 @@ public class FillWithDirectionPreferenceTests
var result = FillHelpers.FillWithDirectionPreference(
dir => dir == NestDirection.Horizontal ? new List<Part>() : vParts,
NestDirection.Horizontal, comparer, workArea);
NestDirection.Horizontal,
comparer,
workArea
);
Assert.Equal(1, result.Count); // Falls back to V
}
+6 -3
View File
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
namespace OpenNest.Tests.Fill;
@@ -57,7 +57,10 @@ public class FillScoreTests
[Fact]
public void Compute_EmptyParts_ReturnsDefault()
{
var score = FillScore.Compute(new System.Collections.Generic.List<Part>(), new Box(0, 0, 100, 100));
var score = FillScore.Compute(
new System.Collections.Generic.List<Part>(),
new Box(0, 0, 100, 100)
);
Assert.Equal(0, score.Count);
}
@@ -69,7 +72,7 @@ public class FillScoreTests
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(20, 0, 10),
TestHelpers.MakePartAt(40, 0, 10)
TestHelpers.MakePartAt(40, 0, 10),
};
var score = FillScore.Compute(parts, new Box(0, 0, 100, 100));
@@ -19,7 +19,12 @@ public class IterativeShrinkFillerTests
public void Fill_EmptyItems_ReturnsEmpty()
{
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) => new List<Part>();
var result = IterativeShrinkFiller.Fill(new List<NestItem>(), new Box(0, 0, 100, 100), fillFunc, 1.0);
var result = IterativeShrinkFiller.Fill(
new List<NestItem>(),
new Box(0, 0, 100, 100),
fillFunc,
1.0
);
Assert.Empty(result.Parts);
Assert.Empty(result.Leftovers);
@@ -42,7 +47,7 @@ public class IterativeShrinkFillerTests
var drawing = MakeRectDrawing(20, 10);
var items = new List<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 5 }
new NestItem { Drawing = drawing, Quantity = 5 },
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
@@ -110,7 +115,7 @@ public class IterativeShrinkFillerTests
{
var items = new List<NestItem>
{
new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 0 }
new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 0 },
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
@@ -134,13 +139,19 @@ public class IterativeShrinkFillerTests
var items = new List<NestItem>
{
new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 10 }
new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 10 },
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 100, 100), fillFunc, 1.0, cts.Token);
var result = IterativeShrinkFiller.Fill(
items,
new Box(0, 0, 100, 100),
fillFunc,
1.0,
cts.Token
);
Assert.NotNull(result);
}
@@ -47,10 +47,10 @@ public class PairOverlapDiagnosticTests
}
[Theory]
[InlineData(0)] // 0 degrees
[InlineData(90)] // 90 degrees
[InlineData(180)] // 180 degrees
[InlineData(270)] // 270 degrees
[InlineData(0)] // 0 degrees
[InlineData(90)] // 90 degrees
[InlineData(180)] // 180 degrees
[InlineData(270)] // 270 degrees
public void PartBoundary_HasEdgesAtAllRotations_RoundedRect(double angleDeg)
{
var drawing = MakeRoundedRect();
@@ -109,8 +109,8 @@ public class PairOverlapDiagnosticTests
}
[Theory]
[InlineData(false)] // simple rect
[InlineData(true)] // rounded rect
[InlineData(false)] // simple rect
[InlineData(true)] // rounded rect
public void FillExtents_NoPairOverlap_At90Degrees(bool rounded)
{
var drawing = rounded ? MakeRoundedRect() : MakeSimpleRect();
@@ -126,8 +126,10 @@ public class PairOverlapDiagnosticTests
for (var i = 0; i < parts.Count; i++)
{
var p = parts[i];
_output.WriteLine($" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° " +
$"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})");
_output.WriteLine(
$" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° "
+ $"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})"
);
}
// Check for overlapping bounding boxes
@@ -137,15 +139,21 @@ public class PairOverlapDiagnosticTests
for (var j = i + 1; j < parts.Count; j++)
{
var b2 = parts[j].BoundingBox;
var overlapX = System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left);
var overlapY = System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom);
var overlapX =
System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left);
var overlapY =
System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom);
if (overlapX > 0.01 && overlapY > 0.01)
_output.WriteLine($" OVERLAP: [{i}] and [{j}] overlap by ({overlapX:F3}, {overlapY:F3})");
_output.WriteLine(
$" OVERLAP: [{i}] and [{j}] overlap by ({overlapX:F3}, {overlapY:F3})"
);
Assert.False(overlapX > 0.01 && overlapY > 0.01,
$"Parts [{i}] and [{j}] have overlapping bounding boxes " +
$"({overlapX:F3} x {overlapY:F3})");
Assert.False(
overlapX > 0.01 && overlapY > 0.01,
$"Parts [{i}] and [{j}] have overlapping bounding boxes "
+ $"({overlapX:F3} x {overlapY:F3})"
);
}
}
}
@@ -172,8 +180,12 @@ public class PairOverlapDiagnosticTests
var b1 = new PartBoundary(part1, partSpacing / 2);
var b2 = new PartBoundary(part2, partSpacing / 2);
_output.WriteLine($"Part1 (90°) boundary edges: L={b1.GetEdges(PushDirection.Left).Length} R={b1.GetEdges(PushDirection.Right).Length}");
_output.WriteLine($"Part2 (270°) boundary edges: L={b2.GetEdges(PushDirection.Left).Length} R={b2.GetEdges(PushDirection.Right).Length}");
_output.WriteLine(
$"Part1 (90°) boundary edges: L={b1.GetEdges(PushDirection.Left).Length} R={b1.GetEdges(PushDirection.Right).Length}"
);
_output.WriteLine(
$"Part2 (270°) boundary edges: L={b2.GetEdges(PushDirection.Left).Length} R={b2.GetEdges(PushDirection.Right).Length}"
);
var movingLines = b2.GetLines(part2.Location, PushDirection.Left);
var stationaryLines = b1.GetLines(part1.Location, PushDirection.Right);
@@ -189,7 +201,11 @@ public class PairOverlapDiagnosticTests
foreach (var l in stationaryLines)
_output.WriteLine($" ({l.pt1.X:F4},{l.pt1.Y:F4})->({l.pt2.X:F4},{l.pt2.Y:F4})");
var slideDist = SpatialQuery.DirectionalDistance(movingLines, stationaryLines, PushDirection.Left);
var slideDist = SpatialQuery.DirectionalDistance(
movingLines,
stationaryLines,
PushDirection.Left
);
_output.WriteLine($"Slide distance: {slideDist:F4}");
if (slideDist < double.MaxValue && slideDist > 0)
@@ -198,8 +214,12 @@ public class PairOverlapDiagnosticTests
part2.UpdateBounds();
}
_output.WriteLine($"Part1 bbox: ({part1.BoundingBox.Left:F2},{part1.BoundingBox.Bottom:F2})-({part1.BoundingBox.Right:F2},{part1.BoundingBox.Top:F2})");
_output.WriteLine($"Part2 bbox: ({part2.BoundingBox.Left:F2},{part2.BoundingBox.Bottom:F2})-({part2.BoundingBox.Right:F2},{part2.BoundingBox.Top:F2})");
_output.WriteLine(
$"Part1 bbox: ({part1.BoundingBox.Left:F2},{part1.BoundingBox.Bottom:F2})-({part1.BoundingBox.Right:F2},{part1.BoundingBox.Top:F2})"
);
_output.WriteLine(
$"Part2 bbox: ({part2.BoundingBox.Left:F2},{part2.BoundingBox.Bottom:F2})-({part2.BoundingBox.Right:F2},{part2.BoundingBox.Top:F2})"
);
// Now tile this pair pattern
var pattern = new Pattern();
@@ -216,8 +236,10 @@ public class PairOverlapDiagnosticTests
for (var i = 0; i < parts.Count; i++)
{
var p = parts[i];
_output.WriteLine($" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° " +
$"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})");
_output.WriteLine(
$" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° "
+ $"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})"
);
}
// Check for overlaps
@@ -230,8 +252,10 @@ public class PairOverlapDiagnosticTests
var ox = System.Math.Min(bi.Right, bj.Right) - System.Math.Max(bi.Left, bj.Left);
var oy = System.Math.Min(bi.Top, bj.Top) - System.Math.Max(bi.Bottom, bj.Bottom);
Assert.False(ox > 0.01 && oy > 0.01,
$"Parts [{i}] and [{j}] overlap ({ox:F3} x {oy:F3})");
Assert.False(
ox > 0.01 && oy > 0.01,
$"Parts [{i}] and [{j}] overlap ({ox:F3} x {oy:F3})"
);
}
}
}
+3 -3
View File
@@ -28,7 +28,7 @@ public class RemnantFillerTests2
var drawing = MakeSquareDrawing(10);
var items = new List<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 5 }
new NestItem { Drawing = drawing, Quantity = 5 },
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
@@ -52,7 +52,7 @@ public class RemnantFillerTests2
var drawing = MakeSquareDrawing(10);
var items = new List<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 3 }
new NestItem { Drawing = drawing, Quantity = 3 },
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
@@ -92,7 +92,7 @@ public class RemnantFillerTests2
var drawing = MakeSquareDrawing(10);
var items = new List<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 5 }
new NestItem { Drawing = drawing, Quantity = 5 },
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
+69 -44
View File
@@ -108,16 +108,15 @@ public class RemnantFinderTests
var remnants = finder.FindRemnants();
var gap = remnants.FirstOrDefault(r =>
r.Length >= 19.9 && r.Length <= 20.1 &&
r.Width >= 99.9);
r.Length >= 19.9 && r.Length <= 20.1 && r.Width >= 99.9
);
Assert.NotNull(gap);
}
[Fact]
public void FromPlate_CreatesFinderWithPartsAsObstacles()
{
var plate = TestHelpers.MakePlate(60, 120,
TestHelpers.MakePartAt(0, 0, 20));
var plate = TestHelpers.MakePlate(60, 120, TestHelpers.MakePartAt(0, 0, 20));
var finder = RemnantFinder.FromPlate(plate);
var remnants = finder.FindRemnants();
@@ -146,8 +145,8 @@ public class RemnantFinderTests
// Should find the 80x100 strip on the left
var left = remnants.FirstOrDefault(r =>
r.Length >= 79.9 && r.Length <= 80.1 &&
r.Width >= 99.9);
r.Length >= 79.9 && r.Length <= 80.1 && r.Width >= 99.9
);
Assert.NotNull(left);
}
@@ -163,9 +162,16 @@ public class RemnantFinderTests
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");
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
@@ -177,16 +183,11 @@ public class RemnantFinderTests
[Fact]
public void ConstructorWithObstaclesList()
{
var obstacles = new List<Box>
{
new Box(0, 0, 40, 100),
new Box(60, 0, 40, 100)
};
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.Length >= 19.9 && r.Length <= 20.1);
var gap = remnants.FirstOrDefault(r => r.Length >= 19.9 && r.Length <= 20.1);
Assert.NotNull(gap);
}
@@ -194,15 +195,10 @@ public class RemnantFinderTests
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)
});
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.Length >= 19.9 && r.Length <= 20.1);
var gap = remnants.FirstOrDefault(r => r.Length >= 19.9 && r.Length <= 20.1);
Assert.NotNull(gap);
}
@@ -239,11 +235,17 @@ public class RemnantFinderTests
{
// 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");
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");
Assert.False(
overlaps2,
$"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 2"
);
}
}
@@ -254,8 +256,8 @@ public class RemnantFinderTests
// 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));
for (var col = 0; col < 5; col++)
finder.AddObstacle(new Box(col * 20, row * 20, 10, 10));
var remnants = finder.FindRemnants();
@@ -304,16 +306,19 @@ public class RemnantFinderTests
// Use smallest drawing bbox dimension as minDim (same as UI).
var minDim = nest.Drawings.Min(d =>
System.Math.Min(d.Program.BoundingBox().Width, d.Program.BoundingBox().Length));
System.Math.Min(d.Program.BoundingBox().Width, d.Program.BoundingBox().Length)
);
var tiered = finder.FindTieredRemnants(minDim);
// Should find a remnant near (0.25, 53.13) — the gap above the main grid.
var topGap = tiered.FirstOrDefault(t =>
t.Box.Bottom > 50 && t.Box.Bottom < 55 &&
t.Box.Left < 1 &&
t.Box.Length > 100 &&
t.Box.Width > 5);
t.Box.Bottom > 50
&& t.Box.Bottom < 55
&& t.Box.Left < 1
&& t.Box.Length > 100
&& t.Box.Width > 5
);
Assert.True(topGap.Box.Length > 0, "Expected remnant above main grid");
}
@@ -337,24 +342,44 @@ public class RemnantFinderTests
double[] oddY = { 0.75, 9.48, 18.21, 26.94, 35.67, 44.40 };
foreach (var cx in colX)
foreach (var ey in evenY)
obstacles.Add(new Box(cx - spacing, ey - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2));
foreach (var ey in evenY)
obstacles.Add(
new Box(cx - spacing, ey - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2)
);
foreach (var cx in colXOdd)
foreach (var oy in oddY)
obstacles.Add(new Box(cx - spacing, oy - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2));
foreach (var oy in oddY)
obstacles.Add(
new Box(cx - spacing, oy - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2)
);
// Right-side rotated parts (only 2 extend high: parts 62 and 66).
obstacles.Add(new Box(106.70 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(new Box(114.19 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(
new Box(106.70 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)
);
obstacles.Add(
new Box(114.19 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)
);
// Parts 63, 67 (lower rotated)
obstacles.Add(new Box(105.02 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(new Box(112.51 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(
new Box(105.02 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)
);
obstacles.Add(
new Box(112.51 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)
);
// Parts 60, 64 (upper-right rotated, lower)
obstacles.Add(new Box(106.70 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(new Box(114.19 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(
new Box(106.70 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)
);
obstacles.Add(
new Box(114.19 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)
);
// Parts 61, 65
obstacles.Add(new Box(105.02 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(new Box(112.51 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(
new Box(105.02 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)
);
obstacles.Add(
new Box(112.51 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)
);
var finder = new RemnantFinder(workArea, obstacles);
var remnants = finder.FindRemnants(5.375);
+16 -10
View File
@@ -72,8 +72,14 @@ public class ShrinkFillerTests
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0,
ShrinkAxis.Length, token: cts.Token);
var result = ShrinkFiller.Shrink(
fillFunc,
item,
box,
1.0,
ShrinkAxis.Length,
token: cts.Token
);
Assert.NotNull(result);
Assert.True(result.Parts.Count > 0);
@@ -84,10 +90,10 @@ public class ShrinkFillerTests
{
var parts = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 5), // Right = 5
TestHelpers.MakePartAt(10, 0, 5), // Right = 15
TestHelpers.MakePartAt(20, 0, 5), // Right = 25
TestHelpers.MakePartAt(30, 0, 5), // Right = 35
TestHelpers.MakePartAt(0, 0, 5), // Right = 5
TestHelpers.MakePartAt(10, 0, 5), // Right = 15
TestHelpers.MakePartAt(20, 0, 5), // Right = 25
TestHelpers.MakePartAt(30, 0, 5), // Right = 35
};
var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Width);
@@ -101,10 +107,10 @@ public class ShrinkFillerTests
{
var parts = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 5), // Top = 5
TestHelpers.MakePartAt(0, 10, 5), // Top = 15
TestHelpers.MakePartAt(0, 20, 5), // Top = 25
TestHelpers.MakePartAt(0, 30, 5), // Top = 35
TestHelpers.MakePartAt(0, 0, 5), // Top = 5
TestHelpers.MakePartAt(0, 10, 5), // Top = 15
TestHelpers.MakePartAt(0, 20, 5), // Top = 25
TestHelpers.MakePartAt(0, 30, 5), // Top = 35
};
var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Length);