feat: add sentinel plate and plate list enhancements
Always keep a trailing empty plate so users can immediately place parts without manually adding a plate. Auto-appends a new sentinel when parts land on the last plate; trims excess trailing empties on removal. Plate list now shows Parts count and Utilization % columns. Empty plates are filtered from save and export. Sentinel updates are deferred via BeginInvoke to avoid collection-modified exceptions and debounced to prevent per-part overhead on bulk operations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -175,9 +175,15 @@ namespace OpenNest.IO
|
|||||||
private List<PlateDto> BuildPlateDtos()
|
private List<PlateDto> BuildPlateDtos()
|
||||||
{
|
{
|
||||||
var list = new List<PlateDto>();
|
var list = new List<PlateDto>();
|
||||||
|
var id = 0;
|
||||||
for (var i = 0; i < nest.Plates.Count; i++)
|
for (var i = 0; i < nest.Plates.Count; i++)
|
||||||
{
|
{
|
||||||
var plate = nest.Plates[i];
|
var plate = nest.Plates[i];
|
||||||
|
|
||||||
|
if (plate.Parts.Count(p => !p.BaseDrawing.IsCutOff) == 0 && plate.CutOffs.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
id++;
|
||||||
var parts = new List<PartDto>();
|
var parts = new List<PartDto>();
|
||||||
foreach (var part in plate.Parts.Where(p => !p.BaseDrawing.IsCutOff))
|
foreach (var part in plate.Parts.Where(p => !p.BaseDrawing.IsCutOff))
|
||||||
{
|
{
|
||||||
@@ -208,7 +214,7 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
list.Add(new PlateDto
|
list.Add(new PlateDto
|
||||||
{
|
{
|
||||||
Id = i + 1,
|
Id = id,
|
||||||
Size = new SizeDto { Width = plate.Size.Width, Length = plate.Size.Length },
|
Size = new SizeDto { Width = plate.Size.Width, Length = plate.Size.Length },
|
||||||
Quadrant = plate.Quadrant,
|
Quadrant = plate.Quadrant,
|
||||||
Quantity = plate.Quantity,
|
Quantity = plate.Quantity,
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ public class NestResponsePersistenceTests
|
|||||||
{
|
{
|
||||||
var nest = new Nest("test-nest");
|
var nest = new Nest("test-nest");
|
||||||
var plate = new Plate(new Size(60, 120));
|
var plate = new Plate(new Size(60, 120));
|
||||||
|
var drawing = new Drawing("test-part");
|
||||||
|
nest.Drawings.Add(drawing);
|
||||||
|
plate.Parts.Add(new Part(drawing));
|
||||||
nest.Plates.Add(plate);
|
nest.Plates.Add(plate);
|
||||||
|
|
||||||
var request = new NestRequest
|
var request = new NestRequest
|
||||||
|
|||||||
Generated
+15
-1
@@ -36,6 +36,8 @@
|
|||||||
plateNumColumn = new System.Windows.Forms.ColumnHeader();
|
plateNumColumn = new System.Windows.Forms.ColumnHeader();
|
||||||
sizeColumn = new System.Windows.Forms.ColumnHeader();
|
sizeColumn = new System.Windows.Forms.ColumnHeader();
|
||||||
qtyColumn = new System.Windows.Forms.ColumnHeader();
|
qtyColumn = new System.Windows.Forms.ColumnHeader();
|
||||||
|
partsColumn = new System.Windows.Forms.ColumnHeader();
|
||||||
|
utilColumn = new System.Windows.Forms.ColumnHeader();
|
||||||
toolStrip1 = new System.Windows.Forms.ToolStrip();
|
toolStrip1 = new System.Windows.Forms.ToolStrip();
|
||||||
tabPage2 = new System.Windows.Forms.TabPage();
|
tabPage2 = new System.Windows.Forms.TabPage();
|
||||||
drawingListBox1 = new OpenNest.Controls.DrawingListBox();
|
drawingListBox1 = new OpenNest.Controls.DrawingListBox();
|
||||||
@@ -100,7 +102,7 @@
|
|||||||
// platesListView
|
// platesListView
|
||||||
//
|
//
|
||||||
platesListView.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
platesListView.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||||
platesListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { plateNumColumn, sizeColumn, qtyColumn });
|
platesListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { plateNumColumn, sizeColumn, qtyColumn, partsColumn, utilColumn });
|
||||||
platesListView.Dock = System.Windows.Forms.DockStyle.Fill;
|
platesListView.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
platesListView.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
|
platesListView.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
|
||||||
platesListView.FullRowSelect = true;
|
platesListView.FullRowSelect = true;
|
||||||
@@ -129,6 +131,16 @@
|
|||||||
//
|
//
|
||||||
qtyColumn.Text = "Qty";
|
qtyColumn.Text = "Qty";
|
||||||
//
|
//
|
||||||
|
// partsColumn
|
||||||
|
//
|
||||||
|
partsColumn.Text = "Parts";
|
||||||
|
partsColumn.Width = 45;
|
||||||
|
//
|
||||||
|
// utilColumn
|
||||||
|
//
|
||||||
|
utilColumn.Text = "Util";
|
||||||
|
utilColumn.Width = 45;
|
||||||
|
//
|
||||||
// toolStrip1
|
// toolStrip1
|
||||||
//
|
//
|
||||||
toolStrip1.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
|
toolStrip1.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
|
||||||
@@ -263,6 +275,8 @@
|
|||||||
private System.Windows.Forms.TabPage tabPage2;
|
private System.Windows.Forms.TabPage tabPage2;
|
||||||
private Controls.DrawingListBox drawingListBox1;
|
private Controls.DrawingListBox drawingListBox1;
|
||||||
private System.Windows.Forms.ColumnHeader plateNumColumn;
|
private System.Windows.Forms.ColumnHeader plateNumColumn;
|
||||||
|
private System.Windows.Forms.ColumnHeader partsColumn;
|
||||||
|
private System.Windows.Forms.ColumnHeader utilColumn;
|
||||||
private System.Windows.Forms.ToolStrip toolStrip2;
|
private System.Windows.Forms.ToolStrip toolStrip2;
|
||||||
private System.Windows.Forms.ToolStripButton toolStripButton2;
|
private System.Windows.Forms.ToolStripButton toolStripButton2;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
|
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ namespace OpenNest.Forms
|
|||||||
public readonly PlateView PlateView;
|
public readonly PlateView PlateView;
|
||||||
|
|
||||||
private readonly Timer updateDrawingListTimer;
|
private readonly Timer updateDrawingListTimer;
|
||||||
|
private bool suppressPlateNavigation;
|
||||||
|
private bool updatingPlateList;
|
||||||
|
private bool sentinelUpdatePending;
|
||||||
|
|
||||||
private Panel plateHeaderPanel;
|
private Panel plateHeaderPanel;
|
||||||
private Label plateInfoLabel;
|
private Label plateInfoLabel;
|
||||||
@@ -67,7 +70,7 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
platesListView.SelectedIndexChanged += (sender, e) =>
|
platesListView.SelectedIndexChanged += (sender, e) =>
|
||||||
{
|
{
|
||||||
if (platesListView.SelectedIndices.Count == 0)
|
if (updatingPlateList || platesListView.SelectedIndices.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
CurrentPlateIndex = platesListView.SelectedIndices[0];
|
CurrentPlateIndex = platesListView.SelectedIndices[0];
|
||||||
@@ -217,6 +220,8 @@ namespace OpenNest.Forms
|
|||||||
if (Nest.Plates.Count == 0)
|
if (Nest.Plates.Count == 0)
|
||||||
Nest.CreatePlate();
|
Nest.CreatePlate();
|
||||||
|
|
||||||
|
EnsureSentinelPlate();
|
||||||
|
|
||||||
UpdatePlateList();
|
UpdatePlateList();
|
||||||
UpdateDrawingList();
|
UpdateDrawingList();
|
||||||
UpdateRemovePlateButton();
|
UpdateRemovePlateButton();
|
||||||
@@ -299,6 +304,10 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
public void UpdatePlateList()
|
public void UpdatePlateList()
|
||||||
{
|
{
|
||||||
|
updatingPlateList = true;
|
||||||
|
var focused = ContainsFocus ? GetFocusedControl() : null;
|
||||||
|
|
||||||
|
platesListView.BeginUpdate();
|
||||||
platesListView.Items.Clear();
|
platesListView.Items.Clear();
|
||||||
|
|
||||||
var items = new ListViewItem[Nest.Plates.Count];
|
var items = new ListViewItem[Nest.Plates.Count];
|
||||||
@@ -311,6 +320,23 @@ namespace OpenNest.Forms
|
|||||||
}
|
}
|
||||||
|
|
||||||
platesListView.Items.AddRange(items);
|
platesListView.Items.AddRange(items);
|
||||||
|
|
||||||
|
if (CurrentPlateIndex < platesListView.Items.Count)
|
||||||
|
platesListView.Items[CurrentPlateIndex].Selected = true;
|
||||||
|
|
||||||
|
platesListView.EndUpdate();
|
||||||
|
updatingPlateList = false;
|
||||||
|
|
||||||
|
if (focused != null && focused != platesListView)
|
||||||
|
focused.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Control GetFocusedControl()
|
||||||
|
{
|
||||||
|
var ctrl = this;
|
||||||
|
while (ctrl is ContainerControl container && container.ActiveControl != null)
|
||||||
|
return container.ActiveControl;
|
||||||
|
return ctrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateDrawingList()
|
public void UpdateDrawingList()
|
||||||
@@ -943,6 +969,13 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
private void Plates_PlateRemoved(object sender, ItemRemovedEventArgs<Plate> e)
|
private void Plates_PlateRemoved(object sender, ItemRemovedEventArgs<Plate> e)
|
||||||
{
|
{
|
||||||
|
if (suppressPlateNavigation)
|
||||||
|
{
|
||||||
|
UpdatePlateList();
|
||||||
|
UpdateRemovePlateButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (Nest.Plates.Count <= CurrentPlateIndex)
|
if (Nest.Plates.Count <= CurrentPlateIndex)
|
||||||
LoadLastPlate();
|
LoadLastPlate();
|
||||||
else
|
else
|
||||||
@@ -958,13 +991,43 @@ namespace OpenNest.Forms
|
|||||||
tabControl1.SelectedIndex = 0;
|
tabControl1.SelectedIndex = 0;
|
||||||
UpdatePlateList();
|
UpdatePlateList();
|
||||||
UpdateRemovePlateButton();
|
UpdateRemovePlateButton();
|
||||||
|
|
||||||
|
if (!suppressPlateNavigation)
|
||||||
|
{
|
||||||
LoadLastPlate();
|
LoadLastPlate();
|
||||||
PlateView.ZoomToFit();
|
PlateView.ZoomToFit();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateRemovePlateButton()
|
private void UpdateRemovePlateButton()
|
||||||
{
|
{
|
||||||
btnRemovePlate.Enabled = Nest.Plates.Count > 1;
|
var plate = Nest.Plates.Count > 0 ? Nest.Plates[CurrentPlateIndex] : null;
|
||||||
|
btnRemovePlate.Enabled = Nest.Plates.Count > 1 && plate != null && plate.Parts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures a single empty sentinel plate exists at the end of the nest.
|
||||||
|
/// </summary>
|
||||||
|
public void EnsureSentinelPlate()
|
||||||
|
{
|
||||||
|
suppressPlateNavigation = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Nest.Plates.Count == 0 || Nest.Plates[^1].Parts.Count > 0)
|
||||||
|
Nest.CreatePlate();
|
||||||
|
|
||||||
|
// Trim excess trailing empty plates back to one
|
||||||
|
while (Nest.Plates.Count > 1
|
||||||
|
&& Nest.Plates[^1].Parts.Count == 0
|
||||||
|
&& Nest.Plates[^2].Parts.Count == 0)
|
||||||
|
{
|
||||||
|
Nest.Plates.RemoveAt(Nest.Plates.Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
suppressPlateNavigation = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -975,6 +1038,13 @@ namespace OpenNest.Forms
|
|||||||
item.Text = id.ToString();
|
item.Text = id.ToString();
|
||||||
item.SubItems.Add(plate.Size.ToString());
|
item.SubItems.Add(plate.Size.ToString());
|
||||||
item.SubItems.Add(plate.Quantity.ToString());
|
item.SubItems.Add(plate.Quantity.ToString());
|
||||||
|
|
||||||
|
var partCount = plate.Parts.Count(p => !p.BaseDrawing.IsCutOff);
|
||||||
|
item.SubItems.Add(partCount.ToString());
|
||||||
|
|
||||||
|
var util = plate.Utilization();
|
||||||
|
item.SubItems.Add(partCount > 0 ? $"{util:P0}" : "");
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,12 +1052,29 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
updateDrawingListTimer.Stop();
|
updateDrawingListTimer.Stop();
|
||||||
updateDrawingListTimer.Start();
|
updateDrawingListTimer.Start();
|
||||||
|
DeferSentinelUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PlateView_PartAdded(object sender, ItemAddedEventArgs<Part> e)
|
private void PlateView_PartAdded(object sender, ItemAddedEventArgs<Part> e)
|
||||||
{
|
{
|
||||||
updateDrawingListTimer.Stop();
|
updateDrawingListTimer.Stop();
|
||||||
updateDrawingListTimer.Start();
|
updateDrawingListTimer.Start();
|
||||||
|
DeferSentinelUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeferSentinelUpdate()
|
||||||
|
{
|
||||||
|
if (sentinelUpdatePending)
|
||||||
|
return;
|
||||||
|
|
||||||
|
sentinelUpdatePending = true;
|
||||||
|
BeginInvoke(new MethodInvoker(() =>
|
||||||
|
{
|
||||||
|
sentinelUpdatePending = false;
|
||||||
|
EnsureSentinelPlate();
|
||||||
|
UpdatePlateList();
|
||||||
|
UpdateRemovePlateButton();
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawingListUpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
private void drawingListUpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||||
|
|||||||
@@ -827,6 +827,7 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
if (activeForm == null) return;
|
if (activeForm == null) return;
|
||||||
activeForm.Nest.Plates.RemoveEmptyPlates();
|
activeForm.Nest.Plates.RemoveEmptyPlates();
|
||||||
|
activeForm.EnsureSentinelPlate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadFirstPlate_Click(object sender, EventArgs e)
|
private void LoadFirstPlate_Click(object sender, EventArgs e)
|
||||||
@@ -963,6 +964,7 @@ namespace OpenNest.Forms
|
|||||||
SetNestingLockout(false);
|
SetNestingLockout(false);
|
||||||
nestingCts.Dispose();
|
nestingCts.Dispose();
|
||||||
nestingCts = null;
|
nestingCts = null;
|
||||||
|
activeForm.EnsureSentinelPlate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1033,6 +1035,14 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
plate.Size = new Geometry.Size(result.ChosenSize.Width, result.ChosenSize.Length);
|
plate.Size = new Geometry.Size(result.ChosenSize.Width, result.ChosenSize.Length);
|
||||||
nestParts = result.Parts;
|
nestParts = result.Parts;
|
||||||
|
|
||||||
|
// Deduct placed quantities — the optimizer clones items internally
|
||||||
|
// so the originals are untouched after dry runs.
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
var placed = nestParts.Count(p => p.BaseDrawing.Name == item.Drawing.Name);
|
||||||
|
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user