mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-13 13:41:08 -05:00
The repo mixed variants such as center/centre and color/colour, so a name could be spelled either way and searches missed half the hits. Standardize on the US spelling, matching .NET's own APIs (Color, Center). Wire values owned by other systems keep their spelling: the material ledger's "Cancelled" hold status and GitHub's "cancelled" job status. No behavior change beyond the spelling of a few status messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
6.7 KiB
C#
140 lines
6.7 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingPlanning;
|
|
using OpenNest.Engine.CuttingPlanning;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests.CuttingPlanning;
|
|
|
|
public class CircleNextCutTests
|
|
{
|
|
private static Program WithHoles()
|
|
{
|
|
var program = LeadPathValidationTests.Rectangle(0, 0, 16, 12);
|
|
foreach (var center in new[] { new Vector(3, 3), new Vector(9, 4), new Vector(11, 8) })
|
|
{
|
|
program.MoveTo(center.X + 1, center.Y);
|
|
program.ArcTo(center.X + 1, center.Y, center.X, center.Y, RotationType.CCW);
|
|
}
|
|
return program;
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(13, 4)]
|
|
[InlineData(3, 13)]
|
|
[InlineData(-7, 2)]
|
|
[InlineData(4, -7)]
|
|
public void CirclesPreferTheExactNextCutDirectionWithoutDroppingPolarOrDiagonalOptions(double x, double y)
|
|
{
|
|
var prepared = PreparedContours.Capture(WithHoles(), HoleLookAheadTests.Parameters());
|
|
var target = new Vector(x, y);
|
|
var catalog = prepared.AutomaticEntryCandidatesWithFallbacks(0, target);
|
|
var ranked = catalog.RankTowardNextCut(target, new Vector(-4, -3));
|
|
var expected = prepared.ClosestEntry(0, target).Point;
|
|
|
|
Assert.True(ranked[0].Choice.Point.DistanceTo(expected) < 1e-8,
|
|
$"Expected next-facing {expected}, got {ranked[0].Choice.Point}");
|
|
Assert.Equal(catalog.Count, ranked.Count);
|
|
Assert.All(catalog, candidate => Assert.Contains(candidate, ranked));
|
|
for (var angle = 0; angle < 8; angle++)
|
|
{
|
|
var point = new Vector(3, 3) + new Vector(System.Math.Cos(angle * System.Math.PI / 4),
|
|
System.Math.Sin(angle * System.Math.PI / 4));
|
|
Assert.Contains(ranked, c => c.Choice.Point.DistanceTo(point) < 1e-8);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void BlockedExactPointRetainsAFeasiblePolarAlternative()
|
|
{
|
|
var prepared = PreparedContours.Capture(WithHoles(), HoleLookAheadTests.Parameters());
|
|
var target = new Vector(13, 4);
|
|
var catalog = prepared.AutomaticEntryCandidatesWithFallbacks(0, target);
|
|
var ranked = catalog.RankTowardNextCut(target, new Vector(-4, -3));
|
|
var exact = prepared.ClosestEntry(0, target).Point;
|
|
var material = LeadMaterialSnapshot.Capture(WithHoles(), Vector.Zero);
|
|
var feasibility = new ContourEntryFeasibility(prepared, Vector.Zero, material, []);
|
|
var blocked = 0;
|
|
var selection = ContourEntrySelection.Select(ranked, candidate =>
|
|
{
|
|
if (candidate.Choice.Point.DistanceTo(exact) < 1e-8)
|
|
{
|
|
blocked++;
|
|
return new(ContourFeasibilityStatus.Blocked, "Exact direction unavailable in this control.");
|
|
}
|
|
return feasibility.Check(candidate.Choice);
|
|
}, 1);
|
|
|
|
Assert.True(blocked > 0);
|
|
var selected = Assert.Single(selection.Choices);
|
|
Assert.True(selected.Point.DistanceTo(new Vector(4, 3)) < 1e-8,
|
|
$"Expected east polar fallback, got {selected.Point}");
|
|
Assert.Equal(ContourFeasibilityStatus.Clear, feasibility.Check(selected).Status);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(45)]
|
|
[InlineData(90)]
|
|
public void ConfiguredAngleRoundingStillSnapsTheActualCircleCuts(double increment)
|
|
{
|
|
var parameters = HoleLookAheadTests.Parameters();
|
|
parameters.RoundLeadInAngles = true;
|
|
parameters.LeadInAngleIncrement = increment;
|
|
var part = new Part(new Drawing("rounded starts", WithHoles()));
|
|
var next = new Part(new Drawing("next part", LeadPathValidationTests.Rectangle(0, 0, 2, 2)),
|
|
new Vector(22, 1));
|
|
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part, next], new Vector(-2, 3),
|
|
confirmedParameters: parameters, preservePartOrder: true));
|
|
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
|
Assert.True(result.IndependentlyReplayed);
|
|
var proposed = result.ProposedOrder[0];
|
|
var runs = HoleLookAheadTests.CutRuns(proposed.Execution);
|
|
var centers = new[] { new Vector(3, 3), new Vector(9, 4), new Vector(11, 8) };
|
|
for (var i = 0; i < 3; i++)
|
|
{
|
|
var radial = runs[i].End - centers[proposed.ContourChoices[i].ContourOrdinal];
|
|
var steps = System.Math.Atan2(radial.Y, radial.X) * 180 / System.Math.PI / increment;
|
|
Assert.True(System.Math.Abs(steps - System.Math.Round(steps)) < 1e-8);
|
|
}
|
|
Assert.True(parameters.RoundLeadInAngles);
|
|
Assert.Equal(increment, parameters.LeadInAngleIncrement);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, 0)]
|
|
[InlineData(23, 17)]
|
|
public void PlannedCircleStartsFaceTheImmediateNextPierceNotTwoFeaturesAhead(double x, double y)
|
|
{
|
|
var location = new Vector(x, y);
|
|
var part = new Part(new Drawing("three round cutouts", WithHoles()), location);
|
|
var next = new Part(new Drawing("next part", LeadPathValidationTests.Rectangle(0, 0, 2, 2)),
|
|
location + new Vector(22, 1));
|
|
var before = ExplicitContourTests.Fingerprint(part.Program);
|
|
var arrival = location + new Vector(-2, 3);
|
|
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part, next], arrival,
|
|
confirmedParameters: HoleLookAheadTests.Parameters(), preservePartOrder: true));
|
|
var result = CuttingPlanService.Plan(snapshot);
|
|
Assert.True(result.Status == CuttingPlanStatus.Ready,
|
|
$"{result.Status}: {string.Join("; ", result.Findings.Select(f => f.Message))}");
|
|
Assert.True(result.IndependentlyReplayed);
|
|
Assert.Equal(new[] { 0, 1 }, result.ProposedOrder.Select(p => p.SourceOrdinal));
|
|
var proposal = result.ProposedOrder[0];
|
|
var choices = proposal.ContourChoices;
|
|
var runs = HoleLookAheadTests.CutRuns(proposal.Execution);
|
|
Assert.Equal(4, runs.Count);
|
|
Assert.Equal(new[] { 0, 1, 2, 3 }, choices.Select(c => c.ContourOrdinal));
|
|
var prepared = snapshot.Placements[0].Prepared;
|
|
for (var i = 0; i < choices.Count - 1; i++)
|
|
{
|
|
var target = runs[i + 1].Pierce - location;
|
|
var expected = prepared.ClosestEntry(choices[i].ContourOrdinal, target).Point;
|
|
Assert.True(choices[i].Point.DistanceTo(expected) < 1e-8,
|
|
$"Hole {i}: expected immediate-next-facing {expected}, got {choices[i].Point}");
|
|
// The emitter rounds circle start angles; verify actual output too, allowing
|
|
// only that existing rounding rather than a snapped compass direction.
|
|
Assert.True((runs[i].End - location).DistanceTo(expected) < 0.01);
|
|
}
|
|
Assert.Empty(new ReleasedContourState().Check(proposal.Execution, arrival, 1));
|
|
Assert.Equal(before, ExplicitContourTests.Fingerprint(part.Program));
|
|
}
|
|
}
|