diff --git a/OpenNest.Console/OpenNest.Console.csproj b/OpenNest.Console/OpenNest.Console.csproj
index f1a0806..dfb174b 100644
--- a/OpenNest.Console/OpenNest.Console.csproj
+++ b/OpenNest.Console/OpenNest.Console.csproj
@@ -10,9 +10,5 @@
-
-
-
-
diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs
index 8ec74e6..24451ee 100644
--- a/OpenNest.Console/Program.cs
+++ b/OpenNest.Console/Program.cs
@@ -6,11 +6,6 @@ using System.Linq;
using OpenNest;
using OpenNest.Geometry;
using OpenNest.IO;
-using Color = System.Drawing.Color;
-using OpenNest.Console;
-using OpenNest.Engine.BestFit;
-using OpenNest.Engine.ML;
-using OpenNest.Gpu;
// Parse arguments.
var nestFile = (string)null;
@@ -26,27 +21,12 @@ var noSave = false;
var noLog = false;
var keepParts = false;
var autoNest = false;
-var collectDataDir = (string)null;
-var dbPath = "nesting_training.db";
-var saveNestsDir = (string)null;
var templateFile = (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 "--collect" when i + 1 < args.Length:
- collectDataDir = args[++i];
- break;
case "--drawing" when i + 1 < args.Length:
drawingName = args[++i];
break;
@@ -82,6 +62,9 @@ for (var i = 0; i < args.Length; i++)
case "--keep-parts":
keepParts = true;
break;
+ case "--template" when i + 1 < args.Length:
+ templateFile = args[++i];
+ break;
case "--autonest":
autoNest = true;
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))
{
PrintUsage();
@@ -149,6 +116,24 @@ if (plateIndex >= nest.Plates.Count)
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.
if (spacing.HasValue)
plate.PartSpacing = spacing.Value;
@@ -207,9 +192,9 @@ if (autoNest)
Console.WriteLine($"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts");
- var parts = NestEngine.AutoNest(nestItems, plate);
- plate.Parts.AddRange(parts);
- success = parts.Count > 0;
+ var nestParts = NestEngine.AutoNest(nestItems, plate);
+ plate.Parts.AddRange(nestParts);
+ success = nestParts.Count > 0;
}
else
{
@@ -262,253 +247,25 @@ if (!noSave)
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()
{
Console.Error.WriteLine("Usage: OpenNest.Console [options]");
- Console.Error.WriteLine(" OpenNest.Console --collect [options]");
Console.Error.WriteLine();
Console.Error.WriteLine("Arguments:");
Console.Error.WriteLine(" nest-file Path to a .zip nest file");
Console.Error.WriteLine();
Console.Error.WriteLine("Options:");
Console.Error.WriteLine(" --drawing Drawing name to fill with (default: first drawing)");
- Console.WriteLine(" --plate Plate index to fill (default: 0)");
- Console.WriteLine(" --quantity Max parts to place (default: 0 = unlimited)");
- Console.WriteLine(" --spacing Override part spacing");
- Console.WriteLine(" --size Override plate size (e.g. 120x60)");
- Console.WriteLine(" --output Output nest file path (default: -result.zip)");
- Console.WriteLine(" --autonest Use NFP-based mixed-part autonesting instead of linear fill");
- Console.WriteLine(" --keep-parts Don't clear existing parts before filling");
- Console.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
- Console.WriteLine(" --no-save Skip saving output file");
- Console.WriteLine(" --no-log Skip writing debug log file");
- Console.WriteLine(" --collect Brute-force process all DXFs in directory to SQLite");
- Console.WriteLine(" --db Path to the SQLite training database (default: nesting_training.db)");
- Console.WriteLine(" --save-nests Directory to save individual .zip nests for each winner");
- Console.WriteLine(" --template Nest template (.nstdot) for plate defaults");
- Console.WriteLine(" -h, --help Show this help");
+ Console.Error.WriteLine(" --plate Plate index to fill (default: 0)");
+ Console.Error.WriteLine(" --quantity Max parts to place (default: 0 = unlimited)");
+ Console.Error.WriteLine(" --spacing Override part spacing");
+ Console.Error.WriteLine(" --size Override plate size (e.g. 120x60)");
+ Console.Error.WriteLine(" --output Output nest file path (default: -result.zip)");
+ Console.Error.WriteLine(" --template Nest template for plate defaults (thickness, quadrant, material, spacing)");
+ Console.Error.WriteLine(" --autonest Use NFP-based mixed-part autonesting instead of linear fill");
+ Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling");
+ Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
+ Console.Error.WriteLine(" --no-save Skip saving output file");
+ Console.Error.WriteLine(" --no-log Skip writing debug log file");
+ Console.Error.WriteLine(" -h, --help Show this help");
}
diff --git a/OpenNest.Console/TrainingDatabase.cs b/OpenNest.Console/TrainingDatabase.cs
deleted file mode 100644
index 2a40f97..0000000
--- a/OpenNest.Console/TrainingDatabase.cs
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/OpenNest.Training/Data/TrainingDbContext.cs b/OpenNest.Training/Data/TrainingDbContext.cs
new file mode 100644
index 0000000..12bc817
--- /dev/null
+++ b/OpenNest.Training/Data/TrainingDbContext.cs
@@ -0,0 +1,38 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace OpenNest.Training.Data
+{
+ public class TrainingDbContext : DbContext
+ {
+ public DbSet Parts { get; set; }
+ public DbSet 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(e =>
+ {
+ e.HasIndex(p => p.FileName).HasDatabaseName("idx_parts_filename");
+ });
+
+ modelBuilder.Entity(e =>
+ {
+ e.HasIndex(r => r.PartId).HasDatabaseName("idx_runs_partid");
+ e.HasOne(r => r.Part)
+ .WithMany(p => p.Runs)
+ .HasForeignKey(r => r.PartId);
+ });
+ }
+ }
+}
diff --git a/OpenNest.Training/Data/TrainingPart.cs b/OpenNest.Training/Data/TrainingPart.cs
new file mode 100644
index 0000000..178180f
--- /dev/null
+++ b/OpenNest.Training/Data/TrainingPart.cs
@@ -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 Runs { get; set; } = new();
+ }
+}
diff --git a/OpenNest.Training/Data/TrainingRun.cs b/OpenNest.Training/Data/TrainingRun.cs
new file mode 100644
index 0000000..a8ae46a
--- /dev/null
+++ b/OpenNest.Training/Data/TrainingRun.cs
@@ -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; }
+ }
+}
diff --git a/OpenNest.Training/OpenNest.Training.csproj b/OpenNest.Training/OpenNest.Training.csproj
new file mode 100644
index 0000000..0a7387d
--- /dev/null
+++ b/OpenNest.Training/OpenNest.Training.csproj
@@ -0,0 +1,19 @@
+
+
+ Exe
+ net8.0-windows
+ OpenNest.Training
+ OpenNest.Training
+ $(DefineConstants);DEBUG;TRACE
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OpenNest.Training/Program.cs b/OpenNest.Training/Program.cs
new file mode 100644
index 0000000..6859c80
--- /dev/null
+++ b/OpenNest.Training/Program.cs
@@ -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 [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 Part spacing (default: 0.5)");
+ Console.Error.WriteLine(" --db SQLite database path (default: OpenNestTraining.db)");
+ Console.Error.WriteLine(" --save-nests Directory to save individual .zip nests for each winner");
+ Console.Error.WriteLine(" --template Nest template (.nstdot) for plate defaults");
+ Console.Error.WriteLine(" -h, --help Show this help");
+}
diff --git a/OpenNest.Training/TrainingDatabase.cs b/OpenNest.Training/TrainingDatabase.cs
new file mode 100644
index 0000000..2579299
--- /dev/null
+++ b/OpenNest.Training/TrainingDatabase.cs
@@ -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();
+ }
+ }
+}
diff --git a/OpenNest.sln b/OpenNest.sln
index 8e76f0c..69cf786 100644
--- a/OpenNest.sln
+++ b/OpenNest.sln
@@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Mcp", "OpenNest.Mc
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Console", "OpenNest.Console\OpenNest.Console.csproj", "{58E00A25-86B5-42C7-87B5-DE4AD22381EA}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Training", "OpenNest.Training\OpenNest.Training.csproj", "{249BF728-25DD-4863-8266-207ACD26E964}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE