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.
This commit is contained in:
aj committed 2026-10-05 23:41:16 -04:00
1 parent b8f3d3bf0a
commit 2419a9e2e3
3 files changed
+34 -16

No files matched your search

@@ -33,14 +33,15 @@ public sealed record CuttingPlanProgress(int PlateIndex, int PlateCount, int Pla
/// </summary>
public sealed class CuttingPlanBatch
{
/// <summary>Budget of a free-order search, the service default.</summary>
public const int ReorderExpansionBudget = 20000;
/// <summary>The smallest budget a plate gets, the service default.</summary>
public const int MinimumExpansionBudget = 20000;
/// <summary>
/// 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.
/// </summary>
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
/// </summary>
public static CuttingPlanBatch Capture(IReadOnlyList<Plate> plates, CuttingParameters confirmedParameters,
bool preservePartOrder, IReadOnlyList<int> plateNumbers = null, CancellationToken token = default) =>
Capture(plates, confirmedParameters, preservePartOrder, plateNumbers, ReorderExpansionBudget, token);
Capture(plates, confirmedParameters, preservePartOrder, plateNumbers, null, token);
/// <param name="reorderBudget">Overrides the new-order budget (tests); null gives <see cref="PlateBudget"/>.</param>
internal static CuttingPlanBatch Capture(IReadOnlyList<Plate> plates, CuttingParameters confirmedParameters,
bool preservePartOrder, IReadOnlyList<int> plateNumbers, int reorderBudget, CancellationToken token)
bool preservePartOrder, IReadOnlyList<int> 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));
/// <summary>
/// Checks and plans every plate from its captured snapshots. Safe on a worker: live plates are
@@ -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()
+2 -2
View File
@@ -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