From ae9a63b5cee60c1bfa969a8393ee76f6f2618655 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 27 Mar 2026 18:50:06 -0400 Subject: [PATCH] feat: add Parts/Groups tabs with editable material, thickness, and per-group plate sizes - Parts tab: shows all BOM items, editable Material/Thickness for matched rows, grayed-out rows for items without DXF files - Groups tab: auto-computed from parts with editable Plate Width/Length per material+thickness group - Editing Material/Thickness on Parts tab immediately re-groups - Per-group plate sizes preserved across re-groups Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest/Forms/BomImportForm.Designer.cs | 446 +++++++++++++++-------- OpenNest/Forms/BomImportForm.cs | 360 +++++++++++++++--- 2 files changed, 606 insertions(+), 200 deletions(-) diff --git a/OpenNest/Forms/BomImportForm.Designer.cs b/OpenNest/Forms/BomImportForm.Designer.cs index dd22230..c213982 100644 --- a/OpenNest/Forms/BomImportForm.Designer.cs +++ b/OpenNest/Forms/BomImportForm.Designer.cs @@ -16,149 +16,270 @@ namespace OpenNest.Forms private void InitializeComponent() { grpInput = new System.Windows.Forms.GroupBox(); + tbl = new System.Windows.Forms.TableLayoutPanel(); + lblJobName = new System.Windows.Forms.Label(); txtJobName = new System.Windows.Forms.TextBox(); + lblBomFile = new System.Windows.Forms.Label(); txtBomFile = new System.Windows.Forms.TextBox(); btnBrowseBom = new System.Windows.Forms.Button(); + lblDxfFolder = new System.Windows.Forms.Label(); txtDxfFolder = new System.Windows.Forms.TextBox(); btnBrowseDxf = new System.Windows.Forms.Button(); + lblPlateSize = new System.Windows.Forms.Label(); + platePanel = new System.Windows.Forms.FlowLayoutPanel(); txtPlateWidth = new System.Windows.Forms.TextBox(); + lblPlateX = new System.Windows.Forms.Label(); txtPlateLength = new System.Windows.Forms.TextBox(); btnAnalyze = new System.Windows.Forms.Button(); - grpGroups = new System.Windows.Forms.GroupBox(); + tabControl = new System.Windows.Forms.TabControl(); + tabParts = new System.Windows.Forms.TabPage(); + dgvParts = new System.Windows.Forms.DataGridView(); + tabGroups = new System.Windows.Forms.TabPage(); dgvGroups = new System.Windows.Forms.DataGridView(); pnlBottom = new System.Windows.Forms.Panel(); lblSummary = new System.Windows.Forms.Label(); - btnClose = new System.Windows.Forms.Button(); btnCreateNests = new System.Windows.Forms.Button(); - - var tbl = new System.Windows.Forms.TableLayoutPanel(); - var lblJobName = new System.Windows.Forms.Label(); - var lblBomFile = new System.Windows.Forms.Label(); - var lblDxfFolder = new System.Windows.Forms.Label(); - var lblPlateSize = new System.Windows.Forms.Label(); - var lblPlateX = new System.Windows.Forms.Label(); - var platePanel = new System.Windows.Forms.FlowLayoutPanel(); - + btnClose = new System.Windows.Forms.Button(); grpInput.SuspendLayout(); - grpGroups.SuspendLayout(); + tbl.SuspendLayout(); + platePanel.SuspendLayout(); + tabControl.SuspendLayout(); + tabParts.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)dgvParts).BeginInit(); + tabGroups.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)dgvGroups).BeginInit(); pnlBottom.SuspendLayout(); SuspendLayout(); - - // ---- TableLayoutPanel for input fields ---- - tbl.ColumnCount = 3; - tbl.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.AutoSize)); - tbl.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - tbl.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.AutoSize)); - tbl.RowCount = 5; - tbl.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize)); - tbl.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize)); - tbl.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize)); - tbl.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize)); - tbl.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize)); - tbl.Dock = System.Windows.Forms.DockStyle.Fill; - tbl.Padding = new System.Windows.Forms.Padding(3); - - // Row 0 — Job Name - lblJobName.Text = "Job Name:"; - lblJobName.AutoSize = true; - lblJobName.Anchor = System.Windows.Forms.AnchorStyles.Left; - lblJobName.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); - tbl.Controls.Add(lblJobName, 0, 0); - - txtJobName.Dock = System.Windows.Forms.DockStyle.Fill; - txtJobName.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); - tbl.Controls.Add(txtJobName, 1, 0); - tbl.SetColumnSpan(txtJobName, 2); - - // Row 1 — BOM File - lblBomFile.Text = "BOM File:"; - lblBomFile.AutoSize = true; - lblBomFile.Anchor = System.Windows.Forms.AnchorStyles.Left; - lblBomFile.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); - tbl.Controls.Add(lblBomFile, 0, 1); - - txtBomFile.Dock = System.Windows.Forms.DockStyle.Fill; - txtBomFile.ReadOnly = true; - txtBomFile.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); - tbl.Controls.Add(txtBomFile, 1, 1); - - btnBrowseBom.Text = "..."; - btnBrowseBom.Size = new System.Drawing.Size(35, 25); - btnBrowseBom.Margin = new System.Windows.Forms.Padding(0, 5, 3, 3); - btnBrowseBom.Click += new System.EventHandler(BrowseBom_Click); - tbl.Controls.Add(btnBrowseBom, 2, 1); - - // Row 2 — DXF Folder - lblDxfFolder.Text = "DXF Folder:"; - lblDxfFolder.AutoSize = true; - lblDxfFolder.Anchor = System.Windows.Forms.AnchorStyles.Left; - lblDxfFolder.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); - tbl.Controls.Add(lblDxfFolder, 0, 2); - - txtDxfFolder.Dock = System.Windows.Forms.DockStyle.Fill; - txtDxfFolder.ReadOnly = true; - txtDxfFolder.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); - tbl.Controls.Add(txtDxfFolder, 1, 2); - - btnBrowseDxf.Text = "..."; - btnBrowseDxf.Size = new System.Drawing.Size(35, 25); - btnBrowseDxf.Margin = new System.Windows.Forms.Padding(0, 5, 3, 3); - btnBrowseDxf.Click += new System.EventHandler(BrowseDxf_Click); - tbl.Controls.Add(btnBrowseDxf, 2, 2); - - // Row 3 — Plate Size - lblPlateSize.Text = "Plate Size:"; - lblPlateSize.AutoSize = true; - lblPlateSize.Anchor = System.Windows.Forms.AnchorStyles.Left; - lblPlateSize.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); - tbl.Controls.Add(lblPlateSize, 0, 3); - - platePanel.AutoSize = true; - platePanel.WrapContents = false; - platePanel.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); - txtPlateWidth.Size = new System.Drawing.Size(60, 23); - txtPlateWidth.Text = "60"; - lblPlateX.Text = " x "; - lblPlateX.AutoSize = true; - lblPlateX.Anchor = System.Windows.Forms.AnchorStyles.Left; - txtPlateLength.Size = new System.Drawing.Size(60, 23); - txtPlateLength.Text = "120"; - platePanel.Controls.Add(txtPlateWidth); - platePanel.Controls.Add(lblPlateX); - platePanel.Controls.Add(txtPlateLength); - tbl.Controls.Add(platePanel, 1, 3); - - // Row 4 — Analyze button - btnAnalyze.Text = "Analyze"; - btnAnalyze.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); - btnAnalyze.Size = new System.Drawing.Size(110, 30); - btnAnalyze.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); - btnAnalyze.Anchor = System.Windows.Forms.AnchorStyles.Right; - btnAnalyze.Click += new System.EventHandler(Analyze_Click); - tbl.Controls.Add(btnAnalyze, 1, 4); - tbl.SetColumnSpan(btnAnalyze, 2); - - // ---- grpInput ---- + // + // grpInput + // grpInput.Controls.Add(tbl); grpInput.Dock = System.Windows.Forms.DockStyle.Top; + grpInput.Location = new System.Drawing.Point(0, 0); grpInput.Name = "grpInput"; - grpInput.Height = 200; grpInput.Padding = new System.Windows.Forms.Padding(6); + grpInput.Size = new System.Drawing.Size(804, 200); grpInput.TabIndex = 0; grpInput.TabStop = false; grpInput.Text = "Input"; - - // ---- grpGroups ---- - grpGroups.Controls.Add(dgvGroups); - grpGroups.Dock = System.Windows.Forms.DockStyle.Fill; - grpGroups.Name = "grpGroups"; - grpGroups.Padding = new System.Windows.Forms.Padding(10, 6, 10, 6); - grpGroups.TabIndex = 1; - grpGroups.TabStop = false; - grpGroups.Text = "Material Groups"; - - // ---- dgvGroups ---- + // + // tbl + // + tbl.ColumnCount = 3; + tbl.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + tbl.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + tbl.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + tbl.Controls.Add(lblJobName, 0, 0); + tbl.Controls.Add(txtJobName, 1, 0); + tbl.Controls.Add(lblBomFile, 0, 1); + tbl.Controls.Add(txtBomFile, 1, 1); + tbl.Controls.Add(btnBrowseBom, 2, 1); + tbl.Controls.Add(lblDxfFolder, 0, 2); + tbl.Controls.Add(txtDxfFolder, 1, 2); + tbl.Controls.Add(btnBrowseDxf, 2, 2); + tbl.Controls.Add(lblPlateSize, 0, 3); + tbl.Controls.Add(platePanel, 1, 3); + tbl.Controls.Add(btnAnalyze, 1, 4); + tbl.Dock = System.Windows.Forms.DockStyle.Fill; + tbl.Location = new System.Drawing.Point(6, 22); + tbl.Name = "tbl"; + tbl.Padding = new System.Windows.Forms.Padding(3); + tbl.RowCount = 5; + tbl.RowStyles.Add(new System.Windows.Forms.RowStyle()); + tbl.RowStyles.Add(new System.Windows.Forms.RowStyle()); + tbl.RowStyles.Add(new System.Windows.Forms.RowStyle()); + tbl.RowStyles.Add(new System.Windows.Forms.RowStyle()); + tbl.RowStyles.Add(new System.Windows.Forms.RowStyle()); + tbl.Size = new System.Drawing.Size(792, 172); + tbl.TabIndex = 0; + // + // lblJobName + // + lblJobName.Anchor = System.Windows.Forms.AnchorStyles.Left; + lblJobName.AutoSize = true; + lblJobName.Location = new System.Drawing.Point(6, 13); + lblJobName.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); + lblJobName.Name = "lblJobName"; + lblJobName.Size = new System.Drawing.Size(63, 15); + lblJobName.TabIndex = 0; + lblJobName.Text = "Job Name:"; + // + // txtJobName + // + tbl.SetColumnSpan(txtJobName, 2); + txtJobName.Dock = System.Windows.Forms.DockStyle.Fill; + txtJobName.Location = new System.Drawing.Point(79, 9); + txtJobName.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); + txtJobName.Name = "txtJobName"; + txtJobName.Size = new System.Drawing.Size(707, 23); + txtJobName.TabIndex = 1; + // + // lblBomFile + // + lblBomFile.Anchor = System.Windows.Forms.AnchorStyles.Left; + lblBomFile.AutoSize = true; + lblBomFile.Location = new System.Drawing.Point(6, 45); + lblBomFile.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); + lblBomFile.Name = "lblBomFile"; + lblBomFile.Size = new System.Drawing.Size(58, 15); + lblBomFile.TabIndex = 2; + lblBomFile.Text = "BOM File:"; + // + // txtBomFile + // + txtBomFile.Dock = System.Windows.Forms.DockStyle.Fill; + txtBomFile.Location = new System.Drawing.Point(79, 41); + txtBomFile.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); + txtBomFile.Name = "txtBomFile"; + txtBomFile.ReadOnly = true; + txtBomFile.Size = new System.Drawing.Size(669, 23); + txtBomFile.TabIndex = 3; + // + // btnBrowseBom + // + btnBrowseBom.Location = new System.Drawing.Point(751, 40); + btnBrowseBom.Margin = new System.Windows.Forms.Padding(0, 5, 3, 3); + btnBrowseBom.Name = "btnBrowseBom"; + btnBrowseBom.Size = new System.Drawing.Size(35, 25); + btnBrowseBom.TabIndex = 4; + btnBrowseBom.Text = "..."; + btnBrowseBom.Click += BrowseBom_Click; + // + // lblDxfFolder + // + lblDxfFolder.Anchor = System.Windows.Forms.AnchorStyles.Left; + lblDxfFolder.AutoSize = true; + lblDxfFolder.Location = new System.Drawing.Point(6, 78); + lblDxfFolder.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); + lblDxfFolder.Name = "lblDxfFolder"; + lblDxfFolder.Size = new System.Drawing.Size(67, 15); + lblDxfFolder.TabIndex = 5; + lblDxfFolder.Text = "DXF Folder:"; + // + // txtDxfFolder + // + txtDxfFolder.Dock = System.Windows.Forms.DockStyle.Fill; + txtDxfFolder.Location = new System.Drawing.Point(79, 74); + txtDxfFolder.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); + txtDxfFolder.Name = "txtDxfFolder"; + txtDxfFolder.ReadOnly = true; + txtDxfFolder.Size = new System.Drawing.Size(669, 23); + txtDxfFolder.TabIndex = 6; + // + // btnBrowseDxf + // + btnBrowseDxf.Location = new System.Drawing.Point(751, 73); + btnBrowseDxf.Margin = new System.Windows.Forms.Padding(0, 5, 3, 3); + btnBrowseDxf.Name = "btnBrowseDxf"; + btnBrowseDxf.Size = new System.Drawing.Size(35, 25); + btnBrowseDxf.TabIndex = 7; + btnBrowseDxf.Text = "..."; + btnBrowseDxf.Click += BrowseDxf_Click; + // + // lblPlateSize + // + lblPlateSize.Anchor = System.Windows.Forms.AnchorStyles.Left; + lblPlateSize.AutoSize = true; + lblPlateSize.Location = new System.Drawing.Point(6, 112); + lblPlateSize.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); + lblPlateSize.Name = "lblPlateSize"; + lblPlateSize.Size = new System.Drawing.Size(59, 15); + lblPlateSize.TabIndex = 8; + lblPlateSize.Text = "Plate Size:"; + // + // platePanel + // + platePanel.AutoSize = true; + platePanel.Controls.Add(txtPlateWidth); + platePanel.Controls.Add(lblPlateX); + platePanel.Controls.Add(txtPlateLength); + platePanel.Location = new System.Drawing.Point(76, 104); + platePanel.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + platePanel.Name = "platePanel"; + platePanel.Size = new System.Drawing.Size(156, 29); + platePanel.TabIndex = 9; + platePanel.WrapContents = false; + // + // txtPlateWidth + // + txtPlateWidth.Location = new System.Drawing.Point(3, 3); + txtPlateWidth.Name = "txtPlateWidth"; + txtPlateWidth.Size = new System.Drawing.Size(60, 23); + txtPlateWidth.TabIndex = 0; + txtPlateWidth.Text = "60"; + // + // lblPlateX + // + lblPlateX.Anchor = System.Windows.Forms.AnchorStyles.Left; + lblPlateX.AutoSize = true; + lblPlateX.Location = new System.Drawing.Point(69, 7); + lblPlateX.Name = "lblPlateX"; + lblPlateX.Size = new System.Drawing.Size(18, 15); + lblPlateX.TabIndex = 1; + lblPlateX.Text = " x "; + // + // txtPlateLength + // + txtPlateLength.Location = new System.Drawing.Point(93, 3); + txtPlateLength.Name = "txtPlateLength"; + txtPlateLength.Size = new System.Drawing.Size(60, 23); + txtPlateLength.TabIndex = 2; + txtPlateLength.Text = "120"; + // + // btnAnalyze + // + btnAnalyze.Anchor = System.Windows.Forms.AnchorStyles.Right; + tbl.SetColumnSpan(btnAnalyze, 2); + btnAnalyze.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + btnAnalyze.Location = new System.Drawing.Point(676, 142); + btnAnalyze.Margin = new System.Windows.Forms.Padding(3, 6, 3, 3); + btnAnalyze.Name = "btnAnalyze"; + btnAnalyze.Size = new System.Drawing.Size(110, 30); + btnAnalyze.TabIndex = 10; + btnAnalyze.Text = "Analyze"; + btnAnalyze.Click += Analyze_Click; + // + // tabControl + // + tabControl.Controls.Add(tabParts); + tabControl.Controls.Add(tabGroups); + tabControl.Dock = System.Windows.Forms.DockStyle.Fill; + tabControl.Location = new System.Drawing.Point(0, 200); + tabControl.Name = "tabControl"; + tabControl.SelectedIndex = 0; + tabControl.Size = new System.Drawing.Size(804, 365); + tabControl.TabIndex = 1; + // + // tabParts + // + tabParts.Controls.Add(dgvParts); + tabParts.Name = "tabParts"; + tabParts.Padding = new System.Windows.Forms.Padding(3); + tabParts.Text = "Parts"; + // + // dgvParts + // + dgvParts.AllowUserToAddRows = false; + dgvParts.AllowUserToDeleteRows = false; + dgvParts.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill; + dgvParts.BackgroundColor = System.Drawing.SystemColors.Window; + dgvParts.BorderStyle = System.Windows.Forms.BorderStyle.None; + dgvParts.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + dgvParts.Dock = System.Windows.Forms.DockStyle.Fill; + dgvParts.Name = "dgvParts"; + dgvParts.RowHeadersVisible = false; + dgvParts.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect; + dgvParts.TabIndex = 0; + // + // tabGroups + // + tabGroups.Controls.Add(dgvGroups); + tabGroups.Name = "tabGroups"; + tabGroups.Padding = new System.Windows.Forms.Padding(3); + tabGroups.Text = "Groups"; + // + // dgvGroups + // dgvGroups.AllowUserToAddRows = false; dgvGroups.AllowUserToDeleteRows = false; dgvGroups.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill; @@ -167,67 +288,82 @@ namespace OpenNest.Forms dgvGroups.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; dgvGroups.Dock = System.Windows.Forms.DockStyle.Fill; dgvGroups.Name = "dgvGroups"; - dgvGroups.ReadOnly = true; dgvGroups.RowHeadersVisible = false; dgvGroups.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect; dgvGroups.TabIndex = 0; - - // ---- pnlBottom ---- + // + // pnlBottom + // pnlBottom.Controls.Add(lblSummary); pnlBottom.Controls.Add(btnCreateNests); pnlBottom.Controls.Add(btnClose); pnlBottom.Dock = System.Windows.Forms.DockStyle.Bottom; - pnlBottom.Height = 50; + pnlBottom.Location = new System.Drawing.Point(0, 565); pnlBottom.Name = "pnlBottom"; - pnlBottom.Padding = new System.Windows.Forms.Padding(10, 10, 10, 10); + pnlBottom.Padding = new System.Windows.Forms.Padding(10); + pnlBottom.Size = new System.Drawing.Size(804, 50); pnlBottom.TabIndex = 2; - - // ---- lblSummary ---- + // + // lblSummary + // lblSummary.AutoSize = true; - lblSummary.ForeColor = System.Drawing.Color.Gray; lblSummary.Dock = System.Windows.Forms.DockStyle.Left; + lblSummary.ForeColor = System.Drawing.Color.Gray; + lblSummary.Location = new System.Drawing.Point(10, 10); lblSummary.Name = "lblSummary"; + lblSummary.Size = new System.Drawing.Size(0, 15); lblSummary.TabIndex = 0; lblSummary.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; - - // ---- btnClose ---- - btnClose.DialogResult = System.Windows.Forms.DialogResult.Cancel; - btnClose.Dock = System.Windows.Forms.DockStyle.Right; - btnClose.Name = "btnClose"; - btnClose.Size = new System.Drawing.Size(80, 30); - btnClose.TabIndex = 2; - btnClose.Text = "Close"; - btnClose.Click += new System.EventHandler(BtnClose_Click); - - // ---- btnCreateNests ---- + // + // btnCreateNests + // + btnCreateNests.Dock = System.Windows.Forms.DockStyle.Right; btnCreateNests.Enabled = false; btnCreateNests.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); - btnCreateNests.Dock = System.Windows.Forms.DockStyle.Right; + btnCreateNests.Location = new System.Drawing.Point(604, 10); + btnCreateNests.Margin = new System.Windows.Forms.Padding(0, 0, 6, 0); btnCreateNests.Name = "btnCreateNests"; btnCreateNests.Size = new System.Drawing.Size(110, 30); btnCreateNests.TabIndex = 1; btnCreateNests.Text = "Create Nests"; - btnCreateNests.Margin = new System.Windows.Forms.Padding(0, 0, 6, 0); - btnCreateNests.Click += new System.EventHandler(CreateNests_Click); - - // ---- BomImportForm ---- + btnCreateNests.Click += CreateNests_Click; + // + // btnClose + // + btnClose.DialogResult = System.Windows.Forms.DialogResult.Cancel; + btnClose.Dock = System.Windows.Forms.DockStyle.Right; + btnClose.Location = new System.Drawing.Point(714, 10); + btnClose.Name = "btnClose"; + btnClose.Size = new System.Drawing.Size(80, 30); + btnClose.TabIndex = 2; + btnClose.Text = "Close"; + btnClose.Click += BtnClose_Click; + // + // BomImportForm + // AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; CancelButton = btnClose; - ClientSize = new System.Drawing.Size(520, 500); - Controls.Add(grpGroups); + ClientSize = new System.Drawing.Size(804, 615); + Controls.Add(tabControl); Controls.Add(pnlBottom); Controls.Add(grpInput); Font = new System.Drawing.Font("Segoe UI", 9F); - MinimumSize = new System.Drawing.Size(400, 350); MaximizeBox = false; + MinimumSize = new System.Drawing.Size(400, 350); Name = "BomImportForm"; StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; Text = "Import BOM"; grpInput.ResumeLayout(false); - grpInput.PerformLayout(); - grpGroups.ResumeLayout(false); + tbl.ResumeLayout(false); + tbl.PerformLayout(); + platePanel.ResumeLayout(false); + platePanel.PerformLayout(); + tabParts.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)dgvParts).EndInit(); + tabGroups.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)dgvGroups).EndInit(); + tabControl.ResumeLayout(false); pnlBottom.ResumeLayout(false); pnlBottom.PerformLayout(); ResumeLayout(false); @@ -244,11 +380,21 @@ namespace OpenNest.Forms private System.Windows.Forms.TextBox txtPlateWidth; private System.Windows.Forms.TextBox txtPlateLength; private System.Windows.Forms.Button btnAnalyze; - private System.Windows.Forms.GroupBox grpGroups; + private System.Windows.Forms.TabControl tabControl; + private System.Windows.Forms.TabPage tabParts; + private System.Windows.Forms.DataGridView dgvParts; + private System.Windows.Forms.TabPage tabGroups; private System.Windows.Forms.DataGridView dgvGroups; private System.Windows.Forms.Panel pnlBottom; private System.Windows.Forms.Label lblSummary; private System.Windows.Forms.Button btnCreateNests; private System.Windows.Forms.Button btnClose; + private System.Windows.Forms.TableLayoutPanel tbl; + private System.Windows.Forms.Label lblJobName; + private System.Windows.Forms.Label lblBomFile; + private System.Windows.Forms.Label lblDxfFolder; + private System.Windows.Forms.Label lblPlateSize; + private System.Windows.Forms.FlowLayoutPanel platePanel; + private System.Windows.Forms.Label lblPlateX; } } diff --git a/OpenNest/Forms/BomImportForm.cs b/OpenNest/Forms/BomImportForm.cs index e8da2c4..9d33466 100644 --- a/OpenNest/Forms/BomImportForm.cs +++ b/OpenNest/Forms/BomImportForm.cs @@ -6,6 +6,7 @@ using OpenNest.IO.Bom; using System; using System.Collections.Generic; using System.Data; +using System.Drawing; using System.IO; using System.Linq; using System.Windows.Forms; @@ -14,15 +15,21 @@ namespace OpenNest.Forms { public partial class BomImportForm : Form { - private BomAnalysis _analysis; + private List _parts; + private Dictionary _plateSizes; + private bool _suppressRegroup; public Form MdiParentForm { get; set; } public BomImportForm() { InitializeComponent(); + _parts = new List(); + _plateSizes = new Dictionary(); } + #region File Browsing + private void BrowseBom_Click(object sender, EventArgs e) { using var dlg = new OpenFileDialog @@ -37,11 +44,9 @@ namespace OpenNest.Forms txtBomFile.Text = dlg.FileName; - // Default DXF folder to the same directory as the BOM file if (string.IsNullOrWhiteSpace(txtDxfFolder.Text)) txtDxfFolder.Text = Path.GetDirectoryName(dlg.FileName); - // Derive job name by stripping " BOM" suffix and extension if (string.IsNullOrWhiteSpace(txtJobName.Text)) { var name = Path.GetFileNameWithoutExtension(dlg.FileName); @@ -65,6 +70,10 @@ namespace OpenNest.Forms txtDxfFolder.Text = dlg.SelectedPath; } + #endregion + + #region Analyze + private void Analyze_Click(object sender, EventArgs e) { if (!File.Exists(txtBomFile.Text)) @@ -80,11 +89,13 @@ namespace OpenNest.Forms using (var reader = new BomReader(txtBomFile.Text)) items = reader.GetItems(); - _analysis = BomAnalyzer.Analyze(items, txtDxfFolder.Text); - - PopulateGrid(_analysis); - UpdateSummary(_analysis); - btnCreateNests.Enabled = _analysis.Groups.Count > 0; + var analysis = BomAnalyzer.Analyze(items, txtDxfFolder.Text); + BuildPartRows(items, analysis); + PopulatePartsGrid(); + RebuildGroups(); + UpdateSummary(); + btnCreateNests.Enabled = true; + tabControl.SelectedTab = tabParts; } catch (Exception ex) { @@ -93,53 +104,280 @@ namespace OpenNest.Forms } } - private void PopulateGrid(BomAnalysis analysis) + private void BuildPartRows(List items, BomAnalysis analysis) { - var table = new DataTable(); - table.Columns.Add("Material", typeof(string)); - table.Columns.Add("Thickness", typeof(string)); - table.Columns.Add("Parts", typeof(int)); - table.Columns.Add("Total Qty", typeof(int)); + var matchedPaths = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var group in analysis.Groups) + foreach (var part in group.Parts) + if (part.DxfPath != null) + matchedPaths[part.Item.FileName ?? ""] = part.DxfPath; + + _parts = new List(); + + foreach (var item in items) { - var partCount = group.Parts.Count; - var totalQty = group.Parts.Sum(p => p.Item.Qty ?? 0); - table.Rows.Add(group.Material, group.Thickness.ToString("0.###"), partCount, totalQty); + var row = new BomPartRow + { + ItemNum = item.ItemNum, + FileName = item.FileName, + Qty = item.Qty, + Description = item.Description, + Material = item.Material, + Thickness = item.Thickness, + }; + + if (string.IsNullOrWhiteSpace(item.FileName)) + { + row.Status = "Skipped"; + row.IsEditable = false; + } + else + { + var lookupName = item.FileName; + if (lookupName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)) + lookupName = Path.GetFileNameWithoutExtension(lookupName); + + if (matchedPaths.TryGetValue(lookupName, out var dxfPath)) + { + row.DxfPath = dxfPath; + row.Status = "Matched"; + row.IsEditable = true; + } + else + { + row.Status = "No DXF"; + row.IsEditable = false; + } + } + + _parts.Add(row); + } + + _plateSizes.Clear(); + } + + #endregion + + #region Parts Tab + + private void PopulatePartsGrid() + { + _suppressRegroup = true; + + var table = new DataTable(); + table.Columns.Add("Item #", typeof(string)); + table.Columns.Add("File Name", typeof(string)); + table.Columns.Add("Qty", typeof(string)); + table.Columns.Add("Description", typeof(string)); + table.Columns.Add("Material", typeof(string)); + table.Columns.Add("Thickness", typeof(string)); + table.Columns.Add("Status", typeof(string)); + + foreach (var part in _parts) + { + table.Rows.Add( + part.ItemNum?.ToString() ?? "", + part.FileName ?? "", + part.Qty?.ToString() ?? "", + part.Description ?? "", + part.Material ?? "", + part.Thickness?.ToString("0.####") ?? "", + part.Status + ); + } + + dgvParts.DataSource = table; + + // Make non-editable columns read-only + foreach (DataGridViewColumn col in dgvParts.Columns) + { + if (col.Name != "Material" && col.Name != "Thickness") + col.ReadOnly = true; + } + + // Style rows by status + for (var i = 0; i < _parts.Count; i++) + { + if (!_parts[i].IsEditable) + { + dgvParts.Rows[i].ReadOnly = true; + dgvParts.Rows[i].DefaultCellStyle.ForeColor = Color.Gray; + } + } + + dgvParts.CellValueChanged -= DgvParts_CellValueChanged; + dgvParts.CellValueChanged += DgvParts_CellValueChanged; + + _suppressRegroup = false; + } + + private void DgvParts_CellValueChanged(object sender, DataGridViewCellEventArgs e) + { + if (_suppressRegroup || e.RowIndex < 0) + return; + + var colName = dgvParts.Columns[e.ColumnIndex].Name; + if (colName != "Material" && colName != "Thickness") + return; + + var part = _parts[e.RowIndex]; + if (!part.IsEditable) + return; + + if (colName == "Material") + part.Material = dgvParts.Rows[e.RowIndex].Cells[e.ColumnIndex].Value?.ToString(); + + if (colName == "Thickness") + { + var text = dgvParts.Rows[e.RowIndex].Cells[e.ColumnIndex].Value?.ToString(); + part.Thickness = double.TryParse(text, out var t) ? t : (double?)null; + } + + RebuildGroups(); + UpdateSummary(); + } + + #endregion + + #region Groups Tab + + private void RebuildGroups() + { + // Save existing plate sizes before rebuilding + SavePlateSizes(); + + var defaultWidth = double.TryParse(txtPlateWidth.Text, out var w) ? w : 60; + var defaultLength = double.TryParse(txtPlateLength.Text, out var l) ? l : 120; + + var groups = _parts + .Where(p => p.IsEditable + && !string.IsNullOrWhiteSpace(p.Material) + && p.Thickness.HasValue) + .GroupBy(p => new + { + Material = p.Material.ToUpperInvariant(), + Thickness = p.Thickness.Value + }) + .OrderBy(g => g.First().Material) + .ThenBy(g => g.Key.Thickness) + .ToList(); + + var table = new DataTable(); + table.Columns.Add("Material", typeof(string)); + table.Columns.Add("Thickness", typeof(double)); + table.Columns.Add("Parts", typeof(int)); + table.Columns.Add("Total Qty", typeof(int)); + table.Columns.Add("Plate Width", typeof(double)); + table.Columns.Add("Plate Length", typeof(double)); + + foreach (var group in groups) + { + var material = group.First().Material; + var thickness = group.Key.Thickness; + var key = GroupKey(material, thickness); + + var plateWidth = _plateSizes.TryGetValue(key, out var size) ? size.Width : defaultWidth; + var plateLength = _plateSizes.TryGetValue(key, out _) ? size.Length : defaultLength; + + table.Rows.Add( + material, + thickness, + group.Count(), + group.Sum(p => p.Qty ?? 0), + plateWidth, + plateLength + ); } dgvGroups.DataSource = table; + + // Material, Thickness, Parts, Total Qty are read-only + if (dgvGroups.Columns.Count >= 6) + { + dgvGroups.Columns["Material"].ReadOnly = true; + dgvGroups.Columns["Thickness"].ReadOnly = true; + dgvGroups.Columns["Parts"].ReadOnly = true; + dgvGroups.Columns["Total Qty"].ReadOnly = true; + } + + btnCreateNests.Enabled = table.Rows.Count > 0; } - private void UpdateSummary(BomAnalysis analysis) + private void SavePlateSizes() { - var parts = new List(); - if (analysis.Skipped.Count > 0) - parts.Add($"{analysis.Skipped.Count} skipped (no DXF file or thickness given)"); - if (analysis.Unmatched.Count > 0) - parts.Add($"{analysis.Unmatched.Count} unmatched (DXF file not found)"); + if (dgvGroups.DataSource is not DataTable table) + return; - lblSummary.Text = parts.Count > 0 - ? string.Join(", ", parts) - : string.Empty; + _plateSizes.Clear(); + foreach (DataRow row in table.Rows) + { + var material = row["Material"]?.ToString() ?? ""; + var thickness = row["Thickness"] is double t ? t : 0; + var key = GroupKey(material, thickness); + + var width = row["Plate Width"] is double pw ? pw : 60; + var length = row["Plate Length"] is double pl ? pl : 120; + + _plateSizes[key] = (width, length); + } } + private static string GroupKey(string material, double thickness) + => $"{material?.ToUpperInvariant()}|{thickness}"; + + #endregion + + #region Summary + + private void UpdateSummary() + { + var skipped = _parts.Count(p => p.Status == "Skipped"); + var noDxf = _parts.Count(p => p.Status == "No DXF"); + var matched = _parts.Count(p => p.Status == "Matched"); + + var summaryParts = new List(); + if (skipped > 0) + summaryParts.Add($"{skipped} skipped (no file name)"); + if (noDxf > 0) + summaryParts.Add($"{noDxf} no DXF found"); + + lblSummary.Text = summaryParts.Count > 0 + ? string.Join(", ", summaryParts) + : $"{matched} parts matched"; + } + + #endregion + + #region Create Nests + private void CreateNests_Click(object sender, EventArgs e) { - if (_analysis == null || _analysis.Groups.Count == 0) + if (_parts == null || _parts.Count == 0) return; - if (!double.TryParse(txtPlateWidth.Text, out var plateWidth) || plateWidth <= 0) - { - MessageBox.Show("Plate width must be a positive number.", "Validation Error", - MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } + // Save latest plate size edits + SavePlateSizes(); - if (!double.TryParse(txtPlateLength.Text, out var plateLength) || plateLength <= 0) + var defaultWidth = double.TryParse(txtPlateWidth.Text, out var dw) ? dw : 60; + var defaultLength = double.TryParse(txtPlateLength.Text, out var dl) ? dl : 120; + + var groups = _parts + .Where(p => p.IsEditable + && !string.IsNullOrWhiteSpace(p.Material) + && p.Thickness.HasValue + && !string.IsNullOrWhiteSpace(p.DxfPath)) + .GroupBy(p => new + { + Material = p.Material.ToUpperInvariant(), + Thickness = p.Thickness.Value + }) + .ToList(); + + if (groups.Count == 0) { - MessageBox.Show("Plate length must be a positive number.", "Validation Error", - MessageBoxButtons.OK, MessageBoxIcon.Warning); + MessageBox.Show("No groups with matched DXF files to create nests from.", "Nothing to Create", + MessageBoxButtons.OK, MessageBoxIcon.Information); return; } @@ -148,36 +386,43 @@ namespace OpenNest.Forms var nestsCreated = 0; var importErrors = new List(); - foreach (var group in _analysis.Groups) + foreach (var group in groups) { - var nestName = $"{jobName} - {group.Thickness:0.###} {group.Material}"; + var material = group.First().Material; + var thickness = group.Key.Thickness; + var key = GroupKey(material, thickness); + + var plateWidth = _plateSizes.TryGetValue(key, out var size) ? size.Width : defaultWidth; + var plateLength = _plateSizes.TryGetValue(key, out _) ? size.Length : defaultLength; + + var nestName = $"{jobName} - {thickness:0.###} {material}"; var nest = new Nest(nestName); nest.DateCreated = DateTime.Now; nest.DateLastModified = DateTime.Now; - nest.PlateDefaults.Size = new Size(plateWidth, plateLength); - nest.PlateDefaults.Thickness = group.Thickness; - nest.PlateDefaults.Material = new Material(group.Material); + nest.PlateDefaults.Size = new Geometry.Size(plateWidth, plateLength); + nest.PlateDefaults.Thickness = thickness; + nest.PlateDefaults.Material = new Material(material); nest.PlateDefaults.Quadrant = 1; nest.PlateDefaults.PartSpacing = 1; nest.PlateDefaults.EdgeSpacing = new Spacing(1, 1, 1, 1); - foreach (var matched in group.Parts) + foreach (var part in group) { - if (string.IsNullOrWhiteSpace(matched.DxfPath) || !File.Exists(matched.DxfPath)) + if (!File.Exists(part.DxfPath)) { - importErrors.Add($"{matched.Item.FileName}: DXF file not found"); + importErrors.Add($"{part.FileName}: DXF file not found"); continue; } try { - var result = importer.Import(matched.DxfPath); + var result = importer.Import(part.DxfPath); - var drawingName = Path.GetFileNameWithoutExtension(matched.DxfPath); + var drawingName = Path.GetFileNameWithoutExtension(part.DxfPath); var drawing = new Drawing(drawingName); - drawing.Source.Path = matched.DxfPath; - drawing.Quantity.Required = matched.Item.Qty ?? 1; - drawing.Material = new Material(group.Material); + drawing.Source.Path = part.DxfPath; + drawing.Quantity.Required = part.Qty ?? 1; + drawing.Material = new Material(material); var pgm = ConvertGeometry.ToProgram(result.Entities); @@ -194,7 +439,7 @@ namespace OpenNest.Forms } catch (Exception ex) { - importErrors.Add($"{matched.Item.FileName}: {ex.Message}"); + importErrors.Add($"{part.FileName}: {ex.Message}"); } } @@ -221,9 +466,24 @@ namespace OpenNest.Forms Close(); } + #endregion + private void BtnClose_Click(object sender, EventArgs e) { Close(); } } + + internal class BomPartRow + { + public int? ItemNum { get; set; } + public string FileName { get; set; } + public int? Qty { get; set; } + public string Description { get; set; } + public string Material { get; set; } + public double? Thickness { get; set; } + public string DxfPath { get; set; } + public string Status { get; set; } + public bool IsEditable { get; set; } + } }