From 2419a9e2e3a4c3d64ab1889385fb6211020b0254 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 5 Oct 2026 23:41:16 -0400 Subject: [PATCH] fix(cutting): give a new part order the plate's expansion budget The desktop batch searched for a new part order with the flat 20000 default and kept the current order with 400 expansions per part. A new order now also plans contour order and entries for every part (about 260 expansions per part on a dense grid), so a 100-part plate ran out of budget and fell back to the current order. Both attempts now get PlateBudget: 400 per part, at least 20000. The constants are renamed MinimumExpansionBudget and ExpansionsPerPart to match. --- .../CuttingPlanning/CuttingPlanBatch.cs | 26 ++++++++++--------- .../CuttingPlanning/CuttingPlanBatchTests.cs | 20 ++++++++++++-- docs/cutting-planner.md | 4 +-- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs b/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs index 9cd266e..341d4d3 100644 --- a/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs +++ b/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs @@ -33,14 +33,15 @@ public sealed record CuttingPlanProgress(int PlateIndex, int PlateCount, int Pla /// public sealed class CuttingPlanBatch { - /// Budget of a free-order search, the service default. - public const int ReorderExpansionBudget = 20000; + /// The smallest budget a plate gets, the service default. + public const int MinimumExpansionBudget = 20000; /// - /// Expansions allowed per part when the order is kept. Measured near 210 per part on a dense - /// grid; the margin keeps 100-150 part plates inside the budget. + /// Expansions allowed per part, whether or not the order is kept: both still choose contour + /// order and entries for every part. Dense grids measured near 260 per part with a new order + /// and 210 when kept; the margin keeps 100-150 part plates inside the budget. /// - public const int KeepOrderExpansionsPerPart = 400; + public const int ExpansionsPerPart = 400; private readonly Entry[] entries; private readonly CuttingParameters ownedParameters; @@ -61,10 +62,11 @@ public sealed class CuttingPlanBatch /// public static CuttingPlanBatch Capture(IReadOnlyList plates, CuttingParameters confirmedParameters, bool preservePartOrder, IReadOnlyList plateNumbers = null, CancellationToken token = default) => - Capture(plates, confirmedParameters, preservePartOrder, plateNumbers, ReorderExpansionBudget, token); + Capture(plates, confirmedParameters, preservePartOrder, plateNumbers, null, token); + /// Overrides the new-order budget (tests); null gives . internal static CuttingPlanBatch Capture(IReadOnlyList plates, CuttingParameters confirmedParameters, - bool preservePartOrder, IReadOnlyList plateNumbers, int reorderBudget, CancellationToken token) + bool preservePartOrder, IReadOnlyList plateNumbers, int? reorderBudget, CancellationToken token) { ArgumentNullException.ThrowIfNull(plates); ArgumentNullException.ThrowIfNull(confirmedParameters); @@ -90,21 +92,21 @@ public sealed class CuttingPlanBatch for (var index = 0; index < plates.Count; index++) { var plate = plates[index]; - var keepOrderBudget = KeepOrderBudget(plate.Parts.Count); + var budget = PlateBudget(plate.Parts.Count); var keepOrder = CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate, - expansionBudget: keepOrderBudget, confirmedParameters: confirmedParameters, + expansionBudget: budget, confirmedParameters: confirmedParameters, preservePartOrder: true), token); var reorder = preservePartOrder ? null : CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate, - expansionBudget: reorderBudget, confirmedParameters: confirmedParameters), token); + expansionBudget: reorderBudget ?? budget, confirmedParameters: confirmedParameters), token); var overlap = PlateOverlapAnalyzer.Capture(plate.Parts.ToArray(), token); entries[index] = new(plate, plateNumbers?[index] ?? index + 1, reorder, keepOrder, overlap); } return new(entries, owned); } - internal static int KeepOrderBudget(int partCount) => + internal static int PlateBudget(int partCount) => (int)System.Math.Min(int.MaxValue, - System.Math.Max((long)ReorderExpansionBudget, (long)partCount * KeepOrderExpansionsPerPart)); + System.Math.Max((long)MinimumExpansionBudget, (long)partCount * ExpansionsPerPart)); /// /// Checks and plans every plate from its captured snapshots. Safe on a worker: live plates are diff --git a/OpenNest.Tests/CuttingPlanning/CuttingPlanBatchTests.cs b/OpenNest.Tests/CuttingPlanning/CuttingPlanBatchTests.cs index cc8188a..d889c4c 100644 --- a/OpenNest.Tests/CuttingPlanning/CuttingPlanBatchTests.cs +++ b/OpenNest.Tests/CuttingPlanning/CuttingPlanBatchTests.cs @@ -273,8 +273,24 @@ public class CuttingPlanBatchTests [InlineData(10, 20000)] [InlineData(100, 40000)] [InlineData(6_000_000, int.MaxValue)] - public void KeepOrderBudget_ScalesWithPartCountFromTheDefault(int parts, int expected) => - Assert.Equal(expected, CuttingPlanBatch.KeepOrderBudget(parts)); + public void PlateBudget_ScalesWithPartCountFromTheDefault(int parts, int expected) => + Assert.Equal(expected, CuttingPlanBatch.PlateBudget(parts)); + + [Fact] + public void Plan_LargeGrid_FindsANewOrderWithinThePlateBudget() + { + // Row-major 10 x 10 needs more than the 20000 default to reorder (about 26000). + var nest = new Nest(); + var plate = Plate(nest, Grid(100)); + plate.Size = new Size(120, 120); + + var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan(); + + var planned = Assert.Single(proposal.Plates); + Assert.True(planned.IsReady, string.Join("\n", proposal.Describe("in"))); + Assert.False(planned.KeptCurrentOrder); + Assert.Equal(100, planned.PartCount); + } [Fact] public void Capture_RejectsMissingDuplicateOrMisnumberedPlates() diff --git a/docs/cutting-planner.md b/docs/cutting-planner.md index 2dbaaf1..d59c281 100644 --- a/docs/cutting-planner.md +++ b/docs/cutting-planner.md @@ -219,8 +219,8 @@ plans every plate that has parts. Both open one dialog built on an incomplete check (see [pre-post verification](post-verification.md)) block that plate whatever its route. A free-order search that ends `NoSolutionWithinBudget` is retried once with the current part order, and the summary - says the order was kept. A kept order is allowed 400 expansions per part (at - least the default 20000), because it still searches contour order and entries. + says the order was kept. Both are allowed 400 expansions per part (at least the + default 20000), because both still plan contour order and entries for every part. - The summary lists every plate: ready plates with part counts and rapid travel, others with their status and findings. Finding part numbers are the plate's current order, as the editor numbers them. The preview shows the active plate detached from the nest (quantity