refactor: extract training data collection into OpenNest.Training
Move brute-force data collection, TrainingDatabase, and GPU init from OpenNest.Console into a dedicated OpenNest.Training project. Replaces raw Microsoft.Data.Sqlite with EF Core. Console is now a pure nesting CLI with template support and cleaned-up usage output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,9 +10,5 @@
|
|||||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||||
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+36
-279
@@ -6,11 +6,6 @@ using System.Linq;
|
|||||||
using OpenNest;
|
using OpenNest;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
using Color = System.Drawing.Color;
|
|
||||||
using OpenNest.Console;
|
|
||||||
using OpenNest.Engine.BestFit;
|
|
||||||
using OpenNest.Engine.ML;
|
|
||||||
using OpenNest.Gpu;
|
|
||||||
|
|
||||||
// Parse arguments.
|
// Parse arguments.
|
||||||
var nestFile = (string)null;
|
var nestFile = (string)null;
|
||||||
@@ -26,27 +21,12 @@ var noSave = false;
|
|||||||
var noLog = false;
|
var noLog = false;
|
||||||
var keepParts = false;
|
var keepParts = false;
|
||||||
var autoNest = false;
|
var autoNest = false;
|
||||||
var collectDataDir = (string)null;
|
|
||||||
var dbPath = "nesting_training.db";
|
|
||||||
var saveNestsDir = (string)null;
|
|
||||||
var templateFile = (string)null;
|
var templateFile = (string)null;
|
||||||
|
|
||||||
for (var i = 0; i < args.Length; i++)
|
for (var i = 0; i < args.Length; i++)
|
||||||
{
|
{
|
||||||
switch (args[i])
|
switch (args[i])
|
||||||
{
|
{
|
||||||
case "--db" when i + 1 < args.Length:
|
|
||||||
dbPath = args[++i];
|
|
||||||
break;
|
|
||||||
case "--save-nests" when i + 1 < args.Length:
|
|
||||||
saveNestsDir = args[++i];
|
|
||||||
break;
|
|
||||||
case "--template" when i + 1 < args.Length:
|
|
||||||
templateFile = args[++i];
|
|
||||||
break;
|
|
||||||
case "--collect" when i + 1 < args.Length:
|
|
||||||
collectDataDir = args[++i];
|
|
||||||
break;
|
|
||||||
case "--drawing" when i + 1 < args.Length:
|
case "--drawing" when i + 1 < args.Length:
|
||||||
drawingName = args[++i];
|
drawingName = args[++i];
|
||||||
break;
|
break;
|
||||||
@@ -82,6 +62,9 @@ for (var i = 0; i < args.Length; i++)
|
|||||||
case "--keep-parts":
|
case "--keep-parts":
|
||||||
keepParts = true;
|
keepParts = true;
|
||||||
break;
|
break;
|
||||||
|
case "--template" when i + 1 < args.Length:
|
||||||
|
templateFile = args[++i];
|
||||||
|
break;
|
||||||
case "--autonest":
|
case "--autonest":
|
||||||
autoNest = true;
|
autoNest = true;
|
||||||
break;
|
break;
|
||||||
@@ -96,22 +79,6 @@ for (var i = 0; i < args.Length; i++)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize GPU if available.
|
|
||||||
if (GpuEvaluatorFactory.GpuAvailable)
|
|
||||||
{
|
|
||||||
BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
|
||||||
Console.WriteLine($"GPU: {GpuEvaluatorFactory.DeviceName}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("GPU: not available (using CPU)");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collectDataDir != null)
|
|
||||||
{
|
|
||||||
return RunDataCollection(collectDataDir, dbPath, saveNestsDir, spacing ?? 0.5, templateFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(nestFile) || !File.Exists(nestFile))
|
if (string.IsNullOrEmpty(nestFile) || !File.Exists(nestFile))
|
||||||
{
|
{
|
||||||
PrintUsage();
|
PrintUsage();
|
||||||
@@ -149,6 +116,24 @@ if (plateIndex >= nest.Plates.Count)
|
|||||||
|
|
||||||
var plate = nest.Plates[plateIndex];
|
var plate = nest.Plates[plateIndex];
|
||||||
|
|
||||||
|
// Apply template defaults.
|
||||||
|
if (templateFile != null)
|
||||||
|
{
|
||||||
|
if (!File.Exists(templateFile))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: Template not found: {templateFile}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
var templateNest = new NestReader(templateFile).Read();
|
||||||
|
var templatePlate = templateNest.PlateDefaults.CreateNew();
|
||||||
|
plate.Thickness = templatePlate.Thickness;
|
||||||
|
plate.Quadrant = templatePlate.Quadrant;
|
||||||
|
plate.Material = templatePlate.Material;
|
||||||
|
plate.EdgeSpacing = templatePlate.EdgeSpacing;
|
||||||
|
plate.PartSpacing = templatePlate.PartSpacing;
|
||||||
|
Console.WriteLine($"Template: {templateFile}");
|
||||||
|
}
|
||||||
|
|
||||||
// Apply overrides.
|
// Apply overrides.
|
||||||
if (spacing.HasValue)
|
if (spacing.HasValue)
|
||||||
plate.PartSpacing = spacing.Value;
|
plate.PartSpacing = spacing.Value;
|
||||||
@@ -207,9 +192,9 @@ if (autoNest)
|
|||||||
|
|
||||||
Console.WriteLine($"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts");
|
Console.WriteLine($"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts");
|
||||||
|
|
||||||
var parts = NestEngine.AutoNest(nestItems, plate);
|
var nestParts = NestEngine.AutoNest(nestItems, plate);
|
||||||
plate.Parts.AddRange(parts);
|
plate.Parts.AddRange(nestParts);
|
||||||
success = parts.Count > 0;
|
success = nestParts.Count > 0;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -262,253 +247,25 @@ if (!noSave)
|
|||||||
|
|
||||||
return checkOverlaps && overlapCount > 0 ? 1 : 0;
|
return checkOverlaps && overlapCount > 0 ? 1 : 0;
|
||||||
|
|
||||||
int RunDataCollection(string dir, string dbPath, string saveDir, double s, string template)
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(dir))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"Error: Directory not found: {dir}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load template nest for plate defaults if provided.
|
|
||||||
Nest templateNest = null;
|
|
||||||
if (template != null)
|
|
||||||
{
|
|
||||||
if (!File.Exists(template))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"Error: Template not found: {template}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
templateNest = new NestReader(template).Read();
|
|
||||||
Console.WriteLine($"Using template: {template}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var PartColors = new[]
|
|
||||||
{
|
|
||||||
Color.FromArgb(205, 92, 92),
|
|
||||||
Color.FromArgb(148, 103, 189),
|
|
||||||
Color.FromArgb(75, 180, 175),
|
|
||||||
Color.FromArgb(210, 190, 75),
|
|
||||||
Color.FromArgb(190, 85, 175),
|
|
||||||
Color.FromArgb(185, 115, 85),
|
|
||||||
Color.FromArgb(120, 100, 190),
|
|
||||||
Color.FromArgb(200, 100, 140),
|
|
||||||
Color.FromArgb(80, 175, 155),
|
|
||||||
Color.FromArgb(195, 160, 85),
|
|
||||||
Color.FromArgb(175, 95, 160),
|
|
||||||
Color.FromArgb(215, 130, 130),
|
|
||||||
};
|
|
||||||
|
|
||||||
var sheetSuite = new[]
|
|
||||||
{
|
|
||||||
new Size(96, 48), new Size(120, 48), new Size(144, 48),
|
|
||||||
new Size(96, 60), new Size(120, 60), new Size(144, 60),
|
|
||||||
new Size(96, 72), new Size(120, 72), new Size(144, 72),
|
|
||||||
new Size(48, 24), new Size(120, 10)
|
|
||||||
};
|
|
||||||
|
|
||||||
var dxfFiles = Directory.GetFiles(dir, "*.dxf", SearchOption.AllDirectories);
|
|
||||||
Console.WriteLine($"Found {dxfFiles.Length} DXF files");
|
|
||||||
Console.WriteLine($"Database: {Path.GetFullPath(dbPath)}");
|
|
||||||
Console.WriteLine($"Sheet sizes: {sheetSuite.Length} configurations");
|
|
||||||
Console.WriteLine($"Spacing: {s:F2}");
|
|
||||||
if (saveDir != null) Console.WriteLine($"Saving nests to: {saveDir}");
|
|
||||||
Console.WriteLine("---");
|
|
||||||
|
|
||||||
using var db = new TrainingDatabase(dbPath);
|
|
||||||
|
|
||||||
var importer = new DxfImporter();
|
|
||||||
var colorIndex = 0;
|
|
||||||
var processed = 0;
|
|
||||||
var skippedGeometry = 0;
|
|
||||||
var skippedFeatures = 0;
|
|
||||||
var skippedExisting = 0;
|
|
||||||
var totalRuns = 0;
|
|
||||||
var totalSw = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
foreach (var file in dxfFiles)
|
|
||||||
{
|
|
||||||
var fileNum = processed + skippedGeometry + skippedFeatures + skippedExisting + 1;
|
|
||||||
var partNo = Path.GetFileNameWithoutExtension(file);
|
|
||||||
Console.Write($"[{fileNum}/{dxfFiles.Length}] {partNo}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var existingRuns = db.RunCount(Path.GetFileName(file));
|
|
||||||
if (existingRuns >= sheetSuite.Length)
|
|
||||||
{
|
|
||||||
Console.WriteLine(" - SKIP (all sizes done)");
|
|
||||||
skippedExisting++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!importer.GetGeometry(file, out var entities))
|
|
||||||
{
|
|
||||||
Console.WriteLine(" - SKIP (no geometry)");
|
|
||||||
skippedGeometry++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var drawing = new Drawing(Path.GetFileName(file));
|
|
||||||
drawing.Program = OpenNest.Converters.ConvertGeometry.ToProgram(entities);
|
|
||||||
drawing.UpdateArea();
|
|
||||||
drawing.Color = PartColors[colorIndex % PartColors.Length];
|
|
||||||
colorIndex++;
|
|
||||||
|
|
||||||
var features = FeatureExtractor.Extract(drawing);
|
|
||||||
if (features == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine(" - SKIP (feature extraction failed)");
|
|
||||||
skippedFeatures++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($" (area={features.Area:F1}, verts={features.VertexCount})");
|
|
||||||
|
|
||||||
// Precompute best-fits once for all sheet sizes.
|
|
||||||
var sizes = sheetSuite.Select(sz => (sz.Width, sz.Length)).ToList();
|
|
||||||
var bfSw = Stopwatch.StartNew();
|
|
||||||
BestFitCache.ComputeForSizes(drawing, s, sizes);
|
|
||||||
bfSw.Stop();
|
|
||||||
Console.WriteLine($" Best-fits computed in {bfSw.ElapsedMilliseconds}ms");
|
|
||||||
|
|
||||||
using var txn = db.BeginTransaction();
|
|
||||||
|
|
||||||
var partId = db.GetOrAddPart(Path.GetFileName(file), features, drawing.Program.ToString());
|
|
||||||
var partSw = Stopwatch.StartNew();
|
|
||||||
var runsThisPart = 0;
|
|
||||||
var bestUtil = 0.0;
|
|
||||||
var bestCount = 0;
|
|
||||||
|
|
||||||
foreach (var size in sheetSuite)
|
|
||||||
{
|
|
||||||
if (db.HasRun(Path.GetFileName(file), size.Width, size.Length, s))
|
|
||||||
{
|
|
||||||
Console.WriteLine($" {size.Length}x{size.Width} - skip (exists)");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Plate runPlate;
|
|
||||||
if (templateNest != null)
|
|
||||||
{
|
|
||||||
runPlate = templateNest.PlateDefaults.CreateNew();
|
|
||||||
runPlate.Size = size;
|
|
||||||
runPlate.PartSpacing = s;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
runPlate = new Plate { Size = size, PartSpacing = s };
|
|
||||||
}
|
|
||||||
|
|
||||||
var sizeSw = Stopwatch.StartNew();
|
|
||||||
var result = BruteForceRunner.Run(drawing, runPlate);
|
|
||||||
sizeSw.Stop();
|
|
||||||
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine($" {size.Length}x{size.Width} - no fit");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.Utilization > bestUtil)
|
|
||||||
{
|
|
||||||
bestUtil = result.Utilization;
|
|
||||||
bestCount = result.PartCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($" {size.Length}x{size.Width} - {result.PartCount}pcs, {result.Utilization:P1}, {sizeSw.ElapsedMilliseconds}ms");
|
|
||||||
|
|
||||||
string savedFilePath = null;
|
|
||||||
if (saveDir != null)
|
|
||||||
{
|
|
||||||
// Deterministic bucket (00-FF) based on filename hash
|
|
||||||
uint hash = 0;
|
|
||||||
foreach (char c in partNo) hash = (hash * 31) + c;
|
|
||||||
var bucket = (hash % 256).ToString("X2");
|
|
||||||
|
|
||||||
var partDir = Path.Combine(saveDir, bucket, partNo);
|
|
||||||
Directory.CreateDirectory(partDir);
|
|
||||||
|
|
||||||
var nestName = $"{partNo}-{size.Length}x{size.Width}-{result.PartCount}pcs";
|
|
||||||
var fileName = nestName + ".zip";
|
|
||||||
savedFilePath = Path.Combine(partDir, fileName);
|
|
||||||
|
|
||||||
// Create nest from template or from scratch
|
|
||||||
Nest nestObj;
|
|
||||||
if (templateNest != null)
|
|
||||||
{
|
|
||||||
nestObj = new Nest(nestName)
|
|
||||||
{
|
|
||||||
Units = templateNest.Units,
|
|
||||||
DateCreated = DateTime.Now
|
|
||||||
};
|
|
||||||
nestObj.PlateDefaults.SetFromExisting(templateNest.PlateDefaults.CreateNew());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
nestObj = new Nest(nestName) { Units = Units.Inches, DateCreated = DateTime.Now };
|
|
||||||
}
|
|
||||||
|
|
||||||
nestObj.Drawings.Add(drawing);
|
|
||||||
var plateObj = nestObj.CreatePlate();
|
|
||||||
plateObj.Size = size;
|
|
||||||
plateObj.PartSpacing = s;
|
|
||||||
plateObj.Parts.AddRange(result.PlacedParts);
|
|
||||||
|
|
||||||
var writer = new NestWriter(nestObj);
|
|
||||||
writer.Write(savedFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.AddRun(partId, size.Width, size.Length, s, result, savedFilePath);
|
|
||||||
runsThisPart++;
|
|
||||||
totalRuns++;
|
|
||||||
}
|
|
||||||
|
|
||||||
txn.Commit();
|
|
||||||
BestFitCache.Invalidate(drawing);
|
|
||||||
partSw.Stop();
|
|
||||||
processed++;
|
|
||||||
Console.WriteLine($" Total: {runsThisPart} runs, best={bestCount}pcs @ {bestUtil:P1}, {partSw.ElapsedMilliseconds}ms");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.Error.WriteLine($" ERROR: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalSw.Stop();
|
|
||||||
Console.WriteLine("---");
|
|
||||||
Console.WriteLine($"Processed: {processed} parts, {totalRuns} total runs");
|
|
||||||
Console.WriteLine($"Skipped: {skippedExisting} (existing) + {skippedGeometry} (no geometry) + {skippedFeatures} (no features)");
|
|
||||||
Console.WriteLine($"Time: {totalSw.Elapsed:h\\:mm\\:ss}");
|
|
||||||
Console.WriteLine($"Database: {Path.GetFullPath(dbPath)}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PrintUsage()
|
void PrintUsage()
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("Usage: OpenNest.Console <nest-file> [options]");
|
Console.Error.WriteLine("Usage: OpenNest.Console <nest-file> [options]");
|
||||||
Console.Error.WriteLine(" OpenNest.Console --collect <dxf-dir> [options]");
|
|
||||||
Console.Error.WriteLine();
|
Console.Error.WriteLine();
|
||||||
Console.Error.WriteLine("Arguments:");
|
Console.Error.WriteLine("Arguments:");
|
||||||
Console.Error.WriteLine(" nest-file Path to a .zip nest file");
|
Console.Error.WriteLine(" nest-file Path to a .zip nest file");
|
||||||
Console.Error.WriteLine();
|
Console.Error.WriteLine();
|
||||||
Console.Error.WriteLine("Options:");
|
Console.Error.WriteLine("Options:");
|
||||||
Console.Error.WriteLine(" --drawing <name> Drawing name to fill with (default: first drawing)");
|
Console.Error.WriteLine(" --drawing <name> Drawing name to fill with (default: first drawing)");
|
||||||
Console.WriteLine(" --plate <index> Plate index to fill (default: 0)");
|
Console.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)");
|
||||||
Console.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)");
|
Console.Error.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)");
|
||||||
Console.WriteLine(" --spacing <value> Override part spacing");
|
Console.Error.WriteLine(" --spacing <value> Override part spacing");
|
||||||
Console.WriteLine(" --size <WxH> Override plate size (e.g. 120x60)");
|
Console.Error.WriteLine(" --size <WxH> Override plate size (e.g. 120x60)");
|
||||||
Console.WriteLine(" --output <path> Output nest file path (default: <input>-result.zip)");
|
Console.Error.WriteLine(" --output <path> Output nest file path (default: <input>-result.zip)");
|
||||||
Console.WriteLine(" --autonest Use NFP-based mixed-part autonesting instead of linear fill");
|
Console.Error.WriteLine(" --template <path> Nest template for plate defaults (thickness, quadrant, material, spacing)");
|
||||||
Console.WriteLine(" --keep-parts Don't clear existing parts before filling");
|
Console.Error.WriteLine(" --autonest Use NFP-based mixed-part autonesting instead of linear fill");
|
||||||
Console.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
|
Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling");
|
||||||
Console.WriteLine(" --no-save Skip saving output file");
|
Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
|
||||||
Console.WriteLine(" --no-log Skip writing debug log file");
|
Console.Error.WriteLine(" --no-save Skip saving output file");
|
||||||
Console.WriteLine(" --collect <dir> Brute-force process all DXFs in directory to SQLite");
|
Console.Error.WriteLine(" --no-log Skip writing debug log file");
|
||||||
Console.WriteLine(" --db <path> Path to the SQLite training database (default: nesting_training.db)");
|
Console.Error.WriteLine(" -h, --help Show this help");
|
||||||
Console.WriteLine(" --save-nests <dir> Directory to save individual .zip nests for each winner");
|
|
||||||
Console.WriteLine(" --template <path> Nest template (.nstdot) for plate defaults");
|
|
||||||
Console.WriteLine(" -h, --help Show this help");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using OpenNest.Engine.ML;
|
|
||||||
|
|
||||||
namespace OpenNest.Console
|
|
||||||
{
|
|
||||||
public class TrainingDatabase : IDisposable
|
|
||||||
{
|
|
||||||
private readonly SqliteConnection _connection;
|
|
||||||
|
|
||||||
public TrainingDatabase(string dbPath)
|
|
||||||
{
|
|
||||||
var connectionString = new SqliteConnectionStringBuilder
|
|
||||||
{
|
|
||||||
DataSource = dbPath,
|
|
||||||
Mode = SqliteOpenMode.ReadWriteCreate
|
|
||||||
}.ToString();
|
|
||||||
|
|
||||||
_connection = new SqliteConnection(connectionString);
|
|
||||||
_connection.Open();
|
|
||||||
|
|
||||||
InitializeSchema();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeSchema()
|
|
||||||
{
|
|
||||||
using var cmd = _connection.CreateCommand();
|
|
||||||
cmd.CommandText = @"
|
|
||||||
CREATE TABLE IF NOT EXISTS Parts (
|
|
||||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
FileName TEXT,
|
|
||||||
Area REAL,
|
|
||||||
Convexity REAL,
|
|
||||||
AspectRatio REAL,
|
|
||||||
BBFill REAL,
|
|
||||||
Circularity REAL,
|
|
||||||
VertexCount INTEGER,
|
|
||||||
Bitmask BLOB,
|
|
||||||
GeometryData TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS Runs (
|
|
||||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
PartId INTEGER,
|
|
||||||
SheetWidth REAL,
|
|
||||||
SheetHeight REAL,
|
|
||||||
Spacing REAL,
|
|
||||||
PartCount INTEGER,
|
|
||||||
Utilization REAL,
|
|
||||||
TimeMs INTEGER,
|
|
||||||
LayoutData TEXT,
|
|
||||||
FilePath TEXT,
|
|
||||||
FOREIGN KEY(PartId) REFERENCES Parts(Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_parts_filename ON Parts(FileName);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_runs_partid ON Runs(PartId);
|
|
||||||
";
|
|
||||||
cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
public long GetOrAddPart(string fileName, PartFeatures features, string geometryData)
|
|
||||||
{
|
|
||||||
// Check if part already exists
|
|
||||||
using (var checkCmd = _connection.CreateCommand())
|
|
||||||
{
|
|
||||||
checkCmd.CommandText = "SELECT Id FROM Parts WHERE FileName = @name";
|
|
||||||
checkCmd.Parameters.AddWithValue("@name", fileName);
|
|
||||||
var result = checkCmd.ExecuteScalar();
|
|
||||||
if (result != null) return (long)result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new part
|
|
||||||
using (var insertCmd = _connection.CreateCommand())
|
|
||||||
{
|
|
||||||
insertCmd.CommandText = @"
|
|
||||||
INSERT INTO Parts (FileName, Area, Convexity, AspectRatio, BBFill, Circularity, VertexCount, Bitmask, GeometryData)
|
|
||||||
VALUES (@name, @area, @conv, @asp, @fill, @circ, @vert, @mask, @geo);
|
|
||||||
SELECT last_insert_rowid();";
|
|
||||||
|
|
||||||
insertCmd.Parameters.AddWithValue("@name", fileName);
|
|
||||||
insertCmd.Parameters.AddWithValue("@area", features.Area);
|
|
||||||
insertCmd.Parameters.AddWithValue("@conv", features.Convexity);
|
|
||||||
insertCmd.Parameters.AddWithValue("@asp", features.AspectRatio);
|
|
||||||
insertCmd.Parameters.AddWithValue("@fill", features.BoundingBoxFill);
|
|
||||||
insertCmd.Parameters.AddWithValue("@circ", features.Circularity);
|
|
||||||
insertCmd.Parameters.AddWithValue("@vert", features.VertexCount);
|
|
||||||
insertCmd.Parameters.AddWithValue("@mask", features.Bitmask);
|
|
||||||
insertCmd.Parameters.AddWithValue("@geo", geometryData);
|
|
||||||
|
|
||||||
return (long)insertCmd.ExecuteScalar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasRun(string fileName, double sheetWidth, double sheetHeight, double spacing)
|
|
||||||
{
|
|
||||||
using var cmd = _connection.CreateCommand();
|
|
||||||
cmd.CommandText = @"SELECT COUNT(*) FROM Runs r JOIN Parts p ON r.PartId = p.Id
|
|
||||||
WHERE p.FileName = @name AND r.SheetWidth = @w AND r.SheetHeight = @h AND r.Spacing = @s";
|
|
||||||
cmd.Parameters.AddWithValue("@name", fileName);
|
|
||||||
cmd.Parameters.AddWithValue("@w", sheetWidth);
|
|
||||||
cmd.Parameters.AddWithValue("@h", sheetHeight);
|
|
||||||
cmd.Parameters.AddWithValue("@s", spacing);
|
|
||||||
return (long)cmd.ExecuteScalar() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int RunCount(string fileName)
|
|
||||||
{
|
|
||||||
using var cmd = _connection.CreateCommand();
|
|
||||||
cmd.CommandText = "SELECT COUNT(*) FROM Runs r JOIN Parts p ON r.PartId = p.Id WHERE p.FileName = @name";
|
|
||||||
cmd.Parameters.AddWithValue("@name", fileName);
|
|
||||||
return (int)(long)cmd.ExecuteScalar();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddRun(long partId, double w, double h, double s, BruteForceResult result, string filePath)
|
|
||||||
{
|
|
||||||
using var cmd = _connection.CreateCommand();
|
|
||||||
cmd.CommandText = @"
|
|
||||||
INSERT INTO Runs (PartId, SheetWidth, SheetHeight, Spacing, PartCount, Utilization, TimeMs, LayoutData, FilePath)
|
|
||||||
VALUES (@pid, @w, @h, @s, @cnt, @util, @time, @layout, @path)";
|
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("@pid", partId);
|
|
||||||
cmd.Parameters.AddWithValue("@w", w);
|
|
||||||
cmd.Parameters.AddWithValue("@h", h);
|
|
||||||
cmd.Parameters.AddWithValue("@s", s);
|
|
||||||
cmd.Parameters.AddWithValue("@cnt", result.PartCount);
|
|
||||||
cmd.Parameters.AddWithValue("@util", result.Utilization);
|
|
||||||
cmd.Parameters.AddWithValue("@time", result.TimeMs);
|
|
||||||
cmd.Parameters.AddWithValue("@layout", result.LayoutData ?? "");
|
|
||||||
cmd.Parameters.AddWithValue("@path", filePath ?? "");
|
|
||||||
|
|
||||||
cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SqliteTransaction BeginTransaction()
|
|
||||||
{
|
|
||||||
return _connection.BeginTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_connection?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace OpenNest.Training.Data
|
||||||
|
{
|
||||||
|
public class TrainingDbContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<TrainingPart> Parts { get; set; }
|
||||||
|
public DbSet<TrainingRun> Runs { get; set; }
|
||||||
|
|
||||||
|
private readonly string _dbPath;
|
||||||
|
|
||||||
|
public TrainingDbContext(string dbPath)
|
||||||
|
{
|
||||||
|
_dbPath = dbPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||||
|
{
|
||||||
|
options.UseSqlite($"Data Source={_dbPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<TrainingPart>(e =>
|
||||||
|
{
|
||||||
|
e.HasIndex(p => p.FileName).HasDatabaseName("idx_parts_filename");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<TrainingRun>(e =>
|
||||||
|
{
|
||||||
|
e.HasIndex(r => r.PartId).HasDatabaseName("idx_runs_partid");
|
||||||
|
e.HasOne(r => r.Part)
|
||||||
|
.WithMany(p => p.Runs)
|
||||||
|
.HasForeignKey(r => r.PartId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace OpenNest.Training.Data
|
||||||
|
{
|
||||||
|
[Table("Parts")]
|
||||||
|
public class TrainingPart
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(260)]
|
||||||
|
public string FileName { get; set; }
|
||||||
|
|
||||||
|
public double Area { get; set; }
|
||||||
|
public double Convexity { get; set; }
|
||||||
|
public double AspectRatio { get; set; }
|
||||||
|
public double BBFill { get; set; }
|
||||||
|
public double Circularity { get; set; }
|
||||||
|
public double PerimeterToAreaRatio { get; set; }
|
||||||
|
public int VertexCount { get; set; }
|
||||||
|
public byte[] Bitmask { get; set; }
|
||||||
|
public string GeometryData { get; set; }
|
||||||
|
|
||||||
|
public List<TrainingRun> Runs { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace OpenNest.Training.Data
|
||||||
|
{
|
||||||
|
[Table("Runs")]
|
||||||
|
public class TrainingRun
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public long PartId { get; set; }
|
||||||
|
public double SheetWidth { get; set; }
|
||||||
|
public double SheetHeight { get; set; }
|
||||||
|
public double Spacing { get; set; }
|
||||||
|
public int PartCount { get; set; }
|
||||||
|
public double Utilization { get; set; }
|
||||||
|
public long TimeMs { get; set; }
|
||||||
|
public string LayoutData { get; set; }
|
||||||
|
public string FilePath { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(PartId))]
|
||||||
|
public TrainingPart Part { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<RootNamespace>OpenNest.Training</RootNamespace>
|
||||||
|
<AssemblyName>OpenNest.Training</AssemblyName>
|
||||||
|
<DefineConstants>$(DefineConstants);DEBUG;TRACE</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
|
using Color = System.Drawing.Color;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
|
using OpenNest.Engine.ML;
|
||||||
|
using OpenNest.Gpu;
|
||||||
|
using OpenNest.Training;
|
||||||
|
|
||||||
|
// Parse arguments.
|
||||||
|
var dbPath = "OpenNestTraining";
|
||||||
|
var saveNestsDir = (string)null;
|
||||||
|
var templateFile = (string)null;
|
||||||
|
var spacing = 0.5;
|
||||||
|
var collectDir = (string)null;
|
||||||
|
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
switch (args[i])
|
||||||
|
{
|
||||||
|
case "--db" when i + 1 < args.Length:
|
||||||
|
dbPath = args[++i];
|
||||||
|
break;
|
||||||
|
case "--save-nests" when i + 1 < args.Length:
|
||||||
|
saveNestsDir = args[++i];
|
||||||
|
break;
|
||||||
|
case "--template" when i + 1 < args.Length:
|
||||||
|
templateFile = args[++i];
|
||||||
|
break;
|
||||||
|
case "--spacing" when i + 1 < args.Length:
|
||||||
|
spacing = double.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--help":
|
||||||
|
case "-h":
|
||||||
|
PrintUsage();
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
if (!args[i].StartsWith("--") && collectDir == null)
|
||||||
|
collectDir = args[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(collectDir) || !Directory.Exists(collectDir))
|
||||||
|
{
|
||||||
|
PrintUsage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize GPU if available.
|
||||||
|
if (GpuEvaluatorFactory.GpuAvailable)
|
||||||
|
{
|
||||||
|
BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
||||||
|
Console.WriteLine($"GPU: {GpuEvaluatorFactory.DeviceName}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("GPU: not available (using CPU)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return RunDataCollection(collectDir, dbPath, saveNestsDir, spacing, templateFile);
|
||||||
|
|
||||||
|
int RunDataCollection(string dir, string dbPath, string saveDir, double s, string template)
|
||||||
|
{
|
||||||
|
// Load template nest for plate defaults if provided.
|
||||||
|
Nest templateNest = null;
|
||||||
|
if (template != null)
|
||||||
|
{
|
||||||
|
if (!File.Exists(template))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: Template not found: {template}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
templateNest = new NestReader(template).Read();
|
||||||
|
Console.WriteLine($"Using template: {template}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var PartColors = new[]
|
||||||
|
{
|
||||||
|
Color.FromArgb(205, 92, 92),
|
||||||
|
Color.FromArgb(148, 103, 189),
|
||||||
|
Color.FromArgb(75, 180, 175),
|
||||||
|
Color.FromArgb(210, 190, 75),
|
||||||
|
Color.FromArgb(190, 85, 175),
|
||||||
|
Color.FromArgb(185, 115, 85),
|
||||||
|
Color.FromArgb(120, 100, 190),
|
||||||
|
Color.FromArgb(200, 100, 140),
|
||||||
|
Color.FromArgb(80, 175, 155),
|
||||||
|
Color.FromArgb(195, 160, 85),
|
||||||
|
Color.FromArgb(175, 95, 160),
|
||||||
|
Color.FromArgb(215, 130, 130),
|
||||||
|
};
|
||||||
|
|
||||||
|
var sheetSuite = new[]
|
||||||
|
{
|
||||||
|
new Size(96, 48), new Size(120, 48), new Size(144, 48),
|
||||||
|
new Size(96, 60), new Size(120, 60), new Size(144, 60),
|
||||||
|
new Size(96, 72), new Size(120, 72), new Size(144, 72),
|
||||||
|
new Size(48, 24), new Size(120, 10)
|
||||||
|
};
|
||||||
|
|
||||||
|
var dxfFiles = Directory.GetFiles(dir, "*.dxf", SearchOption.AllDirectories);
|
||||||
|
Console.WriteLine($"Found {dxfFiles.Length} DXF files");
|
||||||
|
var resolvedDb = dbPath.EndsWith(".db", StringComparison.OrdinalIgnoreCase) ? dbPath : dbPath + ".db";
|
||||||
|
Console.WriteLine($"Database: {Path.GetFullPath(resolvedDb)}");
|
||||||
|
Console.WriteLine($"Sheet sizes: {sheetSuite.Length} configurations");
|
||||||
|
Console.WriteLine($"Spacing: {s:F2}");
|
||||||
|
if (saveDir != null) Console.WriteLine($"Saving nests to: {saveDir}");
|
||||||
|
Console.WriteLine("---");
|
||||||
|
|
||||||
|
using var db = new TrainingDatabase(dbPath);
|
||||||
|
|
||||||
|
var backfilled = db.BackfillPerimeterToAreaRatio();
|
||||||
|
if (backfilled > 0)
|
||||||
|
Console.WriteLine($"Backfilled PerimeterToAreaRatio for {backfilled} existing parts");
|
||||||
|
|
||||||
|
var importer = new DxfImporter();
|
||||||
|
var colorIndex = 0;
|
||||||
|
var processed = 0;
|
||||||
|
var skippedGeometry = 0;
|
||||||
|
var skippedFeatures = 0;
|
||||||
|
var skippedExisting = 0;
|
||||||
|
var totalRuns = 0;
|
||||||
|
var totalSw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
foreach (var file in dxfFiles)
|
||||||
|
{
|
||||||
|
var fileNum = processed + skippedGeometry + skippedFeatures + skippedExisting + 1;
|
||||||
|
var partNo = Path.GetFileNameWithoutExtension(file);
|
||||||
|
Console.Write($"[{fileNum}/{dxfFiles.Length}] {partNo}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var existingRuns = db.RunCount(Path.GetFileName(file));
|
||||||
|
if (existingRuns >= sheetSuite.Length)
|
||||||
|
{
|
||||||
|
Console.WriteLine(" - SKIP (all sizes done)");
|
||||||
|
skippedExisting++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!importer.GetGeometry(file, out var entities))
|
||||||
|
{
|
||||||
|
Console.WriteLine(" - SKIP (no geometry)");
|
||||||
|
skippedGeometry++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var drawing = new Drawing(Path.GetFileName(file));
|
||||||
|
drawing.Program = OpenNest.Converters.ConvertGeometry.ToProgram(entities);
|
||||||
|
drawing.UpdateArea();
|
||||||
|
drawing.Color = PartColors[colorIndex % PartColors.Length];
|
||||||
|
colorIndex++;
|
||||||
|
|
||||||
|
var features = FeatureExtractor.Extract(drawing);
|
||||||
|
if (features == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine(" - SKIP (feature extraction failed)");
|
||||||
|
skippedFeatures++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($" (area={features.Area:F1}, verts={features.VertexCount})");
|
||||||
|
|
||||||
|
// Precompute best-fits once for all sheet sizes.
|
||||||
|
var sizes = sheetSuite.Select(sz => (sz.Width, sz.Length)).ToList();
|
||||||
|
var bfSw = Stopwatch.StartNew();
|
||||||
|
BestFitCache.ComputeForSizes(drawing, s, sizes);
|
||||||
|
bfSw.Stop();
|
||||||
|
Console.WriteLine($" Best-fits computed in {bfSw.ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
var partId = db.GetOrAddPart(Path.GetFileName(file), features, drawing.Program.ToString());
|
||||||
|
var partSw = Stopwatch.StartNew();
|
||||||
|
var runsThisPart = 0;
|
||||||
|
var bestUtil = 0.0;
|
||||||
|
var bestCount = 0;
|
||||||
|
|
||||||
|
foreach (var size in sheetSuite)
|
||||||
|
{
|
||||||
|
if (db.HasRun(Path.GetFileName(file), size.Width, size.Length, s))
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {size.Length}x{size.Width} - skip (exists)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plate runPlate;
|
||||||
|
if (templateNest != null)
|
||||||
|
{
|
||||||
|
runPlate = templateNest.PlateDefaults.CreateNew();
|
||||||
|
runPlate.Size = size;
|
||||||
|
runPlate.PartSpacing = s;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
runPlate = new Plate { Size = size, PartSpacing = s };
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizeSw = Stopwatch.StartNew();
|
||||||
|
var result = BruteForceRunner.Run(drawing, runPlate);
|
||||||
|
sizeSw.Stop();
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {size.Length}x{size.Width} - no fit");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Utilization > bestUtil)
|
||||||
|
{
|
||||||
|
bestUtil = result.Utilization;
|
||||||
|
bestCount = result.PartCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($" {size.Length}x{size.Width} - {result.PartCount}pcs, {result.Utilization:P1}, {sizeSw.ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
string savedFilePath = null;
|
||||||
|
if (saveDir != null)
|
||||||
|
{
|
||||||
|
// Deterministic bucket (00-FF) based on filename hash
|
||||||
|
uint hash = 0;
|
||||||
|
foreach (char c in partNo) hash = (hash * 31) + c;
|
||||||
|
var bucket = (hash % 256).ToString("X2");
|
||||||
|
|
||||||
|
var partDir = Path.Combine(saveDir, bucket, partNo);
|
||||||
|
Directory.CreateDirectory(partDir);
|
||||||
|
|
||||||
|
var nestName = $"{partNo}-{size.Length}x{size.Width}-{result.PartCount}pcs";
|
||||||
|
var fileName = nestName + ".zip";
|
||||||
|
savedFilePath = Path.Combine(partDir, fileName);
|
||||||
|
|
||||||
|
// Create nest from template or from scratch
|
||||||
|
Nest nestObj;
|
||||||
|
if (templateNest != null)
|
||||||
|
{
|
||||||
|
nestObj = new Nest(nestName)
|
||||||
|
{
|
||||||
|
Units = templateNest.Units,
|
||||||
|
DateCreated = DateTime.Now
|
||||||
|
};
|
||||||
|
nestObj.PlateDefaults.SetFromExisting(templateNest.PlateDefaults.CreateNew());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nestObj = new Nest(nestName) { Units = Units.Inches, DateCreated = DateTime.Now };
|
||||||
|
}
|
||||||
|
|
||||||
|
nestObj.Drawings.Add(drawing);
|
||||||
|
var plateObj = nestObj.CreatePlate();
|
||||||
|
plateObj.Size = size;
|
||||||
|
plateObj.PartSpacing = s;
|
||||||
|
plateObj.Parts.AddRange(result.PlacedParts);
|
||||||
|
|
||||||
|
var writer = new NestWriter(nestObj);
|
||||||
|
writer.Write(savedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.AddRun(partId, size.Width, size.Length, s, result, savedFilePath);
|
||||||
|
runsThisPart++;
|
||||||
|
totalRuns++;
|
||||||
|
}
|
||||||
|
|
||||||
|
BestFitCache.Invalidate(drawing);
|
||||||
|
partSw.Stop();
|
||||||
|
processed++;
|
||||||
|
Console.WriteLine($" Total: {runsThisPart} runs, best={bestCount}pcs @ {bestUtil:P1}, {partSw.ElapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.Error.WriteLine($" ERROR: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSw.Stop();
|
||||||
|
Console.WriteLine("---");
|
||||||
|
Console.WriteLine($"Processed: {processed} parts, {totalRuns} total runs");
|
||||||
|
Console.WriteLine($"Skipped: {skippedExisting} (existing) + {skippedGeometry} (no geometry) + {skippedFeatures} (no features)");
|
||||||
|
Console.WriteLine($"Time: {totalSw.Elapsed:h\\:mm\\:ss}");
|
||||||
|
Console.WriteLine($"Database: {Path.GetFullPath(resolvedDb)}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintUsage()
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Usage: OpenNest.Training <dxf-dir> [options]");
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine("Arguments:");
|
||||||
|
Console.Error.WriteLine(" dxf-dir Directory containing DXF files to process");
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine("Options:");
|
||||||
|
Console.Error.WriteLine(" --spacing <value> Part spacing (default: 0.5)");
|
||||||
|
Console.Error.WriteLine(" --db <path> SQLite database path (default: OpenNestTraining.db)");
|
||||||
|
Console.Error.WriteLine(" --save-nests <dir> Directory to save individual .zip nests for each winner");
|
||||||
|
Console.Error.WriteLine(" --template <path> Nest template (.nstdot) for plate defaults");
|
||||||
|
Console.Error.WriteLine(" -h, --help Show this help");
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OpenNest.Engine.ML;
|
||||||
|
using OpenNest.IO;
|
||||||
|
using OpenNest.Training.Data;
|
||||||
|
|
||||||
|
namespace OpenNest.Training
|
||||||
|
{
|
||||||
|
public class TrainingDatabase : IDisposable
|
||||||
|
{
|
||||||
|
private readonly TrainingDbContext _db;
|
||||||
|
|
||||||
|
public TrainingDatabase(string dbPath)
|
||||||
|
{
|
||||||
|
if (!dbPath.EndsWith(".db", StringComparison.OrdinalIgnoreCase))
|
||||||
|
dbPath += ".db";
|
||||||
|
|
||||||
|
_db = new TrainingDbContext(dbPath);
|
||||||
|
_db.Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long GetOrAddPart(string fileName, PartFeatures features, string geometryData)
|
||||||
|
{
|
||||||
|
var existing = _db.Parts.FirstOrDefault(p => p.FileName == fileName);
|
||||||
|
if (existing != null) return existing.Id;
|
||||||
|
|
||||||
|
var part = new TrainingPart
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
Area = features.Area,
|
||||||
|
Convexity = features.Convexity,
|
||||||
|
AspectRatio = features.AspectRatio,
|
||||||
|
BBFill = features.BoundingBoxFill,
|
||||||
|
Circularity = features.Circularity,
|
||||||
|
PerimeterToAreaRatio = features.PerimeterToAreaRatio,
|
||||||
|
VertexCount = features.VertexCount,
|
||||||
|
Bitmask = features.Bitmask,
|
||||||
|
GeometryData = geometryData
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Parts.Add(part);
|
||||||
|
_db.SaveChanges();
|
||||||
|
return part.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasRun(string fileName, double sheetWidth, double sheetHeight, double spacing)
|
||||||
|
{
|
||||||
|
return _db.Runs.Any(r =>
|
||||||
|
r.Part.FileName == fileName &&
|
||||||
|
r.SheetWidth == sheetWidth &&
|
||||||
|
r.SheetHeight == sheetHeight &&
|
||||||
|
r.Spacing == spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int RunCount(string fileName)
|
||||||
|
{
|
||||||
|
return _db.Runs.Count(r => r.Part.FileName == fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRun(long partId, double w, double h, double s, BruteForceResult result, string filePath)
|
||||||
|
{
|
||||||
|
var run = new TrainingRun
|
||||||
|
{
|
||||||
|
PartId = partId,
|
||||||
|
SheetWidth = w,
|
||||||
|
SheetHeight = h,
|
||||||
|
Spacing = s,
|
||||||
|
PartCount = result.PartCount,
|
||||||
|
Utilization = result.Utilization,
|
||||||
|
TimeMs = result.TimeMs,
|
||||||
|
LayoutData = result.LayoutData ?? "",
|
||||||
|
FilePath = filePath ?? ""
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Runs.Add(run);
|
||||||
|
_db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int BackfillPerimeterToAreaRatio()
|
||||||
|
{
|
||||||
|
var partsToFix = _db.Parts
|
||||||
|
.Where(p => p.PerimeterToAreaRatio == 0)
|
||||||
|
.Select(p => new { p.Id, p.GeometryData })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (partsToFix.Count == 0) return 0;
|
||||||
|
|
||||||
|
var updated = 0;
|
||||||
|
foreach (var item in partsToFix)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var stream = new MemoryStream(Encoding.UTF8.GetBytes(item.GeometryData));
|
||||||
|
var programReader = new ProgramReader(stream);
|
||||||
|
var program = programReader.Read();
|
||||||
|
|
||||||
|
var drawing = new Drawing("backfill") { Program = program };
|
||||||
|
drawing.UpdateArea();
|
||||||
|
|
||||||
|
var features = FeatureExtractor.Extract(drawing);
|
||||||
|
if (features == null) continue;
|
||||||
|
|
||||||
|
var part = _db.Parts.Find(item.Id);
|
||||||
|
part.PerimeterToAreaRatio = features.PerimeterToAreaRatio;
|
||||||
|
_db.SaveChanges();
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Skip parts that fail to reconstruct.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveChanges()
|
||||||
|
{
|
||||||
|
_db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_db?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Mcp", "OpenNest.Mc
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Console", "OpenNest.Console\OpenNest.Console.csproj", "{58E00A25-86B5-42C7-87B5-DE4AD22381EA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Console", "OpenNest.Console\OpenNest.Console.csproj", "{58E00A25-86B5-42C7-87B5-DE4AD22381EA}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Training", "OpenNest.Training\OpenNest.Training.csproj", "{249BF728-25DD-4863-8266-207ACD26E964}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -111,6 +113,18 @@ Global
|
|||||||
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x64.Build.0 = Release|Any CPU
|
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x86.ActiveCfg = Release|Any CPU
|
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x86.Build.0 = Release|Any CPU
|
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Reference in New Issue
Block a user