diff --git a/OpenNest.IO/Bom/BomImportGroups.cs b/OpenNest.IO/Bom/BomImportGroups.cs new file mode 100644 index 0000000..a09c40c --- /dev/null +++ b/OpenNest.IO/Bom/BomImportGroups.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; + +namespace OpenNest.IO.Bom; + +/// +/// The rows of one material and thickness; each group becomes one nest. +/// +public sealed class BomImportGroup +{ + public BomImportGroup(string material, double thickness, IReadOnlyList parts) + { + Material = material; + Thickness = thickness; + Parts = parts; + } + + /// The material as the group's first row spells it. + public string Material { get; } + + public double Thickness { get; } + + public IReadOnlyList Parts { get; } + + /// Sum of the rows' quantities; a blank quantity counts as 0. + public int TotalQty => Parts.Sum(p => p.Qty ?? 0); + + public string Key => BomImportGroups.Key(Material, Thickness); +} + +/// +/// Groups importable rows by material and thickness. The Groups tab and +/// Create Nests both use this, so they always agree. +/// +public static class BomImportGroups +{ + /// + /// Returns the groups of editable rows that have a drawing, a material + /// and a thickness. Material is compared case-insensitively. Groups are + /// ordered by material, then thickness. + /// + public static List Build(IEnumerable rows) + { + return rows.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, + }) + .OrderBy(g => g.First().Material) + .ThenBy(g => g.Key.Thickness) + .Select(g => new BomImportGroup(g.First().Material, g.Key.Thickness, g.ToList())) + .ToList(); + } + + /// + /// The key that identifies a group's plate settings across regrouping. + /// + public static string Key(string material, double thickness) => + $"{material?.ToUpperInvariant()}|{thickness}"; +} diff --git a/OpenNest.IO/Bom/BomImportRows.cs b/OpenNest.IO/Bom/BomImportRows.cs new file mode 100644 index 0000000..7c409d1 --- /dev/null +++ b/OpenNest.IO/Bom/BomImportRows.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace OpenNest.IO.Bom; + +/// +/// Builds the import dialog's part rows from the BOM items and the drawing +/// folder. +/// +public static class BomImportRows +{ + /// + /// Returns one row per BOM item, in BOM order, with the drawing file it + /// matched in and its import status. + /// + public static List Build(List items, string dxfFolder) + { + var analysis = BomAnalyzer.Analyze(items, dxfFolder); + 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; + + var rows = new List(); + + foreach (var item in items) + { + 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.EndsWith(".dwg", 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; + } + } + + rows.Add(row); + } + + return rows; + } +} diff --git a/OpenNest.IO/Bom/BomNestBuilder.cs b/OpenNest.IO/Bom/BomNestBuilder.cs new file mode 100644 index 0000000..d7cd475 --- /dev/null +++ b/OpenNest.IO/Bom/BomNestBuilder.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.IO; +using OpenNest.Geometry; + +namespace OpenNest.IO.Bom; + +/// Plate size and spacing for one group's nest. +public sealed class BomGroupPlateSettings +{ + public double PlateWidth { get; set; } + + public double PlateLength { get; set; } + + public double PartSpacing { get; set; } + + public double EdgeLeft { get; set; } + + public double EdgeBottom { get; set; } + + public double EdgeRight { get; set; } + + public double EdgeTop { get; set; } +} + +/// +/// The nest built for one group, or none when no drawing imported, plus a +/// message for every drawing that could not be imported. +/// +public sealed class BomNestBuildResult +{ + public BomNestBuildResult(Nest nest, IReadOnlyList errors) + { + Nest = nest; + Errors = errors; + } + + /// The new nest, or null when none of the group's drawings imported. + public Nest Nest { get; } + + public IReadOnlyList Errors { get; } +} + +/// Builds the nest for one BOM import group. +public static class BomNestBuilder +{ + /// + /// Creates a nest named " - thickness material". + /// runs first (units, quadrant, + /// plate), then the group's plate size and spacing, material and + /// thickness are set. Each row's drawing is imported with the row's + /// quantity (1 when blank). The nest gets one plate when at least one + /// drawing imported. + /// + public static BomNestBuildResult Build( + BomImportGroup group, + BomGroupPlateSettings plate, + string jobName, + Action applySavedDefaults + ) + { + ArgumentNullException.ThrowIfNull(group); + ArgumentNullException.ThrowIfNull(plate); + + var material = group.Material; + var thickness = group.Thickness; + var errors = new List(); + + var nest = new Nest($"{jobName} - {thickness:0.###} {material}"); + nest.DateCreated = DateTime.Now; + nest.DateLastModified = DateTime.Now; + // Saved defaults first (units, quadrant, plate), as New does; + // then the group's own plate size and spacing. + applySavedDefaults?.Invoke(nest); + nest.PlateDefaults.Size = new Size(plate.PlateWidth, plate.PlateLength); + nest.Thickness = thickness; + nest.Material = new Material(material); + nest.PlateDefaults.PartSpacing = plate.PartSpacing; + nest.PlateDefaults.EdgeSpacing = new Spacing( + plate.EdgeLeft, + plate.EdgeBottom, + plate.EdgeRight, + plate.EdgeTop + ); + + foreach (var part in group.Parts) + { + if (!File.Exists(part.DxfPath)) + { + errors.Add($"{part.FileName}: DXF file not found"); + continue; + } + + try + { + var drawing = CadImporter.ImportDrawing( + part.DxfPath, + new CadImportOptions { Quantity = part.Qty ?? 1 } + ); + drawing.Material = new Material(material); + nest.Drawings.Add(drawing); + } + catch (Exception ex) + { + errors.Add($"{part.FileName}: {ex.Message}"); + } + } + + if (nest.Drawings.Count == 0) + return new BomNestBuildResult(null, errors); + + nest.CreatePlate(); + return new BomNestBuildResult(nest, errors); + } +} diff --git a/OpenNest.IO/Bom/BomPartRow.cs b/OpenNest.IO/Bom/BomPartRow.cs new file mode 100644 index 0000000..ccd7f86 --- /dev/null +++ b/OpenNest.IO/Bom/BomPartRow.cs @@ -0,0 +1,27 @@ +namespace OpenNest.IO.Bom; + +/// +/// One BOM line as the import dialog shows and edits it: the values read +/// from the BOM, the drawing file it resolved to and whether it can be +/// imported. +/// +public 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; } +} diff --git a/OpenNest.Tests/Bom/BomImportGroupsTests.cs b/OpenNest.Tests/Bom/BomImportGroupsTests.cs new file mode 100644 index 0000000..2ac2d6c --- /dev/null +++ b/OpenNest.Tests/Bom/BomImportGroupsTests.cs @@ -0,0 +1,71 @@ +using OpenNest.IO.Bom; + +namespace OpenNest.Tests.Bom; + +public class BomImportGroupsTests +{ + [Fact] + public void Build_GroupsByMaterialIgnoringCaseThenThickness() + { + var rows = new List + { + Row("PT01", "Stainless", 0.25, qty: 2), + Row("PT02", "Aluminum", 0.125, qty: 1), + Row("PT03", "STAINLESS", 0.25, qty: null), + Row("PT04", "Stainless", 0.125, qty: 4), + Row("PT05", "Aluminum", 0.0625, qty: 3), + }; + + var groups = BomImportGroups.Build(rows); + + Assert.Equal( + new[] { "Aluminum 0.0625", "Aluminum 0.125", "Stainless 0.125", "Stainless 0.25" }, + groups.Select(g => $"{g.Material} {g.Thickness}") + ); + var stainless = groups[3]; + Assert.Equal(new[] { "PT01", "PT03" }, stainless.Parts.Select(p => p.FileName)); + Assert.Equal(2, stainless.TotalQty); + Assert.Equal(BomImportGroups.Key("stainless", 0.25), stainless.Key); + } + + [Fact] + public void Build_LeavesOutRowsThatCannotBeImported() + { + var locked = Row("PT02", "Stainless", 0.25); + locked.IsEditable = false; + var noDrawing = Row("PT05", "Stainless", 0.25); + noDrawing.DxfPath = null; + var rows = new List + { + Row("PT01", "Stainless", 0.25), + locked, + Row("PT03", " ", 0.25), + Row("PT04", "Stainless", null), + noDrawing, + }; + + var groups = BomImportGroups.Build(rows); + + var group = Assert.Single(groups); + Assert.Equal("PT01", Assert.Single(group.Parts).FileName); + } + + [Fact] + public void Key_IgnoresMaterialCase() + { + Assert.Equal(BomImportGroups.Key("Stainless", 0.25), BomImportGroups.Key("STAINLESS", 0.25)); + Assert.NotEqual(BomImportGroups.Key("Stainless", 0.25), BomImportGroups.Key("Stainless", 0.125)); + } + + private static BomPartRow Row(string fileName, string material, double? thickness, int? qty = 1) => + new() + { + FileName = fileName, + Material = material, + Thickness = thickness, + Qty = qty, + DxfPath = $"/drawings/{fileName}.dxf", + Status = "Matched", + IsEditable = true, + }; +} diff --git a/OpenNest.Tests/Bom/BomImportRowsTests.cs b/OpenNest.Tests/Bom/BomImportRowsTests.cs new file mode 100644 index 0000000..c4fbbdd --- /dev/null +++ b/OpenNest.Tests/Bom/BomImportRowsTests.cs @@ -0,0 +1,93 @@ +using OpenNest.IO.Bom; + +namespace OpenNest.Tests.Bom; + +public sealed class BomImportRowsTests : IDisposable +{ + private readonly string folder = Path.Combine( + Path.GetTempPath(), + "bom-rows-" + Guid.NewGuid().ToString("N") + ); + + public BomImportRowsTests() + { + Directory.CreateDirectory(folder); + } + + public void Dispose() + { + if (Directory.Exists(folder)) + Directory.Delete(folder, recursive: true); + } + + [Fact] + public void Build_MatchesRowsToDrawingFilesInBomOrder() + { + Touch("PT01.dxf"); + Touch("PT03.dwg"); + var items = new List + { + Item("PT01", qty: 2, itemNum: 1, description: "Gusset"), + Item("PT02", qty: 3, itemNum: 2), + Item(null, qty: 4, itemNum: 3), + Item("PT03", qty: null, itemNum: 4), + }; + + var rows = BomImportRows.Build(items, folder); + + Assert.Equal(new[] { 1, 2, 3, 4 }, rows.Select(r => r.ItemNum ?? 0)); + Assert.Equal(new[] { "Matched", "No DXF", "Skipped", "Matched" }, rows.Select(r => r.Status)); + Assert.Equal(new[] { true, false, false, true }, rows.Select(r => r.IsEditable)); + Assert.Equal(Path.Combine(folder, "PT01.dxf"), rows[0].DxfPath); + Assert.Null(rows[1].DxfPath); + Assert.Null(rows[2].DxfPath); + Assert.Equal(Path.Combine(folder, "PT03.dwg"), rows[3].DxfPath); + + Assert.Equal("Gusset", rows[0].Description); + Assert.Equal(2, rows[0].Qty); + Assert.Null(rows[3].Qty); + Assert.Equal("Stainless", rows[0].Material); + Assert.Equal(0.25, rows[0].Thickness); + } + + [Fact] + public void Build_MatchesFileNamesIgnoringCase() + { + Touch("PT01.dxf"); + + var rows = BomImportRows.Build(new List { Item("pt01") }, folder); + + Assert.Equal("Matched", rows[0].Status); + Assert.Equal(Path.Combine(folder, "PT01.dxf"), rows[0].DxfPath); + } + + [Fact] + public void Build_WithMissingFolder_FindsNoDrawings() + { + var missing = Path.Combine(folder, "missing"); + + var rows = BomImportRows.Build(new List { Item("PT01") }, missing); + + Assert.Equal("No DXF", rows[0].Status); + Assert.False(rows[0].IsEditable); + Assert.Null(rows[0].DxfPath); + } + + private void Touch(string name) => File.WriteAllText(Path.Combine(folder, name), ""); + + private static BomItem Item( + string? fileName, + int? qty = 1, + int? itemNum = null, + string? description = null + ) => + new() + { + ItemNum = itemNum, + FileName = fileName, + Qty = qty, + Description = description, + Material = "Stainless", + Thickness = 0.25, + }; +} diff --git a/OpenNest.Tests/Bom/BomNestBuilderTests.cs b/OpenNest.Tests/Bom/BomNestBuilderTests.cs new file mode 100644 index 0000000..4be604f --- /dev/null +++ b/OpenNest.Tests/Bom/BomNestBuilderTests.cs @@ -0,0 +1,130 @@ +using OpenNest.Converters; +using OpenNest.Geometry; +using OpenNest.IO; +using OpenNest.IO.Bom; + +namespace OpenNest.Tests.Bom; + +public sealed class BomNestBuilderTests : IDisposable +{ + private readonly string folder = Path.Combine( + Path.GetTempPath(), + "bom-nests-" + Guid.NewGuid().ToString("N") + ); + + public BomNestBuilderTests() + { + Directory.CreateDirectory(folder); + } + + public void Dispose() + { + if (Directory.Exists(folder)) + Directory.Delete(folder, recursive: true); + } + + [Fact] + public void Build_AppliesSavedDefaultsThenGroupSettings() + { + var group = Group(Row("PT01", qty: 5), Row("PT02", qty: null)); + var plate = Plate(); + + var result = BomNestBuilder.Build( + group, + plate, + "Job 42", + nest => + { + nest.Units = Units.Millimeters; + nest.PlateDefaults.Quadrant = 3; + nest.PlateDefaults.Size = new Size(1, 1); + nest.PlateDefaults.PartSpacing = 9; + } + ); + + Assert.Empty(result.Errors); + var nest = Assert.IsType(result.Nest); + Assert.Equal("Job 42 - 0.25 Stainless", nest.Name); + Assert.Equal(Units.Millimeters, nest.Units); + Assert.Equal(3, nest.PlateDefaults.Quadrant); + Assert.Equal(60, nest.PlateDefaults.Size.Width); + Assert.Equal(120, nest.PlateDefaults.Size.Length); + Assert.Equal(0.2, nest.PlateDefaults.PartSpacing); + Assert.Equal(new[] { 0.1, 0.2, 0.3, 0.4 }, Edges(nest.PlateDefaults.EdgeSpacing)); + Assert.Equal(0.25, nest.Thickness); + Assert.Equal("Stainless", nest.Material.Name); + Assert.Single(nest.Plates); + + var drawings = nest.Drawings.OrderBy(d => d.Name).ToList(); + Assert.Equal(new[] { "PT01", "PT02" }, drawings.Select(d => d.Name)); + Assert.Equal(new[] { 5, 1 }, drawings.Select(d => d.Quantity.Required)); + Assert.All(drawings, d => Assert.Equal("Stainless", d.Material.Name)); + } + + [Fact] + public void Build_ReportsMissingDrawingAndKeepsTheOthers() + { + var missing = Row("PT09"); + File.Delete(missing.DxfPath); + + var result = BomNestBuilder.Build(Group(Row("PT01"), missing), Plate(), "Job", null); + + Assert.Equal("PT09: DXF file not found", Assert.Single(result.Errors)); + var nest = Assert.IsType(result.Nest); + Assert.Equal("PT01", Assert.Single(nest.Drawings).Name); + } + + [Fact] + public void Build_WithNoImportedDrawing_ReturnsNoNest() + { + var unreadable = Row("PT01"); + File.WriteAllText(unreadable.DxfPath, "not a drawing"); + + var result = BomNestBuilder.Build(Group(unreadable), Plate(), "Job", null); + + Assert.Null(result.Nest); + Assert.StartsWith("PT01: ", Assert.Single(result.Errors)); + } + + private BomPartRow Row(string name, int? qty = 1) => + new() + { + FileName = name, + Qty = qty, + Material = "Stainless", + Thickness = 0.25, + DxfPath = WriteSquare(name), + Status = "Matched", + IsEditable = true, + }; + + private static BomImportGroup Group(params BomPartRow[] rows) => new("Stainless", 0.25, rows); + + private static BomGroupPlateSettings Plate() => + new() + { + PlateWidth = 60, + PlateLength = 120, + PartSpacing = 0.2, + EdgeLeft = 0.1, + EdgeBottom = 0.2, + EdgeRight = 0.3, + EdgeTop = 0.4, + }; + + private static double[] Edges(Spacing spacing) => + new[] { spacing.Left, spacing.Bottom, spacing.Right, spacing.Top }; + + private string WriteSquare(string name) + { + var shape = new Shape(); + shape.Entities.Add(new Line(new Vector(0, 0), new Vector(2, 0))); + shape.Entities.Add(new Line(new Vector(2, 0), new Vector(2, 2))); + shape.Entities.Add(new Line(new Vector(2, 2), new Vector(0, 2))); + shape.Entities.Add(new Line(new Vector(0, 2), new Vector(0, 0))); + + var path = Path.Combine(folder, name + ".dxf"); + Dxf.ExportProgram(ConvertGeometry.ToProgram(shape), path); + return path; + } +} diff --git a/OpenNest/Forms/BomImportForm.cs b/OpenNest/Forms/BomImportForm.cs index 2f3027a..faa61e0 100644 --- a/OpenNest/Forms/BomImportForm.cs +++ b/OpenNest/Forms/BomImportForm.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using System.Windows.Forms; using OpenNest.Data; -using OpenNest.Geometry; using OpenNest.IO; using OpenNest.IO.Bom; @@ -15,7 +14,7 @@ namespace OpenNest.Forms public partial class BomImportForm : Form { private List _parts; - private Dictionary _groupSettings; + private Dictionary _groupSettings; private bool _suppressRegroup; private NestDefaults _defaults; @@ -25,7 +24,7 @@ namespace OpenNest.Forms { InitializeComponent(); _parts = new List(); - _groupSettings = new Dictionary(); + _groupSettings = new Dictionary(); _defaults = MainForm.LoadSavedNestDefaults(out _); ApplyDefaults(); } @@ -101,8 +100,8 @@ namespace OpenNest.Forms using (var reader = new BomReader(txtBomFile.Text)) items = reader.GetItems(); - var analysis = BomAnalyzer.Analyze(items, txtDxfFolder.Text); - BuildPartRows(items, analysis); + _parts = BomImportRows.Build(items, txtDxfFolder.Text); + _groupSettings.Clear(); PopulatePartsGrid(); RebuildGroups(); UpdateSummary(); @@ -120,62 +119,6 @@ namespace OpenNest.Forms } } - private void BuildPartRows(List items, BomAnalysis analysis) - { - 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 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.EndsWith(".dwg", 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); - } - - _groupSettings.Clear(); - } - #endregion #region Parts Tab @@ -273,18 +216,7 @@ namespace OpenNest.Forms ? l : _defaults.Size.Length; - 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 groups = BomImportGroups.Build(_parts); var table = new DataTable(); table.Columns.Add("Material", typeof(string)); @@ -301,17 +233,13 @@ namespace OpenNest.Forms foreach (var group in groups) { - var material = group.First().Material; - var thickness = group.Key.Thickness; - var key = GroupKey(material, thickness); - - var existing = _groupSettings.TryGetValue(key, out var gs); + var existing = _groupSettings.TryGetValue(group.Key, out var gs); table.Rows.Add( - material, - thickness, - group.Count(), - group.Sum(p => p.Qty ?? 0), + group.Material, + group.Thickness, + group.Parts.Count, + group.TotalQty, existing ? gs.PlateWidth : defaultWidth, existing ? gs.PlateLength : defaultLength, existing ? gs.PartSpacing : _defaults.PartSpacing, @@ -346,9 +274,9 @@ namespace OpenNest.Forms { var material = row["Material"]?.ToString() ?? ""; var thickness = row["Thickness"] is double t ? t : 0; - var key = GroupKey(material, thickness); + var key = BomImportGroups.Key(material, thickness); - _groupSettings[key] = new GroupSettings + _groupSettings[key] = new BomGroupPlateSettings { PlateWidth = row["Plate Width"] is double pw ? pw @@ -373,9 +301,6 @@ namespace OpenNest.Forms } } - private static string GroupKey(string material, double thickness) => - $"{material?.ToUpperInvariant()}|{thickness}"; - #endregion #region Summary @@ -417,19 +342,7 @@ namespace OpenNest.Forms ? dl : _defaults.Size.Length; - 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(); + var groups = BomImportGroups.Build(_parts); if (groups.Count == 0) { @@ -448,66 +361,27 @@ namespace OpenNest.Forms foreach (var group in groups) { - var material = group.First().Material; - var thickness = group.Key.Thickness; - var key = GroupKey(material, thickness); - - var hasSettings = _groupSettings.TryGetValue(key, out var gs); - var plateWidth = hasSettings ? gs.PlateWidth : defaultWidth; - var plateLength = hasSettings ? gs.PlateLength : defaultLength; - var partSpacing = hasSettings ? gs.PartSpacing : _defaults.PartSpacing; - var edgeLeft = hasSettings ? gs.EdgeLeft : _defaults.EdgeSpacing.Left; - var edgeBottom = hasSettings ? gs.EdgeBottom : _defaults.EdgeSpacing.Bottom; - var edgeRight = hasSettings ? gs.EdgeRight : _defaults.EdgeSpacing.Right; - var edgeTop = hasSettings ? gs.EdgeTop : _defaults.EdgeSpacing.Top; - - var nestName = $"{jobName} - {thickness:0.###} {material}"; - var nest = new Nest(nestName); - nest.DateCreated = DateTime.Now; - nest.DateLastModified = DateTime.Now; - // Saved defaults first (units, quadrant, plate), as New does; - // then the group's own plate size and spacing. - _defaults.ApplyTo(nest); - nest.PlateDefaults.Size = new Geometry.Size(plateWidth, plateLength); - nest.Thickness = thickness; - nest.Material = new Material(material); - nest.PlateDefaults.PartSpacing = partSpacing; - nest.PlateDefaults.EdgeSpacing = new Spacing( - edgeLeft, - edgeBottom, - edgeRight, - edgeTop - ); - - foreach (var part in group) + if (!_groupSettings.TryGetValue(group.Key, out var plate)) { - if (!File.Exists(part.DxfPath)) + plate = new BomGroupPlateSettings { - importErrors.Add($"{part.FileName}: DXF file not found"); - continue; - } - - try - { - var drawing = CadImporter.ImportDrawing( - part.DxfPath, - new CadImportOptions { Quantity = part.Qty ?? 1 } - ); - drawing.Material = new Material(material); - nest.Drawings.Add(drawing); - } - catch (Exception ex) - { - importErrors.Add($"{part.FileName}: {ex.Message}"); - } + PlateWidth = defaultWidth, + PlateLength = defaultLength, + PartSpacing = _defaults.PartSpacing, + EdgeLeft = _defaults.EdgeSpacing.Left, + EdgeBottom = _defaults.EdgeSpacing.Bottom, + EdgeRight = _defaults.EdgeSpacing.Right, + EdgeTop = _defaults.EdgeSpacing.Top, + }; } - if (nest.Drawings.Count == 0) + var result = BomNestBuilder.Build(group, plate, jobName, _defaults.ApplyTo); + importErrors.AddRange(result.Errors); + + if (result.Nest == null) continue; - nest.CreatePlate(); - - var editForm = new EditNestForm(nest); + var editForm = new EditNestForm(result.Nest); editForm.MdiParent = MdiParentForm; editForm.Show(); editForm.PlateView.ZoomToFit(); @@ -538,28 +412,4 @@ namespace OpenNest.Forms 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; } - } - - internal class GroupSettings - { - public double PlateWidth { get; set; } - public double PlateLength { get; set; } - public double PartSpacing { get; set; } - public double EdgeLeft { get; set; } - public double EdgeBottom { get; set; } - public double EdgeRight { get; set; } - public double EdgeTop { get; set; } - } }