mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-26 15:21:06 -04:00
Capture and preparation agree that a nominal contour may close within NominalContourClosure.Tolerance inclusively, which converted drawings rely on: the sampled production corpus carries vertex gaps up to 1e-6. Replay verification still compared those rings at the exact geometry epsilon, so the one joint a rotated tool path legitimately bridges refused the whole plate after the search had already spent its budget. Census of 91 plates from 60 stored nests: every searched plate containing such a seam refused with contour-accounting (4 plates, 76.5s of wasted search, worst 71.7s for one 11-part plate), and none of the 42 ready plates had one. Replay now carries that same admitted endpoint gap as positional and length slack for the affected ring only. Direction sign, coverage order, retrace detection, ring counting and selected-emission binding keep the exact epsilon, and capture's closure admission is unchanged. With the fix the census yields 45 ready plates instead of 42, no plate changed any other way, and expansion counts are identical wherever status matched. P260805-01's 16-part plate plans in 0.76s rather than refusing after 4,129 expansions. Tests: SeamClosureReplayTests covers seams of 0, 5e-7 and 1e-6 plus four accounting faults (missing edge, reversed edge, interior shift beyond the admitted seam, second circuit) that must still refuse, and the beyond-tolerance capture refusal. OpenNest.Tests 3751, OpenNest.Engine.Tests 690 and OpenNest.IO.Tests 79 pass; Windows-only projects cannot build on Linux.
133 lines
6.0 KiB
C#
133 lines
6.0 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingPlanning;
|
|
using OpenNest.CNC.CuttingStrategy;
|
|
using OpenNest.Engine.CuttingPlanning;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests.CuttingPlanning;
|
|
|
|
/// <summary>
|
|
/// A nominal contour may lawfully close within <see cref="NominalContourClosure.Tolerance"/>, and
|
|
/// capture and preparation already agree on that inclusive boundary. Replay verification must
|
|
/// agree too: a rotated tool path legitimately bridges that one admitted joint, so refusing the
|
|
/// whole plate over it makes every such converted drawing unplanable.
|
|
/// </summary>
|
|
public class SeamClosureReplayTests
|
|
{
|
|
private static readonly Vector Start = new(-2, 5);
|
|
|
|
/// <summary>
|
|
/// Square entering mid-edge so the tool path wraps across the closing seam. The seam offset is
|
|
/// perpendicular to the edge that follows it, so bridging it shifts that edge's support by the
|
|
/// admitted gap instead of merely shortening it.
|
|
/// </summary>
|
|
private static Program SeamRing(double seam)
|
|
{
|
|
var program = new Program();
|
|
program.MoveTo(0, 0);
|
|
program.LineTo(0, 10);
|
|
program.LineTo(10, 10);
|
|
program.LineTo(10, 0);
|
|
program.LineTo(seam, 0);
|
|
return program;
|
|
}
|
|
|
|
private static (Part Part, CuttingParameters Settings) Fixture(double seam)
|
|
{
|
|
var settings = ExplicitContourTests.Parameters();
|
|
var program = SeamRing(seam);
|
|
var prepared = PreparedContours.Capture(program, settings);
|
|
Assert.Equal(1, prepared.Count); // Preparation accepts the same closure boundary as capture.
|
|
var part = new Part(new Drawing("seam", program));
|
|
Assert.True(part.RestoreLeadInProgram(prepared.Emit([prepared.Entry(0, 0, new Vector(0, 5))]), false));
|
|
return (part, settings);
|
|
}
|
|
|
|
private static CuttingPlanSnapshot Capture(Part part, CuttingParameters settings)
|
|
=> CuttingPlanService.Capture(new CuttingPlanRequest([part], Start, confirmedParameters: settings));
|
|
|
|
private static Program Path(params Vector[] points)
|
|
{
|
|
var program = new Program();
|
|
program.MoveTo(-0.3, 5);
|
|
program.Codes.Add(new LinearMove(0, 5) { Layer = LayerType.Leadin });
|
|
foreach (var point in points) program.LineTo(point);
|
|
return program;
|
|
}
|
|
|
|
private static OwnedExecution Read(Program program) => ExecutionMotionReader.Read(program, Vector.Zero, null, default);
|
|
|
|
[Theory]
|
|
[InlineData(0.0)] // exactly closed: unchanged behaviour
|
|
[InlineData(5e-7)] // admitted closure seam
|
|
[InlineData(1e-6)] // inclusive closure boundary that capture already accepts
|
|
public void AdmittedClosureSeam_StillReplays(double seam)
|
|
{
|
|
var (part, settings) = Fixture(seam);
|
|
var snapshot = Capture(part, settings);
|
|
Assert.True(snapshot.Placements[0].Material!.IsComplete, snapshot.Placements[0].Material!.Reason);
|
|
|
|
var source = snapshot.Placements[0];
|
|
var choice = source.Prepared!.Entry(0, 0, new Vector(0, 5)); // path wraps across the seam
|
|
var result = CuttingPlanService.ReplayPrograms(snapshot,
|
|
[source.Propose(source.Prepared.Emit([choice]), Read(source.Prepared.Emit([choice])), [choice])],
|
|
0, default);
|
|
|
|
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
|
Assert.True(result.IndependentlyReplayed);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdmittedClosureSeam_StillPlansWithGeneratedLeadIns()
|
|
{
|
|
// The same plate through the planner entry point: a searched route over an admitted seam
|
|
// must not be refused at replay.
|
|
var (part, settings) = Fixture(9e-7);
|
|
var result = CuttingPlanService.Plan(Capture(part, settings));
|
|
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("missing-edge")] // a real coverage hole next to an admitted seam still refuses
|
|
[InlineData("reversed-edge")] // wrong traversal direction still refuses
|
|
[InlineData("interior-shift")] // displacement larger than the admitted seam still refuses
|
|
[InlineData("second-circuit")] // retracing the ring still refuses
|
|
public void AdmittedClosureSeam_KeepsRefusingRealAccountingFaults(string fault)
|
|
{
|
|
var (part, settings) = Fixture(9e-7);
|
|
var snapshot = Capture(part, settings);
|
|
var source = snapshot.Placements[0];
|
|
var choice = source.Prepared!.Entry(0, 0, new Vector(0, 5));
|
|
var actual = fault switch
|
|
{
|
|
// Leaves the bottom edge uncut: coverage, not closure slack, is what fails.
|
|
"missing-edge" => Path(new Vector(0, 10), new Vector(10, 10), new Vector(10, 0)),
|
|
// Runs the right edge backwards against the nominal direction.
|
|
"reversed-edge" => Path(new(0, 5), new(0, 10), new(10, 10), new(10, 0), new(0, 0), new(0, 5)),
|
|
// Closes exactly but shifts an interior vertex by far more than the admitted seam.
|
|
"interior-shift" => Path(new Vector(0, 5), new(0, 10), new(10, 9.9999), new(10, 0), new(0, 0), new(0, 5)),
|
|
// Two circuits are never closure slack.
|
|
"second-circuit" => Path(new(0, 5), new(0, 10), new(10, 10), new(10, 0), new(0, 0), new(0, 5),
|
|
new(0, 10), new(10, 10), new(10, 0), new(0, 0), new(0, 5)),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(fault))
|
|
};
|
|
|
|
var result = CuttingPlanService.ReplayPrograms(snapshot, [source.Propose(actual, Read(actual), [choice])],
|
|
0, default);
|
|
|
|
Assert.NotEqual(CuttingPlanStatus.Ready, result.Status);
|
|
Assert.Empty(result.ProposedOrder);
|
|
Assert.False(result.IndependentlyReplayed);
|
|
}
|
|
|
|
[Fact]
|
|
public void GapBeyondAdmittedClosure_StillRefusesAtCapture()
|
|
{
|
|
// The fix must not widen closure admission itself: beyond tolerance capture still refuses.
|
|
var program = SeamRing(2e-6);
|
|
var material = LeadMaterialSnapshot.Capture(program, new Vector(30, 20));
|
|
Assert.False(material.IsComplete);
|
|
Assert.Contains("closure tolerance", material.Reason);
|
|
}
|
|
}
|