refactor(gpt6astra): use host geometry, tolerances and layout checks

Gpt6Astra reverse-engineered the validator: hand-tuned paddings and a
copied check sequence (ValidationOverlap) to match its rounding. It now
reads parts with JobPartGeometry, takes clearance from NestTolerances,
checks candidates with NestLayoutCheck.Clears, and assembles results with
NestJobResultBuilder and NestJobCost; its tests use the shared kit. Its
contact search, beam search and extra Automatic angles are unchanged.

Synthetic benchmark (5 jobs, salvage 0.5): all valid, 2 sheets each,
cost 5574.07 -> 5470.07; time 1871 -> 2400 ms from the stricter shared
check on arc-heavy jobs.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-25 09:29:27 -04:00
co-authored by Codex Claude Opus 5.5
parent c691381f30
commit d0c6af783b
10 changed files with 120 additions and 217 deletions
+26 -48
View File
@@ -11,16 +11,17 @@ internal sealed record SheetTrial(int StockIndex, int[] Counts, List<PackedShape
/// <summary>Searches vertices of the available translation region and exact-fit contacts.</summary>
internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geometry, CancellationToken token)
{
private static readonly double GridUnit = M.Pow(10, -NestTolerances.ClipperPrecision);
private readonly Dictionary<(int, int, double, double, double, double, double), bool> validationCache = new();
private double validationOriginX;
private double validationOriginY;
internal SheetTrial Pack(int stockIndex, NestPlateStock stock, int[] committed, int[] flexibility, int mode)
{
validationOriginX = (stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length) + stock.EdgeSpacing.Left;
validationOriginY = (stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width) + stock.EdgeSpacing.Bottom;
var width = stock.Size.Length - stock.EdgeSpacing.Left - stock.EdgeSpacing.Right;
var height = stock.Size.Width - stock.EdgeSpacing.Bottom - stock.EdgeSpacing.Top;
validationOriginX = stock.WorkArea.Left;
validationOriginY = stock.WorkArea.Bottom;
var width = stock.WorkArea.Length;
var height = stock.WorkArea.Width;
var counts = (int[])committed.Clone();
var placed = new List<PackedShape>();
var spaces = new Dictionary<int, SearchSpace>();
@@ -112,10 +113,10 @@ internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geomet
// separately, then validate against material, not the outer envelope.
foreach (var hole in other.Variant.Material.Where(p => !Clipper.IsPositive(p)))
{
var l = hole.Min(p => p.x) + other.X + spacing + 0.0004;
var b = hole.Min(p => p.y) + other.Y + spacing + 0.0004;
var r = hole.Max(p => p.x) + other.X - spacing - moving.Width - 0.0004;
var t = hole.Max(p => p.y) + other.Y - spacing - moving.Height - 0.0004;
var l = hole.Min(p => p.x) + other.X + spacing + (4 * GridUnit);
var b = hole.Min(p => p.y) + other.Y + spacing + (4 * GridUnit);
var r = hole.Max(p => p.x) + other.X - spacing - moving.Width - (4 * GridUnit);
var t = hole.Max(p => p.y) + other.Y - spacing - moving.Height - (4 * GridUnit);
if (r < l || t < b) continue;
Add(l, b); Add(r, b); Add(l, t); Add(r, t); Add((l + r) / 2, (b + t) / 2);
// Box corners miss the useful interior of circular and rounded holes.
@@ -144,8 +145,8 @@ internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geomet
// Exact contacts can be invalid only after the host's four-decimal
// polygon rounding. Try nearby outward contacts without changing angle.
foreach (var (dx, dy) in new (double, double)[] {
(0.0003, 0), (0, 0.0003), (0.0003, 0.0003), (-0.0003, 0),
(0, -0.0003), (-0.0003, 0.0003), (0.0003, -0.0003), (-0.0003, -0.0003) })
((3 * GridUnit), 0), (0, (3 * GridUnit)), ((3 * GridUnit), (3 * GridUnit)), (-(3 * GridUnit), 0),
(0, -(3 * GridUnit)), (-(3 * GridUnit), (3 * GridUnit)), ((3 * GridUnit), -(3 * GridUnit)), (-(3 * GridUnit), -(3 * GridUnit)) })
{
var nudged = pose with { X = pose.X + dx, Y = pose.Y + dy };
if (nudged.X < 0 || nudged.Y < 0 || nudged.X > maxX || nudged.Y > maxY) continue;
@@ -202,10 +203,9 @@ internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geomet
private bool Valid(PackedShape candidate, List<PackedShape> placed, double spacing)
{
PathsD? material = null;
PathsD? validationMaterial = null;
foreach (var other in placed)
{
var gap = spacing + (candidate.Variant.Curved || other.Variant.Curved ? 0.003 : 0.0001);
var gap = spacing + (candidate.Variant.Curved || other.Variant.Curved ? NestTolerances.SafeClearanceMargin(NestTolerances.ValidationOutline) : NestTolerances.SafeClearanceMargin(0));
if (candidate.X >= other.X + other.Variant.Width + gap ||
other.X >= candidate.X + candidate.Variant.Width + gap ||
candidate.Y >= other.Y + other.Variant.Height + gap ||
@@ -223,46 +223,24 @@ internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geomet
var obstacle = GeometryPrecision.Translate(other.Variant.Halo(spacing), other.X, other.Y);
var overlap = Clipper.Intersect(material, obstacle, FillRule.NonZero, GeometryPrecision.Digits);
if (M.Abs(Clipper.Area(overlap)) > 1e-8) return false;
validationMaterial ??= GeometryPrecision.Translate(candidate.Variant.ValidationRegion(0), candidate.X, candidate.Y);
var validationObstacle = GeometryPrecision.Translate(other.Variant.ValidationRegion(spacing), other.X, other.Y);
if (M.Abs(Clipper.Area(Clipper.Intersect(validationMaterial, validationObstacle,
FillRule.NonZero, GeometryPrecision.Digits))) > 1e-8) return false;
if (spacing == 0 || candidate.Variant.Material.Count > 1 || other.Variant.Material.Count > 1)
var key = (candidate.Variant.Id, other.Variant.Id, spacing,
candidate.X + validationOriginX, candidate.Y + validationOriginY,
other.X + validationOriginX, other.Y + validationOriginY);
if (!validationCache.TryGetValue(key, out var collides))
{
var outerIntersection = Clipper.Intersect(
new PathsD(validationMaterial.Where(Clipper.IsPositive)),
new PathsD(validationObstacle.Where(Clipper.IsPositive)), FillRule.NonZero, GeometryPrecision.Digits);
if (spacing != 0 && M.Abs(Clipper.Area(outerIntersection)) <= 1e-8) continue;
var key = (candidate.Variant.Id, other.Variant.Id, spacing,
candidate.X + validationOriginX, candidate.Y + validationOriginY,
other.X + validationOriginX, other.Y + validationOriginY);
if (!validationCache.TryGetValue(key, out var collides))
{
collides = ValidationOverlap(
GeometryPrecision.Translate(validationMaterial, validationOriginX, validationOriginY),
GeometryPrecision.Translate(validationObstacle, validationOriginX, validationOriginY));
if (validationCache.Count >= 4096) validationCache.Clear();
validationCache[key] = collides;
}
if (collides) return false;
NestJobPlacement Pose(PackedShape p) => new("check", 0,
validationOriginX + p.X - p.Variant.OriginX,
validationOriginY + p.Y - p.Variant.OriginY, p.Variant.Angle);
// Equal-left ties follow commit order, just as the full-layout check does.
collides = !NestLayoutCheck.Clears(other.Variant.Geometry, Pose(other),
candidate.Variant.Geometry, Pose(candidate), spacing);
if (validationCache.Count >= 4096) validationCache.Clear();
validationCache[key] = collides;
}
if (collides) return false;
}
return true;
}
private static bool ValidationOverlap(PathsD a, PathsD b)
{
var holesA = a.Where(p => !Clipper.IsPositive(p)).Select(ClipperBridge.ToPolygon).ToList();
var holesB = b.Where(p => !Clipper.IsPositive(p)).Select(ClipperBridge.ToPolygon).ToList();
foreach (var outerA in a.Where(Clipper.IsPositive))
foreach (var outerB in b.Where(Clipper.IsPositive))
{
var pa = ClipperBridge.ToPolygon(outerA);
var pb = ClipperBridge.ToPolygon(outerB);
// The benchmark orders by world-space left bound before clipping.
if (pa.Left <= pb.Left ? Collision.HasOverlap(pa, pb, holesA, holesB) :
Collision.HasOverlap(pb, pa, holesB, holesA)) return true;
}
return false;
}
}