From ddc4710dfdc6243f6cf3e815f379c864a7f27a6b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 11 Oct 2026 20:26:47 -0400 Subject: [PATCH] perf(cutting): defer alternate hole entries in preferred pass --- .../CuttingPlanning/JointCuttingPlanSearch.cs | 18 ++++++++++++------ .../CuttingPlanning/HoleEndpointBudgetTests.cs | 6 ++++++ docs/cutting-planner.md | 11 +++++++---- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs b/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs index aafeedf..7461cc6 100644 --- a/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs +++ b/OpenNest.Engine/CuttingPlanning/JointCuttingPlanSearch.cs @@ -402,12 +402,18 @@ internal static class JointCuttingPlanSearch .FirstOrDefault(c => !choices.Any(e => e.ContourOrdinal == c), -1); var target = downstream < 0 ? preference.PerimeterPierce : preference.Pierces[downstream]; - entries = SelectEntries(source, contour, target, node.Position - source.Location); - // Preference only reorders retained candidates. Neither a failed - // preferred entry nor an alternate hole order prunes this branch. - if (preference.Entries.TryGetValue(contour, out var preferred)) - entries = entries.OrderBy(e => e.Point.DistanceTo(preferred.Point) - <= PostVerificationGeometry.Epsilon ? 0 : 1).ToArray(); + // The coarse pass already checked one owned, lead-feasible entry + // for this hole. Do not prepare every alternate before trying it. + // The subsequent full pass still enumerates and checks them all. + if (preferredOnly && preference.Entries.TryGetValue(contour, out var first)) + entries = [first]; + else + { + entries = SelectEntries(source, contour, target, node.Position - source.Location); + if (preference.Entries.TryGetValue(contour, out var preferred)) + entries = entries.OrderBy(e => e.Point.DistanceTo(preferred.Point) + <= PostVerificationGeometry.Epsilon ? 0 : 1).ToArray(); + } } } else diff --git a/OpenNest.Tests/CuttingPlanning/HoleEndpointBudgetTests.cs b/OpenNest.Tests/CuttingPlanning/HoleEndpointBudgetTests.cs index b188e2c..53d842b 100644 --- a/OpenNest.Tests/CuttingPlanning/HoleEndpointBudgetTests.cs +++ b/OpenNest.Tests/CuttingPlanning/HoleEndpointBudgetTests.cs @@ -55,6 +55,12 @@ public class HoleEndpointBudgetTests Assert.Contains(checker.Check(snapshot.Placements[1].Execution, execution.DeparturePoint, 2), f => f.Kind == PostVerificationKind.RapidCrossing); + var measured = JointCuttingPlanSearch.Run(snapshot, default); + // The untouched baseline spent 63 prechecks in both modes because it evaluated + // alternates even in the single-chain pass. The fallback remains available. + Assert.Equal(CuttingPlanStatus.Ready, measured.Status); + Assert.True(measured.LeadPrechecks < 63, + $"Expected fewer prechecks than the baseline's 63; got {measured.LeadPrechecks}."); var result = CuttingPlanService.Plan(snapshot); Assert.True(result.Status == CuttingPlanStatus.Ready, diff --git a/docs/cutting-planner.md b/docs/cutting-planner.md index 491d74e..53bb6f7 100644 --- a/docs/cutting-planner.md +++ b/docs/cutting-planner.md @@ -97,10 +97,13 @@ revisit an earlier entry when a later part cannot be reached safely. For multi-p requests containing regenerated holes, it first tries one ranked hole chain for each outside endpoint. This lets a later blocked approach change the previous part's departure without first exhausting combinations of its earlier holes. -This preferred pass uses the same emitted-motion checks, shared expansion budget -and stall limit as the retained search; on failure the full entry/hole-order -backtracking pass remains available. Before that full pass, a failed preferred tour -also gets a directed-contour recovery pass for parts with at most 16 holes. It +This preferred pass uses the hole centres to propose an order and checks only the +one previously lead-feasible entry selected for each hole; it does not prepare +alternative entries until a subsequent recovery/full pass needs them. It still +checks emitted motions under the shared expansion budget and stall limit, and +on failure the full entry/hole-order backtracking pass remains available. A failed +preferred tour first gets a directed-contour recovery pass for parts with at most +16 holes. It tries native outside entries beyond the ranked shortlist, then builds a hole route from actual emitted pierces, checking each transition against already released contours. This reaches inner edges of interlocking concave parts which can be