fix(gpt6astra): rank lower Priority first and drop etch marks from geometry

Priority was sorted descending, the reverse of the host (StockLadder and
NestJobCandidateComparer treat a lower number as more important), so a
priority-9 part beat a priority-0 part for scarce stock. The existing test
encoded the inverted rule and now asserts the host's direction.

Part geometry filtered only rapids, so scribe/etch moves counted as
material - the bug OpenNest fixed in 1b5e1b1. Use SpecialLayers.IsMaterial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-25 07:45:12 -04:00
co-authored by Claude Opus 5.5
parent f973c1c15c
commit ffa3590b46
5 changed files with 36 additions and 8 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geomet
var placed = new List<PackedShape>();
var spaces = new Dictionary<int, SearchSpace>();
var order = Enumerable.Range(0, parts.Length)
.OrderByDescending(i => parts[i].Requirement.Priority)
.OrderBy(i => parts[i].Requirement.Priority)
.ThenBy(i => flexibility[i])
.ThenByDescending(i => parts[i].Variants.Select(v => v.Width * v.Height).DefaultIfEmpty(0).Min())
.ThenBy(i => i).ToArray();
@@ -22,7 +22,7 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
var best = initial;
Plan? complete = IsComplete(initial) ? initial : null;
var trials = new Dictionary<string, SheetTrial>(StringComparer.Ordinal);
var priorities = job.Parts.Select(p => p.Priority).Distinct().OrderDescending().ToArray();
var priorities = job.Parts.Select(p => p.Priority).Distinct().Order().ToArray();
var evaluated = 0;
var unitCosts = Enumerable.Repeat(double.PositiveInfinity, parts.Length).ToArray();
while (frontier.Count > 0)
@@ -78,7 +78,7 @@ internal static class GeometryPreparation
{
token.ThrowIfCancellationRequested();
var entities = ConvertProgram.ToGeometry(DrawingJobMapper.ToProgram(part.Geometry))
.Where(e => !ReferenceEquals(e.Layer, SpecialLayers.Rapid)).ToList();
.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())
+2 -2
View File
@@ -20,8 +20,8 @@ concave interlocking, and insertion into straight-edged and curved holes.
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.
3. Process high-priority parts first, then parts fitting fewer available stock types, then larger
envelopes. Larger frames precede inserts. Search every retained orientation for each instance.
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;
concave pairs use Clipper's integer Minkowski sum. Arc-heavy concave contact outlines use
a coarser mesh with both approximation bounds added to clearance; fine material geometry
@@ -102,8 +102,11 @@ public class Gpt6AstraNestingEngineTests
[Fact]
public void PriorityWinsScarceSpaceAndProgressReflectsCommits()
{
var low = Rectangle("low", 2, 2, 1);
var high = new NestJobPart("high", low.Geometry, 1, priority: 9);
// Lower Priority number ranks higher, as in StockLadderNestingEngine and
// NestJobCandidateComparer; input order is chosen so it cannot decide the winner.
var template = Rectangle("template", 2, 2, 1);
var low = new NestJobPart("low", template.Geometry, 1, priority: 9);
var high = new NestJobPart("high", template.Geometry, 1, priority: 0);
var job = new NestJob(new[] { low, high }, new[] { new NestPlateStock("s", new Size(2, 2), 1) });
var updates = new List<NestJobProgress>();
var result = new Gpt6AstraNestingEngine().Solve(job, new CallbackProgress(updates.Add));
@@ -287,6 +290,31 @@ public class Gpt6AstraNestingEngineTests
Validate(job, result);
}
[Fact]
public void EtchMarksAreLeftOutOfNestingGeometry()
{
// A bend tick starts on material and ends 1.0 into a side notch, outside the part but
// inside its bounding box (the PEP case that crashed nesting before 1b5e1b1). As
// material it is open geometry leaving the part; as a mark it must be ignored.
var job = new NestJob(new[] { new NestJobPart("part", PartGeometrySnapshot.FromProgram(NotchedPartWithEtch()), 2,
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);
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); }
@@ -321,7 +349,7 @@ public class Gpt6AstraNestingEngineTests
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 => !ReferenceEquals(e.Layer, SpecialLayers.Rapid)).ToArray();
.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));