mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-14 18:21:55 -04:00
feat(desktop): choose cutting-planner cut-off settings in the automatic cut-off dialog
This commit is contained in:
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) { }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user