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:
@@ -25,7 +25,7 @@ internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geomet
|
|||||||
var placed = new List<PackedShape>();
|
var placed = new List<PackedShape>();
|
||||||
var spaces = new Dictionary<int, SearchSpace>();
|
var spaces = new Dictionary<int, SearchSpace>();
|
||||||
var order = Enumerable.Range(0, parts.Length)
|
var order = Enumerable.Range(0, parts.Length)
|
||||||
.OrderByDescending(i => parts[i].Requirement.Priority)
|
.OrderBy(i => parts[i].Requirement.Priority)
|
||||||
.ThenBy(i => flexibility[i])
|
.ThenBy(i => flexibility[i])
|
||||||
.ThenByDescending(i => parts[i].Variants.Select(v => v.Width * v.Height).DefaultIfEmpty(0).Min())
|
.ThenByDescending(i => parts[i].Variants.Select(v => v.Width * v.Height).DefaultIfEmpty(0).Min())
|
||||||
.ThenBy(i => i).ToArray();
|
.ThenBy(i => i).ToArray();
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
|
|||||||
var best = initial;
|
var best = initial;
|
||||||
Plan? complete = IsComplete(initial) ? initial : null;
|
Plan? complete = IsComplete(initial) ? initial : null;
|
||||||
var trials = new Dictionary<string, SheetTrial>(StringComparer.Ordinal);
|
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 evaluated = 0;
|
||||||
var unitCosts = Enumerable.Repeat(double.PositiveInfinity, parts.Length).ToArray();
|
var unitCosts = Enumerable.Repeat(double.PositiveInfinity, parts.Length).ToArray();
|
||||||
while (frontier.Count > 0)
|
while (frontier.Count > 0)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ internal static class GeometryPreparation
|
|||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
var entities = ConvertProgram.ToGeometry(DrawingJobMapper.ToProgram(part.Geometry))
|
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
|
// Input validation has established that open marks lie inside material. They
|
||||||
// must not be interpreted as holes by ShapeProfile.
|
// must not be interpreted as holes by ShapeProfile.
|
||||||
var closed = ShapeBuilder.GetShapes(entities).Where(s => s.IsClosed())
|
var closed = ShapeBuilder.GetShapes(entities).Where(s => s.IsClosed())
|
||||||
|
|||||||
@@ -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
|
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
|
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 721 integer step indices, including permitted half-turn equivalents.
|
||||||
3. Process high-priority parts first, then parts fitting fewer available stock types, then larger
|
3. Process high-priority parts first (a lower `Priority` number ranks higher, as in the host),
|
||||||
envelopes. Larger frames precede inserts. Search every retained orientation for each instance.
|
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;
|
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
|
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
|
a coarser mesh with both approximation bounds added to clearance; fine material geometry
|
||||||
|
|||||||
@@ -102,8 +102,11 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void PriorityWinsScarceSpaceAndProgressReflectsCommits()
|
public void PriorityWinsScarceSpaceAndProgressReflectsCommits()
|
||||||
{
|
{
|
||||||
var low = Rectangle("low", 2, 2, 1);
|
// Lower Priority number ranks higher, as in StockLadderNestingEngine and
|
||||||
var high = new NestJobPart("high", low.Geometry, 1, priority: 9);
|
// 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 job = new NestJob(new[] { low, high }, new[] { new NestPlateStock("s", new Size(2, 2), 1) });
|
||||||
var updates = new List<NestJobProgress>();
|
var updates = new List<NestJobProgress>();
|
||||||
var result = new Gpt6AstraNestingEngine().Solve(job, new CallbackProgress(updates.Add));
|
var result = new Gpt6AstraNestingEngine().Solve(job, new CallbackProgress(updates.Add));
|
||||||
@@ -287,6 +290,31 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
Validate(job, result);
|
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>
|
private sealed class CallbackProgress(Action<NestJobProgress> callback) : IProgress<NestJobProgress>
|
||||||
{ public void Report(NestJobProgress value) => callback(value); }
|
{ public void Report(NestJobProgress value) => callback(value); }
|
||||||
|
|
||||||
@@ -321,7 +349,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
var part = job.Parts.Single(p => p.Id == pose.PartId);
|
var part = job.Parts.Single(p => p.Id == pose.PartId);
|
||||||
Assert.True(part.Rotation.Allows(pose.Rotation));
|
Assert.True(part.Rotation.Allows(pose.Rotation));
|
||||||
var geometry = ConvertProgram.ToGeometry(DrawingJobMapper.ToProgram(part.Geometry))
|
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); }
|
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),
|
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));
|
R: geometry.Max(e => e.Right), T: geometry.Max(e => e.Top));
|
||||||
|
|||||||
Reference in New Issue
Block a user