feat(desktop): choose cutting-planner cut-off settings in the automatic cut-off dialog

This commit is contained in:
aj committed 2026-10-13 20:55:24 -04:00
1 parent 9f2ece37ed
commit b9a387b517
5 files changed
+259 -18

No files matched your search

@@ -96,6 +96,48 @@ public class AutomaticCutOffFormTests
});
}
[Fact]
public void SettingsMode_SeedsTheGivenOptionsAndOkReturnsThemWithoutAddingCutoffs()
{
RunSta(() =>
{
using var view = CreateView();
var initial = new AutomaticCutOffOptions { Spacing = 30, MinimumStripLength = 20, MinimumTailLength = 8 };
using var form = AutomaticCutOffForm.ForSettings(view, Units.Inches, null, initial);
Assert.Equal("&Use These Settings", Control<Button>(form, "applyButton").Text);
Assert.Equal(30, double.Parse(Control<TextBox>(form, "spacingBox").Text));
Assert.Equal(20, double.Parse(Control<TextBox>(form, "minimumStripBox").Text));
Assert.Equal(8, double.Parse(Control<TextBox>(form, "minimumTailBox").Text));
Control<TextBox>(form, "spacingBox").Text = "40";
Assert.Equal(20, double.Parse(Control<TextBox>(form, "minimumStripBox").Text)); // Seeded minimum is kept.
Invoke(form, "ApplyButton_Click", null, EventArgs.Empty);
Assert.Equal(DialogResult.OK, form.DialogResult);
Assert.Equal((40.0, (double?)20, 8.0), (form.SelectedOptions!.Spacing, form.SelectedOptions.MinimumStripLength,
form.SelectedOptions.MinimumTailLength));
Assert.Empty(view.Plate.CutOffs);
Assert.DoesNotContain(view.Plate.Parts, p => p.BaseDrawing.IsCutOff);
Assert.Empty(PreviewParts(view));
});
}
[Fact]
public void SettingsMode_InvalidInputReturnsNothing()
{
RunSta(() =>
{
using var view = CreateView();
using var form = AutomaticCutOffForm.ForSettings(view, Units.Inches, null, null);
Control<TextBox>(form, "minimumStripBox").Text = "40";
Invoke(form, "ApplyButton_Click", null, EventArgs.Empty);
Assert.NotEqual(DialogResult.OK, form.DialogResult);
Assert.Null(form.SelectedOptions);
Assert.Contains("Minimum strip", Control<TextBox>(form, "diagnosticsBox").Text);
Assert.Empty(view.Plate.CutOffs);
});
}
[Fact]
public void PreviewIsDetachedAndDisposeClearsIt()
{
@@ -542,6 +542,68 @@ public class CuttingInspectionPanelTests
panel.RequestClose();
});
[Fact]
public void CutOffSettingsOpensTheAutomaticCutOffDialogAndReplansWithItsChoice() => RunSta(() =>
{
var remembered = (System.Collections.IDictionary)typeof(CuttingInspectionPanel)
.GetField("lastCutOffOptions", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!
.GetValue(null)!;
try
{
using var editor = Editor();
editor.Show();
editor.PlateManager.LoadAt(0);
var plate = editor.PlateView.Plate;
editor.OpenCuttingInspection();
var panel = editor.CuttingInspection!;
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.Equal(35, panel.CutOffOptions.Spacing);
panel.ShowCutOffSettings = form => DialogResult.Cancel;
Field<Button>(panel, "cutOffSettingsButton").PerformClick();
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.False(Field<CheckBox>(panel, "autoCutOffsCheckBox").Checked);
Assert.Equal(35, panel.CutOffOptions.Spacing);
Assert.Empty(plate.CutOffs);
AutomaticCutOffForm? shown = null;
panel.ShowCutOffSettings = form =>
{
shown = form;
Assert.Equal(35, double.Parse(form.Controls.Find("spacingBox", true).Single().Text));
form.Controls.Find("spacingBox", true).Single().Text = "30";
form.Controls.Find("minimumTailBox", true).Single().Text = "10";
typeof(AutomaticCutOffForm).GetMethod("ApplyButton_Click",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
.Invoke(form, [null, EventArgs.Empty]);
Assert.Empty(plate.CutOffs); // The dialog only chooses settings.
return form.DialogResult;
};
Field<Button>(panel, "cutOffSettingsButton").PerformClick();
PumpUntil(() => panel.Proposal != null && !panel.IsPlanning);
Assert.NotNull(shown);
Assert.Equal((30.0, 10.0), (panel.CutOffOptions.Spacing, panel.CutOffOptions.MinimumTailLength));
Assert.True(Field<CheckBox>(panel, "autoCutOffsCheckBox").Checked);
Assert.Contains("30 in strips", Field<CheckBox>(panel, "autoCutOffsCheckBox").Text);
Assert.NotNull(panel.StagedCutOffs);
Assert.NotEmpty(plate.CutOffs);
Assert.Equal(plate.CutOffs.Count, panel.Proposal.Plates[0].Result.ProposedOrder.Count(p => p.IsCutOff));
panel.RequestClose();
Assert.Empty(plate.CutOffs);
editor.OpenCuttingInspection();
var reopened = editor.CuttingInspection!;
Assert.Equal(30, reopened.CutOffOptions.Spacing); // Remembered for this session.
PumpUntil(() => reopened.Proposal != null && !reopened.IsPlanning);
reopened.RequestClose();
}
finally
{
remembered.Clear();
}
});
private sealed class MenuHost : MainForm
{
protected override void OnLoad(EventArgs e) { }
+58 -7
View File
@@ -22,6 +22,30 @@ public partial class AutomaticCutOffForm : Form
private readonly string unit;
private bool minimumStripEdited;
private bool settingMinimumStrip;
private bool settingsOnly;
/// <summary>In settings mode, the options chosen with OK; null otherwise or when cancelled.</summary>
internal AutomaticCutOffOptions SelectedOptions { get; private set; }
/// <summary>
/// Opens the same dialog to choose settings for another workflow (Plan Cutting): it previews
/// on the active plate exactly as usual, but OK only returns the options and adds nothing.
/// </summary>
internal static AutomaticCutOffForm ForSettings(PlateView plateView, Units units, Nest nest,
AutomaticCutOffOptions initial)
{
var form = new AutomaticCutOffForm(plateView, units, nest, null) { settingsOnly = true };
form.Text = "Automatic Scrap Cut-offs — Settings for Plan Cutting";
form.applyButton.Text = "&Use These Settings";
if (initial != null)
{
form.spacingBox.Text = initial.Spacing.ToString(CultureInfo.CurrentCulture);
form.minimumStripBox.Text = (initial.MinimumStripLength ?? initial.Spacing).ToString(CultureInfo.CurrentCulture);
form.minimumTailBox.Text = initial.MinimumTailLength.ToString(CultureInfo.CurrentCulture);
form.minimumStripEdited = true; // Keep the given minimum when the spacing is edited.
}
return form;
}
public AutomaticCutOffForm(PlateView plateView, Units units, Func<bool> isOperationBusy = null)
: this(plateView, units, null, isOperationBusy) { }
@@ -91,6 +115,16 @@ public partial class AutomaticCutOffForm : Form
if (!plates.Contains(plate))
throw new InvalidOperationException("The active plate is no longer in this nest. Close this dialog and try again.");
var options = ParseOptions();
var settings = plateView.CutOffSettings;
var plans = apply
? AutomaticCutOffBatch.Apply(plates, options, settings)
: AutomaticCutOffBatch.Create(plates, options, settings);
return (plates, plans);
}
private AutomaticCutOffOptions ParseOptions()
{
if (!double.TryParse(spacingBox.Text, NumberStyles.Float, CultureInfo.CurrentCulture, out var spacing))
throw new ArgumentException($"Enter a numeric spacing in {unit}.");
if (!double.TryParse(minimumStripBox.Text, NumberStyles.Float, CultureInfo.CurrentCulture, out var minimumStrip))
@@ -98,17 +132,12 @@ public partial class AutomaticCutOffForm : Form
if (!double.TryParse(minimumTailBox.Text, NumberStyles.Float, CultureInfo.CurrentCulture, out var minimumTail))
throw new ArgumentException($"Enter a numeric minimum tail length in {unit}.");
var options = new AutomaticCutOffOptions
return new AutomaticCutOffOptions
{
Spacing = spacing,
MinimumStripLength = minimumStrip,
MinimumTailLength = minimumTail,
};
var settings = plateView.CutOffSettings;
var plans = apply
? AutomaticCutOffBatch.Apply(plates, options, settings)
: AutomaticCutOffBatch.Create(plates, options, settings);
return (plates, plans);
}
private string DiagnosticLine(AutomaticCutOffPlan plan) => string.Join(Environment.NewLine,
@@ -139,7 +168,9 @@ public partial class AutomaticCutOffForm : Form
var blocked = plans.Any(p => p.HasBlockingDiagnostics);
var definitionCount = plans.Sum(p => p.Definitions.Count);
applyButton.Enabled = !blocked && definitionCount > 0;
// Settings may be chosen even when this layout needs no new lines or is blocked:
// Plan Cutting reports the blocked plate itself.
applyButton.Enabled = settingsOnly || !blocked && definitionCount > 0;
var issues = new List<string>();
for (var index = 0; index < plans.Count; index++)
@@ -280,6 +311,26 @@ public partial class AutomaticCutOffForm : Form
private void ApplyButton_Click(object sender, EventArgs e)
{
ClearPreview();
if (settingsOnly)
{
try
{
// Validate exactly as planning will, without changing any plate.
Calculate(apply: false);
SelectedOptions = ParseOptions();
DialogResult = DialogResult.OK;
Close();
}
catch (Exception ex)
{
ShowError(ex);
}
finally
{
ClearPreview();
}
return;
}
try
{
// The batch service replans every plate before mutating any; never accept previews.
+88 -8
View File
@@ -84,7 +84,12 @@ internal sealed class CuttingInspectionPanel : UserControl
BackColor = SystemColors.Control,
UseVisualStyleBackColor = true
};
private readonly AutomaticCutOffOptions cutOffOptions;
private readonly Button cutOffSettingsButton = new() { Text = "Cut-off Settings…", AutoSize = true };
private readonly Units units;
private readonly bool allPlates;
private AutomaticCutOffOptions cutOffOptions;
// Last cut-off settings chosen here, per unit system, for the rest of this session.
private static readonly System.Collections.Generic.Dictionary<Units, AutomaticCutOffOptions> lastCutOffOptions = new();
private AutomaticCutOffStaging stagedCutOffs;
private readonly CheckBox acceptWarningsCheckBox = new() { Text = "Accept UNVERIFIED plan warnings (post consent happens separately)", AutoSize = true, Visible = false };
private readonly Label settingsLabel = new() { AutoSize = true, MaximumSize = new Size(440, 0) };
@@ -139,14 +144,15 @@ internal sealed class CuttingInspectionPanel : UserControl
{
if (!configuringMode) SwitchMode((CuttingInspectionMode)modeSelector.SelectedIndex);
};
var units = editor.Nest.Units;
var unit = UnitsHelper.GetShortString(units);
cutOffOptions = DefaultCutOffOptions(units);
autoCutOffsCheckBox.Text = $"Add automatic scrap cut-offs ({cutOffOptions.Spacing:0.###} {unit} strips, "
+ $"keep tails of {cutOffOptions.MinimumTailLength:0.###} {unit} or more)";
this.allPlates = allPlates;
units = editor.Nest.Units;
cutOffOptions = lastCutOffOptions.TryGetValue(units, out var last) ? Copy(last) : DefaultCutOffOptions(units);
ShowCutOffOptions();
var cutOffRow = new FlowLayoutPanel { AutoSize = true, WrapContents = false, Margin = Padding.Empty };
cutOffRow.Controls.AddRange([autoCutOffsCheckBox, cutOffSettingsButton]);
existingWorkLabel.Font = new Font(Font, FontStyle.Bold);
existingWorkBanner.Controls.AddRange([existingWorkLabel, clearExistingButton]);
header.Controls.AddRange([modeSelector, existingWorkBanner, settingsLabel, keepOrderCheckBox, autoCutOffsCheckBox, buttons,
header.Controls.AddRange([modeSelector, existingWorkBanner, settingsLabel, keepOrderCheckBox, cutOffRow, buttons,
acceptWarningsCheckBox, navigation, previewLabel, validationScopeLabel]);
var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3 };
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
@@ -170,6 +176,7 @@ internal sealed class CuttingInspectionPanel : UserControl
settingsButton.Click += (_, _) => EditSettings();
keepOrderCheckBox.CheckedChanged += (_, _) => StartPlanning();
autoCutOffsCheckBox.CheckedChanged += (_, _) => StartPlanning();
cutOffSettingsButton.Click += (_, _) => EditCutOffSettings();
clearExistingButton.Click += (_, _) => ClearExistingWork();
acceptWarningsCheckBox.CheckedChanged += (_, _) => UpdateApply();
applyButton.Click += (_, _) => Apply();
@@ -195,6 +202,78 @@ internal sealed class CuttingInspectionPanel : UserControl
/// <summary>Automatic cut-offs added to the live plates for the current plan, until Apply keeps them.</summary>
internal AutomaticCutOffStaging StagedCutOffs => stagedCutOffs;
/// <summary>Shows the cut-off settings dialog; tests replace it. Null uses ShowDialog.</summary>
internal Func<AutomaticCutOffForm, DialogResult> ShowCutOffSettings { get; set; }
internal AutomaticCutOffOptions CutOffOptions => Copy(cutOffOptions);
private static AutomaticCutOffOptions Copy(AutomaticCutOffOptions options) => new()
{
Spacing = options.Spacing,
MinimumStripLength = options.MinimumStripLength,
MinimumTailLength = options.MinimumTailLength,
};
private void ShowCutOffOptions()
{
var unit = UnitsHelper.GetShortString(units);
autoCutOffsCheckBox.Text = $"Add automatic scrap cut-offs ({cutOffOptions.Spacing:0.###} {unit} strips, "
+ $"keep tails of {cutOffOptions.MinimumTailLength:0.###} {unit} or more)";
}
/// <summary>
/// Opens the Automatic Scrap Cut-offs dialog in settings mode for this panel's scope. Unapplied
/// cut-offs come off first so its preview starts from the plates' real state; OK ticks the box
/// and replans with the chosen settings, Cancel replans with the previous ones.
/// </summary>
private void EditCutOffSettings()
{
if (closed || closeRequested || IsPlanning || Mode != CuttingInspectionMode.Plan)
return;
ClearResult();
try
{
RevertStagedCutOffs();
}
catch (Exception ex)
{
resultLabel.Text = $"Unable to remove the unapplied automatic cut-offs: {ex.Message}";
return;
}
RefreshExistingWork();
AutomaticCutOffOptions chosen = null;
try
{
using var form = AutomaticCutOffForm.ForSettings(view, units, allPlates ? editor.Nest : null, cutOffOptions);
var result = ShowCutOffSettings?.Invoke(form) ?? form.ShowDialog(this);
if (result == DialogResult.OK)
chosen = form.SelectedOptions;
}
catch (Exception ex)
{
resultLabel.Text = $"Unable to open the cut-off settings: {ex.Message}";
}
finally
{
if (!view.IsDisposed)
view.ClearPreviewParts();
}
if (closed || closeRequested)
return;
if (chosen != null)
{
cutOffOptions = Copy(chosen);
lastCutOffOptions[units] = Copy(chosen);
ShowCutOffOptions();
if (!autoCutOffsCheckBox.Checked)
{
autoCutOffsCheckBox.Checked = true; // Replans through CheckedChanged.
return;
}
}
StartPlanning();
}
/// <summary>The automatic cut-off dialog's defaults: 35 in strips, half-strip minimum, 12 in tail.</summary>
internal static AutomaticCutOffOptions DefaultCutOffOptions(Units units)
{
@@ -230,7 +309,7 @@ internal sealed class CuttingInspectionPanel : UserControl
configuringMode = false;
var planning = Mode == CuttingInspectionMode.Plan;
settingsButton.Visible = settingsLabel.Visible = keepOrderCheckBox.Visible = applyButton.Visible = planning;
autoCutOffsCheckBox.Visible = planning;
autoCutOffsCheckBox.Visible = cutOffSettingsButton.Visible = planning;
RefreshExistingWork();
previousButton.Visible = nextButton.Visible = planning;
validationScopeLabel.Visible = !planning;
@@ -501,6 +580,7 @@ internal sealed class CuttingInspectionPanel : UserControl
private void SetPlanning(bool running)
{
settingsButton.Enabled = keepOrderCheckBox.Enabled = autoCutOffsCheckBox.Enabled = planButton.Enabled = !running;
cutOffSettingsButton.Enabled = !running;
clearExistingButton.Enabled = !running;
previousButton.Enabled = !running && selectedPlate > 0;
nextButton.Enabled = !running && selectedPlate < plates.Length - 1;
+9 -3
View File
@@ -417,13 +417,19 @@ panel is wired in the editor. Non-editor callers still use the shared analyzer.
Cut-offs...` asks first, then removes every applied lead-in (locked and hand-placed
ones too), every cut-off and the plates' recorded cutting settings, keeping part
poses, order and quantities, and replans from scratch. There is no undo.
- `Add automatic scrap cut-offs` runs the automatic cut-off planner with its dialog
defaults (35 in / 889 mm strips, half-strip minimum, 12 in / 304.8 mm tail) on every
plate in scope *before* capture, so the cutting plan sequences the new lines like any
- `Add automatic scrap cut-offs` runs the automatic cut-off planner (defaults 35 in /
889 mm strips, half-strip minimum, 12 in / 304.8 mm tail) on every plate in scope
*before* capture, so the cutting plan sequences the new lines like any
other cut-off. These are the only live changes made before Apply: Apply keeps them,
while unticking the box, Cancel/Close or switching to Validate removes them and
restores the earlier cut-offs and sequence. A blocked cut-off plan adds nothing and
plans nothing; the panel names the plate and reason.
`Cut-off Settings...` opens the Automatic Scrap Cut-offs dialog in settings mode for
the same scope: it previews on the active plate as usual, but `Use These Settings`
only returns the values (it adds nothing), ticks the box and replans. The panel
remembers the last settings per unit system until the application closes. A cut-off
still goes before every part its nominal line passes through; a line that passes
through no part keeps a free position in the order (user decision, 2026-10-13).
- Lead-ins are optional for planning. With `None` (or a style that emits no lead
motion), the emitter places the pierce directly on the selected contour point.
Unlocked contour starts can still move to improve travel; locked programs keep