fix(cutoffs): accept absolute arc roundoff in automatic cut-off planning

Rotated imported arcs whose start and end radii differ by ~5e-11 are
converted without refitting their centre, so their rebuilt start point
lands just outside the cached part bounds. The allowance scaled with
coordinate magnitude (4096 steps, ~2.9e-11 here) and rejected them,
which stopped Plan Cutting's "Add scrap cut-offs" with "Converted
material extends outside cached part bounds". Use the existing 1e-10
cap as an absolute allowance; larger mismatches still block planning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-18 22:14:21 -04:00
1 parent fb505ef0ff
commit ee8be3d79e
3 files changed
+68 -3

No files matched your search

+4 -2
View File
@@ -278,8 +278,10 @@ namespace OpenNest
// Imported, nearly tangent arcs can amplify endpoint rounding when
// ConvertProgram fits their centers to a chord. Keep that allowance
// tiny and apply it to both pruning and fallback exclusions above;
// larger refits still require an explicit geometry repair.
var arcFitRoundoff = System.Math.Min(4096 * step, Math.Tolerance.Epsilon / 100000);
// larger refits still require an explicit geometry repair. The allowance is
// absolute: rotated small arcs on ordinary sheets refit by ~5e-11, which a
// magnitude-scaled step count would reject.
var arcFitRoundoff = Math.Tolerance.Epsilon / 100000;
return double.IsFinite(step)
? System.Math.Min(System.Math.Max(8 * step, arcFitRoundoff), Math.Tolerance.Epsilon / 4)
: 0;
@@ -66,6 +66,69 @@ public class CutOffRoundoffTests
Assert.Empty(AutomaticCutOffPlanner.Create(loadedPlate, options, settings).Definitions);
}
// A pie slice whose arc ends 2d further from its centre than it starts. ConvertProgram
// only refits centres beyond Tolerance.Epsilon, so it rebuilds the start point ~2d past
// the cached bounds, as rotated imported arcs do at d = 2.5e-11.
private static Plate MakeSlicePlate(double d)
{
var start = 10 * System.Math.PI / 180;
var end = 60 * System.Math.PI / 180;
var program = new Program();
program.Codes.Add(new RapidMove(0, 0));
program.Codes.Add(new LinearMove(10 * System.Math.Cos(start), 10 * System.Math.Sin(start)));
program.Codes.Add(new ArcMove((10 + 2 * d) * System.Math.Cos(end), (10 + 2 * d) * System.Math.Sin(end),
0, 0, RotationType.CCW));
program.Codes.Add(new LinearMove(0, 0));
var plate = new Plate(60, 120) { PartSpacing = 0 };
plate.Parts.Add(new Part(new Drawing("slice", program), new Vector(10, 10)));
return plate;
}
private static double Protrusion(Part part) => ConvertProgram.ToGeometry(part.Program)
.Where(e => SpecialLayers.IsMaterial(e.Layer))
.Max(e => e.BoundingBox.Translate(part.Location).Right) - part.BoundingBox.Right;
[Fact]
public void ArcRadiusRoundoff_PlansAndAppliesWithoutChangingThePart()
{
var plate = MakeSlicePlate(2.5e-11);
var part = Assert.Single(plate.Parts);
var program = part.Program;
var text = NestWriter.GetProgramText(program);
var right = part.BoundingBox.Right;
var step = System.Math.BitIncrement(right) - right;
// Above the old magnitude-scaled allowance (4096 steps), below the absolute one.
Assert.InRange(Protrusion(part), 4096 * step, CutOff.GetBoundsRoundoff(part));
var options = new AutomaticCutOffOptions { Spacing = 5 };
var settings = new CutOffSettings();
var plan = AutomaticCutOffPlanner.Create(plate, options, settings);
Assert.False(plan.HasBlockingDiagnostics);
Assert.NotEmpty(plan.Definitions);
plate.CutOffs.AddRange(plan.Definitions);
plate.RegenerateCutOffs(settings);
Assert.Same(program, part.Program);
Assert.Equal(text, NestWriter.GetProgramText(part.Program));
var arc = Assert.Single(ConvertProgram.ToGeometry(part.Program).OfType<Arc>());
arc.Offset(part.Location);
foreach (var cut in plate.Parts.Where(p => p.BaseDrawing.IsCutOff))
foreach (var line in ConvertProgram.ToGeometry(cut.Program).OfType<Line>()
.Where(e => SpecialLayers.IsMaterial(e.Layer)))
Assert.False(arc.Intersects(line, out _), line.ToString());
}
[Fact]
public void ArcRadiusMismatchBeyondRoundoff_StillPreventsPlanning()
{
var plate = MakeSlicePlate(1e-9);
var part = Assert.Single(plate.Parts);
Assert.True(Protrusion(part) > CutOff.GetBoundsRoundoff(part));
Assert.ThrowsAny<ArgumentException>(() =>
AutomaticCutOffPlanner.Create(plate, new AutomaticCutOffOptions { Spacing = 5 }, new CutOffSettings()));
}
[SkippableFact]
public void RoundedImportedArcs_PlanAllPlatesWithoutEditingSource()
{
+1 -1
View File
@@ -12,7 +12,7 @@ Finish nesting, then choose **Nest > Automatic Scrap Cutoffs (All Plates)** to p
For the Nest command, the on-sheet overlay and upper measurements show the active plate; a per-plate table lists each plate's proposed cut-off count, occupied and used spans, retained tail, and readiness (Ready, Blocked, or No change), while the details list reports only issues and notices, each prefixed with its plate number. Each plate uses its own dimensions, quadrant and part spacing with the dialog's common spacing/minimum-strip/minimum-tail settings and current cutoff clearance/direction settings. No plate navigation or per-plate confirmation is required. Empty or unchanged plates are left untouched. A blocking conflict or invalid plate prevents changes to **all** plates, with the plate number reported; resolve it before retrying. If application fails after changes begin, all touched plates' original cutoff programs and sequence are restored. An explicit incomplete-recovery warning requires manual review before saving or cutting.
An empty sheet is left untouched. Invalid input or out-of-sheet part geometry prevents automatic planning. Excessively small spacing is rejected rather than allocating an unbounded number of cuts. Small bounded discrepancies between converted material and cached bounds—including amplified roundoff in fitted near-tangent arcs—are accepted without changing the part or its program. The same conservative numerical padding applies to cutoff pruning and exclusions. Their broad-phase envelope uses the farther edge of the cached box and converted perimeter; a cutoff within the narrow uncertainty band at a cached edge gets a full-envelope exclusion instead of relying on a near-tangent contour intersection. Elsewhere the contour is retained for precise trimming. Larger arc-center discrepancies still prevent automatic planning rather than risking a cut through material; repair the geometry before retrying.
An empty sheet is left untouched. Invalid input or out-of-sheet part geometry prevents automatic planning. Excessively small spacing is rejected rather than allocating an unbounded number of cuts. Small bounded discrepancies between converted material and cached bounds—including amplified roundoff in fitted near-tangent arcs—are accepted without changing the part or its program. That allowance is absolute (10⁻¹⁰ model units), not a count of floating-point steps, so it does not shrink for parts near the origin: an arc whose end radius differs from its start radius by less than `Tolerance.Epsilon` is converted without refitting its centre, and rotated imported arcs can then rebuild a start point ~5 × 10⁻¹¹ outside the cached bounds. The same conservative numerical padding applies to cutoff pruning and exclusions. Their broad-phase envelope uses the farther edge of the cached box and converted perimeter; a cutoff within the narrow uncertainty band at a cached edge gets a full-envelope exclusion instead of relying on a near-tangent contour intersection. Elsewhere the contour is retained for precise trimming. Larger arc-center discrepancies still prevent automatic planning rather than risking a cut through material; repair the geometry before retrying.
## Preserving the unused tail