feat(desktop): add Nest > Clear Nest

Clear Nest removes every plate after the first and empties the remaining
plate (parts, cut-offs, repeat quantity), keeping nest metadata and the
first plate's configuration. It asks first, defaulting to No, and refuses
while nesting, a fill, a busy plate action or a database save is running.
The menu item is enabled only when there is something to clear.
This commit is contained in:
aj committed 2026-10-19 13:12:25 -04:00
1 parent 1ed66bbf20
commit 274651b810
7 files changed
+258 -1

No files matched your search

+48
View File
@@ -195,6 +195,54 @@ namespace OpenNest
nest.Plates.RemoveAt(CurrentIndex);
}
/// <summary>
/// Whether <see cref="ClearAll"/> would change anything: more than one plate, or the
/// remaining plate still holds parts or cut-off definitions.
/// </summary>
public bool CanClearAll
{
get
{
if (Count == 0)
return false;
var remaining = nest.Plates[0];
return Count > 1
|| remaining.Parts.Count > 0
|| remaining.CutOffs.Count > 0
|| remaining.Quantity > 1;
}
}
/// <summary>
/// Clears the nest: removes every plate after the first and empties the remaining
/// plate (parts and cut-off definitions), resetting its repeat quantity. Nest metadata
/// and plate defaults are preserved; the first plate's configuration stays as the
/// workspace sentinel — the same result as removing every other plate one at a time.
/// The plate count never reaches zero, so observers never see a null current plate.
/// </summary>
public void ClearAll()
{
if (Count == 0)
return;
BeginBatch();
try
{
while (nest.Plates.Count > 1)
nest.Plates.RemoveAt(nest.Plates.Count - 1);
var remaining = nest.Plates[0];
remaining.CutOffs.Clear();
remaining.Parts.Clear();
remaining.Quantity = 1;
}
finally
{
EndBatch();
}
}
private void SubscribeToTailPlates()
{
UnsubscribeFromTailPlates();
+160
View File
@@ -399,6 +399,166 @@ public class PlateManagerTests
Assert.True(mgr.CanRemoveCurrent);
}
[Fact]
public void ClearAll_RemovesAllButFirstAndEmptiesRemaining()
{
var nest = CreateNest();
var plate1 = nest.CreatePlate();
plate1.Parts.Add(MakePart());
var plate2 = nest.CreatePlate();
plate2.Parts.Add(MakePart());
var plate3 = nest.CreatePlate();
plate3.Parts.Add(MakePart());
plate3.CutOffs.Add(new CutOff(new Vector(1, 1), CutOffAxis.Vertical));
plate3.Quantity = 5;
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
Assert.Equal(4, nest.Plates.Count);
mgr.LoadAt(2);
mgr.ClearAll();
// The first plate survives, empty, with its repeat quantity reset
Assert.Single(nest.Plates);
Assert.Same(plate1, nest.Plates[0]);
Assert.Empty(nest.Plates[0].Parts);
Assert.Empty(nest.Plates[0].CutOffs);
Assert.Equal(1, nest.Plates[0].Quantity);
Assert.Equal(0, mgr.CurrentIndex);
Assert.Same(plate1, mgr.CurrentPlate);
}
[Fact]
public void ClearAll_NeverExposesNullCurrentPlate()
{
var nest = CreateNest();
for (var i = 0; i < 3; i++)
{
var plate = nest.CreatePlate();
plate.Parts.Add(MakePart());
}
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
var currentPlates = new List<Plate>();
mgr.CurrentPlateChanged += (s, e) => currentPlates.Add(e.Plate);
mgr.ClearAll();
// Every event during the clear reported a live plate (the one that stays)
Assert.NotEmpty(currentPlates);
Assert.All(currentPlates, p => Assert.NotNull(p));
Assert.All(currentPlates, p => Assert.Same(nest.Plates[0], p));
}
[Fact]
public void ClearAll_FiresPlateListAndCurrentPlateChanged()
{
var nest = CreateNest();
var plate1 = nest.CreatePlate();
plate1.Parts.Add(MakePart());
nest.CreatePlate();
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
var listChanged = 0;
var currentChanged = 0;
mgr.PlateListChanged += (s, e) => listChanged++;
mgr.CurrentPlateChanged += (s, e) => currentChanged++;
mgr.ClearAll();
Assert.True(listChanged > 0);
Assert.True(currentChanged > 0);
}
[Fact]
public void ClearAll_SinglePlateWithParts_EmptiesIt()
{
var nest = CreateNest();
var plate1 = nest.CreatePlate();
plate1.Parts.Add(MakePart());
plate1.CutOffs.Add(new CutOff(new Vector(2, 2), CutOffAxis.Horizontal));
plate1.Quantity = 3;
using var mgr = new PlateManager(nest);
mgr.ClearAll();
Assert.Single(nest.Plates);
Assert.Same(plate1, nest.Plates[0]);
Assert.Empty(plate1.Parts);
Assert.Empty(plate1.CutOffs);
Assert.Equal(1, plate1.Quantity);
}
[Fact]
public void ClearAll_PreservesNestMetadataAndFirstPlateConfiguration()
{
var nest = CreateNest();
nest.Name = "my nest";
nest.Thickness = 0.25;
var defaults = nest.PlateDefaults.Size;
var plate1 = nest.CreatePlate();
plate1.Parts.Add(MakePart());
// Edit the survivor away from the defaults so replacement would be visible
plate1.Size = new Size(99, 88);
plate1.PartSpacing = 0.4;
nest.CreatePlate();
using var mgr = new PlateManager(nest);
mgr.ClearAll();
Assert.Equal("my nest", nest.Name);
Assert.Equal(0.25, nest.Thickness);
// The survivor keeps its own configuration; it was not replaced by a default plate
Assert.Same(plate1, nest.Plates[0]);
Assert.Equal(new Size(99, 88), nest.Plates[0].Size);
Assert.Equal(0.4, nest.Plates[0].PartSpacing);
Assert.Equal(defaults, nest.PlateDefaults.Size);
}
[Fact]
public void ClearAll_EmptyNest_DoesNothing()
{
var nest = CreateNest();
using var mgr = new PlateManager(nest);
mgr.ClearAll();
Assert.Empty(nest.Plates);
}
[Fact]
public void CanClearAll_ReflectsClearableState()
{
var nest = CreateNest();
using var mgr = new PlateManager(nest);
Assert.False(mgr.CanClearAll);
var plate1 = nest.CreatePlate();
Assert.False(mgr.CanClearAll); // a single untouched empty plate has nothing to clear
plate1.Quantity = 2;
Assert.True(mgr.CanClearAll);
plate1.Quantity = 1;
Assert.False(mgr.CanClearAll);
plate1.CutOffs.Add(new CutOff(new Vector(0, 0), CutOffAxis.Vertical));
Assert.True(mgr.CanClearAll);
plate1.CutOffs.Clear();
Assert.False(mgr.CanClearAll);
plate1.Parts.Add(MakePart());
Assert.Equal(2, nest.Plates.Count); // trailing sentinel created reactively
Assert.True(mgr.CanClearAll); // multiple plates are clearable
plate1.Parts.Clear();
// The reactive sentinel collapses the now-trailing empty pair back to one plate
Assert.Single(nest.Plates);
Assert.False(mgr.CanClearAll);
}
[Fact]
public void RemoveEmptyPlates_SentinelIsRestored()
{
+5
View File
@@ -894,6 +894,11 @@ namespace OpenNest.Forms
PlateManager.RemoveCurrent();
}
public void ClearNest()
{
PlateManager.ClearAll();
}
public void AutoSequenceCurrentPlate()
{
SequencePlate(PlateView.Plate);
+10 -1
View File
@@ -102,6 +102,7 @@ namespace OpenNest.Forms
runAutoNestToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
autoSequenceAllPlatesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
mnuNestRemoveEmptyPlates = new System.Windows.Forms.ToolStripMenuItem();
mnuNestClear = new System.Windows.Forms.ToolStripMenuItem();
mnuNestValidate = new System.Windows.Forms.ToolStripMenuItem();
mnuNestPost = new System.Windows.Forms.ToolStripMenuItem();
toolStripMenuItem19 = new System.Windows.Forms.ToolStripSeparator();
@@ -573,7 +574,7 @@ namespace OpenNest.Forms
//
// mnuNest
//
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestClamps, mnuNestImportDrawing, mnuNestShapeLibrary, mnuNestDrawingLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestValidate, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestClamps, mnuNestImportDrawing, mnuNestShapeLibrary, mnuNestDrawingLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestClear, mnuNestValidate, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
mnuNest.Name = "mnuNest";
mnuNest.Size = new System.Drawing.Size(43, 20);
mnuNest.Text = "&Nest";
@@ -685,6 +686,13 @@ namespace OpenNest.Forms
mnuNestRemoveEmptyPlates.Text = "Remove Empty Plates";
mnuNestRemoveEmptyPlates.Click += RemoveEmptyPlates_Click;
//
// mnuNestClear
//
mnuNestClear.Name = "mnuNestClear";
mnuNestClear.Size = new System.Drawing.Size(205, 22);
mnuNestClear.Text = "Clear Nest...";
mnuNestClear.Click += ClearNest_Click;
//
// mnuNestValidate
//
mnuNestValidate.Name = "mnuNestValidate";
@@ -1401,6 +1409,7 @@ namespace OpenNest.Forms
private System.Windows.Forms.ToolStripButton btnNew;
private System.Windows.Forms.ToolStripMenuItem mnuPlateAdd;
private System.Windows.Forms.ToolStripMenuItem mnuNestRemoveEmptyPlates;
private System.Windows.Forms.ToolStripMenuItem mnuNestClear;
private System.Windows.Forms.ToolStripMenuItem mnuViewZoomIn;
private System.Windows.Forms.ToolStripMenuItem mnuViewZoomOut;
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem14;
+1
View File
@@ -26,6 +26,7 @@ namespace OpenNest.Forms
NavigationEnableCheck();
statusBar.UpdatePlate();
mnuPlateRemove.Enabled = activeForm?.PlateManager.CanRemoveCurrent ?? false;
mnuNestClear.Enabled = activeForm?.PlateManager.CanClearAll ?? false;
};
editForm.WindowState = windowState;
editForm.Show();
+33
View File
@@ -43,6 +43,8 @@ namespace OpenNest.Forms
mnuNest.DropDown.ShowItemToolTips = true;
mnuNestDrawingLibrary.Enabled = activeForm != null && databaseMode;
mnuNestDrawingLibrary.ToolTipText = databaseMode ? "" : NestStorage.DatabaseModeOnly;
// Parts can be added without a plate change, so refresh the clear state on open.
mnuNestClear.Enabled = activeForm?.PlateManager.CanClearAll ?? false;
}
private async void EditNest_Click(object sender, EventArgs e)
@@ -85,6 +87,37 @@ namespace OpenNest.Forms
private void RemoveEmptyPlates_Click(object sender, EventArgs e) => WithActive(f => f.Nest.Plates.RemoveEmptyPlates());
private void ClearNest_Click(object sender, EventArgs e)
{
var editForm = activeForm;
if (editForm == null)
return;
const string busy = "Finish or cancel the current nesting, save or plate action first.";
if (storage.IsSaving)
{
MessageBox.Show(this, busy, "Clear Nest", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (!TryBeginPlateCommand(editForm.Nest, busy, "Clear Nest"))
return;
var result = MessageBox.Show(
this,
"Remove all plates and parts from this nest?\nOne empty plate will remain. This cannot be undone.",
"Clear Nest",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2
);
if (result != DialogResult.Yes)
return;
editForm.ClearNest();
}
private void LoadFirstPlate_Click(object sender, EventArgs e) => WithActive(f => f.PlateManager.LoadFirst());
private void LoadLastPlate_Click(object sender, EventArgs e) => WithActive(f => f.PlateManager.LoadLast());
+1
View File
@@ -15,6 +15,7 @@ namespace OpenNest.Forms
btnSave.Enabled = hasValue && !storage.IsSaving;
btnSaveAs.Enabled = hasValue && !storage.IsSaving;
mnuFileSaveAs.Enabled = hasValue && !storage.IsSaving;
mnuNestClear.Enabled = activeForm?.PlateManager.CanClearAll ?? false;
exportNestMenu.Enabled = hasValue;
mnuFileExport.Enabled = hasValue;
mnuFileExportAll.Enabled = hasValue;