diff --git a/OpenNest.Engine.Opus55/Opus55NestingEngine.cs b/OpenNest.Engine.Opus55/Opus55NestingEngine.cs
index e0cfd4e..9e86d0b 100644
--- a/OpenNest.Engine.Opus55/Opus55NestingEngine.cs
+++ b/OpenNest.Engine.Opus55/Opus55NestingEngine.cs
@@ -20,11 +20,15 @@ namespace OpenNest.Engine.Opus55;
public sealed class Opus55NestingEngine : INestingEngine
{
///
- /// Extra clearance beyond the stock's part spacing, in job units. Validators polygonize arcs
- /// circumscribed at 0.01 per side, so two tangent true arcs can read as up to 0.02 closer
- /// than they are; the rest absorbs Clipper's 1e-4 grid and inner-fit clamping.
+ /// Extra clearance beyond the stock's part spacing, in job units. Footprints already contain
+ /// the true outline grown by half the clearance (chord tolerance is added separately), so this
+ /// 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.
///
- internal const double ClearanceMargin = 0.022;
+ internal const double ClearanceMargin = 0.003;
/// Strategy variants, tried in order: (front direction, area exponent beta).
private static readonly (PackAxis Axis, double Beta)[] Variants =
diff --git a/OpenNest.Engine.Opus55/PartCatalog.cs b/OpenNest.Engine.Opus55/PartCatalog.cs
index 6402a17..87f53b7 100644
--- a/OpenNest.Engine.Opus55/PartCatalog.cs
+++ b/OpenNest.Engine.Opus55/PartCatalog.cs
@@ -111,7 +111,7 @@ internal static class PartCatalog
{
var entities = ConvertProgram
.ToGeometry(DrawingJobMapper.ToProgram(geometry))
- .Where(e => !ReferenceEquals(e.Layer, SpecialLayers.Rapid))
+ .Where(e => SpecialLayers.IsMaterial(e.Layer))
.ToList();
if (entities.Count == 0)
return null;
diff --git a/OpenNest.Engine.Opus55/README.md b/OpenNest.Engine.Opus55/README.md
index c63ba4a..759fa94 100644
--- a/OpenNest.Engine.Opus55/README.md
+++ b/OpenNest.Engine.Opus55/README.md
@@ -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
(`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
- round offset) by `(spacing + 0.022) / 2 + chordTolerance`. Two parts respect the spacing
- when their footprints don't overlap. The 0.022 covers validators that polygonize arcs
- circumscribed at 0.01 per side, plus Clipper's 1e-4 grid.
+ round offset) by `(spacing + 0.003) / 2 + chordTolerance`. Two parts respect the spacing
+ when their footprints don't overlap. The 0.003 covers `NestValidator` flattening each arc
+ 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)
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.
@@ -83,9 +83,10 @@ The engine reports as `Opus55NestingEngine`.
## Known limitations
- **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
- required spacing, to stay valid under circumscribed-polygon validators. That's negligible in mm
- and about 0.02" in inches. The constants are absolute and assume job units near inch/mm scale.
+- **Clearance padding:** gaps are ~0.003 (plus up to the chord tolerance) wider than the
+ required spacing, to stay valid under `NestValidator`'s 0.001 arc flattening. The margin was
+ 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
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
diff --git a/OpenNest.Engine.Opus55/tests/Opus55NestingEngineTests.cs b/OpenNest.Engine.Opus55/tests/Opus55NestingEngineTests.cs
index 4beb32b..9cb4557 100644
--- a/OpenNest.Engine.Opus55/tests/Opus55NestingEngineTests.cs
+++ b/OpenNest.Engine.Opus55/tests/Opus55NestingEngineTests.cs
@@ -191,6 +191,24 @@ public class Opus55NestingEngineTests
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]
public void HasPublicParameterlessConstructorForPluginDiscovery()
{