diff --git a/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs b/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs index a8f3086..1ee9e2b 100644 --- a/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs +++ b/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs @@ -13,7 +13,8 @@ namespace OpenNest.Engine.CuttingPlanning; /// Plans whole parts and their emitted contour prefixes along a part order. A preserved order is /// followed with full backtracking. Otherwise the order comes from ; /// when a part on it cannot be reached without crossing parts already cut, the search learns -/// "cut this part before those", keeps the parts cut before them and re-plans the rest. +/// "cut this part before those", keeps the parts cut before them and re-plans the rest. Once +/// nothing new can be learned, the remaining budget goes to a full search over every ready part. /// internal static class JointCuttingPlanSearch { @@ -52,7 +53,12 @@ internal static class JointCuttingPlanSearch if (attempt.Order != null) return walk.Ready(attempt.Order); if (!Learn(attempt, prerequisites)) - return walk.Exhausted(); + { + // "Cut before" rules are a heuristic: a part blocked straight after another can + // still be reachable via a third. Search every order with what budget remains. + var full = walk.Follow(null, null, null); + return full.Order != null ? walk.Ready(full.Order) : walk.Exhausted(); + } // Back up to just before the earliest part the blocked approach crossed, keep the // parts cut before it, and re-plan the rest from where the tool is at that point. var back = attempt.Crossed.Min(part => Array.IndexOf(sequence, part)); @@ -154,7 +160,8 @@ internal static class JointCuttingPlanSearch } /// - /// Follows from (or the start point). + /// Follows from (or the start point); + /// a null sequence tries every dependency-ready part, nearest first. /// With a stall limit the attempt ends once that many expansions pass without getting /// further along the order; it never backtracks behind its starting node. /// @@ -199,8 +206,11 @@ internal static class JointCuttingPlanSearch else { var finished = node.Order.Select(o => o.SourceOrdinal).ToHashSet(); - var next = snapshot.Placements[sequence[node.Order.Length]]; - sources = snapshot.Dependencies.IsReady(next.SourceOrdinal, finished) ? [next] : []; + sources = sequence == null + ? snapshot.Placements.Where(p => !finished.Contains(p.SourceOrdinal) + && snapshot.Dependencies.IsReady(p.SourceOrdinal, finished)) + : snapshot.Dependencies.IsReady(sequence[node.Order.Length], finished) + ? [snapshot.Placements[sequence[node.Order.Length]]] : []; } foreach (var source in sources) { @@ -296,7 +306,7 @@ internal static class JointCuttingPlanSearch private Node Deepest { get; set; } /// The ordinal of the part the attempt could not get past, or null. - internal int? Blocked => Deepest == null || Deepest.Order.Length >= sequence.Length ? null + internal int? Blocked => sequence == null || Deepest == null || Deepest.Order.Length >= sequence.Length ? null : sequence[Deepest.Order.Length]; /// Parts whose completed contours the blocked part's motions crossed (it can be among them). diff --git a/OpenNest.Tests/CuttingPlanning/ReorderSearchTests.cs b/OpenNest.Tests/CuttingPlanning/ReorderSearchTests.cs index ea65636..634ee08 100644 --- a/OpenNest.Tests/CuttingPlanning/ReorderSearchTests.cs +++ b/OpenNest.Tests/CuttingPlanning/ReorderSearchTests.cs @@ -76,6 +76,24 @@ public class ReorderSearchTests Assert.True(result.IndependentlyReplayed); } + [Fact] + public void FreeOrder_LearnedOrderContradicts_FallsBackToTheFullSearch() + { + // Left leads on its left, right on its right: right straight after left crosses left, and + // left straight after right crosses right, so "cut before" rules contradict each other. + // Going via the part above, which leaves downward, reaches right safely. + var left = LeftLeadRectangle("left", 0, 0, 2, 2); + var right = LeftLeadRectangle("right", 4, 0, 2, 2, mirror: true); + var via = LeftLeadRectangle("via", -1, 5, 2, 2, departure: new Vector(-0.25, -0.25)); + + var result = CuttingPlanService.Plan(new CuttingPlanRequest([left, right, via], + confirmedParameters: ExplicitContourTests.Parameters())); + + Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result)); + Assert.True(result.IndependentlyReplayed); + Assert.Equal(new[] { left, via, right }, result.ProposedOrder.Select(p => p.SourcePart)); + } + [Fact] public void FreeOrder_NoSafeOrder_RefusesWithTheCrossing() { @@ -107,9 +125,9 @@ public class ReorderSearchTests private static (double, double) Key(Part part) => (part.Location.X, part.Location.Y); // A locked rectangle whose lead-in and lead-out sit 0.25 outside its left edge (its right - // edge when mirrored), so the tool departs on that side. + // edge when mirrored), so the tool departs on that side unless a final rapid moves it on. private static Part LeftLeadRectangle(string name, double x, double y, double width, double height, - bool mirror = false) + bool mirror = false, Vector? departure = null) { var clean = LeadPathValidationTests.Rectangle(0, 0, width, height); var part = new Part(new Drawing(name, clean), new Vector(x, y)); @@ -131,6 +149,8 @@ public class ReorderSearchTests } placed.LineTo(edge, height / 2); placed.Codes.Add(new LinearMove(outside, height / 2) { Layer = LayerType.Leadout }); + if (departure is { } end) + placed.MoveTo(end.X, end.Y); Assert.True(part.RestoreLeadInProgram(placed, true)); return part; } diff --git a/docs/cutting-planner.md b/docs/cutting-planner.md index d59c281..08abdf6 100644 --- a/docs/cutting-planner.md +++ b/docs/cutting-planner.md @@ -88,7 +88,11 @@ before those", backs up to just before the earliest of them and re-plans the res from the tool position there; parts cut before that point are kept. An attempt stops backtracking after a stall of 8 x entries x contours expansions without getting further, so it learns instead of retrying every entry combination of the -parts before it. When nothing new can be learned the result is a refusal. +parts before it. A rule that would contradict an order already required is skipped. +"Cut before" rules are a heuristic (a part blocked straight after another may be +reachable via a third), so once nothing new can be learned the remaining budget +goes to a full search that tries every ready part, nearest first; only when that +also fails is the result a refusal. Candidates use native closest points, vertices, midpoints and circle angles in stable order, capped by `maxEntries`. Circle rounding, clamping, corner resolution