fix(qwen38flashnext): make layouts deterministic and use host services

The gap-fill pass stopped on a 120 ms stopwatch and QWEN_* environment
variables switched strategies, so the same job could nest differently
with machine load or environment. Gap fill now stops after eight failed
insertion sweeps, and the switches are internal properties with the
old defaults. Part reading, work area, rotation angles, scoring and
result assembly now use the host APIs; its collision gate is unchanged.

Synthetic benchmark (5 jobs, salvage 0.5): all valid, cost 7660.01 ->
7572.05, time 839 -> 553 ms. Not yet calibrated on production-size jobs.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-25 09:29:28 -04:00
co-authored by Codex Claude Opus 5.5
parent 0308a862b8
commit 0cdef009f8
6 changed files with 98 additions and 329 deletions
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using OpenNest.Converters;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Geometry;
using OpenNest.Math;
@@ -53,10 +51,11 @@ internal sealed class PartModel
/// <summary>Material area (perimeter minus holes), from the analytic shapes.</summary>
public double Area { get; }
internal List<double>? Angles { get; set; }
/// <summary>
/// Chord tolerance for the engine's internal collision polygons. Must stay FINER
/// than the validator's OutlineTolerance (0.001): any chord cuts the cap off a
/// than NestTolerances.ValidationOutline (0.001): any chord cuts the cap off a
/// concave arc, and a coarser polygon cuts MORE - so a coarse flattening is a
/// subset of the validator's material in notched regions and admits real spacing
/// violations (observed on arc-heavy PEP parts at 0.02). Finer than the validator,
@@ -70,47 +69,11 @@ internal sealed class PartModel
/// </summary>
public static PartModel? TryCreate(NestJobPart part)
{
try
{
var entities = new List<Entity>();
foreach (
var entity in ConvertProgram.ToGeometry(
DrawingJobMapper.ToProgram(part.Geometry)
)
)
if (SpecialLayers.IsMaterial(entity.Layer))
entities.Add(entity);
if (entities.Count == 0)
return null;
var profile = new ShapeProfile(entities);
if (profile.Perimeter == null)
return null;
profile.NormalizeWinding();
var area = Math.Abs(profile.Perimeter.Area());
foreach (var cutout in profile.Cutouts)
area -= Math.Abs(cutout.Area());
if (!double.IsFinite(area) || area <= Tolerance.Epsilon)
return null;
return new PartModel(
part.Id,
part.Quantity,
part.Priority,
part.Rotation,
profile,
profile.Perimeter,
new List<Shape>(profile.Cutouts),
area
);
}
catch (Exception)
{
// Malformed snapshots are unplaceable, not fatal: report them unplaced so
// the rest of the job still nests.
return null;
}
var geometry = JobPartGeometry.TryRead(part.Geometry);
if (geometry == null || geometry.MaterialArea <= Tolerance.Epsilon) return null;
// Read normalizes winding before the collision and offset preparation below.
return new PartModel(part.Id, part.Quantity, part.Priority, part.Rotation,
geometry.Profile, geometry.Perimeter, geometry.Cutouts.ToList(), geometry.MaterialArea);
}
}
@@ -441,84 +404,15 @@ internal sealed class PartPreparation
/// </summary>
public static List<double> CandidateAngles(PartModel model)
{
var angles = new List<double>();
var policy = model.Rotation;
if (policy.Kind == RotationPolicyKind.Automatic)
{
angles.Add(0);
angles.Add(Math.PI / 2);
angles.Add(Math.PI);
angles.Add(3 * Math.PI / 2);
try
{
var hull = ConvexHull.Compute(
model
.PerimeterShape
.ToPolygonWithTolerance(PartModel.CollisionTolerance, circumscribe: true)
.Vertices
);
var obb = RotatingCalipers.MinimumBoundingRectangle(hull);
var normalized = OpenNest.Math.Angle.NormalizeRad(obb.Angle);
if (normalized > 0.001 && normalized < Math.PI - 0.001)
{
angles.Add(normalized);
angles.Add(OpenNest.Math.Angle.NormalizeRad(normalized + Math.PI));
}
}
catch (Exception)
{
// A calipers failure only costs candidate angles, never correctness.
}
}
else if (policy.Kind == RotationPolicyKind.Fixed)
{
angles.Add(policy.Start);
if (policy.Allow180Equivalent)
angles.Add(policy.Start + Math.PI);
}
else
{
// BoundedSweep: enumerate the exact step grid the policy allows.
var count = (int)Math.Floor((policy.End - policy.Start) / policy.Step + 1e-9);
if (count < 0)
count = 0;
if (count > 4000)
count = 4000;
for (var i = 0; i <= count; i++)
{
angles.Add(policy.Start + i * policy.Step);
if (policy.Allow180Equivalent)
angles.Add(policy.Start + i * policy.Step + Math.PI);
}
}
// Normalize to [0, 2pi), deduplicate, preserve first-seen order (deterministic).
var unique = new List<double>();
foreach (var angle in angles)
{
var normalized = OpenNest.Math.Angle.NormalizeRad(angle);
if (normalized < 0)
normalized += 2 * Math.PI;
var duplicate = false;
foreach (var existing in unique)
if (Math.Abs(SignedDelta(existing, normalized)) < 1e-9)
{
duplicate = true;
break;
}
if (!duplicate)
unique.Add(normalized);
}
return unique;
if (model.Angles != null) return model.Angles;
var angles = model.Rotation.Kind == RotationPolicyKind.Automatic
? RotationCandidates.ForShape(model.Rotation, model.PerimeterShape)
: model.Rotation.EnumerateAngles(maxSamples: 4000);
// Perimeter symmetry does not establish symmetry of the cutouts.
return model.Angles = (model.CutoutShapes.Count == 0
? RotationCandidates.DistinctOutlines(model.PerimeterShape, angles)
: angles).ToList();
}
private static double SignedDelta(double a, double b)
{
var delta = (a - b) % (2 * Math.PI);
if (delta > Math.PI)
delta -= 2 * Math.PI;
if (delta < -Math.PI)
delta += 2 * Math.PI;
return delta;
}
}