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}"))
+ "|"