Files
OpenNest/OpenNest/Forms/AutoNestForm.cs
T

458 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using OpenNest.Engine;
namespace OpenNest.Forms
{
public partial class AutoNestForm : Form
{
private static readonly Regex SizePattern = new(
@"^(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)$",
RegexOptions.None,
TimeSpan.FromMilliseconds(250)
);
public AutoNestForm(Nest nest)
{
InitializeComponent();
cancelButton.CausesValidation = false;
SetupPartsGrid();
SetupPlateGrid();
LoadEngines();
LoadDrawings(nest);
LoadDefaultPlateOptions();
SetPlateOptimizerVisible(false);
partsGrid.DataError += PartsGrid_DataError;
}
public string EngineName
{
get { return engineComboBox.SelectedItem as string; }
set { engineComboBox.SelectedItem = value; }
}
public bool OptimizePlateSize
{
get { return optimizePlateSizeBox.Checked; }
set { optimizePlateSizeBox.Checked = value; }
}
public double SalvageRate
{
get
{
if (double.TryParse(salvageRateBox.Text, out var val))
return System.Math.Clamp(val / 100.0, 0, 1);
return 0.5;
}
set { salvageRateBox.Text = (value * 100).ToString("F0"); }
}
public double MinRemnantSize
{
get
{
if (double.TryParse(minRemnantBox.Text, out var val) && val > 0)
return val;
return 12.0;
}
}
private void LoadEngines()
{
foreach (var name in EngineSelection.UiEngineNames)
engineComboBox.Items.Add(name);
engineComboBox.SelectedItem = EngineSelection.EngineName;
}
private void SetupPartsGrid()
{
partsGrid.Columns.Add(
new DataGridViewTextBoxColumn
{
DataPropertyName = "DrawingName",
HeaderText = "Drawing Name",
Width = 160,
ReadOnly = true,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
}
);
partsGrid.Columns.Add(
new DataGridViewTextBoxColumn
{
DataPropertyName = "Quantity",
HeaderText = "Qty",
Width = 50,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
}
);
partsGrid.Columns.Add(
new DataGridViewTextBoxColumn
{
DataPropertyName = "Priority",
HeaderText = "Priority",
Width = 55,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
}
);
partsGrid.Columns.Add(
new DataGridViewTextBoxColumn
{
DataPropertyName = "RotationStart",
HeaderText = "Rot Start",
Width = 65,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
}
);
partsGrid.Columns.Add(
new DataGridViewTextBoxColumn
{
DataPropertyName = "RotationEnd",
HeaderText = "Rot End",
Width = 60,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
}
);
partsGrid.Columns.Add(
new DataGridViewTextBoxColumn
{
DataPropertyName = "StepAngle",
HeaderText = "Step",
Width = 55,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
}
);
partsGrid.CellValueChanged += PartsGrid_CellValueChanged;
partsGrid.CurrentCellDirtyStateChanged += (s, e) =>
{
if (partsGrid.IsCurrentCellDirty)
partsGrid.CommitEdit(DataGridViewDataErrorContexts.Commit);
};
}
private void SetupPlateGrid()
{
plateGrid.Columns.Add(
new DataGridViewTextBoxColumn
{
DataPropertyName = "Size",
HeaderText = "Size",
Width = 120,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
}
);
plateGrid.Columns.Add(
new DataGridViewTextBoxColumn
{
DataPropertyName = "Cost",
HeaderText = "Cost",
Width = 70,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
}
);
plateGrid.CellValidating += PlateGrid_CellValidating;
}
private void LoadDrawings(Nest nest)
{
var items = new List<DataGridViewItem>();
foreach (var drawing in nest.Drawings)
items.Add(GetDataGridViewItem(drawing));
partsGrid.DataSource = items;
UpdateSummary();
}
public List<NestItem> GetNestItems()
{
var nestItems = new List<NestItem>();
var gridItems = partsGrid.DataSource as List<DataGridViewItem>;
if (gridItems == null)
return nestItems;
foreach (var gridItem in gridItems)
{
if (gridItem.Quantity < 1)
continue;
var nestItem = new NestItem();
nestItem.Drawing = gridItem.RefDrawing;
nestItem.Priority = gridItem.Priority;
nestItem.Quantity = gridItem.Quantity;
nestItem.RotationEnd = gridItem.RotationEnd;
nestItem.RotationStart = gridItem.RotationStart;
nestItem.StepAngle = gridItem.StepAngle;
nestItems.Add(nestItem);
}
return nestItems;
}
public List<PlateOption> GetPlateOptions()
{
if (!TryGetPlateOptions(out var options, out var error))
throw new FormatException(error);
return options;
}
public bool TryGetPlateOptions(out List<PlateOption> options, out string error)
{
options = new List<PlateOption>();
error = null;
if (!plateGrid.EndEdit())
{
var value = plateGrid.EditingControl?.Text ?? plateGrid.CurrentCell?.Value?.ToString();
error = $"Invalid stock size '{value}'. Enter positive dimensions as W x L.";
return false;
}
var gridItems = plateGrid.DataSource as BindingList<PlateOptionItem>;
if (gridItems == null)
return true;
var validated = new List<PlateOption>();
for (var index = 0; index < gridItems.Count; index++)
{
var item = gridItems[index];
if (string.IsNullOrWhiteSpace(item.Size))
continue;
if (!TryParseSize(item.Size, out var width, out var length))
{
error = $"Invalid stock size '{item.Size}' in row {index + 1}. Enter positive dimensions as W x L.";
if (index < plateGrid.Rows.Count)
plateGrid.Rows[index].ErrorText = error;
return false;
}
if (index < plateGrid.Rows.Count)
plateGrid.Rows[index].ErrorText = "";
validated.Add(new PlateOption { Width = width, Length = length, Cost = item.Cost });
}
// Publish only a completely validated collection, never a valid prefix.
options = validated;
return true;
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (DialogResult == DialogResult.OK && OptimizePlateSize
&& !TryGetPlateOptions(out _, out var error))
{
e.Cancel = true;
DialogResult = DialogResult.None;
ReportStockValidationFailure(error);
}
base.OnFormClosing(e);
}
internal virtual void ReportStockValidationFailure(string error)
{
MessageBox.Show(this, error, "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
public void LoadPlateOptions(List<PlateOption> options, double salvageRate)
{
if (options != null && options.Count > 0)
{
var items = options
.Select(o => new PlateOptionItem
{
Size = FormatSize(o.Width, o.Length),
Cost = o.Cost,
})
.ToList();
plateGrid.DataSource = new BindingList<PlateOptionItem>(items);
optimizePlateSizeBox.Checked = true;
}
SalvageRate = salvageRate;
}
private void LoadDefaultPlateOptions()
{
// A bound DataGridView needs IBindingList.AddNew support for its blank last row.
var items = new BindingList<PlateOptionItem>
{
new() { Size = "48 x 96", Cost = 0 },
new() { Size = "48 x 120", Cost = 0 },
new() { Size = "48 x 144", Cost = 0 },
new() { Size = "60 x 96", Cost = 0 },
new() { Size = "60 x 120", Cost = 0 },
new() { Size = "60 x 144", Cost = 0 },
new() { Size = "72 x 96", Cost = 0 },
new() { Size = "72 x 120", Cost = 0 },
new() { Size = "72 x 144", Cost = 0 },
};
plateGrid.DataSource = items;
}
private void optimizePlateSizeBox_CheckedChanged(object sender, EventArgs e)
{
SetPlateOptimizerVisible(optimizePlateSizeBox.Checked);
}
private void SetPlateOptimizerVisible(bool visible)
{
plateGrid.Visible = visible;
salvageRateLabel.Visible = visible;
salvageRateBox.Visible = visible;
salvageRatePercentLabel.Visible = visible;
minRemnantLabel.Visible = visible;
minRemnantBox.Visible = visible;
}
private void UpdateSummary()
{
var gridItems = partsGrid.DataSource as List<DataGridViewItem>;
if (gridItems == null)
{
summaryLabel.Text = "";
return;
}
var totalQty = gridItems.Sum(i => System.Math.Max(0, i.Quantity));
var drawingCount = gridItems.Count(i => i.Quantity > 0);
summaryLabel.Text = $"{totalQty} parts across {drawingCount} drawings";
}
private void PartsGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0)
return;
if (partsGrid.Columns[e.ColumnIndex].DataPropertyName == "Quantity")
UpdateSummary();
}
private void PlateGrid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
if (plateGrid.Columns[e.ColumnIndex].DataPropertyName != "Size")
return;
var value = e.FormattedValue?.ToString();
if (string.IsNullOrWhiteSpace(value))
{
plateGrid.Rows[e.RowIndex].ErrorText = "";
return;
}
if (!TryParseSize(value, out _, out _))
{
e.Cancel = true;
plateGrid.Rows[e.RowIndex].ErrorText = "Enter size as W x L (e.g. 48 x 96)";
}
else
{
plateGrid.Rows[e.RowIndex].ErrorText = "";
}
}
private bool TryParseSize(string value, out double width, out double length)
{
width = 0;
length = 0;
if (string.IsNullOrWhiteSpace(value))
return false;
try
{
var match = MatchStockSize(value.Trim());
if (!match.Success)
return false;
var parsedWidth = double.Parse(
match.Groups[1].Value,
System.Globalization.CultureInfo.InvariantCulture
);
var parsedLength = double.Parse(
match.Groups[2].Value,
System.Globalization.CultureInfo.InvariantCulture
);
if (!double.IsFinite(parsedWidth) || !double.IsFinite(parsedLength)
|| parsedWidth <= 0 || parsedLength <= 0)
return false;
width = parsedWidth;
length = parsedLength;
return true;
}
catch (RegexMatchTimeoutException)
{
return false;
}
catch (FormatException)
{
return false;
}
}
internal virtual Match MatchStockSize(string value) => SizePattern.Match(value);
private static string FormatSize(double width, double length)
{
return $"{width:G} x {length:G}";
}
private void PartsGrid_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
MessageBox.Show(
"Invalid input. Expected input type is "
+ partsGrid[e.ColumnIndex, e.RowIndex].ValueType.Name
);
}
private DataGridViewItem GetDataGridViewItem(Drawing dwg)
{
var item = new DataGridViewItem();
item.RefDrawing = dwg;
item.Quantity = dwg.Quantity.Remaining > 0 ? dwg.Quantity.Remaining : 0;
item.Priority = dwg.Priority;
item.RotationStart = dwg.Constraints.StartAngle;
item.RotationEnd = dwg.Constraints.EndAngle;
item.StepAngle = dwg.Constraints.StepAngle;
return item;
}
private class DataGridViewItem
{
internal Drawing RefDrawing { get; set; }
[ReadOnly(true)]
[DisplayName("Drawing Name")]
public string DrawingName
{
get { return RefDrawing.Name; }
set { RefDrawing.Name = value; }
}
public int Quantity { get; set; }
public int Priority { get; set; }
[DisplayName("Rot Start")]
public double RotationStart { get; set; }
[DisplayName("Rot End")]
public double RotationEnd { get; set; }
[DisplayName("Step")]
public double StepAngle { get; set; }
}
private class PlateOptionItem
{
public PlateOptionItem() { }
public string Size { get; set; }
public double Cost { get; set; }
}
}
}