Files
OpenNest-Engines/OpenNest.Engine.Gpt6Astra/ContactGeometry.cs
T
d0c6af783b 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>
2026-09-25 09:29:27 -04:00

68 lines
3.2 KiB
C#

using Clipper2Lib;
using OpenNest.Geometry;
using OpenNest.Engine.Jobs;
using M = System.Math;
namespace OpenNest.Engine.Gpt6Astra;
/// <summary>Per-solve configuration-space cache, never a shared mutable geometry cache.</summary>
internal sealed class ContactGeometry
{
private readonly Dictionary<(int, int, double), PathsD> cache = new();
internal PathsD Forbidden(ShapeVariant stationary, ShapeVariant moving, double spacing, CancellationToken token)
{
var key = (stationary.Id, moving.Id, spacing);
if (cache.TryGetValue(key, out var value)) return value;
token.ThrowIfCancellationRequested();
PathsD paths;
if (stationary.BoxLike && moving.BoxLike)
{
// Exact axis-aligned rectangle contacts need four configuration-space
// vertices, not hundreds of round-offset samples. The square corner is
// conservative for diagonal clearance and leaves row/column fits exact.
var gap = spacing;
paths = new PathsD { new PathD {
new(-moving.Width - gap, -moving.Height - gap),
new(stationary.Width + gap, -moving.Height - gap),
new(stationary.Width + gap, stationary.Height + gap),
new(-moving.Width - gap, stationary.Height + gap) } };
if (cache.Count >= 8192) cache.Clear();
return cache[key] = paths;
}
if (stationary.Convex && moving.Convex)
{
var nfp = NoFitPolygon.ComputeConvex(stationary.Hull, moving.Hull);
paths = new PathsD { ClipperBridge.ToPath(nfp, positive: true) };
}
else
{
// Minkowski edge quads may enclose spurious interior voids. Filling all
// positive outer paths is conservative for solid perimeter nesting; real
// part holes are searched separately and checked against material regions.
var a = ToInteger(stationary.ContactOutline, false);
var b = ToInteger(moving.ContactOutline, true);
var sum = Clipper.MinkowskiSum(b, a, true);
paths = new PathsD(sum.Where(Clipper.IsPositive).Select(path => new PathD(
path.Select(p => new PointD(p.X / GeometryPrecision.Scale, p.Y / GeometryPrecision.Scale)))));
}
token.ThrowIfCancellationRequested();
var delta = spacing + stationary.ContactError + moving.ContactError
+ (stationary.Curved || moving.Curved
? NestTolerances.SafeClearanceMargin(NestTolerances.ValidationOutline)
: spacing > 0 ? NestTolerances.SafeClearanceMargin(0) : 0);
if (delta > 0) paths = Clipper.InflatePaths(paths, delta, JoinType.Round,
EndType.Polygon, 2, GeometryPrecision.Digits, 0.00001);
// Bound cache residency for jobs with many distinct rotation pairs.
if (cache.Count >= 8192) cache.Clear();
return cache[key] = paths;
}
private static Path64 ToInteger(Polygon polygon, bool reflect)
{
var scale = reflect ? -GeometryPrecision.Scale : GeometryPrecision.Scale;
var path = ClipperBridge.ToPath(polygon, positive: true);
return new Path64(path.Select(p => new Point64((long)M.Round(p.x * scale), (long)M.Round(p.y * scale))));
}
}