fix(opus55): cut clearance margin to 0.003 and drop etch marks from geometry

The 0.022 margin assumed validators flatten arcs at 0.01; NestValidator
uses 0.001, so a pair can read at most 0.002 closer than true. 0.003
covers that plus the 1e-4 Clipper grid, returning ~0.019 per gap. Arc,
disc, obround and zero-spacing tests still pass.

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 ffa3590b46
commit 1579fa6810
4 changed files with 34 additions and 11 deletions
@@ -20,11 +20,15 @@ namespace OpenNest.Engine.Opus55;
public sealed class Opus55NestingEngine : INestingEngine public sealed class Opus55NestingEngine : INestingEngine
{ {
/// <summary> /// <summary>
/// Extra clearance beyond the stock's part spacing, in job units. Validators polygonize arcs /// Extra clearance beyond the stock's part spacing, in job units. Footprints already contain
/// circumscribed at 0.01 per side, so two tangent true arcs can read as up to 0.02 closer /// the true outline grown by half the clearance (chord tolerance is added separately), so this
/// than they are; the rest absorbs Clipper's 1e-4 grid and inner-fit clamping. /// only has to cover the validator's side: NestValidator flattens each perimeter within
/// 0.001 of the true arc (OutlineTolerance), so two true arcs can read up to 0.002 closer
/// than they are. The remaining 0.001 absorbs the 1e-4 Clipper grid on both sides (footprint
/// offset, Minkowski union, free-region difference, the validator's own offset); the
/// validator's round-join chord error (0.0001) only ever makes it more lenient.
/// </summary> /// </summary>
internal const double ClearanceMargin = 0.022; internal const double ClearanceMargin = 0.003;
/// <summary>Strategy variants, tried in order: (front direction, area exponent beta).</summary> /// <summary>Strategy variants, tried in order: (front direction, area exponent beta).</summary>
private static readonly (PackAxis Axis, double Beta)[] Variants = private static readonly (PackAxis Axis, double Beta)[] Variants =
+1 -1
View File
@@ -111,7 +111,7 @@ internal static class PartCatalog
{ {
var entities = ConvertProgram var entities = ConvertProgram
.ToGeometry(DrawingJobMapper.ToProgram(geometry)) .ToGeometry(DrawingJobMapper.ToProgram(geometry))
.Where(e => !ReferenceEquals(e.Layer, SpecialLayers.Rapid)) .Where(e => SpecialLayers.IsMaterial(e.Layer))
.ToList(); .ToList();
if (entities.Count == 0) if (entities.Count == 0)
return null; return null;
+7 -6
View File
@@ -15,9 +15,9 @@ Every placement decision (which part, which rotation, where, on which sheet) com
angles plus the two orientations that axis-align the minimum-area bounding rectangle angles plus the two orientations that axis-align the minimum-area bounding rectangle
(`RotatingCalipers`); for sweeps, up to 8 evenly spaced legal steps. Point-symmetric duplicates are dropped. (`RotatingCalipers`); for sweeps, up to 8 evenly spaced legal steps. Point-symmetric duplicates are dropped.
- Each orientation gets a **footprint**: outline inflated (miter joins, so it contains the exact - Each orientation gets a **footprint**: outline inflated (miter joins, so it contains the exact
round offset) by `(spacing + 0.022) / 2 + chordTolerance`. Two parts respect the spacing round offset) by `(spacing + 0.003) / 2 + chordTolerance`. Two parts respect the spacing
when their footprints don't overlap. The 0.022 covers validators that polygonize arcs when their footprints don't overlap. The 0.003 covers `NestValidator` flattening each arc
circumscribed at 0.01 per side, plus Clipper's 1e-4 grid. within 0.001 of true (0.002 for a pair), plus Clipper's 1e-4 grid on both sides.
- **No-fit polygons** between footprints come from Clipper2 Minkowski sums: an O(n+m) - **No-fit polygons** between footprints come from Clipper2 Minkowski sums: an O(n+m)
edge merge for convex pairs, and for concave pairs the boundary sweep ∪ (A + p₀) ∪ (−B + a₀). edge merge for convex pairs, and for concave pairs the boundary sweep ∪ (A + p₀) ∪ (−B + a₀).
The last two terms cover "B inside A" and "B swallows A". NFPs are cached per orientation pair. The last two terms cover "B inside A" and "B swallows A". NFPs are cached per orientation pair.
@@ -83,9 +83,10 @@ The engine reports as `Opus55NestingEngine`.
## Known limitations ## Known limitations
- **No part-in-part:** holes are treated as solid, so small parts never nest inside cutouts. - **No part-in-part:** holes are treated as solid, so small parts never nest inside cutouts.
- **Clearance padding:** gaps are ~0.022 (plus up to the chord tolerance) wider than the - **Clearance padding:** gaps are ~0.003 (plus up to the chord tolerance) wider than the
required spacing, to stay valid under circumscribed-polygon validators. That's negligible in mm required spacing, to stay valid under `NestValidator`'s 0.001 arc flattening. The margin was
and about 0.02" in inches. The constants are absolute and assume job units near inch/mm scale. 0.022 while it assumed a 0.01 validator tolerance. The constants are absolute and assume job
units near inch/mm scale.
- **Rotation coverage:** `Automatic` parts try at most 8 orientations (fewer when a job has many - **Rotation coverage:** `Automatic` parts try at most 8 orientations (fewer when a job has many
distinct parts: `48 / partCount`, minimum 2). Free-angle rotations aren't explored beyond the MBR alignment. distinct parts: `48 / partCount`, minimum 2). Free-angle rotations aren't explored beyond the MBR alignment.
- **Greedy core:** there is no order/permutation search. The variants and tail re-plan are the only - **Greedy core:** there is no order/permutation search. The variants and tail re-plan are the only
@@ -191,6 +191,24 @@ public class Opus55NestingEngineTests
Assert.Equal(Describe(first), Describe(second)); Assert.Equal(Describe(first), Describe(second));
} }
[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 etched = Polyline((0, 0), (10, 0), (10, 4), (8, 4), (8, 6), (10, 6), (10, 10), (0, 10));
etched.Codes.Add(new RapidMove(7.5, 5));
etched.Codes.Add(new LinearMove(9, 5) { Layer = LayerType.Scribe });
var job = Job(new[] { Part("part", etched, 2, RotationPolicy.Fixed(0)) }, new[] { Stock("sheet", 10.4, 20.6, spacing: 0.2) });
var result = new Opus55NestingEngine().Solve(job);
AssertValid(job, result);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
}
[Fact] [Fact]
public void HasPublicParameterlessConstructorForPluginDiscovery() public void HasPublicParameterlessConstructorForPluginDiscovery()
{ {