Files
OpenNest/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs
T
ajandClaude Opus 5.5 4d55e11318 style: use one spelling across the codebase
The repo mixed variants such as center/centre and color/colour, so a
name could be spelled either way and searches missed half the hits.
Standardize on the US spelling, matching .NET's own APIs (Color, Center).

Wire values owned by other systems keep their spelling: the material
ledger's "Cancelled" hold status and GitHub's "cancelled" job status.

No behavior change beyond the spelling of a few status messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:11:30 -04:00

298 lines
13 KiB
C#

using OpenNest.CNC;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Engine.NestingEngines.Default;
using OpenNest.Engine.NestingEngines.Rectangles;
using OpenNest.Geometry;
namespace OpenNest.Engine.Tests.Jobs;
/// <summary>
/// C16 (PM 6859db53): a clamp zone the job validator accepts may be far smaller than the
/// validation outline tolerance (here 1e-5 square). Every keep-out consumer must check it
/// without throwing, flag a placement whose material overlaps it (plus the D6 clearance),
/// and keep a placement that only touches or clears it clear. Tolerances are unchanged (C5):
/// the expected verdicts below are exact rectangle arithmetic, not the checker's output.
/// </summary>
public class TinyClampZoneTests
{
private const double Tiny = 1e-5;
private static readonly ClampZone TinyZone = new("tiny", new Box(8, 0, Tiny, Tiny));
private static Program SquareProgram()
{
var program = new Program();
program.Codes.Add(new RapidMove(0, 0));
program.Codes.Add(new LinearMove(2, 0));
program.Codes.Add(new LinearMove(2, 2));
program.Codes.Add(new LinearMove(0, 2));
program.Codes.Add(new LinearMove(0, 0));
return program;
}
private static NestJobPart Square() =>
new("square", PartGeometrySnapshot.FromProgram(SquareProgram()), 1, 0, RotationPolicy.Fixed(0));
private static NestPlateStock Stock(double bottom) =>
new("sheet", new Size(10, 12), null, 0, new Spacing(0, bottom, 0, 0), 1,
new[] { TinyZone });
private static NestJob Job(double bottom) => new(new[] { Square() }, new[] { Stock(bottom) });
private static IReadOnlyList<string> ClampViolations(NestJob job, NestJobResult result) =>
NestLayoutCheck.Violations(job, result).Where(v => v.Contains("clamp zone")).ToList();
[Fact]
public void RectanglesResultOnATinyClampIsCheckedWithoutThrowing()
{
// The PM probe: Rectangles places the square flush against the tiny clamp.
var job = Job(0);
NestJobValidator.Validate(job);
var result = new RectanglesNestingEngine().Solve(job);
Assert.Equal(NestJobStatus.Complete, result.Status);
var violations = ClampViolations(job, result);
// Independent oracle: the square's footprint shares no positive area with the zone.
var pose = Assert.Single(result.Plates.SelectMany(p => p.Placements));
var overlapX = System.Math.Min(pose.X + 2, 8 + Tiny) - System.Math.Max(pose.X, 8);
var overlapY = System.Math.Min(pose.Y + 2, Tiny) - System.Math.Max(pose.Y, 0);
var expectOverlap = overlapX > 0 && overlapY > 0;
Assert.Equal(expectOverlap, violations.Count > 0);
}
[Fact]
public void DefaultSolvesATinyClampJobToAValidResult()
{
var job = Job(0);
var result = new DefaultNestingEngine().Solve(job);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Empty(NestLayoutCheck.Violations(job, result));
}
// (bottom edge spacing, x, y, material overlaps the zone inflated by the clearance).
// The keep-out is [8 - c, 8 + 1e-5 + c] x [-c, 1e-5 + c] with c = bottom - 0.0005
// (the shared spacing slack); the 2x2 square spans [x, x+2] x [y, y+2].
[Theory]
[InlineData(0.0, 7.5, 0.0, true)] // covers the zone
[InlineData(0.0, 6.0, 0.0, false)] // right edge flush on the zone's left side
[InlineData(0.0, 8.00001, 0.0, false)] // left edge flush on the zone's right side
[InlineData(0.0, 8.0, Tiny, false)] // bottom edge flush on the zone's top
[InlineData(0.0, 3.0, 0.0, false)] // well clear
[InlineData(0.25, 7.5, 0.1, true)] // over the zone, inside the clearance band
[InlineData(0.25, 8.0, 0.24, true)] // above the zone but inside the clearance
[InlineData(0.25, 8.25001, 0.1, false)] // exactly at the clearance to the right
[InlineData(0.25, 7.0, 0.25001, false)] // exactly at the clearance above
[InlineData(0.25, 3.0, 0.25, false)] // well clear
public void LayoutCheckJudgesPlacementsAgainstATinyClamp(double bottom, double x, double y,
bool overlaps)
{
var job = Job(bottom);
NestJobValidator.Validate(job);
var stock = job.Plates[0];
var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
new[] { new NestJobPlateResult(0, stock, new[] { new NestJobPlacement("square", 0, x, y, 0) }) },
Array.Empty<PartFulfillment>(), Array.Empty<StockUsage>());
var violations = ClampViolations(job, result);
Assert.Equal(overlaps, violations.Count > 0);
}
private static Program FrameProgram()
{
// 4x4 square with a 2x2 hole in its middle.
var program = new Program();
program.Codes.Add(new RapidMove(0, 0));
program.Codes.Add(new LinearMove(4, 0));
program.Codes.Add(new LinearMove(4, 4));
program.Codes.Add(new LinearMove(0, 4));
program.Codes.Add(new LinearMove(0, 0));
program.Codes.Add(new RapidMove(1, 1));
program.Codes.Add(new LinearMove(1, 3));
program.Codes.Add(new LinearMove(3, 3));
program.Codes.Add(new LinearMove(3, 1));
program.Codes.Add(new LinearMove(1, 1));
return program;
}
// The tiny zone inside the frame's hole is clear; inside its material it is not.
[Theory]
[InlineData(6.0, -2.0, false)] // hole spans [7, 9] x [-1, 1] around the zone
[InlineData(6.0, 0.0, true)] // the frame's bottom bar covers the zone
public void LayoutCheckSeesHolesAroundATinyClamp(double x, double y, bool overlaps)
{
var frame = new NestJobPart("frame", PartGeometrySnapshot.FromProgram(FrameProgram()), 1, 0,
RotationPolicy.Fixed(0));
var job = new NestJob(new[] { frame }, new[] { Stock(0) });
var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
new[] { new NestJobPlateResult(0, job.Plates[0], new[] { new NestJobPlacement("frame", 0, x, y, 0) }) },
Array.Empty<PartFulfillment>(), Array.Empty<StockUsage>());
Assert.Equal(overlaps, ClampViolations(job, result).Count > 0);
}
[Theory]
[InlineData(7.5, 0.0, true)]
[InlineData(6.0, 0.0, false)]
[InlineData(8.00001, 0.0, false)]
[InlineData(3.0, 0.0, false)]
public void InteractiveKeepOutTestJudgesATinyClamp(double x, double y, bool overlaps)
{
var drawing = new Drawing("square", SquareProgram());
var part = new Part(drawing, new Vector(x, y));
Assert.Equal(overlaps, InteractiveClamps.MaterialInKeepOut(part, new Box(8, 0, Tiny, Tiny)));
}
private static Plate TinyZonePlate()
{
var plate = new Plate(new Size(10, 12)); // zero edge and part spacing
plate.ClampZones.Add(TinyZone);
return plate;
}
// Positive-area overlap with the zone, admitting 1e-9 of floating closure (a ten
// thousandth of the zone): a push that stops flush on the zone is clear.
private static bool EntersTinyZone(Part part) =>
System.Math.Min(part.BoundingBox.Right, 8 + Tiny) - System.Math.Max(part.BoundingBox.Left, 8) > 1e-9
&& System.Math.Min(part.BoundingBox.Top, Tiny) - System.Math.Max(part.BoundingBox.Bottom, 0) > 1e-9;
[Theory]
[InlineData("Fill")]
[InlineData("Strip")]
[InlineData("Vertical Remnant")]
[InlineData("Horizontal Remnant")]
public void InteractiveFillNeverCoversATinyClamp(string strategy)
{
var plate = TinyZonePlate();
var item = new NestItem { Drawing = new Drawing("square", SquareProgram()), Quantity = 30 };
var parts = PlateFillService.FillItem(strategy, plate, item, plate.WorkArea(), null,
System.Threading.CancellationToken.None);
Assert.NotEmpty(parts);
Assert.DoesNotContain(parts, EntersTinyZone);
}
[Fact]
public void PushDownStopsBeforeCoveringATinyClamp()
{
var plate = TinyZonePlate();
var mover = new Part(new Drawing("square", SquareProgram()), new Vector(7.5, 5));
plate.Parts.Add(mover);
Compactor.Push(new List<Part> { mover }, plate, PushDirection.Down);
Assert.False(EntersTinyZone(mover), $"pushed to ({mover.Location.X:R},{mover.Location.Y:R})");
}
// PM 407f5eb7: the tolerance-free test must read the part's real material, not the
// 1e-4-grid validation outline or a 0.001 arc flattening.
private static Program RectProgram(double w, double h)
{
var program = new Program();
program.MoveTo(0, 0);
program.LineTo(w, 0);
program.LineTo(w, h);
program.LineTo(0, h);
program.LineTo(0, 0);
return program;
}
private static Program DiskProgram()
{
// Radius 2 centerd at (10, 10), two absolute CCW half circles.
var program = new Program();
program.MoveTo(12, 10);
program.ArcTo(8, 10, 10, 10, RotationType.CCW);
program.ArcTo(12, 10, 10, 10, RotationType.CCW);
return program;
}
private static Box DiskBox(double radius)
{
var cx = 10 + radius * System.Math.Cos(0.02);
var cy = 10 + radius * System.Math.Sin(0.02);
return new Box(cx - 0.000005, cy - 0.000005, Tiny, Tiny);
}
public static TheoryData<string, Box, bool> RealMaterialRows => new()
{
{ "rect-8.00004", new Box(8.00002, 0.5, Tiny, Tiny), true }, // wholly inside the part
{ "rect-8.00004", new Box(8.00005, 0.5, Tiny, Tiny), false }, // just beyond its edge
{ "rect-2.00004", new Box(2.000032, 1, 5e-6, Tiny), true },
{ "disk", DiskBox(1.9998), true }, // inside the true arc, outside its 0.001 chords
{ "disk", DiskBox(2.0002), false }, // just outside the true arc
// The arc cuts a corner off the zone, 6.2e-7 deep: less than a fine chord's sag.
{ "disk", new Box(11.935615871667656, 10.503377293956166, Tiny, Tiny), true },
};
private static Program MaterialProgram(string name) => name switch
{
"rect-8.00004" => RectProgram(8.00004, 2),
"rect-2.00004" => RectProgram(2.00004, 2),
_ => DiskProgram(),
};
[Theory]
[MemberData(nameof(RealMaterialRows))]
public void LayoutCheckReadsRealMaterialAroundATinyClamp(string material, Box zone, bool overlaps)
{
var program = MaterialProgram(material);
var stock = new NestPlateStock("sheet", new Size(20, 20), null, 0, default, 1,
new[] { new ClampZone("tiny", zone) });
var job = new NestJob(new[] { new NestJobPart("p", PartGeometrySnapshot.FromProgram(program), 1, 0,
RotationPolicy.Fixed(0)) }, new[] { stock });
NestJobValidator.Validate(job);
var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
new[] { new NestJobPlateResult(0, stock, new[] { new NestJobPlacement("p", 0, 0, 0, 0) }) },
Array.Empty<PartFulfillment>(), Array.Empty<StockUsage>());
Assert.Equal(overlaps, ClampViolations(job, result).Count > 0);
}
[Theory]
[MemberData(nameof(RealMaterialRows))]
public void InteractiveKeepOutTestReadsRealMaterial(string material, Box zone, bool overlaps)
{
var part = new Part(new Drawing(material, MaterialProgram(material)));
Assert.Equal(overlaps, InteractiveClamps.MaterialInKeepOut(part, zone));
}
// Delta review 2: a concave (notch) arc must not lose material either. The zone's corner
// sits 6e-7 into the material beside a semicircular notch, at a flattening vertex.
internal static (Program Program, Box Zone) NotchCornerFixture()
{
var program = new Program();
program.MoveTo(0, 0);
program.LineTo(20, 0);
program.LineTo(20, 20);
program.LineTo(12, 20);
program.ArcTo(8, 20, 10, 20, RotationType.CW);
program.LineTo(0, 20);
program.LineTo(0, 0);
var arc = new ShapeProfile(OpenNest.Converters.ConvertProgram.ToGeometry(program))
.Perimeter.Entities.OfType<Arc>().Single();
var native = arc.ToPoints(arc.SegmentsForTolerance(Tiny / 4)).First(v => v.X > 11.9 && v.Y < 19.5);
var center = new Vector(10, 20);
var outward = center + (native - center) / 2 * (2 + 6e-7);
return (program, new Box(outward.X - Tiny, outward.Y, Tiny, Tiny));
}
[Fact]
public void ConcaveArcMaterialIsNotLostBesideATinyClamp()
{
var (program, zone) = NotchCornerFixture();
var stock = new NestPlateStock("sheet", new Size(25, 25), null, 0, default, 1,
new[] { new ClampZone("tiny", zone) });
var job = new NestJob(new[] { new NestJobPart("p", PartGeometrySnapshot.FromProgram(program), 1, 0,
RotationPolicy.Fixed(0)) }, new[] { stock });
NestJobValidator.Validate(job);
var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
new[] { new NestJobPlateResult(0, stock, new[] { new NestJobPlacement("p", 0, 0, 0, 0) }) },
Array.Empty<PartFulfillment>(), Array.Empty<StockUsage>());
Assert.NotEmpty(ClampViolations(job, result));
Assert.True(InteractiveClamps.MaterialInKeepOut(new Part(new Drawing("notch", program)), zone));
}
}