fix(engine): allow 0.0005 spacing slack in layout validation

Layouts placed exactly at the part spacing can land ~1e-4 short once
rotated, rounded (e.g. PEP's 4-decimal exports) and snapped to the
Clipper grid, so both validators rejected layouts that were correct in
practice. NestTolerances.SpacingSlack (0.0005, far below anything a
cutting machine resolves) is now subtracted from the spacing by
NestLayoutCheck's inflation and NestJobPlacementValidator's edge-distance
check. The frozen LegacyNestValidator takes the same rule so the
equivalence tests keep comparing like with like.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-25 14:19:04 -04:00
co-authored by Claude Opus 5.5
parent d1f3907505
commit 1b862dc1a8
6 changed files with 47 additions and 10 deletions
@@ -66,7 +66,8 @@ internal static class NestJobPlacementValidator
continue;
if (Overlaps(shape, other))
throw new InvalidOperationException("Candidate placements overlap.");
if (stock.PartSpacing > 0 && Distance(shape, other) < stock.PartSpacing - Epsilon)
if (stock.PartSpacing > 0
&& Distance(shape, other) < stock.PartSpacing - NestTolerances.SpacingSlack)
throw new InvalidOperationException(
"Candidate placements violate required part spacing."
);
+12 -5
View File
@@ -52,7 +52,8 @@ public static class NestLayoutCheck
(al, bl) = (bl, al);
(ar, br) = (br, ar);
}
var inflated = spacing > Tolerance.Epsilon ? Outline(ap, al, spacing) : ar;
var inflateBy = InflationFor(spacing);
var inflated = inflateBy > Tolerance.Epsilon ? Outline(ap, al, inflateBy) : ar;
return !BoxesTouch(inflated.Perimeter.BoundingBox, br.Perimeter.BoundingBox)
|| !Overlaps(inflated, br);
}
@@ -289,11 +290,12 @@ public static class NestLayoutCheck
{
var raw = new PartOutline[parts.Count];
var inflated = new PartOutline[parts.Count];
var inflateBy = InflationFor(spacing);
for (var i = 0; i < parts.Count; i++)
{
raw[i] = Outline(parts[i], 0);
inflated[i] = spacing > Tolerance.Epsilon ? Outline(parts[i], spacing) : raw[i];
inflated[i] = inflateBy > Tolerance.Epsilon ? Outline(parts[i], inflateBy) : raw[i];
}
var order = Enumerable
@@ -339,6 +341,10 @@ public static class NestLayoutCheck
.GetBoundingBox()
.Translate(part.Location);
/// <summary>Inflation that enforces <paramref name="spacing"/> less the shared slack.</summary>
private static double InflationFor(double spacing) =>
System.Math.Max(0, spacing - NestTolerances.SpacingSlack);
private static bool BoxesTouch(Box a, Box b) =>
a.Left <= b.Right + Tolerance.Epsilon
&& b.Left <= a.Right + Tolerance.Epsilon
@@ -398,9 +404,10 @@ public static class NestLayoutCheck
/// that closes up under the offset is dropped, which treats it as solid:
/// conservative, since it has no room for another part at the required
/// spacing anyway. Arcs are flattened conservatively (perimeter arcs
/// circumscribed, cutout arcs inscribed) but nothing is padded, so a layout
/// exactly at the spacing passes; the only leniency is the round-join chord
/// error at convex corners (OutlineTolerance / 10).
/// circumscribed, cutout arcs inscribed) but nothing is padded. Callers inflate
/// by the spacing less NestTolerances.SpacingSlack, so a layout exactly at the
/// spacing passes despite rounding; the only other leniency is the round-join
/// chord error at convex corners (OutlineTolerance / 10).
/// part.Program is already rotated; only a Location offset is needed.
/// </summary>
private static PartOutline Outline(Part part, double inflateBy)
+6
View File
@@ -12,6 +12,12 @@ public static class NestTolerances
/// perimeter arcs and inscribes cutouts; the placement validator uses inscribed arcs.</summary>
public const double ValidationOutline = 0.001;
/// <summary>How far under the part spacing a gap may fall and still pass both validators.
/// Layouts placed exactly at the spacing land up to ~1e-4 short once rotated, rounded
/// coordinates (e.g. PEP's 4-decimal exports) meet the Clipper grid; 0.0005 covers that
/// with margin and is far below anything a cutting machine can resolve.</summary>
public const double SpacingSlack = 0.0005;
/// <summary>Clipper decimal precision (a 1e-4 coordinate grid).</summary>
public const int ClipperPrecision = ClipperBridge.Precision;