mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-15 05:41:54 -04:00
fix(clamps): judge tiny clamps against the part's real material
The tolerance-free rectangle test was fed approximated material: the layout check passed it the 1e-4-grid validation outline (a strip of real material that wholly contains a 1e-5 clamp could vanish), and interactive fill and the overlap check passed 0.001 inscribed arc chords (a clamp inside a disk but outside its chords read as clear). All three now flatten the native contours at a quarter of the clamp's smaller side, lines kept exact, so the arc-chord sliver can never hold the clamp. Ordinary clamps are unchanged. PM 407f5eb7-329f-41e4-947e-64ad52aa4034
This commit is contained in:
1 parent
e38980e54b
commit
6908e00969
7 files changed
+161
-11
No files matched your search
@@ -10,7 +10,13 @@ namespace OpenNest.Diagnostics;
|
||||
/// <summary>Request-local, validated single-outer material. Never exposed to report consumers.</summary>
|
||||
internal sealed record OverlapMaterial(Polygon Outer, List<Polygon> Holes)
|
||||
{
|
||||
internal static OverlapMaterial Read(List<Entity> entities, CancellationToken cancellationToken)
|
||||
internal static OverlapMaterial Read(List<Entity> entities, CancellationToken cancellationToken) =>
|
||||
Read(entities, cancellationToken, PlateOverlapAnalyzer.ChordTolerance);
|
||||
|
||||
/// <summary>As <see cref="Read(List{Entity}, CancellationToken)"/> with a caller-chosen
|
||||
/// chord tolerance (a finer one for keep-outs smaller than the default).</summary>
|
||||
internal static OverlapMaterial Read(List<Entity> entities, CancellationToken cancellationToken,
|
||||
double chordTolerance)
|
||||
{
|
||||
// ShapeBuilder can reverse entities while chaining. Own a fresh copy for each analysis.
|
||||
var shapes = ShapeBuilder.GetShapes(entities.Select(entity => entity.Clone()));
|
||||
@@ -21,7 +27,7 @@ internal sealed record OverlapMaterial(Polygon Outer, List<Polygon> Holes)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ValidateChain(shape);
|
||||
var polygon = shape.ToPolygonWithTolerance(PlateOverlapAnalyzer.ChordTolerance);
|
||||
var polygon = shape.ToPolygonWithTolerance(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())
|
||||
|
||||
@@ -280,7 +280,12 @@ public static class PlateOverlapAnalyzer
|
||||
// (negligibly small) clamp rectangle as the overlap fragment.
|
||||
if (KeepOutOverlap.NeedsToleranceFreeTest(zone))
|
||||
{
|
||||
if (KeepOutOverlap.MaterialEnters(part.Material.Outer, part.Material.Holes, 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);
|
||||
if (KeepOutOverlap.MaterialEnters(fine.Outer, fine.Holes, zone))
|
||||
findings.Add(TinyClampFinding(part, clamp));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,35 @@ public static class KeepOutOverlap
|
||||
public static bool NeedsToleranceFreeTest(Box keepOut) =>
|
||||
keepOut.Length < ToleranceFreeBelow || keepOut.Width < ToleranceFreeBelow;
|
||||
|
||||
/// <summary>
|
||||
/// Chord tolerance for flattening real part material against <paramref name="keepOut"/>:
|
||||
/// a quarter of its smaller side (at most <see cref="ToleranceFreeBelow"/>), 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.
|
||||
/// </summary>
|
||||
public static double FlatteningTolerance(Box keepOut) =>
|
||||
System.Math.Min(ToleranceFreeBelow, System.Math.Min(keepOut.Length, keepOut.Width) / 4);
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="MaterialEnters"/> for a part's native contours (lines kept exactly, arcs
|
||||
/// flattened at <see cref="FlatteningTolerance"/>), translated by <paramref name="offset"/>.
|
||||
/// Never fed through a grid-rounding offset or a coarse flattening.
|
||||
/// </summary>
|
||||
public static bool ProfileEnters(ShapeProfile profile, Vector offset, Box keepOut)
|
||||
{
|
||||
var tolerance = FlatteningTolerance(keepOut);
|
||||
var perimeter = profile.Perimeter.ToPolygonWithTolerance(tolerance);
|
||||
perimeter.Offset(offset);
|
||||
var holes = new List<Polygon>(profile.Cutouts.Count);
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
{
|
||||
var hole = cutout.ToPolygonWithTolerance(tolerance);
|
||||
hole.Offset(offset);
|
||||
holes.Add(hole);
|
||||
}
|
||||
return MaterialEnters(perimeter, holes, keepOut);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the material (perimeter minus holes) shares positive area with the open
|
||||
/// rectangle: some boundary edge runs through its interior, or its interior lies wholly
|
||||
|
||||
@@ -186,4 +186,74 @@ public class TinyClampZoneTests
|
||||
|
||||
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<string, Box, bool> 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
|
||||
};
|
||||
|
||||
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<PartFulfillment>(), Array.Empty<StockUsage>());
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -100,8 +100,7 @@ internal static class InteractiveClamps
|
||||
// A keep-out below the polygon tolerance collapses in the tolerant test and would
|
||||
// read as clear (PM b0062c73); judge it tolerance-free instead.
|
||||
if (KeepOutOverlap.NeedsToleranceFreeTest(keepOut))
|
||||
return KeepOutOverlap.MaterialEnters(partProfile.Perimeter.ToPolygonWithTolerance(0.001),
|
||||
HolePolygons(partProfile), keepOut);
|
||||
return KeepOutOverlap.ProfileEnters(partProfile, new Vector(0, 0), keepOut);
|
||||
var zoneProfile = new ShapeProfile(RectangleEntities(keepOut));
|
||||
if (zoneProfile.Perimeter == null)
|
||||
return true;
|
||||
|
||||
@@ -186,8 +186,7 @@ public static class NestLayoutCheck
|
||||
if (!BoxesTouch(keepOut, MaterialBounds(part)))
|
||||
continue;
|
||||
|
||||
var material = Outline(part, 0);
|
||||
if (material == null || !OverlapsKeepOut(material, keepOutOutline, keepOut))
|
||||
if (!OverlapsKeepOut(part, keepOutOutline, keepOut))
|
||||
continue;
|
||||
|
||||
result.Add(
|
||||
@@ -218,13 +217,21 @@ public static class NestLayoutCheck
|
||||
/// <summary>
|
||||
/// Material against a keep-out rectangle. A rectangle below the outline tolerance, or one
|
||||
/// the triangulated test cannot prepare, is judged by the tolerance-free rectangle test
|
||||
/// instead: the tolerant path would call real overlap clear.
|
||||
/// on the part's native contours (not the grid-rounded validation outline): the
|
||||
/// tolerant path would call real overlap clear. A part without material is skipped.
|
||||
/// </summary>
|
||||
private static bool OverlapsKeepOut(PartOutline material, PartOutline keepOutOutline, Box keepOut)
|
||||
private static bool OverlapsKeepOut(Part part, PartOutline keepOutOutline, Box keepOut)
|
||||
{
|
||||
if (KeepOutOverlap.NeedsToleranceFreeTest(keepOut) || keepOutOutline.Triangles == null)
|
||||
return KeepOutOverlap.MaterialEnters(material.Perimeter, material.Holes, keepOut);
|
||||
return Overlaps(material, keepOutOutline);
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ToList();
|
||||
var profile = entities.Count == 0 ? null : new ShapeProfile(entities);
|
||||
return profile?.Perimeter != null && KeepOutOverlap.ProfileEnters(profile, part.Location, keepOut);
|
||||
}
|
||||
var material = Outline(part, 0);
|
||||
return material != null && Overlaps(material, keepOutOutline);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -214,6 +214,40 @@ public class PlateOverlapAnalyzerClampZoneTests
|
||||
Assert.Equal(overlaps, report.ClampFindings.Any(f => f.PartBName.Contains("tiny")));
|
||||
}
|
||||
|
||||
// PM 407f5eb7: real material, not the 0.001 arc flattening, decides a tiny clamp.
|
||||
[Theory]
|
||||
[InlineData("rect", 8.00002, 0.5, true)]
|
||||
[InlineData("rect", 8.00005, 0.5, false)]
|
||||
[InlineData("disk", 1.9998, 0, true)]
|
||||
[InlineData("disk", 2.0002, 0, false)]
|
||||
public void TinyClampIsJudgedAgainstRealMaterial(string material, double a, double b, bool overlaps)
|
||||
{
|
||||
Program program;
|
||||
Box zone;
|
||||
if (material == "rect")
|
||||
{
|
||||
program = Rect(8.00004, 2);
|
||||
zone = new Box(a, b, 1e-5, 1e-5);
|
||||
}
|
||||
else
|
||||
{
|
||||
program = new Program();
|
||||
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);
|
||||
}
|
||||
var parts = new List<Part> { new(new Drawing(material, program), new Vector(0, 0)) };
|
||||
|
||||
var report = PlateOverlapAnalyzer.Analyze(
|
||||
PlateOverlapAnalyzer.Capture(parts, new[] { new ClampZone("tiny", zone) }));
|
||||
|
||||
Assert.True(report.IsComplete, string.Join("; ", report.Issues.Select(issue => issue.Message)));
|
||||
Assert.Equal(overlaps, report.ClampFindings.Count > 0);
|
||||
}
|
||||
|
||||
private static string Describe(PlateOverlapReport report) =>
|
||||
string.Join(";", report.Pairs.Select(p => $"{p.PartAId}/{p.PartBId}:{p.Area:R}"))
|
||||
+ "|"
|
||||
|
||||
Reference in new issue
Block a user