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:
@@ -1,5 +1,6 @@
|
||||
using Clipper2Lib;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using M = System.Math;
|
||||
|
||||
namespace OpenNest.Engine.Gpt6Astra;
|
||||
@@ -47,7 +48,9 @@ internal sealed class ContactGeometry
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
var delta = spacing + stationary.ContactError + moving.ContactError
|
||||
+ (stationary.Curved || moving.Curved ? 0.003 : spacing > 0 ? 0.0003 : 0);
|
||||
+ (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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
|
||||
NestJobValidator.Validate(job);
|
||||
var parts = GeometryPreparation.Prepare(job, token);
|
||||
var fit = parts.Select(p => job.Plates.Select(s => p.Variants.Any(v =>
|
||||
v.Width <= s.Size.Length - s.EdgeSpacing.Left - s.EdgeSpacing.Right + 1e-9 &&
|
||||
v.Height <= s.Size.Width - s.EdgeSpacing.Top - s.EdgeSpacing.Bottom + 1e-9)).ToArray()).ToArray();
|
||||
s.Fits(v.Width, v.Height))).ToArray()).ToArray();
|
||||
var placer = new ContactPlacer(parts, new ContactGeometry(), token);
|
||||
var initial = new Plan(new int[parts.Length], new int[job.Plates.Count], new List<SheetTrial>(), 0);
|
||||
var frontier = new List<Plan> { initial };
|
||||
@@ -56,7 +55,9 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
|
||||
evaluated++;
|
||||
}
|
||||
if (trial.Shapes.Count == 0) continue;
|
||||
var sheetCost = job.Plates[s].Size.Length * job.Plates[s].Size.Width;
|
||||
var sheetCost = NestJobCost.NetSheetArea(job,
|
||||
new NestJobPlateResult(0, job.Plates[s], Poses(trial).Select(p =>
|
||||
new NestJobPlacement(p.PartId, 0, p.X, p.Y, p.Rotation))));
|
||||
for (var p = 0; p < parts.Length; p++)
|
||||
{
|
||||
var delivered = trial.Counts[p] - state.Counts[p];
|
||||
@@ -65,7 +66,7 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
|
||||
var used = (int[])state.Used.Clone(); used[s]++;
|
||||
var sheets = new List<SheetTrial>(state.Sheets) { trial };
|
||||
var next = new Plan(trial.Counts, used, sheets,
|
||||
state.Cost + job.Plates[s].Size.Length * job.Plates[s].Size.Width);
|
||||
state.Cost + sheetCost);
|
||||
if (BetterFulfillment(next, best)) best = next;
|
||||
if (IsComplete(next))
|
||||
{
|
||||
@@ -97,29 +98,26 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
|
||||
}
|
||||
}
|
||||
var selected = complete ?? best;
|
||||
var counts = new int[parts.Length];
|
||||
var plates = new List<NestJobPlateResult>();
|
||||
var builder = new NestJobResultBuilder(job, progress);
|
||||
foreach (var sheet in selected.Sheets)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var stock = job.Plates[sheet.StockIndex];
|
||||
var x = (stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length) + stock.EdgeSpacing.Left;
|
||||
var y = (stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width) + stock.EdgeSpacing.Bottom;
|
||||
var placements = sheet.Shapes.Select(p => new NestJobPlacement(job.Parts[p.Variant.Part].Id,
|
||||
counts[p.Variant.Part]++, x + p.X - p.Variant.OriginX,
|
||||
y + p.Y - p.Variant.OriginY, p.Variant.Angle)).ToArray();
|
||||
plates.Add(new(plates.Count, stock, placements));
|
||||
progress?.Report(new(NestJobStage.PlateCommitted, stock.Id, plates.Count - 1,
|
||||
plates.Count, counts.Sum()));
|
||||
builder.AddSheet(job.Plates[sheet.StockIndex], Poses(sheet));
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
var reason = complete != null ? NestJobStopReason.Completed :
|
||||
selected.Sheets.Count >= (job.Options.MaxPlates ?? int.MaxValue) ? NestJobStopReason.PlateLimitReached :
|
||||
!Enumerable.Range(0, job.Plates.Count).Any(s => Available(selected, s)) ? NestJobStopReason.StockExhausted :
|
||||
NestJobStopReason.NoPlacementFound;
|
||||
return new(complete != null ? NestJobStatus.Complete : NestJobStatus.Incomplete, reason, plates,
|
||||
job.Parts.Select((p, i) => new PartFulfillment(p.Id, p.Quantity, counts[i], p.Quantity - counts[i])),
|
||||
job.Plates.Select((s, i) => new StockUsage(s.Id, selected.Used[i], s.Quantity - selected.Used[i])));
|
||||
return builder.Build(reason);
|
||||
|
||||
IEnumerable<(string PartId, double X, double Y, double Rotation)> Poses(SheetTrial sheet)
|
||||
{
|
||||
var work = job.Plates[sheet.StockIndex].WorkArea;
|
||||
return sheet.Shapes.Select(p => (job.Parts[p.Variant.Part].Id,
|
||||
work.Left + p.X - p.Variant.OriginX, work.Bottom + p.Y - p.Variant.OriginY,
|
||||
p.Variant.Angle));
|
||||
}
|
||||
|
||||
bool Available(Plan p, int s) => p.Used[s] < (job.Plates[s].Quantity ?? int.MaxValue);
|
||||
bool IsComplete(Plan p) => parts.Select((part, i) => p.Counts[i] == part.Requirement.Quantity).All(v => v);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using Clipper2Lib;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
using OpenNest.Geometry;
|
||||
using M = System.Math;
|
||||
|
||||
@@ -25,24 +23,11 @@ internal sealed class ShapeVariant
|
||||
internal required double ContactError { get; init; }
|
||||
internal required Polygon Hull { get; init; }
|
||||
internal required bool Convex { get; init; }
|
||||
internal required ShapeProfile ValidationProfile { get; init; }
|
||||
internal required JobPartGeometry Geometry { get; init; }
|
||||
internal bool BoxLike => Material.Count == 1 && GridAligned(OriginX) && GridAligned(OriginY) &&
|
||||
GridAligned(Width) && GridAligned(Height) &&
|
||||
M.Abs(Outline.Area() - Width * Height) < 1e-8 * M.Max(1, Width * Height);
|
||||
private static bool GridAligned(double x) => M.Abs(x - M.Round(x * 10000) / 10000) < 1e-9;
|
||||
private readonly Dictionary<double, PathsD> validationRegions = new();
|
||||
|
||||
internal PathsD ValidationRegion(double spacing)
|
||||
{
|
||||
if (validationRegions.TryGetValue(spacing, out var cached)) return cached;
|
||||
// Match the external validator's sequence: flatten/round in the original
|
||||
// rotated snapshot frame, then translate. Rounding after normalization is
|
||||
// not equivalent at a zero-clearance contact.
|
||||
var region = ClipperBridge.OffsetForValidation(ValidationProfile, spacing, 0.001);
|
||||
var paths = new PathsD(region.Outers.Select(p => ClipperBridge.ToPath(p, true)));
|
||||
paths.AddRange(region.Holes.Select(p => ClipperBridge.ToPath(p, false)));
|
||||
return validationRegions[spacing] = GeometryPrecision.Translate(paths, -OriginX, -OriginY);
|
||||
}
|
||||
private readonly Dictionary<double, PathsD> halos = new();
|
||||
|
||||
internal PathsD Halo(double spacing)
|
||||
@@ -50,7 +35,7 @@ internal sealed class ShapeVariant
|
||||
if (halos.TryGetValue(spacing, out var cached)) return cached;
|
||||
// Raw outlines already circumscribe curves; the extra clearance covers independent
|
||||
// flattenings after pose materialization and the validator's four-decimal grid.
|
||||
var delta = spacing + (Curved ? 0.0021 : spacing > 0 ? 0.00015 : 0);
|
||||
var delta = spacing + (Curved ? NestTolerances.SafeClearanceMargin(NestTolerances.ValidationOutline) : spacing > 0 ? NestTolerances.SafeClearanceMargin(0) : 0);
|
||||
return halos[spacing] = delta == 0 ? Material : Clipper.InflatePaths(Material, delta,
|
||||
JoinType.Round, EndType.Polygon, 2, GeometryPrecision.Digits, 0.00001);
|
||||
}
|
||||
@@ -77,17 +62,19 @@ internal static class GeometryPreparation
|
||||
return job.Parts.Select((part, index) =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var entities = ConvertProgram.ToGeometry(DrawingJobMapper.ToProgram(part.Geometry))
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList();
|
||||
// Input validation has established that open marks lie inside material. They
|
||||
// must not be interpreted as holes by ShapeProfile.
|
||||
var closed = ShapeBuilder.GetShapes(entities).Where(s => s.IsClosed())
|
||||
.SelectMany(s => s.Entities).ToList();
|
||||
var baseProfile = new ShapeProfile(closed);
|
||||
var area = baseProfile.Perimeter.Area() - baseProfile.Cutouts.Sum(h => h.Area());
|
||||
var geometry = JobPartGeometry.Read(part.Geometry);
|
||||
var baseProfile = geometry.Profile;
|
||||
var closed = new[] { geometry.Perimeter }.Concat(geometry.Cutouts)
|
||||
.SelectMany(shape => shape.Entities).ToList();
|
||||
var area = geometry.MaterialArea;
|
||||
var variants = new List<ShapeVariant>();
|
||||
var keys = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var angle in Angles(part.Rotation, baseProfile))
|
||||
var angles = Angles(part.Rotation, baseProfile).ToArray();
|
||||
// The host symmetry primitive compares perimeters only. Keep full-material
|
||||
// signatures for holed parts, whose cutouts can break perimeter symmetry.
|
||||
var distinct = baseProfile.Cutouts.Count == 0
|
||||
? RotationCandidates.DistinctOutlines(baseProfile.Perimeter, angles) : angles;
|
||||
foreach (var angle in distinct)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var rotated = closed.Select(e => { var copy = e.Clone(); copy.Rotate(angle); return copy; }).ToList();
|
||||
@@ -97,15 +84,14 @@ internal static class GeometryPreparation
|
||||
var h = rotated.Max(e => e.Top) - y;
|
||||
if (!double.IsFinite(w) || !double.IsFinite(h) || w <= 0 || h <= 0)
|
||||
throw new ArgumentException($"Unusable rotated bounds: {part.Id}.");
|
||||
var validationProfile = new ShapeProfile(rotated.Select(e => e.Clone()).ToList());
|
||||
foreach (var e in rotated) e.Offset(-x, -y);
|
||||
var profile = new ShapeProfile(rotated);
|
||||
var material = ClipperBridge.ToRegion(profile, 0.001, circumscribe: true);
|
||||
var material = ClipperBridge.ToRegion(profile, NestTolerances.ValidationOutline, circumscribe: true);
|
||||
// Circular/symmetric parts should not multiply identical NFP work. Compare
|
||||
// normalized closed contours, including holes, independent of start vertex.
|
||||
var key = string.Join("|", material.Select(Canonical).Order(StringComparer.Ordinal));
|
||||
if (!keys.Add(key)) continue;
|
||||
var outline = ClipperBridge.Flatten(profile.Perimeter, 0.001, circumscribe: true);
|
||||
var outline = ClipperBridge.Flatten(profile.Perimeter, NestTolerances.ValidationOutline, circumscribe: true);
|
||||
var hull = ConvexHull.Compute(outline.Vertices);
|
||||
var convex = M.Abs(hull.Area() - outline.Area()) < 1e-7 * M.Max(1, hull.Area());
|
||||
// Concave Minkowski sums have quadratic input size. Only the contact
|
||||
@@ -118,7 +104,7 @@ internal static class GeometryPreparation
|
||||
OriginX = x, OriginY = y, Width = w, Height = h,
|
||||
Curved = rotated.Any(e => e is Arc or Circle), Material = material,
|
||||
Outline = outline, ContactOutline = contactOutline, ContactError = contactError,
|
||||
Hull = hull, Convex = convex, ValidationProfile = validationProfile });
|
||||
Hull = hull, Convex = convex, Geometry = geometry });
|
||||
}
|
||||
var ordered = variants.OrderBy(v => M.Round(v.Width * v.Height, 7)).ToArray();
|
||||
if (part.Rotation.Kind == RotationPolicyKind.Automatic && ordered.Length > 8)
|
||||
@@ -130,8 +116,7 @@ internal static class GeometryPreparation
|
||||
// discard every fitting orientation merely because its envelope is larger.
|
||||
foreach (var stock in job.Plates)
|
||||
{
|
||||
bool Fits(ShapeVariant v) => v.Width <= stock.Size.Length - stock.EdgeSpacing.Left - stock.EdgeSpacing.Right + 1e-9 &&
|
||||
v.Height <= stock.Size.Width - stock.EdgeSpacing.Top - stock.EdgeSpacing.Bottom + 1e-9;
|
||||
bool Fits(ShapeVariant v) => stock.Fits(v.Width, v.Height);
|
||||
if (!shortlist.Any(Fits)) shortlist.AddRange(all.Where(Fits).Take(4));
|
||||
}
|
||||
ordered = shortlist.DistinctBy(v => v.Id).ToArray();
|
||||
@@ -151,7 +136,7 @@ internal static class GeometryPreparation
|
||||
|
||||
private static IEnumerable<double> Angles(RotationPolicy policy, ShapeProfile profile)
|
||||
{
|
||||
var values = new List<double>();
|
||||
var values = new List<double>(RotationCandidates.ForShape(policy, profile.Perimeter));
|
||||
if (policy.Kind == RotationPolicyKind.Automatic)
|
||||
{
|
||||
// All half-turns matter for asymmetric parts, unlike envelope-only packing.
|
||||
@@ -163,19 +148,7 @@ internal static class GeometryPreparation
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var last = policy.Kind == RotationPolicyKind.Fixed ? 0 : M.Floor((policy.End - policy.Start) / policy.Step);
|
||||
if (!double.IsFinite(last)) last = 720;
|
||||
var samples = (int)M.Min(720, last);
|
||||
for (var i = 0; i <= samples; i++)
|
||||
{
|
||||
var k = samples == 0 ? 0 : M.Floor(last * ((double)i / samples));
|
||||
var angle = policy.Start + k * policy.Step;
|
||||
if (!double.IsFinite(angle) || !policy.Allows(angle)) continue;
|
||||
values.Add(angle);
|
||||
if (policy.Allow180Equivalent) values.Add(angle + M.PI);
|
||||
}
|
||||
}
|
||||
values = policy.EnumerateAngles().ToList();
|
||||
var seen = new HashSet<long>();
|
||||
foreach (var value in values)
|
||||
{
|
||||
|
||||
@@ -11,15 +11,14 @@ sheet's usable translation rectangle exposes contact positions where another par
|
||||
This permits overlapping bounding rectangles, complementary triangle pairs, staggered circles,
|
||||
concave interlocking, and insertion into straight-edged and curved holes.
|
||||
|
||||
1. Validate immutable job input. Reconstruct owned analytic entities with `DrawingJobMapper`
|
||||
and `ConvertProgram`. Closed contours define material; internal open marks do not become
|
||||
1. Validate immutable job input. Read owned analytic material with `JobPartGeometry.Read`. Closed contours define material; internal open marks do not become
|
||||
holes. Preserve the snapshot's origin when converting normalized placements back to poses.
|
||||
2. Prepare rotated outlines and material regions with holes, using conservative curve flattening.
|
||||
Automatic angles combine 15-degree samples over a full turn with orientations aligned to the
|
||||
Automatic angles extend `RotationCandidates.ForShape` with 15-degree samples over a full turn with orientations aligned to the
|
||||
longest straight edges. Symmetric duplicates are removed. Prefer up to 16 orientations whose
|
||||
envelope area is within 8% of the minimum; retain additional orientations when needed to fit
|
||||
a candidate stock. Fixed and bounded rotation policies remain enforced. Bounded sweeps use
|
||||
up to 721 integer step indices, including permitted half-turn equivalents.
|
||||
up to 720 base samples through `RotationPolicy.EnumerateAngles`, including permitted half-turn equivalents.
|
||||
3. Process high-priority parts first (a lower `Priority` number ranks higher, as in the host),
|
||||
then parts fitting fewer available stock types, then larger envelopes. Larger frames precede inserts. Search every retained orientation for each instance.
|
||||
4. Build cached Minkowski/no-fit regions. Convex pairs use Core's linear convex NFP primitive;
|
||||
@@ -40,7 +39,7 @@ concave interlocking, and insertion into straight-edged and curved holes.
|
||||
area lower bound prunes plans only once a complete cheaper plan exists. After 24 evaluated
|
||||
trials only one directional objective is used; after 64, beam width reduces to two.
|
||||
Work counts, not elapsed time or randomness, control search breadth.
|
||||
7. Select a complete plan with lowest purchased area, breaking equal-cost ties by sheet count.
|
||||
7. Select a complete plan with lowest `NestJobCost.NetSheetArea` including salvage, breaking equal-cost ties by sheet count.
|
||||
If no complete plan is found, maximize fulfilled counts by priority, then minimize cost.
|
||||
Emit committed-sheet progress, contiguous per-part instance indices, inventory, fulfillment
|
||||
and the contract's job-level stop reason. Cancellation throws without returning a partial job.
|
||||
@@ -53,10 +52,11 @@ corrects curved-hole validation; the placement algorithm remains entirely in Gpt
|
||||
## Precision and safety
|
||||
|
||||
Analytic rotated bounds govern sheet containment. Material curves are conservatively flattened
|
||||
at 0.001 job units. Positive configuration-space spacing includes 0.0003 extra units for non-rectangular
|
||||
straight outlines; curved outlines reserve 0.003 extra units even at zero spacing, accounting for offset/chord error and the
|
||||
benchmark validator's four-decimal grid. Axis-aligned rectangle contacts preserve exact requested
|
||||
spacing. Actual material intersection checks backstop candidate construction. Both straight-edged
|
||||
at `NestTolerances.ValidationOutline`. Contact generation and material halos both use
|
||||
`SafeClearanceMargin` so proposed contacts pass the same clearance gate. Axis-aligned
|
||||
rectangle contacts preserve exact requested spacing. `NestLayoutCheck.Clears` replaces
|
||||
the copied validator construction and collision sequence, with world-pose caching.
|
||||
Actual material intersection checks backstop candidate construction. Both straight-edged
|
||||
and curved holes are available for insertion. The shared collision routine now subtracts hole
|
||||
triangles into disjoint fragments with consistent half-space clipping, resolving the reproduced
|
||||
curved-hole false positive. See the benchmark report for regression results.
|
||||
@@ -123,3 +123,15 @@ non-cardinal rotations, hole insertion, automatic diagonal-only stock fits, all
|
||||
curves, incremental geometry, determinism, inventory, cancellation and stock-plan regressions.
|
||||
All 34 synthetic/generated benchmark cases and all four repository-DXF cases were valid and complete.
|
||||
Existing nullable warnings originate from the benchmark validator linked into the test project.
|
||||
|
||||
## Shared services migration
|
||||
|
||||
Stock bounds and fit checks use the host stock primitives; committed results and progress
|
||||
use `NestJobResultBuilder`. The test project shares `Engine.Testing` and no longer links
|
||||
benchmark source. The synthetic console also validates through `NestLayoutCheck` and
|
||||
reports `NestJobCost.Evaluate`. Automatic 15-degree/edge sampling, beam search, contact
|
||||
placement, exact rectangle handling, and full-material symmetry signatures for holed
|
||||
parts remain engine-owned. The host symmetry helper compares only perimeters.
|
||||
|
||||
The five salvage benchmarks stayed valid and complete. Cost fell from 5574.07 to
|
||||
5470.07 overall (no job worsened at report precision); see [PR 5 results](../MIGRATION-PR5.md).
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
<PropertyGroup><OutputType>Exe</OutputType><Nullable>disable</Nullable></PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../OpenNest.Engine.Gpt6Astra.csproj" />
|
||||
<Compile Include="$(OpenNestRoot)OpenNest.Benchmark/NestValidator.cs" Link="NestValidator.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -7,7 +7,6 @@ using OpenNest.CNC;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Benchmark;
|
||||
using CncProgram = OpenNest.CNC.Program;
|
||||
|
||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
@@ -30,10 +29,8 @@ if (args.Contains("--diagnose-ring"))
|
||||
new NestJobPlacement("insert", 0, x + offset, y, 0) }) },
|
||||
new[] { new PartFulfillment("ring", 1, 1, 0), new PartFulfillment("insert", 1, 1, 0) },
|
||||
new[] { new StockUsage("s", 1, 0) });
|
||||
var materialized = NestResultMaterializer.Materialize(j, result);
|
||||
var check = NestValidator.Validate(materialized.Nest.Plates.Select(p => (p, p.Parts.ToList())).ToList(),
|
||||
j.Parts.ToDictionary(p => materialized.DrawingsByPartId[p.Id], p => (p.Id, p.Quantity)));
|
||||
Console.WriteLine($"q={quadrant} offset={offset} valid={check.Valid}: {string.Join(';', check.Violations)}");
|
||||
var violations = NestLayoutCheck.Violations(j, result);
|
||||
Console.WriteLine($"q={quadrant} offset={offset} valid={violations.Count == 0}: {string.Join(';', violations)}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -73,7 +70,7 @@ Add("tail", new[] { Part("rect", Rect(6, 4), 17) }, new[] {
|
||||
Add("plate-cap", new[] { Part("r", Rect(5, 5), 10) }, new[] {
|
||||
new NestPlateStock("small", new Size(10, 10)), new NestPlateStock("large", new Size(20, 20)) }, new NestJobOptions(maxPlates: 1));
|
||||
cases.AddRange(OpenNest.Engine.Gpt6Astra.Benchmarks.GeneratedCases.Create());
|
||||
Console.WriteLine("case,valid,placed,requested,sheets,area,milliseconds");
|
||||
Console.WriteLine("case,valid,placed,requested,sheets,cost,milliseconds");
|
||||
foreach (var (name, job) in cases)
|
||||
{
|
||||
if (args.Length > 1 && !name.Contains(args[1], StringComparison.OrdinalIgnoreCase)) continue;
|
||||
@@ -82,13 +79,10 @@ foreach (var (name, job) in cases)
|
||||
try
|
||||
{
|
||||
var result = engine.Solve(job, token: cts.Token); sw.Stop();
|
||||
var nest = NestResultMaterializer.Materialize(job, result);
|
||||
var validation = NestValidator.Validate(nest.Nest.Plates.Select(p => (p, p.Parts.ToList())).ToList(),
|
||||
job.Parts.ToDictionary(p => nest.DrawingsByPartId[p.Id], p => (p.Id, p.Quantity)));
|
||||
NestValidator.ValidateAgainstJob(job, result, job.Parts.ToDictionary(p => p.Id, p => p.Id), validation);
|
||||
if (!validation.Valid) Environment.ExitCode = 1;
|
||||
Console.WriteLine($"{name},{validation.Valid},{result.Fulfillment.Sum(f => f.Placed)},{job.Parts.Sum(p => p.Quantity)},{result.Plates.Count},{result.Plates.Sum(p => p.Stock.Size.Length * p.Stock.Size.Width)},{sw.ElapsedMilliseconds}");
|
||||
foreach (var violation in validation.Violations.Take(4)) Console.Error.WriteLine($"{name}: {violation}");
|
||||
var violations = NestLayoutCheck.Violations(job, result);
|
||||
if (violations.Count != 0) Environment.ExitCode = 1;
|
||||
Console.WriteLine($"{name},{violations.Count == 0},{result.Fulfillment.Sum(f => f.Placed)},{job.Parts.Sum(p => p.Quantity)},{result.Plates.Count},{NestJobCost.Evaluate(job, result)},{sw.ElapsedMilliseconds}");
|
||||
foreach (var violation in violations.Take(4)) Console.Error.WriteLine($"{name}: {violation}");
|
||||
}
|
||||
catch (Exception ex) { Environment.ExitCode = 1; Console.WriteLine($"{name},ERROR,,,,,{sw.ElapsedMilliseconds}"); Console.Error.WriteLine(ex); }
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ claim of performance on an unseen competition dataset.
|
||||
## Synthetic cases
|
||||
|
||||
Both versions were run on exactly the same programmatically generated geometry and stock.
|
||||
The driver validates materialized output with `OpenNest.Benchmark.NestValidator`, including
|
||||
The driver validates materialized output with `NestLayoutCheck.Violations`, including
|
||||
quantity, stock settings, rotation, material overlap and spacing. Timings cover `Solve` only,
|
||||
exclude external validation, and are single-run observations rather than stable distributions.
|
||||
Every contact result is valid and complete. The baseline is valid but incomplete on `plate-cap`.
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using OpenNest.Engine.Testing;
|
||||
using static OpenNest.Engine.Testing.JobBuilder;
|
||||
using static OpenNest.Engine.Testing.Shapes;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.Jobs;
|
||||
@@ -23,7 +26,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
var before = job.Parts.Select(p => p.Geometry.Motions.ToArray()).ToArray();
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
for (var i = 0; i < job.Parts.Count; i++) Assert.Equal(before[i], job.Parts[i].Geometry.Motions);
|
||||
var again = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(result.Plates.SelectMany(p => p.Placements), again.Plates.SelectMany(p => p.Placements));
|
||||
@@ -37,7 +40,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
new NestPlateStock("large", new Size(20, 20)), new NestPlateStock("small", new Size(2, 2)) });
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal("small", Assert.Single(result.Plates).StockId);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -50,7 +53,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(reason, result.StopReason);
|
||||
Assert.Equal(2, Assert.Single(result.Fulfillment).Unplaced);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -83,7 +86,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
}, new[] { new NestPlateStock("s", new Size(20, 20), partSpacing: 0.4) });
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -130,7 +133,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
}, new[] { new NestPlateStock("s", new Size(20, 30), partSpacing: 0.2) });
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -148,7 +151,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
});
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +165,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Single(result.Plates);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -176,7 +179,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Single(result.Plates);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -188,7 +191,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal("large", Assert.Single(result.Plates).StockId);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -198,7 +201,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
new[] { new NestPlateStock("s", new Size(8, 8), 1) });
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -210,7 +213,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
new[] { new NestPlateStock("s", new Size(24, 48), partSpacing: 0.15) });
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -222,7 +225,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.True(result.Plates.Sum(p => p.Stock.Size.Length * p.Stock.Size.Width) <= 3600);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -232,7 +235,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
new[] { new NestPlateStock("s", new Size(4.25, 4.25), 1, 0.25) });
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -250,7 +253,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
}, new[] { new NestPlateStock("s", new Size(20, 25), partSpacing: spacing) });
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +274,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -287,7 +290,7 @@ public class Gpt6AstraNestingEngineTests
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job, token: cancellation.Token);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Single(result.Plates);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -300,76 +303,19 @@ public class Gpt6AstraNestingEngineTests
|
||||
rotation: RotationPolicy.Fixed(0)) },
|
||||
new[] { new NestPlateStock("s", new Size(10.4, 20.6), 1, partSpacing: 0.2) });
|
||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||
Validate(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
|
||||
}
|
||||
|
||||
private static Program NotchedPartWithEtch()
|
||||
{
|
||||
var p = new Program();
|
||||
p.MoveTo(0, 0); p.LineTo(10, 0); p.LineTo(10, 4); p.LineTo(8, 4); p.LineTo(8, 6);
|
||||
p.LineTo(10, 6); p.LineTo(10, 10); p.LineTo(0, 10); p.LineTo(0, 0);
|
||||
p.MoveTo(7.5, 5);
|
||||
p.Codes.Add(new LinearMove(9, 5) { Layer = LayerType.Scribe });
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
private sealed class CallbackProgress(Action<NestJobProgress> callback) : IProgress<NestJobProgress>
|
||||
{ public void Report(NestJobProgress value) => callback(value); }
|
||||
|
||||
private static NestJobPart Rectangle(string id, double w, double h, int count,
|
||||
RotationPolicy? rotation = null, double x = 0, double y = 0)
|
||||
{
|
||||
var p = new Program();
|
||||
p.MoveTo(x, y); p.LineTo(x + w, y); p.LineTo(x + w, y + h);
|
||||
p.LineTo(x, y + h); p.LineTo(x, y);
|
||||
return new(id, PartGeometrySnapshot.FromProgram(p), count, rotation: rotation ?? RotationPolicy.Fixed(0));
|
||||
}
|
||||
|
||||
private static void Validate(NestJob job, NestJobResult result)
|
||||
{
|
||||
var materialized = NestResultMaterializer.Materialize(job, result);
|
||||
var requirements = job.Parts.ToDictionary(p => materialized.DrawingsByPartId[p.Id],
|
||||
p => (Name: p.Id, Quantity: p.Quantity));
|
||||
var validation = OpenNest.Benchmark.NestValidator.Validate(
|
||||
materialized.Nest.Plates.Select(p => (p, p.Parts.ToList())).ToList(), requirements);
|
||||
OpenNest.Benchmark.NestValidator.ValidateAgainstJob(job, result,
|
||||
job.Parts.ToDictionary(p => p.Id, p => p.Id), validation);
|
||||
Assert.True(validation.Valid, string.Join("; ", validation.Violations));
|
||||
foreach (var sheet in result.Plates)
|
||||
{
|
||||
var s = sheet.Stock;
|
||||
var left = (s.Quadrant is 1 or 4 ? 0 : -s.Size.Length) + s.EdgeSpacing.Left;
|
||||
var bottom = (s.Quadrant is 1 or 2 ? 0 : -s.Size.Width) + s.EdgeSpacing.Bottom;
|
||||
var right = left + s.Size.Length - s.EdgeSpacing.Left - s.EdgeSpacing.Right;
|
||||
var top = bottom + s.Size.Width - s.EdgeSpacing.Bottom - s.EdgeSpacing.Top;
|
||||
foreach (var pose in sheet.Placements)
|
||||
{
|
||||
var part = job.Parts.Single(p => p.Id == pose.PartId);
|
||||
Assert.True(part.Rotation.Allows(pose.Rotation));
|
||||
var geometry = ConvertProgram.ToGeometry(DrawingJobMapper.ToProgram(part.Geometry))
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToArray();
|
||||
foreach (var entity in geometry) { entity.Rotate(pose.Rotation); entity.Offset(pose.X, pose.Y); }
|
||||
var b = (L: geometry.Min(e => e.Left), B: geometry.Min(e => e.Bottom),
|
||||
R: geometry.Max(e => e.Right), T: geometry.Max(e => e.Top));
|
||||
Assert.True(b.L >= left - 1e-7 && b.B >= bottom - 1e-7 && b.R <= right + 1e-7 && b.T <= top + 1e-7);
|
||||
}
|
||||
}
|
||||
foreach (var part in job.Parts)
|
||||
{
|
||||
var placed = result.Plates.SelectMany(s => s.Placements).Where(p => p.PartId == part.Id).ToArray();
|
||||
Assert.Equal(Enumerable.Range(0, placed.Length), placed.Select(p => p.InstanceIndex).Order());
|
||||
var fulfillment = result.Fulfillment.Single(f => f.PartId == part.Id);
|
||||
Assert.Equal(placed.Length, fulfillment.Placed);
|
||||
Assert.Equal(part.Quantity, fulfillment.Placed + fulfillment.Unplaced);
|
||||
}
|
||||
foreach (var usage in result.StockUsage)
|
||||
{
|
||||
var stock = job.Plates.Single(s => s.Id == usage.StockId);
|
||||
Assert.Equal(result.Plates.Count(s => s.StockId == stock.Id), usage.Used);
|
||||
Assert.Equal(stock.Quantity - usage.Used, usage.Remaining);
|
||||
Assert.True(usage.Remaining is null or >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public sealed class Gpt6AstraContractTests : EngineContractTests<Gpt6AstraNestingEngine> { }
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Compile Include="$(OpenNestRoot)OpenNest.Benchmark/NestValidator.cs" Link="NestValidator.cs" />
|
||||
<ProjectReference Include="../../Engine.Testing/OpenNest.Engine.Testing.csproj" />
|
||||
<ProjectReference Include="../OpenNest.Engine.Gpt6Astra.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user