From f2cc471a33025bffa2723b89ed557b23cb5beb1c Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 13 Oct 2026 18:06:21 -0400 Subject: [PATCH] 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) --- .../Diagnostics/PlateOverlapAnalyzer.cs | 7 +-- OpenNest.Core/Geometry/KeepOutOverlap.cs | 46 +++++++++++-------- .../Jobs/TinyClampZoneTests.cs | 36 +++++++++++++++ .../PlateOverlapAnalyzerClampZoneTests.cs | 26 +++++++++++ 4 files changed, 94 insertions(+), 21 deletions(-) diff --git a/OpenNest.Core/Diagnostics/PlateOverlapAnalyzer.cs b/OpenNest.Core/Diagnostics/PlateOverlapAnalyzer.cs index 7434f009..7accabc0 100644 --- a/OpenNest.Core/Diagnostics/PlateOverlapAnalyzer.cs +++ b/OpenNest.Core/Diagnostics/PlateOverlapAnalyzer.cs @@ -286,9 +286,10 @@ public static class PlateOverlapAnalyzer var profile = new ShapeProfile(part.Input.Source.Entities.Select(e => e.Clone()).ToList()); if (profile.Perimeter == null) throw new ArgumentException("Drawing has no closed material contour."); - var (outer, holes) = KeepOutOverlap.ConservativePolygons(profile, zone); - var fine = new OverlapMaterial(outer, holes).Transform(part.Input.Rotation, part.Input.Location); - if (KeepOutOverlap.MaterialEnters(fine.Outer, fine.Holes, zone)) + if (KeepOutOverlap.ConservativePolygons(profile, zone) + .Select(polygons => new OverlapMaterial(polygons.Perimeter, polygons.Holes) + .Transform(part.Input.Rotation, part.Input.Location)) + .Any(fine => KeepOutOverlap.MaterialEnters(fine.Outer, fine.Holes, zone))) findings.Add(TinyClampFinding(part, clamp)); continue; } diff --git a/OpenNest.Core/Geometry/KeepOutOverlap.cs b/OpenNest.Core/Geometry/KeepOutOverlap.cs index 8d3caa6a..32644360 100644 --- a/OpenNest.Core/Geometry/KeepOutOverlap.cs +++ b/OpenNest.Core/Geometry/KeepOutOverlap.cs @@ -27,33 +27,43 @@ public static class KeepOutOverlap System.Math.Min(ToleranceFreeBelow, System.Math.Min(keepOut.Length, keepOut.Width) / 4); /// - /// A part's native contours flattened for a tiny keep-out, never through a grid-rounding - /// offset: lines exact, arcs at , perimeter arcs - /// circumscribed and cutout arcs inscribed, so the polygons hold all real material and - /// any real overlap is found. The price is a possible report within that tolerance of - /// an arc, never a false clear. + /// A part's native contours flattened two ways for a tiny keep-out, never through a + /// grid-rounding offset: lines exact, every arc (perimeter and cutouts) inscribed in the + /// first and circumscribed in the second, at . Along a + /// convex arc the circumscribed version holds all real material, along a concave arc + /// (a notch, a round hole) the inscribed one does, so together they hold all of it and + /// testing both finds any real overlap. The price is a possible report within that + /// tolerance of an arc, never a false clear. /// - public static (Polygon Perimeter, List Holes) ConservativePolygons(ShapeProfile profile, Box keepOut) + public static IEnumerable<(Polygon Perimeter, List Holes)> ConservativePolygons( + ShapeProfile profile, Box keepOut) { var tolerance = FlatteningTolerance(keepOut); - var perimeter = profile.Perimeter.ToPolygonWithTolerance(tolerance, circumscribe: true); - var holes = new List(profile.Cutouts.Count); - foreach (var cutout in profile.Cutouts) - holes.Add(cutout.ToPolygonWithTolerance(tolerance)); - return (perimeter, holes); + foreach (var circumscribe in new[] { false, true }) + { + var perimeter = profile.Perimeter.ToPolygonWithTolerance(tolerance, circumscribe); + var holes = new List(profile.Cutouts.Count); + foreach (var cutout in profile.Cutouts) + holes.Add(cutout.ToPolygonWithTolerance(tolerance, circumscribe)); + yield return (perimeter, holes); + } } /// - /// on , translated by - /// . + /// True when either of the , translated by + /// , enters . /// public static bool ProfileEnters(ShapeProfile profile, Vector offset, Box keepOut) { - var (perimeter, holes) = ConservativePolygons(profile, keepOut); - perimeter.Offset(offset); - foreach (var hole in holes) - hole.Offset(offset); - return MaterialEnters(perimeter, holes, keepOut); + foreach (var (perimeter, holes) in ConservativePolygons(profile, keepOut)) + { + perimeter.Offset(offset); + foreach (var hole in holes) + hole.Offset(offset); + if (MaterialEnters(perimeter, holes, keepOut)) + return true; + } + return false; } /// diff --git a/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs b/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs index 7a5920a3..6fdb3903 100644 --- a/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs +++ b/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs @@ -258,4 +258,40 @@ public class TinyClampZoneTests 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)); + } } diff --git a/OpenNest.Tests/Diagnostics/PlateOverlapAnalyzerClampZoneTests.cs b/OpenNest.Tests/Diagnostics/PlateOverlapAnalyzerClampZoneTests.cs index efc1396c..fab500f5 100644 --- a/OpenNest.Tests/Diagnostics/PlateOverlapAnalyzerClampZoneTests.cs +++ b/OpenNest.Tests/Diagnostics/PlateOverlapAnalyzerClampZoneTests.cs @@ -256,6 +256,32 @@ public class PlateOverlapAnalyzerClampZoneTests Assert.Equal(overlaps, report.ClampFindings.Count > 0); } + // Delta review 2: material beside a concave (notch) arc is not lost for a tiny clamp. + [Fact] + public void TinyClampBesideAConcaveArcIsReported() + { + 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(1e-5 / 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); + var zone = new ClampZone("tiny", new Box(outward.X - 1e-5, outward.Y, 1e-5, 1e-5)); + + var report = PlateOverlapAnalyzer.Analyze(PlateOverlapAnalyzer.Capture( + new List { new(new Drawing("notch", program), new Vector(0, 0)) }, new[] { zone })); + + Assert.True(report.IsComplete, string.Join("; ", report.Issues.Select(issue => issue.Message))); + Assert.NotEmpty(report.ClampFindings); + } + private static string Describe(PlateOverlapReport report) => string.Join(";", report.Pairs.Select(p => $"{p.PartAId}/{p.PartBId}:{p.Area:R}")) + "|"