diff --git a/OpenNest.Engine.Tests/NestingEngines/IrregularExactContactTests.cs b/OpenNest.Engine.Tests/NestingEngines/IrregularExactContactTests.cs
new file mode 100644
index 0000000..15e0cb4
--- /dev/null
+++ b/OpenNest.Engine.Tests/NestingEngines/IrregularExactContactTests.cs
@@ -0,0 +1,206 @@
+using OpenNest.Engine.Jobs;
+using OpenNest.Engine.NestingEngines.Irregular;
+using OpenNest.Geometry;
+using static OpenNest.Engine.Tests.NestingEngines.JobBuilder;
+
+namespace OpenNest.Engine.Tests.NestingEngines;
+
+///
+/// A part whose material exactly fills the work area in one allowed rotation. The engine must
+/// place it: preparation padding for arc flattening may not shrink a line-only part's room.
+///
+public class IrregularExactContactTests
+{
+ private const double PartLength = 8;
+ private const double PartWidth = 3;
+ private const double Edge = 0.25;
+ private const double Spacing = 0.25;
+
+ /// Gap shortfall the independent oracle tolerates (float noise only).
+ private const double OracleNoise = 1e-9;
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ [InlineData(4)]
+ public void ExactContactRotatedRectangleIsPlacedInEveryQuadrant(int quadrant)
+ {
+ var job = RectangleJob(8.5, 3.5, quadrant, RotationPolicy.Automatic);
+
+ var result = new IrregularNestingEngine().Solve(job);
+
+ AssertPlacedSafely(job, result);
+ }
+
+ [Theory]
+ [InlineData(8.501, 3.501)]
+ [InlineData(8.5, 3.501)]
+ [InlineData(8.501, 3.5)]
+ [InlineData(8.52, 3.52)] // Generator smoke stock: a looser control, not exact-contact acceptance.
+ public void LargerNeighbouringStockStillPlacesThePart(double width, double length)
+ {
+ var job = RectangleJob(width, length, 1, RotationPolicy.Automatic);
+
+ AssertPlacedSafely(job, new IrregularNestingEngine().Solve(job));
+ }
+
+ [Theory]
+ [InlineData(8.499, 3.5, 1)]
+ [InlineData(8.5, 3.499, 1)]
+ [InlineData(8.499, 3.499, 3)]
+ public void StockOneThousandthTooSmallLeavesThePartUnplaced(double width, double length, int quadrant)
+ {
+ var job = RectangleJob(width, length, quadrant, RotationPolicy.Automatic);
+
+ AssertUnplaced(job, new IrregularNestingEngine().Solve(job));
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(3)]
+ public void ZeroRotationOnlyCannotFitAndStaysUnplaced(int quadrant)
+ {
+ var job = RectangleJob(8.5, 3.5, quadrant, RotationPolicy.Fixed(0));
+
+ AssertUnplaced(job, new IrregularNestingEngine().Solve(job));
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ [InlineData(4)]
+ public void HandPoseAtExactContactPassesAndOneThousandthOverEdgeFails(int quadrant)
+ {
+ var job = RectangleJob(8.5, 3.5, quadrant, RotationPolicy.Automatic);
+ var stock = job.Plates[0];
+ var work = stock.WorkArea;
+ // +90 degrees maps the source rectangle to X in [-3, 0], Y in [0, 8].
+ var exact = new NestJobPlacement("rect", 0, work.Left + PartWidth, work.Bottom, System.Math.PI / 2);
+ Assert.True(OracleGaps(stock, exact).Min() >= Edge - OracleNoise);
+ Assert.Empty(Violations(job, exact));
+ Validate(job, exact);
+
+ foreach (var (dx, dy) in new[] { (0.001, 0.0), (-0.001, 0.0), (0.0, 0.001), (0.0, -0.001) })
+ {
+ var over = exact with { X = exact.X + dx, Y = exact.Y + dy };
+ Assert.True(OracleGaps(stock, over).Min() < Edge - 0.0009);
+ Assert.NotEmpty(Violations(job, over));
+ Assert.Throws(() => Validate(job, over));
+ }
+ }
+
+ [Fact]
+ public void LineOnlyBoundsAreExactWhileFlatteningToleranceIsKept()
+ {
+ var job = RectangleJob(8.5, 3.5, 1, RotationPolicy.Automatic);
+ var type = PartCatalog.Build(job).Single();
+ var rotated = type.Orientations.Single(o => System.Math.Abs(o.Rotation - System.Math.PI / 2) < 1e-9);
+
+ // Chord tolerance still drives tessellation and NFP footprints.
+ Assert.Equal(PartCatalog.ChordTolerance, rotated.Tolerance);
+ Assert.Equal(-PartWidth, rotated.MinX, 12);
+ Assert.Equal(0, rotated.MinY, 12);
+ Assert.Equal(0, rotated.MaxX, 12);
+ Assert.Equal(PartLength, rotated.MaxY, 12);
+ Assert.True(job.Plates[0].Fits(rotated.Width, rotated.Height));
+ }
+
+ [Theory]
+ [InlineData(0.0)]
+ [InlineData(0.3)]
+ [InlineData(1.2)]
+ public void ArcBoundsStillContainTheTruePerimeterAndTheFlattenedOutline(double angle)
+ {
+ var job = Job(new[] { Part("obround", Shapes.Obround(9, 4), 1, RotationPolicy.Fixed(angle)) },
+ new[] { Stock("sheet", 40, 40, spacing: Spacing) });
+ var type = PartCatalog.Build(job).Single();
+ var o = type.Orientations.Single();
+ Assert.True(o.Tolerance >= PartCatalog.ChordTolerance);
+
+ // Independent oracle: sample the stadium analytically (straight sides plus semicircles).
+ const double length = 9, width = 4, r = width / 2;
+ var points = new List<(double X, double Y)>();
+ for (var i = 0; i <= 2000; i++)
+ {
+ var t = System.Math.PI * i / 2000;
+ points.Add((1 + length - r + r * System.Math.Sin(t), 1 + r - r * System.Math.Cos(t)));
+ points.Add((1 + r - r * System.Math.Sin(t), 1 + r + r * System.Math.Cos(t)));
+ }
+ var (c, s) = (System.Math.Cos(angle), System.Math.Sin(angle));
+ foreach (var (x, y) in points)
+ {
+ var rx = x * c - y * s;
+ var ry = x * s + y * c;
+ Assert.InRange(rx, o.MinX - 1e-9, o.MaxX + 1e-9);
+ Assert.InRange(ry, o.MinY - 1e-9, o.MaxY + 1e-9);
+ }
+ foreach (var p in o.Outline)
+ {
+ Assert.InRange(p.x, o.MinX, o.MaxX);
+ Assert.InRange(p.y, o.MinY, o.MaxY);
+ }
+ }
+
+ private static NestJob RectangleJob(double width, double length, int quadrant, RotationPolicy rotation) =>
+ Job(new[] { Rectangle("rect", PartLength, PartWidth, 1, rotation) },
+ new[] { Stock("sheet", width, length, spacing: Spacing, edge: new Spacing(Edge, Edge, Edge, Edge), quadrant: quadrant) });
+
+ private static void AssertPlacedSafely(NestJob job, NestJobResult result)
+ {
+ Assert.Equal(NestJobStatus.Complete, result.Status);
+ var fulfillment = Assert.Single(result.Fulfillment);
+ Assert.Equal((1, 1, 0), (fulfillment.Requested, fulfillment.Placed, fulfillment.Unplaced));
+ var sheet = Assert.Single(result.Plates);
+ Assert.Equal(1, Assert.Single(result.StockUsage).Used);
+ var pose = Assert.Single(sheet.Placements);
+
+ Assert.True(OracleGaps(sheet.Stock, pose).Min() >= Edge - OracleNoise,
+ $"pose ({pose.X:R}, {pose.Y:R}, {pose.Rotation:R}) gaps {string.Join(", ", OracleGaps(sheet.Stock, pose))}");
+ LayoutAssert.Valid(job, result);
+ Assert.Empty(NestLayoutCheck.Violations(job, result));
+ Validate(job, pose);
+ }
+
+ private static void AssertUnplaced(NestJob job, NestJobResult result)
+ {
+ Assert.NotEqual(NestJobStatus.Complete, result.Status);
+ Assert.Empty(result.Plates);
+ var fulfillment = Assert.Single(result.Fulfillment);
+ Assert.Equal((1, 0, 1), (fulfillment.Requested, fulfillment.Placed, fulfillment.Unplaced));
+ LayoutAssert.Valid(job, result);
+ }
+
+ /// Clearance from the rotated nominal rectangle to each physical sheet edge
+ /// (left, bottom, right, top), computed from corners and the quadrant's sheet origin.
+ private static double[] OracleGaps(NestPlateStock stock, NestJobPlacement pose)
+ {
+ var (c, s) = (System.Math.Cos(pose.Rotation), System.Math.Sin(pose.Rotation));
+ var corners = new[] { (0.0, 0.0), (PartLength, 0.0), (PartLength, PartWidth), (0.0, PartWidth) }
+ .Select(p => (X: p.Item1 * c - p.Item2 * s + pose.X, Y: p.Item1 * s + p.Item2 * c + pose.Y))
+ .ToArray();
+ var sheetLeft = stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length;
+ var sheetBottom = stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width;
+ return new[]
+ {
+ corners.Min(p => p.X) - sheetLeft,
+ corners.Min(p => p.Y) - sheetBottom,
+ sheetLeft + stock.Size.Length - corners.Max(p => p.X),
+ sheetBottom + stock.Size.Width - corners.Max(p => p.Y),
+ };
+ }
+
+ private static IReadOnlyList Violations(NestJob job, NestJobPlacement pose)
+ {
+ var stock = job.Plates[0];
+ var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
+ new[] { new NestJobPlateResult(0, stock, new[] { pose }) },
+ new[] { new PartFulfillment("rect", 1, 1, 0) }, new[] { new StockUsage(stock.Id, 1, null) });
+ return NestLayoutCheck.Violations(job, result);
+ }
+
+ private static void Validate(NestJob job, NestJobPlacement pose) =>
+ NestJobValidator.ValidateCandidate(new PlateCandidate(new[] { pose }), job.Plates[0],
+ new Dictionary { ["rect"] = 1 }, job.Parts.ToDictionary(p => p.Id));
+}