diff --git a/OpenNest.Core/Diagnostics/OverlapMaterial.cs b/OpenNest.Core/Diagnostics/OverlapMaterial.cs index 94651f2..37f6615 100644 --- a/OpenNest.Core/Diagnostics/OverlapMaterial.cs +++ b/OpenNest.Core/Diagnostics/OverlapMaterial.cs @@ -10,13 +10,7 @@ namespace OpenNest.Diagnostics; /// Request-local, validated single-outer material. Never exposed to report consumers. internal sealed record OverlapMaterial(Polygon Outer, List Holes) { - internal static OverlapMaterial Read(List entities, CancellationToken cancellationToken) => - Read(entities, cancellationToken, PlateOverlapAnalyzer.ChordTolerance); - - /// As with a caller-chosen - /// chord tolerance (a finer one for keep-outs smaller than the default). - internal static OverlapMaterial Read(List entities, CancellationToken cancellationToken, - double chordTolerance) + internal static OverlapMaterial Read(List entities, CancellationToken cancellationToken) { // ShapeBuilder can reverse entities while chaining. Own a fresh copy for each analysis. var shapes = ShapeBuilder.GetShapes(entities.Select(entity => entity.Clone())); @@ -27,7 +21,7 @@ internal sealed record OverlapMaterial(Polygon Outer, List Holes) { cancellationToken.ThrowIfCancellationRequested(); ValidateChain(shape); - var polygon = shape.ToPolygonWithTolerance(chordTolerance); + var polygon = shape.ToPolygonWithTolerance(PlateOverlapAnalyzer.ChordTolerance); // The analytic chain was validated above. Normalize its sampled seam (e.g. // sin(2*pi) is not exactly zero), rather than adding a spurious microscopic edge. if (polygon.IsClosed()) diff --git a/OpenNest.Core/Diagnostics/PlateOverlapAnalyzer.cs b/OpenNest.Core/Diagnostics/PlateOverlapAnalyzer.cs index 0ecbf76..7434f00 100644 --- a/OpenNest.Core/Diagnostics/PlateOverlapAnalyzer.cs +++ b/OpenNest.Core/Diagnostics/PlateOverlapAnalyzer.cs @@ -280,11 +280,14 @@ public static class PlateOverlapAnalyzer // (negligibly small) clamp rectangle as the overlap fragment. if (KeepOutOverlap.NeedsToleranceFreeTest(zone)) { - // Re-read the native contours flattened finely enough for this clamp: - // the shared 0.001 chords can hide material between an arc and its chord. - var fine = OverlapMaterial - .Read(part.Input.Source.Entities, cancellationToken, KeepOutOverlap.FlatteningTolerance(zone)) - .Transform(part.Input.Rotation, part.Input.Location); + // Re-read the native contours conservatively and finely enough for + // this clamp: the shared 0.001 chords can hide material between an + // arc and its chord. + 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)) findings.Add(TinyClampFinding(part, clamp)); continue; diff --git a/OpenNest.Core/Geometry/KeepOutOverlap.cs b/OpenNest.Core/Geometry/KeepOutOverlap.cs index d6e0c65..8d3caa6 100644 --- a/OpenNest.Core/Geometry/KeepOutOverlap.cs +++ b/OpenNest.Core/Geometry/KeepOutOverlap.cs @@ -21,30 +21,38 @@ public static class KeepOutOverlap /// /// Chord tolerance for flattening real part material against : - /// a quarter of its smaller side (at most ), so the - /// sliver between an arc and its chord can never hold the whole rectangle - any real - /// overlap then shows up as a polygon edge through it or its centre inside. + /// a quarter of its smaller side, at most . /// public static double FlatteningTolerance(Box keepOut) => System.Math.Min(ToleranceFreeBelow, System.Math.Min(keepOut.Length, keepOut.Width) / 4); /// - /// for a part's native contours (lines kept exactly, arcs - /// flattened at ), translated by . - /// Never fed through a grid-rounding offset or a coarse flattening. + /// 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. + /// + public static (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); + } + + /// + /// on , translated by + /// . /// public static bool ProfileEnters(ShapeProfile profile, Vector offset, Box keepOut) { - var tolerance = FlatteningTolerance(keepOut); - var perimeter = profile.Perimeter.ToPolygonWithTolerance(tolerance); + var (perimeter, holes) = ConservativePolygons(profile, keepOut); perimeter.Offset(offset); - var holes = new List(profile.Cutouts.Count); - foreach (var cutout in profile.Cutouts) - { - var hole = cutout.ToPolygonWithTolerance(tolerance); + foreach (var hole in holes) hole.Offset(offset); - holes.Add(hole); - } return MaterialEnters(perimeter, holes, keepOut); } diff --git a/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs b/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs index e444758..7a5920a 100644 --- a/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs +++ b/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs @@ -223,6 +223,8 @@ public class TinyClampZoneTests { "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 diff --git a/OpenNest.Tests/Diagnostics/PlateOverlapAnalyzerClampZoneTests.cs b/OpenNest.Tests/Diagnostics/PlateOverlapAnalyzerClampZoneTests.cs index e49e35b..efc1396 100644 --- a/OpenNest.Tests/Diagnostics/PlateOverlapAnalyzerClampZoneTests.cs +++ b/OpenNest.Tests/Diagnostics/PlateOverlapAnalyzerClampZoneTests.cs @@ -220,6 +220,7 @@ public class PlateOverlapAnalyzerClampZoneTests [InlineData("rect", 8.00005, 0.5, false)] [InlineData("disk", 1.9998, 0, true)] [InlineData("disk", 2.0002, 0, false)] + [InlineData("disk-box", 11.935615871667656, 10.503377293956166, true)] // arc cuts a corner public void TinyClampIsJudgedAgainstRealMaterial(string material, double a, double b, bool overlaps) { Program program; @@ -235,9 +236,16 @@ public class PlateOverlapAnalyzerClampZoneTests program.MoveTo(12, 10); program.ArcTo(8, 10, 10, 10, RotationType.CCW); program.ArcTo(12, 10, 10, 10, RotationType.CCW); - var cx = 10 + a * System.Math.Cos(0.02); - var cy = 10 + a * System.Math.Sin(0.02); - zone = new Box(cx - 0.000005, cy - 0.000005, 1e-5, 1e-5); + if (material == "disk-box") + { + zone = new Box(a, b, 1e-5, 1e-5); + } + else + { + var cx = 10 + a * System.Math.Cos(0.02); + var cy = 10 + a * System.Math.Sin(0.02); + zone = new Box(cx - 0.000005, cy - 0.000005, 1e-5, 1e-5); + } } var parts = new List { new(new Drawing(material, program), new Vector(0, 0)) };