Files
OpenNest/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs
T
aj f2cc471a33 fix(clamps): test tiny clamps against both arc flattenings
Circumscribing every arc holds all material along convex arcs but drops a
sliver along concave ones (a notch), so a tiny clamp 6e-7 into the material
beside a notch read as clear. Tiny-clamp checks now test the part flattened
both ways (all arcs inscribed, all arcs circumscribed): along any arc one of
the two holds the real material, so together they find every real overlap.

PM 407f5eb7-329f-41e4-947e-64ad52aa4034 (delta review s6-delta2-review.md)
2026-10-13 18:15:50 -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 centred 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 centre = new Vector(10, 20);
var outward = centre + (native - centre) / 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));
}
}