mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-06 23:02:11 -04:00
refactor(bom): build import rows, groups and nests in OpenNest.IO
Move the BOM import dialog's row building, grouping and per-group nest construction out of BomImportForm into OpenNest.IO/Bom so they can be tested on Linux: - BomPartRow (now public) and BomImportRows.Build, a verbatim move of the form's BuildPartRows (still via BomAnalyzer). - BomImportGroups.Build: one grouping used by both the Groups tab and Create Nests, which each carried their own copy. Create Nests now creates nests in the Groups tab's order (material, then thickness). - BomNestBuilder.Build: the nest for one group (saved defaults first, then the group's plate size, spacing, material and thickness; one drawing per row with the row's quantity, 1 when blank). The form keeps the file dialogs, grids, EditNestForm windows and the completion message. Behavior is otherwise unchanged; the two row-status defects found while planning are fixed in the following commits.
This commit is contained in:
1 parent
03b3ed47b2
commit
ca153a1c20
8 files changed
+603
-178
No files matched your search
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.IO.Bom;
|
||||
|
||||
/// <summary>
|
||||
/// The rows of one material and thickness; each group becomes one nest.
|
||||
/// </summary>
|
||||
public sealed class BomImportGroup
|
||||
{
|
||||
public BomImportGroup(string material, double thickness, IReadOnlyList<BomPartRow> parts)
|
||||
{
|
||||
Material = material;
|
||||
Thickness = thickness;
|
||||
Parts = parts;
|
||||
}
|
||||
|
||||
/// <summary>The material as the group's first row spells it.</summary>
|
||||
public string Material { get; }
|
||||
|
||||
public double Thickness { get; }
|
||||
|
||||
public IReadOnlyList<BomPartRow> Parts { get; }
|
||||
|
||||
/// <summary>Sum of the rows' quantities; a blank quantity counts as 0.</summary>
|
||||
public int TotalQty => Parts.Sum(p => p.Qty ?? 0);
|
||||
|
||||
public string Key => BomImportGroups.Key(Material, Thickness);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groups importable rows by material and thickness. The Groups tab and
|
||||
/// Create Nests both use this, so they always agree.
|
||||
/// </summary>
|
||||
public static class BomImportGroups
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static List<BomImportGroup> Build(IEnumerable<BomPartRow> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The key that identifies a group's plate settings across regrouping.
|
||||
/// </summary>
|
||||
public static string Key(string material, double thickness) =>
|
||||
$"{material?.ToUpperInvariant()}|{thickness}";
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace OpenNest.IO.Bom;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the import dialog's part rows from the BOM items and the drawing
|
||||
/// folder.
|
||||
/// </summary>
|
||||
public static class BomImportRows
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns one row per BOM item, in BOM order, with the drawing file it
|
||||
/// matched in <paramref name="dxfFolder"/> and its import status.
|
||||
/// </summary>
|
||||
public static List<BomPartRow> Build(List<BomItem> items, string dxfFolder)
|
||||
{
|
||||
var analysis = BomAnalyzer.Analyze(items, dxfFolder);
|
||||
var matchedPaths = new Dictionary<string, string>(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<BomPartRow>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.IO.Bom;
|
||||
|
||||
/// <summary>Plate size and spacing for one group's nest.</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The nest built for one group, or none when no drawing imported, plus a
|
||||
/// message for every drawing that could not be imported.
|
||||
/// </summary>
|
||||
public sealed class BomNestBuildResult
|
||||
{
|
||||
public BomNestBuildResult(Nest nest, IReadOnlyList<string> errors)
|
||||
{
|
||||
Nest = nest;
|
||||
Errors = errors;
|
||||
}
|
||||
|
||||
/// <summary>The new nest, or null when none of the group's drawings imported.</summary>
|
||||
public Nest Nest { get; }
|
||||
|
||||
public IReadOnlyList<string> Errors { get; }
|
||||
}
|
||||
|
||||
/// <summary>Builds the nest for one BOM import group.</summary>
|
||||
public static class BomNestBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a nest named "<paramref name="jobName"/> - thickness material".
|
||||
/// <paramref name="applySavedDefaults"/> 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.
|
||||
/// </summary>
|
||||
public static BomNestBuildResult Build(
|
||||
BomImportGroup group,
|
||||
BomGroupPlateSettings plate,
|
||||
string jobName,
|
||||
Action<Nest> applySavedDefaults
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(group);
|
||||
ArgumentNullException.ThrowIfNull(plate);
|
||||
|
||||
var material = group.Material;
|
||||
var thickness = group.Thickness;
|
||||
var errors = new List<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace OpenNest.IO.Bom;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using OpenNest.IO.Bom;
|
||||
|
||||
namespace OpenNest.Tests.Bom;
|
||||
|
||||
public class BomImportGroupsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_GroupsByMaterialIgnoringCaseThenThickness()
|
||||
{
|
||||
var rows = new List<BomPartRow>
|
||||
{
|
||||
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<BomPartRow>
|
||||
{
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -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<BomItem>
|
||||
{
|
||||
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<BomItem> { 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<BomItem> { 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,
|
||||
};
|
||||
}
|
||||
@@ -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<Nest>(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<Nest>(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;
|
||||
}
|
||||
}
|
||||
+28
-178
@@ -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<BomPartRow> _parts;
|
||||
private Dictionary<string, GroupSettings> _groupSettings;
|
||||
private Dictionary<string, BomGroupPlateSettings> _groupSettings;
|
||||
private bool _suppressRegroup;
|
||||
private NestDefaults _defaults;
|
||||
|
||||
@@ -25,7 +24,7 @@ namespace OpenNest.Forms
|
||||
{
|
||||
InitializeComponent();
|
||||
_parts = new List<BomPartRow>();
|
||||
_groupSettings = new Dictionary<string, GroupSettings>();
|
||||
_groupSettings = new Dictionary<string, BomGroupPlateSettings>();
|
||||
_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<BomItem> items, BomAnalysis analysis)
|
||||
{
|
||||
var matchedPaths = new Dictionary<string, string>(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<BomPartRow>();
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user