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)
This commit is contained in:
aj committed 2026-10-13 18:15:50 -04:00
1 parent 07cba5719f
commit f2cc471a33
4 files changed
+94 -21

No files matched your search

@@ -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;
}
+28 -18
View File
@@ -27,33 +27,43 @@ public static class KeepOutOverlap
System.Math.Min(ToleranceFreeBelow, System.Math.Min(keepOut.Length, keepOut.Width) / 4);
/// <summary>
/// A part's native contours flattened for a tiny keep-out, never through a grid-rounding
/// offset: lines exact, arcs at <see cref="FlatteningTolerance"/>, 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 <see cref="FlatteningTolerance"/>. 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.
/// </summary>
public static (Polygon Perimeter, List<Polygon> Holes) ConservativePolygons(ShapeProfile profile, Box keepOut)
public static IEnumerable<(Polygon Perimeter, List<Polygon> Holes)> ConservativePolygons(
ShapeProfile profile, Box keepOut)
{
var tolerance = FlatteningTolerance(keepOut);
var perimeter = profile.Perimeter.ToPolygonWithTolerance(tolerance, circumscribe: true);
var holes = new List<Polygon>(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<Polygon>(profile.Cutouts.Count);
foreach (var cutout in profile.Cutouts)
holes.Add(cutout.ToPolygonWithTolerance(tolerance, circumscribe));
yield return (perimeter, holes);
}
}
/// <summary>
/// <see cref="MaterialEnters"/> on <see cref="ConservativePolygons"/>, translated by
/// <paramref name="offset"/>.
/// True when either of the <see cref="ConservativePolygons"/>, translated by
/// <paramref name="offset"/>, enters <paramref name="keepOut"/>.
/// </summary>
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;
}
/// <summary>
@@ -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<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));
}
}
@@ -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<Arc>().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<Part> { 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}"))
+ "|"