diff --git a/OpenNest.Core/Geometry/KeepOutOverlap.cs b/OpenNest.Core/Geometry/KeepOutOverlap.cs
new file mode 100644
index 0000000..54ecce8
--- /dev/null
+++ b/OpenNest.Core/Geometry/KeepOutOverlap.cs
@@ -0,0 +1,114 @@
+using System.Collections.Generic;
+
+namespace OpenNest.Geometry;
+
+///
+/// Tolerance-free test of material against an axis-aligned keep-out rectangle, for
+/// rectangles too small for the tolerance-based overlap machinery (validation offsets,
+/// Clipper grids, triangulation and vector equality work at 1e-3 to 1e-5 scale and collapse
+/// such a rectangle, PM 6859db53). Positive-area overlap only: flush contact is clear, as
+/// everywhere else.
+///
+public static class KeepOutOverlap
+{
+ /// The scale below which tolerance-based overlap tests are not trusted for a
+ /// keep-out rectangle (the validation outline chord tolerance).
+ public const double ToleranceFreeBelow = 0.001;
+
+ /// A keep-out narrower than on either axis.
+ public static bool NeedsToleranceFreeTest(Box keepOut) =>
+ keepOut.Length < ToleranceFreeBelow || keepOut.Width < ToleranceFreeBelow;
+
+ ///
+ /// 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
+ /// inside the material (tested at its centre).
+ ///
+ public static bool MaterialEnters(Polygon perimeter, IReadOnlyList holes, Box box)
+ {
+ if (RingEnters(perimeter, box))
+ return true;
+ if (holes != null)
+ foreach (var hole in holes)
+ if (RingEnters(hole, box))
+ return true;
+
+ var centre = new Vector((box.Left + box.Right) / 2, (box.Bottom + box.Top) / 2);
+ if (!Contains(perimeter, centre))
+ return false;
+ if (holes != null)
+ foreach (var hole in holes)
+ if (Contains(hole, centre))
+ return false;
+ return true;
+ }
+
+ private static bool RingEnters(Polygon ring, Box box)
+ {
+ var vertices = ring.Vertices;
+ var count = vertices.Count;
+ for (var i = 0; i < count; i++)
+ {
+ var next = vertices[(i + 1) % count];
+ if (SegmentEntersOpenBox(vertices[i], next, box))
+ return true;
+ }
+ return false;
+ }
+
+ /// Liang-Barsky clip against the open rectangle; a positive-length part of the
+ /// segment must lie strictly inside it.
+ private static bool SegmentEntersOpenBox(Vector p, Vector q, Box box)
+ {
+ var dx = q.X - p.X;
+ var dy = q.Y - p.Y;
+ var t0 = 0.0;
+ var t1 = 1.0;
+ return Clip(-dx, p.X - box.Left, ref t0, ref t1)
+ && Clip(dx, box.Right - p.X, ref t0, ref t1)
+ && Clip(-dy, p.Y - box.Bottom, ref t0, ref t1)
+ && Clip(dy, box.Top - p.Y, ref t0, ref t1)
+ && t0 < t1;
+ }
+
+ /// Restricts [t0, t1] to the parameters where denominator * t < numerator.
+ private static bool Clip(double denominator, double numerator, ref double t0, ref double t1)
+ {
+ if (denominator == 0)
+ return numerator > 0;
+ var r = numerator / denominator;
+ if (denominator < 0)
+ {
+ if (r >= t1)
+ return false;
+ if (r > t0)
+ t0 = r;
+ }
+ else
+ {
+ if (r <= t0)
+ return false;
+ if (r < t1)
+ t1 = r;
+ }
+ return true;
+ }
+
+ /// Even-odd ray cast; exact for a point that is not on the ring (callers only
+ /// ask after ruling out any edge through the rectangle containing the point).
+ private static bool Contains(Polygon ring, Vector point)
+ {
+ var inside = false;
+ var vertices = ring.Vertices;
+ var count = vertices.Count;
+ for (int i = 0, j = count - 1; i < count; j = i++)
+ {
+ var a = vertices[i];
+ var b = vertices[j];
+ if ((a.Y > point.Y) != (b.Y > point.Y)
+ && point.X < (b.X - a.X) * (point.Y - a.Y) / (b.Y - a.Y) + a.X)
+ inside = !inside;
+ }
+ return inside;
+ }
+}
diff --git a/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs b/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs
new file mode 100644
index 0000000..681ead2
--- /dev/null
+++ b/OpenNest.Engine.Tests/Jobs/TinyClampZoneTests.cs
@@ -0,0 +1,134 @@
+using OpenNest.CNC;
+using OpenNest.Engine.Jobs;
+using OpenNest.Engine.NestingEngines.Default;
+using OpenNest.Engine.NestingEngines.Rectangles;
+using OpenNest.Geometry;
+
+namespace OpenNest.Engine.Tests.Jobs;
+
+///
+/// C16 (PM 6859db53): a clamp zone the job validator accepts may be far smaller than the
+/// validation outline tolerance (here 1e-5 square). Every keep-out consumer must check it
+/// without throwing, flag a placement whose material overlaps it (plus the D6 clearance),
+/// and keep a placement that only touches or clears it clear. Tolerances are unchanged (C5):
+/// the expected verdicts below are exact rectangle arithmetic, not the checker's output.
+///
+public class TinyClampZoneTests
+{
+ private const double Tiny = 1e-5;
+
+ private static readonly ClampZone TinyZone = new("tiny", new Box(8, 0, Tiny, Tiny));
+
+ private static Program SquareProgram()
+ {
+ var program = new Program();
+ program.Codes.Add(new RapidMove(0, 0));
+ program.Codes.Add(new LinearMove(2, 0));
+ program.Codes.Add(new LinearMove(2, 2));
+ program.Codes.Add(new LinearMove(0, 2));
+ program.Codes.Add(new LinearMove(0, 0));
+ return program;
+ }
+
+ private static NestJobPart Square() =>
+ new("square", PartGeometrySnapshot.FromProgram(SquareProgram()), 1, 0, RotationPolicy.Fixed(0));
+
+ private static NestPlateStock Stock(double bottom) =>
+ new("sheet", new Size(10, 12), null, 0, new Spacing(0, bottom, 0, 0), 1,
+ new[] { TinyZone });
+
+ private static NestJob Job(double bottom) => new(new[] { Square() }, new[] { Stock(bottom) });
+
+ private static IReadOnlyList ClampViolations(NestJob job, NestJobResult result) =>
+ NestLayoutCheck.Violations(job, result).Where(v => v.Contains("clamp zone")).ToList();
+
+ [Fact]
+ public void RectanglesResultOnATinyClampIsCheckedWithoutThrowing()
+ {
+ // The PM probe: Rectangles places the square flush against the tiny clamp.
+ var job = Job(0);
+ NestJobValidator.Validate(job);
+ var result = new RectanglesNestingEngine().Solve(job);
+ Assert.Equal(NestJobStatus.Complete, result.Status);
+
+ var violations = ClampViolations(job, result);
+
+ // Independent oracle: the square's footprint shares no positive area with the zone.
+ var pose = Assert.Single(result.Plates.SelectMany(p => p.Placements));
+ var overlapX = System.Math.Min(pose.X + 2, 8 + Tiny) - System.Math.Max(pose.X, 8);
+ var overlapY = System.Math.Min(pose.Y + 2, Tiny) - System.Math.Max(pose.Y, 0);
+ var expectOverlap = overlapX > 0 && overlapY > 0;
+ Assert.Equal(expectOverlap, violations.Count > 0);
+ }
+
+ [Fact]
+ public void DefaultSolvesATinyClampJobToAValidResult()
+ {
+ var job = Job(0);
+ var result = new DefaultNestingEngine().Solve(job);
+ Assert.Equal(NestJobStatus.Complete, result.Status);
+ Assert.Empty(NestLayoutCheck.Violations(job, result));
+ }
+
+ // (bottom edge spacing, x, y, material overlaps the zone inflated by the clearance).
+ // The keep-out is [8 - c, 8 + 1e-5 + c] x [-c, 1e-5 + c] with c = bottom - 0.0005
+ // (the shared spacing slack); the 2x2 square spans [x, x+2] x [y, y+2].
+ [Theory]
+ [InlineData(0.0, 7.5, 0.0, true)] // covers the zone
+ [InlineData(0.0, 6.0, 0.0, false)] // right edge flush on the zone's left side
+ [InlineData(0.0, 8.00001, 0.0, false)] // left edge flush on the zone's right side
+ [InlineData(0.0, 8.0, Tiny, false)] // bottom edge flush on the zone's top
+ [InlineData(0.0, 3.0, 0.0, false)] // well clear
+ [InlineData(0.25, 7.5, 0.1, true)] // over the zone, inside the clearance band
+ [InlineData(0.25, 8.0, 0.24, true)] // above the zone but inside the clearance
+ [InlineData(0.25, 8.25001, 0.1, false)] // exactly at the clearance to the right
+ [InlineData(0.25, 7.0, 0.25001, false)] // exactly at the clearance above
+ [InlineData(0.25, 3.0, 0.25, false)] // well clear
+ public void LayoutCheckJudgesPlacementsAgainstATinyClamp(double bottom, double x, double y,
+ bool overlaps)
+ {
+ var job = Job(bottom);
+ NestJobValidator.Validate(job);
+ var stock = job.Plates[0];
+ var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
+ new[] { new NestJobPlateResult(0, stock, new[] { new NestJobPlacement("square", 0, x, y, 0) }) },
+ Array.Empty(), Array.Empty());
+
+ var violations = ClampViolations(job, result);
+
+ Assert.Equal(overlaps, violations.Count > 0);
+ }
+
+ private static Program FrameProgram()
+ {
+ // 4x4 square with a 2x2 hole in its middle.
+ var program = new Program();
+ program.Codes.Add(new RapidMove(0, 0));
+ program.Codes.Add(new LinearMove(4, 0));
+ program.Codes.Add(new LinearMove(4, 4));
+ program.Codes.Add(new LinearMove(0, 4));
+ program.Codes.Add(new LinearMove(0, 0));
+ program.Codes.Add(new RapidMove(1, 1));
+ program.Codes.Add(new LinearMove(1, 3));
+ program.Codes.Add(new LinearMove(3, 3));
+ program.Codes.Add(new LinearMove(3, 1));
+ program.Codes.Add(new LinearMove(1, 1));
+ return program;
+ }
+
+ // The tiny zone inside the frame's hole is clear; inside its material it is not.
+ [Theory]
+ [InlineData(6.0, -2.0, false)] // hole spans [7, 9] x [-1, 1] around the zone
+ [InlineData(6.0, 0.0, true)] // the frame's bottom bar covers the zone
+ public void LayoutCheckSeesHolesAroundATinyClamp(double x, double y, bool overlaps)
+ {
+ var frame = new NestJobPart("frame", PartGeometrySnapshot.FromProgram(FrameProgram()), 1, 0,
+ RotationPolicy.Fixed(0));
+ var job = new NestJob(new[] { frame }, new[] { Stock(0) });
+ var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
+ new[] { new NestJobPlateResult(0, job.Plates[0], new[] { new NestJobPlacement("frame", 0, x, y, 0) }) },
+ Array.Empty(), Array.Empty());
+
+ Assert.Equal(overlaps, ClampViolations(job, result).Count > 0);
+ }
+}
diff --git a/OpenNest.Engine/Jobs/NestLayoutCheck.cs b/OpenNest.Engine/Jobs/NestLayoutCheck.cs
index dbad50a..5bd6ddd 100644
--- a/OpenNest.Engine/Jobs/NestLayoutCheck.cs
+++ b/OpenNest.Engine/Jobs/NestLayoutCheck.cs
@@ -187,7 +187,7 @@ public static class NestLayoutCheck
continue;
var material = Outline(part, 0);
- if (material == null || !Overlaps(material, keepOutOutline))
+ if (material == null || !OverlapsKeepOut(material, keepOutOutline, keepOut))
continue;
result.Add(
@@ -198,17 +198,33 @@ public static class NestLayoutCheck
}
}
+ ///
+ /// The keep-out as an exact four-vertex ring. Routing a synthetic rectangle through the
+ /// tolerance-based validation offset collapses a zone smaller than that tolerance to
+ /// nothing (PM 6859db53), so the rectangle is never offset or flattened here.
+ ///
private static PartOutline RectangleOutline(Box box)
{
- var ring = new List
- {
- new Line(box.Left, box.Bottom, box.Right, box.Bottom),
- new Line(box.Right, box.Bottom, box.Right, box.Top),
- new Line(box.Right, box.Top, box.Left, box.Top),
- new Line(box.Left, box.Top, box.Left, box.Bottom),
- };
- var profile = new ShapeProfile(ring);
- return Outline(profile, new Vector(0, 0), 0);
+ var perimeter = new Polygon();
+ perimeter.Vertices.Add(new Vector(box.Left, box.Bottom));
+ perimeter.Vertices.Add(new Vector(box.Right, box.Bottom));
+ perimeter.Vertices.Add(new Vector(box.Right, box.Top));
+ perimeter.Vertices.Add(new Vector(box.Left, box.Top));
+ perimeter.Vertices.Add(new Vector(box.Left, box.Bottom));
+ perimeter.UpdateBounds();
+ return new PartOutline { Perimeter = perimeter, Holes = new List() };
+ }
+
+ ///
+ /// 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.
+ ///
+ private static bool OverlapsKeepOut(PartOutline material, PartOutline keepOutOutline, Box keepOut)
+ {
+ if (KeepOutOverlap.NeedsToleranceFreeTest(keepOut) || keepOutOutline.Triangles == null)
+ return KeepOutOverlap.MaterialEnters(material.Perimeter, material.Holes, keepOut);
+ return Overlaps(material, keepOutOutline);
}
///