From ffa3590b469c55fb53004f86d7590a8defe0457b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 25 Sep 2026 07:45:12 -0400 Subject: [PATCH] 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 --- OpenNest.Engine.Gpt6Astra/ContactPlacer.cs | 2 +- .../Gpt6AstraNestingEngine.cs | 2 +- OpenNest.Engine.Gpt6Astra/PreparedGeometry.cs | 2 +- OpenNest.Engine.Gpt6Astra/README.md | 4 +-- .../tests/Gpt6AstraNestingEngineTests.cs | 34 +++++++++++++++++-- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/OpenNest.Engine.Gpt6Astra/ContactPlacer.cs b/OpenNest.Engine.Gpt6Astra/ContactPlacer.cs index 16e61f4..9e33b0f 100644 --- a/OpenNest.Engine.Gpt6Astra/ContactPlacer.cs +++ b/OpenNest.Engine.Gpt6Astra/ContactPlacer.cs @@ -25,7 +25,7 @@ internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geomet var placed = new List(); var spaces = new Dictionary(); 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(); diff --git a/OpenNest.Engine.Gpt6Astra/Gpt6AstraNestingEngine.cs b/OpenNest.Engine.Gpt6Astra/Gpt6AstraNestingEngine.cs index 7686478..41a32a6 100644 --- a/OpenNest.Engine.Gpt6Astra/Gpt6AstraNestingEngine.cs +++ b/OpenNest.Engine.Gpt6Astra/Gpt6AstraNestingEngine.cs @@ -22,7 +22,7 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine var best = initial; Plan? complete = IsComplete(initial) ? initial : null; var trials = new Dictionary(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) diff --git a/OpenNest.Engine.Gpt6Astra/PreparedGeometry.cs b/OpenNest.Engine.Gpt6Astra/PreparedGeometry.cs index da85732..ee67ffd 100644 --- a/OpenNest.Engine.Gpt6Astra/PreparedGeometry.cs +++ b/OpenNest.Engine.Gpt6Astra/PreparedGeometry.cs @@ -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()) diff --git a/OpenNest.Engine.Gpt6Astra/README.md b/OpenNest.Engine.Gpt6Astra/README.md index 500c52a..5739263 100644 --- a/OpenNest.Engine.Gpt6Astra/README.md +++ b/OpenNest.Engine.Gpt6Astra/README.md @@ -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 diff --git a/OpenNest.Engine.Gpt6Astra/tests/Gpt6AstraNestingEngineTests.cs b/OpenNest.Engine.Gpt6Astra/tests/Gpt6AstraNestingEngineTests.cs index 84197f4..1ee3e73 100644 --- a/OpenNest.Engine.Gpt6Astra/tests/Gpt6AstraNestingEngineTests.cs +++ b/OpenNest.Engine.Gpt6Astra/tests/Gpt6AstraNestingEngineTests.cs @@ -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(); 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 callback) : IProgress { 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));