mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-12 07:44:21 -04:00
fix(cutting): aim round cutouts toward the next cut
This commit is contained in:
1 parent
1c923976a8
commit
fc300705a8
4 files changed
+173
-3
No files matched your search
@@ -8,7 +8,8 @@ namespace OpenNest.CNC.CuttingPlanning;
|
||||
|
||||
/// <summary>
|
||||
/// Pure deterministic ordering of the automatic entry catalogue toward the next cut:
|
||||
/// facing sides first, then tier, then travel, then a stable geometric key. It adds no
|
||||
/// circles rank by outgoing distance; other contours use facing sides, tier and travel,
|
||||
/// then a stable geometric key. It adds no
|
||||
/// candidates, mutates nothing, runs no lead checks and applies no cap — feasibility
|
||||
/// filtering and the bounded selection belong to S07/S08, the wiring to S09.
|
||||
/// </summary>
|
||||
@@ -33,6 +34,19 @@ internal static class ContourEntryRanking
|
||||
if (candidates.Count == 0)
|
||||
return new List<ContourEntryCandidate>();
|
||||
|
||||
// Whole circles have no corners. A diagonal compass point is equally near two
|
||||
// bounding-box sides, but must not gain the two-side bonus of a real corner.
|
||||
// Rank outgoing travel first so arrival cannot pull the start away from the next
|
||||
// cut. Retain every compass/polar alternative for feasibility and rapid checks;
|
||||
// configured angle rounding remains the emitter's responsibility.
|
||||
if (target is { } next && candidates.Any(c => c.Kind == AutomaticEntryKind.CircleCompass)
|
||||
&& candidates.All(c => c.Kind is AutomaticEntryKind.CircleCompass or AutomaticEntryKind.TargetFacing))
|
||||
return candidates.OrderBy(c => c.Choice.Point.DistanceTo(next))
|
||||
.ThenBy(c => arrival is { } from ? c.Choice.Point.DistanceTo(from) : 0.0)
|
||||
.ThenBy(c => c.GeometryKey.X)
|
||||
.ThenBy(c => c.GeometryKey.Y)
|
||||
.ToList();
|
||||
|
||||
// Candidate bounding rectangle in the contour's local coordinates; every candidate
|
||||
// lies on the contour, so distances to the four side lines order side proximity.
|
||||
var minX = double.PositiveInfinity;
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
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 centre in new[] { new Vector(3, 3), new Vector(9, 4), new Vector(11, 8) })
|
||||
{
|
||||
program.MoveTo(centre.X + 1, centre.Y);
|
||||
program.ArcTo(centre.X + 1, centre.Y, centre.X, centre.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 catalogue = prepared.AutomaticEntryCandidatesWithFallbacks(0, target);
|
||||
var ranked = catalogue.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(catalogue.Count, ranked.Count);
|
||||
Assert.All(catalogue, 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 catalogue = prepared.AutomaticEntryCandidatesWithFallbacks(0, target);
|
||||
var ranked = catalogue.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 centres = 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 - centres[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));
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,9 @@ public class HoleLookAheadTests
|
||||
[Fact]
|
||||
public void CrossingPreferredHolePathBacktracksToACheckedAlternative()
|
||||
{
|
||||
// Calibrated: the preferred rapid from hole 1 to hole 2 touches already-cut hole 3.
|
||||
var centres = new[] { new Vector(3, 3), new Vector(7, 15), new Vector(3, 7), new Vector(3, 11) };
|
||||
// Calibrated for next-facing circle entries: the preferred rapid from hole 1
|
||||
// to hole 2 touches already-cut hole 3; retain checked entry backtracking.
|
||||
var centres = new[] { new Vector(3, 3), new Vector(6, 15), new Vector(3, 7), new Vector(3, 11) };
|
||||
var clean = LeadPathValidationTests.Rectangle(0, 0, 18, 18);
|
||||
foreach (var c in centres)
|
||||
{
|
||||
|
||||
@@ -107,6 +107,22 @@ and tab trimming happen during emission. Validation uses the actual emitted
|
||||
motions, never the nominal entry point alone. Existing lead styles are not
|
||||
shortened, disabled or substituted as a search fallback.
|
||||
|
||||
Whole-circle candidates rank by distance to their next-cut target; for a hole,
|
||||
that is the next contour's actual pierce. They do not reward a diagonal point as
|
||||
if it were a bounding-box corner. All eight
|
||||
compass options remain available, including the four polar points at 0°, 90°,
|
||||
180° and 270°, and every alternative still passes the emitted-lead and rapid checks.
|
||||
Without a next-cut target, the arrival-based rule is unchanged. Polygon corner
|
||||
preferences are unchanged.
|
||||
|
||||
`Round Lead-In Angles` remains an explicit cutting setting: a 90° increment snaps
|
||||
circular-hole starts to the four polar directions; 45° also permits diagonals.
|
||||
Snapping can increase reuse of identical hole subprograms and reduce output for
|
||||
posts that support that reuse, at the cost of a less direct departure. It is not
|
||||
a clearance exemption or a guaranteed file-size reduction. The planner does not
|
||||
silently enable rounding, change the increment, or bypass checks on the rounded
|
||||
motions.
|
||||
|
||||
Every candidate rapid is checked against contours already completed, including
|
||||
earlier holes in the same part. Future contours are not yet obstacles. Rapid
|
||||
checks skip completed contours whose extents are more than 0.001 clear of the
|
||||
|
||||
Reference in new issue
Block a user