From 3133228fc973086730fad8d32cc066a9097637b7 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 13 Mar 2026 23:40:14 -0400 Subject: [PATCH] feat(console): use GPU for best-fit when available Wire up GpuEvaluatorFactory in the Console app the same way the GUI app does, so BestFitCache uses GPU-accelerated slide computation when a CUDA/OpenCL device is detected. Co-Authored-By: Claude Opus 4.6 --- OpenNest.Console/OpenNest.Console.csproj | 4 + OpenNest.Console/Program.cs | 212 +++++++++++++++++++++-- 2 files changed, 205 insertions(+), 11 deletions(-) diff --git a/OpenNest.Console/OpenNest.Console.csproj b/OpenNest.Console/OpenNest.Console.csproj index dfb174b..f1a0806 100644 --- a/OpenNest.Console/OpenNest.Console.csproj +++ b/OpenNest.Console/OpenNest.Console.csproj @@ -10,5 +10,9 @@ + + + + diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs index f96e900..58bc9b1 100644 --- a/OpenNest.Console/Program.cs +++ b/OpenNest.Console/Program.cs @@ -6,6 +6,11 @@ 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; @@ -21,11 +26,27 @@ 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; @@ -75,6 +96,22 @@ 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(); @@ -225,24 +262,177 @@ 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 dxfFiles = Directory.GetFiles(dir, "*.dxf", SearchOption.AllDirectories); + Console.WriteLine($"Found {dxfFiles.Length} DXF files. Initializing SQLite database at: {dbPath}"); + + using var db = new TrainingDatabase(dbPath); + + 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 importer = new DxfImporter(); + var colorIndex = 0; + var processed = 0; + + foreach (var file in dxfFiles) + { + try + { + if (!importer.GetGeometry(file, out var entities)) continue; + + var partNo = Path.GetFileNameWithoutExtension(file); + 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) continue; + + using var txn = db.BeginTransaction(); + + var partId = db.GetOrAddPart(Path.GetFileName(file), features, drawing.Program.ToString()); + + foreach (var size in sheetSuite) + { + Plate runPlate; + if (templateNest != null) + { + runPlate = templateNest.PlateDefaults.CreateNew(); + runPlate.Size = size; + runPlate.PartSpacing = s; + } + else + { + runPlate = new Plate { Size = size, PartSpacing = s }; + } + + var result = BruteForceRunner.Run(drawing, runPlate); + if (result == null) continue; + + 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 fileName = $"{partNo}-{size.Length}x{size.Width}-{result.PartCount}pcs.zip"; + savedFilePath = Path.Combine(partDir, fileName); + + // Create nest from template or from scratch + Nest nestObj; + if (templateNest != null) + { + nestObj = new Nest(partNo) + { + Units = templateNest.Units, + DateCreated = DateTime.Now + }; + nestObj.PlateDefaults.SetFromExisting(templateNest.PlateDefaults.CreateNew()); + } + else + { + nestObj = new Nest(partNo) { 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); + } + + txn.Commit(); + processed++; + if (processed % 10 == 0) Console.WriteLine($"Processed {processed}/{dxfFiles.Length} parts across all sheet sizes..."); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error processing {file}: {ex.Message}"); + } + } + + Console.WriteLine($"Done! Brute-force data for {processed} parts saved to {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.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(" --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"); + 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"); }