fix(opus55): honor part priority and use host scoring and tolerances
Opus55 ignored NestJobPart.Priority, so the shared contract test (lower number wins scarce stock) failed; lower-number priority now precedes its placement score. SheetEconomics is replaced by the host's NestJobCost so it optimizes exactly what the benchmark scores, and its footprint margin comes from NestTolerances.SafeClearanceMargin plus four Clipper grid units - the same 0.003 total as before, which keeps its contact points. Synthetic benchmark (5 jobs, salvage 0.5): all valid, cost unchanged at 5452.79, time 611 -> 456 ms. Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
using Clipper2Lib;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Opus55;
|
||||
@@ -56,8 +54,6 @@ internal static class PartCatalog
|
||||
/// <summary>Hard cap on distinct orientations evaluated per part type.</summary>
|
||||
private const int MaxOrientations = 8;
|
||||
|
||||
private const double TwoPi = System.Math.PI * 2;
|
||||
|
||||
public static IReadOnlyList<PartType> Build(NestJob job)
|
||||
{
|
||||
// Fewer orientations per type for jobs with many distinct parts; every (type, rotation)
|
||||
@@ -83,21 +79,15 @@ internal static class PartCatalog
|
||||
continue;
|
||||
}
|
||||
|
||||
var angles = CandidateAngles(part.Rotation, perimeter, perType);
|
||||
var angles = RotationCandidates.DistinctOutlines(perimeter,
|
||||
CandidateAngles(part.Rotation, perimeter, perType));
|
||||
var tolerance = ChooseTolerance(perimeter);
|
||||
var orientations = new List<Orientation>();
|
||||
var signatures = new List<string>();
|
||||
foreach (var angle in angles)
|
||||
{
|
||||
var outline = Polygonize(perimeter, angle, tolerance);
|
||||
if (outline.Count < 3)
|
||||
continue;
|
||||
// Point-symmetric parts (rectangles, discs...) look identical at several angles;
|
||||
// evaluating duplicates only costs time.
|
||||
var signature = Signature(outline);
|
||||
if (signatures.Contains(signature))
|
||||
continue;
|
||||
signatures.Add(signature);
|
||||
orientations.Add(MakeOrientation(index, orientations.Count, angle, outline, tolerance));
|
||||
}
|
||||
|
||||
@@ -107,17 +97,8 @@ internal static class PartCatalog
|
||||
return types;
|
||||
}
|
||||
|
||||
private static Shape? ReadPerimeter(PartGeometrySnapshot geometry)
|
||||
{
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(DrawingJobMapper.ToProgram(geometry))
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ToList();
|
||||
if (entities.Count == 0)
|
||||
return null;
|
||||
var profile = new ShapeProfile(entities);
|
||||
return profile.Perimeter is { } perimeter && perimeter.Area() > 1e-9 ? perimeter : null;
|
||||
}
|
||||
private static Shape? ReadPerimeter(PartGeometrySnapshot geometry) =>
|
||||
JobPartGeometry.TryRead(geometry)?.Perimeter;
|
||||
|
||||
/// <summary>
|
||||
/// Coarsens arc polygonization (up to 0.1% of the part size) until the outline is small
|
||||
@@ -170,105 +151,6 @@ internal static class PartCatalog
|
||||
};
|
||||
}
|
||||
|
||||
private static string Signature(PathD outline)
|
||||
{
|
||||
var bounds = Clipper.GetBounds(outline);
|
||||
var points = outline
|
||||
.Select(p => (System.Math.Round(p.x - bounds.left, 5), System.Math.Round(p.y - bounds.top, 5)))
|
||||
.OrderBy(p => p.Item1)
|
||||
.ThenBy(p => p.Item2)
|
||||
.Select(p => $"{p.Item1:R},{p.Item2:R}");
|
||||
return string.Join(";", points);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotations to try, all satisfying the part's policy. Automatic parts get the four
|
||||
/// right angles plus the two orientations that align their minimum-area bounding
|
||||
/// rectangle with the sheet axes.
|
||||
/// </summary>
|
||||
internal static List<double> CandidateAngles(RotationPolicy policy, Shape perimeter, int limit)
|
||||
{
|
||||
var raw = new List<double>();
|
||||
switch (policy.Kind)
|
||||
{
|
||||
case RotationPolicyKind.Fixed:
|
||||
raw.Add(policy.Start);
|
||||
if (policy.Allow180Equivalent)
|
||||
raw.Add(policy.Start + System.Math.PI);
|
||||
break;
|
||||
|
||||
case RotationPolicyKind.BoundedSweep:
|
||||
{
|
||||
var steps = (int)System.Math.Floor((policy.End - policy.Start) / policy.Step + 1e-9);
|
||||
var samples = System.Math.Min(steps + 1, policy.Allow180Equivalent ? System.Math.Max(1, limit / 2) : limit);
|
||||
for (var i = 0; i < samples; i++)
|
||||
{
|
||||
var k = samples == 1 ? 0 : (int)System.Math.Round(i * (double)steps / (samples - 1));
|
||||
raw.Add(policy.Start + k * policy.Step);
|
||||
if (policy.Allow180Equivalent)
|
||||
raw.Add(policy.Start + k * policy.Step + System.Math.PI);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
var rightAngles = new[] { 0, System.Math.PI / 2, System.Math.PI, System.Math.PI * 1.5 };
|
||||
var aligned = AlignedAngle(perimeter);
|
||||
raw.Add(0);
|
||||
raw.Add(System.Math.PI / 2);
|
||||
if (aligned is double a)
|
||||
{
|
||||
raw.Add(Normalize(a));
|
||||
raw.Add(Normalize(a + System.Math.PI / 2));
|
||||
}
|
||||
raw.Add(System.Math.PI);
|
||||
raw.Add(System.Math.PI * 1.5);
|
||||
if (aligned is double b)
|
||||
{
|
||||
raw.Add(Normalize(b + System.Math.PI));
|
||||
raw.Add(Normalize(b + System.Math.PI * 1.5));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var result = new List<double>();
|
||||
foreach (var angle in raw)
|
||||
{
|
||||
if (!policy.Allows(angle))
|
||||
continue;
|
||||
if (result.Any(existing => SameTurn(existing, angle)))
|
||||
continue;
|
||||
result.Add(angle);
|
||||
if (result.Count >= limit)
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static double? AlignedAngle(Shape perimeter)
|
||||
{
|
||||
var polygon = perimeter.ToPolygonWithTolerance(ChordTolerance * 5);
|
||||
if (polygon.Vertices.Count < 3)
|
||||
return null;
|
||||
var mbr = RotatingCalipers.MinimumBoundingRectangle(polygon.Vertices);
|
||||
var angle = Normalize(-mbr.Angle) % (System.Math.PI / 2);
|
||||
// Already axis-aligned (within ~0.05°): the right angles cover it.
|
||||
if (angle < 1e-3 || System.Math.PI / 2 - angle < 1e-3)
|
||||
return null;
|
||||
return angle;
|
||||
}
|
||||
|
||||
private static double Normalize(double angle)
|
||||
{
|
||||
var value = angle % TwoPi;
|
||||
return value < 0 ? value + TwoPi : value;
|
||||
}
|
||||
|
||||
private static bool SameTurn(double a, double b)
|
||||
{
|
||||
var delta = System.Math.Abs(Normalize(a - b));
|
||||
return delta < 1e-9 || TwoPi - delta < 1e-9;
|
||||
}
|
||||
internal static List<double> CandidateAngles(RotationPolicy policy, Shape perimeter, int limit) =>
|
||||
RotationCandidates.ForShape(policy, perimeter, limit).ToList();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user