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; /// /// 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. /// 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 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(), Array.Empty()); 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(), Array.Empty()); 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 { 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 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(), Array.Empty()); 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().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(), Array.Empty()); Assert.NotEmpty(ClampViolations(job, result)); Assert.True(InteractiveClamps.MaterialInKeepOut(new Part(new Drawing("notch", program)), zone)); } }