mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-13 12:31:55 -04:00
249 lines
12 KiB
C#
249 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingPlanning;
|
|
using OpenNest.CNC.CuttingStrategy;
|
|
using OpenNest.Engine.CuttingPlanning;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests.CuttingPlanning;
|
|
|
|
public class SideCoverageRegressionTests
|
|
{
|
|
[Fact]
|
|
public void DefaultCap_ReusesClearBottomCandidatePassedWhileSeekingLeft()
|
|
{
|
|
var clean = new Program();
|
|
clean.MoveTo(0, 0);
|
|
clean.LineTo(0, 10);
|
|
for (var i = 1; i <= 12; i++)
|
|
clean.LineTo(i * 10.0 / 12, 10);
|
|
for (var i = 1; i <= 12; i++)
|
|
clean.LineTo(10, 10 - i * 10.0 / 12);
|
|
clean.LineTo(0, 0);
|
|
var parameters = Parameters(0.15, 90, 0);
|
|
var prepared = PreparedContours.Capture(clean, parameters);
|
|
var blockers = new[]
|
|
{
|
|
Rectangle(-0.5, -0.5, 1.5, 0.4), Rectangle(9, -0.5, 1.05, 0.4),
|
|
Rectangle(10.1, -0.3, 0.5, 0.6), Rectangle(-0.5, 9.8, 0.4, 0.4),
|
|
};
|
|
var target = new Vector(20, 20);
|
|
var ranked = prepared.AutomaticEntryCandidatesWithFallbacks(0, target)
|
|
.RankTowardNextCut(target, new Vector(10, -2));
|
|
var adapter = Adapter(prepared, clean, blockers);
|
|
var calls = new HashSet<(long, long)>();
|
|
|
|
var selected = ContourEntrySelection.Select(ranked, candidate =>
|
|
{
|
|
Assert.True(calls.Add(candidate.GeometryKey), "A candidate was evaluated twice.");
|
|
return adapter.Check(candidate.Choice);
|
|
});
|
|
|
|
Assert.Equal(16, selected.Choices.Count);
|
|
var bottom = ranked.Single(c => c.Choice.Point.DistanceTo(new Vector(5, 0)) < 1e-9);
|
|
Assert.Equal(32, ranked.ToList().IndexOf(bottom));
|
|
Assert.True(adapter.Check(bottom.Choice).IsClear);
|
|
Assert.Equal(new[] { 0, 1, 2, 3 }, ClearSides(ranked, adapter));
|
|
Assert.Equal(new[] { 0, 1, 2, 3 }, SelectedSides(selected, ranked));
|
|
Assert.Contains(selected.Choices, c => c.Point.DistanceTo(bottom.Choice.Point) < 1e-9);
|
|
Assert.All(selected.Choices, c => Assert.True(adapter.Check(c).IsClear));
|
|
Assert.Equal(ContourSelectionShortfall.None, selected.Shortfall);
|
|
AssertGlobalOrder(selected, ranked);
|
|
Assert.Equal(calls.Count, selected.EvaluatedCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void SupportedCapFour_ReusesClearTopCandidatePassedWhileSeekingBottom()
|
|
{
|
|
var clean = Polygon();
|
|
var prepared = PreparedContours.Capture(clean, Parameters());
|
|
var target = new Vector(100 * System.Math.Cos(System.Math.PI), 100 * System.Math.Sin(System.Math.PI));
|
|
var arrival = new Vector(40 * System.Math.Cos(3 * System.Math.PI / 2), 40 * System.Math.Sin(3 * System.Math.PI / 2));
|
|
var ranked = prepared.AutomaticEntryCandidatesWithFallbacks(0, target).RankTowardNextCut(target, arrival);
|
|
var adapter = Adapter(prepared, clean, []);
|
|
|
|
var selected = ContourEntrySelection.Select(ranked, c => adapter.Check(c.Choice), 4);
|
|
|
|
Assert.Equal(4, selected.Choices.Count);
|
|
Assert.True(adapter.Check(ranked[4].Choice).IsClear);
|
|
Assert.Contains(3, Sides(ranked[4].Choice.Point, ranked));
|
|
Assert.Equal(new[] { 0, 1, 2, 3 }, ClearSides(ranked, adapter));
|
|
Assert.Equal(new[] { 0, 1, 2, 3 }, SelectedSides(selected, ranked));
|
|
Assert.All(selected.Choices, c => Assert.True(adapter.Check(c).IsClear));
|
|
AssertGlobalOrder(selected, ranked);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(4)]
|
|
[InlineData(16)]
|
|
public void FullCheckedRoute_RetainsTopDepartureToLockedMarkedSquare(int cap)
|
|
{
|
|
var parameters = Parameters();
|
|
var first = new Part(new Drawing("convex polygon", Polygon()));
|
|
var nextClean = Rectangle(0, 0, 2, 2);
|
|
nextClean.MoveTo(101, 21);
|
|
nextClean.Codes.Add(new LinearMove(102, 21) { Layer = LayerType.Scribe });
|
|
var next = new Part(new Drawing("locked marked square", nextClean), new Vector(-101, -1));
|
|
var nextPrepared = PreparedContours.Capture(nextClean, parameters);
|
|
Assert.True(next.RestoreLeadInProgram(nextPrepared.Emit([nextPrepared.ClosestEntry(0, new Vector(3, 1))]), true));
|
|
var before = new[] { ExplicitContourTests.Fingerprint(first.Program), ExplicitContourTests.Fingerprint(next.Program) };
|
|
var start = new Vector(-7.347880794884118e-15, -40);
|
|
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([first, next], start,
|
|
confirmedParameters: parameters, preservePartOrder: true, maxEntries: cap));
|
|
var source = snapshot.Placements[0];
|
|
var prepared = source.Prepared;
|
|
var witness = prepared.AutomaticEntryCandidatesWithFallbacks(0, new Vector(-100, 0))
|
|
.First(c => c.Choice.Point.DistanceTo(new Vector(-4.99220639970189, 8.664749001718139)) < 1e-9);
|
|
var witnessProgram = prepared.Emit([witness.Choice]);
|
|
var witnessExecution = ExecutionMotionReader.Read(witnessProgram, source.Location, start, default);
|
|
var witnessProposal = source.Propose(witnessProgram, witnessExecution, [witness.Choice], default);
|
|
var witnessReplay = CuttingPlanService.ReplayPrograms(snapshot, [witnessProposal, snapshot.Placements[1]], 0, default);
|
|
Assert.Equal(CuttingPlanStatus.Ready, witnessReplay.Status);
|
|
Assert.True(witnessReplay.IndependentlyReplayed);
|
|
|
|
var result = CuttingPlanService.Plan(snapshot);
|
|
|
|
Assert.True(result.Status == CuttingPlanStatus.Ready,
|
|
$"{result.Status}, {result.Expansions} expansions: {string.Join("; ", result.Findings.Select(f => f.Message))}");
|
|
Assert.True(result.IndependentlyReplayed);
|
|
Assert.Equal(new[] { 0, 1 }, result.ProposedOrder.Select(p => p.SourceOrdinal));
|
|
Assert.Single(result.ProposedOrder[0].ContourChoices);
|
|
Assert.False(result.ProposedOrder[1].IsRegenerated);
|
|
var replay = CuttingPlanService.ReplayPrograms(snapshot, result.ProposedOrder, 0, default);
|
|
Assert.Equal(CuttingPlanStatus.Ready, replay.Status);
|
|
Assert.True(replay.IndependentlyReplayed);
|
|
Assert.Equal(before, new[] { ExplicitContourTests.Fingerprint(first.Program), ExplicitContourTests.Fingerprint(next.Program) });
|
|
}
|
|
|
|
[Fact]
|
|
public void CorrectiveRescan_PreservesGlobalRankAndLeavesTailUnevaluated()
|
|
{
|
|
var candidates = CorrectiveCandidates();
|
|
var calls = new HashSet<(long, long)>();
|
|
var result = ContourEntrySelection.Select(candidates, c =>
|
|
{
|
|
Assert.True(calls.Add(c.GeometryKey));
|
|
return new(ContourFeasibilityStatus.Clear, null);
|
|
}, 4);
|
|
|
|
Assert.Equal(new[] { candidates[0].Choice, candidates[1].Choice, candidates[4].Choice, candidates[5].Choice }, result.Choices);
|
|
Assert.Equal(6, result.EvaluatedCount);
|
|
Assert.DoesNotContain(candidates[6].GeometryKey, result.EvaluatedKeys);
|
|
Assert.Equal(ContourSelectionShortfall.None, result.Shortfall);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(ContourFeasibilityStatus.Blocked)]
|
|
[InlineData(ContourFeasibilityStatus.Incomplete)]
|
|
public void CorrectiveRescan_DoesNotPromoteUnclearSide(ContourFeasibilityStatus status)
|
|
{
|
|
var candidates = CorrectiveCandidates();
|
|
var calls = new HashSet<(long, long)>();
|
|
var result = ContourEntrySelection.Select(candidates, c =>
|
|
{
|
|
Assert.True(calls.Add(c.GeometryKey));
|
|
return c.GeometryKey == candidates[4].GeometryKey
|
|
? new(status, "unclear bottom") : new(ContourFeasibilityStatus.Clear, null);
|
|
}, 4);
|
|
|
|
Assert.DoesNotContain(candidates[4].Choice, result.Choices);
|
|
Assert.Equal(4, result.Choices.Count);
|
|
Assert.Equal(status == ContourFeasibilityStatus.Incomplete ? ContourSelectionShortfall.Incomplete : ContourSelectionShortfall.None, result.Shortfall);
|
|
if (status == ContourFeasibilityStatus.Incomplete)
|
|
{
|
|
Assert.Contains(candidates[4].Choice, result.UncertainChoices);
|
|
Assert.DoesNotContain("No tested lead-in fits", result.Reason);
|
|
}
|
|
Assert.Equal(calls.Count, result.EvaluatedCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CorrectiveRescan_HonoursCancellationBeforeReusingCachedVerdict()
|
|
{
|
|
var candidates = CorrectiveCandidates();
|
|
using var cancellation = new CancellationTokenSource();
|
|
Assert.ThrowsAny<OperationCanceledException>(() => ContourEntrySelection.Select(candidates, c =>
|
|
{
|
|
if (c.GeometryKey == candidates[5].GeometryKey)
|
|
cancellation.Cancel();
|
|
return new(ContourFeasibilityStatus.Clear, null);
|
|
}, 4, cancellation.Token));
|
|
}
|
|
|
|
private static ContourEntryCandidate[] CorrectiveCandidates()
|
|
{
|
|
var prepared = PreparedContours.Capture(Rectangle(0, 0, 10, 10), Parameters());
|
|
return new[] { (10, 5), (5, 10), (9, 5), (5, 9), (5, 0), (0, 5), (6, 1) }
|
|
.Select(p => new ContourEntryCandidate(new ContourChoice(0, 0, new Vector(p.Item1, p.Item2)) { Owner = prepared }, AutomaticEntryKind.ConvexCorner)).ToArray();
|
|
}
|
|
|
|
private static ContourEntryFeasibility Adapter(PreparedContours prepared, Program clean, Program[] blockers)
|
|
{
|
|
var own = LeadMaterialSnapshot.Capture(clean, Vector.Zero);
|
|
var others = blockers.Select(p => LeadMaterialSnapshot.Capture(p, Vector.Zero)).ToArray();
|
|
Assert.True(own.IsComplete, own.Reason);
|
|
Assert.All(others, m => Assert.True(m.IsComplete, m.Reason));
|
|
return new(prepared, Vector.Zero, own, others);
|
|
}
|
|
|
|
private static int[] ClearSides(IReadOnlyList<ContourEntryCandidate> ranked, ContourEntryFeasibility adapter) =>
|
|
ranked.Where(c => adapter.Check(c.Choice).IsClear).SelectMany(c => Sides(c.Choice.Point, ranked)).Distinct().Order().ToArray();
|
|
|
|
private static int[] SelectedSides(ContourSelectionResult selected, IReadOnlyList<ContourEntryCandidate> ranked) =>
|
|
selected.Choices.SelectMany(c => Sides(c.Point, ranked)).Distinct().Order().ToArray();
|
|
|
|
private static IEnumerable<int> Sides(Vector point, IReadOnlyList<ContourEntryCandidate> ranked)
|
|
{
|
|
var gaps = new[]
|
|
{
|
|
point.X - ranked.Min(c => c.Choice.Point.X), ranked.Max(c => c.Choice.Point.X) - point.X,
|
|
point.Y - ranked.Min(c => c.Choice.Point.Y), ranked.Max(c => c.Choice.Point.Y) - point.Y,
|
|
};
|
|
return Enumerable.Range(0, 4).Where(side => gaps[side] <= gaps.Min() + 1e-6);
|
|
}
|
|
|
|
private static void AssertGlobalOrder(ContourSelectionResult selected, IReadOnlyList<ContourEntryCandidate> ranked)
|
|
{
|
|
var ranks = selected.Choices.Select(c => ranked.ToList().FindIndex(candidate => ReferenceEquals(candidate.Choice, c))).ToArray();
|
|
Assert.Equal(ranks.Order(), ranks);
|
|
}
|
|
|
|
private static Program Polygon()
|
|
{
|
|
var points = Enumerable.Range(0, 16).Select(i => new Vector(
|
|
10 * System.Math.Cos(0.13 - i * 2 * System.Math.PI / 16),
|
|
10 * System.Math.Sin(0.13 - i * 2 * System.Math.PI / 16))).ToArray();
|
|
var program = new Program();
|
|
program.MoveTo(points[0].X, points[0].Y);
|
|
foreach (var point in points.Skip(1).Append(points[0]))
|
|
program.LineTo(point.X, point.Y);
|
|
return program;
|
|
}
|
|
|
|
private static Program Rectangle(double x, double y, double width, double height)
|
|
{
|
|
var program = new Program();
|
|
program.MoveTo(x, y);
|
|
program.LineTo(x, y + height);
|
|
program.LineTo(x + width, y + height);
|
|
program.LineTo(x + width, y);
|
|
program.LineTo(x, y);
|
|
return program;
|
|
}
|
|
|
|
private static CuttingParameters Parameters(double length = 0.3, double angle = 45, double clearance = 0.05) => new()
|
|
{
|
|
ExternalLeadIn = new LineLeadIn { Length = length, ApproachAngle = angle },
|
|
InternalLeadIn = new LineLeadIn { Length = length, ApproachAngle = angle },
|
|
ArcCircleLeadIn = new LineLeadIn { Length = length },
|
|
ExternalLeadOut = new NoLeadOut(),
|
|
InternalLeadOut = new NoLeadOut(),
|
|
TabsEnabled = false,
|
|
RoundLeadInAngles = false,
|
|
PierceClearance = clearance,
|
|
};
|
|
}
|