Files
OpenNest/OpenNest/Forms/AutoNestForm.cs
AJ Isaacs 81c167320d feat: redesign AutoNest dialog with grouped layout and engine selector
Rebuild the dialog from a flat layout into grouped sections: engine
selector at top, Parts group with rotation columns and summary label,
Options group, collapsible Plate Optimizer with single-field size
parsing, and a clean button bar. Adds engine sync between dialog and
toolbar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:16:09 -04:00

355 lines
12 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*)$");
public AutoNestForm(Nest nest)
{
InitializeComponent();
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 AllowPlateCreation
{
get { return createNewPlatesAsNeededBox.Checked; }
set { createNewPlatesAsNeededBox.Checked = 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"); }
}
private void LoadEngines()
{
foreach (var engine in NestEngineRegistry.AvailableEngines)
engineComboBox.Items.Add(engine.Name);
engineComboBox.SelectedItem = NestEngineRegistry.ActiveEngineName;
}
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()
{
var result = new List<PlateOption>();
var gridItems = plateGrid.DataSource as List<PlateOptionItem>;
if (gridItems == null) return result;
foreach (var item in gridItems)
{
if (!TryParseSize(item.Size, out var width, out var length))
continue;
if (width <= 0 || length <= 0)
continue;
result.Add(new PlateOption
{
Width = width,
Length = length,
Cost = item.Cost,
});
}
return result;
}
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 = items;
optimizePlateSizeBox.Checked = true;
}
SalvageRate = salvageRate;
}
private void LoadDefaultPlateOptions()
{
var items = new List<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;
}
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))
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 static bool TryParseSize(string value, out double width, out double length)
{
width = 0;
length = 0;
if (string.IsNullOrWhiteSpace(value)) return false;
var match = SizePattern.Match(value.Trim());
if (!match.Success) return false;
width = double.Parse(match.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture);
length = double.Parse(match.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture);
return true;
}
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 string Size { get; set; }
public double Cost { get; set; }
}
}
}