diff --git a/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs b/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs
index db72f054..c466db25 100644
--- a/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs
+++ b/OpenNest.Engine/CuttingPlanning/CuttingPlanBatch.cs
@@ -43,7 +43,18 @@ public sealed class CuttingPlanBatch
///
public const int ExpansionsPerPart = 400;
+ /// The planning-effort multiplier that gives today's budgets.
+ public const double DefaultBudgetMultiplier = 1.0;
+
+ /// The smallest planning-effort multiplier; it still leaves the first pass room to run.
+ public const double MinimumBudgetMultiplier = 0.5;
+
+ /// The largest planning-effort multiplier, a bound on one plate's planning time.
+ public const double MaximumBudgetMultiplier = 10.0;
+
// Extra bounded work on hard/refused plates; the full-cap search still gets its whole budget.
+ // Not scaled by the multiplier: it is a fixed-cost probe for easy plates, so raising the
+ // effort spends more only where the full search runs and leaves easy plates' plans unchanged.
internal const int FirstPassBudget = 1000;
internal const int FirstPassEntries = 8;
@@ -64,16 +75,26 @@ public sealed class CuttingPlanBatch
/// current-order request is captured too, so a free search that runs out of budget can be
/// retried on the worker without reading live plates again.
///
+ ///
+ /// Scales both searches' , between
+ /// and . The caller passes the operator's setting; the
+ /// worker never reads it.
+ ///
public static CuttingPlanBatch Capture(IReadOnlyList plates, CuttingParameters confirmedParameters,
- bool preservePartOrder, IReadOnlyList plateNumbers = null, CancellationToken token = default) =>
- Capture(plates, confirmedParameters, preservePartOrder, plateNumbers, null, token);
+ bool preservePartOrder, IReadOnlyList plateNumbers = null, CancellationToken token = default,
+ double budgetMultiplier = DefaultBudgetMultiplier) =>
+ Capture(plates, confirmedParameters, preservePartOrder, plateNumbers, null, token, budgetMultiplier);
/// 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,
+ double budgetMultiplier = DefaultBudgetMultiplier)
{
ArgumentNullException.ThrowIfNull(plates);
ArgumentNullException.ThrowIfNull(confirmedParameters);
+ if (!(budgetMultiplier >= MinimumBudgetMultiplier && budgetMultiplier <= MaximumBudgetMultiplier))
+ throw new ArgumentOutOfRangeException(nameof(budgetMultiplier), budgetMultiplier,
+ $"The budget multiplier must be between {MinimumBudgetMultiplier} and {MaximumBudgetMultiplier}.");
if (plates.Count == 0)
throw new ArgumentException("At least one plate is required.", nameof(plates));
if (plates.Any(plate => plate == null)
@@ -96,7 +117,7 @@ public sealed class CuttingPlanBatch
for (var index = 0; index < plates.Count; index++)
{
var plate = plates[index];
- var budget = PlateBudget(plate.Parts.Count);
+ var budget = PlateBudget(plate.Parts.Count, budgetMultiplier);
var keepOrder = CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate,
expansionBudget: budget, confirmedParameters: confirmedParameters,
preservePartOrder: true), token);
@@ -112,9 +133,21 @@ public sealed class CuttingPlanBatch
return new(entries, owned);
}
- internal static int PlateBudget(int partCount) =>
- (int)System.Math.Min(int.MaxValue,
- System.Math.Max((long)MinimumExpansionBudget, (long)partCount * ExpansionsPerPart));
+ /// A plate's expansion budget: per part, at least
+ /// , times the multiplier.
+ internal static int PlateBudget(int partCount, double multiplier = DefaultBudgetMultiplier)
+ {
+ var budget = System.Math.Max((double)MinimumExpansionBudget, (double)partCount * ExpansionsPerPart);
+ return (int)System.Math.Min(int.MaxValue, System.Math.Round(budget * multiplier));
+ }
+
+ ///
+ /// Brings a stored planning-effort value inside the supported range; a non-finite value gives
+ /// .
+ ///
+ public static double ClampBudgetMultiplier(double multiplier) => double.IsFinite(multiplier)
+ ? System.Math.Clamp(multiplier, MinimumBudgetMultiplier, MaximumBudgetMultiplier)
+ : DefaultBudgetMultiplier;
///
/// Checks and plans every plate from its captured snapshots. Safe on a worker: live plates are
@@ -232,6 +265,15 @@ public sealed class CuttingPlanPlateResult
public bool CanApplyWithWarnings => Overlap != null && Overlap.Pairs.Count == 0
&& (IsRouteReady || Result.Status == CuttingPlanStatus.BestEffort);
+ ///
+ /// Refused after a search ran out of its expansion budget: this plan, or the free-order attempt
+ /// before the current order was kept. A higher planning effort searches further, without a promise.
+ ///
+ public bool SearchLimitReached => !CanApplyWithWarnings && !IsRouteReady
+ && (Result.Status == CuttingPlanStatus.NoSolutionWithinBudget || KeptCurrentOrder
+ && Result.Status is not (CuttingPlanStatus.Cancelled or CuttingPlanStatus.UnsupportedGeometry
+ or CuttingPlanStatus.InvalidInput));
+
///
/// The route was replayed and passed every strict check; only fixed emissions (locked or
/// pinned leads) carry warnings, so the proposal is UNVERIFIED and needs acceptance.
@@ -455,8 +497,14 @@ public sealed class CuttingPlanProposal
+ "current order was refused:";
foreach (var line in Limit(result.Findings.Select(DescribeFinding)))
yield return line;
+ if (!missingLead && plate.SearchLimitReached)
+ yield return " " + SearchLimitHint;
}
+ /// Operator hint for a plate refused at the search limit; raising the effort is not a promise.
+ public const string SearchLimitHint = "Raising Cut planning effort in Tools > Options lets the planner search "
+ + "further before it stops; it may still find no plan.";
+
private static IEnumerable Limit(IEnumerable lines)
{
var all = lines.ToList();
diff --git a/OpenNest.Tests/CuttingPlanning/CuttingPlanBatchTests.cs b/OpenNest.Tests/CuttingPlanning/CuttingPlanBatchTests.cs
index ba25dfce..6f47a10c 100644
--- a/OpenNest.Tests/CuttingPlanning/CuttingPlanBatchTests.cs
+++ b/OpenNest.Tests/CuttingPlanning/CuttingPlanBatchTests.cs
@@ -222,6 +222,8 @@ public class CuttingPlanBatchTests
Assert.False(planned.OrderChanged);
Assert.Equal(order, planned.Result.ProposedOrder.Select(p => p.SourcePart));
Assert.Contains("the current order is kept", string.Join("\n", proposal.Describe("in")));
+ Assert.False(planned.SearchLimitReached); // A ready plate is not a refusal.
+ Assert.DoesNotContain(CuttingPlanProposal.SearchLimitHint, string.Join("\n", proposal.Describe("in")));
Assert.True(proposal.CanApply);
}
@@ -335,6 +337,54 @@ public class CuttingPlanBatchTests
public void PlateBudget_ScalesWithPartCountFromTheDefault(int parts, int expected) =>
Assert.Equal(expected, CuttingPlanBatch.PlateBudget(parts));
+ [Theory]
+ [InlineData(0, 1.0, 20000)]
+ [InlineData(100, 1.0, 40000)]
+ [InlineData(10, 0.5, 10000)]
+ [InlineData(10, 2.5, 50000)]
+ [InlineData(100, 10.0, 400000)]
+ [InlineData(6_000_000, 10.0, int.MaxValue)]
+ public void PlateBudget_ScalesWithTheMultiplier(int parts, double multiplier, int expected) =>
+ Assert.Equal(expected, CuttingPlanBatch.PlateBudget(parts, multiplier));
+
+ [Theory]
+ [InlineData(1.0, 20000)]
+ [InlineData(2.5, 50000)]
+ public void Capture_BudgetMultiplier_ScalesTheSearchBudgetItPlansWith(double multiplier, int expected)
+ {
+ var plate = Plate(new Nest(), Clean("a", 1, 1), Clean("b", 12, 1));
+
+ var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), preservePartOrder: true,
+ budgetMultiplier: multiplier).Plan();
+
+ var planned = Assert.Single(proposal.Plates);
+ Assert.True(planned.IsReady);
+ Assert.Equal(expected, planned.Result.Snapshot.ExpansionBudget);
+ }
+
+ [Theory]
+ [InlineData(0.4)]
+ [InlineData(10.1)]
+ [InlineData(double.NaN)]
+ [InlineData(double.PositiveInfinity)]
+ public void Capture_BudgetMultiplierOutOfRange_Throws(double multiplier)
+ {
+ var plate = Plate(new Nest(), Clean("a", 1, 1));
+
+ Assert.Throws(() =>
+ CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false, budgetMultiplier: multiplier));
+ }
+
+ [Theory]
+ [InlineData(1.0, 1.0)]
+ [InlineData(3.5, 3.5)]
+ [InlineData(0.0, 0.5)]
+ [InlineData(50.0, 10.0)]
+ [InlineData(double.NaN, 1.0)]
+ [InlineData(double.NegativeInfinity, 1.0)]
+ public void ClampBudgetMultiplier_KeepsStoredValuesInRange(double stored, double expected) =>
+ Assert.Equal(expected, CuttingPlanBatch.ClampBudgetMultiplier(stored));
+
[Fact]
public void Plan_LargeGrid_FindsANewOrderWithinThePlateBudget()
{
diff --git a/OpenNest.Tests/CuttingPlanning/CuttingPlanMessageTests.cs b/OpenNest.Tests/CuttingPlanning/CuttingPlanMessageTests.cs
index 074d8e56..36d55818 100644
--- a/OpenNest.Tests/CuttingPlanning/CuttingPlanMessageTests.cs
+++ b/OpenNest.Tests/CuttingPlanning/CuttingPlanMessageTests.cs
@@ -161,8 +161,24 @@ public class CuttingPlanMessageTests
Assert.Contains("Plate 7: no complete plan was found within the search limit.", text);
Assert.Contains("planning with the current order was refused:", text);
Assert.Contains(finding.Message, text);
+ Assert.Contains(CuttingPlanProposal.SearchLimitHint, text);
+ Assert.Contains("it may still find no plan", CuttingPlanProposal.SearchLimitHint);
+ Assert.True(proposal.Plates[0].SearchLimitReached);
Assert.DoesNotContain("Open Cutting Settings...", text);
Assert.DoesNotContain("missing or zero-length lead-in", text);
Assert.False(proposal.CanApply);
}
+
+ [Theory]
+ [InlineData(CuttingPlanStatus.UnsupportedGeometry)]
+ [InlineData(CuttingPlanStatus.Cancelled)]
+ public void Describe_KeptOrderRefusalAMoreEffortCannotFix_OmitsTheSearchLimitHint(CuttingPlanStatus status)
+ {
+ var attempt = new CuttingPlanResult(CuttingPlanStatus.NoSolutionWithinBudget);
+ var proposal = new CuttingPlanProposal([new(new Nest().CreatePlate(), 1, new(status), attempt, null)],
+ ExplicitContourTests.Parameters());
+
+ Assert.False(proposal.Plates[0].SearchLimitReached);
+ Assert.DoesNotContain(CuttingPlanProposal.SearchLimitHint, string.Join("\n", proposal.Describe("in")));
+ }
}
diff --git a/OpenNest/Forms/CuttingInspectionPanel.cs b/OpenNest/Forms/CuttingInspectionPanel.cs
index da095fac..68961d48 100644
--- a/OpenNest/Forms/CuttingInspectionPanel.cs
+++ b/OpenNest/Forms/CuttingInspectionPanel.cs
@@ -617,7 +617,8 @@ internal sealed class CuttingInspectionPanel : UserControl
try
{
// Capture on the owner thread; no worker recapture of the live plate or settings.
- var batch = CuttingPlanBatch.Capture(plates, parameters, keepOrderCheckBox.Checked, plateNumbers);
+ var batch = CuttingPlanBatch.Capture(plates, parameters, keepOrderCheckBox.Checked, plateNumbers,
+ budgetMultiplier: CuttingPlanBatch.ClampBudgetMultiplier(Properties.Settings.Default.CuttingPlanEffort));
var run = ++generation;
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
PlanningTask = completion.Task;
@@ -790,6 +791,8 @@ internal sealed class CuttingInspectionPanel : UserControl
return "Move the overlapping parts apart, then Replan.";
if (!plate.IsRouteReady && plate.Result.Findings.Any(f => f.Kind == PostVerificationKind.MissingLeadIn))
return "In Cutting Settings, choose a lead-in with a nonzero length (or unlock the part), then Replan.";
+ if (plate.SearchLimitReached)
+ return "Raise Cut planning effort in Tools > Options to search further, then Replan; it may still find no plan.";
return null;
}
diff --git a/OpenNest/Forms/OptionsForm.Designer.cs b/OpenNest/Forms/OptionsForm.Designer.cs
index b53e4b04..716d8542 100644
--- a/OpenNest/Forms/OptionsForm.Designer.cs
+++ b/OpenNest/Forms/OptionsForm.Designer.cs
@@ -33,6 +33,8 @@
this.label1 = new System.Windows.Forms.Label();
this.toolTip1 = new System.Windows.Forms.ToolTip(this.components);
this.numericUpDown1 = new OpenNest.Controls.NumericUpDown();
+ this.planningEffortLabel = new System.Windows.Forms.Label();
+ this.planningEffortBox = new OpenNest.Controls.NumericUpDown();
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
this.saveButton = new System.Windows.Forms.Button();
this.cancelButton = new System.Windows.Forms.Button();
@@ -48,6 +50,7 @@
this.updateFeedUrlBox = new System.Windows.Forms.TextBox();
this.setUpdateFeedButton = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)(this.planningEffortBox)).BeginInit();
this.tableLayoutPanel1.SuspendLayout();
this.bottomPanel1.SuspendLayout();
this.serverGroupBox.SuspendLayout();
@@ -59,7 +62,7 @@
this.checkBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
this.checkBox1.AutoSize = true;
this.tableLayoutPanel1.SetColumnSpan(this.checkBox1, 2);
- this.checkBox1.Location = new System.Drawing.Point(3, 130);
+ this.checkBox1.Location = new System.Drawing.Point(3, 165);
this.checkBox1.Name = "checkBox1";
this.checkBox1.Size = new System.Drawing.Size(281, 20);
this.checkBox1.TabIndex = 7;
@@ -92,6 +95,33 @@
this.numericUpDown1.TabIndex = 4;
this.toolTip1.SetToolTip(this.numericUpDown1, "The amount to round the plate size up.");
//
+ // planningEffortLabel
+ //
+ this.planningEffortLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
+ this.planningEffortLabel.AutoSize = true;
+ this.planningEffortLabel.Location = new System.Drawing.Point(3, 117);
+ this.planningEffortLabel.Name = "planningEffortLabel";
+ this.planningEffortLabel.Size = new System.Drawing.Size(145, 16);
+ this.planningEffortLabel.TabIndex = 12;
+ this.planningEffortLabel.Text = "Cut planning effort:";
+ //
+ // planningEffortBox
+ //
+ this.planningEffortBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
+ this.planningEffortBox.DecimalPlaces = 1;
+ this.planningEffortBox.Increment = new decimal(new int[] {
+ 5,
+ 0,
+ 0,
+ 65536});
+ this.planningEffortBox.Location = new System.Drawing.Point(154, 114);
+ this.planningEffortBox.Name = "planningEffortBox";
+ this.planningEffortBox.Size = new System.Drawing.Size(130, 22);
+ this.planningEffortBox.Suffix = "";
+ this.planningEffortBox.TabIndex = 13;
+ this.toolTip1.SetToolTip(this.planningEffortBox, "Multiplies the cut planner's search limit (1 = standard). "
+ + "Higher values search longer on hard plates; results are the same on every computer.");
+ //
// tableLayoutPanel1
//
this.tableLayoutPanel1.ColumnCount = 4;
@@ -102,15 +132,18 @@
this.tableLayoutPanel1.Controls.Add(this.label1, 0, 0);
this.tableLayoutPanel1.Controls.Add(this.colorSchemeLabel, 0, 1);
this.tableLayoutPanel1.Controls.Add(this.colorSchemeCombo, 1, 1);
- this.tableLayoutPanel1.Controls.Add(this.checkBox1, 0, 2);
+ this.tableLayoutPanel1.Controls.Add(this.planningEffortLabel, 0, 2);
+ this.tableLayoutPanel1.Controls.Add(this.planningEffortBox, 1, 2);
+ this.tableLayoutPanel1.Controls.Add(this.checkBox1, 0, 3);
this.tableLayoutPanel1.Controls.Add(this.numericUpDown1, 1, 0);
this.tableLayoutPanel1.Location = new System.Drawing.Point(12, 12);
this.tableLayoutPanel1.Name = "tableLayoutPanel1";
- this.tableLayoutPanel1.RowCount = 3;
- this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33333F));
- this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33333F));
- this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33333F));
- this.tableLayoutPanel1.Size = new System.Drawing.Size(684, 160);
+ this.tableLayoutPanel1.RowCount = 4;
+ this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 25F));
+ this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 25F));
+ this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 25F));
+ this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 25F));
+ this.tableLayoutPanel1.Size = new System.Drawing.Size(684, 200);
this.tableLayoutPanel1.TabIndex = 0;
//
// saveButton
@@ -143,7 +176,7 @@
this.bottomPanel1.Controls.Add(this.cancelButton);
this.bottomPanel1.Controls.Add(this.saveButton);
this.bottomPanel1.Dock = System.Windows.Forms.DockStyle.Bottom;
- this.bottomPanel1.Location = new System.Drawing.Point(0, 509);
+ this.bottomPanel1.Location = new System.Drawing.Point(0, 549);
this.bottomPanel1.Name = "bottomPanel1";
this.bottomPanel1.Size = new System.Drawing.Size(708, 50);
this.bottomPanel1.TabIndex = 1;
@@ -162,7 +195,7 @@
// strategyGroupBox
//
this.strategyGroupBox.Controls.Add(this.strategyGrid);
- this.strategyGroupBox.Location = new System.Drawing.Point(12, 319);
+ this.strategyGroupBox.Location = new System.Drawing.Point(12, 359);
this.strategyGroupBox.Name = "strategyGroupBox";
this.strategyGroupBox.Size = new System.Drawing.Size(684, 180);
this.strategyGroupBox.TabIndex = 2;
@@ -192,7 +225,7 @@
//
this.serverGroupBox.Controls.Add(this.serverUrlBox);
this.serverGroupBox.Controls.Add(this.setServerButton);
- this.serverGroupBox.Location = new System.Drawing.Point(12, 178);
+ this.serverGroupBox.Location = new System.Drawing.Point(12, 218);
this.serverGroupBox.Name = "serverGroupBox";
this.serverGroupBox.Size = new System.Drawing.Size(684, 69);
this.serverGroupBox.TabIndex = 1;
@@ -221,7 +254,7 @@
//
this.updateGroupBox.Controls.Add(this.updateFeedUrlBox);
this.updateGroupBox.Controls.Add(this.setUpdateFeedButton);
- this.updateGroupBox.Location = new System.Drawing.Point(12, 253);
+ this.updateGroupBox.Location = new System.Drawing.Point(12, 293);
this.updateGroupBox.Name = "updateGroupBox";
this.updateGroupBox.Size = new System.Drawing.Size(684, 60);
this.updateGroupBox.TabIndex = 3;
@@ -251,7 +284,7 @@
this.AcceptButton = this.saveButton;
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
this.CancelButton = this.cancelButton;
- this.ClientSize = new System.Drawing.Size(708, 559);
+ this.ClientSize = new System.Drawing.Size(708, 599);
this.Controls.Add(this.strategyGroupBox);
this.Controls.Add(this.updateGroupBox);
this.Controls.Add(this.serverGroupBox);
@@ -267,6 +300,7 @@
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Options";
((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).EndInit();
+ ((System.ComponentModel.ISupportInitialize)(this.planningEffortBox)).EndInit();
this.tableLayoutPanel1.ResumeLayout(false);
this.tableLayoutPanel1.PerformLayout();
this.bottomPanel1.ResumeLayout(false);
@@ -285,6 +319,8 @@
private System.Windows.Forms.Button saveButton;
private System.Windows.Forms.Label label1;
private Controls.NumericUpDown numericUpDown1;
+ private System.Windows.Forms.Label planningEffortLabel;
+ private Controls.NumericUpDown planningEffortBox;
private System.Windows.Forms.ToolTip toolTip1;
private Controls.BottomPanel bottomPanel1;
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
diff --git a/OpenNest/Forms/OptionsForm.cs b/OpenNest/Forms/OptionsForm.cs
index 777b1877..e790cbf8 100644
--- a/OpenNest/Forms/OptionsForm.cs
+++ b/OpenNest/Forms/OptionsForm.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Windows.Forms;
using OpenNest.Controls;
using OpenNest.Data;
+using OpenNest.Engine.CuttingPlanning;
using OpenNest.Engine.Strategies;
using OpenNest.Properties;
@@ -14,6 +15,8 @@ namespace OpenNest.Forms
public OptionsForm()
{
InitializeComponent();
+ planningEffortBox.Minimum = (decimal)CuttingPlanBatch.MinimumBudgetMultiplier;
+ planningEffortBox.Maximum = (decimal)CuttingPlanBatch.MaximumBudgetMultiplier;
GridStyle.Apply(strategyGrid);
BuildStrategyGrid();
}
@@ -79,6 +82,7 @@ namespace OpenNest.Forms
updateFeedUrlBox.Text = Settings.Default.UpdateFeedUrl;
checkBox1.Checked = Settings.Default.CreateNewNestOnOpen;
numericUpDown1.Value = (decimal)Settings.Default.AutoSizePlateFactor;
+ planningEffortBox.Value = (decimal)CuttingPlanBatch.ClampBudgetMultiplier(Settings.Default.CuttingPlanEffort);
colorSchemeCombo.Items.Clear();
foreach (var scheme in ColorSchemeRegistry.AllSchemes)
@@ -98,6 +102,7 @@ namespace OpenNest.Forms
{
Settings.Default.CreateNewNestOnOpen = checkBox1.Checked;
Settings.Default.AutoSizePlateFactor = (double)numericUpDown1.Value;
+ Settings.Default.CuttingPlanEffort = (double)planningEffortBox.Value;
Settings.Default.ActiveColorScheme =
colorSchemeCombo.SelectedItem as string ?? "Classic";
diff --git a/OpenNest/Properties/Settings.Designer.cs b/OpenNest/Properties/Settings.Designer.cs
index 8a66dc0f..57d11716 100644
--- a/OpenNest/Properties/Settings.Designer.cs
+++ b/OpenNest/Properties/Settings.Designer.cs
@@ -345,5 +345,18 @@ namespace OpenNest.Properties {
this["ClampDimensionToCenter"] = value;
}
}
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("1")]
+ // Cut planner budget multiplier; 1 keeps the built-in budgets.
+ public double CuttingPlanEffort {
+ get {
+ return ((double)(this["CuttingPlanEffort"]));
+ }
+ set {
+ this["CuttingPlanEffort"] = value;
+ }
+ }
}
}
diff --git a/OpenNest/Properties/Settings.settings b/OpenNest/Properties/Settings.settings
index cbe87e74..34df9ef3 100644
--- a/OpenNest/Properties/Settings.settings
+++ b/OpenNest/Properties/Settings.settings
@@ -83,5 +83,8 @@
False
+
+ 1
+
diff --git a/docs/cutting-planner.md b/docs/cutting-planner.md
index 1bd3bcd8..b1b6b700 100644
--- a/docs/cutting-planner.md
+++ b/docs/cutting-planner.md
@@ -615,6 +615,19 @@ panel is wired in the editor. Non-editor callers still use the shared analyzer.
`NoSolutionWithinBudget` is retried once with the current part order, and the summary
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.
+- **Tools > Options > Cut planning effort** (`Settings.CuttingPlanEffort`, default 1.0,
+ 0.5–10) multiplies both of those budgets. The panel reads it when planning starts and
+ passes it to `CuttingPlanBatch.Capture(..., budgetMultiplier)`; the worker never reads
+ settings, and out-of-range stored values are clamped. The budget counts expansions, not
+ time, so a given effort gives the same plan on every computer; faster hardware only
+ finishes it sooner, and raising the effort is how to spend that speed on hard plates.
+ The eight-entry, 1,000-expansion first pass is not scaled: it is a cheap probe for easy
+ plates, so a higher effort spends more only where the full search runs and leaves easy
+ plates' plans unchanged. A plate refused because a search reached its limit (its own
+ plan, or the free-order attempt before the current order was kept) says the effort can
+ be raised; a longer search may still find no plan. Only the desktop plans cuts: the
+ console, MCP server and API create plates but do not call the planner, so they have no
+ budget to configure. Direct `CuttingPlanRequest` callers set `expansionBudget` themselves.
- The docked panel keeps text short: one colored outcome line for the batch, one line for the
shown plate, and one detail line (a remedy for a blocked plate, or what the selected marker
shows). Below them is a read-only, unsorted issue grid with Plate, source Part, With, and full