mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-14 14:51:56 -04:00
test(diagnostics): lock the start-point clamp rapid warning
D10 (user decision 2026-10-13, keep the warning): a clamp footprint covering the modeled program start (0,0) makes the first rapid of every posting attempt cross it, and that warning has no exemption and no persisted bypass. Lock the behavior in all four quadrants with a physical-footprint oracle and a mutation red (skipping rapids that start inside a zone reddens exactly the four origin-covering rows). Document the design in docs/post-verification.md.
This commit is contained in:
1 parent
92d8964339
commit
83ae1a48f0
2 files changed
+157
No files matched your search
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
using Xunit;
|
||||
|
||||
namespace OpenNest.Tests.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// D10 (user decision 2026-10-13, "keep the warning"): a clamp whose physical footprint
|
||||
/// covers the modeled program start (0,0) makes every direct XY rapid that leaves the start
|
||||
/// point cross it. That warning stays: no exemption for the first rapid, the start point or
|
||||
/// "head is already up", and no persisted bypass — every posting attempt re-reports it.
|
||||
/// Fixtures build the rail with <see cref="Plate.SetClampRail"/> and predict footprints from
|
||||
/// the physical rule (center +/- width/2 from the origin along the edge, depth into the
|
||||
/// sheet) plus an independent segment/box oracle, never from the analyzer's own math.
|
||||
/// </summary>
|
||||
public class StartPointClampRapidWarningTests
|
||||
{
|
||||
private const double OriginCenter = 0.25;
|
||||
private const double ClampWidth = 1.5;
|
||||
private const double ClampDepth = 0.625;
|
||||
|
||||
private static int XSign(int quadrant) => quadrant is 2 or 3 ? -1 : 1;
|
||||
private static int YSign(int quadrant) => quadrant is 3 or 4 ? -1 : 1;
|
||||
|
||||
private static Box PhysicalFootprint(int quadrant, double center)
|
||||
{
|
||||
var near = center - ClampWidth / 2;
|
||||
var far = center + ClampWidth / 2;
|
||||
var x = XSign(quadrant) > 0 ? near : -far;
|
||||
var y = YSign(quadrant) > 0 ? 0 : -ClampDepth;
|
||||
return new Box(x, y, ClampWidth, ClampDepth);
|
||||
}
|
||||
|
||||
/// <summary>Independent oracle: does the closed segment from->to touch the closed box?</summary>
|
||||
private static bool SegmentTouchesBox(Vector from, Vector to, Box box)
|
||||
{
|
||||
var tEnter = 0.0;
|
||||
var tExit = 1.0;
|
||||
Clip(-dx(from, to), from.X - box.Left);
|
||||
Clip(dx(from, to), box.Right - from.X);
|
||||
Clip(-dy(from, to), from.Y - box.Bottom);
|
||||
Clip(dy(from, to), box.Top - from.Y);
|
||||
return tEnter <= tExit;
|
||||
|
||||
void Clip(double p, double q)
|
||||
{
|
||||
if (p == 0)
|
||||
{
|
||||
if (q < 0) tEnter = 2; // provably disjoint
|
||||
return;
|
||||
}
|
||||
var ratio = q / p;
|
||||
if (p < 0) tEnter = System.Math.Max(tEnter, ratio);
|
||||
else tExit = System.Math.Min(tExit, ratio);
|
||||
}
|
||||
}
|
||||
|
||||
private static double dx(Vector from, Vector to) => to.X - from.X;
|
||||
private static double dy(Vector from, Vector to) => to.Y - from.Y;
|
||||
|
||||
private static Plate RailPlate(int quadrant, params double[] centers)
|
||||
{
|
||||
var plate = new Plate(81, 120) { Quadrant = quadrant };
|
||||
plate.SetClampRail(new ClampRail(ClampWidth, ClampDepth, centers));
|
||||
return plate;
|
||||
}
|
||||
|
||||
private static Part ScribeSquare(Vector location)
|
||||
{
|
||||
var program = new Program();
|
||||
program.MoveTo(0, 0);
|
||||
program.LineTo(0.1, 0);
|
||||
program.LineTo(0.1, 0.1);
|
||||
program.LineTo(0, 0.1);
|
||||
program.LineTo(0, 0);
|
||||
foreach (var line in program.Codes.OfType<LinearMove>())
|
||||
line.Layer = LayerType.Scribe;
|
||||
return new Part(new Drawing("scribe", program), location);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[InlineData(4)]
|
||||
public void OriginCoveringClampWarnsOnceOnTheFirstRapid(int quadrant)
|
||||
{
|
||||
// Oracle precondition: the rail rule puts a clamp at center 0.25 over the start point.
|
||||
var footprint = PhysicalFootprint(quadrant, OriginCenter);
|
||||
Assert.True(footprint.Left <= 0 && 0 <= footprint.Right);
|
||||
Assert.True(footprint.Bottom <= 0 && 0 <= footprint.Top);
|
||||
|
||||
var plate = RailPlate(quadrant, OriginCenter);
|
||||
var partLocation = new Vector(XSign(quadrant) * 40, YSign(quadrant) * 20);
|
||||
plate.Parts.Add(ScribeSquare(partLocation));
|
||||
// The oracle sees the incoming rapid (0,0) -> part start crossing the footprint.
|
||||
Assert.True(SegmentTouchesBox(Vector.Zero, partLocation, footprint));
|
||||
|
||||
// Every posting attempt re-runs the analysis; nothing is remembered between attempts.
|
||||
var report = PostVerificationAnalyzer.Analyze(NestOf(plate));
|
||||
var again = PostVerificationAnalyzer.Analyze(NestOf(plate));
|
||||
|
||||
var rapid = Assert.Single(report.Findings, f => f.Kind == PostVerificationKind.RapidCrossing);
|
||||
Assert.Equal("clamp-1", rapid.ClampId);
|
||||
Assert.Equal(1, rapid.PartNumber);
|
||||
Assert.Equal(Vector.Zero, rapid.RapidStart);
|
||||
var contact = Assert.Single(rapid.ContactPoints);
|
||||
Assert.InRange(contact.X, footprint.Left, footprint.Right);
|
||||
Assert.InRange(contact.Y, footprint.Bottom, footprint.Top);
|
||||
Assert.Contains("Direct XY rapid crosses active clamp 'clamp-1'", rapid.Message);
|
||||
|
||||
var repeated = Assert.Single(again.Findings, f => f.Kind == PostVerificationKind.RapidCrossing);
|
||||
Assert.Equal(rapid.ClampId, repeated.ClampId);
|
||||
|
||||
// Consent is per attempt: never postable without it, always postable with it.
|
||||
Assert.False(report.CanPost(false));
|
||||
Assert.True(report.CanPost(true));
|
||||
Assert.False(again.CanPost(false));
|
||||
Assert.True(again.CanPost(true));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[InlineData(4)]
|
||||
public void RailWithoutOriginClampGivesNoClampRapidFinding(int quadrant)
|
||||
{
|
||||
var plate = RailPlate(quadrant, 6);
|
||||
var footprint = PhysicalFootprint(quadrant, 6);
|
||||
Assert.False(footprint.Left <= 0 && 0 <= footprint.Right &&
|
||||
footprint.Bottom <= 0 && 0 <= footprint.Top);
|
||||
|
||||
var partLocation = new Vector(XSign(quadrant) * 10, YSign(quadrant) * 20);
|
||||
plate.Parts.Add(ScribeSquare(partLocation));
|
||||
// The oracle: this first rapid passes above the clamp, and the part clears it.
|
||||
Assert.False(SegmentTouchesBox(Vector.Zero, partLocation, footprint));
|
||||
|
||||
var report = PostVerificationAnalyzer.Analyze(NestOf(plate));
|
||||
|
||||
Assert.DoesNotContain(report.Findings, f => f.Kind == PostVerificationKind.RapidCrossing);
|
||||
Assert.DoesNotContain(report.Findings, f => f.ClampId != null);
|
||||
}
|
||||
|
||||
private static Nest NestOf(Plate plate)
|
||||
{
|
||||
var nest = new Nest();
|
||||
nest.Plates.Add(plate);
|
||||
return nest;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,9 @@ shows all three check categories, even when there are no findings:
|
||||
an obstacle yet. Actual uncut gaps are respected; enabling tabs in settings alone
|
||||
is not evidence that a gap exists. Clamp-rapid warnings are conservative: head
|
||||
height/retracts are not modeled, so inspect the emitted route and machine setup.
|
||||
A clamp whose footprint covers the program start point warns on every posting
|
||||
attempt by design; there is no exemption for the first rapid and no persisted
|
||||
bypass (settled decision D10).
|
||||
|
||||
Findings identify the plate and part's position in the cutting sequence. Read the
|
||||
scrollable report, cancel to fix the nest, then post again to rerun verification.
|
||||
|
||||
Reference in new issue
Block a user