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; }
- }
}